From 6565152317958c2b26f213118296195bc885e61a Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:27:38 +0100 Subject: [PATCH 01/19] Upgrade isolated Presentation 2 canvases Fixes #53. Cover the context-free TPEN canvas through conversion and normalization. --- .../isolated-canvas.test.ts | 27 + .../presentation-2/tpen-isolated-canvas.json | 1526 +++++++++++++++++ src/presentation-2/upgrader.ts | 3 +- 3 files changed, 1555 insertions(+), 1 deletion(-) create mode 100644 __tests__/presentation-2-parser/isolated-canvas.test.ts create mode 100644 fixtures/presentation-2/tpen-isolated-canvas.json diff --git a/__tests__/presentation-2-parser/isolated-canvas.test.ts b/__tests__/presentation-2-parser/isolated-canvas.test.ts new file mode 100644 index 0000000..b9e7420 --- /dev/null +++ b/__tests__/presentation-2-parser/isolated-canvas.test.ts @@ -0,0 +1,27 @@ +import canvas from "../../fixtures/presentation-2/tpen-isolated-canvas.json"; +import { normalize } from "../../src"; +import { upgrade } from "../../src/upgrader"; +import { convertPresentation2, presentation2to3 } from "../../src/presentation-2"; + +describe("isolated Presentation 2 canvases (#53)", () => { + test("upgrades the context-free TPEN canvas through the public entrypoints", () => { + // Source: https://t-pen.org/TPEN/canvas/13250383 + for (const convert of [convertPresentation2, upgrade]) { + const result = convert(structuredClone(canvas)) as any; + expect(result).toMatchObject({ + id: canvas["@id"], + type: "Canvas", + label: { none: [canvas.label] }, + width: 666, + height: 1000, + }); + expect(result.annotations[0].type).toBe("AnnotationPage"); + expect(normalize(result).resource).toEqual({ id: canvas["@id"], type: "Canvas" }); + } + }); + + test("preserves already upgraded resources", () => { + const result = presentation2to3.traverseCanvas(structuredClone(canvas) as any); + expect(convertPresentation2(result)).toBe(result); + }); +}); diff --git a/fixtures/presentation-2/tpen-isolated-canvas.json b/fixtures/presentation-2/tpen-isolated-canvas.json new file mode 100644 index 0000000..e2aadd6 --- /dev/null +++ b/fixtures/presentation-2/tpen-isolated-canvas.json @@ -0,0 +1,1526 @@ +{ + "@id" : "https://t-pen.org/TPEN/canvas/13250383", + "@type" : "sc:Canvas", + "label" : "bav_pal_lat_51_0041.jpg", + "width" : 666, + "height" : 1000, + "otherContent" : [ { + "@id" : "https://t-pen.org/TPEN/project/7157/annotations/13250383", + "@type" : "sc:AnnotationList", + "label" : "https://t-pen.org/TPEN/canvas/13250383 List", + "proj" : 7157, + "on" : "https://t-pen.org/TPEN/canvas/13250383", + "@context" : "http://iiif.io/api/presentation/2/context.json", + "resources" : [ ] + }, { + "@id" : "https://t-pen.org/TPEN/project/7158/annotations/13250383", + "@type" : "sc:AnnotationList", + "label" : "https://t-pen.org/TPEN/canvas/13250383 List", + "proj" : 7158, + "on" : "https://t-pen.org/TPEN/canvas/13250383", + "@context" : "http://iiif.io/api/presentation/2/context.json", + "resources" : [ { + "@id" : "https://t-pen.org/TPEN/line/104710760", + "_tpen_line_id" : "line/104710760", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "¶Hec singule beatitudines ut impleantur postulant suffragium, septe donorum, que " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,81,346,24", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:49:55.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710968", + "_tpen_line_id" : "line/104710968", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "dona ut habeantur in oratione dominica, vii precibus oramus. Ecce qualiter sapientia dei " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,105,346,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:50:07.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710969", + "_tpen_line_id" : "line/104710969", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "cuncta prouidit uirtus beatitudinis precipitur ex dono spiritus sancti, donum prece ora" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,120,346,22", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:50:24.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710761", + "_tpen_line_id" : "line/104710761", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "tionis quam composuit impetratur." + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,142,181,12", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:50:26.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710807", + "_tpen_line_id" : "line/104710807", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Septem quippe sunt dona, sed unus spiritus; " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,154,181,12", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:52:15.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710808", + "_tpen_line_id" : "line/104710808", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "vii orationes, sed una petitio. " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,166,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:52:24.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710809", + "_tpen_line_id" : "line/104710809", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Unde unam petii a domino hanc requiram. Septem " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,180,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2489, + "modified" : "2021-05-17 09:36:07.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710810", + "_tpen_line_id" : "line/104710810", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "uirtutum beatitudines, una ca" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,195,181,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:52:55.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710811", + "_tpen_line_id" : "line/104710811", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "ritas. ¶ Iusticie lumen est misericordia, misericordie " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,209,181,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:54:10.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710812", + "_tpen_line_id" : "line/104710812", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "uirtus iusticia. Hec misericordia eget spiritu " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,225,181,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:54:27.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710813", + "_tpen_line_id" : "line/104710813", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "consilii, sine quo nemo circumspecte mi" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,241,181,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:54:37.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710814", + "_tpen_line_id" : "line/104710814", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "seretur. Initium est sui misereri, finis " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,257,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:54:45.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710815", + "_tpen_line_id" : "line/104710815", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "est pro alio mori. Ordo enim miseren" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,272,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:55:06.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710816", + "_tpen_line_id" : "line/104710816", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "di est, ut unusquisque incipiat a se finem " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,287,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:55:13.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710817", + "_tpen_line_id" : "line/104710817", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Christo insinuat, pro peccatoribus mortuus ex " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,302,181,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:55:25.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710818", + "_tpen_line_id" : "line/104710818", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "misericordia. ¶ Misericordia de precedentibus nascitur quia " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,315,181,18", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:56:17.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710819", + "_tpen_line_id" : "line/104710819", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "si precesserit uera humilitas et animus " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,333,181,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:56:28.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710820", + "_tpen_line_id" : "line/104710820", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "mansuescat, et suos et aliorum casus " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,346,181,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:56:38.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710821", + "_tpen_line_id" : "line/104710821", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "fleat, et iusticiam esuriat; post nasci" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,362,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:56:51.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710822", + "_tpen_line_id" : "line/104710822", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "tur uera misericordia, tunc enim alienas miserias " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,377,181,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:57:02.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710823", + "_tpen_line_id" : "line/104710823", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "faciet suas, et pro uiribus uiuabit, et si " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,393,181,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:57:08.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710824", + "_tpen_line_id" : "line/104710824", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "facultas uiuandi deest, compassio " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,410,181,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:57:19.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710825", + "_tpen_line_id" : "line/104710825", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "non deerit, de hac dicitur, \"Estote miseri" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,426,181,19", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-06-07 13:20:06.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710826", + "_tpen_line_id" : "line/104710826", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "cordes, sicut et pater uerum misericors est,\" id est, ad hoc misere" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,445,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-06-07 13:20:17.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710827", + "_tpen_line_id" : "line/104710827", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "amini, ad quod et deus, ut bonitas redun" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,460,181,19", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-18 12:58:16.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710828", + "_tpen_line_id" : "line/104710828", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "det in omnibus. ¶Pax est unigenitus de quo " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,479,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:30:27.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710829", + "_tpen_line_id" : "line/104710829", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "dicitur ipse pax nostra. Qui pacem diligit " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,494,181,18", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:30:35.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710830", + "_tpen_line_id" : "line/104710830", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "filius est pacis. ¶ sunt qui omnes motus " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,512,181,12", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:32:43.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710831", + "_tpen_line_id" : "line/104710831", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "animi componunt, et rationi subiciunt quia his " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,524,181,18", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:32:53.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710832", + "_tpen_line_id" : "line/104710832", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "nihil repugnat, similes patri suo " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,541,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:33:02.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710833", + "_tpen_line_id" : "line/104710833", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "sunt , qui est summa quies. De hac pace eiectus " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,556,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:33:24.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710834", + "_tpen_line_id" : "line/104710834", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "est qui peruersis dominatur; hac pace intus fir" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,571,181,18", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:33:40.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710835", + "_tpen_line_id" : "line/104710835", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "ma quecumque exterius concitauerit, qui eiectus est " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,589,181,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:34:04.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710836", + "_tpen_line_id" : "line/104710836", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "foras, auget gloriam et cum deficit inno" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,602,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:34:22.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710837", + "_tpen_line_id" : "line/104710837", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "tescere facit quanta firmitas intus sit. " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,617,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:34:31.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710838", + "_tpen_line_id" : "line/104710838", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Unde “Beati qui persecutionem patiuntur” propter iusticiam quoniam " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,632,181,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-06-07 13:21:18.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710839", + "_tpen_line_id" : "line/104710839", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "ipsorum est regnum celorum .” Pax a nobis incipit " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,646,181,9", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:35:50.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710840", + "_tpen_line_id" : "line/104710840", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "quia dum lex carnis repugnat legi " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,655,181,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:35:59.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710841", + "_tpen_line_id" : "line/104710841", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "mentis, non modo alterius sed nobis possumus " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,669,181,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:36:20.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710842", + "_tpen_line_id" : "line/104710842", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "esse pacifici. Sed postquam spiritus impetrat ut " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,682,181,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:36:38.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710843", + "_tpen_line_id" : "line/104710843", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "totus homo spiritui seruiat, tunc pax ad alios " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,698,181,18", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:36:48.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710806", + "_tpen_line_id" : "line/104710806", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "deriuatur, ut cum omnibus pacem habeamus. Vera quidem pax est ubi nichil repugnat, pax dei omnem sensum superat, que et si non potest capi ut est, tamen pro capacitate " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,716,562,12", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:37:32.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710844", + "_tpen_line_id" : "line/104710844", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "nostra capi potest non quanta est, sed quantum ad reconciliationem adoptionis sufficit infinitas eterne pacis. Et si non potest intelligi, et quomodo filiis ire pax conueniat, " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,728,562,18", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:41:03.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710845", + "_tpen_line_id" : "line/104710845", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "et quomodo deus pax seuere iudicat, et quanta et qualis sit pax totius trinitatis, ubi una uirtus, una operatio, tamen quod est et quod tanta et talis est, quanta predicatur, fide capi" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,746,562,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:41:36.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710846", + "_tpen_line_id" : "line/104710846", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "mus ad gradum pacis qui purgatus est intendit et dirigitur." + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=53,759,562,22", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:41:40.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710759", + "_tpen_line_id" : "line/104710759", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Congrua ascensio prius conte(m)ptus seculi " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,81,222,21", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:44:25.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710762", + "_tpen_line_id" : "line/104710762", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "cui dantur eterna, inde mores temperat mansue" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,102,222,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:44:37.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710763", + "_tpen_line_id" : "line/104710763", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "tudo et lenitas, et non est mitis qui prius pauper non fuerit. " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,118,222,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:44:52.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710764", + "_tpen_line_id" : "line/104710764", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Impossibilem est enim secundum gaudium ascendere nisi primum " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,135,222,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:45:39.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710765", + "_tpen_line_id" : "line/104710765", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "apprehenderis, quia inter diuitias, curas, odia ani" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,148,222,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:45:57.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710766", + "_tpen_line_id" : "line/104710766", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "mus non est mitis. Sed huius incendii materia uix ad " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,161,222,12", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:46:11.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710767", + "_tpen_line_id" : "line/104710767", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "purum detrahitur et ideo hec lenitas sepe corrumpitur. " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,173,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-04-30 14:26:09.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710768", + "_tpen_line_id" : "line/104710768", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Unde subdit, ‘Beati qui lugent pro suis uel aliorum pec" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,188,222,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:50:22.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710769", + "_tpen_line_id" : "line/104710769", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "catis, quod est ab irriguo inferius. Sed quod fit " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,204,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:50:40.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710770", + "_tpen_line_id" : "line/104710770", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "ab irriguo superius est ex desiderio celestis patrie. " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,219,222,18", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:50:47.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710771", + "_tpen_line_id" : "line/104710771", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Ille lauat presentes sordes, hic accendit artius " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,237,222,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:51:04.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710772", + "_tpen_line_id" : "line/104710772", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "aeternorum amatores. Qui ante peccata deposu" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,250,222,10", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:51:19.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710773", + "_tpen_line_id" : "line/104710773", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "it mores mansuetudine correxit, flens etiam " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,260,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:51:28.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710774", + "_tpen_line_id" : "line/104710774", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "potest esurire et sitire iusticiam, quod prius " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,275,222,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:52:16.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710777", + "_tpen_line_id" : "line/104710777", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "non poterat. Hec uirtus sine spiritu fortitudinis non " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,288,222,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:52:28.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710778", + "_tpen_line_id" : "line/104710778", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "impletur, quia per eum esuriendo ad satietatem " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,302,222,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:52:35.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710779", + "_tpen_line_id" : "line/104710779", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "quandoque ueniemus." + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,318,222,18", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:52:37.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710780", + "_tpen_line_id" : "line/104710780", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "¶Octaua sententia que ad caput redit perfectumque " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,336,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:53:43.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710781", + "_tpen_line_id" : "line/104710781", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "hominem declarat significat et circumcisio" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,351,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:53:56.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710782", + "_tpen_line_id" : "line/104710782", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "nem octaua die et resurrectionem domini post sabbatum, " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,365,222,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:54:09.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710783", + "_tpen_line_id" : "line/104710783", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "qui est octauus et primus dies et octauas quas in " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,382,222,12", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:54:30.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710784", + "_tpen_line_id" : "line/104710784", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "nouo testamento celebramus et numerus pente" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,393,222,19", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:54:49.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710785", + "_tpen_line_id" : "line/104710785", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "costes. Nam septem numero septies multipli" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,413,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:55:01.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710786", + "_tpen_line_id" : "line/104710786", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "cato quo fuerit , xlix. quasi octauus additur ut " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,428,222,21", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:55:14.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710787", + "_tpen_line_id" : "line/104710787", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "l. compleantur, et tanquam redeatur ad caput quo die " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,448,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:55:27.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710788", + "_tpen_line_id" : "line/104710788", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "missus est spiritus quo in regno celorum ducimur, et heredi" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,463,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:55:49.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710789", + "_tpen_line_id" : "line/104710789", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "tatem accipimus, consolamur, et pascimur, et misericordiam " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,478,222,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:57:00.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710790", + "_tpen_line_id" : "line/104710790", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "consequimur et mundamur et pacificamur et ita perse" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,494,222,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:57:09.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710791", + "_tpen_line_id" : "line/104710791", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "cutiones exterius illatas, et molestias pro ueritate et " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,507,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:57:19.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710792", + "_tpen_line_id" : "line/104710792", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "iusti[ci]a sustinemus. " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,522,222,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:57:21.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710793", + "_tpen_line_id" : "line/104710793", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "¶Mundicia cordis sexto loco ponitur, ut homo sexta " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,538,222,10", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 13:59:18.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710794", + "_tpen_line_id" : "line/104710794", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "die conditus per hanc restituatur, et depulsis uiciis " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,549,222,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:00:14.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710795", + "_tpen_line_id" : "line/104710795", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "preceptum domini illuminet oculos, ut possit uideri. " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,565,222,12", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:00:42.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710796", + "_tpen_line_id" : "line/104710796", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Premissis ergo quinque gradibus purgandus est mentis oculos, " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,577,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:00:55.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710797", + "_tpen_line_id" : "line/104710797", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "ne mandatorum offensus animus fiat omnium reus. " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,592,222,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:02:28.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710798", + "_tpen_line_id" : "line/104710798", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Ideo homo se a mundo exuat, eterna diligat. Am" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,605,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:02:57.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710799", + "_tpen_line_id" : "line/104710799", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "plectatur deum quo appossit uidere eum, quem tantum " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,620,222,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:03:16.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710800", + "_tpen_line_id" : "line/104710800", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "uidet quantum seculo moritur. In futuro sicut est oculo " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,633,222,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:03:42.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710801", + "_tpen_line_id" : "line/104710801", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "mentis uidebitur. Ideo spiritus intelligentie huic " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,649,222,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:03:51.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710802", + "_tpen_line_id" : "line/104710802", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "gradui aptatur, quia intellectus mentis est oculos, et ex" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,666,222,15", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:04:07.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710803", + "_tpen_line_id" : "line/104710803", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "pletio mandatorum dei aperit intellectum." + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,680,222,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:04:09.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710804", + "_tpen_line_id" : "line/104710804", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "beatitudo martyrio terminatur, que probatum reddit " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,694,222,12", + "_tpen_note" : "This line and the one right after it are a continuation of an interlinear gloss.", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:06:18.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104710805", + "_tpen_line_id" : "line/104710805", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "et suppletur precedentibus bonis" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=404,706,222,4", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-24 14:06:20.0" + } ] + }, { + "@id" : "https://t-pen.org/TPEN/project/7162/annotations/13250383", + "@type" : "sc:AnnotationList", + "label" : "https://t-pen.org/TPEN/canvas/13250383 List", + "proj" : 7162, + "on" : "https://t-pen.org/TPEN/canvas/13250383", + "@context" : "http://iiif.io/api/presentation/2/context.json", + "resources" : [ { + "@id" : "https://t-pen.org/TPEN/line/104708459", + "_tpen_line_id" : "line/104708459", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "¶Restat post predicta rimarum colluuionem luctu pur" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,130,240,22", + "_tpen_note" : "This gloss continues onto the next line", + "_tpen_creator" : 2490, + "modified" : "2021-03-05 15:43:32.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708460", + "_tpen_line_id" : "line/104708460", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "gare. Luctus pro peccatis uel desiderio celesti consolationem meretur " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,152,240,22", + "_tpen_note" : "This gloss continues onto the next line", + "_tpen_creator" : 2490, + "modified" : "2021-03-05 15:44:04.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708461", + "_tpen_line_id" : "line/104708461", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "et non alius. ¶ propter consolationem beati. ¶Spiritu paracleto de eterna" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,175,240,13", + "_tpen_note" : "\"Spiritu\" starts a gloss which continues onto the next line", + "_tpen_creator" : 2490, + "modified" : "2021-03-05 15:52:25.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708462", + "_tpen_line_id" : "line/104708462", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "leticia. ¶Amatores ueri boni, quibus non satis est quod iusti sunt, " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,188,240,12", + "_tpen_note" : "\"Amatores\" starts a gloss which continues onto the next line.", + "_tpen_creator" : 2490, + "modified" : "2021-03-05 15:55:17.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708463", + "_tpen_line_id" : "line/104708463", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "sed semper sitiunt opera iusticie ¶ Esuries iusticie spiritu " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,200,240,12", + "_tpen_note" : "\"Esuries\" starts a gloss which continues onto the next line", + "_tpen_creator" : 2490, + "modified" : "2021-03-05 15:57:59.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708464", + "_tpen_line_id" : "line/104708464", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "fortitudinis fulcitur, ne in uia lacescat." + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,212,240,25", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-05 15:58:08.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708465", + "_tpen_line_id" : "line/104708465", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "¶Ueritia est sua cuiqus tribuere sibi et proximis et deo. Hec iusticia" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,237,240,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-04-30 12:43:05.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708466", + "_tpen_line_id" : "line/104708466", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "non plene in nobis inplebitur donec deus omnia in omnibus. Ideo hec possi" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,250,240,12", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 11:36:14.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708467", + "_tpen_line_id" : "line/104708467", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "mus esurire, non satiari. " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,262,240,36", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 11:37:00.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708468", + "_tpen_line_id" : "line/104708468", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "¶ Adepto quod desiderant. ¶Qui aliis in quibuslibet assis" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,297,240,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 11:41:14.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708469", + "_tpen_line_id" : "line/104708469", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "tunt, quibus hoc a deo rependitur, ut a miseria liberentur." + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,311,240,10", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 11:41:24.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708470", + "_tpen_line_id" : "line/104708470", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "¶Misericordia est dare et dimittere." + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,321,240,37", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 11:42:24.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708471", + "_tpen_line_id" : "line/104708471", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,358,240,59", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-04 14:17:19.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708472", + "_tpen_line_id" : "line/104708472", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "¶Iudicium sine misericordia illi qui noluit misereri. ¶ Quos non " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,417,240,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 12:01:17.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708473", + "_tpen_line_id" : "line/104708473", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "arguit conscientia peccati, templum enim dei sanctum est." + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,430,240,12", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 12:01:28.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708474", + "_tpen_line_id" : "line/104708474", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "¶ Quorum mentes premissa beatitudinis illustrant exercicia, " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,442,240,34", + "_tpen_note" : "This line contains a gloss which goes on to the next line", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 12:02:48.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708475", + "_tpen_line_id" : "line/104708475", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "et earum mundicia cor ad deum gubernat, ut refor" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,476,240,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 12:02:58.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708476", + "_tpen_line_id" : "line/104708476", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "metur imago per peccatum deleta. " + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,490,240,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 12:03:00.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708477", + "_tpen_line_id" : "line/104708477", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "¶ hic per spem, illic per rem." + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,503,240,31", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 12:04:16.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708478", + "_tpen_line_id" : "line/104708478", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,534,240,62", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-04 14:17:30.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708479", + "_tpen_line_id" : "line/104708479", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "Hec pax ubi nisi bona uoluntate nutritur, unde \"et in terra" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,596,240,13", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-06-07 14:11:03.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708480", + "_tpen_line_id" : "line/104708480", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "pax hominibus bone uoluntatis\" hanc mundus dare non potest, sed Iesus qui ait, \"Pacem" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,609,240,12", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-06-07 14:12:17.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708481", + "_tpen_line_id" : "line/104708481", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "meam do uobis.\" Hec superat omnem sensum, sed fide capitur, unde et ea" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,621,240,30", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-06-07 14:11:25.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708482", + "_tpen_line_id" : "line/104708482", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "ea illustramur et ei reconciliati ab omni quod ab ea dissentit nos" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,651,240,16", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 12:13:32.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708483", + "_tpen_line_id" : "line/104708483", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "abdicimus, cum autem deus erit omnia in omnibus tunc beatitudo ad" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,667,240,10", + "_tpen_note" : "", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 12:15:53.0" + }, { + "@id" : "https://t-pen.org/TPEN/line/104708484", + "_tpen_line_id" : "line/104708484", + "@type" : "oa:Annotation", + "motivation" : "oad:transcribing", + "resource" : { + "@type" : "cnt:ContentAsText", + "cnt:chars" : "optionis dabitur. ¶Octaua uere circumcisionis" + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383#xywh=201,678,240,35", + "_tpen_note" : "Octaua uere\" starts a gloss which continues in the margin. There is a line pointing to the continuation of the gloss, \"beatitudo martyrio terminatur, que probatum reddit et suppletur", + "_tpen_creator" : 2490, + "modified" : "2021-03-11 12:19:24.0" + } ] + } ], + "images" : [ { + "@type" : "oa:Annotation", + "motivation" : "sc:painting", + "resource" : { + "@id" : "https://t-pen.org/TPEN/pageImage?folio=13250383", + "@type" : "dctypes:Image", + "format" : "image/jpeg", + "width" : 1332, + "height" : 2000 + }, + "on" : "https://t-pen.org/TPEN/canvas/13250383" + } ] +} \ No newline at end of file diff --git a/src/presentation-2/upgrader.ts b/src/presentation-2/upgrader.ts index d9254d1..921e94e 100644 --- a/src/presentation-2/upgrader.ts +++ b/src/presentation-2/upgrader.ts @@ -949,7 +949,8 @@ export function convertPresentation2(entity: any): Presentation3.Manifest | Pres entity["@context"] === "http://iiif.io/api/image/2/context.json" || // No-context is possible. (entity["@id"] && entity["@type"] === "sc:Collection") || - (entity["@id"] && entity["@type"] === "sc:Manifest") + (entity["@id"] && entity["@type"] === "sc:Manifest") || + (entity["@id"] && entity["@type"] === "sc:Canvas") ) { if (!entity["@context"]) { entity["@context"] = "http://iiif.io/api/presentation/2/context.json"; From e6e568622a8a8af8701b0c3ea045cea1daf757a7 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:29:05 +0100 Subject: [PATCH 02/19] Protect mixed Presentation 2 language values Covers #39 using the official multiple-description manifest. --- .../language-mapping.test.ts | 24 +++++++++++ .../iiif-multiple-descriptions.json | 42 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 __tests__/presentation-2-parser/language-mapping.test.ts create mode 100644 fixtures/presentation-2/iiif-multiple-descriptions.json diff --git a/__tests__/presentation-2-parser/language-mapping.test.ts b/__tests__/presentation-2-parser/language-mapping.test.ts new file mode 100644 index 0000000..585a347 --- /dev/null +++ b/__tests__/presentation-2-parser/language-mapping.test.ts @@ -0,0 +1,24 @@ +import manifest from "../../fixtures/presentation-2/iiif-multiple-descriptions.json"; +import { convertPresentation2, convertLanguageMapping } from "../../src/presentation-2"; + +describe("mixed Presentation 2 language values (#39)", () => { + test("retains both descriptions from the official Presentation 2 fixture", () => { + // Source: https://iiif.io/api/presentation/2.1/example/fixtures/6/manifest.json + expect(convertPresentation2(structuredClone(manifest)).summary).toEqual({ + none: ["This is one description"], + en: ["This is another"], + }); + }); + + test("retains repeated languages and untagged values in their original order", () => { + expect( + convertLanguageMapping([ + "First", + { "@value": "English", "@language": "en" }, + "Second", + { "@value": "Another English", "@language": "en" }, + { "@value": "Untagged" } as any, + ]) + ).toEqual({ none: ["First", "Second", "Untagged"], en: ["English", "Another English"] }); + }); +}); diff --git a/fixtures/presentation-2/iiif-multiple-descriptions.json b/fixtures/presentation-2/iiif-multiple-descriptions.json new file mode 100644 index 0000000..3b85d93 --- /dev/null +++ b/fixtures/presentation-2/iiif-multiple-descriptions.json @@ -0,0 +1,42 @@ +{ + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/6/manifest.json", + "@type": "sc:Manifest", + "label": "Test 6 Manifest: Multiple Descriptions", + "description": [ + "This is one description", + { + "@value": "This is another", + "@language": "en" + } + ], + "within": "http://iiif.io/api/presentation/2.1/example/fixtures/collection.json", + "sequences": [ + { + "@type": "sc:Sequence", + "label": "Test 6 Sequence 1", + "canvases": [ + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/canvas/6/c1.json", + "@type": "sc:Canvas", + "label": "Test 6 Canvas: 1", + "height": 1800, + "width": 1200, + "images": [ + { + "@type": "oa:Annotation", + "motivation": "sc:painting", + "resource": { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/resources/page1-full.png", + "@type": "dctypes:Image", + "height": 1800, + "width": 1200 + }, + "on": "http://iiif.io/api/presentation/2.1/example/fixtures/canvas/6/c1.json" + } + ] + } + ] + } + ] +} \ No newline at end of file From fbebefe66a279087f92e2cb5c9c40c8e39e1be2f Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:31:54 +0100 Subject: [PATCH 03/19] Retain invalid Presentation 2 label spellings Fixes #43. Use @label as a fallback while preserving existing empty-label behavior. --- .../invalid-label.test.ts | 32 ++ .../leipzig-invalid-label-collection.json | 406 ++++++++++++++++++ src/presentation-2/upgrader.ts | 3 +- 3 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 __tests__/presentation-2-parser/invalid-label.test.ts create mode 100644 fixtures/presentation-2/leipzig-invalid-label-collection.json diff --git a/__tests__/presentation-2-parser/invalid-label.test.ts b/__tests__/presentation-2-parser/invalid-label.test.ts new file mode 100644 index 0000000..f8ae065 --- /dev/null +++ b/__tests__/presentation-2-parser/invalid-label.test.ts @@ -0,0 +1,32 @@ +import collection from "../../fixtures/presentation-2/leipzig-invalid-label-collection.json"; +import { convertPresentation2, presentation2to3 } from "../../src/presentation-2"; + +describe("Presentation 2 @label compatibility (#43)", () => { + test("retains the Leipzig collection's manifest labels", () => { + // Source: https://iiif.ub.uni-leipzig.de/static/collections/misc/cdvost2018.json + const result = convertPresentation2(structuredClone(collection)) as any; + expect(result.items).toHaveLength(collection.manifests.length); + result.items.forEach((item: any, index: number) => { + expect(item.label).toEqual({ none: [collection.manifests[index]!["@label"]] }); + }); + }); + + test("preserves empty-label behavior and falls back to @label", () => { + const input = { "@id": "https://example.org/canvas", "@type": "sc:Canvas", label: "" }; + expect(presentation2to3.traverseCanvas({ ...input } as any).label).toBeUndefined(); + expect(presentation2to3.traverseCanvas({ ...input, "@label": "Fallback" } as any).label).toEqual({ + none: ["Fallback"], + }); + }); + + test("prefers label when both spellings are supplied", () => { + expect( + presentation2to3.traverseCanvas({ + "@id": "https://example.org/canvas", + "@type": "sc:Canvas", + label: "Correct", + "@label": "Fallback", + } as any).label + ).toEqual({ none: ["Correct"] }); + }); +}); diff --git a/fixtures/presentation-2/leipzig-invalid-label-collection.json b/fixtures/presentation-2/leipzig-invalid-label-collection.json new file mode 100644 index 0000000..9eeb002 --- /dev/null +++ b/fixtures/presentation-2/leipzig-invalid-label-collection.json @@ -0,0 +1,406 @@ +{ + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://iiif.ub.uni-leipzig.de/static/collections/misc/cdvost2018.json", + "@type": "sc:Collection", + "label": "Auswahl illuminierter mittelalterlicher Handschriften", + "description": "Auswahl illuminierter mittelalterlicher Handschriften der Universitaetsbibliothek Leipzig. Aufbereitet anlaesslich Coding da Vinci Ost 2018. Kontakt: https://www.ub.uni-leipzig.de", + "attribution": "Public Domain Mark 1.0", + "logo": "http://iiif.ub.uni-leipzig.de/ubl-logo.png", + "manifests": [ + { + "@id":"https://iiif.ub.uni-leipzig.de/0000002636/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 13" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000002974/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 14" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004536/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 49" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000002216/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 51" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000002519/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 57" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004578/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 58" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004723/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 61" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004684/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 63" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000005111/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 75" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000007413/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 85" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000007023/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 91" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004763/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 93" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000007221/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 101" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004596/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 106" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000001791/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 115" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000007416/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 148" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006377/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 173" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004595/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 187" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000007336/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 202" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000003064/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 219" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006144/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 220" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006231/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 232" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006939/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 244" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006946/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 247" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006345/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 283" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006769/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 290" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006352/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 305" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000008291/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 316" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004682/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 326" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006672/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 332" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004467/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 335" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000007343/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 372" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006395/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 388" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006384/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 389" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000002165/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 391" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004718/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 397" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000002660/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 410" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004604/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 412" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000005957/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 433" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006217/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 667" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004967/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 763" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004986/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 777" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004673/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 786" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000002544/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 788" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004694/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 793" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004984/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 809" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004133/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 827" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004762/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 828" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000005368/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 843" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004410/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 847" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004132/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 848" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000005375/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 849" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000003065/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 855" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004409/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 859" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004726/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 861" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000005128/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1111" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004482/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1329" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004476/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1361" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004124/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1369" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004395/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1399" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004084/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1402" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004077/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1403" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004114/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1405" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000003063/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1419" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000003150/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1420" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000003860/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1452" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000003234/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1459" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004120/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1466" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004437/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1505" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004411/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1515" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004368/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1526" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004380/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1528" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004631/manifest.json", + "@type":"sc:Manifest", + "@label":"Ms 1555" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000006185/manifest.json", + "@type":"sc:Manifest", + "@label":"Rep. I 4" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004677/manifest.json", + "@type":"sc:Manifest", + "@label":"Rep. I 16" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004590/manifest.json", + "@type":"sc:Manifest", + "@label":"Rep. I 19" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004679/manifest.json", + "@type":"sc:Manifest", + "@label":"Rep. I 72" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004577/manifest.json", + "@type":"sc:Manifest", + "@label":"Cod. gr. 6" + }, + { + "@id":"https://iiif.ub.uni-leipzig.de/0000004592/manifest.json", + "@type":"sc:Manifest", + "@label":"Cod. gr. 76" + } + ] +} diff --git a/src/presentation-2/upgrader.ts b/src/presentation-2/upgrader.ts index 921e94e..75f21ca 100644 --- a/src/presentation-2/upgrader.ts +++ b/src/presentation-2/upgrader.ts @@ -443,12 +443,13 @@ function descriptiveProperties ): T { const [rights, extraMetadata] = fixLicense(resource.license); + const label = resource.label || (resource as any)["@label"]; const allMetadata = [...(resource.metadata ? convertMetadata(resource.metadata) : []), ...extraMetadata]; return { rights, metadata: allMetadata.length ? allMetadata : undefined, - label: resource.label ? convertLanguageMapping(resource.label) : undefined, + label: label ? convertLanguageMapping(label) : undefined, requiredStatement: resource.attribution ? { label: convertLanguageMapping(configuration.attributionLabel), From 005cd77f98d960ad35dff9e1b65ac44eb17921cc Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:33:42 +0100 Subject: [PATCH 04/19] Preserve string URIs when upgrading Presentation 2 links Fixes #40. Keep real link identifiers instead of minting placeholder IDs. Update only numeric generated-ID expectations in existing tests. --- .../linking-string.test.ts | 32 +++++++++++++++++ .../presentation-2-parser/upgrade.test.ts | 20 +++++------ .../presentation-2/iiif-string-seealso.json | 36 +++++++++++++++++++ src/presentation-2/upgrader.ts | 4 ++- 4 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 __tests__/presentation-2-parser/linking-string.test.ts create mode 100644 fixtures/presentation-2/iiif-string-seealso.json diff --git a/__tests__/presentation-2-parser/linking-string.test.ts b/__tests__/presentation-2-parser/linking-string.test.ts new file mode 100644 index 0000000..ca235d6 --- /dev/null +++ b/__tests__/presentation-2-parser/linking-string.test.ts @@ -0,0 +1,32 @@ +import manifest from "../../fixtures/presentation-2/iiif-string-seealso.json"; +import { convertPresentation2, presentation2to3 } from "../../src/presentation-2"; + +describe("Presentation 2 linking strings (#40)", () => { + test("retains the seeAlso URI from the official Presentation 2 fixture", () => { + // Source: https://iiif.io/api/presentation/2.1/example/fixtures/8/manifest.json + expect(convertPresentation2(structuredClone(manifest)).seeAlso).toEqual([ + { id: manifest.seeAlso, type: "unknown" }, + ]); + }); + + test("handles mixed links without losing object metadata", () => { + const result = presentation2to3.traverseManifest({ + "@id": "https://example.org/manifest", + "@type": "sc:Manifest", + seeAlso: [ + "https://example.org/metadata", + { + "@id": "https://example.org/data.json", + format: "application/json", + label: "Data", + }, + ], + rendering: "https://example.org/book.pdf", + } as any); + expect(result.seeAlso).toEqual([ + { id: "https://example.org/metadata", type: "unknown" }, + { id: "https://example.org/data.json", type: "Dataset", format: "application/json", label: { none: ["Data"] } }, + ]); + expect(result.rendering).toEqual([{ id: "https://example.org/book.pdf", type: "unknown" }]); + }); +}); diff --git a/__tests__/presentation-2-parser/upgrade.test.ts b/__tests__/presentation-2-parser/upgrade.test.ts index dd3ea6f..0c24d50 100644 --- a/__tests__/presentation-2-parser/upgrade.test.ts +++ b/__tests__/presentation-2-parser/upgrade.test.ts @@ -178,7 +178,7 @@ describe("Presentation 2 to 3", () => { const body = results.items[0]!.items[0]!.items[0].body as any; expect(body.type).toEqual("Choice"); - expect(body.id).toEqual("http://example.org/oa:Choice/335"); + expect(body.id).toMatch(/^http:\/\/example\.org\/oa:Choice\/\d+$/); expect(body.items.length).toEqual(4); expect(validator.validators.manifest!.errors).toEqual(null); @@ -449,10 +449,10 @@ describe("Presentation 2 to 3", () => { { "body": { "chars": "Top of First Page to Display", - "id": "http://example.org/cnt:ContentAsText/438", + "id": "http://example.org/cnt:ContentAsText/382", "type": "TextualBody", }, - "id": "http://example.org/oa:Annotation/439", + "id": "http://example.org/oa:Annotation/383", "motivation": "painting", "target": { "selector": { @@ -471,18 +471,18 @@ describe("Presentation 2 to 3", () => { "body": [ { "chars": "character", - "id": "http://example.org/oa:Tag/440", + "id": "http://example.org/oa:Tag/384", "type": "Tag", }, { "chars": "

