Skip to content

Commit 1a78594

Browse files
committed
Fixes to parser
1 parent c522dca commit 1a78594

File tree

4 files changed

+468
-89
lines changed

4 files changed

+468
-89
lines changed

__tests__/presentation-4-parser/serialize-v3-downgrade.test.ts

Lines changed: 135 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,112 @@
1-
import { readFileSync } from 'node:fs';
2-
import { join } from 'node:path';
3-
import { cwd } from 'node:process';
4-
import { describe, expect, test } from 'vitest';
5-
import {
6-
normalize,
7-
serialize,
8-
serializeConfigPresentation3,
9-
} from '../../src/presentation-4';
1+
import { readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import { cwd } from "node:process";
4+
import { describe, expect, test } from "vitest";
5+
import { normalize, serialize, serializeConfigPresentation3 } from "../../src/presentation-4";
106

11-
describe('presentation-4 to presentation-3 downgrade serializer', () => {
12-
test('downgrades Timeline to Canvas with minimal dimensions', () => {
7+
describe("presentation-4 to presentation-3 downgrade serializer", () => {
8+
test("serializes annotation targets by unwrapping List, compressing SpecificResource, and mapping Timeline/Scene to Canvas", () => {
9+
const manifest = {
10+
"@context": "http://iiif.io/api/presentation/4/context.json",
11+
id: "https://example.org/iiif/target-manifest",
12+
type: "Manifest",
13+
label: { en: ["Target test"] },
14+
items: [
15+
{
16+
id: "https://example.org/iiif/canvas/1",
17+
type: "Canvas",
18+
width: 1000,
19+
height: 1000,
20+
items: [
21+
{
22+
id: "https://example.org/iiif/canvas/1/page/1",
23+
type: "AnnotationPage",
24+
items: [
25+
{
26+
id: "https://example.org/iiif/canvas/1/page/1/anno/1",
27+
type: "Annotation",
28+
motivation: ["painting"],
29+
body: {
30+
id: "https://example.org/iiif/image/1.jpg",
31+
type: "Image",
32+
format: "image/jpeg",
33+
},
34+
target: {
35+
type: "List",
36+
items: [
37+
{
38+
type: "SpecificResource",
39+
source: {
40+
id: "https://example.org/iiif/timeline/1",
41+
type: "Timeline",
42+
},
43+
selector: {
44+
type: "FragmentSelector",
45+
value: "t=0,10",
46+
},
47+
},
48+
{
49+
id: "https://example.org/iiif/scene/1",
50+
type: "Scene",
51+
},
52+
],
53+
},
54+
},
55+
],
56+
},
57+
],
58+
},
59+
],
60+
};
61+
62+
const normalized = normalize(manifest);
63+
const serialized = serialize<any>(
64+
{
65+
entities: normalized.entities as any,
66+
mapping: normalized.mapping as any,
67+
requests: {},
68+
},
69+
normalized.resource,
70+
serializeConfigPresentation3
71+
);
72+
73+
expect(serialized.items[0].items[0].items[0].target).toEqual([
74+
"https://example.org/iiif/timeline/1#t=0,10",
75+
{
76+
id: "https://example.org/iiif/scene/1",
77+
type: "Canvas",
78+
},
79+
]);
80+
});
81+
82+
test("downgrades Timeline to Canvas with minimal dimensions", () => {
1383
const timelineManifest = {
14-
'@context': 'http://iiif.io/api/presentation/4/context.json',
15-
id: 'https://example.org/iiif/timeline-manifest',
16-
type: 'Manifest',
17-
label: { en: ['Timeline'] },
84+
"@context": "http://iiif.io/api/presentation/4/context.json",
85+
id: "https://example.org/iiif/timeline-manifest",
86+
type: "Manifest",
87+
label: { en: ["Timeline"] },
1888
items: [
1989
{
20-
id: 'https://example.org/iiif/timeline-1',
21-
type: 'Timeline',
90+
id: "https://example.org/iiif/timeline-1",
91+
type: "Timeline",
2292
duration: 20,
2393
items: [
2494
{
25-
id: 'https://example.org/iiif/timeline-1/page',
26-
type: 'AnnotationPage',
95+
id: "https://example.org/iiif/timeline-1/page",
96+
type: "AnnotationPage",
2797
items: [
2898
{
29-
id: 'https://example.org/iiif/timeline-1/painting',
30-
type: 'Annotation',
31-
motivation: ['painting'],
99+
id: "https://example.org/iiif/timeline-1/painting",
100+
type: "Annotation",
101+
motivation: ["painting"],
32102
body: [
33103
{
34-
id: 'https://example.org/audio.mp3',
35-
type: 'Sound',
36-
format: 'audio/mp3',
104+
id: "https://example.org/audio.mp3",
105+
type: "Sound",
106+
format: "audio/mp3",
37107
},
38108
],
39-
target: ['https://example.org/iiif/timeline-1'],
109+
target: ["https://example.org/iiif/timeline-1"],
40110
},
41111
],
42112
},
@@ -56,17 +126,15 @@ describe('presentation-4 to presentation-3 downgrade serializer', () => {
56126
serializeConfigPresentation3
57127
);
58128

59-
expect(serialized['@context']).toEqual('http://iiif.io/api/presentation/3/context.json');
60-
expect(serialized.items[0].type).toEqual('Canvas');
61-
expect(serialized.items[0].width).toEqual(1);
62-
expect(serialized.items[0].height).toEqual(1);
129+
expect(serialized["@context"]).toEqual("http://iiif.io/api/presentation/3/context.json");
130+
expect(serialized.items[0].type).toEqual("Canvas");
131+
expect(serialized.items[0].width).not.toBeDefined();
132+
expect(serialized.items[0].height).not.toBeDefined();
63133
expect(serialized.items[0].duration).toEqual(20);
64134
});
65135

66-
test('fails downgrade when Scene is present', () => {
67-
const fixture = JSON.parse(
68-
readFileSync(join(cwd(), 'fixtures/presentation-4/01-model-in-scene.json'), 'utf8')
69-
);
136+
test("fails downgrade when Scene is present", () => {
137+
const fixture = JSON.parse(readFileSync(join(cwd(), "fixtures/presentation-4/01-model-in-scene.json"), "utf8"));
70138
const normalized = normalize(fixture);
71139

72140
expect(() =>
@@ -82,35 +150,35 @@ describe('presentation-4 to presentation-3 downgrade serializer', () => {
82150
).toThrow(/unsupported/i);
83151
});
84152

85-
test('fails downgrade when activating annotations are present', () => {
153+
test("fails downgrade when activating annotations are present", () => {
86154
const manifest = {
87-
'@context': 'http://iiif.io/api/presentation/4/context.json',
88-
id: 'https://example.org/iiif/activating-manifest',
89-
type: 'Manifest',
90-
label: { en: ['Activating'] },
155+
"@context": "http://iiif.io/api/presentation/4/context.json",
156+
id: "https://example.org/iiif/activating-manifest",
157+
type: "Manifest",
158+
label: { en: ["Activating"] },
91159
items: [
92160
{
93-
id: 'https://example.org/iiif/canvas/1',
94-
type: 'Canvas',
161+
id: "https://example.org/iiif/canvas/1",
162+
type: "Canvas",
95163
width: 1000,
96164
height: 1000,
97165
items: [
98166
{
99-
id: 'https://example.org/iiif/canvas/1/page/1',
100-
type: 'AnnotationPage',
167+
id: "https://example.org/iiif/canvas/1/page/1",
168+
type: "AnnotationPage",
101169
items: [
102170
{
103-
id: 'https://example.org/iiif/canvas/1/page/1/anno/1',
104-
type: 'Annotation',
105-
motivation: ['activating'],
171+
id: "https://example.org/iiif/canvas/1/page/1/anno/1",
172+
type: "Annotation",
173+
motivation: ["activating"],
106174
body: [
107175
{
108-
id: 'https://example.org/iiif/image/1.jpg',
109-
type: 'Image',
110-
format: 'image/jpeg',
176+
id: "https://example.org/iiif/image/1.jpg",
177+
type: "Image",
178+
format: "image/jpeg",
111179
},
112180
],
113-
target: ['https://example.org/iiif/canvas/1'],
181+
target: ["https://example.org/iiif/canvas/1"],
114182
},
115183
],
116184
},
@@ -134,41 +202,41 @@ describe('presentation-4 to presentation-3 downgrade serializer', () => {
134202
).toThrow(/activating/i);
135203
});
136204

137-
test('fails downgrade when content has transform metadata', () => {
205+
test("fails downgrade when content has transform metadata", () => {
138206
const manifest = {
139-
'@context': 'http://iiif.io/api/presentation/4/context.json',
140-
id: 'https://example.org/iiif/transform-manifest',
141-
type: 'Manifest',
142-
label: { en: ['Transform'] },
207+
"@context": "http://iiif.io/api/presentation/4/context.json",
208+
id: "https://example.org/iiif/transform-manifest",
209+
type: "Manifest",
210+
label: { en: ["Transform"] },
143211
items: [
144212
{
145-
id: 'https://example.org/iiif/canvas/2',
146-
type: 'Canvas',
213+
id: "https://example.org/iiif/canvas/2",
214+
type: "Canvas",
147215
width: 1000,
148216
height: 1000,
149217
items: [
150218
{
151-
id: 'https://example.org/iiif/canvas/2/page/1',
152-
type: 'AnnotationPage',
219+
id: "https://example.org/iiif/canvas/2/page/1",
220+
type: "AnnotationPage",
153221
items: [
154222
{
155-
id: 'https://example.org/iiif/canvas/2/page/1/anno/1',
156-
type: 'Annotation',
157-
motivation: ['painting'],
223+
id: "https://example.org/iiif/canvas/2/page/1/anno/1",
224+
type: "Annotation",
225+
motivation: ["painting"],
158226
body: [
159227
{
160-
id: 'https://example.org/iiif/image/2.jpg',
161-
type: 'Image',
162-
format: 'image/jpeg',
228+
id: "https://example.org/iiif/image/2.jpg",
229+
type: "Image",
230+
format: "image/jpeg",
163231
transform: [
164232
{
165-
type: 'RotateTransform',
233+
type: "RotateTransform",
166234
angle: 90,
167235
},
168236
],
169237
},
170238
],
171-
target: ['https://example.org/iiif/canvas/2'],
239+
target: ["https://example.org/iiif/canvas/2"],
172240
},
173241
],
174242
},

__tests__/presentation-4-parser/validator.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,52 @@ describe("presentation-4 validator", () => {
248248
expect(report.issues.some((issue) => issue.code === "annotation-target-entry-object")).toBe(true);
249249
});
250250

251+
test("reports malformed List-form annotation targets when items is not an array", () => {
252+
const invalid = {
253+
"@context": "http://iiif.io/api/presentation/4/context.json",
254+
id: "https://example.org/manifest/target-list-items-shape",
255+
type: "Manifest",
256+
label: { en: ["target list shape"] },
257+
items: [
258+
{
259+
id: "https://example.org/canvas/1",
260+
type: "Canvas",
261+
width: 1000,
262+
height: 1000,
263+
items: [
264+
{
265+
id: "https://example.org/canvas/1/page/1",
266+
type: "AnnotationPage",
267+
items: [
268+
{
269+
id: "https://example.org/canvas/1/page/1/anno/1",
270+
type: "Annotation",
271+
motivation: ["painting"],
272+
body: {
273+
id: "https://example.org/image.jpg",
274+
type: "Image",
275+
format: "image/jpeg",
276+
},
277+
target: {
278+
type: "List",
279+
items: {
280+
id: "https://example.org/canvas/1",
281+
type: "Canvas",
282+
},
283+
},
284+
},
285+
],
286+
},
287+
],
288+
},
289+
],
290+
};
291+
292+
const report = validatePresentation4(invalid, { mode: "tolerant" });
293+
expect(report.valid).toBe(false);
294+
expect(report.issues.some((issue) => issue.code === "annotation-target-list-items-array")).toBe(true);
295+
});
296+
251297
test("coerces PointSelector.t to instant during validation upgrade pass", () => {
252298
const fixture = {
253299
"@context": "http://iiif.io/api/presentation/4/context.json",
@@ -490,6 +536,46 @@ describe("presentation-4 validator", () => {
490536
expect(targetClassRequirementIssues).toEqual([]);
491537
});
492538

539+
test("does not validate range item canvas references as full canvases", () => {
540+
const manifest = {
541+
"@context": "http://iiif.io/api/presentation/4/context.json",
542+
id: "https://example.org/manifest/range-canvas-ref",
543+
type: "Manifest",
544+
label: { en: ["range canvas ref"] },
545+
items: [
546+
{
547+
id: "https://example.org/canvas/1",
548+
type: "Canvas",
549+
width: 1000,
550+
height: 1000,
551+
items: [],
552+
},
553+
],
554+
structures: [
555+
{
556+
id: "https://example.org/range/1",
557+
type: "Range",
558+
label: { en: ["chapter 1"] },
559+
items: [
560+
{
561+
id: "https://example.org/canvas/1",
562+
type: "Canvas",
563+
},
564+
],
565+
},
566+
],
567+
};
568+
569+
const report = validatePresentation4(manifest, { mode: "tolerant" });
570+
const rangeCanvasClassRequirementIssues = report.issues.filter(
571+
(item) => item.code.startsWith("class-requirement-") && item.path.startsWith("$.structures[0].items[0].")
572+
);
573+
574+
expect(rangeCanvasClassRequirementIssues).toEqual([]);
575+
expect(report.issues.some((issue) => issue.code === "canvas-width-required")).toBe(false);
576+
expect(report.issues.some((issue) => issue.code === "canvas-height-required")).toBe(false);
577+
});
578+
493579
test("does not apply class requirements to partOf references", () => {
494580
const manifest = {
495581
"@context": "http://iiif.io/api/presentation/4/context.json",

0 commit comments

Comments
 (0)