ochre-sdk 1.0.78 → 1.1.0

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.
Files changed (84) hide show
  1. package/README.md +31 -3
  2. package/dist/_virtual/_rolldown/runtime.mjs +13 -0
  3. package/dist/categories.d.mts +249 -0
  4. package/dist/categories.mjs +259 -0
  5. package/dist/constants.d.mts +14 -0
  6. package/dist/constants.mjs +18 -1
  7. package/dist/errors.d.mts +23 -0
  8. package/dist/{utilities.mjs → errors.mjs} +33 -84
  9. package/dist/fetchers/gallery.mjs +15 -51
  10. package/dist/fetchers/item-children.mjs +20 -66
  11. package/dist/fetchers/item-links.mjs +26 -79
  12. package/dist/fetchers/item-ocr-data.d.mts +2 -2
  13. package/dist/fetchers/item-ocr-data.mjs +11 -15
  14. package/dist/fetchers/item.d.mts +0 -15
  15. package/dist/fetchers/item.mjs +20 -72
  16. package/dist/fetchers/request.d.mts +70 -0
  17. package/dist/fetchers/request.mjs +100 -0
  18. package/dist/fetchers/set/items.mjs +27 -73
  19. package/dist/fetchers/set/property-values.d.mts +2 -3
  20. package/dist/fetchers/set/property-values.mjs +96 -130
  21. package/dist/fetchers/website-metadata.mjs +35 -57
  22. package/dist/fetchers/website.d.mts +2 -3
  23. package/dist/fetchers/website.mjs +22 -31
  24. package/dist/getters.d.mts +78 -148
  25. package/dist/getters.mjs +127 -208
  26. package/dist/helpers.d.mts +0 -4
  27. package/dist/helpers.mjs +19 -6
  28. package/dist/index.d.mts +8 -6
  29. package/dist/index.mjs +6 -4
  30. package/dist/ocr.d.mts +37 -0
  31. package/dist/ocr.mjs +52 -0
  32. package/dist/parsers/helpers.d.mts +21 -1
  33. package/dist/parsers/helpers.mjs +26 -6
  34. package/dist/parsers/index.d.mts +0 -8
  35. package/dist/parsers/index.mjs +118 -259
  36. package/dist/parsers/languages.d.mts +72 -0
  37. package/dist/parsers/languages.mjs +132 -0
  38. package/dist/parsers/multilingual.d.mts +49 -74
  39. package/dist/parsers/multilingual.mjs +88 -189
  40. package/dist/parsers/property-token.d.mts +34 -0
  41. package/dist/parsers/property-token.mjs +29 -0
  42. package/dist/parsers/string.d.mts +19 -0
  43. package/dist/parsers/string.mjs +45 -25
  44. package/dist/parsers/website/bounds.d.mts +10 -0
  45. package/dist/parsers/website/bounds.mjs +28 -0
  46. package/dist/parsers/website/components.d.mts +91 -0
  47. package/dist/parsers/website/components.mjs +681 -0
  48. package/dist/parsers/website/index.d.mts +0 -7
  49. package/dist/parsers/website/index.mjs +92 -1153
  50. package/dist/parsers/website/links.d.mts +36 -0
  51. package/dist/parsers/website/links.mjs +58 -0
  52. package/dist/parsers/website/messages.d.mts +24 -0
  53. package/dist/parsers/website/messages.mjs +31 -0
  54. package/dist/parsers/website/options.d.mts +6 -0
  55. package/dist/parsers/website/options.mjs +114 -0
  56. package/dist/parsers/website/properties.d.mts +12 -0
  57. package/dist/parsers/website/properties.mjs +158 -0
  58. package/dist/parsers/website/reader.d.mts +54 -4
  59. package/dist/parsers/website/reader.mjs +65 -20
  60. package/dist/parsers/website/slug.d.mts +64 -0
  61. package/dist/parsers/website/slug.mjs +82 -0
  62. package/dist/parsers/website/styles.d.mts +28 -0
  63. package/dist/parsers/website/styles.mjs +103 -0
  64. package/dist/parsers/website/walk.d.mts +68 -0
  65. package/dist/parsers/website/walk.mjs +116 -0
  66. package/dist/query.d.mts +66 -18
  67. package/dist/query.mjs +202 -48
  68. package/dist/reflection.d.mts +64 -0
  69. package/dist/reflection.mjs +79 -0
  70. package/dist/schemas.d.mts +7 -0
  71. package/dist/schemas.mjs +12 -3
  72. package/dist/types/index.d.mts +1 -31
  73. package/dist/types/utilities.d.mts +9 -0
  74. package/dist/types/utilities.mjs +1 -0
  75. package/dist/types/website.d.mts +49 -58
  76. package/dist/xml/metadata.d.mts +16 -0
  77. package/dist/xml/metadata.mjs +32 -11
  78. package/dist/xml/schemas.d.mts +5970 -3
  79. package/dist/xml/schemas.mjs +43 -45
  80. package/dist/xml/types.d.mts +13 -30
  81. package/dist/xquery.d.mts +46 -0
  82. package/dist/xquery.mjs +66 -0
  83. package/package.json +3 -3
  84. package/dist/utilities.d.mts +0 -54