万

", "format": "text/html", - "id": "http://example.org/dctypes:Text/441", + "id": "http://example.org/dctypes:Text/385", "type": "Text", }, ], - "id": "http://example.org/oa:Annotation/442", + "id": "http://example.org/oa:Annotation/386", "motivation": [ "tagging", "commenting", @@ -526,17 +526,17 @@ describe("Presentation 2 to 3", () => { { "chars": "

Zone of interest
Place
Cauca of Huila

", "format": "text/html", - "id": "http://example.org/dctypes:Text/443", + "id": "http://example.org/dctypes:Text/387", "type": "Text", }, { "chars": "Colombia", - "id": "http://example.org/oa:Tag/444", + "id": "http://example.org/oa:Tag/388", "type": "Tag", }, { "chars": "Cauca", - "id": "http://example.org/oa:Tag/445", + "id": "http://example.org/oa:Tag/389", "type": "Tag", }, ], @@ -584,7 +584,7 @@ describe("Presentation 2 to 3", () => { "body": { "chars": "

Surface tool
Feuille
70.97 mm²

", "format": "text/html", - "id": "http://example.org/dctypes:Text/446", + "id": "http://example.org/dctypes:Text/390", "type": "Text", }, "id": "https://collections.recolnat.org/annotate-server/iiif/2/annotationList/30/febbe57672bb5eaf097de6c5448c43ad8a82e012/list/3", diff --git a/fixtures/presentation-2/iiif-string-seealso.json b/fixtures/presentation-2/iiif-string-seealso.json new file mode 100644 index 0000000..e8dca5c --- /dev/null +++ b/fixtures/presentation-2/iiif-string-seealso.json @@ -0,0 +1,36 @@ +{ + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/8/manifest.json", + "@type": "sc:Manifest", + "label": "Test 8 Manifest: SeeAlso link / Manifest", + "seeAlso": "http://www.example.org/link/to/metadata", + "within": "http://iiif.io/api/presentation/2.1/example/fixtures/collection.json", + "sequences": [ + { + "@type": "sc:Sequence", + "label": "Test 8 Sequence 1", + "canvases": [ + { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/canvas/8/c1.json", + "@type": "sc:Canvas", + "label": "Test 8 Canvas: 1", + "height": 1800, + "width": 1200, + "images": [ + { + "@type": "oa:Annotation", + "motivation": "sc:painting", + "resource": { + "@id": "http://iiif.io/api/presentation/2.1/example/fixtures/resources/page1-full.png", + "@type": "dctypes:Image", + "height": 1800, + "width": 1200 + }, + "on": "http://iiif.io/api/presentation/2.1/example/fixtures/canvas/8/c1.json" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/presentation-2/upgrader.ts b/src/presentation-2/upgrader.ts index 75f21ca..478200a 100644 --- a/src/presentation-2/upgrader.ts +++ b/src/presentation-2/upgrader.ts @@ -832,7 +832,9 @@ function upgradeContentResourceOrChoice( } function upgradeContentResource(inputContentResource: Presentation2.ContentResource): Presentation3.ContentResource { - const contentResource = inputContentResource as Presentation2.CommonContentResource; + const contentResource = (typeof inputContentResource === "string" + ? { "@id": inputContentResource } + : inputContentResource) as Presentation2.CommonContentResource; // @todo there might be some field dropped here. return removeUndefinedProperties({ From cef079e22aa28776f2203598baeecaf3c0baca4c Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:37:58 +0100 Subject: [PATCH 05/19] Retain all Image API selector parameters during upgrade Fixes #27. Preserve size, quality and format and retain required-property type narrowing for existing consumers. --- .../image-api-selector.test.ts | 70 +++++++++++++++++++ .../legacy/resources/content-resource.d.ts | 28 ++++---- src/presentation-2/upgrader.ts | 3 + 3 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 __tests__/presentation-2-parser/image-api-selector.test.ts diff --git a/__tests__/presentation-2-parser/image-api-selector.test.ts b/__tests__/presentation-2-parser/image-api-selector.test.ts new file mode 100644 index 0000000..770a701 --- /dev/null +++ b/__tests__/presentation-2-parser/image-api-selector.test.ts @@ -0,0 +1,70 @@ +import { presentation2to3 } from "../../src/presentation-2"; +import type { ContentResourceSelector } from "../../src/presentation-2/types"; +import { normalize, serialize, serializeConfigPresentation3 } from "../../src"; + +const selector = { + "@context": "http://iiif.io/api/annex/openannotation/context.json", + "@type": "iiif:ImageApiSelector", + region: "50,50,100,100", + size: "200,", + rotation: "!90", + quality: "gray", + format: "png", +} satisfies ContentResourceSelector; + +describe("Image API selector fields (#27)", () => { + test("preserves every image parameter through upgrade, normalization and serialization", () => { + const annotation = presentation2to3.traverseAnnotation({ + "@id": "https://example.org/annotation", + "@type": "oa:Annotation", + motivation: "sc:painting", + on: { + "@type": "oa:SpecificResource", + full: "https://example.org/canvas", + selector, + }, + resource: { "@id": "https://example.org/image.png", "@type": "dctypes:Image" }, + } as any); + const expected = { + type: "ImageApiSelector", + region: selector.region, + size: selector.size, + rotation: selector.rotation, + quality: selector.quality, + format: selector.format, + }; + expect((annotation.target as any).selector).toEqual(expected); + const result = normalize(annotation); + const output = serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); + expect(output.target.selector).toEqual(expected); + }); + + test("allows selectors that rely on default parameters", () => { + const defaults = { "@context": selector["@context"], "@type": selector["@type"] } satisfies ContentResourceSelector; + const annotation = presentation2to3.traverseAnnotation({ + "@id": "https://example.org/defaults", + "@type": "oa:Annotation", + on: { "@type": "oa:SpecificResource", full: "https://example.org/canvas", selector: defaults }, + resource: { "@id": "https://example.org/image", "@type": "dctypes:Image" }, + } as any); + expect(JSON.parse(JSON.stringify((annotation.target as any).selector))).toEqual({ type: "ImageApiSelector" }); + }); +}); + +// Existing consumers can still narrow required region/rotation values with `in`. +function readParameter(input: ContentResourceSelector): string | undefined { + if (input["@type"] === "iiif:ImageApiSelector") { + if ("region" in input) { + const region: string = input.region; + return region; + } + if ("rotation" in input) { + const rotation: string = input.rotation; + return rotation; + } + } +} + +test("preserves existing selector parameter narrowing", () => { + expect(readParameter(selector)).toBe(selector.region); +}); diff --git a/src/presentation-2/types/legacy/resources/content-resource.d.ts b/src/presentation-2/types/legacy/resources/content-resource.d.ts index 9a95ac5..f07d135 100644 --- a/src/presentation-2/types/legacy/resources/content-resource.d.ts +++ b/src/presentation-2/types/legacy/resources/content-resource.d.ts @@ -3,27 +3,23 @@ */ import { ImageService, Service as P3Service } from "../../../../presentation-3/types"; +type ImageApiSelectorParameters = { + "@context": "http://iiif.io/api/annex/openannotation/context.json"; + "@type": "iiif:ImageApiSelector"; + size?: string; + quality?: string; + format?: string; +}; + export type ContentResourceSelector = - | { - "@context": "http://iiif.io/api/annex/openannotation/context.json"; - "@type": "iiif:ImageApiSelector"; - region: string; - } + | (ImageApiSelectorParameters & { region: string }) + | (ImageApiSelectorParameters & { rotation: string }) + | (ImageApiSelectorParameters & { region: string; rotation: string }) + | ImageApiSelectorParameters | { "@type": ["oa:SvgSelector", "cnt:ContentAsText"] | ["cnt:ContentAsText", "oa:SvgSelector"] | "oa:SvgSelector"; chars: string; } - | { - "@context": "http://iiif.io/api/annex/openannotation/context.json"; - "@type": "iiif:ImageApiSelector"; - rotation: string; - } - | { - "@context": "http://iiif.io/api/annex/openannotation/context.json"; - "@type": "iiif:ImageApiSelector"; - region: string; - rotation: string; - } | { "@type": "oa:Choice"; default: ContentResourceSelector; diff --git a/src/presentation-2/upgrader.ts b/src/presentation-2/upgrader.ts index 478200a..ba8e8ba 100644 --- a/src/presentation-2/upgrader.ts +++ b/src/presentation-2/upgrader.ts @@ -995,6 +995,9 @@ function upgradeSelector( type: "ImageApiSelector", region: "region" in selector ? selector.region : undefined, rotation: "rotation" in selector ? selector.rotation : undefined, + size: selector.size, + quality: selector.quality, + format: selector.format, }; } throw new Error(`Unsupported selector type: ${selector["@type"]}`); From 7472e57a9a400658921376caa61d58cd973a3160 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:43:42 +0100 Subject: [PATCH 06/19] Upgrade related links to resource homepages Fixes #52. Preserve all related links and keep provider metadata tied to the logo. Existing input fixtures remain unchanged; output snapshots reflect the corrected link location. --- .../__snapshots__/upgrade.test.ts.snap | 25 ++-- .../related-links.test.ts | 46 +++++++ .../presentation-2/wellcome-ixif-audio.json | 126 ++++++++++++++++++ src/presentation-2/upgrader.ts | 8 +- 4 files changed, 188 insertions(+), 17 deletions(-) create mode 100644 __tests__/presentation-2-parser/related-links.test.ts create mode 100644 fixtures/presentation-2/wellcome-ixif-audio.json diff --git a/__tests__/presentation-2-parser/__snapshots__/upgrade.test.ts.snap b/__tests__/presentation-2-parser/__snapshots__/upgrade.test.ts.snap index 103a8d6..5b8dcb0 100644 --- a/__tests__/presentation-2-parser/__snapshots__/upgrade.test.ts.snap +++ b/__tests__/presentation-2-parser/__snapshots__/upgrade.test.ts.snap @@ -60,6 +60,18 @@ exports[`Presentation 2 to 3 > goettingen manifest 1`] = ` "paged", "paged", ], + "homepage": [ + { + "format": "text/html", + "id": "http://kalliope-verbund.info/DE-611-HS-3216958", + "label": { + "none": [ + "Kalliope", + ], + }, + "type": "Text", + }, + ], "id": "https://manifests.sub.uni-goettingen.de/id/DE-611-HS-3216958", "items": [ { @@ -2960,18 +2972,6 @@ exports[`Presentation 2 to 3 > goettingen manifest 1`] = ` "navDate": "1902-01-01T00:00:00+01:00", "provider": [ { - "homepage": [ - { - "format": "text/html", - "id": "http://kalliope-verbund.info/DE-611-HS-3216958", - "label": { - "none": [ - "Kalliope", - ], - }, - "type": "Text", - }, - ], "id": "http://example.org/provider", "label": { "none": [ @@ -40346,7 +40346,6 @@ exports[`Presentation 2 to 3 > nested ranges 1`] = ` "navDate": "1623-01-01T00:00:00Z", "provider": [ { - "homepage": undefined, "id": "http://example.org/provider", "label": { "none": [ diff --git a/__tests__/presentation-2-parser/related-links.test.ts b/__tests__/presentation-2-parser/related-links.test.ts new file mode 100644 index 0000000..ee4c97f --- /dev/null +++ b/__tests__/presentation-2-parser/related-links.test.ts @@ -0,0 +1,46 @@ +import manifest from "../../fixtures/presentation-2/wellcome-ixif-audio.json"; +import { convertPresentation2, presentation2to3 } from "../../src/presentation-2"; +import { normalize, serialize, serializeConfigPresentation3 } from "../../src"; + +describe("Presentation 2 related links (#52)", () => { + test("puts the Wellcome catalogue link on the manifest homepage", () => { + // Source: https://wellcomelibrary.org/iiif/b17307922/manifest + const upgraded = convertPresentation2(structuredClone(manifest)); + const expected = [ + { + id: manifest.related["@id"], + type: "Text", + format: "text/html", + label: { none: [manifest.related.label] }, + }, + ]; + expect(upgraded.homepage).toEqual(expected); + expect(upgraded.provider?.[0]?.logo?.[0]).toMatchObject({ id: manifest.logo, type: "Image" }); + expect(upgraded.provider?.[0]?.homepage).toBeUndefined(); + const result = normalize(upgraded); + expect(serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3).homepage).toEqual( + expected + ); + }); + + test("retains every related link and avoids a provider when no logo exists", () => { + const upgraded = presentation2to3.traverseManifest({ + "@id": "https://example.org/manifest", + "@type": "sc:Manifest", + label: "Manifest", + related: [ + "https://example.org/one", + { + "@id": "https://example.org/two", + format: "text/html", + label: "Two", + }, + ], + } as any); + expect(upgraded.homepage).toEqual([ + { id: "https://example.org/one", type: "Text" }, + { id: "https://example.org/two", type: "Text", format: "text/html", label: { none: ["Two"] } }, + ]); + expect(upgraded.provider).toBeUndefined(); + }); +}); diff --git a/fixtures/presentation-2/wellcome-ixif-audio.json b/fixtures/presentation-2/wellcome-ixif-audio.json new file mode 100644 index 0000000..0ce78eb --- /dev/null +++ b/fixtures/presentation-2/wellcome-ixif-audio.json @@ -0,0 +1,126 @@ +{ + "@context": [ + "http://wellcomelibrary.org/ld/ixif/0/context.json", + "http://iiif.io/api/presentation/2/context.json" + ], + "@id": "https://iiif.wellcomecollection.org/presentation/v2/b17307922", + "@type": "sc:Manifest", + "label": "Florence Nightingale : greetings to the dear old comrades of Balaclava. Rendition 1/2.", + "metadata": [ + { + "label": "Description", + "value": "

