cozy-iiif 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.ts CHANGED
@@ -1,5 +1,9 @@
1
- import { Manifest, Canvas, ImageService2, ImageService3, IIIFExternalWebResource } from '@iiif/presentation-3';
1
+ import { Manifest, Canvas, ImageService2, ImageService3, IIIFExternalWebResource, Collection, Range } from '@iiif/presentation-3';
2
2
  export type CozyParseResult = {
3
+ type: 'collection';
4
+ url: string;
5
+ resource: CozyCollection;
6
+ } | {
3
7
  type: 'manifest';
4
8
  url: string;
5
9
  resource: CozyManifest;
@@ -18,14 +22,38 @@ export type CozyParseResult = {
18
22
  code: 'INVALID_URL' | 'INVALID_HTTP_RESPONSE' | 'FETCH_ERROR' | 'INVALID_MANIFEST' | 'UNSUPPORTED_FORMAT';
19
23
  message: string;
20
24
  };
25
+ export interface CozyCollection {
26
+ readonly majorVersion: number;
27
+ readonly source: Collection;
28
+ readonly id: string;
29
+ readonly items: CozyCollectionItem[];
30
+ getLabel(locale?: string): string | undefined;
31
+ getMetadata(locale?: string): CozyMetadata[];
32
+ }
33
+ export interface CozyCollectionItem {
34
+ readonly id: string;
35
+ readonly type: string;
36
+ readonly source: any;
37
+ getLabel(locale?: string): string | undefined;
38
+ }
21
39
  export interface CozyManifest {
22
40
  readonly majorVersion: number;
23
41
  readonly source: Manifest;
24
42
  readonly id: string;
25
43
  readonly canvases: CozyCanvas[];
44
+ readonly structure: CozyRange[];
26
45
  getLabel(locale?: string): string | undefined;
46
+ getTableOfContents(): CozyTOCNode[];
27
47
  getMetadata(locale?: string): CozyMetadata[];
28
48
  }
49
+ export interface CozyRange {
50
+ readonly source: Range;
51
+ readonly id: string;
52
+ readonly items: (CozyCanvas | CozyRange)[];
53
+ readonly canvases: CozyCanvas[];
54
+ readonly ranges: CozyRange[];
55
+ getLabel(locale?: string): string | undefined;
56
+ }
29
57
  export interface CozyCanvas {
30
58
  readonly source: Canvas;
31
59
  readonly id: string;
@@ -37,45 +65,53 @@ export interface CozyCanvas {
37
65
  getThumbnailURL(minSize?: number): string;
38
66
  }
39
67
  export interface CozyMetadata {
40
- label: string;
41
- value: string;
68
+ readonly label: string;
69
+ readonly value: string;
70
+ }
71
+ export interface CozyTOCNode {
72
+ readonly id: string;
73
+ readonly type: 'range' | 'canvas';
74
+ getLabel(locale?: string): string | undefined;
75
+ children: CozyTOCNode[];
76
+ parent?: CozyTOCNode;
77
+ level: number;
42
78
  }
43
79
  export type CozyImageResource = StaticImageResource | ImageServiceResource;
44
80
  export type ImageServiceResource = DynamicImageServiceResource | Level0ImageServiceResource;
45
81
  interface BaseImageResource {
46
82
  readonly source: IIIFExternalWebResource;
47
- type: 'static' | 'dynamic' | 'level0';
48
- width: number;
49
- height: number;
83
+ readonly type: 'static' | 'dynamic' | 'level0';
84
+ readonly width: number;
85
+ readonly height: number;
50
86
  }
51
87
  export interface StaticImageResource extends BaseImageResource {
52
- type: 'static';
53
- url: string;
88
+ readonly type: 'static';
89
+ readonly url: string;
54
90
  }
55
91
  export interface DynamicImageServiceResource extends BaseImageResource {
56
- type: 'dynamic';
57
- service: ImageService2 | ImageService3;
58
- serviceUrl: string;
59
- majorVersion: number;
92
+ readonly type: 'dynamic';
93
+ readonly service: ImageService2 | ImageService3;
94
+ readonly serviceUrl: string;
95
+ readonly majorVersion: number;
60
96
  getRegionURL(bounds: Bounds, minSize?: number): string;
61
97
  }
62
98
  export interface Level0ImageServiceResource extends BaseImageResource {
63
- type: 'level0';
64
- majorVersion: number;
65
- service: ImageService2 | ImageService3;
66
- serviceUrl: string;
99
+ readonly type: 'level0';
100
+ readonly majorVersion: number;
101
+ readonly service: ImageService2 | ImageService3;
102
+ readonly serviceUrl: string;
67
103
  }
68
104
  export interface ImageRequestOptions {
69
- width?: number;
70
- height?: number;
71
- region?: 'full' | 'square' | {
105
+ readonly width?: number;
106
+ readonly height?: number;
107
+ readonly region?: 'full' | 'square' | {
72
108
  x: number;
73
109
  y: number;
74
110
  width: number;
75
111
  height: number;
76
112
  };
77
- quality?: 'default' | 'color' | 'gray' | 'bitonal';
78
- format?: 'jpg' | 'png' | 'gif' | 'webp';
113
+ readonly quality?: 'default' | 'color' | 'gray' | 'bitonal';
114
+ readonly format?: 'jpg' | 'png' | 'gif' | 'webp';
79
115
  }
80
116
  export interface Bounds {
81
117
  x: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-iiif",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "A developer-friendly collection of abstractions and utilities built on top of @iiif/presentation-3 and @iiif/parser",
5
5
  "license": "MIT",
6
6
  "author": "Rainer Simon",
@@ -19,15 +19,17 @@
19
19
  "main": "dist/index.js",
20
20
  "types": "dist/index.d.ts",
21
21
  "scripts": {
22
- "build": "vite build"
22
+ "build": "vite build",
23
+ "test": "vitest"
23
24
  },
24
25
  "exports": {
25
26
  ".": "./dist/index.js",
26
27
  "./level-0": "./dist/level-0/index.js"
27
28
  },
28
29
  "devDependencies": {
29
- "vite": "^6.2.2",
30
- "vite-plugin-dts": "^4.5.3"
30
+ "vite": "^6.2.5",
31
+ "vite-plugin-dts": "^4.5.3",
32
+ "vitest": "^3.1.1"
31
33
  },
32
34
  "dependencies": {
33
35
  "@iiif/parser": "^2.1.7",
package/src/Cozy.ts CHANGED
@@ -1,16 +1,25 @@
1
- import type { Canvas, Manifest } from '@iiif/presentation-3';
1
+ import type { Canvas, Collection, Manifest, Range } from '@iiif/presentation-3';
2
2
  import { convertPresentation2 } from '@iiif/parser/presentation-2';
3
3
  import { Traverse } from '@iiif/parser';
4
- import type { CozyCanvas, CozyManifest, CozyParseResult, ImageServiceResource } from './types';
5
4
  import {
6
5
  getImages,
7
6
  getLabel,
8
7
  getMetadata,
9
8
  getPropertyValue,
9
+ getTableOfContents,
10
10
  getThumbnailURL,
11
11
  normalizeServiceUrl,
12
12
  parseImageService
13
13
  } from './core';
14
+ import type {
15
+ CozyCanvas,
16
+ CozyCollection,
17
+ CozyCollectionItem,
18
+ CozyManifest,
19
+ CozyParseResult,
20
+ CozyRange,
21
+ ImageServiceResource
22
+ } from './types';
14
23
 
15
24
  export const Cozy = {
16
25
 
@@ -88,7 +97,13 @@ export const Cozy = {
88
97
  if (context.includes('presentation/2') || context.includes('presentation/3')) {
89
98
  const majorVersion = context.includes('presentation/2') ? 2 : 3;
90
99
 
91
- return {
100
+ const type = getPropertyValue(json, 'type');
101
+
102
+ return type.includes('Collection') ? {
103
+ type: 'collection',
104
+ url: input,
105
+ resource: parseCollectionResource(json, majorVersion)
106
+ } : {
92
107
  type: 'manifest',
93
108
  url: input,
94
109
  resource: parseManifestResource(json, majorVersion)
@@ -125,18 +140,53 @@ export const Cozy = {
125
140
 
126
141
  }
127
142
 
143
+ const parseCollectionResource = (resource: any, majorVersion: number): CozyCollection => {
144
+
145
+ const parseV3 = (collection: Collection) => {
146
+ const items: any[] = [];
147
+
148
+ const modelBuilder = new Traverse({
149
+ manifest: [item => items.push(item)]
150
+ });
151
+
152
+ modelBuilder.traverseCollection(collection);
153
+
154
+ return items.map(source => ({
155
+ id: source.id,
156
+ type: source.type,
157
+ getLabel: getLabel(source),
158
+ source
159
+ }) as CozyCollectionItem);
160
+ }
161
+
162
+ const v3: Collection = majorVersion === 2 ? convertPresentation2(resource) : resource;
163
+
164
+ const items = parseV3(v3);
165
+
166
+ return {
167
+ source: v3,
168
+ id: v3.id,
169
+ majorVersion,
170
+ items,
171
+ getLabel: getLabel(v3),
172
+ getMetadata: getMetadata(v3)
173
+ };
174
+ }
175
+
128
176
  const parseManifestResource = (resource: any, majorVersion: number): CozyManifest => {
129
177
 
130
178
  const parseV3 = (manifest: Manifest) => {
131
- const canvases: Canvas[] = [];
179
+ const sourceCanvases: Canvas[] = [];
180
+ const sourceRanges: Range[] = [];
132
181
 
133
182
  const modelBuilder = new Traverse({
134
- canvas: [canvas => { if (canvas.items) canvases.push(canvas) }]
183
+ canvas: [canvas => { if (canvas.items) sourceCanvases.push(canvas) }],
184
+ range: [range => { if (range.type === 'Range') sourceRanges.push(range) }]
135
185
  });
136
186
 
137
187
  modelBuilder.traverseManifest(manifest);
138
188
 
139
- return canvases.map((c: Canvas) => {
189
+ const canvases = sourceCanvases.map((c: Canvas) => {
140
190
  const images = getImages(c);
141
191
  return {
142
192
  source: c,
@@ -149,19 +199,49 @@ const parseManifestResource = (resource: any, majorVersion: number): CozyManifes
149
199
  getThumbnailURL: getThumbnailURL(c, images)
150
200
  } as CozyCanvas;
151
201
  });
202
+
203
+ const toRange = (source: Range): CozyRange => {
204
+ const items = source.items || [];
205
+
206
+ const nestedCanvases: CozyCanvas[] = items
207
+ .filter((item: any) => item.type === 'Canvas')
208
+ .map((item: any) => canvases.find(c => c.id === item.id)!)
209
+ .filter(Boolean);
210
+
211
+ const nestedRanges = items
212
+ .filter((item: any) => item.type === 'Range')
213
+ .map((item: any) => toRange(item));
214
+
215
+ const nestedItems = [...nestedCanvases, ...nestedRanges];
216
+
217
+ return {
218
+ source,
219
+ id: source.id,
220
+ // Maintain original order
221
+ items: items.map((i: any) => nestedItems.find(cozy => cozy.id === i.id)),
222
+ canvases: nestedCanvases,
223
+ ranges: nestedRanges,
224
+ getLabel: getLabel(source)
225
+ } as CozyRange;
226
+ }
227
+
228
+ const ranges = sourceRanges.map((source: Range) => toRange(source));
229
+ return { canvases, ranges };
152
230
  }
153
231
 
154
232
  const v3: Manifest = majorVersion === 2 ? convertPresentation2(resource) : resource;
155
233
 
156
- const canvases = parseV3(v3);
234
+ const { canvases, ranges } = parseV3(v3);
157
235
 
158
236
  return {
159
237
  source: v3,
160
238
  id: v3.id,
161
239
  majorVersion,
162
240
  canvases,
241
+ structure: ranges,
163
242
  getLabel: getLabel(v3),
164
- getMetadata: getMetadata(v3)
243
+ getMetadata: getMetadata(v3),
244
+ getTableOfContents: getTableOfContents(ranges)
165
245
  }
166
246
  }
167
247
 
package/src/core/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './canvas';
2
2
  export * from './image-service';
3
+ export * from './manifest';
3
4
  export * from './resource';
@@ -0,0 +1,38 @@
1
+ import type { CozyRange, CozyTOCNode } from '../types';
2
+
3
+ export const getTableOfContents = (ranges: CozyRange[]) => () => {
4
+
5
+ const buildTree = (range: CozyRange, parent: CozyTOCNode | undefined, level: number = 0): CozyTOCNode => {
6
+ const node: CozyTOCNode = {
7
+ id: range.id,
8
+ type: 'range',
9
+ getLabel: range.getLabel,
10
+ children: [],
11
+ parent,
12
+ level
13
+ };
14
+
15
+ if (range.items && range.items.length > 0) {
16
+ range.items.forEach(item => {
17
+ if (item.source.type === 'Range') {
18
+ const childNode = buildTree(item as CozyRange, node, level + 1);
19
+ node.children.push(childNode);
20
+ } else {
21
+ node.children.push({
22
+ id: item.id,
23
+ type: 'canvas',
24
+ getLabel: item.getLabel,
25
+ children: [],
26
+ parent: node,
27
+ level: level + 1
28
+ });
29
+ }
30
+ });
31
+ }
32
+
33
+ return node;
34
+ };
35
+
36
+ const topRanges = ranges.filter(range => range.source.behavior?.includes('top'));
37
+ return topRanges.map(range => buildTree(range, undefined));
38
+ }
package/src/types.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { Manifest, Canvas, ImageService2, ImageService3, IIIFExternalWebResource } from '@iiif/presentation-3';
1
+ import type { Manifest, Canvas, ImageService2, ImageService3, IIIFExternalWebResource, Collection, Range } from '@iiif/presentation-3';
2
2
 
3
3
  export type CozyParseResult =
4
+ | { type: 'collection', url: string, resource: CozyCollection }
4
5
  | { type: 'manifest'; url: string, resource: CozyManifest }
5
6
  | { type: 'iiif-image'; url: string, resource: CozyImageResource }
6
7
  | { type: 'plain-image'; url: string }
@@ -11,6 +12,34 @@ export type CozyParseResult =
11
12
  message: string;
12
13
  };
13
14
 
15
+ export interface CozyCollection {
16
+
17
+ readonly majorVersion: number;
18
+
19
+ readonly source: Collection;
20
+
21
+ readonly id: string;
22
+
23
+ readonly items: CozyCollectionItem[];
24
+
25
+ getLabel(locale?: string): string | undefined;
26
+
27
+ getMetadata(locale?: string): CozyMetadata[];
28
+
29
+ }
30
+
31
+ export interface CozyCollectionItem {
32
+
33
+ readonly id: string;
34
+
35
+ readonly type: string;
36
+
37
+ readonly source: any;
38
+
39
+ getLabel(locale?: string): string | undefined;
40
+
41
+ }
42
+
14
43
  export interface CozyManifest {
15
44
 
16
45
  readonly majorVersion: number;
@@ -21,12 +50,32 @@ export interface CozyManifest {
21
50
 
22
51
  readonly canvases: CozyCanvas[];
23
52
 
53
+ readonly structure: CozyRange[];
54
+
24
55
  getLabel(locale?: string): string | undefined;
25
56
 
57
+ getTableOfContents(): CozyTOCNode[];
58
+
26
59
  getMetadata(locale?: string): CozyMetadata[];
27
60
 
28
61
  }
29
62
 
63
+ export interface CozyRange {
64
+
65
+ readonly source: Range;
66
+
67
+ readonly id: string;
68
+
69
+ readonly items: (CozyCanvas | CozyRange)[];
70
+
71
+ readonly canvases: CozyCanvas[];
72
+
73
+ readonly ranges: CozyRange[];
74
+
75
+ getLabel(locale?: string): string | undefined;
76
+
77
+ }
78
+
30
79
  export interface CozyCanvas {
31
80
 
32
81
  readonly source: Canvas;
@@ -49,9 +98,25 @@ export interface CozyCanvas {
49
98
 
50
99
  export interface CozyMetadata {
51
100
 
52
- label: string;
101
+ readonly label: string;
53
102
 
54
- value: string;
103
+ readonly value: string;
104
+
105
+ }
106
+
107
+ export interface CozyTOCNode {
108
+
109
+ readonly id: string;
110
+
111
+ readonly type: 'range' | 'canvas';
112
+
113
+ getLabel(locale?: string): string | undefined;
114
+
115
+ children: CozyTOCNode[];
116
+
117
+ parent?: CozyTOCNode;
118
+
119
+ level: number;
55
120
 
56
121
  }
57
122
 
@@ -67,31 +132,31 @@ interface BaseImageResource {
67
132
 
68
133
  readonly source: IIIFExternalWebResource;
69
134
 
70
- type: 'static' | 'dynamic' | 'level0';
135
+ readonly type: 'static' | 'dynamic' | 'level0';
71
136
 
72
- width: number;
137
+ readonly width: number;
73
138
 
74
- height: number;
139
+ readonly height: number;
75
140
 
76
141
  }
77
142
 
78
143
  export interface StaticImageResource extends BaseImageResource {
79
144
 
80
- type: 'static';
145
+ readonly type: 'static';
81
146
 
82
- url: string;
147
+ readonly url: string;
83
148
 
84
149
  }
85
150
 
86
151
  export interface DynamicImageServiceResource extends BaseImageResource {
87
152
 
88
- type: 'dynamic';
153
+ readonly type: 'dynamic';
89
154
 
90
- service: ImageService2 | ImageService3;
155
+ readonly service: ImageService2 | ImageService3;
91
156
 
92
- serviceUrl: string;
157
+ readonly serviceUrl: string;
93
158
 
94
- majorVersion: number;
159
+ readonly majorVersion: number;
95
160
 
96
161
  getRegionURL(bounds: Bounds, minSize?: number): string;
97
162
 
@@ -99,28 +164,28 @@ export interface DynamicImageServiceResource extends BaseImageResource {
99
164
 
100
165
  export interface Level0ImageServiceResource extends BaseImageResource {
101
166
 
102
- type: 'level0';
167
+ readonly type: 'level0';
103
168
 
104
- majorVersion: number;
169
+ readonly majorVersion: number;
105
170
 
106
- service: ImageService2 | ImageService3;
171
+ readonly service: ImageService2 | ImageService3;
107
172
 
108
- serviceUrl: string;
173
+ readonly serviceUrl: string;
109
174
 
110
175
  }
111
176
 
112
177
 
113
178
  export interface ImageRequestOptions {
114
179
 
115
- width?: number;
180
+ readonly width?: number;
116
181
 
117
- height?: number;
182
+ readonly height?: number;
118
183
 
119
- region?: 'full' | 'square' | { x: number; y: number; width: number; height: number };
184
+ readonly region?: 'full' | 'square' | { x: number; y: number; width: number; height: number };
120
185
 
121
- quality?: 'default' | 'color' | 'gray' | 'bitonal';
186
+ readonly quality?: 'default' | 'color' | 'gray' | 'bitonal';
122
187
 
123
- format?: 'jpg' | 'png' | 'gif' | 'webp';
188
+ readonly format?: 'jpg' | 'png' | 'gif' | 'webp';
124
189
 
125
190
  }
126
191
 
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Cozy, CozyManifest } from '../src';
3
+
4
+ import { COLLECTION, WITH_STRUCTURES } from './fixtures';
5
+
6
+ describe('Cozy', () => {
7
+
8
+ it('should parse collection manifests correctly', async () => {
9
+ const result = await Cozy.parseURL(COLLECTION);
10
+ expect(result.type).toBe('collection');
11
+ });
12
+
13
+ it('should parse strctures in presentation manifests', async () => {
14
+ const result = await Cozy.parseURL(WITH_STRUCTURES);
15
+ expect(result.type).toBe('manifest');
16
+ expect('resource' in result).toBeTruthy();
17
+
18
+ const manifest = (result as any).resource as CozyManifest;
19
+ expect(manifest.structure.length > 0).toBeTruthy();
20
+
21
+ const tableOfContents = manifest.getTableOfContents();
22
+ expect(tableOfContents.length).toBe(1);
23
+ expect(tableOfContents[0].children.length).toBe(14);
24
+ })
25
+
26
+ });
27
+
@@ -0,0 +1,5 @@
1
+ export const COLLECTION =
2
+ 'https://www.davidrumsey.com/luna/servlet/iiif/collection/s/1k986a';
3
+
4
+ export const WITH_STRUCTURES =
5
+ 'https://lib.is/IE19255085/manifest';
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ watch: false
6
+ }
7
+ });