@@ -0,0 +1,681 @@
1
+ import { componentSchema } from "../../schemas.mjs";
2
+ import { parseXMLContent, transformPermanentIdentificationUrlToItemLink } from "../string.mjs";
3
+ import { parseStringContent } from "../helpers.mjs";
4
+ import { parseBibliographyList, parseIdentification, parseLinks, parseSimplifiedProperties } from "../index.mjs";
5
+ import { parseBounds } from "./bounds.mjs";
6
+ import { findWebsiteLink, findWebsiteLinkByCategories, getWebsiteLinks, parseWebsiteLinkTarget, webImageFromLink } from "./links.mjs";
7
+ import { formatComponentError, formatXMLWebsiteResourceMetadata } from "./messages.mjs";
8
+ import { parseWebsiteOptions } from "./options.mjs";
9
+ import { websitePresentationReader } from "./reader.mjs";
10
+ import { parseResponsiveCssStyles } from "./styles.mjs";
11
+ import { normalizeWebsiteResources } from "./walk.mjs";
12
+ import * as v from "valibot";
13
+ //#region src/parsers/website/components.ts
14
+ /**
15
+ * Default values for a collection's display properties, shared between the
16
+ * "collection" component and the "query" component's collection overrides.
17
+ */
18
+ const COLLECTION_PROPERTY_DEFAULTS = {
19
+ variant: "slide",
20
+ paginationVariant: "default",
21
+ loadingVariant: "skeleton",
22
+ minimumColumnCount: null,
23
+ maximumColumnCount: null,
24
+ expectedItemCount: null,
25
+ isSortDisplayed: false,
26
+ isUsingQueryParams: false,
27
+ isInteractive: true
28
+ };
29
+ /**
30
+ * Default values for a collection's image display properties, merged separately
31
+ * from {@link COLLECTION_PROPERTY_DEFAULTS} because overrides of the nested
32
+ * "image" object are themselves partial.
33
+ */
34
+ const COLLECTION_IMAGE_DEFAULTS = {
35
+ layout: "start",
36
+ fit: "fit",
37
+ alignment: null,
38
+ isPlaceholderDisplayed: true
39
+ };
40
+ /**
41
+ * Reads the collection display properties explicitly set on a reader, omitting
42
+ * any that are unset. The "collection" component merges these over
43
+ * {@link COLLECTION_PROPERTY_DEFAULTS} and {@link COLLECTION_IMAGE_DEFAULTS},
44
+ * while the "query" component uses them as partial overrides for its embedded
45
+ * collection.
46
+ */
47
+ function parseCollectionPropertyOverrides(reader) {
48
+ const overrides = {};
49
+ const imageOverrides = {};
50
+ function read(key, label) {
51
+ const value = reader.value(label);
52
+ if (value != null) overrides[key] = value;
53
+ }
54
+ function readImage(key, label) {
55
+ const value = reader.value(label);
56
+ if (value != null) imageOverrides[key] = value;
57
+ }
58
+ read("variant", "variant");
59
+ read("paginationVariant", "pagination-variant");
60
+ read("loadingVariant", "loading-variant");
61
+ read("minimumColumnCount", "minimum-column-count");
62
+ read("maximumColumnCount", "maximum-column-count");
63
+ read("expectedItemCount", "item-count");
64
+ read("isSortDisplayed", "sort-displayed");
65
+ read("isUsingQueryParams", "is-using-query-params");
66
+ read("isInteractive", "is-interactive");
67
+ readImage("layout", "image-layout");
68
+ readImage("fit", "image-fit");
69
+ readImage("alignment", "image-alignment");
70
+ readImage("isPlaceholderDisplayed", "image-placeholder-displayed");
71
+ return Object.keys(imageOverrides).length > 0 ? {
72
+ ...overrides,
73
+ image: imageOverrides
74
+ } : overrides;
75
+ }
76
+ /**
77
+ * Parses the "use-property" values defining which item properties a collection
78
+ * displays, returning `null` when none are set.
79
+ */
80
+ function parseCollectionDisplayedProperties(reader) {
81
+ const property = reader.property("use-property");
82
+ if (property == null) return null;
83
+ return property.values.flatMap((value) => value.uuid === null ? [] : [{
84
+ uuid: value.uuid,
85
+ label: value.label
86
+ }]);
87
+ }
88
+ const THREE_D_VIEWER_COMPONENT_DEFAULTS = {
89
+ isInteractive: true,
90
+ isControlsDisplayed: true
91
+ };
92
+ function parse3dViewerComponent(parameters) {
93
+ const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
94
+ const resourceLink = findWebsiteLink(websiteLinks, "resource", (link) => link.fileFormat === "model/obj");
95
+ if (resourceLink == null) throw new Error(formatComponentError("Resource link not found", "3d-viewer", elementResource), { cause: componentProperty });
96
+ const properties = { ...THREE_D_VIEWER_COMPONENT_DEFAULTS };
97
+ componentReader.readAll(properties, {
98
+ isInteractive: "is-interactive",
99
+ isControlsDisplayed: "controls-displayed"
100
+ });
101
+ const { isInteractive, isControlsDisplayed } = properties;
102
+ return {
103
+ component: "3d-viewer",
104
+ linkUuid: resourceLink.uuid,
105
+ fileSize: resourceLink.fileSize,
106
+ isInteractive,
107
+ isControlsDisplayed
108
+ };
109
+ }
110
+ function parseAdvancedSearchComponent(parameters) {
111
+ const { componentProperty, componentReader, elementResource, options, pageSlugs } = parameters;
112
+ const boundElementPropertyUuid = componentReader.uuid("bound-element");
113
+ const href = parseWebsiteLinkTarget(componentReader.valueNode("link-to"), pageSlugs);
114
+ if (boundElementPropertyUuid == null && href == null) throw new Error(formatComponentError("Bound element or href not found", "advanced-search", elementResource), { cause: componentProperty });
115
+ return {
116
+ component: "advanced-search",
117
+ boundElementUuid: boundElementPropertyUuid,
118
+ href,
119
+ options: parseWebsiteOptions(elementResource.options, options)
120
+ };
121
+ }
122
+ function parseAnnotatedDocumentComponent(parameters) {
123
+ const { componentProperty, elementResource, websiteLinks } = parameters;
124
+ const documentLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "internalDocument");
125
+ if (documentLink == null) throw new Error(formatComponentError("Document link not found", "annotated-document", elementResource), { cause: componentProperty });
126
+ return {
127
+ component: "annotated-document",
128
+ linkUuid: documentLink.uuid
129
+ };
130
+ }
131
+ const ANNOTATED_IMAGE_COMPONENT_DEFAULTS = {
132
+ isFilterInputDisplayed: true,
133
+ isOptionsDisplayed: true,
134
+ isAnnotationHighlightsDisplayed: true,
135
+ isAnnotationTooltipsDisplayed: true
136
+ };
137
+ function parseAnnotatedImageComponent(parameters) {
138
+ const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
139
+ const imageLinks = getWebsiteLinks(websiteLinks, "resource").filter((link) => link.type === "image" || link.type === "IIIF");
140
+ if (imageLinks.length === 0) throw new Error(formatComponentError("Image link not found", "annotated-image", elementResource), { cause: componentProperty });
141
+ const properties = { ...ANNOTATED_IMAGE_COMPONENT_DEFAULTS };
142
+ componentReader.readAll(properties, {
143
+ isFilterInputDisplayed: "filter-input-displayed",
144
+ isOptionsDisplayed: "options-displayed",
145
+ isAnnotationHighlightsDisplayed: "annotation-highlights-displayed",
146
+ isAnnotationTooltipsDisplayed: "annotation-tooltips-displayed"
147
+ });
148
+ const { isFilterInputDisplayed, isOptionsDisplayed, isAnnotationHighlightsDisplayed, isAnnotationTooltipsDisplayed } = properties;
149
+ return {
150
+ component: "annotated-image",
151
+ linkUuid: imageLinks[0].uuid,
152
+ isFilterInputDisplayed,
153
+ isOptionsDisplayed,
154
+ isAnnotationHighlightsDisplayed,
155
+ isAnnotationTooltipsDisplayed
156
+ };
157
+ }
158
+ const AUDIO_PLAYER_COMPONENT_DEFAULTS = {
159
+ isSpeedControlsDisplayed: true,
160
+ isVolumeControlsDisplayed: true,
161
+ isSeekBarDisplayed: true
162
+ };
163
+ function parseAudioPlayerComponent(parameters) {
164
+ const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
165
+ const audioLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "audio");
166
+ if (audioLink == null) throw new Error(formatComponentError("Audio link not found", "audio-player", elementResource), { cause: componentProperty });
167
+ const properties = { ...AUDIO_PLAYER_COMPONENT_DEFAULTS };
168
+ componentReader.readAll(properties, {
169
+ isSpeedControlsDisplayed: "speed-controls-displayed",
170
+ isVolumeControlsDisplayed: "volume-controls-displayed",
171
+ isSeekBarDisplayed: "seek-bar-displayed"
172
+ });
173
+ const { isSpeedControlsDisplayed, isVolumeControlsDisplayed, isSeekBarDisplayed } = properties;
174
+ return {
175
+ component: "audio-player",
176
+ linkUuid: audioLink.uuid,
177
+ isSpeedControlsDisplayed,
178
+ isVolumeControlsDisplayed,
179
+ isSeekBarDisplayed
180
+ };
181
+ }
182
+ function parseBibliographyComponent(parameters) {
183
+ const { componentProperty, componentReader, elementResource, websiteLinks, options } = parameters;
184
+ const bibliographies = parseBibliographyList(elementResource.bibliographies, options);
185
+ if (websiteLinks.length === 0 && bibliographies.length === 0) throw new Error(formatComponentError("No links found", "bibliography", elementResource), { cause: componentProperty });
186
+ const layout = componentReader.valueOr("layout", "long");
187
+ const isSourceDocumentDisplayed = componentReader.valueOr("source-document-displayed", true);
188
+ return {
189
+ component: "bibliography",
190
+ linkUuids: websiteLinks.map((link) => link.uuid),
191
+ bibliographies,
192
+ layout,
193
+ isSourceDocumentDisplayed
194
+ };
195
+ }
196
+ function parseButtonComponent(parameters) {
197
+ const { componentProperty, componentReader, elementResource, websiteLinks, options, pageSlugs } = parameters;
198
+ const variant = componentReader.valueOr("variant", "default");
199
+ let isExternal = false;
200
+ let isRelative = false;
201
+ let href = parseWebsiteLinkTarget(componentReader.valueNode("navigate-to"), pageSlugs);
202
+ if (href === null) {
203
+ href = parseWebsiteLinkTarget(componentReader.valueNode("link-to"), pageSlugs);
204
+ if (href === null) throw new Error(formatComponentError("Properties “navigate-to” or “link-to” not found", "button", elementResource), { cause: componentProperty });
205
+ isExternal = href.startsWith("http");
206
+ isRelative = !href.startsWith("/");
207
+ }
208
+ const startIcon = componentReader.value("start-icon");
209
+ const endIcon = componentReader.value("end-icon");
210
+ const imageLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "image" || link.type === "IIIF");
211
+ const image = imageLink == null ? null : webImageFromLink(imageLink, "high");
212
+ const childResources = normalizeWebsiteResources(elementResource.resource);
213
+ const elements = [];
214
+ for (const childResource of childResources) {
215
+ const childReader = websitePresentationReader(parseSimplifiedProperties(childResource.properties, options));
216
+ if (childReader.value("presentation") !== "element") continue;
217
+ if (childReader.nestedByValue("presentation", "element").value("component") === "button") continue;
218
+ elements.push(parseWebElement(childResource, options, pageSlugs));
219
+ }
220
+ return {
221
+ component: "button",
222
+ variant,
223
+ href,
224
+ isExternal,
225
+ isRelative,
226
+ label: elementResource.document && "content" in elementResource.document ? parseXMLContent(elementResource.document, options) : null,
227
+ startIcon,
228
+ endIcon,
229
+ image,
230
+ elements
231
+ };
232
+ }
233
+ const COLLECTION_FILTER_DEFAULTS = {
234
+ isResultsBarDisplayed: false,
235
+ isInputDisplayed: false,
236
+ isLimitedToInputFilter: false,
237
+ isLimitedToLeafPropertyValues: false,
238
+ isSidebarDisplayed: false,
239
+ sidebarSort: "default",
240
+ isSidebarHelpTooltipsDisplayed: false
241
+ };
242
+ function parseCollectionComponent(parameters) {
243
+ const { componentProperty, componentReader, elementResource, websiteLinks, options } = parameters;
244
+ const setLinks = getWebsiteLinks(websiteLinks, "set");
245
+ if (setLinks.length === 0) throw new Error(formatComponentError("Set links not found", "collection", elementResource), { cause: componentProperty });
246
+ const filter = { ...COLLECTION_FILTER_DEFAULTS };
247
+ componentReader.readAll(filter, {
248
+ isResultsBarDisplayed: "filter-results-bar-displayed",
249
+ isInputDisplayed: "filter-input-displayed",
250
+ isLimitedToInputFilter: "filter-limit-to-input-filter",
251
+ isLimitedToLeafPropertyValues: "filter-limit-to-leaf-property-values",
252
+ isSidebarDisplayed: "filter-sidebar-displayed",
253
+ sidebarSort: "filter-sidebar-sort",
254
+ isSidebarHelpTooltipsDisplayed: "filter-sidebar-help-tooltips-displayed"
255
+ });
256
+ const componentOptions = parseWebsiteOptions(elementResource.options, options);
257
+ const propertyOverrides = parseCollectionPropertyOverrides(componentReader);
258
+ return {
259
+ component: "collection",
260
+ linkUuids: setLinks.map((link) => link.uuid),
261
+ displayedProperties: parseCollectionDisplayedProperties(componentReader),
262
+ ...COLLECTION_PROPERTY_DEFAULTS,
263
+ ...propertyOverrides,
264
+ image: {
265
+ ...COLLECTION_IMAGE_DEFAULTS,
266
+ ...propertyOverrides.image
267
+ },
268
+ filter,
269
+ options: componentOptions
270
+ };
271
+ }
272
+ function parseEmptySpaceComponent(parameters) {
273
+ const { componentReader } = parameters;
274
+ return {
275
+ component: "empty-space",
276
+ height: componentReader.stringValue("height"),
277
+ width: componentReader.stringValue("width")
278
+ };
279
+ }
280
+ function parseEntriesComponent(parameters) {
281
+ const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
282
+ const entriesLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
283
+ if (entriesLink == null) throw new Error(formatComponentError("Entries link not found", "entries", elementResource), { cause: componentProperty });
284
+ const variant = componentReader.valueOr("variant", "entry");
285
+ const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", false);
286
+ return {
287
+ component: "entries",
288
+ linkUuid: entriesLink.uuid,
289
+ variant,
290
+ isFilterInputDisplayed
291
+ };
292
+ }
293
+ function parseIframeComponent(parameters) {
294
+ const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
295
+ const webpageLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "webpage");
296
+ if (webpageLink?.href == null) throw new Error(formatComponentError("URL not found", "iframe", elementResource), { cause: componentProperty });
297
+ return {
298
+ component: "iframe",
299
+ href: transformPermanentIdentificationUrlToItemLink(webpageLink.href),
300
+ height: componentReader.stringValue("height"),
301
+ width: componentReader.stringValue("width")
302
+ };
303
+ }
304
+ function parseIiifViewerComponent(parameters) {
305
+ const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
306
+ const manifestLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "IIIF");
307
+ if (manifestLink == null) throw new Error(formatComponentError("Manifest link not found", "iiif-viewer", elementResource), { cause: componentProperty });
308
+ const variant = componentReader.valueOr("variant", "universal-viewer");
309
+ return {
310
+ component: "iiif-viewer",
311
+ linkUuid: manifestLink.uuid,
312
+ variant
313
+ };
314
+ }
315
+ /**
316
+ * The defaults an image component falls back to
317
+ *
318
+ * Annotated rather than `as const`, so each field keeps the full union its
319
+ * type allows and the parser can still compare against the other members.
320
+ */
321
+ const IMAGE_COMPONENT_DEFAULTS = {
322
+ imageQuality: "high",
323
+ variant: "default",
324
+ captionLayout: "bottom",
325
+ isFullWidth: true,
326
+ isFullHeight: true,
327
+ captionSource: "name",
328
+ altTextSource: "name",
329
+ isTransparentBackground: false,
330
+ isCover: false
331
+ };
332
+ function parseImageComponent(parameters) {
333
+ const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
334
+ if (websiteLinks.length === 0) throw new Error(formatComponentError("No links found", "image", elementResource), { cause: componentProperty });
335
+ const properties = { ...IMAGE_COMPONENT_DEFAULTS };
336
+ componentReader.readAll(properties, {
337
+ imageQuality: "image-quality",
338
+ variant: "variant",
339
+ captionLayout: "layout-caption",
340
+ isFullWidth: "is-full-width",
341
+ isFullHeight: "is-full-height",
342
+ captionSource: "source-caption",
343
+ altTextSource: "alt-text-source",
344
+ isTransparentBackground: "is-transparent",
345
+ isCover: "is-cover"
346
+ });
347
+ const { imageQuality, variant, captionLayout, isFullWidth, isFullHeight, captionSource, altTextSource, isTransparentBackground, isCover } = properties;
348
+ const images = Array.from(websiteLinks, (link) => webImageFromLink(link, imageQuality));
349
+ const width = componentReader.stringValue("width");
350
+ const height = componentReader.stringValue("height");
351
+ const variantReader = componentReader.nested("variant");
352
+ let carouselOptions = null;
353
+ if (images.length > 1) {
354
+ const secondsPerImage = variant === "carousel" ? variantReader.stringValue("seconds-per-image") : null;
355
+ carouselOptions = { secondsPerImage: Number(secondsPerImage ?? 5) };
356
+ }
357
+ let heroOptions = null;
358
+ if (variant === "hero") heroOptions = {
359
+ isBackgroundImageDisplayed: variantReader.valueOr("background-image-displayed", true),
360
+ isDocumentDisplayed: variantReader.valueOr("document-displayed", true)
361
+ };
362
+ return {
363
+ component: "image",
364
+ images,
365
+ variant,
366
+ width,
367
+ height,
368
+ isFullWidth,
369
+ isFullHeight,
370
+ imageQuality,
371
+ captionLayout,
372
+ captionSource,
373
+ altTextSource,
374
+ isTransparentBackground,
375
+ isCover,
376
+ carouselOptions,
377
+ heroOptions
378
+ };
379
+ }
380
+ function parseImageGalleryComponent(parameters) {
381
+ const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
382
+ const galleryLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
383
+ if (galleryLink == null) throw new Error(formatComponentError("Image gallery link not found", "image-gallery", elementResource), { cause: componentProperty });
384
+ const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", true);
385
+ return {
386
+ component: "image-gallery",
387
+ linkUuid: galleryLink.uuid,
388
+ isFilterInputDisplayed
389
+ };
390
+ }
391
+ const MAP_COMPONENT_DEFAULTS = {
392
+ isInteractive: true,
393
+ isClustered: false,
394
+ isUsingPins: false,
395
+ isControlsDisplayed: false,
396
+ isFullHeight: false
397
+ };
398
+ function parseMapComponent(parameters) {
399
+ const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
400
+ const mapLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
401
+ if (mapLink == null) throw new Error(formatComponentError("Map link not found", "map", elementResource), { cause: componentProperty });
402
+ const properties = { ...MAP_COMPONENT_DEFAULTS };
403
+ componentReader.readAll(properties, {
404
+ isInteractive: "is-interactive",
405
+ isClustered: "is-clustered",
406
+ isUsingPins: "is-using-pins",
407
+ isControlsDisplayed: "controls-displayed",
408
+ isFullHeight: "is-full-height"
409
+ });
410
+ const { isInteractive, isClustered, isUsingPins, isControlsDisplayed, isFullHeight } = properties;
411
+ const customBasemap = componentReader.value("custom-basemap");
412
+ let initialBounds = null;
413
+ const initialBoundsProperty = componentReader.value("initial-bounds");
414
+ if (initialBoundsProperty !== null) initialBounds = parseBounds(String(initialBoundsProperty));
415
+ let maximumBounds = null;
416
+ const maximumBoundsProperty = componentReader.value("maximum-bounds");
417
+ if (maximumBoundsProperty !== null) maximumBounds = parseBounds(String(maximumBoundsProperty));
418
+ return {
419
+ component: "map",
420
+ linkUuid: mapLink.uuid,
421
+ customBasemap,
422
+ initialBounds,
423
+ maximumBounds,
424
+ isInteractive,
425
+ isClustered,
426
+ isUsingPins,
427
+ isControlsDisplayed,
428
+ isFullHeight
429
+ };
430
+ }
431
+ function parseQueryItemQueries(propertyVariables, queryLanguage, elementResource) {
432
+ const queries = [];
433
+ for (const propertyVariable of propertyVariables) {
434
+ if (propertyVariable.uuid === null) throw new Error(formatComponentError("Property variable UUID not found", "query", elementResource), { cause: propertyVariable });
435
+ const dataType = propertyVariable.dataType;
436
+ if (dataType === "coordinate") throw new Error(formatComponentError("Query prompts with data type \"coordinate\" are not supported", "query", elementResource), { cause: propertyVariable });
437
+ queries.push({
438
+ target: "property",
439
+ propertyVariable: propertyVariable.uuid,
440
+ dataType,
441
+ matchMode: "exact",
442
+ isCaseSensitive: true,
443
+ language: queryLanguage
444
+ });
445
+ }
446
+ return queries;
447
+ }
448
+ function parseQueryComponent(parameters) {
449
+ const { componentProperty, componentReader, elementResource, websiteLinks, options } = parameters;
450
+ const setLinks = getWebsiteLinks(websiteLinks, "set");
451
+ if (setLinks.length === 0) throw new Error(formatComponentError("Set links not found", "query", elementResource), { cause: componentProperty });
452
+ if (componentProperty.properties.length === 0) throw new Error(formatComponentError("Query properties not found", "query", elementResource), { cause: componentProperty });
453
+ const items = [];
454
+ for (const queryItem of componentProperty.properties) {
455
+ const queryReader = websitePresentationReader(queryItem.properties);
456
+ const label = queryReader.multilingualValue("query-prompt", options);
457
+ if (label === null) continue;
458
+ const propertyVariables = queryReader.property("use-property")?.values.filter((value) => value.uuid !== null) ?? [];
459
+ const queryLanguage = options.languages[0];
460
+ if (queryLanguage == null) throw new Error(formatComponentError("Query language not found", "query", elementResource));
461
+ const queries = parseQueryItemQueries(propertyVariables, queryLanguage, elementResource);
462
+ const startIcon = queryReader.value("start-icon");
463
+ const endIcon = queryReader.value("end-icon");
464
+ items.push({
465
+ label,
466
+ queries,
467
+ startIcon,
468
+ endIcon
469
+ });
470
+ }
471
+ if (items.length === 0) throw new Error(formatComponentError("No queries found", "query", elementResource), { cause: componentProperty });
472
+ const componentOptions = parseWebsiteOptions(elementResource.options, options);
473
+ const collectionProperties = parseCollectionPropertyOverrides(componentReader.nestedByValue("sub-component-override", "collection"));
474
+ const displayedProperties = parseCollectionDisplayedProperties(componentReader);
475
+ if (displayedProperties != null) collectionProperties.displayedProperties = displayedProperties;
476
+ return {
477
+ component: "query",
478
+ linkUuids: setLinks.map((link) => link.uuid),
479
+ items,
480
+ options: componentOptions,
481
+ collectionProperties
482
+ };
483
+ }
484
+ function parseTableComponent(parameters) {
485
+ const { componentProperty, elementResource, websiteLinks } = parameters;
486
+ const tableLink = findWebsiteLink(websiteLinks, "set");
487
+ if (tableLink == null) throw new Error(formatComponentError("Table link not found", "table", elementResource), { cause: componentProperty });
488
+ return {
489
+ component: "table",
490
+ linkUuid: tableLink.uuid
491
+ };
492
+ }
493
+ function parseSearchBarComponent(parameters) {
494
+ const { componentProperty, componentReader, elementResource, options, pageSlugs } = parameters;
495
+ const queryVariant = componentReader.valueOr("query-variant", "submit");
496
+ const boundElementUuid = componentReader.uuid("bound-element");
497
+ const href = parseWebsiteLinkTarget(componentReader.valueNode("link-to"), pageSlugs);
498
+ if (boundElementUuid === null && href === null) throw new Error(formatComponentError("Bound element or href not found", "search-bar", elementResource), { cause: componentProperty });
499
+ return {
500
+ component: "search-bar",
501
+ queryVariant,
502
+ placeholder: componentReader.multilingualValue("placeholder-text", options),
503
+ baseFilterQueries: componentReader.value("base-filter-queries")?.replaceAll(String.raw`\{`, "{").replaceAll(String.raw`\}`, "}") ?? null,
504
+ boundElementUuid,
505
+ href
506
+ };
507
+ }
508
+ function parseTextComponent(parameters) {
509
+ const { componentProperty, componentReader, elementResource, options } = parameters;
510
+ const content = elementResource.document && "content" in elementResource.document ? parseXMLContent(elementResource.document, options) : null;
511
+ if (content == null) throw new Error(formatComponentError("Content not found", "text", elementResource), { cause: componentProperty });
512
+ let variantName = "block";
513
+ let variant;
514
+ const variantProperty = componentReader.property("variant");
515
+ if (variantProperty !== null) {
516
+ const variantReader = websitePresentationReader(variantProperty.properties);
517
+ variantName = variantProperty.values[0].content;
518
+ switch (variantName) {
519
+ case "paragraph":
520
+ variant = {
521
+ name: variantName,
522
+ size: variantReader.valueOr("size", "md")
523
+ };
524
+ break;
525
+ case "label":
526
+ variant = {
527
+ name: variantName,
528
+ size: variantReader.valueOr("size", "md")
529
+ };
530
+ break;
531
+ case "heading":
532
+ variant = {
533
+ name: variantName,
534
+ size: variantReader.valueOr("size", "md")
535
+ };
536
+ break;
537
+ case "display":
538
+ variant = {
539
+ name: variantName,
540
+ size: variantReader.valueOr("size", "md")
541
+ };
542
+ break;
543
+ default: variant = { name: variantName };
544
+ }
545
+ } else variant = { name: variantName };
546
+ const headingLevel = componentReader.value("heading-level");
547
+ return {
548
+ component: "text",
549
+ variant,
550
+ headingLevel,
551
+ content
552
+ };
553
+ }
554
+ function parseTimelineComponent(parameters) {
555
+ const { componentProperty, elementResource, websiteLinks } = parameters;
556
+ const timelineLink = findWebsiteLink(websiteLinks, "tree");
557
+ if (timelineLink == null) throw new Error(formatComponentError("Timeline link not found", "timeline", elementResource), { cause: componentProperty });
558
+ return {
559
+ component: "timeline",
560
+ linkUuid: timelineLink.uuid
561
+ };
562
+ }
563
+ function parseVideoComponent(parameters) {
564
+ const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
565
+ const videoLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "video");
566
+ if (videoLink == null) throw new Error(formatComponentError("Video link not found", "video", elementResource), { cause: componentProperty });
567
+ const isChaptersDisplayed = componentReader.valueOr("chapters-displayed", true);
568
+ return {
569
+ component: "video",
570
+ linkUuid: videoLink.uuid,
571
+ isChaptersDisplayed
572
+ };
573
+ }
574
+ /**
575
+ * Parses raw web element properties into a standardized WebElementComponent structure
576
+ *
577
+ * @param componentProperty - Raw component property data in OCHRE format
578
+ * @param elementResource - Raw element resource data in OCHRE format
579
+ * @returns Parsed WebElementComponent object
580
+ */
581
+ /**
582
+ * The parser for each web element component, keyed by its OCHRE component name
583
+ *
584
+ * Every component parser takes the same {@link WebElementComponentParameters}
585
+ * bag, so this is a table rather than a `switch`: a missing component is a type
586
+ * error here instead of a runtime throw, and each parser can be exercised on
587
+ * its own.
588
+ */
589
+ const WEB_ELEMENT_COMPONENT_PARSERS = {
590
+ "3d-viewer": parse3dViewerComponent,
591
+ "advanced-search": parseAdvancedSearchComponent,
592
+ "annotated-document": parseAnnotatedDocumentComponent,
593
+ "annotated-image": parseAnnotatedImageComponent,
594
+ "audio-player": parseAudioPlayerComponent,
595
+ bibliography: parseBibliographyComponent,
596
+ button: parseButtonComponent,
597
+ collection: parseCollectionComponent,
598
+ "empty-space": parseEmptySpaceComponent,
599
+ entries: parseEntriesComponent,
600
+ iframe: parseIframeComponent,
601
+ "iiif-viewer": parseIiifViewerComponent,
602
+ image: parseImageComponent,
603
+ "image-gallery": parseImageGalleryComponent,
604
+ map: parseMapComponent,
605
+ query: parseQueryComponent,
606
+ table: parseTableComponent,
607
+ "search-bar": parseSearchBarComponent,
608
+ text: parseTextComponent,
609
+ timeline: parseTimelineComponent,
610
+ video: parseVideoComponent
611
+ };
612
+ function parseWebElementProperties(componentProperty, elementResource, options, pageSlugs) {
613
+ const unparsedComponentName = componentProperty.values[0].content;
614
+ const componentNameResult = v.safeParse(componentSchema, unparsedComponentName);
615
+ const parse = componentNameResult.success ? WEB_ELEMENT_COMPONENT_PARSERS[componentNameResult.output] : void 0;
616
+ if (parse == null) throw new Error(`Invalid or non-implemented component name \u{201C}${unparsedComponentName.toString()}\u{201D} for the following element: \u{201C}${parseStringContent(elementResource.identification.label, options)}\u{201D}`);
617
+ return parse({
618
+ componentProperty,
619
+ componentReader: websitePresentationReader(componentProperty.properties),
620
+ elementResource,
621
+ websiteLinks: parseLinks(elementResource.links, options),
622
+ options,
623
+ pageSlugs
624
+ });
625
+ }
626
+ function parseWebTitle(properties, identification, overrides) {
627
+ const title = {
628
+ label: identification.label,
629
+ variant: "default",
630
+ properties: {
631
+ isNameDisplayed: overrides?.isNameDisplayed ?? false,
632
+ isDescriptionDisplayed: overrides?.isDescriptionDisplayed ?? false,
633
+ isDateDisplayed: overrides?.isDateDisplayed ?? false,
634
+ isCreatorsDisplayed: overrides?.isCreatorsDisplayed ?? false,
635
+ isCountDisplayed: overrides?.isCountDisplayed ?? false
636
+ }
637
+ };
638
+ const titleReader = websitePresentationReader(properties).nestedByValue("presentation", "title");
639
+ if (titleReader.size > 0) {
640
+ titleReader.readAll(title, { variant: "variant" });
641
+ titleReader.readAll(title.properties, {
642
+ isNameDisplayed: "name-displayed",
643
+ isDescriptionDisplayed: "description-displayed",
644
+ isDateDisplayed: "date-displayed",
645
+ isCreatorsDisplayed: "creators-displayed",
646
+ isCountDisplayed: "count-displayed"
647
+ });
648
+ }
649
+ return title;
650
+ }
651
+ /**
652
+ * Parses raw web element data into a standardized WebElement structure
653
+ *
654
+ * @param elementResource - Raw element resource data in OCHRE format
655
+ * @returns Parsed WebElement object
656
+ */
657
+ function parseWebElement(elementResource, options, pageSlugs) {
658
+ const identification = parseIdentification(elementResource.identification, options);
659
+ const elementProperties = parseSimplifiedProperties(elementResource.properties, options);
660
+ const presentationProperty = websitePresentationReader(elementProperties).requiredProperty("presentation", `Presentation property not found for element (${formatXMLWebsiteResourceMetadata(elementResource)})`);
661
+ const properties = parseWebElementProperties(websitePresentationReader(presentationProperty.properties).requiredProperty("component", `Component property not found for element (${formatXMLWebsiteResourceMetadata(elementResource)})`), elementResource, options, pageSlugs);
662
+ const cssStyles = parseResponsiveCssStyles(elementProperties);
663
+ const title = parseWebTitle(elementProperties, identification, {
664
+ isNameDisplayed: [
665
+ "annotated-image",
666
+ "annotated-document",
667
+ "collection"
668
+ ].includes(properties.component),
669
+ isCountDisplayed: properties.component === "collection"
670
+ });
671
+ return {
672
+ uuid: elementResource.uuid,
673
+ language: elementResource.lang ?? null,
674
+ type: "element",
675
+ title,
676
+ cssStyles,
677
+ ...properties
678
+ };
679
+ }
680
+ //#endregion
681
+ export { WEB_ELEMENT_COMPONENT_PARSERS, parseWebElement, parseWebTitle };