This short recording was made on 30th July 1890 to raise money for the impoverished veterans of the Charge of the Light Brigade. The full transcript of the recording says: 'When I am no longer even a memory, just a name, I hope my voice may perpetuate the great work of my life. God bless my dear old comrades of Balaclava and bring them safe to shore. Florence Nightingale.' In fact, there are two recitations; the second has slightly altered wording to the first, which was presumably a practice session.

" + }, + { + "label": "Publication/creation", + "value": "[Place of publication not identified], 1890." + }, + { + "label": "Physical description", + "value": "1 encoded audio file (01.14 min.) : 44.1kHz + 1 PDF transcript" + }, + { + "label": "Notes", + "value": "This sound recording is a copy made from the original brown wax cylinder on behalf of Wellcome Library by the British Library (18th March, 2004) featuring the voice of Florence Nightingale recorded on 30 July 1890. The wax cylinder recording had been donated to Sir Henry Wellcome in the 1930s but was forgotten in the collection for decades." + }, + { + "label": "Subjects", + "value": "History of Medicine; England; Nightingale, Florence, 1820-1910." + }, + { + "label": "Attribution", + "value": "Wellcome Collection" + }, + { + "label": "Full conditions of use", + "value": "This work has been identified as being free of known restrictions under copyright law, including all related and neighbouring rights and is being made available under the Creative Commons, Public Domain Mark.

You can copy, modify, distribute and perform the work, even for commercial purposes, without asking permission.
" + } + ], + "description": "

This short recording was made on 30th July 1890 to raise money for the impoverished veterans of the Charge of the Light Brigade. The full transcript of the recording says: 'When I am no longer even a memory, just a name, I hope my voice may perpetuate the great work of my life. God bless my dear old comrades of Balaclava and bring them safe to shore. Florence Nightingale.' In fact, there are two recitations; the second has slightly altered wording to the first, which was presumably a practice session.

", + "license": "http://creativecommons.org/publicdomain/mark/1.0/", + "logo": "https://iiif.wellcomecollection.org/logos/wellcome-collection-black.png", + "related": { + "@id": "https://wellcomecollection.org/works/tp9njewm", + "format": "text/html", + "label": "Florence Nightingale : greetings to the dear old comrades of Balaclava. Rendition 1/2." + }, + "seeAlso": { + "@id": "https://api.wellcomecollection.org/catalogue/v2/works/tp9njewm", + "profile": "https://api.wellcomecollection.org/catalogue/v2/context.json", + "format": "application/json", + "label": "Wellcome Collection Catalogue API" + }, + "service": [ + { + "@context": "http://universalviewer.io/context.json", + "@id": "http://wellcomelibrary.org/service/trackingLabels/b17307922", + "profile": "http://universalviewer.io/tracking-extensions-profile", + "trackingLabel": "Format: Audio, Institution: n/a, Identifier: b17307922, Digicode: digaudio, Collection code: n/a" + }, + { + "@context": "http://wellcomelibrary.org/ld/iiif-ext/0/context.json", + "@id": "https://wellcomelibrary.org/iiif/b17307922/access-control-hints-service", + "profile": "http://wellcomelibrary.org/ld/iiif-ext/access-control-hints", + "accessHint": "open" + } + ], + "mediaSequences": [ + { + "@id": "https://iiif.wellcomecollection.org/iiif/b17307922/xsequence/s0", + "@type": "ixif:MediaSequence", + "label": "XSequence 0", + "elements": [ + { + "@id": "https://iiif.wellcomecollection.org/av/b17307922_0056-0000-4402-0102-0-0000-0000-0.mp3/full/max/default.mp3#identity", + "@type": "dctypes:Sound", + "format": "audio/mp3", + "label": "Florence Nightingale : greetings to the dear old comrades of Balaclava. Rendition 1/2.", + "metadata": [ + { + "label": "length", + "value": "74.057144 s" + } + ], + "thumbnail": "https://iiif.wellcomecollection.org/thumb/b17307922", + "rendering": { + "@id": "https://iiif.wellcomecollection.org/av/b17307922_0056-0000-4402-0102-0-0000-0000-0.mp3/full/max/default.mp3", + "format": "audio/mp3" + } + } + ] + } + ], + "sequences": [ + { + "@id": "https://wellcomecollection.org/iiif/ixif-message/sequence/seq", + "@type": "sc:Sequence", + "label": "Unsupported extension. This manifest is being used as a wrapper for non-IIIF content (e.g., audio, video) and is unfortunately incompatible with IIIF viewers.", + "compatibilityHint": "displayIfContentUnsupported", + "canvases": [ + { + "@id": "https://wellcomelibrary.org/iiif/ixif-message/canvas/c1", + "@type": "sc:Canvas", + "label": "Placeholder image", + "thumbnail": "https://wellcomecollection.org/placeholder.jpg", + "height": 600, + "width": 600, + "images": [ + { + "@id": "https://wellcomecollection.org/iiif/ixif-message/imageanno/placeholder", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "resource": { + "@id": "https://wellcomecollection.org/iiif/ixif-message-0/res/placeholder", + "@type": "dctypes:Image", + "height": 600, + "width": 600 + }, + "on": "https://wellcomecollection.org/iiif/ixif-message/canvas/c1" + } + ] + } + ] + } + ], + "within": "https://iiif.wellcomecollection.org/presentation/v2/collections/subjects/cfg7w8cr" +} \ No newline at end of file diff --git a/src/presentation-2/upgrader.ts b/src/presentation-2/upgrader.ts index ba8e8ba..8de4140 100644 --- a/src/presentation-2/upgrader.ts +++ b/src/presentation-2/upgrader.ts @@ -510,24 +510,24 @@ function parseWithin(resource: Presentation2.AbstractResource): undefined | Pres } function linkingProperties(resource: Presentation2.LinkingProperties & Presentation2.RightsProperties) { - // @todo related links to metadata. - const related = resource.related ? (Array.isArray(resource.related) ? resource.related : [resource.related]) : []; const layer = resource.contentLayer as Presentation2.Layer; return { provider: - resource.logo || related.length + resource.logo ? [ { id: configuration.providerId, type: "Agent" as const, - homepage: related.length ? [related[0] as any] : undefined, logo: resource.logo ? (Array.isArray(resource.logo) ? resource.logo : [resource.logo]) : undefined, label: convertLanguageMapping(configuration.providerName), }, ] : undefined, + homepage: related.length + ? related.map((link: any) => ({ ...link, type: link.type === "unknown" ? "Text" : link.type })) + : undefined, partOf: parseWithin(resource), rendering: resource.rendering, seeAlso: resource.seeAlso, From 2ae609e9e3004e577081dca2aace22c3ba08763d Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:45:51 +0100 Subject: [PATCH 07/19] Preserve collection pagination across upgrades and serializers Fixes #42. Retain first, last, prev, next, total and startIndex, including zero values, without adding navigation pages to collection items. --- .../paginated-collections.test.ts | 57 +++++++++++++++++++ src/presentation-2/upgrader.ts | 9 +++ .../serialize-presentation-2.ts | 3 + .../serialize-presentation-3.ts | 12 ++++ 4 files changed, 81 insertions(+) create mode 100644 __tests__/presentation-3-parser/paginated-collections.test.ts diff --git a/__tests__/presentation-3-parser/paginated-collections.test.ts b/__tests__/presentation-3-parser/paginated-collections.test.ts new file mode 100644 index 0000000..a66cf4b --- /dev/null +++ b/__tests__/presentation-3-parser/paginated-collections.test.ts @@ -0,0 +1,57 @@ +import collection from "../../fixtures/presentation-2/paginated-collection.json"; +import page from "../../fixtures/presentation-2/paginated-collection-page.json"; +import { normalize, serialize, serializeConfigPresentation3, serializeConfigPresentation2 } from "../../src"; + +describe("paged Presentation 2 collections (#42)", () => { + test.each([collection, page])("keeps navigation after normalization and serialization of $@id", (input) => { + const result = normalize(structuredClone(input)); + const output = serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); + for (const key of ["first", "prev", "next"] as const) { + if ((input as any)[key]) expect(output[key]).toEqual({ id: (input as any)[key], type: "Collection" }); + } + if ("total" in input) expect(output.total).toBe(input.total); + expect(output.items?.some((item: any) => item.id === output.next?.id)).not.toBe(true); + }); + + test("keeps zero totals and object pagination references", () => { + const result = normalize({ + "@id": "https://example.org/collection", + "@type": "sc:Collection", + label: "Empty", + total: 0, + next: { "@id": "https://example.org/next", "@type": "sc:Collection" }, + }); + const output = serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); + expect(output.total).toBe(0); + expect(output.next).toEqual({ id: "https://example.org/next", type: "Collection" }); + }); + test("keeps last, startIndex and embedded collection pagination in both serializers", () => { + const input = { + "@id": "https://example.org/parent", + "@type": "sc:Collection", + label: "Parent", + last: "https://example.org/last", + startIndex: 0, + total: 0, + collections: [ + { + "@id": "https://example.org/child", + "@type": "sc:Collection", + label: "Child", + first: "https://example.org/first", + total: 0, + }, + ], + }; + const result = normalize(input); + const store = { ...result, requests: {} }; + const p3 = serialize(store, result.resource, serializeConfigPresentation3); + expect(p3.last).toEqual({ id: input.last, type: "Collection" }); + expect(p3.startIndex).toBe(0); + expect(p3.items[0]).toMatchObject({ first: { id: "https://example.org/first", type: "Collection" }, total: 0 }); + const p2 = serialize(store, result.resource, serializeConfigPresentation2); + expect(p2.last).toBe(input.last); + expect(p2.startIndex).toBe(0); + expect(p2.total).toBe(0); + }); +}); diff --git a/src/presentation-2/upgrader.ts b/src/presentation-2/upgrader.ts index 8de4140..b710cc0 100644 --- a/src/presentation-2/upgrader.ts +++ b/src/presentation-2/upgrader.ts @@ -590,6 +590,15 @@ function paginationProperties(collection: Presentation2.Collection) { } } + if ((collection as any).last) { + const ref = stringOrRefToRef((collection as any).last, "Collection"); + if (ref) additionalProperties.last = ref; + } + + if ((collection as any).startIndex || (collection as any).startIndex === 0) { + additionalProperties.startIndex = (collection as any).startIndex; + } + if ((collection as any).total || (collection as any).total === 0) { additionalProperties.total = (collection as any).total; } diff --git a/src/presentation-3/serialize-presentation-2.ts b/src/presentation-3/serialize-presentation-2.ts index e55f30d..74b7c91 100644 --- a/src/presentation-3/serialize-presentation-2.ts +++ b/src/presentation-3/serialize-presentation-2.ts @@ -272,6 +272,9 @@ export const serializeConfigPresentation2: SerializeConfig = { Collection: function* (entity) { return [ ...technicalProperties(entity, "sc:Collection"), + ...["first", "last", "prev", "next"].map((key) => [key, (entity as any)[key]?.id || (entity as any)[key]]), + ["total", (entity as any).total], + ["startIndex", (entity as any).startIndex], ...(yield* descriptiveProperties(entity)), ...(yield* linkingProperties(entity)), ["members", yield* entity.items], diff --git a/src/presentation-3/serialize-presentation-3.ts b/src/presentation-3/serialize-presentation-3.ts index 3842fc1..21d0273 100644 --- a/src/presentation-3/serialize-presentation-3.ts +++ b/src/presentation-3/serialize-presentation-3.ts @@ -322,6 +322,12 @@ export const serializeConfigPresentation3: SerializeConfig = { return [ ["@context", context], ...technicalProperties(entity), + ["first", (entity as any).first], + ["last", (entity as any).last], + ["startIndex", (entity as any).startIndex], + ["prev", (entity as any).prev], + ["next", (entity as any).next], + ["total", (entity as any).total], ...(yield* descriptiveProperties(entity)), ...(yield* linkingProperties(entity)), ["items", filterEmpty(yield entity.items)], @@ -330,6 +336,12 @@ export const serializeConfigPresentation3: SerializeConfig = { } return [ ...technicalProperties(entity), + ["first", (entity as any).first], + ["last", (entity as any).last], + ["startIndex", (entity as any).startIndex], + ["prev", (entity as any).prev], + ["next", (entity as any).next], + ["total", (entity as any).total], ...(yield* descriptiveProperties(entity)), ["navPlace", (entity as any).navPlace], ]; From f9a85bcd96b00929100485768e4b17e15c2514a3 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:48:17 +0100 Subject: [PATCH 08/19] Preserve linked metadata in Presentation 3 serialization Fixes #28. Serialize typed inline content links and linked fields on manifest and collection snippets while preserving normalized store shapes and finite parent references. --- .../linked-fields.test.ts | 121 ++++++++++++++++++ .../serialize-presentation-3.ts | 7 +- src/presentation-3/serialize.ts | 7 +- 3 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 __tests__/presentation-3-parser/linked-fields.test.ts diff --git a/__tests__/presentation-3-parser/linked-fields.test.ts b/__tests__/presentation-3-parser/linked-fields.test.ts new file mode 100644 index 0000000..6dd5482 --- /dev/null +++ b/__tests__/presentation-3-parser/linked-fields.test.ts @@ -0,0 +1,121 @@ +import { normalize, serialize, serializeConfigPresentation3, Traverse } from "../../src"; +import homepage from "../../fixtures/cookbook/0047-homepage.json"; +import seeAlso from "../../fixtures/cookbook/0053-seeAlso.json"; +import provider from "../../fixtures/cookbook/0234-provider.json"; + +function roundTrip(input: any): any { + const result = normalize(structuredClone(input)); + return serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); +} + +describe("Presentation 3 linked fields (#28)", () => { + test.each([homepage, seeAlso, provider])("preserves published cookbook links for $id", (manifest) => { + const result = roundTrip(manifest); + for (const key of ["homepage", "seeAlso", "provider"] as const) { + if ((manifest as any)[key]) expect(result[key]).toEqual((manifest as any)[key]); + } + expect(JSON.stringify(result)).not.toContain("__$UNSET$__"); + }); + + test("resolves links on annotations, their bodies, and agents", () => { + const link = { id: "https://example.org/data", type: "Dataset", format: "application/json" }; + const agent = { + id: "https://example.org/agent", + type: "Agent", + label: { en: ["Provider"] }, + homepage: [{ id: "https://example.org", type: "Text", format: "text/html" }], + seeAlso: [link], + }; + const input = { + id: "https://example.org/annotation", + type: "Annotation", + motivation: "commenting", + label: { en: ["Comment"] }, + summary: { en: ["Summary"] }, + seeAlso: [link], + provider: [agent], + target: "https://example.org/canvas", + body: { id: "https://example.org/body", type: "TextualBody", value: "Hello", seeAlso: [link] }, + }; + expect(roundTrip(input)).toEqual(input); + }); + + test.each(["Manifest", "Collection"])("preserves homepage on a %s referenced through partOf", (type) => { + const manifest = { + id: "https://example.org/parent", + type, + label: { en: ["Parent"] }, + homepage: [{ id: "https://example.org/home", type: "Text", format: "text/html" }], + }; + const input = { id: "https://example.org/canvas", type: "Canvas", items: [], partOf: [manifest] }; + expect(roundTrip(input).partOf).toEqual([manifest]); + }); + test.each(["Manifest", "Collection"])("serializes %s items with a back-reference to their collection", (type) => { + const id = "https://example.org/collection"; + const input = { + id, + type: "Collection", + label: { en: ["Parent"] }, + items: [ + { + id: "https://example.org/child", + type, + label: { en: ["Child"] }, + partOf: [{ id, type: "Collection" }], + }, + ], + }; + expect(roundTrip(input).items[0].partOf).toBeUndefined(); + }); + + test("keeps the content resource as the service callback parent", () => { + const image = { + id: "https://example.org/image", + type: "Image", + service: [{ id: "https://example.org/service", type: "ImageService3", profile: "level1" }], + }; + const parents: unknown[] = []; + new Traverse({ + service: [ + (_, context) => { + parents.push(context.parent); + }, + ], + }).traverseContentResource(image as any); + expect(parents).toEqual([image]); + }); + + test("preserves the existing inline body-link store shape", () => { + const link = { id: "https://example.org/data", type: "Dataset", format: "application/json" }; + const result = normalize({ + id: "https://example.org/annotation", + type: "Annotation", + target: "https://example.org/canvas", + body: { id: "https://example.org/body", type: "TextualBody", value: "Hello", seeAlso: [link] }, + }); + expect((result.entities.ContentResource as any)["https://example.org/body"].seeAlso).toEqual([link]); + }); + + test("keeps self-referencing inline links finite", () => { + const id = "https://example.org/thumbnail"; + const input = { + "@context": "http://iiif.io/api/presentation/3/context.json", + id: "https://example.org/manifest", + type: "Manifest", + label: { en: ["Manifest"] }, + items: [], + thumbnail: [{ id, type: "Image", seeAlso: [{ id, type: "Image" }] }], + }; + expect(roundTrip(input)).toEqual(input); + }); + + test("keeps ignoring untyped links in manually populated stores", () => { + const result = normalize({ id: "https://example.org/manifest", type: "Manifest", items: [] }); + (result.entities.Manifest as any)[result.resource.id].homepage = [ + "https://example.org/link", + { id: "https://example.org/link" }, + ]; + const output = serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); + expect(output.homepage).toBeUndefined(); + }); +}); diff --git a/src/presentation-3/serialize-presentation-3.ts b/src/presentation-3/serialize-presentation-3.ts index 21d0273..bd6844a 100644 --- a/src/presentation-3/serialize-presentation-3.ts +++ b/src/presentation-3/serialize-presentation-3.ts @@ -107,10 +107,11 @@ function* descriptiveProperties( function* linkingProperties( entity: Partial, - parent?: any + parent?: any, + omitPartOf = false ): Generator> { let filteredPartOf = []; - for (let partOf of entity.partOf || []) { + for (let partOf of omitPartOf ? [] : entity.partOf || []) { if (partOf.type === "Manifest" && parent?.type === "Manifest") continue; filteredPartOf.push(yield partOf); } @@ -141,6 +142,7 @@ export const serializeConfigPresentation3: SerializeConfig = { // Only a snippet. ...technicalProperties(entity), ...(yield* descriptiveProperties(entity)), + ...(yield* linkingProperties(entity, undefined, true)), ["navPlace", (entity as any).navPlace], ]; } @@ -343,6 +345,7 @@ export const serializeConfigPresentation3: SerializeConfig = { ["next", (entity as any).next], ["total", (entity as any).total], ...(yield* descriptiveProperties(entity)), + ...(yield* linkingProperties(entity, undefined, true)), ["navPlace", (entity as any).navPlace], ]; }, diff --git a/src/presentation-3/serialize.ts b/src/presentation-3/serialize.ts index 3645549..c7da5bc 100644 --- a/src/presentation-3/serialize.ts +++ b/src/presentation-3/serialize.ts @@ -96,15 +96,16 @@ export function serialize(state: CompatibleStore, subject: Reference, co } function flatten(sub: Reference, parent?: any, depth = 0) { - const generator = config[sub.type as keyof SerializeConfig]; + const generator = config[sub.type as keyof SerializeConfig] || (typeof sub?.type === "string" && typeof sub.id === "string" && !state.entities[sub.type] && config.ContentResource); if (!generator) { return UNSET; } if (depth > 20) { throw new Error("Circular reference: " + sub.id + " " + sub.type); } - const [resource, fullResource] = - resolveIfExists(state, sub.type ? sub : sub.id, parent) || (sub.id && sub.type ? sub : null); + const [resource, fullResource] = state.entities[sub.type] + ? resolveIfExists(state, sub, parent) + : [sub, sub]; if (!resource) { return UNSET; } From a384b2354e405f2aff0b74c6a58e18c86fd01cab Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:50:00 +0100 Subject: [PATCH 09/19] Normalize range selectors against canonical canvas identifiers Fixes #3. Split media fragments before storing canvases, keep Canvas resources out of the Range table, and preserve unrelated anchors. Refresh output snapshots without changing input fixtures. --- .../__snapshots__/cookbook.tests.ts.snap | 1936 +++++++---------- .../__snapshots__/smoke.tests.ts.snap | 1685 ++++++-------- .../range-selectors.test.ts | 43 + src/presentation-3/traverse.ts | 16 +- src/shared/canvas-fragments.ts | 2 +- 5 files changed, 1498 insertions(+), 2184 deletions(-) create mode 100644 __tests__/presentation-3-parser/range-selectors.test.ts diff --git a/__tests__/presentation-3-parser/__snapshots__/cookbook.tests.ts.snap b/__tests__/presentation-3-parser/__snapshots__/cookbook.tests.ts.snap index 173d14d..2713588 100644 --- a/__tests__/presentation-3-parser/__snapshots__/cookbook.tests.ts.snap +++ b/__tests__/presentation-3-parser/__snapshots__/cookbook.tests.ts.snap @@ -16016,6 +16016,13 @@ exports[`Cookbook > Testing normalize %p (%p) 71`] = ` "height": 2504, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p1", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p1", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r1", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/page/p1/1", @@ -16050,6 +16057,13 @@ exports[`Cookbook > Testing normalize %p (%p) 71`] = ` "height": 2512, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p2", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p2", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r1", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/page/p2/1", @@ -16084,6 +16098,13 @@ exports[`Cookbook > Testing normalize %p (%p) 71`] = ` "height": 2456, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p3", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p3", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r2/1", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/page/p3/1", @@ -16118,6 +16139,13 @@ exports[`Cookbook > Testing normalize %p (%p) 71`] = ` "height": 2440, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p4", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p4", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r2/1", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/page/p4/1", @@ -16152,6 +16180,13 @@ exports[`Cookbook > Testing normalize %p (%p) 71`] = ` "height": 2416, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p5", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p5", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r2/2", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/page/p5/1", @@ -16186,6 +16221,13 @@ exports[`Cookbook > Testing normalize %p (%p) 71`] = ` "height": 2416, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p6", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p6", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r2/2", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/page/p6/1", @@ -16413,198 +16455,6 @@ exports[`Cookbook > Testing normalize %p (%p) 71`] = ` }, }, "Range": { - "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p1": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p1", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p1", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r1", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p2": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p2", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p2", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r1", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p3": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p3", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p3", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r2/1", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p4": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p4", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p4", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r2/1", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p5": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p5", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p5", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r2/2", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p6": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p6", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/canvas/p6", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r2/2", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, "https://iiif.io/api/cookbook/recipe/0024-book-4-toc/range/r0": { "accompanyingCanvas": null, "annotations": [], @@ -17325,6 +17175,92 @@ exports[`Cookbook > Testing normalize %p (%p) 73`] = ` "height": 1080, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1", + "iiif-parser:hasPart": [ + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "duration": {}, + "height": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/3", + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1/annotation_page/1", + "type": "AnnotationPage", + }, + ], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/4", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/5", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + ], + "iiif-parser:isExternal": true, "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1/annotation_page/1", @@ -17420,109 +17356,13 @@ exports[`Cookbook > Testing normalize %p (%p) 73`] = ` }, }, "Range": { - "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=0,302.05": { + "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/1": { "accompanyingCanvas": null, "annotations": [], "behavior": [], "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=0,302.05", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=0,302.05", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/3", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=302.05,3971.24": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=302.05,3971.24", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=302.05,3971.24", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/4", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=3971.24": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=3971.24", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=3971.24", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/5", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/1": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/1", - "items": [ + "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/1", + "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0026-toc-opera/range/2", "type": "Range", @@ -17750,9 +17590,6 @@ exports[`Cookbook > Testing normalize %p (%p) 73`] = ` "mapping": { "https://fixtures.iiif.io/video/indiana/donizetti-elixir/vae0637_accessH264_low.mp4": "ContentResource", "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1": "Canvas", - "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=0,302.05": "Canvas", - "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=302.05,3971.24": "Canvas", - "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1#t=3971.24": "Canvas", "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1/annotation_page/1": "AnnotationPage", "https://iiif.io/api/cookbook/recipe/0026-toc-opera/canvas/1/annotation_page/1/annotation/1": "Annotation", "https://iiif.io/api/cookbook/recipe/0026-toc-opera/manifest.json": "Manifest", @@ -20237,6 +20074,13 @@ exports[`Cookbook > Testing normalize %p (%p) 81`] = ` "height": 7230, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p1", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p1", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/page/p1/1", @@ -20271,6 +20115,13 @@ exports[`Cookbook > Testing normalize %p (%p) 81`] = ` "height": 7230, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p2", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p2", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/page/p2/1", @@ -20305,6 +20156,13 @@ exports[`Cookbook > Testing normalize %p (%p) 81`] = ` "height": 7230, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p3", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p3", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/page/p3/1", @@ -20339,6 +20197,13 @@ exports[`Cookbook > Testing normalize %p (%p) 81`] = ` "height": 7230, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p4", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p4", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/page/p4/1", @@ -20373,6 +20238,13 @@ exports[`Cookbook > Testing normalize %p (%p) 81`] = ` "height": 7230, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p5", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p5", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/page/p5/1", @@ -20407,6 +20279,13 @@ exports[`Cookbook > Testing normalize %p (%p) 81`] = ` "height": 7230, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p6", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p6", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/page/p6/1", @@ -20634,21 +20513,31 @@ exports[`Cookbook > Testing normalize %p (%p) 81`] = ` }, }, "Range": { - "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p1": { + "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r0": { "accompanyingCanvas": null, "annotations": [], "behavior": [], "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p1", - "iiif-parser:hasPart": [ + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r0", + "items": [ { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p1", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1", - "type": "Canvas", + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1", + "type": "Range", + }, + { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2", + "type": "Range", + }, + { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3", + "type": "Range", }, ], - "items": [], - "label": null, + "label": { + "de": [ + "Gottesdienstliche Ceremonien, Oder H. Kirchen-Gebräuche Und Religions-Pflichten Der Christen", + ], + }, "metadata": [], "navDate": null, "partOf": [], @@ -20663,24 +20552,45 @@ exports[`Cookbook > Testing normalize %p (%p) 81`] = ` "summary": null, "supplementary": null, "thumbnail": [], - "type": "Canvas", + "type": "Range", "viewingDirection": "left-to-right", }, - "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p2": { + "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1": { "accompanyingCanvas": null, "annotations": [], "behavior": [], "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p2", + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1", "iiif-parser:hasPart": [ { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p2", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1", - "type": "Canvas", + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r0", + "type": "Range", }, ], - "items": [], - "label": null, + "items": [ + { + "selector": undefined, + "source": { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p1", + "type": "Canvas", + }, + "type": "SpecificResource", + }, + { + "selector": undefined, + "source": { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p2", + "type": "Canvas", + }, + "type": "SpecificResource", + }, + ], + "label": { + "en": [ + "Front Matter", + ], + }, "metadata": [], "navDate": null, "partOf": [], @@ -20695,24 +20605,45 @@ exports[`Cookbook > Testing normalize %p (%p) 81`] = ` "summary": null, "supplementary": null, "thumbnail": [], - "type": "Canvas", + "type": "Range", "viewingDirection": "left-to-right", }, - "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p3": { + "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2": { "accompanyingCanvas": null, "annotations": [], "behavior": [], "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p3", + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2", "iiif-parser:hasPart": [ { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p3", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2", - "type": "Canvas", + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r0", + "type": "Range", }, ], - "items": [], - "label": null, + "items": [ + { + "selector": undefined, + "source": { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p3", + "type": "Canvas", + }, + "type": "SpecificResource", + }, + { + "selector": undefined, + "source": { + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p4", + "type": "Canvas", + }, + "type": "SpecificResource", + }, + ], + "label": { + "de": [ + "Erste Ausgabe. Begreift die Ceremonien der Lutheraner von der Augspurgischen Confession, der Reformirten, der Holländischen u. a. Kirchen", + ], + }, "metadata": [], "navDate": null, "partOf": [], @@ -20727,264 +20658,20 @@ exports[`Cookbook > Testing normalize %p (%p) 81`] = ` "summary": null, "supplementary": null, "thumbnail": [], - "type": "Canvas", + "type": "Range", "viewingDirection": "left-to-right", }, - "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p4": { + "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3": { "accompanyingCanvas": null, "annotations": [], "behavior": [], "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p4", + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3", "iiif-parser:hasPart": [ { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p4", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p5": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p5", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p5", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p6": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p6", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p6", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r0": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r0", - "items": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1", - "type": "Range", - }, - { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2", - "type": "Range", - }, - { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3", - "type": "Range", - }, - ], - "label": { - "de": [ - "Gottesdienstliche Ceremonien, Oder H. Kirchen-Gebräuche Und Religions-Pflichten Der Christen", - ], - }, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Range", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r1", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r0", - "type": "Range", - }, - ], - "items": [ - { - "selector": undefined, - "source": { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p1", - "type": "Canvas", - }, - "type": "SpecificResource", - }, - { - "selector": undefined, - "source": { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p2", - "type": "Canvas", - }, - "type": "SpecificResource", - }, - ], - "label": { - "en": [ - "Front Matter", - ], - }, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Range", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r2", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r0", - "type": "Range", - }, - ], - "items": [ - { - "selector": undefined, - "source": { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p3", - "type": "Canvas", - }, - "type": "SpecificResource", - }, - { - "selector": undefined, - "source": { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/canvas/p4", - "type": "Canvas", - }, - "type": "SpecificResource", - }, - ], - "label": { - "de": [ - "Erste Ausgabe. Begreift die Ceremonien der Lutheraner von der Augspurgischen Confession, der Reformirten, der Holländischen u. a. Kirchen", - ], - }, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Range", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r0", - "type": "Range", + "id": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r3", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0031-bound-multivolume/range/r0", + "type": "Range", }, ], "items": [ @@ -23709,33 +23396,319 @@ exports[`Cookbook > Testing normalize %p (%p) 89`] = ` "height": 1080, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", - "items": [ + "iiif-parser:hasPart": [ { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1/annotation_page/1", - "type": "AnnotationPage", - }, - ], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "summary": null, - "thumbnail": [], - "type": "Canvas", - "width": 1920, - }, - }, - "Collection": {}, - "ContentResource": { - "https://fixtures.iiif.io/video/indiana/donizetti-elixir/thumbnails/thumb-nav-01.png": { - "format": "image/png", + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "duration": {}, + "height": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/1.1", + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1/annotation_page/1", + "type": "AnnotationPage", + }, + ], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/2", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/3", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/4", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/5", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/6", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/7", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/8", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/9", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/10", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/11", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + }, + ], + "iiif-parser:isExternal": true, + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1/annotation_page/1", + "type": "AnnotationPage", + }, + ], + "label": null, + "metadata": [], + "navDate": null, + "partOf": [], + "placeholderCanvas": null, + "provider": [], + "rendering": [], + "requiredStatement": null, + "rights": null, + "seeAlso": [], + "service": [], + "summary": null, + "thumbnail": [], + "type": "Canvas", + "width": 1920, + }, + }, + "Collection": {}, + "ContentResource": { + "https://fixtures.iiif.io/video/indiana/donizetti-elixir/thumbnails/thumb-nav-01.png": { + "format": "image/png", "height": 1266, "id": "https://fixtures.iiif.io/video/indiana/donizetti-elixir/thumbnails/thumb-nav-01.png", "iiif-parser:hasPart": [ @@ -23894,390 +23867,30 @@ exports[`Cookbook > Testing normalize %p (%p) 89`] = ` "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/manifest.json ": { "@context": [ "http://iiif.io/api/presentation/3/context.json", - ], - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/manifest.json ", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/manifest.json ", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/manifest.json ", - "type": "Manifest", - }, - ], - "items": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", - "type": "Canvas", - }, - ], - "label": { - "en": [ - "Video navigation with thumbnails in a Range", - ], - }, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "services": [], - "start": null, - "structures": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/1", - "type": "Range", - }, - ], - "summary": null, - "thumbnail": [], - "type": "Manifest", - "viewingDirection": "left-to-right", - }, - }, - "Range": { - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=0,9": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=0,9", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=0,9", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/1.1", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1220,1525": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1220,1525", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1220,1525", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/6", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1525,1830": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1525,1830", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1525,1830", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/7", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1830,2135": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1830,2135", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1830,2135", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/8", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2135,2440": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2135,2440", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2135,2440", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/9", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2440,2745": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2440,2745", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2440,2745", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/10", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2745,3307.22": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2745,3307.22", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2745,3307.22", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/11", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=305,610": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=305,610", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=305,610", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/3", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=610,915": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=610,915", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=610,915", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/4", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=9,305": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=9,305", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=9,305", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/2", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=915,1220": { + ], "accompanyingCanvas": null, "annotations": [], "behavior": [], "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=915,1220", + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/manifest.json ", "iiif-parser:hasPart": [ { - "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=915,1220", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/5", + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/manifest.json ", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/manifest.json ", + "type": "Manifest", + }, + ], + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1", "type": "Canvas", }, ], - "items": [], - "label": null, + "label": { + "en": [ + "Video navigation with thumbnails in a Range", + ], + }, "metadata": [], "navDate": null, "partOf": [], @@ -24288,13 +23901,21 @@ exports[`Cookbook > Testing normalize %p (%p) 89`] = ` "rights": null, "seeAlso": [], "service": [], + "services": [], "start": null, + "structures": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/1", + "type": "Range", + }, + ], "summary": null, - "supplementary": null, "thumbnail": [], - "type": "Canvas", + "type": "Manifest", "viewingDirection": "left-to-right", }, + }, + "Range": { "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/range/1": { "accompanyingCanvas": null, "annotations": [], @@ -24964,17 +24585,6 @@ exports[`Cookbook > Testing normalize %p (%p) 89`] = ` "https://fixtures.iiif.io/video/indiana/donizetti-elixir/thumbnails/thumb-nav-10.png": "ContentResource", "https://fixtures.iiif.io/video/indiana/donizetti-elixir/vae0637_accessH264_low_act_2.mp4": "ContentResource", "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=0,9": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1220,1525": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1525,1830": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=1830,2135": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2135,2440": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2440,2745": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=2745,3307.22": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=305,610": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=610,915": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=9,305": "Canvas", - "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1#t=915,1220": "Canvas", "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1/annotation_page/1": "AnnotationPage", "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/canvas/1/annotation_page/1/annotation/2": "Annotation", "https://iiif.io/api/cookbook/recipe/0229-behavior-ranges/manifest.json ": "Manifest", @@ -26418,6 +26028,97 @@ exports[`Cookbook > Testing normalize %p (%p) 99`] = ` "height": 1080, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1", + "iiif-parser:hasPart": [ + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "duration": {}, + "height": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/range/3", + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1/annotation_page/1", + "type": "AnnotationPage", + }, + ], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": [ + { + "id": "https://fixtures.iiif.io/video/indiana/donizetti-elixir/act1-thumbnail.png", + "type": "ContentResource", + }, + ], + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/range/4", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": [], + "type": "Canvas", + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/range/5", + "items": [], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": [], + "type": "Canvas", + }, + ], + "iiif-parser:isExternal": true, "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1/annotation_page/1", @@ -26569,102 +26270,6 @@ exports[`Cookbook > Testing normalize %p (%p) 99`] = ` }, }, "Range": { - "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=0,302.05": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=0,302.05", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=0,302.05", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/range/3", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=302.05,3971.24": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=302.05,3971.24", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=302.05,3971.24", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/range/4", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=3971.24,7278.422": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=3971.24,7278.422", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=3971.24,7278.422", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/range/5", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/range/1": { "accompanyingCanvas": null, "annotations": [], @@ -26901,9 +26506,6 @@ exports[`Cookbook > Testing normalize %p (%p) 99`] = ` "https://fixtures.iiif.io/video/indiana/donizetti-elixir/vae0637_accessH264_low_act_1.mp4": "ContentResource", "https://fixtures.iiif.io/video/indiana/donizetti-elixir/vae0637_accessH264_low_act_2.mp4": "ContentResource", "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1": "Canvas", - "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=0,302.05": "Canvas", - "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=302.05,3971.24": "Canvas", - "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1#t=3971.24,7278.422": "Canvas", "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1/annotation_page/1": "AnnotationPage", "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1/annotation_page/1/annotation/1": "Annotation", "https://iiif.io/api/cookbook/recipe/0064-opera-one-canvas/canvas/1/annotation_page/1/annotation/2": "Annotation", @@ -27213,6 +26815,71 @@ exports[`Cookbook > Testing normalize %p (%p) 101`] = ` "height": 1080, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1", + "iiif-parser:hasPart": [ + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "duration": {}, + "height": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/range/3", + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1/annotation_page/1", + "type": "AnnotationPage", + }, + ], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": [ + { + "id": "https://fixtures.iiif.io/video/indiana/donizetti-elixir/act1-thumbnail.png", + "type": "ContentResource", + }, + ], + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "homepage": {}, + "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/range/4", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": [], + "type": "Canvas", + }, + ], + "iiif-parser:isExternal": true, "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1/annotation_page/1", @@ -27252,6 +26919,14 @@ exports[`Cookbook > Testing normalize %p (%p) 101`] = ` "height": 1080, "homepage": [], "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/2", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/2", + "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/range/5", + "type": "Canvas", + }, + ], + "iiif-parser:isExternal": true, "items": [ { "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/2/annotation_page/1", @@ -27422,102 +27097,6 @@ exports[`Cookbook > Testing normalize %p (%p) 101`] = ` }, }, "Range": { - "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1#t=0,302.05": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1#t=0,302.05", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1#t=0,302.05", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/range/3", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1#t=302.05,3971.24": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1#t=302.05,3971.24", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1#t=302.05,3971.24", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/range/4", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/2#t=0,3307.22": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/2#t=0,3307.22", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/2#t=0,3307.22", - "iiif-parser:partOf": "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/range/5", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/range/1": { "accompanyingCanvas": null, "annotations": [], @@ -27755,12 +27334,9 @@ exports[`Cookbook > Testing normalize %p (%p) 101`] = ` "https://fixtures.iiif.io/video/indiana/donizetti-elixir/vae0637_accessH264_low_act_1.mp4": "ContentResource", "https://fixtures.iiif.io/video/indiana/donizetti-elixir/vae0637_accessH264_low_act_2.mp4": "ContentResource", "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1": "Canvas", - "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1#t=0,302.05": "Canvas", - "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1#t=302.05,3971.24": "Canvas", "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1/annotation_page/1": "AnnotationPage", "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/1/annotation_page/1/annotation/1": "Annotation", "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/2": "Canvas", - "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/2#t=0,3307.22": "Canvas", "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/2/annotation_page/1": "AnnotationPage", "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/canvas/2/annotation_page/1/annotation/1": "Annotation", "https://iiif.io/api/cookbook/recipe/0065-opera-multiple-canvases/manifest.json": "Manifest", diff --git a/__tests__/presentation-3-parser/__snapshots__/smoke.tests.ts.snap b/__tests__/presentation-3-parser/__snapshots__/smoke.tests.ts.snap index c694541..79ad407 100644 --- a/__tests__/presentation-3-parser/__snapshots__/smoke.tests.ts.snap +++ b/__tests__/presentation-3-parser/__snapshots__/smoke.tests.ts.snap @@ -838,6 +838,14 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/bl-ranges.json 1`] "height": 0, "homepage": [], "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000004", + "iiif-parser:hasPart": [ + { + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000004", + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000d", + "type": "Canvas", + }, + ], + "iiif-parser:isExternal": true, "items": [ { "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000004/anno1", @@ -872,6 +880,14 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/bl-ranges.json 1`] "height": 0, "homepage": [], "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000005", + "iiif-parser:hasPart": [ + { + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000005", + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000e", + "type": "Canvas", + }, + ], + "iiif-parser:isExternal": true, "items": [ { "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000005/anno2", @@ -906,6 +922,172 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/bl-ranges.json 1`] "height": 0, "homepage": [], "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007", + "iiif-parser:hasPart": [ + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "duration": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000c/nn3", + "items": [ + { + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007/anno3", + "type": "AnnotationPage", + }, + ], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000f", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000016", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000016/nn3", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000018", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001b", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + ], + "iiif-parser:isExternal": true, "items": [ { "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007/anno3", @@ -940,6 +1122,224 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/bl-ranges.json 1`] "height": 0, "homepage": [], "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008", + "iiif-parser:hasPart": [ + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "duration": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001b", + "items": [ + { + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008/anno4", + "type": "AnnotationPage", + }, + ], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001b/nn2", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001c", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001c/nn3", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001d", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000024", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000024/nn6", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000026", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + ], + "iiif-parser:isExternal": true, "items": [ { "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008/anno4", @@ -974,6 +1374,198 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/bl-ranges.json 1`] "height": 0, "homepage": [], "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a", + "iiif-parser:hasPart": [ + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "duration": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000028", + "items": [ + { + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a/anno5", + "type": "AnnotationPage", + }, + ], + "label": {}, + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000028/nn8", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00002a", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00002a/nn9", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00002c", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top/nn9", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + { + "@explicit": true, + "accompanyingCanvas": {}, + "annotations": {}, + "behavior": {}, + "height": {}, + "homepage": {}, + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a", + "iiif-parser:isExternal": {}, + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00002e", + "items": [], + "metadata": {}, + "navDate": {}, + "partOf": {}, + "placeholderCanvas": {}, + "provider": {}, + "rendering": {}, + "requiredStatement": {}, + "rights": {}, + "seeAlso": {}, + "service": {}, + "summary": {}, + "thumbnail": {}, + "type": "Canvas", + "width": {}, + }, + ], + "iiif-parser:isExternal": true, "items": [ { "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a/anno5", @@ -1008,6 +1600,14 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/bl-ranges.json 1`] "height": 0, "homepage": [], "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000b", + "iiif-parser:hasPart": [ + { + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000b", + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00002e", + "type": "Canvas", + }, + ], + "iiif-parser:isExternal": true, "items": [ { "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000b/anno6", @@ -2150,821 +2750,39 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/bl-ranges.json 1`] }, { "label": { - "en": [ - "Shelfmark", - ], - }, - "value": { - "en": [ - "C733/48", - ], - }, - }, - { - "label": { - "en": [ - "Link to catalogue", - ], - }, - "value": { - "en": [ - "View the catalogue record", - ], - }, - }, - ], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [ - { - "id": "https://api.bl.uk/item/download/ark:/81055/vdc_100052320369.0x000002/ark:/81055/vdc_100052320369.0x000002/top", - "type": "ContentResource", - }, - ], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Range", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top/nn9": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [ - "no-nav", - ], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top/nn9", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top/nn9", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top", - "type": "Range", - }, - ], - "items": [ - { - "selector": { - "type": "FragmentSelector", - "value": "t=1346.4,1406.32", - }, - "source": { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a", - "type": "Canvas", - }, - "type": "SpecificResource", - }, - ], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Range", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000004#t=0,2023.56": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000004#t=0,2023.56", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000004#t=0,2023.56", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000d", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000005#t=0,1760.04": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000005#t=0,1760.04", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000005#t=0,1760.04", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000e", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=0,0.36": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=0,0.36", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=0,0.36", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000c/nn3", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=0.36,484.8": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=0.36,484.8", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=0.36,484.8", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000f", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=1027,1392.56": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=1027,1392.56", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=1027,1392.56", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001b", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=487.56,526.36": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=487.56,526.36", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=487.56,526.36", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000016", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=526.36,526.64": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=526.36,526.64", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=526.36,526.64", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000016/nn3", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=526.64,1026.48": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=526.64,1026.48", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=526.64,1026.48", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000018", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=0,259.8": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=0,259.8", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=0,259.8", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001b", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=259.8,260": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=259.8,260", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=259.8,260", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001b/nn2", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=260,276.24": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=260,276.24", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=260,276.24", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001c", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=276.24,277.44": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=276.24,277.44", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=276.24,277.44", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001c/nn3", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=277.44,821.36": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=277.44,821.36", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=277.44,821.36", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00001d", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=821.76,992.32": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=821.76,992.32", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=821.76,992.32", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000024", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=992.32,992.96": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=992.32,992.96", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=992.32,992.96", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000024/nn6", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=992.96,1404.92": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=992.96,1404.92", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=992.96,1404.92", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000026", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=0,736": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=0,736", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=0,736", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000028", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1158.84,1159.12": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1158.84,1159.12", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1158.84,1159.12", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00002a/nn9", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1159.12,1346.4": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1159.12,1346.4", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1159.12,1346.4", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00002c", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1346.4,1406.32": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1346.4,1406.32", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1346.4,1406.32", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top/nn9", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1346.52,1406.32": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1346.52,1406.32", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1346.52,1406.32", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00002e", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=736,736.72": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=736,736.72", - "iiif-parser:hasPart": [ - { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=736,736.72", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000028/nn8", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=736.72,1158.84": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=736.72,1158.84", - "iiif-parser:hasPart": [ + "en": [ + "Shelfmark", + ], + }, + "value": { + "en": [ + "C733/48", + ], + }, + }, { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=736.72,1158.84", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00002a", - "type": "Canvas", + "label": { + "en": [ + "Link to catalogue", + ], + }, + "value": { + "en": [ + "View the catalogue record", + ], + }, }, ], - "items": [], - "label": null, - "metadata": [], "navDate": null, "partOf": [], "placeholderCanvas": null, "provider": [], - "rendering": [], + "rendering": [ + { + "id": "https://api.bl.uk/item/download/ark:/81055/vdc_100052320369.0x000002/ark:/81055/vdc_100052320369.0x000002/top", + "type": "ContentResource", + }, + ], "requiredStatement": null, "rights": null, "seeAlso": [], @@ -2973,23 +2791,37 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/bl-ranges.json 1`] "summary": null, "supplementary": null, "thumbnail": [], - "type": "Canvas", + "type": "Range", "viewingDirection": "left-to-right", }, - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000b#t=0,1398.84": { + "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top/nn9": { "accompanyingCanvas": null, "annotations": [], - "behavior": [], + "behavior": [ + "no-nav", + ], "homepage": [], - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000b#t=0,1398.84", + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top/nn9", "iiif-parser:hasPart": [ { - "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000b#t=0,1398.84", - "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00002e", - "type": "Canvas", + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top/nn9", + "iiif-parser:partOf": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top", + "type": "Range", + }, + ], + "items": [ + { + "selector": { + "type": "FragmentSelector", + "value": "t=1346.4,1406.32", + }, + "source": { + "id": "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a", + "type": "Canvas", + }, + "type": "SpecificResource", }, ], - "items": [], "label": null, "metadata": [], "navDate": null, @@ -3005,7 +2837,7 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/bl-ranges.json 1`] "summary": null, "supplementary": null, "thumbnail": [], - "type": "Canvas", + "type": "Range", "viewingDirection": "left-to-right", }, "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000c": { @@ -7025,45 +6857,21 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/bl-ranges.json 1`] "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top": "Range", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000002/top/nn9": "Range", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000004": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000004#t=0,2023.56": "Canvas", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000004/anno1": "AnnotationPage", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000004/anno1/1": "Annotation", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000005": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000005#t=0,1760.04": "Canvas", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000005/anno2": "AnnotationPage", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000005/anno2/1": "Annotation", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=0,0.36": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=0.36,484.8": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=1027,1392.56": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=487.56,526.36": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=526.36,526.64": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007#t=526.64,1026.48": "Canvas", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007/anno3": "AnnotationPage", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000007/anno3/1": "Annotation", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=0,259.8": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=259.8,260": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=260,276.24": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=276.24,277.44": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=277.44,821.36": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=821.76,992.32": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=992.32,992.96": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008#t=992.96,1404.92": "Canvas", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008/anno4": "AnnotationPage", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x000008/anno4/1": "Annotation", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=0,736": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1158.84,1159.12": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1159.12,1346.4": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1346.4,1406.32": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=1346.52,1406.32": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=736,736.72": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a#t=736.72,1158.84": "Canvas", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a/anno5": "AnnotationPage", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000a/anno5/1": "Annotation", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000b": "Canvas", - "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000b#t=0,1398.84": "Canvas", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000b/anno6": "AnnotationPage", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000b/anno6/1": "Annotation", "https://api.bl.uk/metadata/iiif/ark:/81055/vdc_100052320369.0x00000c": "Range", @@ -11508,6 +11316,13 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/wellcome-p3.json 1` "height": 4412, "homepage": [], "id": "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0001.jp2", + "iiif-parser:hasPart": [ + { + "id": "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0001.jp2", + "iiif-parser:partOf": "https://iiif-test.wellcomecollection.org/presentation/b28857021/ranges/LOG_0001", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0001.jp2/painting", @@ -11949,6 +11764,13 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/wellcome-p3.json 1` "height": 4424, "homepage": [], "id": "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0010.jp2", + "iiif-parser:hasPart": [ + { + "id": "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0010.jp2", + "iiif-parser:partOf": "https://iiif-test.wellcomecollection.org/presentation/b28857021/ranges/LOG_0002", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0010.jp2/painting", @@ -13605,70 +13427,6 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/wellcome-p3.json 1` }, }, "Range": { - "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0001.jp2": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0001.jp2", - "iiif-parser:hasPart": [ - { - "id": "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0001.jp2", - "iiif-parser:partOf": "https://iiif-test.wellcomecollection.org/presentation/b28857021/ranges/LOG_0001", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0010.jp2": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0010.jp2", - "iiif-parser:hasPart": [ - { - "id": "https://iiif-test.wellcomecollection.org/presentation/b28857021/canvases/b28857021_0010.jp2", - "iiif-parser:partOf": "https://iiif-test.wellcomecollection.org/presentation/b28857021/ranges/LOG_0002", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, "https://iiif-test.wellcomecollection.org/presentation/b28857021/ranges/LOG_0001": { "accompanyingCanvas": null, "annotations": [], @@ -17120,6 +16878,13 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/wellcome-p3-2.json "height": 3543, "homepage": [], "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0001.JP2", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0001.JP2", + "iiif-parser:partOf": "https://iiif.wellcomecollection.org/presentation/b18035723/ranges/LOG_0001", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0001.JP2/painting", @@ -17169,6 +16934,13 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/wellcome-p3-2.json "height": 3040, "homepage": [], "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0002.JP2", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0002.JP2", + "iiif-parser:partOf": "https://iiif.wellcomecollection.org/presentation/b18035723/ranges/LOG_0002", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0002.JP2/painting", @@ -17267,6 +17039,13 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/wellcome-p3-2.json "height": 3372, "homepage": [], "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0004.JP2", + "iiif-parser:hasPart": [ + { + "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0004.JP2", + "iiif-parser:partOf": "https://iiif.wellcomecollection.org/presentation/b18035723/ranges/LOG_0003", + "type": "Canvas", + }, + ], "items": [ { "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0004.JP2/painting", @@ -22439,102 +22218,6 @@ exports[`Smoke tests > Smoke test: ./fixtures/presentation-3/wellcome-p3-2.json }, }, "Range": { - "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0001.JP2": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0001.JP2", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0001.JP2", - "iiif-parser:partOf": "https://iiif.wellcomecollection.org/presentation/b18035723/ranges/LOG_0001", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0002.JP2": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0002.JP2", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0002.JP2", - "iiif-parser:partOf": "https://iiif.wellcomecollection.org/presentation/b18035723/ranges/LOG_0002", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, - "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0004.JP2": { - "accompanyingCanvas": null, - "annotations": [], - "behavior": [], - "homepage": [], - "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0004.JP2", - "iiif-parser:hasPart": [ - { - "id": "https://iiif.wellcomecollection.org/presentation/b18035723/canvases/b18035723_0004.JP2", - "iiif-parser:partOf": "https://iiif.wellcomecollection.org/presentation/b18035723/ranges/LOG_0003", - "type": "Canvas", - }, - ], - "items": [], - "label": null, - "metadata": [], - "navDate": null, - "partOf": [], - "placeholderCanvas": null, - "provider": [], - "rendering": [], - "requiredStatement": null, - "rights": null, - "seeAlso": [], - "service": [], - "start": null, - "summary": null, - "supplementary": null, - "thumbnail": [], - "type": "Canvas", - "viewingDirection": "left-to-right", - }, "https://iiif.wellcomecollection.org/presentation/b18035723/ranges/LOG_0001": { "accompanyingCanvas": null, "annotations": [], diff --git a/__tests__/presentation-3-parser/range-selectors.test.ts b/__tests__/presentation-3-parser/range-selectors.test.ts new file mode 100644 index 0000000..75f5934 --- /dev/null +++ b/__tests__/presentation-3-parser/range-selectors.test.ts @@ -0,0 +1,43 @@ +import { normalize, serialize, serializeConfigPresentation3 } from "../../src"; +import rangeManifest from "../../fixtures/cookbook/0065-opera-multiple-canvases.json"; + +const canvasId = "https://example.org/canvas"; +const fragment = "xywh=0,1,2,3"; + +describe("range selectors (#3)", () => { + test.each([ + `${canvasId}#${fragment}`, + { id: `${canvasId}#${fragment}`, type: "Canvas" }, + { type: "SpecificResource", source: canvasId, selector: { type: "FragmentSelector", value: fragment } }, + { + type: "SpecificResource", + source: { id: canvasId, type: "Canvas" }, + selector: { type: "FragmentSelector", value: fragment }, + }, + ])("keeps a canonical canvas reference for %j", (item) => { + const result = normalize({ id: "https://example.org/range", type: "Range", items: [structuredClone(item)] }); + const range = (result.entities.Range as any)[result.resource.id]; + expect(range.items[0]).toEqual({ + type: "SpecificResource", + source: { id: canvasId, type: "Canvas" }, + selector: { type: "FragmentSelector", value: fragment }, + }); + expect((result.entities.Canvas as any)[canvasId]).toBeDefined(); + expect((result.mapping as any)[`${canvasId}#${fragment}`]).toBeUndefined(); + const output = serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); + expect(output.items).toEqual([{ id: `${canvasId}#${fragment}`, type: "Canvas" }]); + }); + + test("preserves temporal canvas selectors from the opera cookbook manifest", () => { + const result = normalize(structuredClone(rangeManifest)); + expect(Object.keys(result.entities.Canvas).every((id) => !id.includes("#t="))).toBe(true); + const output = serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); + expect(output.structures).toEqual(rangeManifest.structures); + }); + test("keeps unrelated anchor names intact", () => { + const id = `${canvasId}#part=2`; + const result = normalize({ id: "https://example.org/range", type: "Range", items: [id] }); + expect((result.entities.Canvas as any)[id]).toBeDefined(); + expect((result.entities.Range as any)[result.resource.id].items[0].selector).toBeUndefined(); + }); +}); diff --git a/src/presentation-3/traverse.ts b/src/presentation-3/traverse.ts index b2d6946..62b8bf0 100644 --- a/src/presentation-3/traverse.ts +++ b/src/presentation-3/traverse.ts @@ -22,6 +22,7 @@ import type { } from "./types"; import type { GeoJSON } from "../shared/geojson"; import { isSpecificResource } from "../shared/is-specific-resource"; +import { splitCanvasFragment } from "../shared/canvas-fragments"; import { ensureArray } from "../shared/ensure-array"; import { compose } from "../shared/compose"; @@ -466,8 +467,19 @@ export class Traverse { traverseRangeRanges(range: Range): Range { if (range.items) { range.items = range.items.map((rangeOrManifest: RangeItems) => { - if (typeof rangeOrManifest === "string") { - return this.traverseCanvas({ id: rangeOrManifest, type: "Canvas" }, range); + if (typeof rangeOrManifest === "string" || rangeOrManifest.type === "Canvas") { + const canvas = typeof rangeOrManifest === "string" + ? { id: rangeOrManifest, type: "Canvas" as const } + : rangeOrManifest; + const [id, fragment] = splitCanvasFragment(canvas.id); + if (fragment) { + return this.traverseSpecificResource({ + type: "SpecificResource", + source: { ...canvas, id }, + selector: { type: "FragmentSelector", value: fragment }, + } as SpecificResource, "Canvas", range); + } + return this.traverseCanvas(canvas as Canvas, range); } if (isSpecificResource(rangeOrManifest)) { return this.traverseSpecificResource(rangeOrManifest, "Canvas", range); diff --git a/src/shared/canvas-fragments.ts b/src/shared/canvas-fragments.ts index 014aedb..58cf856 100644 --- a/src/shared/canvas-fragments.ts +++ b/src/shared/canvas-fragments.ts @@ -7,5 +7,5 @@ export function splitCanvasFragment(originalUrl?: string): string[] { } export function isValidCanvasFragment(fragment: string): boolean { - return fragment.includes("xywh=") || fragment.includes("t="); + return /(?:^|&)(?:xywh|t)=/.test(fragment); } From 93b9358da7b1049b5999e77b2cc7c90d736f8957 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:51:02 +0100 Subject: [PATCH 10/19] Upgrade selected annotation bodies without losing their source Completes #27 for oa:SpecificResource bodies. Preserve image operations and extension selectors while retaining strict target handling. --- .../specific-resource-body.test.ts | 37 +++++++++++++++++++ .../regressions/p2-image-selector-body.json | 29 +++++++++++++++ src/presentation-2/upgrader.ts | 24 ++++++++++-- 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 __tests__/presentation-2-parser/specific-resource-body.test.ts create mode 100644 fixtures/regressions/p2-image-selector-body.json diff --git a/__tests__/presentation-2-parser/specific-resource-body.test.ts b/__tests__/presentation-2-parser/specific-resource-body.test.ts new file mode 100644 index 0000000..bdf6128 --- /dev/null +++ b/__tests__/presentation-2-parser/specific-resource-body.test.ts @@ -0,0 +1,37 @@ +import annotation from "../../fixtures/regressions/p2-image-selector-body.json"; +import { convertPresentation2 } from "../../src/presentation-2"; +import { normalize, serialize, serializeConfigPresentation3 } from "../../src"; + +// P2 form of the rotation example: https://iiif.io/api/annex/openannotation/#iiif-image-api-selector +test("upgrades specific resource bodies without losing the source or selector (#27)", () => { + const upgraded = convertPresentation2(structuredClone(annotation)) as any; + expect(upgraded.body).toMatchObject({ + type: "SpecificResource", + source: { + id: annotation.resource.full["@id"], + type: "Image", + label: { none: ["Image to rotate"] }, + service: [{ "@id": annotation.resource.full.service["@id"], "@type": "ImageService2" }], + }, + selector: { + type: "ImageApiSelector", + region: "pct:0,0,10,10", + size: "200,", + rotation: "90", + quality: "gray", + format: "png", + }, + }); + const result = normalize(structuredClone(upgraded)); + const output = serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); + expect(output.body.source.id).toBe(annotation.resource.full["@id"]); + expect(output.body.selector).toEqual(upgraded.body.selector); +}); + +test("retains extension selectors in bodies without rejecting the document", () => { + const input = structuredClone(annotation) as any; + input.resource.selector = { "@type": "oa:TextQuoteSelector", exact: "A quotation" }; + const output = convertPresentation2(input) as any; + expect(output.body.source.id).toBe(annotation.resource.full["@id"]); + expect(output.body.selector).toEqual({ type: "TextQuoteSelector", exact: "A quotation" }); +}); diff --git a/fixtures/regressions/p2-image-selector-body.json b/fixtures/regressions/p2-image-selector-body.json new file mode 100644 index 0000000..54bcd2e --- /dev/null +++ b/fixtures/regressions/p2-image-selector-body.json @@ -0,0 +1,29 @@ +{ + "@context": "http://iiif.io/api/presentation/2/context.json", + "@id": "https://example.org/annotation/rotation", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "on": "https://example.org/canvas/rotation", + "resource": { + "@type": "oa:SpecificResource", + "full": { + "@id": "https://example.org/iiif/image1", + "@type": "dctypes:Image", + "label": "Image to rotate", + "service": { + "@id": "https://example.org/iiif/image1", + "@context": "http://iiif.io/api/image/2/context.json", + "profile": "http://iiif.io/api/image/2/level1.json" + } + }, + "selector": { + "@context": "http://iiif.io/api/annex/openannotation/context.json", + "@type": "iiif:ImageApiSelector", + "region": "pct:0,0,10,10", + "size": "200,", + "rotation": "90", + "quality": "gray", + "format": "png" + } + } +} diff --git a/src/presentation-2/upgrader.ts b/src/presentation-2/upgrader.ts index b710cc0..e32995e 100644 --- a/src/presentation-2/upgrader.ts +++ b/src/presentation-2/upgrader.ts @@ -834,13 +834,24 @@ function upgradeAnnotation(annotation: Presentation2.Annotation): Presentation3. function upgradeContentResourceOrChoice( resource: Presentation2.ContentResource | Presentation2.ChoiceEmbeddedContent ): Presentation3.ContentResource | Presentation3.ChoiceBody { - if ((resource as any).type === "Choice") { + if ((resource as any).type === "Choice" || (resource as any).type === "SpecificResource") { return resource as any; } return upgradeContentResource(resource); } function upgradeContentResource(inputContentResource: Presentation2.ContentResource): Presentation3.ContentResource { + if (inputContentResource["@type"] === "oa:SpecificResource" && "full" in inputContentResource) { + const specific = inputContentResource as Presentation2.ImageResourceSegmentWithService; + return removeUndefinedProperties({ + id: specific["@id"], + type: "SpecificResource", + source: presentation2to3.traverseContentResource(specific.full), + selector: specific.selector ? upgradeSelector(specific.selector, true) : undefined, + styleClass: specific.style, + }) as Presentation3.ContentResource; + } + const contentResource = (typeof inputContentResource === "string" ? { "@id": inputContentResource } : inputContentResource) as Presentation2.CommonContentResource; @@ -973,7 +984,8 @@ export function convertPresentation2(entity: any): Presentation3.Manifest | Pres } function upgradeSelector( - selector: Presentation2.ContentResourceSelector + selector: Presentation2.ContentResourceSelector, + preserveUnknown = false ): Presentation3.Selector | Presentation3.Selector[] { const isSvgSelector = ((Array.isArray(selector["@type"]) && selector["@type"].includes("oa:SvgSelector")) || @@ -993,9 +1005,9 @@ function upgradeSelector( } if (selector["@type"] === "oa:Choice") { return [ - upgradeSelector(selector.default) as Presentation3.Selector, + upgradeSelector(selector.default, preserveUnknown) as Presentation3.Selector, ...((Array.isArray(selector.item) ? selector.item : [selector.item]).map( - upgradeSelector + (item) => upgradeSelector(item, preserveUnknown) ) as Presentation3.Selector[]), ]; } @@ -1009,5 +1021,9 @@ function upgradeSelector( format: selector.format, }; } + if (preserveUnknown) { + const { "@type": type, ...properties } = selector as any; + return { ...properties, type: typeof type === "string" ? removePrefix(type) : type }; + } throw new Error(`Unsupported selector type: ${selector["@type"]}`); } From 1d057add532a31c29c0cb11822caad9f376a889d Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 10:55:46 +0100 Subject: [PATCH 11/19] Convert legacy audio and video manifests to temporal canvases Fixes #14. Use real Wellcome IxIF audio and video examples, preserve playable alternatives, and retain fallback content when media cannot be converted. --- .../presentation-2-parser/legacy-av.test.ts | 54 +++++++ .../presentation-2/wellcome-ixif-video.json | 138 ++++++++++++++++++ .../types/legacy/resources/manifest.d.ts | 7 + src/presentation-2/upgrader.ts | 110 +++++++++++--- 4 files changed, 292 insertions(+), 17 deletions(-) create mode 100644 __tests__/presentation-2-parser/legacy-av.test.ts create mode 100644 fixtures/presentation-2/wellcome-ixif-video.json diff --git a/__tests__/presentation-2-parser/legacy-av.test.ts b/__tests__/presentation-2-parser/legacy-av.test.ts new file mode 100644 index 0000000..ee9da51 --- /dev/null +++ b/__tests__/presentation-2-parser/legacy-av.test.ts @@ -0,0 +1,54 @@ +import audio from "../../fixtures/presentation-2/wellcome-ixif-audio.json"; +import video from "../../fixtures/presentation-2/wellcome-ixif-video.json"; +import { convertPresentation2 } from "../../src/presentation-2"; +import { normalize, serialize, serializeConfigPresentation3 } from "../../src"; + +describe("legacy IxIF audio and video (#14)", () => { + test.each([ + [audio, "Sound", 74.057144], + [video, "Video", 1180], + ] as const)("upgrades the real Wellcome $1 manifest", (input, type, duration) => { + const upgraded = convertPresentation2(structuredClone(input)) as any; + expect(upgraded.items).toHaveLength(1); + const canvas = upgraded.items[0]; + expect(canvas).toMatchObject({ type: "Canvas", duration }); + const annotation = canvas.items[0].items[0]; + expect(annotation).toMatchObject({ type: "Annotation", motivation: "painting", target: canvas.id }); + const bodies = annotation.body.type === "Choice" ? annotation.body.items : [annotation.body]; + expect(bodies.every((body: any) => body.type === type && body.duration === duration)).toBe(true); + expect(bodies.map((body: any) => body.id)).toEqual( + [input.mediaSequences[0]!.elements[0]!.rendering].flat().map((body) => body["@id"]) + ); + expect(bodies.every((body: any) => body.id !== canvas.id)).toBe(true); + expect(canvas.thumbnail[0].id).toBe(input.mediaSequences[0]!.elements[0]!.thumbnail); + if (type === "Video") expect(canvas).toMatchObject({ width: 720, height: 720 }); + const result = normalize(structuredClone(upgraded)); + const output = serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); + expect(output.items[0].duration).toBe(duration); + expect(output.items[0].items[0].items[0].body).toMatchObject({ type: annotation.body.type }); + }); + + test("retains real image sequences and keeps the placeholder for unsupported media", () => { + const input = structuredClone(audio) as any; + input.sequences.push({ + "@type": "sc:Sequence", + canvases: [{ "@id": "https://example.org/cover", "@type": "sc:Canvas", label: "Cover", width: 1, height: 1 }], + }); + expect((convertPresentation2(structuredClone(input)) as any).items.map((item: any) => item.id)).toContain( + "https://example.org/cover" + ); + input.mediaSequences[0].elements[0]["@type"] = "foaf:Document"; + input.mediaSequences[0].elements[0].format = "application/pdf"; + const unsupported = convertPresentation2(input) as any; + expect(unsupported.items).toHaveLength(2); + expect(unsupported.items[0].id).toContain("ixif-message"); + }); + + test("does not invent a duration when the media length is unavailable", () => { + const input = structuredClone(audio) as any; + input.mediaSequences[0].elements[0].metadata = []; + const output = convertPresentation2(input) as any; + expect(output.items[0].id).toContain("ixif-message"); + expect(output.items[0].duration).toBeUndefined(); + }); +}); diff --git a/fixtures/presentation-2/wellcome-ixif-video.json b/fixtures/presentation-2/wellcome-ixif-video.json new file mode 100644 index 0000000..885430a --- /dev/null +++ b/fixtures/presentation-2/wellcome-ixif-video.json @@ -0,0 +1,138 @@ +{ + "@context": [ + "http://wellcomelibrary.org/ld/ixif/0/context.json", + "http://iiif.io/api/presentation/2/context.json" + ], + "@id": "https://iiif.wellcomecollection.org/presentation/v2/b24744669", + "@type": "sc:Manifest", + "label": "The antibiotics and terramycin.", + "metadata": [ + { + "label": "Description", + "value": "

A well articulated and structured history of antibiotics with examples from nature about the value of bacteria and molds. It has high production values such as stock footage and good quality animations. The action of antibiotics is described despite the rise of diseases illustrated via animation. Sulfonamides, then bacitracin, streptomycin and polymyxin were all developed as antigonists to diseases. The film outlines the desired therapeutic properties of antibiotics and discusses their application in the treatment of disease. Chemical and therapeutic properties of terramycin are explained in detail. The film is optimistic about the ongoing development of new and effective antibiotics. A male doctor is shown examining a small boy, then putting his stethoscope away in his medical case.

" + }, + { + "label": "Physical description", + "value": "1 encoded moving image (19:40 min.) : sound, colour." + }, + { + "label": "Contributors", + "value": "Basilaukas, V. F.; Churchill Productions" + }, + { + "label": "Copyright note", + "value": "Pfizer Inc" + }, + { + "label": "Creator/production credits", + "value": "Dr V. F. Basilaukas and Churchill Productions for Pfizer Ltd" + }, + { + "label": "Subjects", + "value": "Pharmacology; Anti-Bacterial Agents; Oxytetracycline; Sulfonamides" + }, + { + "label": "Attribution", + "value": "Wellcome Collection" + }, + { + "label": "Full conditions of use", + "value": "You have permission to make copies of this work under a Creative Commons, Attribution, Non-commercial license.

Non-commercial use includes private study, academic research, teaching, and other activities that are not primarily intended for, or directed towards, commercial advantage or private monetary compensation. See the Legal Code for further information.

Image source should be attributed as specified in the full catalogue record. If no source is given the image should be attributed to Wellcome Collection.
" + } + ], + "description": "

A well articulated and structured history of antibiotics with examples from nature about the value of bacteria and molds. It has high production values such as stock footage and good quality animations. The action of antibiotics is described despite the rise of diseases illustrated via animation. Sulfonamides, then bacitracin, streptomycin and polymyxin were all developed as antigonists to diseases. The film outlines the desired therapeutic properties of antibiotics and discusses their application in the treatment of disease. Chemical and therapeutic properties of terramycin are explained in detail. The film is optimistic about the ongoing development of new and effective antibiotics. A male doctor is shown examining a small boy, then putting his stethoscope away in his medical case.

", + "license": "http://creativecommons.org/licenses/by-nc/4.0/", + "logo": "https://iiif.wellcomecollection.org/logos/wellcome-collection-black.png", + "related": { + "@id": "https://wellcomecollection.org/works/m2raqspe", + "format": "text/html", + "label": "The antibiotics and terramycin." + }, + "seeAlso": { + "@id": "https://api.wellcomecollection.org/catalogue/v2/works/m2raqspe", + "profile": "https://api.wellcomecollection.org/catalogue/v2/context.json", + "format": "application/json", + "label": "Wellcome Collection Catalogue API" + }, + "service": [ + { + "@context": "http://universalviewer.io/context.json", + "@id": "http://wellcomelibrary.org/service/trackingLabels/b24744669", + "profile": "http://universalviewer.io/tracking-extensions-profile", + "trackingLabel": "Format: Video, Institution: n/a, Identifier: b24744669, Digicode: digfilm, Collection code: n/a" + }, + { + "@context": "http://wellcomelibrary.org/ld/iiif-ext/0/context.json", + "@id": "https://wellcomelibrary.org/iiif/b24744669/access-control-hints-service", + "profile": "http://wellcomelibrary.org/ld/iiif-ext/access-control-hints", + "accessHint": "open" + } + ], + "mediaSequences": [ + { + "@id": "https://iiif.wellcomecollection.org/iiif/b24744669/xsequence/s0", + "@type": "ixif:MediaSequence", + "label": "XSequence 0", + "elements": [ + { + "@id": "https://iiif.wellcomecollection.org/av/b24744669_0055-0000-7653-0000-0-0000-0000-0.mpg/full/full/max/max/0/default.mp4#identity", + "@type": "dctypes:MovingImage", + "format": "video/mp4", + "label": "The antibiotics and terramycin.", + "metadata": [ + { + "label": "length", + "value": "1180 s" + } + ], + "thumbnail": "https://iiif.wellcomecollection.org/thumb/b24744669", + "rendering": [ + { + "@id": "https://iiif.wellcomecollection.org/av/b24744669_0055-0000-7653-0000-0-0000-0000-0.mpg/full/full/max/max/0/default.mp4", + "format": "video/mp4" + }, + { + "@id": "https://iiif.wellcomecollection.org/av/b24744669_0055-0000-7653-0000-0-0000-0000-0.mpg/full/full/max/max/0/default.webm", + "format": "video/webm" + } + ], + "width": 720, + "height": 720 + } + ] + } + ], + "sequences": [ + { + "@id": "https://wellcomecollection.org/iiif/ixif-message/sequence/seq", + "@type": "sc:Sequence", + "label": "Unsupported extension. This manifest is being used as a wrapper for non-IIIF content (e.g., audio, video) and is unfortunately incompatible with IIIF viewers.", + "compatibilityHint": "displayIfContentUnsupported", + "canvases": [ + { + "@id": "https://wellcomelibrary.org/iiif/ixif-message/canvas/c1", + "@type": "sc:Canvas", + "label": "Placeholder image", + "thumbnail": "https://wellcomecollection.org/placeholder.jpg", + "height": 600, + "width": 600, + "images": [ + { + "@id": "https://wellcomecollection.org/iiif/ixif-message/imageanno/placeholder", + "@type": "oa:Annotation", + "motivation": "sc:painting", + "resource": { + "@id": "https://wellcomecollection.org/iiif/ixif-message-0/res/placeholder", + "@type": "dctypes:Image", + "height": 600, + "width": 600 + }, + "on": "https://wellcomecollection.org/iiif/ixif-message/canvas/c1" + } + ] + } + ] + } + ], + "within": "https://iiif.wellcomecollection.org/presentation/v2/collections/contributors/Basilaukas,_V._F." +} \ No newline at end of file diff --git a/src/presentation-2/types/legacy/resources/manifest.d.ts b/src/presentation-2/types/legacy/resources/manifest.d.ts index 870bdc9..e833b63 100644 --- a/src/presentation-2/types/legacy/resources/manifest.d.ts +++ b/src/presentation-2/types/legacy/resources/manifest.d.ts @@ -5,6 +5,7 @@ import { Sequence } from "./sequence"; import { LinkingProperties } from "../iiif/linking"; import { Range } from "./range"; import { RightsProperties } from "../iiif/rights"; +import { ContentResource } from "./content-resource"; type ManifestOmittedTechnical = "format" | "height" | "width"; type ManifestOmittedLinking = "startCanvas"; @@ -18,6 +19,12 @@ export type ManifestStructural = { */ sequences: Sequence[]; structures?: Range[]; + /** Legacy IxIF media sequences, upgraded to temporal canvases when their duration is known. */ + mediaSequences?: Array<{ + "@id"?: string; + "@type"?: "ixif:MediaSequence"; + elements: Array & { duration?: number }>; + }>; }; type ManifestLinking = OmitProperties; diff --git a/src/presentation-2/upgrader.ts b/src/presentation-2/upgrader.ts index e32995e..f6259f5 100644 --- a/src/presentation-2/upgrader.ts +++ b/src/presentation-2/upgrader.ts @@ -206,6 +206,8 @@ function getNewType(resource: any): string { } switch (oldType) { + case "MovingImage": + return "Video"; case "Layer": return "AnnotationCollection"; case "AnnotationList": @@ -221,6 +223,8 @@ function getNewType(resource: any): string { } if (resource.format) { + if (resource.format.startsWith("audio/")) return "Sound"; + if (resource.format.startsWith("video/")) return "Video"; if (resource.format.startsWith("image/")) { return "Image"; } @@ -514,17 +518,16 @@ function linkingProperties(resource: Presentation2.LinkingProperties & Presentat const layer = resource.contentLayer as Presentation2.Layer; return { - provider: - resource.logo - ? [ - { - id: configuration.providerId, - type: "Agent" as const, - logo: resource.logo ? (Array.isArray(resource.logo) ? resource.logo : [resource.logo]) : undefined, - label: convertLanguageMapping(configuration.providerName), - }, - ] - : undefined, + provider: resource.logo + ? [ + { + id: configuration.providerId, + type: "Agent" as const, + logo: resource.logo ? (Array.isArray(resource.logo) ? resource.logo : [resource.logo]) : undefined, + label: convertLanguageMapping(configuration.providerName), + }, + ] + : undefined, homepage: related.length ? related.map((link: any) => ({ ...link, type: link.type === "unknown" ? "Text" : link.type })) : undefined, @@ -652,10 +655,18 @@ function flattenArray(array: T[][]): T[] { function upgradeManifest(manifest: Presentation2.Manifest): Presentation3.Manifest { const allCanvases = []; + const mediaElements = (manifest.mediaSequences || []).flatMap((sequence) => sequence.elements || []); + const mediaCanvases = upgradeMediaElements(manifest, mediaElements); const behavior = []; let start = undefined; let viewingDirection = undefined; for (const sequence of manifest.sequences || []) { + if ( + mediaCanvases.length && + mediaCanvases.length === mediaElements.length && + (sequence as any).compatibilityHint === "displayIfContentUnsupported" + ) + continue; if (sequence.canvases.length) { allCanvases.push(...sequence.canvases); } @@ -686,11 +697,74 @@ function upgradeManifest(manifest: Presentation2.Manifest): Presentation3.Manife ...linkingProperties(manifest), viewingDirection, start: start, - items: allCanvases, + items: [...allCanvases, ...mediaCanvases], structures: flattenStructures(manifest.structures as any), }); } +function upgradeMediaElements( + manifest: Presentation2.Manifest, + elements: NonNullable[number]["elements"] +): Presentation3.Canvas[] { + const canvases: Presentation3.Canvas[] = []; + for (const [index, element] of elements.entries()) { + const type = getNewType(element); + if (type !== "Sound" && type !== "Video") continue; + const length = element.duration ?? element.metadata?.find((entry) => entry.label === "length")?.value; + const match = String(length ?? "") + .trim() + .match(/^(?:(\d+(?:\.\d+)?)\s*h\s*)?(?:(\d+(?:\.\d+)?)\s*(?:mn|min|m)\s*)?(?:(\d+(?:\.\d+)?)\s*s?)?$/); + const duration = match ? Number(match[1] || 0) * 3600 + Number(match[2] || 0) * 60 + Number(match[3] || 0) : 0; + if (!Number.isFinite(duration) || duration <= 0) continue; + if (type === "Video" && (!(element as any).width || !(element as any).height)) continue; + + const upgraded = presentation2to3.traverseContentResource(structuredClone(element) as any) as any; + const renderings = (upgraded.rendering || []).filter((resource: any) => + resource.format?.startsWith(type === "Sound" ? "audio/" : "video/") + ); + const bodies = (renderings.length ? renderings : [upgraded]).map((resource: any) => + removeUndefinedProperties({ + ...resource, + type, + duration, + width: upgraded.width, + height: upgraded.height, + label: resource.label || upgraded.label, + }) + ); + const id = `${manifest["@id"]}/canvas/av-${index + 1}`; + canvases.push( + removeUndefinedProperties({ + id, + type: "Canvas", + label: upgraded.label, + duration, + width: upgraded.width, + height: upgraded.height, + thumbnail: upgraded.thumbnail, + metadata: upgraded.metadata, + rendering: upgraded.rendering, + items: [ + { + id: `${id}/page`, + type: "AnnotationPage", + items: [ + { + id: `${id}/annotation`, + type: "Annotation", + motivation: "painting", + target: id, + body: bodies.length === 1 ? bodies[0] : { type: "Choice", items: bodies }, + }, + ], + }, + ], + }) as Presentation3.Canvas + ); + } + return canvases; +} + function flattenStructures(structures: Presentation3.Range[]): Presentation3.Range[] { if (!structures) { return structures; @@ -755,6 +829,7 @@ function upgradeSequence(sequence: Presentation2.Sequence): { behavior?: string[]; startCanvas?: Presentation3.Reference<"Canvas"> | undefined; viewingDirection?: "left-to-right" | "right-to-left" | "top-to-bottom" | "bottom-to-top"; + compatibilityHint?: string; } { /* rng = {"id": s.get('@id', self.mint_uri()), "type": "Range"} @@ -786,6 +861,7 @@ function upgradeSequence(sequence: Presentation2.Sequence): { behavior: sequence.viewingHint ? [sequence.viewingHint] : [], viewingDirection: sequence.viewingDirection, startCanvas: sequence.startCanvas as any, + ...((sequence as any).compatibilityHint ? { compatibilityHint: (sequence as any).compatibilityHint } : {}), }; } @@ -852,9 +928,9 @@ function upgradeContentResource(inputContentResource: Presentation2.ContentResou }) as Presentation3.ContentResource; } - const contentResource = (typeof inputContentResource === "string" - ? { "@id": inputContentResource } - : inputContentResource) as Presentation2.CommonContentResource; + const contentResource = ( + typeof inputContentResource === "string" ? { "@id": inputContentResource } : inputContentResource + ) as Presentation2.CommonContentResource; // @todo there might be some field dropped here. return removeUndefinedProperties({ @@ -1006,8 +1082,8 @@ function upgradeSelector( if (selector["@type"] === "oa:Choice") { return [ upgradeSelector(selector.default, preserveUnknown) as Presentation3.Selector, - ...((Array.isArray(selector.item) ? selector.item : [selector.item]).map( - (item) => upgradeSelector(item, preserveUnknown) + ...((Array.isArray(selector.item) ? selector.item : [selector.item]).map((item) => + upgradeSelector(item, preserveUnknown) ) as Presentation3.Selector[]), ]; } From f37c2fb048cf85f9b73261689e0d59211a815962 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 11:02:23 +0100 Subject: [PATCH 12/19] Serialize Presentation 2 annotations without dropping content Fixes #49. Preserve bodies, motivations and selectors; emit flat painting annotations, annotation lists and reference-form collection members. --- .../p2-annotations.test.ts | 81 ++++++++++++ .../serialize-presentation-2.ts | 122 +++++++++++++++--- 2 files changed, 184 insertions(+), 19 deletions(-) create mode 100644 __tests__/presentation-3-parser/p2-annotations.test.ts diff --git a/__tests__/presentation-3-parser/p2-annotations.test.ts b/__tests__/presentation-3-parser/p2-annotations.test.ts new file mode 100644 index 0000000..3f0bca5 --- /dev/null +++ b/__tests__/presentation-3-parser/p2-annotations.test.ts @@ -0,0 +1,81 @@ +import { normalize, serialize, serializeConfigPresentation2 } from "../../src"; +import annotationList from "../../fixtures/presentation-2/iiif-fixture-annotation-list.json"; +import manifest from "../../fixtures/presentation-2/iiif-fixture-manifest.json"; +import selectedImage from "../../fixtures/regressions/p2-image-selector-body.json"; + +function asPresentation2(input: any): any { + const result = normalize(structuredClone(input)); + return serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation2); +} + +describe("Presentation 2 Open Annotation serialization (#49)", () => { + test("preserves text, motivations and selectors in the official annotation list", () => { + const output = asPresentation2(annotationList); + expect(output["@type"]).toBe("sc:AnnotationList"); + expect(Array.isArray(output.resources)).toBe(true); + expect(output.resources).toHaveLength(2); + expect(output.resources[0].resource).toMatchObject({ + "@type": "cnt:ContentAsText", + chars: "Top of First Page to Display", + }); + expect(output.resources[0].on).toBe(`${(annotationList as any).resources[0].on.full["@id"]}#xywh=225,70,750,150`); + expect(output.resources[1].motivation).toEqual(["oa:tagging", "oa:commenting"]); + expect(output.resources[1].resource.map((body: any) => body.chars)).toEqual( + (annotationList as any).resources[1].resource.map((body: any) => body.chars) + ); + expect(output.resources[1].on).toMatchObject({ + "@type": "oa:SpecificResource", + selector: { "@type": "oa:Choice" }, + }); + expect(JSON.stringify(output)).not.toContain("vault://"); + }); + + test("serializes painting pages as flat images arrays and external lists as otherContent", () => { + const input = structuredClone(manifest) as any; + input.sequences[0].canvases[0].otherContent = [ + { "@id": "https://example.org/comments", "@type": "sc:AnnotationList" }, + ]; + const output = asPresentation2(input); + const canvas = output.sequences[0].canvases[0]; + expect(Array.isArray(canvas.images)).toBe(true); + expect(Array.isArray(canvas.images[0])).toBe(false); + expect(canvas.images[0]["@type"]).toBe("oa:Annotation"); + expect(canvas.otherContent[0]["@id"]).toBe("https://example.org/comments"); + expect(canvas.annotations).toBeUndefined(); + }); + + test("converts selected image bodies and all image selector parameters", () => { + const output = asPresentation2(selectedImage); + expect(output.resource).toMatchObject({ + "@type": "oa:SpecificResource", + full: { "@id": selectedImage.resource.full["@id"], "@type": "dctypes:Image" }, + selector: selectedImage.resource.selector, + }); + }); + + test("serializes collection members and fragment-bearing range canvas references", () => { + const output = asPresentation2({ + id: "https://example.org/collection", + type: "Collection", + label: { en: ["Collection"] }, + items: [{ id: "https://example.org/manifest", type: "Manifest", label: { en: ["Manifest"] }, items: [] }], + }); + expect(output.members).toHaveLength(1); + expect(output.members[0]["@id"]).toBe("https://example.org/manifest"); + expect(output.members[0].sequences).toBeUndefined(); + expect(output.members[0].structures).toBeUndefined(); + const range = asPresentation2({ + id: "https://example.org/range", + type: "Range", + label: { en: ["Range"] }, + items: [ + { + type: "SpecificResource", + source: { id: "https://example.org/canvas", type: "Canvas" }, + selector: { type: "FragmentSelector", value: "t=10,20" }, + }, + ], + }); + expect(range.canvases).toEqual(["https://example.org/canvas#t=10,20"]); + }); +}); diff --git a/src/presentation-3/serialize-presentation-2.ts b/src/presentation-3/serialize-presentation-2.ts index 74b7c91..2f9a456 100644 --- a/src/presentation-3/serialize-presentation-2.ts +++ b/src/presentation-3/serialize-presentation-2.ts @@ -10,6 +10,7 @@ import type { import type { DescriptiveNormalized, LinkingNormalized } from "../presentation-3-normalized/types"; import type * as Presentation2 from "../presentation-2/types"; import { compressSpecificResource } from "../shared/compress-specific-resource"; +import { IS_EXTERNAL } from "./utilities"; export function languageString2to3( value: InternationalString | null | undefined @@ -52,6 +53,7 @@ export function languageString2to3( } function parseCanvasTarget(target: any): any { + if (!target) return undefined; if (Array.isArray(target)) { return target.map((t) => parseCanvasTarget(t)); } @@ -64,9 +66,46 @@ function parseCanvasTarget(target: any): any { return target.id; } + if (target.type === "SpecificResource") { + const source = typeof target.source === "string" ? target.source : target.source.id; + if (!target.selector) return source; + if (target.selector.type === "FragmentSelector" && !target.styleClass) { + return `${source}#${target.selector.value}`; + } + return { + "@type": "oa:SpecificResource", + full: source, + selector: selectorToPresentation2(target.selector), + ...(target.styleClass ? { style: target.styleClass } : {}), + }; + } + + if (target.items) return target.items.map(parseCanvasTarget); + return target; } +function selectorToPresentation2(selector: any): any { + if (Array.isArray(selector)) { + return { + "@type": "oa:Choice", + default: selectorToPresentation2(selector[0]), + item: selector.slice(1).map(selectorToPresentation2), + }; + } + if (selector.type === "SvgSelector") return { "@type": "oa:SvgSelector", chars: selector.value }; + if (selector.type === "ImageApiSelector") { + const { type: _type, ...parameters } = selector; + return { + "@context": "http://iiif.io/api/annex/openannotation/context.json", + "@type": "iiif:ImageApiSelector", + ...parameters, + }; + } + const { type, ...properties } = selector; + return { "@type": `oa:${type}`, ...properties }; +} + function unNestArray(oneOrArray: T[] | undefined, onlyOne = false): T | T[] | undefined { if (!oneOrArray) { return undefined; @@ -105,7 +144,7 @@ function convertService(service: any) { function technicalProperties(props: Partial, type?: string) { return [ - ["@id", props.id], + ["@id", props.id?.startsWith("vault://") ? undefined : props.id], ["@type", type], ["format", props.format], ["height", props.height], @@ -170,7 +209,7 @@ function isFragmentSelector(resource: unknown): resource is FragmentSelector { function specificResourceToString(resource: Reference | SpecificResource) { if (resource && isSpecificResource(resource)) { - let id = resource.id; + let id = typeof resource.source === "string" ? resource.source : resource.source.id; const selector: Selector | undefined = resource.selector ? Array.isArray(resource.selector) ? resource.selector[0] @@ -187,6 +226,9 @@ function specificResourceToString(resource: Reference | SpecificResource) { export const serializeConfigPresentation2: SerializeConfig = { Manifest: function* (entity, state, { isTopLevel }) { + if (!isTopLevel) { + return [...technicalProperties(entity, "sc:Manifest"), ...(yield* descriptiveProperties(entity))]; + } return [ ...(isTopLevel ? [["@context", "http://iiif.io/api/presentation/2/context.json"]] : []), ...technicalProperties(entity, "sc:Manifest"), @@ -210,42 +252,74 @@ export const serializeConfigPresentation2: SerializeConfig = { Canvas: function* (entity) { const paintingPage = yield entity.items; - const resources = paintingPage[0]; + const resources = paintingPage.flatMap((page: any) => page.resources || []); return [ // Items. ...technicalProperties(entity, "sc:Canvas"), ...(yield* descriptiveProperties(entity)), ...(yield* linkingProperties(entity)), - ["images", resources ? [resources.resources] : undefined], + ["images", resources.length ? resources : undefined], [ // @todo use otherContent if they are inlined - "annotations", - entity.annotations && entity.annotations.length ? unNestArray(yield entity.annotations) : undefined, + "otherContent", + entity.annotations && entity.annotations.length ? yield entity.annotations : undefined, ], ]; }, - AnnotationPage: function* (entity) { + AnnotationPage: function* (entity, state, { isTopLevel }) { return [ + ...(isTopLevel ? [["@context", "http://iiif.io/api/presentation/2/context.json"]] : []), ...technicalProperties(entity, "sc:AnnotationList"), ...(yield* descriptiveProperties(entity)), - ["resources", entity.items && entity.items.length ? unNestArray(yield entity.items) : undefined], + ["resources", (entity as any)[IS_EXTERNAL] ? undefined : yield entity.items], ]; }, - Annotation: function* (entity) { + Annotation: function* (entity, state, { isTopLevel }) { + const bodies = []; + const annotationBodies = (Array.isArray(entity.body) ? entity.body : entity.body ? [entity.body] : []) as any[]; + for (const body of annotationBodies) { + if (isSpecificResource(body)) { + bodies.push({ + "@type": "oa:SpecificResource", + full: yield body.source, + ...(body.selector ? { selector: selectorToPresentation2(body.selector) } : {}), + ...(body.styleClass ? { style: body.styleClass } : {}), + }); + } else { + bodies.push(yield body); + } + } + const motivations = ( + Array.isArray(entity.motivation) ? entity.motivation : entity.motivation ? [entity.motivation] : [] + ).map((motivation) => + motivation.includes(":") ? motivation : `${motivation === "painting" ? "sc" : "oa"}:${motivation}` + ); return [ - ["@id", entity.id], - ["@type", "oa:Annotation"], - // This could be improved. - ["motivation", "sc:painting"], + ...(isTopLevel ? [["@context", "http://iiif.io/api/presentation/2/context.json"]] : []), + ...technicalProperties(entity as any, "oa:Annotation"), + ...(yield* descriptiveProperties(entity as any)), + ...(yield* linkingProperties(entity)), + ["motivation", unNestArray(motivations)], ["on", parseCanvasTarget(entity.target)], - ["resource", unNestArray(yield entity.body, true)], + ["resource", unNestArray(bodies)], ]; }, ContentResource: function* (entity: any) { switch (entity.type) { + case "TextualBody": + return [ + ...technicalProperties(entity, "cnt:ContentAsText"), + ["chars", entity.value ?? entity.chars], + ["language", entity.language], + ...(yield* descriptiveProperties(entity)), + ]; + case "Choice": { + const items = yield entity.items; + return [...technicalProperties(entity, "oa:Choice"), ["default", items[0]], ["item", items.slice(1)]]; + } case "Image": return [ // Image properties. @@ -255,8 +329,17 @@ export const serializeConfigPresentation2: SerializeConfig = { ]; case "Text": case "Dataset": + case "Sound": + case "Video": default: - return [...technicalProperties(entity, undefined), ...(yield* descriptiveProperties(entity))]; + return [ + ...technicalProperties( + entity, + entity.type === "Sound" ? "dctypes:Sound" : entity.type === "Video" ? "dctypes:MovingImage" : undefined + ), + ["chars", entity.chars ?? entity.value], + ...(yield* descriptiveProperties(entity)), + ]; } }, @@ -269,15 +352,16 @@ export const serializeConfigPresentation2: SerializeConfig = { ]; }, - Collection: function* (entity) { + Collection: function* (entity, state, { isTopLevel }) { return [ + ...(isTopLevel ? [["@context", "http://iiif.io/api/presentation/2/context.json"]] : []), ...technicalProperties(entity, "sc:Collection"), ...["first", "last", "prev", "next"].map((key) => [key, (entity as any)[key]?.id || (entity as any)[key]]), ["total", (entity as any).total], ["startIndex", (entity as any).startIndex], ...(yield* descriptiveProperties(entity)), ...(yield* linkingProperties(entity)), - ["members", yield* entity.items], + ["members", isTopLevel ? yield entity.items : undefined], ]; }, @@ -292,12 +376,12 @@ export const serializeConfigPresentation2: SerializeConfig = { const canvas = yield item; members.push({ "@id": specificResourceToString(_item), - "@type": item.type, + "@type": `sc:${item.type}`, label: canvas ? canvas.label : undefined, within: entity.id, }); if (item.type === "Canvas") { - canvases.push(item.id); + canvases.push(specificResourceToString(_item)); } } } From 668febf2274dd74d2d76a1c282cbf2ac0ab2c1f3 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 11:04:35 +0100 Subject: [PATCH 13/19] Preserve source type hints when selecting annotation targets Addresses #29 target coverage. Carry options through alternative targets and apply known source types to fragment targets. --- .../target-hints.test.ts | 33 +++++++++++++++++++ src/shared/expand-target.ts | 6 ++-- 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 __tests__/presentation-3-parser/target-hints.test.ts diff --git a/__tests__/presentation-3-parser/target-hints.test.ts b/__tests__/presentation-3-parser/target-hints.test.ts new file mode 100644 index 0000000..817ad11 --- /dev/null +++ b/__tests__/presentation-3-parser/target-hints.test.ts @@ -0,0 +1,33 @@ +import { expandTargetToSpecificResource } from "../../src/shared/expand-target"; + +const id = "https://example.org/canvas"; + +describe("annotation target hints (#29)", () => { + test.each([[[id]], { type: "Choice", items: [id] }, { type: "List", items: [id] }])( + "keeps the configured source type when choosing the first target in %j", + (input) => { + expect(expandTargetToSpecificResource(input as any, { typeHint: "Canvas" }).source).toEqual({ + id, + type: "Canvas", + }); + } + ); + + test("uses the known source type for fragment targets", () => { + const result = expandTargetToSpecificResource(`${id}#t=10,20`, { typeMap: { [id]: "Canvas" } }); + expect(result.source).toEqual({ id, type: "Canvas" }); + expect(result.selector).toEqual({ type: "FragmentSelector", value: "t=10,20" }); + }); + + test("normalizes a canvas's manifest membership", () => { + expect( + expandTargetToSpecificResource({ + type: "SpecificResource", + source: { id, type: "Canvas", partOf: "https://example.org/manifest" }, + selector: { type: "FragmentSelector", value: "xywh=1,2,3,4" }, + } as any) + ).toMatchObject({ + source: { id, type: "Canvas", partOf: [{ id: "https://example.org/manifest", type: "Manifest" }] }, + }); + }); +}); diff --git a/src/shared/expand-target.ts b/src/shared/expand-target.ts index 1ec158c..975b853 100644 --- a/src/shared/expand-target.ts +++ b/src/shared/expand-target.ts @@ -10,7 +10,7 @@ export function expandTargetToSpecificResource( ): SpecificResource { if (Array.isArray(target)) { // Don't support multiple targets for now. - return expandTargetToSpecificResource(target[0]!); + return expandTargetToSpecificResource(target[0]!, options); } if (typeof target === "string") { @@ -29,7 +29,7 @@ export function expandTargetToSpecificResource( return { type: "SpecificResource", - source: { id, type: options.typeHint || "Unknown" }, + source: { id, type: (options.typeMap && options.typeMap[id!]) || options.typeHint || "Unknown" } as any, selector: { type: "FragmentSelector", value: fragment, @@ -45,7 +45,7 @@ export function expandTargetToSpecificResource( target.type === "Independents" ) { // we also don't support these, just choose the first. - return expandTargetToSpecificResource(target.items[0]!); + return expandTargetToSpecificResource(target.items[0]!, options); } if (!target.type && "source" in target) { From da7e33d07af64a0c9297d0fc7a0e7bb0fff49ccc Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 11:06:18 +0100 Subject: [PATCH 14/19] Retain merged links and handle null values safely Addresses #29 merge coverage. Match legacy identifier aliases, retain new entities, and avoid duplicating equal values while preserving incoming multiplicity and existing service representations. --- .../merge-coverage.test.ts | 59 +++++++++++++++++++ src/presentation-3/normalize.ts | 51 ++++++++++------ 2 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 __tests__/presentation-3-parser/merge-coverage.test.ts diff --git a/__tests__/presentation-3-parser/merge-coverage.test.ts b/__tests__/presentation-3-parser/merge-coverage.test.ts new file mode 100644 index 0000000..6e2ad7e --- /dev/null +++ b/__tests__/presentation-3-parser/merge-coverage.test.ts @@ -0,0 +1,59 @@ +import { merge, mergeEntities } from "../../src/presentation-3/normalize"; + +describe("normalization merge coverage (#29)", () => { + test("merges repeated entity links and appends new links without losing either", () => { + const existing = [{ id: "https://example.org/one", type: "Dataset", label: { en: ["One"] } }]; + const incoming = [ + { id: "https://example.org/one", type: "Dataset", format: "application/json" }, + { id: "https://example.org/two", type: "Dataset", format: "text/csv" }, + ]; + expect(merge(existing, incoming)).toEqual([{ ...existing[0], format: "application/json" }, incoming[1]]); + expect(existing).toHaveLength(1); + expect(existing[0]).not.toHaveProperty("format"); + }); + + test("handles null values in both directions and within link arrays", () => { + const resource = { label: { en: ["Label"] } }; + expect(merge(null, resource)).toEqual(resource); + expect(merge(resource, null)).toBe(resource); + expect(merge([], [null, undefined, "one"])).toEqual(["one"]); + const link = { id: "https://example.org/link", type: "Dataset" }; + expect(merge([null], [link])).toEqual([null, link]); + }); + + test("rejects incompatible containers and preserves unchanged values", () => { + expect(() => merge([], {})).toThrow("Cannot merge array with non-array"); + expect(() => merge({}, [])).toThrow("Cannot merge object with non-object"); + expect(merge({ en: ["One"] }, { en: ["One", "Two"] })).toEqual({ en: ["One", "Two"] }); + expect(merge(["One"], ["One"])).toEqual(["One"]); + expect(merge([], ["One", "One"])).toEqual(["One", "One"]); + expect(merge({ en: [] }, { en: ["One", "One"] })).toEqual({ en: ["One", "One"] }); + expect(merge([["One"]], [["Two"]])).toEqual([["One"], ["Two"]]); + }); + + test("keeps string entities and rejects missing or mismatched identifiers and types", () => { + expect(mergeEntities("https://example.org/canvas" as any, {})).toBe("https://example.org/canvas"); + const canvas = { id: "https://example.org/canvas", type: "Canvas" }; + expect(() => mergeEntities(canvas, { id: canvas.id })).toThrow("identical identifiers and type"); + expect(() => mergeEntities(canvas, { type: "Canvas" })).toThrow("identical identifiers and type"); + expect(mergeEntities(canvas, canvas)).toEqual(canvas); + }); + test("matches legacy service aliases without duplicating them", () => { + const id = "https://example.org/image-service"; + const existing = [{ id, "@id": id, "@type": "ImageService2", profile: "level1" }]; + const incoming = [{ "@id": id, "@type": "ImageService2", profile: "level1", width: 1000 }]; + expect(merge(existing, incoming)).toEqual([{ ...existing[0], width: 1000 }]); + }); + + test("deduplicates equal untyped objects when merging service sizes", () => { + const id = "https://example.org/image-service"; + const sizes = [ + { width: 100, height: 50 }, + { width: 200, height: 100 }, + ]; + const existing = [{ id, "@type": "ImageService2", sizes }]; + const incoming = [{ "@id": id, "@type": "ImageService2", sizes: structuredClone(sizes) }]; + expect(merge(existing, incoming)[0].sizes).toEqual(sizes); + expect(merge([{ value: { en: ["One"] } }], [{ value: { en: ["One"] } }])).toHaveLength(1); + }); +}); diff --git a/src/presentation-3/normalize.ts b/src/presentation-3/normalize.ts index 1be06a7..540c2a0 100644 --- a/src/presentation-3/normalize.ts +++ b/src/presentation-3/normalize.ts @@ -86,13 +86,13 @@ function mapToEntities(entities: Record if (resource && resource.id && type) { storeType[resource.id] = storeType[resource.id] ? (mergeEntities(storeType[resource.id]!, resource, { - parent: context.parent, - isTopLevel: topLevel.id === resource.id, - }) as any) + parent: context.parent, + isTopLevel: topLevel.id === resource.id, + }) as any) : mergeEntities({ id: resource.id, type: resource.type } as any, resource, { - parent: context.parent, - isTopLevel: topLevel.id === resource.id, - }); + parent: context.parent, + isTopLevel: topLevel.id === resource.id, + }); return { id: resource.id, type: type === "ContentResource" ? type : resource.type, @@ -103,11 +103,23 @@ function mapToEntities(entities: Record }; } +function equalMergeValues(left: any, right: any): boolean { + if (left === right) return true; + if (!left || !right || typeof left !== "object" || typeof right !== "object") return false; + if (Array.isArray(left) !== Array.isArray(right)) return false; + const keys = Object.keys(left); + return ( + keys.length === Object.keys(right).length && + keys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && equalMergeValues(left[key], right[key])) + ); +} + export function merge(existing: any, incoming: any, context?: { parent?: any; isTopLevel?: boolean }): any { if (!incoming) { // Falsy values are ignored return existing; } + if (existing === null || typeof existing === "undefined") return incoming; if (Array.isArray(existing)) { if (!Array.isArray(incoming)) { throw new Error("Cannot merge array with non-array"); @@ -118,24 +130,24 @@ export function merge(existing: any, incoming: any, context?: { parent?: any; is // merged with the existing value. const merged = [...existing]; for (const item of incoming) { - if (item["@id"] && !item.id) { - item.id = item["@id"]; - } - if (item["@type"] && !item.type) { - item.type = item["@type"]; - } - if (item === null || item === undefined) { - continue; - } + if (item === null || item === undefined) continue; + if (item["@id"] && !item.id) item.id = item["@id"]; + if (item["@type"] && !item.type) item.type = item["@type"]; + const itemId = item.id; + const itemType = item.type; if (Array.isArray(item)) { // FIXME: How to handle this properly? merged.push(item); - } else if (typeof item === "object" && item.id && item.type) { - const existingIdx = merged.findIndex((e) => e.id === item.id && e.type === item.type); + } else if (typeof item === "object" && itemId && itemType) { + const existingIdx = merged.findIndex( + (e) => (e?.id || e?.["@id"]) === itemId && (e?.type || e?.["@type"]) === itemType + ); if (existingIdx >= 0) { merged[existingIdx] = merge(merged[existingIdx], item); + } else { + merged.push(item); } - } else if (existing.indexOf(item) === -1) { + } else if (!existing.some((entry: any) => equalMergeValues(entry, item))) { merged.push(item); } } @@ -258,7 +270,8 @@ export function mergeEntities( } throw new Error( - `Can only merge entities with identical identifiers and type! ${incoming.type}(${incoming.id}) => ${(existing as any).type + `Can only merge entities with identical identifiers and type! ${incoming.type}(${incoming.id}) => ${ + (existing as any).type }(${(existing as any).id})` ); } From e078c5a4e0f21123be58098f06232c8ba9252e2e Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 11:06:47 +0100 Subject: [PATCH 15/19] Preserve annotation collection fields and authored empty pages Addresses #29. Support standalone annotation collections and serialize their pages and linking fields while preserving the existing inline model for linked collections. --- .../annotation-collections.test.ts | 46 +++++++++++++++++++ src/presentation-3/normalize.ts | 8 ++++ .../serialize-presentation-3.ts | 33 +++++++------ src/presentation-3/serialize.ts | 12 +++-- src/presentation-3/traverse.ts | 35 ++++++++++---- 5 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 __tests__/presentation-3-parser/annotation-collections.test.ts diff --git a/__tests__/presentation-3-parser/annotation-collections.test.ts b/__tests__/presentation-3-parser/annotation-collections.test.ts new file mode 100644 index 0000000..79e792c --- /dev/null +++ b/__tests__/presentation-3-parser/annotation-collections.test.ts @@ -0,0 +1,46 @@ +import { normalize, serialize, serializeConfigPresentation3 } from "../../src"; + +function roundTrip(input: any): any { + const result = normalize(structuredClone(input)); + return serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); +} + +describe("AnnotationCollection coverage (#29)", () => { + test("preserves descriptive/linking fields, counts, and first/last pages", () => { + const input = { + "@context": "http://iiif.io/api/presentation/3/context.json", + id: "https://example.org/comments", + type: "AnnotationCollection", + label: { en: ["Comments"] }, + summary: { en: ["Transcriptions"] }, + total: 2, + homepage: [{ id: "https://example.org/comments.html", type: "Text", format: "text/html" }], + first: { id: "https://example.org/comments/page1", type: "AnnotationPage", items: [] }, + last: "https://example.org/comments/page2", + }; + expect(roundTrip(input)).toEqual(input); + }); + + test("serializes a first page with a reference back to its collection", () => { + const id = "https://example.org/comments"; + const input = { + id, + type: "AnnotationCollection", + total: 0, + first: { id: `${id}/page`, type: "AnnotationPage", partOf: [{ id, type: "AnnotationCollection" }], items: [] }, + }; + expect(roundTrip(input)).toEqual(input); + }); + test("keeps linked collections inline in the existing page store model", () => { + const collection = { + id: "https://example.org/comments", + type: "AnnotationCollection", + label: { en: ["Comments"] }, + total: 0, + }; + const input = { id: "https://example.org/page", type: "AnnotationPage", items: [], partOf: [collection] }; + const result = normalize(structuredClone(input)); + expect((result.entities.AnnotationPage as any)[input.id].partOf).toEqual([collection]); + expect(roundTrip(input).partOf).toEqual([collection]); + }); +}); diff --git a/src/presentation-3/normalize.ts b/src/presentation-3/normalize.ts index 540c2a0..a25fabe 100644 --- a/src/presentation-3/normalize.ts +++ b/src/presentation-3/normalize.ts @@ -1,5 +1,6 @@ import type { Annotation, + AnnotationCollection, AnnotationPage, Canvas, Collection, @@ -549,6 +550,13 @@ export function normalize(unknownEntity: unknown) { addToMapping("Annotation"), addToEntities("Annotation"), ], + annotationCollection: [ + (collection, context) => { + if (context.parent) return collection; + addToMapping("AnnotationCollection")(collection); + return addToEntities("AnnotationCollection")(collection, context); + }, + ], contentResource: [ // This won't be normalized before going into the state. // It will be normalized through selectors and pattern matching. diff --git a/src/presentation-3/serialize-presentation-3.ts b/src/presentation-3/serialize-presentation-3.ts index bd6844a..4dfb0b9 100644 --- a/src/presentation-3/serialize-presentation-3.ts +++ b/src/presentation-3/serialize-presentation-3.ts @@ -112,6 +112,10 @@ function* linkingProperties( ): Generator> { let filteredPartOf = []; for (let partOf of omitPartOf ? [] : entity.partOf || []) { + if (parent?.type === "AnnotationCollection" && partOf.id === parent.id) { + filteredPartOf.push({ id: partOf.id, type: "AnnotationCollection" }); + continue; + } if (partOf.type === "Manifest" && parent?.type === "Manifest") continue; filteredPartOf.push(yield partOf); } @@ -194,7 +198,7 @@ export const serializeConfigPresentation3: SerializeConfig = { ]; }, - AnnotationPage: function* (entity) { + AnnotationPage: function* (entity, state, { parent }) { const entries = Object.entries(entity) .map(([key, item]) => { return [key, Array.isArray(item) ? filterEmpty(item as any) : item]; @@ -209,8 +213,8 @@ export const serializeConfigPresentation3: SerializeConfig = { // Any more properties? ["id", !entity.id?.startsWith("vault://") ? entity.id : undefined], ...entries, - ...(yield* linkingProperties(entity)), - ["items", items.length || (entity as any)[IS_EXTERNAL] === false ? items : UNSET], + ...(yield* linkingProperties(entity, parent)), + ["items", items.length || (entity as any)[IS_EXTERNAL] !== true ? items : UNSET], ]; }, @@ -303,10 +307,13 @@ export const serializeConfigPresentation3: SerializeConfig = { AnnotationCollection: function* (entity) { return [ - // @todo expand properties if they are actually used. - ["id", entity.id], - ["type", "AnnotationCollection"], - ["label", entity.label], + ["@context", (entity as any)["@context"]], + ...technicalProperties(entity), + ...(yield* descriptiveProperties(entity)), + ...(yield* linkingProperties(entity)), + ["total", (entity as any).total], + ["first", typeof (entity as any).first === "string" ? (entity as any).first : yield (entity as any).first], + ["last", typeof (entity as any).last === "string" ? (entity as any).last : yield (entity as any).last], ]; }, @@ -338,12 +345,12 @@ export const serializeConfigPresentation3: SerializeConfig = { } return [ ...technicalProperties(entity), - ["first", (entity as any).first], - ["last", (entity as any).last], - ["startIndex", (entity as any).startIndex], - ["prev", (entity as any).prev], - ["next", (entity as any).next], - ["total", (entity as any).total], + ["first", (entity as any).first], + ["last", (entity as any).last], + ["startIndex", (entity as any).startIndex], + ["prev", (entity as any).prev], + ["next", (entity as any).next], + ["total", (entity as any).total], ...(yield* descriptiveProperties(entity)), ...(yield* linkingProperties(entity, undefined, true)), ["navPlace", (entity as any).navPlace], diff --git a/src/presentation-3/serialize.ts b/src/presentation-3/serialize.ts index c7da5bc..d5a0715 100644 --- a/src/presentation-3/serialize.ts +++ b/src/presentation-3/serialize.ts @@ -96,16 +96,20 @@ export function serialize(state: CompatibleStore, subject: Reference, co } function flatten(sub: Reference, parent?: any, depth = 0) { - const generator = config[sub.type as keyof SerializeConfig] || (typeof sub?.type === "string" && typeof sub.id === "string" && !state.entities[sub.type] && config.ContentResource); + const generator = + config[sub.type as keyof SerializeConfig] || + (typeof sub?.type === "string" && + typeof sub.id === "string" && + !state.entities[sub.type] && + config.ContentResource); if (!generator) { return UNSET; } if (depth > 20) { throw new Error("Circular reference: " + sub.id + " " + sub.type); } - const [resource, fullResource] = state.entities[sub.type] - ? resolveIfExists(state, sub, parent) - : [sub, sub]; + const [storedResource, fullResource] = state.entities[sub.type] ? resolveIfExists(state, sub, parent) : [sub, sub]; + const resource = storedResource || (sub.type === "AnnotationCollection" ? sub : undefined); if (!resource) { return UNSET; } diff --git a/src/presentation-3/traverse.ts b/src/presentation-3/traverse.ts index 62b8bf0..f17365b 100644 --- a/src/presentation-3/traverse.ts +++ b/src/presentation-3/traverse.ts @@ -350,6 +350,20 @@ export class Traverse { return annotationPage; } + traverseAnnotationCollection(collection: AnnotationCollection, parent?: Presentation3Resource): AnnotationCollection { + for (const key of ["first", "last"] as const) { + const page = collection[key]; + if (page && typeof page !== "string") { + collection[key] = this.traverseAnnotationPage(page as AnnotationPage, collection); + } + } + return this.traverseType( + this.traverseDescriptive(this.traverseLinking(collection as any)) as AnnotationCollection, + { parent }, + this.traversals.annotationCollection + ); + } + _traverseAnnotationPage: (page: AnnotationPage) => AnnotationPage = compose( this.traverseAnnotationPageItems.bind(this), this.traverseLinking.bind(this), @@ -468,16 +482,19 @@ export class Traverse { if (range.items) { range.items = range.items.map((rangeOrManifest: RangeItems) => { if (typeof rangeOrManifest === "string" || rangeOrManifest.type === "Canvas") { - const canvas = typeof rangeOrManifest === "string" - ? { id: rangeOrManifest, type: "Canvas" as const } - : rangeOrManifest; + const canvas = + typeof rangeOrManifest === "string" ? { id: rangeOrManifest, type: "Canvas" as const } : rangeOrManifest; const [id, fragment] = splitCanvasFragment(canvas.id); if (fragment) { - return this.traverseSpecificResource({ - type: "SpecificResource", - source: { ...canvas, id }, - selector: { type: "FragmentSelector", value: fragment }, - } as SpecificResource, "Canvas", range); + return this.traverseSpecificResource( + { + type: "SpecificResource", + source: { ...canvas, id }, + selector: { type: "FragmentSelector", value: fragment }, + } as SpecificResource, + "Canvas", + range + ); } return this.traverseCanvas(canvas as Canvas, range); } @@ -564,6 +581,8 @@ export class Traverse { return this.traverseCanvas(resource as Canvas, parent); case "AnnotationPage": return this.traverseAnnotationPage(resource as AnnotationPage, parent); + case "AnnotationCollection": + return this.traverseAnnotationCollection(resource as AnnotationCollection, parent) as any; case "Annotation": return this.traverseAnnotation(resource as Annotation, parent); case "ContentResource": From 268789cc264c034e4c491f9473044521f3436161 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 11:08:11 +0100 Subject: [PATCH 16/19] Retain identifiers when parsing base image service URLs Addresses #29. Cover prefixed service URLs, rotations, error paths and fixed image sizes. --- .../image-3-parser/backlog-coverage.test.ts | 55 +++++++++++++++++++ .../parser/parse-image-service-request.ts | 4 +- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 __tests__/image-3-parser/backlog-coverage.test.ts diff --git a/__tests__/image-3-parser/backlog-coverage.test.ts b/__tests__/image-3-parser/backlog-coverage.test.ts new file mode 100644 index 0000000..48a5ffd --- /dev/null +++ b/__tests__/image-3-parser/backlog-coverage.test.ts @@ -0,0 +1,55 @@ +import { + extractFixedSizeScales, + fixedSizesFromScales, + parseImageServiceRequest, + imageServiceRequestToString, + parseRotationParameter, + rotationParameterToString, + parseImageServiceUrl, + parseRegionParameter, +} from "../../src/image-3"; + +describe("Image API coverage gaps (#29)", () => { + test.each(["!90", "45.5", "0"])("parses and serializes rotation %s", (value) => { + expect(rotationParameterToString(parseRotationParameter(value))).toBe(value); + }); + + test("normalizes angles outside one revolution and rejects invalid rotation", () => { + expect(parseRotationParameter("!450")).toEqual({ mirror: true, angle: 90 }); + expect(() => parseRotationParameter("invalid")).toThrow("Invalid rotation"); + }); + + test("round-trips prefixed info and image requests from the existing Munch examples", () => { + const prefix = "apis/iiif/image/v2"; + for (const url of [ + "https://munch.emuseum.com/apis/iiif/image/v2/17261/info.json", + "https://munch.emuseum.com/apis/iiif/image/v2/17261/full/max/!90/default.jpg", + ]) { + expect(imageServiceRequestToString(parseImageServiceRequest(url, prefix))).toBe(url); + } + expect(() => parseImageServiceUrl("https://example.org/image", "other")).toThrow("Path does not start with prefix"); + expect(() => parseImageServiceRequest("https://example.org/image/invalid")).toThrow("Invalid image service URL"); + expect(() => parseRegionParameter(null as any)).toThrow("Expected"); + }); + + test("extracts and regenerates advertised fixed sizes", () => { + const sizes = [ + { width: 2000, height: 1000 }, + { width: 1000, height: 500 }, + { width: 500, height: 250 }, + ]; + expect(extractFixedSizeScales(2000, 1000, sizes)).toEqual([1, 2, 4]); + expect(fixedSizesFromScales(2000, 1000, [1, 2, 4])).toEqual(sizes); + expect(fixedSizesFromScales(2000, 1000, [0, 2])).toEqual([sizes[1]]); + }); + test("keeps the identifier on base-service URLs with and without prefixes", () => { + for (const [url, prefix] of [ + ["https://example.org/identifier", ""], + ["https://example.org/iiif/identifier", "iiif"], + ]) { + const parsed = parseImageServiceRequest(url!, prefix); + expect(parsed).toMatchObject({ type: "base", identifier: "identifier" }); + expect(imageServiceRequestToString(parsed)).toBe(url); + } + }); +}); diff --git a/src/image-3/parser/parse-image-service-request.ts b/src/image-3/parser/parse-image-service-request.ts index 7b3156e..fc387bc 100644 --- a/src/image-3/parser/parse-image-service-request.ts +++ b/src/image-3/parser/parse-image-service-request.ts @@ -11,9 +11,9 @@ export function parseImageServiceRequest(input: string, _prefix = ""): ImageServ const [fileName, rotation, size, region, ...others] = parts; const identifier = others.reverse().filter(Boolean).join("/"); - if (parts.length === 1 || fileName === "") { + if (parts.filter(Boolean).length === 1 || fileName === "") { // likely the server will want to redirect this - return { type: "base", scheme, server, prefix, identifier }; + return { type: "base", scheme, server, prefix, identifier: path.split("/").filter(Boolean).join("/") }; } if (fileName === "info.json") { From af25ef8972c4b91176dfdc2a8081c64a34cc1bc9 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 11:08:11 +0100 Subject: [PATCH 17/19] Cover annotation and image service compatibility paths Completes the reachable compatibility cases in #29 without changing service upgrading. --- .../compatibility-coverage.test.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 __tests__/presentation-3-parser/compatibility-coverage.test.ts diff --git a/__tests__/presentation-3-parser/compatibility-coverage.test.ts b/__tests__/presentation-3-parser/compatibility-coverage.test.ts new file mode 100644 index 0000000..be830a9 --- /dev/null +++ b/__tests__/presentation-3-parser/compatibility-coverage.test.ts @@ -0,0 +1,62 @@ +import { normalize, serialize, serializeConfigPresentation3 } from "../../src"; + +describe("Presentation 3 compatibility coverage (#29)", () => { + test("retains annotation audience, accessibility and non-array motivation", () => { + const id = "https://example.org/annotation"; + const input = { + id, + type: "Annotation", + motivation: "commenting", + target: "https://example.org/canvas", + audience: { id: "https://example.org/audience", type: "Audience" }, + accessibility: "alternativeText", + seeAlso: [{ id: "https://example.org/data", type: "Dataset", format: "application/json" }], + body: { type: "TextualBody", value: "A description" }, + }; + const result = normalize(structuredClone(input)); + const stored = (result.entities.Annotation as any)[id]; + expect(stored.audience).toEqual([input.audience]); + expect(stored.accessibility).toEqual(["alternativeText"]); + expect(stored.motivation).toEqual(["commenting"]); + stored.motivation = "commenting"; + const output = serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); + expect(output.motivation).toBe("commenting"); + expect(output.seeAlso).toEqual([{ id: "https://example.org/data", type: "Dataset", format: "application/json" }]); + expect(output.body.value).toBe("A description"); + }); + + test("serializes ImageService2 and records a single-object nested service", () => { + const input = { + id: "https://example.org/image.jpg", + type: "Image", + format: "image/jpeg", + service: [ + { + id: "https://example.org/image", + type: "ImageService2", + profile: "level1", + service: { + id: "https://example.org/auth", + type: "AuthCookieService1", + profile: "http://iiif.io/api/auth/1/login", + }, + }, + ], + }; + const result = normalize({ + id: "https://example.org/annotation", + type: "Annotation", + motivation: "painting", + target: "https://example.org/canvas", + body: input, + }); + expect((result.entities.Service as any)["https://example.org/auth"]).toBeDefined(); + const output = serialize({ ...result, requests: {} }, result.resource, serializeConfigPresentation3); + expect(output.body.service[0]).toMatchObject({ + "@id": "https://example.org/image", + "@type": "ImageService2", + profile: "http://iiif.io/api/image/2/level1.json", + }); + expect(output.body.service[0].service).toHaveLength(1); + }); +}); From a010b07068a5f9e4d9e91841f37ecf426f977b7a Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 11:31:49 +0100 Subject: [PATCH 18/19] Updated git repository URL to match for NPM publishing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c86d170..4017056 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/iiif-commons/parser.git" + "url": "git+https://github.com/IIIF-Commons/parser" }, "bin": { "iiif-parser": "./dist/cli.mjs" From 59ed9cc1e1a606986b21e47e687f2ea5a0799681 Mon Sep 17 00:00:00 2001 From: Stephen Fraser Date: Tue, 20 Oct 2026 11:47:20 +0100 Subject: [PATCH 19/19] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4017056..22c697c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/IIIF-Commons/parser" + "url": "https://github.com/IIIF-Commons/parser" }, "bin": { "iiif-parser": "./dist/cli.mjs"