ochre-sdk 1.0.13 → 1.0.15

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 (51) hide show
  1. package/dist/constants.d.mts +17 -0
  2. package/dist/constants.mjs +85 -0
  3. package/dist/fetchers/gallery.d.mts +38 -0
  4. package/dist/fetchers/gallery.mjs +91 -0
  5. package/dist/fetchers/item-links.d.mts +32 -0
  6. package/dist/fetchers/item-links.mjs +120 -0
  7. package/dist/fetchers/item.d.mts +74 -0
  8. package/dist/fetchers/item.mjs +146 -0
  9. package/dist/fetchers/set/items.d.mts +48 -0
  10. package/dist/fetchers/set/items.mjs +268 -0
  11. package/dist/fetchers/set/property-values.d.mts +46 -0
  12. package/dist/fetchers/set/property-values.mjs +514 -0
  13. package/dist/fetchers/website.d.mts +25 -0
  14. package/dist/fetchers/website.mjs +38 -0
  15. package/dist/getters.d.mts +193 -0
  16. package/dist/getters.mjs +341 -0
  17. package/dist/helpers.d.mts +18 -0
  18. package/dist/helpers.mjs +33 -0
  19. package/dist/index.d.mts +12 -1971
  20. package/dist/index.mjs +9 -7236
  21. package/dist/parsers/helpers.d.mts +27 -0
  22. package/dist/parsers/helpers.mjs +53 -0
  23. package/dist/parsers/index.d.mts +65 -0
  24. package/dist/parsers/index.mjs +1338 -0
  25. package/dist/parsers/mdx.d.mts +4 -0
  26. package/dist/parsers/mdx.mjs +9 -0
  27. package/dist/parsers/multilingual.d.mts +189 -0
  28. package/dist/parsers/multilingual.mjs +410 -0
  29. package/dist/parsers/string.d.mts +29 -0
  30. package/dist/parsers/string.mjs +445 -0
  31. package/dist/parsers/website/index.d.mts +20 -0
  32. package/dist/parsers/website/index.mjs +1245 -0
  33. package/dist/parsers/website/reader.d.mts +29 -0
  34. package/dist/parsers/website/reader.mjs +75 -0
  35. package/dist/query.d.mts +13 -0
  36. package/dist/query.mjs +827 -0
  37. package/dist/schemas.d.mts +79 -0
  38. package/dist/schemas.mjs +223 -0
  39. package/dist/types/index.d.mts +840 -0
  40. package/dist/types/index.mjs +1 -0
  41. package/dist/types/website.d.mts +501 -0
  42. package/dist/types/website.mjs +1 -0
  43. package/dist/utils.d.mts +34 -0
  44. package/dist/utils.mjs +172 -0
  45. package/dist/xml/metadata.d.mts +5 -0
  46. package/dist/xml/metadata.mjs +30 -0
  47. package/dist/xml/schemas.d.mts +13 -0
  48. package/dist/xml/schemas.mjs +849 -0
  49. package/dist/xml/types.d.mts +901 -0
  50. package/dist/xml/types.mjs +1 -0
  51. package/package.json +19 -17
@@ -0,0 +1,1245 @@
1
+ import { componentSchema } from "../../schemas.mjs";
2
+ import { parseXMLContent } from "../string.mjs";
3
+ import { cleanObject, parseLicense, parseStringContent } from "../helpers.mjs";
4
+ import { parseBibliographyList, parseIdentification, parseLinks, parseMetadata, parseMetadataLanguages, parseNotes, parsePersonList, parseSimplifiedProperties, resolveDefaultLanguage, resolveLanguages } from "../index.mjs";
5
+ import { websitePresentationReader } from "./reader.mjs";
6
+ import * as v from "valibot";
7
+ //#region src/parsers/website/index.ts
8
+ function isWebsiteLink(link, category) {
9
+ return link.category === category;
10
+ }
11
+ function findWebsiteLink(links, category, predicate) {
12
+ for (const link of links) if (isWebsiteLink(link, category) && (predicate == null || predicate(link))) return link;
13
+ return null;
14
+ }
15
+ function findWebsiteLinkByCategories(links, categories) {
16
+ for (const link of links) for (const category of categories) if (isWebsiteLink(link, category)) return link;
17
+ return null;
18
+ }
19
+ function getWebsiteLinks(links, category) {
20
+ const matchedLinks = [];
21
+ for (const link of links) if (isWebsiteLink(link, category)) matchedLinks.push(link);
22
+ return matchedLinks;
23
+ }
24
+ function transformPermanentIdentificationUrlToItemLink(url) {
25
+ return url.replace("https://pi.lib.uchicago.edu/1001/org/ochre/", "/item/");
26
+ }
27
+ function normalizeWebsiteResources(resources) {
28
+ const normalized = [];
29
+ for (const resource of resources ?? []) {
30
+ if ("identification" in resource) {
31
+ normalized.push(resource);
32
+ continue;
33
+ }
34
+ if ("resource" in resource) normalized.push(...resource.resource);
35
+ }
36
+ return normalized;
37
+ }
38
+ const SEGMENT_UNIQUE_SLUG_PREFIX_REGEX = /^\$[^-]*-/;
39
+ function prefixSlug(slug, slugPrefix) {
40
+ if (slugPrefix == null || slugPrefix === "") return slug;
41
+ if (slug === "") return slugPrefix;
42
+ return `${slugPrefix}/${slug}`;
43
+ }
44
+ function formatXMLWebsiteResourceMetadata(resource) {
45
+ const metadata = [`label “${parseStringContent(resource.identification.label)}”`, `uuid “${resource.uuid}”`];
46
+ if (resource.slug != null) metadata.push(`slug “${resource.slug}”`);
47
+ if (resource.identification.abbreviation != null) metadata.push(`abbreviation “${parseStringContent(resource.identification.abbreviation)}”`);
48
+ return metadata.join(", ");
49
+ }
50
+ function formatComponentError(message, componentName, elementResource) {
51
+ return `${message} for component “${componentName ?? "(unknown)"}” (${formatXMLWebsiteResourceMetadata(elementResource)})`;
52
+ }
53
+ /**
54
+ * Extracts CSS style properties for a given presentation variant.
55
+ *
56
+ * @param properties - Array of properties to parse
57
+ * @param cssVariant - CSS variant to parse
58
+ * @returns Array of CSS styles
59
+ */
60
+ function parseCssStylesFromProperties(properties, cssVariant) {
61
+ const label = cssVariant != null ? `css-${cssVariant}` : "css";
62
+ const cssProperties = websitePresentationReader(properties).nestedByValue("presentation", label).properties;
63
+ const styles = [];
64
+ for (const property of cssProperties) {
65
+ const value = property.values[0]?.content.toString();
66
+ if (value != null) styles.push({
67
+ label: property.variable.label,
68
+ value
69
+ });
70
+ }
71
+ return styles;
72
+ }
73
+ /**
74
+ * Parses responsive CSS styles (default, tablet, mobile) from properties.
75
+ *
76
+ * @param properties - Array of properties to parse
77
+ * @returns Object containing responsive CSS styles
78
+ */
79
+ function parseResponsiveCssStyles(properties) {
80
+ return {
81
+ default: parseCssStylesFromProperties(properties),
82
+ tablet: parseCssStylesFromProperties(properties, "tablet"),
83
+ mobile: parseCssStylesFromProperties(properties, "mobile")
84
+ };
85
+ }
86
+ /**
87
+ * Parses raw bounds data into a standardized bounds structure
88
+ *
89
+ * @param bounds - Raw bounds data in OCHRE format
90
+ * @returns Parsed bounds object
91
+ */
92
+ function parseBounds(bounds) {
93
+ const [southWest, northEast] = bounds.trim().startsWith("[") ? parseJsonBounds(bounds) : bounds.split(";").map((pair) => pair.split(",").map((coordinate) => Number.parseFloat(coordinate.trim())));
94
+ if (southWest?.length !== 2 || northEast?.length !== 2 || southWest.some((coordinate) => Number.isNaN(coordinate)) || northEast.some((coordinate) => Number.isNaN(coordinate))) throw new Error(`Invalid bounds: ${bounds}`, { cause: bounds });
95
+ return [[southWest[0], southWest[1]], [northEast[0], northEast[1]]];
96
+ }
97
+ function parseJsonBounds(bounds) {
98
+ let parsed;
99
+ try {
100
+ parsed = JSON.parse(bounds);
101
+ } catch {
102
+ throw new Error(`Invalid bounds: ${bounds}`, { cause: bounds });
103
+ }
104
+ if (!isNumberPairArray(parsed)) throw new Error(`Invalid bounds: ${bounds}`, { cause: bounds });
105
+ return parsed;
106
+ }
107
+ function isNumberPairArray(value) {
108
+ return Array.isArray(value) && value.every((pair) => Array.isArray(pair) && pair.every((coordinate) => typeof coordinate === "number"));
109
+ }
110
+ /**
111
+ * Parses all context option arrays from an options object.
112
+ *
113
+ * @param options - Options object containing context options
114
+ * @param options.flattenContexts - Flatten contexts
115
+ * @param options.suppressContexts - Suppress contexts
116
+ * @param options.filterContexts - Filter contexts
117
+ * @param options.sortContexts - Sort contexts
118
+ * @param options.detailContexts - Detail contexts
119
+ * @param options.downloadContexts - Download contexts
120
+ * @param options.labelContexts - Label contexts
121
+ * @param options.prominentContexts - Prominent contexts
122
+ * @returns Parsed context options
123
+ */
124
+ function parseAllOptionContexts(options, parserOptions) {
125
+ function handleContexts(v) {
126
+ return parseContexts(v ?? [], parserOptions);
127
+ }
128
+ function handleFilterContexts(v) {
129
+ return parseFilterContexts(v ?? [], parserOptions);
130
+ }
131
+ return {
132
+ flatten: handleContexts(options.flattenContexts),
133
+ suppress: handleContexts(options.suppressContexts),
134
+ filter: handleFilterContexts(options.filterContexts),
135
+ sort: handleContexts(options.sortContexts),
136
+ detail: handleContexts(options.detailContexts),
137
+ download: handleContexts(options.downloadContexts),
138
+ label: handleContexts(options.labelContexts),
139
+ prominent: handleContexts(options.prominentContexts)
140
+ };
141
+ }
142
+ function parseWebsiteScopes(scopes, options) {
143
+ if (scopes == null) return null;
144
+ const parsedScopes = [];
145
+ for (const scope of scopes.scope) parsedScopes.push({
146
+ uuid: scope.uuid.payload,
147
+ type: scope.uuid.type,
148
+ identification: parseIdentification(scope.identification, options)
149
+ });
150
+ return parsedScopes;
151
+ }
152
+ function parseWebsiteOptions(rawOptions, options) {
153
+ const parsedOptions = {
154
+ scopes: parseWebsiteScopes(rawOptions?.scopes, options),
155
+ contextTree: rawOptions == null ? null : parseAllOptionContexts(rawOptions, options),
156
+ labels: { title: null }
157
+ };
158
+ for (const note of parseNotes(rawOptions?.notes, options)) if (note.title?.getText() === "Title label") {
159
+ parsedOptions.labels.title = note.content;
160
+ break;
161
+ }
162
+ return parsedOptions;
163
+ }
164
+ function parseStylesheets(styles) {
165
+ const parsedStyles = [];
166
+ for (const style of styles) {
167
+ const defaultStyles = [];
168
+ for (const [label, value] of Object.entries(style)) {
169
+ if (label === "variableUuid" || label === "valueUuid" || label === "category" || label === "payload" || label === "content") continue;
170
+ const valueString = value?.toString();
171
+ if (valueString != null) defaultStyles.push({
172
+ label,
173
+ value: valueString
174
+ });
175
+ }
176
+ const stylesByViewport = {
177
+ default: defaultStyles,
178
+ tablet: [],
179
+ mobile: []
180
+ };
181
+ if (style.category === "propertyValue" || style.valueUuid != null) {
182
+ if (style.valueUuid == null) throw new Error(`Stylesheet property value "${style.variableUuid}" is missing a value UUID`, { cause: style });
183
+ parsedStyles.push({
184
+ uuid: style.valueUuid,
185
+ category: "propertyValue",
186
+ variableUuid: style.variableUuid,
187
+ icon: style.lucideIcon ?? null,
188
+ styles: stylesByViewport
189
+ });
190
+ continue;
191
+ }
192
+ parsedStyles.push({
193
+ uuid: style.variableUuid,
194
+ category: "propertyVariable",
195
+ icon: style.lucideIcon ?? null,
196
+ styles: stylesByViewport
197
+ });
198
+ }
199
+ return parsedStyles;
200
+ }
201
+ /**
202
+ * Parses raw web element properties into a standardized WebElementComponent structure
203
+ *
204
+ * @param componentProperty - Raw component property data in OCHRE format
205
+ * @param elementResource - Raw element resource data in OCHRE format
206
+ * @returns Parsed WebElementComponent object
207
+ */
208
+ function parseWebElementProperties(componentProperty, elementResource, options) {
209
+ const unparsedComponentName = componentProperty.values[0].content;
210
+ const componentNameResult = v.safeParse(componentSchema, unparsedComponentName);
211
+ const componentName = componentNameResult.success ? componentNameResult.output : void 0;
212
+ let properties = null;
213
+ const websiteLinks = parseLinks(elementResource.links, options);
214
+ const componentReader = websitePresentationReader(componentProperty.properties);
215
+ switch (componentName) {
216
+ case "3d-viewer": {
217
+ const resourceLink = findWebsiteLink(websiteLinks, "resource", (link) => link.fileFormat === "model/obj");
218
+ if (resourceLink == null) throw new Error(formatComponentError("Resource link not found", componentName, elementResource), { cause: componentProperty });
219
+ const isInteractive = componentReader.valueOr("is-interactive", true);
220
+ const isControlsDisplayed = componentReader.valueOr("controls-displayed", true);
221
+ properties = {
222
+ component: "3d-viewer",
223
+ linkUuid: resourceLink.uuid,
224
+ fileSize: resourceLink.fileSize,
225
+ isInteractive,
226
+ isControlsDisplayed
227
+ };
228
+ break;
229
+ }
230
+ case "advanced-search": {
231
+ const boundElementPropertyUuid = componentReader.uuid("bound-element");
232
+ const href = componentReader.linkTarget("link-to", transformPermanentIdentificationUrlToItemLink);
233
+ if (boundElementPropertyUuid == null && href == null) throw new Error(formatComponentError("Bound element or href not found", componentName, elementResource), { cause: componentProperty });
234
+ properties = {
235
+ component: "advanced-search",
236
+ boundElementUuid: boundElementPropertyUuid,
237
+ href
238
+ };
239
+ break;
240
+ }
241
+ case "annotated-document": {
242
+ const documentLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "internalDocument");
243
+ if (documentLink == null) throw new Error(formatComponentError("Document link not found", componentName, elementResource), { cause: componentProperty });
244
+ properties = {
245
+ component: "annotated-document",
246
+ linkUuid: documentLink.uuid
247
+ };
248
+ break;
249
+ }
250
+ case "annotated-image": {
251
+ const imageLinks = getWebsiteLinks(websiteLinks, "resource").filter((link) => link.type === "image" || link.type === "IIIF");
252
+ if (imageLinks.length === 0) throw new Error(formatComponentError("Image link not found", componentName, elementResource), { cause: componentProperty });
253
+ const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", true);
254
+ const isOptionsDisplayed = componentReader.valueOr("options-displayed", true);
255
+ const isAnnotationHighlightsDisplayed = componentReader.valueOr("annotation-highlights-displayed", true);
256
+ const isAnnotationTooltipsDisplayed = componentReader.valueOr("annotation-tooltips-displayed", true);
257
+ properties = {
258
+ component: "annotated-image",
259
+ linkUuid: imageLinks[0].uuid,
260
+ isFilterInputDisplayed,
261
+ isOptionsDisplayed,
262
+ isAnnotationHighlightsDisplayed,
263
+ isAnnotationTooltipsDisplayed
264
+ };
265
+ break;
266
+ }
267
+ case "audio-player": {
268
+ const audioLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "audio");
269
+ if (audioLink == null) throw new Error(formatComponentError("Audio link not found", componentName, elementResource), { cause: componentProperty });
270
+ const isSpeedControlsDisplayed = componentReader.valueOr("speed-controls-displayed", true);
271
+ const isVolumeControlsDisplayed = componentReader.valueOr("volume-controls-displayed", true);
272
+ const isSeekBarDisplayed = componentReader.valueOr("seek-bar-displayed", true);
273
+ properties = {
274
+ component: "audio-player",
275
+ linkUuid: audioLink.uuid,
276
+ isSpeedControlsDisplayed,
277
+ isVolumeControlsDisplayed,
278
+ isSeekBarDisplayed
279
+ };
280
+ break;
281
+ }
282
+ case "bibliography": {
283
+ const itemLinks = websiteLinks.filter((link) => link.category !== "bibliography");
284
+ const bibliographies = parseBibliographyList(elementResource.bibliographies, options);
285
+ if (itemLinks.length === 0 && bibliographies.length === 0) throw new Error(formatComponentError("No links found", componentName, elementResource), { cause: componentProperty });
286
+ const layout = componentReader.valueOr("layout", "long");
287
+ const isSourceDocumentDisplayed = componentReader.valueOr("source-document-displayed", true);
288
+ properties = {
289
+ component: "bibliography",
290
+ linkUuids: itemLinks.map((link) => link.uuid),
291
+ bibliographies,
292
+ layout,
293
+ isSourceDocumentDisplayed
294
+ };
295
+ break;
296
+ }
297
+ case "button": {
298
+ const variant = componentReader.valueOr("variant", "default");
299
+ let isExternal = false;
300
+ let href = componentReader.linkTarget("navigate-to", transformPermanentIdentificationUrlToItemLink);
301
+ if (href === null) {
302
+ href = componentReader.linkTarget("link-to", transformPermanentIdentificationUrlToItemLink);
303
+ if (href === null) throw new Error(formatComponentError("Properties “navigate-to” or “link-to” not found", componentName, elementResource), { cause: componentProperty });
304
+ else isExternal = true;
305
+ }
306
+ const startIcon = componentReader.value("start-icon");
307
+ const endIcon = componentReader.value("end-icon");
308
+ let image = null;
309
+ const imageLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "image" || link.type === "IIIF");
310
+ if (imageLink != null) image = {
311
+ uuid: imageLink.uuid,
312
+ label: imageLink.identification.label,
313
+ width: imageLink.image?.width ?? 0,
314
+ height: imageLink.image?.height ?? 0,
315
+ description: imageLink.description,
316
+ quality: "high"
317
+ };
318
+ properties = {
319
+ component: "button",
320
+ variant,
321
+ href,
322
+ isExternal,
323
+ label: elementResource.document && "content" in elementResource.document ? parseXMLContent(elementResource.document, options) : null,
324
+ startIcon,
325
+ endIcon,
326
+ image
327
+ };
328
+ break;
329
+ }
330
+ case "collection": {
331
+ const setLinks = getWebsiteLinks(websiteLinks, "set");
332
+ if (setLinks.length === 0) throw new Error(formatComponentError("Set links not found", componentName, elementResource), { cause: componentProperty });
333
+ const displayedProperties = componentReader.property("use-property");
334
+ const variant = componentReader.valueOr("variant", "slide");
335
+ const paginationVariant = componentReader.valueOr("pagination-variant", "default");
336
+ const loadingVariant = componentReader.valueOr("loading-variant", "skeleton");
337
+ const expectedItemCount = componentReader.valueOr("item-count", null);
338
+ const isUsingQueryParams = componentReader.valueOr("is-using-query-params", false);
339
+ const isFilterResultsBarDisplayed = componentReader.valueOr("filter-results-bar-displayed", false);
340
+ const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", false);
341
+ const isFilterLimitedToInputFilter = componentReader.valueOr("filter-limit-to-input-filter", false);
342
+ const isFilterLimitedToLeafPropertyValues = componentReader.valueOr("filter-limit-to-leaf-property-values", false);
343
+ const isSortDisplayed = componentReader.valueOr("sort-displayed", false);
344
+ const isFilterSidebarDisplayed = componentReader.valueOr("filter-sidebar-displayed", false);
345
+ const filterSidebarSort = componentReader.valueOr("filter-sidebar-sort", "default");
346
+ const imageLayout = componentReader.valueOr("image-layout", "start");
347
+ const componentOptions = parseWebsiteOptions(elementResource.options, options);
348
+ properties = {
349
+ component: "collection",
350
+ linkUuids: setLinks.map((link) => link.uuid),
351
+ displayedProperties: displayedProperties?.values.filter(({ uuid }) => uuid !== null).map((value) => ({
352
+ uuid: value.uuid,
353
+ label: value.label
354
+ })) ?? null,
355
+ variant,
356
+ paginationVariant,
357
+ loadingVariant,
358
+ imageLayout,
359
+ expectedItemCount,
360
+ isUsingQueryParams,
361
+ isSortDisplayed,
362
+ filter: {
363
+ isSidebarDisplayed: isFilterSidebarDisplayed,
364
+ isResultsBarDisplayed: isFilterResultsBarDisplayed,
365
+ isInputDisplayed: isFilterInputDisplayed,
366
+ isLimitedToInputFilter: isFilterLimitedToInputFilter,
367
+ isLimitedToLeafPropertyValues: isFilterLimitedToLeafPropertyValues,
368
+ sidebarSort: filterSidebarSort
369
+ },
370
+ options: componentOptions
371
+ };
372
+ break;
373
+ }
374
+ case "empty-space":
375
+ properties = {
376
+ component: "empty-space",
377
+ height: componentReader.stringValue("height"),
378
+ width: componentReader.stringValue("width")
379
+ };
380
+ break;
381
+ case "entries": {
382
+ const entriesLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
383
+ if (entriesLink == null) throw new Error(formatComponentError("Entries link not found", componentName, elementResource), { cause: componentProperty });
384
+ const variant = componentReader.valueOr("variant", "entry");
385
+ const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", false);
386
+ properties = {
387
+ component: "entries",
388
+ linkUuid: entriesLink.uuid,
389
+ variant,
390
+ isFilterInputDisplayed
391
+ };
392
+ break;
393
+ }
394
+ case "iframe": {
395
+ const webpageLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "webpage");
396
+ if (webpageLink?.href == null) throw new Error(formatComponentError("URL not found", componentName, elementResource), { cause: componentProperty });
397
+ properties = {
398
+ component: "iframe",
399
+ href: transformPermanentIdentificationUrlToItemLink(webpageLink.href),
400
+ height: componentReader.stringValue("height"),
401
+ width: componentReader.stringValue("width")
402
+ };
403
+ break;
404
+ }
405
+ case "iiif-viewer": {
406
+ const manifestLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "IIIF");
407
+ if (manifestLink == null) throw new Error(formatComponentError("Manifest link not found", componentName, elementResource), { cause: componentProperty });
408
+ const variant = componentReader.valueOr("variant", "universal-viewer");
409
+ properties = {
410
+ component: "iiif-viewer",
411
+ linkUuid: manifestLink.uuid,
412
+ variant
413
+ };
414
+ break;
415
+ }
416
+ case "image": {
417
+ if (websiteLinks.length === 0) throw new Error(formatComponentError("No links found", componentName, elementResource), { cause: componentProperty });
418
+ const imageQuality = componentReader.valueOr("image-quality", "high");
419
+ const images = [];
420
+ for (const link of websiteLinks) images.push({
421
+ uuid: link.uuid,
422
+ label: link.identification.label,
423
+ width: "image" in link ? link.image?.width ?? 0 : 0,
424
+ height: "image" in link ? link.image?.height ?? 0 : 0,
425
+ description: link.description,
426
+ quality: imageQuality
427
+ });
428
+ const variant = componentReader.valueOr("variant", "default");
429
+ const captionLayout = componentReader.valueOr("layout-caption", "bottom");
430
+ let width = null;
431
+ const widthProperty = componentReader.value("width");
432
+ if (widthProperty !== null) {
433
+ if (typeof widthProperty === "number") width = widthProperty;
434
+ else if (typeof widthProperty === "string") width = Number.parseFloat(widthProperty);
435
+ }
436
+ let height = null;
437
+ const heightProperty = componentReader.value("height");
438
+ if (heightProperty !== null) {
439
+ if (typeof heightProperty === "number") height = heightProperty;
440
+ else if (typeof heightProperty === "string") height = Number.parseFloat(heightProperty);
441
+ }
442
+ const isFullWidth = componentReader.valueOr("is-full-width", true);
443
+ const isFullHeight = componentReader.valueOr("is-full-height", true);
444
+ const captionSource = componentReader.valueOr("source-caption", "name");
445
+ const altTextSource = componentReader.valueOr("alt-text-source", "name");
446
+ const isTransparentBackground = componentReader.valueOr("is-transparent", false);
447
+ const isCover = componentReader.valueOr("is-cover", false);
448
+ const variantReader = componentReader.nested("variant");
449
+ let carouselOptions = null;
450
+ if (images.length > 1) {
451
+ let secondsPerImage = 5;
452
+ if (variant === "carousel") {
453
+ const secondsPerImageProperty = variantReader.value("seconds-per-image");
454
+ if (secondsPerImageProperty !== null) {
455
+ if (typeof secondsPerImageProperty === "number") secondsPerImage = secondsPerImageProperty;
456
+ else if (typeof secondsPerImageProperty === "string") secondsPerImage = Number.parseFloat(secondsPerImageProperty);
457
+ }
458
+ }
459
+ carouselOptions = { secondsPerImage };
460
+ }
461
+ let heroOptions = null;
462
+ if (variant === "hero") heroOptions = {
463
+ isBackgroundImageDisplayed: variantReader.valueOr("background-image-displayed", true),
464
+ isDocumentDisplayed: variantReader.valueOr("document-displayed", true)
465
+ };
466
+ properties = {
467
+ component: "image",
468
+ images,
469
+ variant,
470
+ width,
471
+ height,
472
+ isFullWidth,
473
+ isFullHeight,
474
+ imageQuality,
475
+ captionLayout,
476
+ captionSource,
477
+ altTextSource,
478
+ isTransparentBackground,
479
+ isCover,
480
+ carouselOptions,
481
+ heroOptions
482
+ };
483
+ break;
484
+ }
485
+ case "image-gallery": {
486
+ const galleryLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
487
+ if (galleryLink == null) throw new Error(formatComponentError("Image gallery link not found", componentName, elementResource), { cause: componentProperty });
488
+ const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", true);
489
+ properties = {
490
+ component: "image-gallery",
491
+ linkUuid: galleryLink.uuid,
492
+ isFilterInputDisplayed
493
+ };
494
+ break;
495
+ }
496
+ case "map": {
497
+ const mapLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
498
+ if (mapLink == null) throw new Error(formatComponentError("Map link not found", componentName, elementResource), { cause: componentProperty });
499
+ const isInteractive = componentReader.valueOr("is-interactive", true);
500
+ const isClustered = componentReader.valueOr("is-clustered", false);
501
+ const isUsingPins = componentReader.valueOr("is-using-pins", false);
502
+ const customBasemap = componentReader.value("custom-basemap");
503
+ let initialBounds = null;
504
+ const initialBoundsProperty = componentReader.value("initial-bounds");
505
+ if (initialBoundsProperty !== null) initialBounds = parseBounds(String(initialBoundsProperty));
506
+ let maximumBounds = null;
507
+ const maximumBoundsProperty = componentReader.value("maximum-bounds");
508
+ if (maximumBoundsProperty !== null) maximumBounds = parseBounds(String(maximumBoundsProperty));
509
+ const isControlsDisplayed = componentReader.valueOr("controls-displayed", false);
510
+ const isFullHeight = componentReader.valueOr("is-full-height", false);
511
+ properties = {
512
+ component: "map",
513
+ linkUuid: mapLink.uuid,
514
+ customBasemap,
515
+ initialBounds,
516
+ maximumBounds,
517
+ isInteractive,
518
+ isClustered,
519
+ isUsingPins,
520
+ isControlsDisplayed,
521
+ isFullHeight
522
+ };
523
+ break;
524
+ }
525
+ case "query": {
526
+ const setLinks = getWebsiteLinks(websiteLinks, "set");
527
+ if (setLinks.length === 0) throw new Error(formatComponentError("Set links not found", componentName, elementResource), { cause: componentProperty });
528
+ const items = [];
529
+ if (componentProperty.properties.length === 0) throw new Error(formatComponentError("Query properties not found", componentName, elementResource), { cause: componentProperty });
530
+ for (const queryItem of componentProperty.properties) {
531
+ const queryReader = websitePresentationReader(queryItem.properties);
532
+ const label = queryReader.multilingualValue("query-prompt", options);
533
+ if (label === null) continue;
534
+ const propertyVariables = queryReader.values("use-property").filter((value) => value.uuid !== null);
535
+ const queryLanguage = options.languages[0];
536
+ if (queryLanguage == null) throw new Error(formatComponentError("Query language not found", componentName, elementResource));
537
+ const queries = [];
538
+ for (const propertyVariable of propertyVariables) {
539
+ if (propertyVariable.uuid === null) throw new Error(formatComponentError("Property variable UUID not found", componentName, elementResource), { cause: propertyVariable });
540
+ const dataType = propertyVariable.dataType;
541
+ if (dataType === "coordinate") throw new Error(formatComponentError("Query prompts with data type \"coordinate\" are not supported", componentName, elementResource), { cause: propertyVariable });
542
+ queries.push({
543
+ target: "property",
544
+ propertyVariable: propertyVariable.uuid,
545
+ dataType,
546
+ matchMode: "exact",
547
+ isCaseSensitive: true,
548
+ language: queryLanguage
549
+ });
550
+ }
551
+ const startIcon = queryReader.value("start-icon");
552
+ const endIcon = queryReader.value("end-icon");
553
+ items.push({
554
+ label,
555
+ queries,
556
+ startIcon,
557
+ endIcon
558
+ });
559
+ }
560
+ if (items.length === 0) throw new Error(formatComponentError("No queries found", componentName, elementResource), { cause: componentProperty });
561
+ const componentOptions = parseWebsiteOptions(elementResource.options, options);
562
+ const displayedProperties = componentReader.property("use-property");
563
+ const variant = componentReader.valueOr("variant", "slide");
564
+ const paginationVariant = componentReader.valueOr("pagination-variant", "default");
565
+ const loadingVariant = componentReader.valueOr("loading-variant", "skeleton");
566
+ const imageLayout = componentReader.valueOr("image-layout", "start");
567
+ properties = {
568
+ component: "query",
569
+ linkUuids: setLinks.map((link) => link.uuid),
570
+ items,
571
+ options: componentOptions,
572
+ collectionProperties: {
573
+ displayedProperties: displayedProperties?.values.filter((value) => value.uuid !== null).map((value) => ({
574
+ uuid: value.uuid,
575
+ label: value.label
576
+ })) ?? null,
577
+ variant,
578
+ paginationVariant,
579
+ loadingVariant,
580
+ imageLayout
581
+ }
582
+ };
583
+ break;
584
+ }
585
+ case "table": {
586
+ const tableLink = findWebsiteLink(websiteLinks, "set");
587
+ if (tableLink == null) throw new Error(formatComponentError("Table link not found", componentName, elementResource), { cause: componentProperty });
588
+ properties = {
589
+ component: "table",
590
+ linkUuid: tableLink.uuid
591
+ };
592
+ break;
593
+ }
594
+ case "search-bar": {
595
+ const queryVariant = componentReader.valueOr("query-variant", "submit");
596
+ const boundElementUuid = componentReader.uuid("bound-element");
597
+ const href = componentReader.linkTarget("link-to", transformPermanentIdentificationUrlToItemLink);
598
+ if (!boundElementUuid && !href) throw new Error(formatComponentError("Bound element or href not found", componentName, elementResource), { cause: componentProperty });
599
+ properties = {
600
+ component: "search-bar",
601
+ queryVariant,
602
+ placeholder: componentReader.multilingualValue("placeholder-text", options),
603
+ baseFilterQueries: componentReader.value("base-filter-queries")?.replaceAll(String.raw`\{`, "{").replaceAll(String.raw`\}`, "}") ?? null,
604
+ boundElementUuid,
605
+ href
606
+ };
607
+ break;
608
+ }
609
+ case "text": {
610
+ const content = elementResource.document && "content" in elementResource.document ? parseXMLContent(elementResource.document, options) : null;
611
+ if (content == null) throw new Error(formatComponentError("Content not found", componentName, elementResource), { cause: componentProperty });
612
+ let variantName = "block";
613
+ let variant;
614
+ const variantProperty = componentReader.property("variant");
615
+ if (variantProperty !== null) {
616
+ const variantReader = websitePresentationReader(variantProperty.properties);
617
+ variantName = variantProperty.values[0].content;
618
+ switch (variantName) {
619
+ case "paragraph":
620
+ variant = {
621
+ name: variantName,
622
+ size: variantReader.valueOr("size", "md")
623
+ };
624
+ break;
625
+ case "label":
626
+ variant = {
627
+ name: variantName,
628
+ size: variantReader.valueOr("size", "md")
629
+ };
630
+ break;
631
+ case "heading":
632
+ variant = {
633
+ name: variantName,
634
+ size: variantReader.valueOr("size", "md")
635
+ };
636
+ break;
637
+ case "display":
638
+ variant = {
639
+ name: variantName,
640
+ size: variantReader.valueOr("size", "md")
641
+ };
642
+ break;
643
+ default: variant = { name: variantName };
644
+ }
645
+ } else variant = { name: variantName };
646
+ const headingLevel = componentReader.value("heading-level");
647
+ properties = {
648
+ component: "text",
649
+ variant,
650
+ headingLevel,
651
+ content
652
+ };
653
+ break;
654
+ }
655
+ case "timeline": {
656
+ const timelineLink = findWebsiteLink(websiteLinks, "tree");
657
+ if (timelineLink == null) throw new Error(formatComponentError("Timeline link not found", componentName, elementResource), { cause: componentProperty });
658
+ properties = {
659
+ component: "timeline",
660
+ linkUuid: timelineLink.uuid
661
+ };
662
+ break;
663
+ }
664
+ case "video": {
665
+ const videoLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "video");
666
+ if (videoLink == null) throw new Error(formatComponentError("Video link not found", componentName, elementResource), { cause: componentProperty });
667
+ const isChaptersDisplayed = componentReader.valueOr("chapters-displayed", true);
668
+ properties = {
669
+ component: "video",
670
+ linkUuid: videoLink.uuid,
671
+ isChaptersDisplayed
672
+ };
673
+ break;
674
+ }
675
+ default: throw new Error(`Invalid or non-implemented component name “${unparsedComponentName.toString()}” for the following element: “${parseStringContent(elementResource.identification.label, options)}”`);
676
+ }
677
+ return properties;
678
+ }
679
+ function parseWebTitle(properties, identification, overrides) {
680
+ const title = {
681
+ label: identification.label,
682
+ variant: "default",
683
+ properties: {
684
+ isNameDisplayed: overrides?.isNameDisplayed ?? false,
685
+ isDescriptionDisplayed: overrides?.isDescriptionDisplayed ?? false,
686
+ isDateDisplayed: overrides?.isDateDisplayed ?? false,
687
+ isCreatorsDisplayed: overrides?.isCreatorsDisplayed ?? false,
688
+ isCountDisplayed: overrides?.isCountDisplayed ?? false
689
+ }
690
+ };
691
+ const titleReader = websitePresentationReader(properties).nestedByValue("presentation", "title");
692
+ if (titleReader.size > 0) {
693
+ title.variant = titleReader.valueOr("variant", "default");
694
+ title.properties.isNameDisplayed = titleReader.valueOr("name-displayed", false);
695
+ title.properties.isDescriptionDisplayed = titleReader.valueOr("description-displayed", false);
696
+ title.properties.isDateDisplayed = titleReader.valueOr("date-displayed", false);
697
+ title.properties.isCreatorsDisplayed = titleReader.valueOr("creators-displayed", false);
698
+ title.properties.isCountDisplayed = titleReader.valueOr("count-displayed", false);
699
+ }
700
+ return title;
701
+ }
702
+ /**
703
+ * Parses raw web element data into a standardized WebElement structure
704
+ *
705
+ * @param elementResource - Raw element resource data in OCHRE format
706
+ * @returns Parsed WebElement object
707
+ */
708
+ function parseWebElement(elementResource, options) {
709
+ const identification = parseIdentification(elementResource.identification, options);
710
+ const elementProperties = elementResource.properties?.property ? parseSimplifiedProperties(elementResource.properties, options) : [];
711
+ const properties = parseWebElementProperties(websitePresentationReader(websitePresentationReader(elementProperties).requiredProperty("presentation", `Presentation property not found for element (${formatXMLWebsiteResourceMetadata(elementResource)})`).properties).requiredProperty("component", `Component property not found for element (${formatXMLWebsiteResourceMetadata(elementResource)})`), elementResource, options);
712
+ const cssStyles = parseResponsiveCssStyles(elementProperties);
713
+ const title = parseWebTitle(elementProperties, identification, {
714
+ isNameDisplayed: properties.component === "annotated-image" || properties.component === "annotated-document" || properties.component === "collection",
715
+ isCountDisplayed: properties.component === "collection"
716
+ });
717
+ return {
718
+ uuid: elementResource.uuid,
719
+ type: "element",
720
+ title,
721
+ cssStyles,
722
+ ...properties
723
+ };
724
+ }
725
+ /**
726
+ * Parses raw webpage data into a standardized Webpage structure
727
+ *
728
+ * @param webpageResource - Raw webpage resource data in OCHRE format
729
+ * @returns Parsed Webpage object
730
+ */
731
+ function parseWebpage(webpageResource, options, context, slugPrefix) {
732
+ const webpageProperties = webpageResource.properties ? parseSimplifiedProperties(webpageResource.properties, options) : [];
733
+ const webpageReader = websitePresentationReader(webpageProperties);
734
+ if (webpageReader.value("presentation") !== "page") return null;
735
+ const identification = parseIdentification(webpageResource.identification, options);
736
+ const slug = webpageResource.slug?.replace(SEGMENT_UNIQUE_SLUG_PREFIX_REGEX, "") ?? null;
737
+ if (slug == null) throw new Error(`Slug not found for page (${formatXMLWebsiteResourceMetadata(webpageResource)})`, { cause: webpageResource });
738
+ const returnWebpage = {
739
+ uuid: webpageResource.uuid,
740
+ type: "page",
741
+ title: identification.label,
742
+ slug: prefixSlug(slug, slugPrefix),
743
+ publicationDateTime: webpageResource.publicationDateTime ?? null,
744
+ items: [],
745
+ segments: [],
746
+ properties: {
747
+ width: "default",
748
+ variant: "default",
749
+ isBreadcrumbsDisplayed: false,
750
+ isSidebarDisplayed: true,
751
+ isDisplayedInNavbar: true,
752
+ isNavbarSearchBarDisplayed: true,
753
+ backgroundImage: null,
754
+ cssStyles: {
755
+ default: [],
756
+ tablet: [],
757
+ mobile: []
758
+ }
759
+ },
760
+ webpages: []
761
+ };
762
+ const imageLink = findWebsiteLink(parseLinks(webpageResource.links, options), "resource", (link) => link.type === "image" || link.type === "IIIF");
763
+ const webpageResources = webpageResource.resource != null ? normalizeWebsiteResources(webpageResource.resource) : [];
764
+ const items = [];
765
+ for (const resource of webpageResources) {
766
+ const resourceType = websitePresentationReader(resource.properties != null ? parseSimplifiedProperties(resource.properties, options) : []).value("presentation");
767
+ if (resourceType === null) continue;
768
+ switch (resourceType) {
769
+ case "element": {
770
+ const element = parseWebElement(resource, options);
771
+ items.push(element);
772
+ break;
773
+ }
774
+ case "block": {
775
+ const block = parseWebBlock(resource, options);
776
+ if (block) items.push(block);
777
+ break;
778
+ }
779
+ }
780
+ }
781
+ returnWebpage.items = items;
782
+ returnWebpage.webpages = parseWebpages(webpageResources, options, context, slugPrefix == null ? void 0 : returnWebpage.slug);
783
+ returnWebpage.segments = parseWebsiteSegments(webpageResource.resource, context, options, returnWebpage.slug);
784
+ const pageReader = webpageReader.nestedByValue("presentation", "page");
785
+ if (pageReader.size > 0) {
786
+ returnWebpage.properties.isDisplayedInNavbar = pageReader.valueOr("displayed-in-navbar", true);
787
+ returnWebpage.properties.width = pageReader.valueOr("width", "default");
788
+ returnWebpage.properties.variant = pageReader.valueOr("variant", "default");
789
+ returnWebpage.properties.isSidebarDisplayed = pageReader.valueOr("sidebar-displayed", true);
790
+ returnWebpage.properties.isBreadcrumbsDisplayed = pageReader.valueOr("breadcrumbs-displayed", false);
791
+ returnWebpage.properties.isNavbarSearchBarDisplayed = pageReader.valueOr("navbar-search-bar-displayed", true);
792
+ }
793
+ if (imageLink != null) returnWebpage.properties.backgroundImage = {
794
+ uuid: imageLink.uuid,
795
+ label: imageLink.identification.label,
796
+ description: imageLink.description,
797
+ width: imageLink.image?.width ?? 0,
798
+ height: imageLink.image?.height ?? 0,
799
+ quality: "high"
800
+ };
801
+ returnWebpage.properties.cssStyles = parseResponsiveCssStyles(webpageProperties);
802
+ return returnWebpage;
803
+ }
804
+ /**
805
+ * Parses raw webpage resources into an array of Webpage objects
806
+ *
807
+ * @param webpageResources - Array of raw webpage resources in OCHRE format
808
+ * @returns Array of parsed Webpage objects
809
+ */
810
+ function parseWebpages(webpageResources, options, context, slugPrefix) {
811
+ const returnPages = [];
812
+ for (const webpageResource of webpageResources) {
813
+ const webpage = parseWebpage(webpageResource, options, context, slugPrefix);
814
+ if (webpage !== null) returnPages.push(webpage);
815
+ }
816
+ return returnPages;
817
+ }
818
+ function parseWebpageView(view, options, context) {
819
+ return parseWebpages(view?.resource ?? [], options, context)[0] ?? null;
820
+ }
821
+ function parseWebsiteSegments(resources, context, options, slugPrefix) {
822
+ const segments = [];
823
+ for (const resource of resources ?? []) {
824
+ if (!("segments" in resource)) continue;
825
+ for (const tree of resource.segments.tree) {
826
+ const segmentSlug = tree.identification.abbreviation == null ? null : parseStringContent(tree.identification.abbreviation, options);
827
+ if (segmentSlug == null) throw new Error(`Slug not found for segment website (website uuid “${tree.uuid}”)`, { cause: tree });
828
+ segments.push(parseWebsiteTree(tree, context, "segment", options, prefixSlug(segmentSlug, slugPrefix)));
829
+ }
830
+ }
831
+ return segments;
832
+ }
833
+ /**
834
+ * Parses raw sidebar data into a standardized Sidebar structure
835
+ *
836
+ * @param resources - Array of raw sidebar resources in OCHRE format
837
+ * @returns Parsed Sidebar object
838
+ */
839
+ function parseSidebar(resources, options) {
840
+ let returnSidebar = null;
841
+ const items = [];
842
+ let title = null;
843
+ let layout = "start";
844
+ let mobileLayout = "default";
845
+ const cssStyles = {
846
+ default: [],
847
+ tablet: [],
848
+ mobile: []
849
+ };
850
+ const sidebarResource = resources.find((resource) => {
851
+ const resourceReader = websitePresentationReader(resource.properties ? parseSimplifiedProperties(resource.properties, options) : []);
852
+ return resourceReader.value("presentation") === "element" && resourceReader.nestedByValue("presentation", "element").value("component") === "sidebar";
853
+ });
854
+ if (sidebarResource != null) {
855
+ const sidebarBaseProperties = sidebarResource.properties ? parseSimplifiedProperties(sidebarResource.properties, options) : [];
856
+ title = parseWebTitle(sidebarBaseProperties, parseIdentification(sidebarResource.identification, options));
857
+ const sidebarReader = websitePresentationReader(sidebarBaseProperties).nestedByValue("presentation", "element").nestedByValue("component", "sidebar");
858
+ layout = sidebarReader.valueOr("layout", "start");
859
+ mobileLayout = sidebarReader.valueOr("layout-mobile", "default");
860
+ const parsedCssStyles = parseResponsiveCssStyles(sidebarBaseProperties);
861
+ cssStyles.default = parsedCssStyles.default;
862
+ cssStyles.tablet = parsedCssStyles.tablet;
863
+ cssStyles.mobile = parsedCssStyles.mobile;
864
+ const sidebarResources = sidebarResource.resource ? normalizeWebsiteResources(sidebarResource.resource) : [];
865
+ for (const resource of sidebarResources) {
866
+ const resourceType = websitePresentationReader(resource.properties ? parseSimplifiedProperties(resource.properties, options) : []).value("presentation");
867
+ if (resourceType === null) continue;
868
+ switch (resourceType) {
869
+ case "element": {
870
+ const element = parseWebElement(resource, options);
871
+ items.push(element);
872
+ break;
873
+ }
874
+ case "block": {
875
+ const block = parseWebBlock(resource, options);
876
+ if (block) items.push(block);
877
+ break;
878
+ }
879
+ }
880
+ }
881
+ }
882
+ if (items.length > 0 && title != null) returnSidebar = {
883
+ isDisplayed: true,
884
+ items,
885
+ title,
886
+ layout,
887
+ mobileLayout,
888
+ cssStyles
889
+ };
890
+ return returnSidebar;
891
+ }
892
+ /**
893
+ * Parses raw text element data for accordion layout with items support
894
+ *
895
+ * @param elementResource - Raw element resource data in OCHRE format
896
+ * @returns Parsed text WebElement with items array
897
+ */
898
+ function parseWebElementForAccordion(elementResource, options) {
899
+ const textElement = parseWebElement(elementResource, options);
900
+ const childResources = elementResource.resource ? normalizeWebsiteResources(elementResource.resource) : [];
901
+ const items = [];
902
+ for (const resource of childResources) {
903
+ const resourceType = websitePresentationReader(resource.properties ? parseSimplifiedProperties(resource.properties, options) : []).value("presentation");
904
+ if (resourceType === null) continue;
905
+ switch (resourceType) {
906
+ case "element": {
907
+ const element = parseWebElement(resource, options);
908
+ items.push(element);
909
+ break;
910
+ }
911
+ case "block": {
912
+ const block = parseWebBlock(resource, options);
913
+ if (block) items.push(block);
914
+ break;
915
+ }
916
+ }
917
+ }
918
+ return {
919
+ ...textElement,
920
+ items
921
+ };
922
+ }
923
+ /**
924
+ * Parses raw block data into a standardized WebBlock structure
925
+ *
926
+ * @param blockResource - Raw block resource data in OCHRE format
927
+ * @returns Parsed WebBlock object
928
+ */
929
+ function parseWebBlock(blockResource, options) {
930
+ const blockProperties = blockResource.properties ? parseSimplifiedProperties(blockResource.properties, options) : [];
931
+ const returnBlock = {
932
+ uuid: blockResource.uuid,
933
+ type: "block",
934
+ title: parseWebTitle(blockProperties, parseIdentification(blockResource.identification, options)),
935
+ items: [],
936
+ properties: {
937
+ default: {
938
+ layout: "vertical",
939
+ wrap: "nowrap",
940
+ spacing: null,
941
+ gap: null
942
+ },
943
+ mobile: null,
944
+ tablet: null
945
+ },
946
+ cssStyles: {
947
+ default: [],
948
+ tablet: [],
949
+ mobile: []
950
+ }
951
+ };
952
+ const blockReader = websitePresentationReader(blockProperties).nestedByValue("presentation", "block");
953
+ if (blockReader.size > 0) {
954
+ returnBlock.properties.default.layout = blockReader.valueOr("layout", "vertical");
955
+ returnBlock.properties.default.wrap = blockReader.valueOr("wrap", "nowrap");
956
+ if (returnBlock.properties.default.layout === "accordion") {
957
+ returnBlock.properties.default.isAccordionEnabled = blockReader.valueOr("accordion-enabled", true);
958
+ returnBlock.properties.default.isAccordionExpandedByDefault = blockReader.valueOr("accordion-expanded", true);
959
+ returnBlock.properties.default.isAccordionSidebarDisplayed = blockReader.valueOr("accordion-sidebar-displayed", false);
960
+ }
961
+ returnBlock.properties.default.spacing = blockReader.valueOr("spacing", null);
962
+ returnBlock.properties.default.gap = blockReader.valueOr("gap", null);
963
+ const tabletOverwriteReader = blockReader.nested("overwrite-tablet");
964
+ if (tabletOverwriteReader.size > 0) {
965
+ const propertiesTablet = {
966
+ layout: tabletOverwriteReader.value("layout") ?? void 0,
967
+ wrap: tabletOverwriteReader.value("wrap") ?? void 0,
968
+ spacing: tabletOverwriteReader.value("spacing") ?? void 0,
969
+ gap: tabletOverwriteReader.value("gap") ?? void 0,
970
+ isAccordionEnabled: void 0,
971
+ isAccordionExpandedByDefault: void 0,
972
+ isAccordionSidebarDisplayed: void 0
973
+ };
974
+ if (propertiesTablet.layout === "accordion" || returnBlock.properties.default.layout === "accordion") {
975
+ propertiesTablet.isAccordionEnabled = tabletOverwriteReader.value("accordion-enabled") ?? void 0;
976
+ propertiesTablet.isAccordionExpandedByDefault = tabletOverwriteReader.value("accordion-expanded") ?? void 0;
977
+ propertiesTablet.isAccordionSidebarDisplayed = tabletOverwriteReader.value("accordion-sidebar-displayed") ?? void 0;
978
+ }
979
+ const cleanedPropertiesTablet = cleanObject(propertiesTablet);
980
+ if (Object.keys(cleanedPropertiesTablet).length > 0) returnBlock.properties.tablet = cleanedPropertiesTablet;
981
+ }
982
+ const mobileOverwriteReader = blockReader.nested("overwrite-mobile");
983
+ if (mobileOverwriteReader.size > 0) {
984
+ const propertiesMobile = {
985
+ layout: mobileOverwriteReader.value("layout") ?? void 0,
986
+ wrap: mobileOverwriteReader.value("wrap") ?? void 0,
987
+ spacing: mobileOverwriteReader.value("spacing") ?? void 0,
988
+ gap: mobileOverwriteReader.value("gap") ?? void 0,
989
+ isAccordionEnabled: void 0,
990
+ isAccordionExpandedByDefault: void 0,
991
+ isAccordionSidebarDisplayed: void 0
992
+ };
993
+ if (propertiesMobile.layout === "accordion" || returnBlock.properties.default.layout === "accordion") {
994
+ propertiesMobile.isAccordionEnabled = mobileOverwriteReader.value("accordion-enabled") ?? void 0;
995
+ propertiesMobile.isAccordionExpandedByDefault = mobileOverwriteReader.value("accordion-expanded") ?? void 0;
996
+ propertiesMobile.isAccordionSidebarDisplayed = mobileOverwriteReader.value("accordion-sidebar-displayed") ?? void 0;
997
+ }
998
+ const cleanedPropertiesMobile = cleanObject(propertiesMobile);
999
+ if (Object.keys(cleanedPropertiesMobile).length > 0) returnBlock.properties.mobile = cleanedPropertiesMobile;
1000
+ }
1001
+ }
1002
+ const blockResources = blockResource.resource ? normalizeWebsiteResources(blockResource.resource) : [];
1003
+ if (returnBlock.properties.default.layout === "accordion") {
1004
+ const accordionItems = [];
1005
+ for (const resource of blockResources) {
1006
+ const resourceReader = websitePresentationReader(resource.properties ? parseSimplifiedProperties(resource.properties, options) : []);
1007
+ const resourceType = resourceReader.value("presentation");
1008
+ if (resourceType !== "element") throw new Error(`Accordion only accepts elements, but got “${resourceType}” (${formatXMLWebsiteResourceMetadata(resource)})`, { cause: resource });
1009
+ const componentType = resourceReader.nestedByValue("presentation", "element").value("component");
1010
+ if (componentType !== "text") throw new Error(`Accordion only accepts text components, but got “${componentType}” (${formatXMLWebsiteResourceMetadata(resource)})`, { cause: resource });
1011
+ const element = parseWebElementForAccordion(resource, options);
1012
+ accordionItems.push(element);
1013
+ }
1014
+ returnBlock.items = accordionItems;
1015
+ } else {
1016
+ const blockItems = [];
1017
+ for (const resource of blockResources) {
1018
+ const resourceType = websitePresentationReader(resource.properties ? parseSimplifiedProperties(resource.properties, options) : []).value("presentation");
1019
+ if (resourceType === null) continue;
1020
+ switch (resourceType) {
1021
+ case "element": {
1022
+ const element = parseWebElement(resource, options);
1023
+ blockItems.push(element);
1024
+ break;
1025
+ }
1026
+ case "block": {
1027
+ const block = parseWebBlock(resource, options);
1028
+ if (block) blockItems.push(block);
1029
+ break;
1030
+ }
1031
+ }
1032
+ }
1033
+ returnBlock.items = blockItems;
1034
+ }
1035
+ returnBlock.cssStyles = parseResponsiveCssStyles(blockProperties);
1036
+ return returnBlock;
1037
+ }
1038
+ /**
1039
+ * Parses raw website properties into a standardized Website properties structure
1040
+ *
1041
+ * @param properties - Array of raw website properties in OCHRE format
1042
+ * @returns Parsed WebsiteProperties object
1043
+ */
1044
+ function parseWebsiteProperties(properties, websiteTree, sidebar, options) {
1045
+ const websiteReader = websitePresentationReader(parseSimplifiedProperties({ property: properties }, options)).nested("presentation");
1046
+ const returnProperties = {
1047
+ type: websiteReader.valueOr("webUI", "traditional"),
1048
+ status: websiteReader.valueOr("status", "development"),
1049
+ versionLabel: websiteReader.valueOr("version-label", "release"),
1050
+ privacy: websiteReader.valueOr("privacy", "public"),
1051
+ contact: null,
1052
+ loadingVariant: "spinner",
1053
+ theme: {
1054
+ isThemeToggleDisplayed: true,
1055
+ defaultTheme: "system"
1056
+ },
1057
+ icon: {
1058
+ logoUuid: null,
1059
+ faviconUuid: null,
1060
+ appleTouchIconUuid: null
1061
+ },
1062
+ navbar: {
1063
+ isDisplayed: true,
1064
+ variant: "default",
1065
+ alignment: "start",
1066
+ isProjectDisplayed: true,
1067
+ searchBarBoundElementUuid: null,
1068
+ items: null
1069
+ },
1070
+ footer: {
1071
+ isDisplayed: true,
1072
+ logoUuid: null,
1073
+ items: null
1074
+ },
1075
+ sidebar,
1076
+ itemPage: {
1077
+ isMainContentDisplayed: true,
1078
+ isDescriptionDisplayed: true,
1079
+ isDocumentDisplayed: true,
1080
+ isNotesDisplayed: true,
1081
+ isEventsDisplayed: true,
1082
+ isPeriodsDisplayed: true,
1083
+ isPropertiesDisplayed: true,
1084
+ isBibliographyDisplayed: true,
1085
+ isPropertyValuesGrouped: true,
1086
+ isPublicationDateTimeDisplayed: true,
1087
+ isPersistentIdentifierDisplayed: true,
1088
+ iiifViewer: "universal-viewer"
1089
+ },
1090
+ options: {
1091
+ contextTree: null,
1092
+ scopes: null,
1093
+ labels: { title: null },
1094
+ stylesheets: { properties: [] }
1095
+ }
1096
+ };
1097
+ const contactProperty = websiteReader.property("contact");
1098
+ if (contactProperty !== null) {
1099
+ const contactContent = contactProperty.values[0]?.content.toString().split(";") ?? [];
1100
+ if (contactContent.length === 2) returnProperties.contact = {
1101
+ name: contactContent[0],
1102
+ email: contactContent[1] ?? null
1103
+ };
1104
+ else throw new Error(`Contact property must use “name;email”, got “${contactProperty.values[0]?.content}” (website uuid “${websiteTree.uuid}”)`, { cause: websiteTree });
1105
+ }
1106
+ returnProperties.loadingVariant = websiteReader.valueOr("loading-variant", "spinner");
1107
+ returnProperties.theme.isThemeToggleDisplayed = websiteReader.valueOr("supports-theme-toggle", true);
1108
+ returnProperties.theme.defaultTheme = websiteReader.valueOr("default-theme", "system");
1109
+ returnProperties.icon.logoUuid = websiteReader.uuid("navbar-logo");
1110
+ returnProperties.icon.faviconUuid = websiteReader.uuid("favicon-ico");
1111
+ returnProperties.icon.appleTouchIconUuid = websiteReader.uuid("favicon-img");
1112
+ returnProperties.navbar.isDisplayed = websiteReader.valueOr("navbar-displayed", true);
1113
+ returnProperties.navbar.variant = websiteReader.valueOr("navbar-variant", "default");
1114
+ returnProperties.navbar.alignment = websiteReader.valueOr("navbar-alignment", "start");
1115
+ returnProperties.navbar.isProjectDisplayed = websiteReader.valueOr("navbar-project-displayed", true);
1116
+ returnProperties.navbar.searchBarBoundElementUuid = websiteReader.uuid("bound-element-navbar-search-bar");
1117
+ returnProperties.footer.isDisplayed = websiteReader.valueOr("footer-displayed", true);
1118
+ returnProperties.footer.logoUuid = websiteReader.uuid("footer-logo");
1119
+ const itemPageReader = websiteReader.nestedByValue("page-type", "item-page");
1120
+ if (itemPageReader.size > 0) {
1121
+ returnProperties.itemPage.isMainContentDisplayed = itemPageReader.valueOr("item-page-main-content-displayed", true);
1122
+ returnProperties.itemPage.isDescriptionDisplayed = itemPageReader.valueOr("item-page-description-displayed", true);
1123
+ returnProperties.itemPage.isDocumentDisplayed = itemPageReader.valueOr("item-page-document-displayed", true);
1124
+ returnProperties.itemPage.isNotesDisplayed = itemPageReader.valueOr("item-page-notes-displayed", true);
1125
+ returnProperties.itemPage.isEventsDisplayed = itemPageReader.valueOr("item-page-events-displayed", true);
1126
+ returnProperties.itemPage.isPeriodsDisplayed = itemPageReader.valueOr("item-page-periods-displayed", true);
1127
+ returnProperties.itemPage.isPropertiesDisplayed = itemPageReader.valueOr("item-page-properties-displayed", true);
1128
+ returnProperties.itemPage.isBibliographyDisplayed = itemPageReader.valueOr("item-page-bibliography-displayed", true);
1129
+ returnProperties.itemPage.isPropertyValuesGrouped = itemPageReader.valueOr("item-page-property-values-grouped", true);
1130
+ returnProperties.itemPage.isPublicationDateTimeDisplayed = itemPageReader.valueOr("item-page-publication-date-time-displayed", true);
1131
+ returnProperties.itemPage.isPersistentIdentifierDisplayed = itemPageReader.valueOr("item-page-persistent-identifier-displayed", true);
1132
+ returnProperties.itemPage.iiifViewer = itemPageReader.valueOr("item-page-iiif-viewer", "universal-viewer");
1133
+ }
1134
+ if (websiteTree.options != null) {
1135
+ const parsedOptions = parseWebsiteOptions(websiteTree.options, options);
1136
+ returnProperties.options.scopes = parsedOptions.scopes;
1137
+ returnProperties.options.contextTree = parsedOptions.contextTree;
1138
+ returnProperties.options.labels = parsedOptions.labels;
1139
+ }
1140
+ if ("styleOptions" in websiteTree && websiteTree.styleOptions != null) returnProperties.options.stylesheets.properties = parseStylesheets(websiteTree.styleOptions.style);
1141
+ return returnProperties;
1142
+ }
1143
+ function parseContextItem(contextItemToParse, options) {
1144
+ let type = "";
1145
+ const levels = [];
1146
+ for (const level of contextItemToParse.levels?.level ?? []) {
1147
+ const [rawVariableUuid = "", rawValueUuid] = level.payload.split(",");
1148
+ const valueUuid = rawValueUuid == null || rawValueUuid.trim() === "null" ? null : rawValueUuid.trim();
1149
+ type = level.dataType ?? type;
1150
+ levels.push({
1151
+ variableUuid: rawVariableUuid.trim(),
1152
+ valueUuid
1153
+ });
1154
+ }
1155
+ return {
1156
+ context: levels,
1157
+ type,
1158
+ identification: parseIdentification(contextItemToParse.identification, options)
1159
+ };
1160
+ }
1161
+ function parseFilterContextDisplay(filterOption) {
1162
+ switch (filterOption) {
1163
+ case "inline-displayed": return {
1164
+ isInlineDisplayed: true,
1165
+ isSidebarDisplayed: false,
1166
+ isSidebarOpen: false
1167
+ };
1168
+ case "inline-sidebar-displayed-closed": return {
1169
+ isInlineDisplayed: true,
1170
+ isSidebarDisplayed: true,
1171
+ isSidebarOpen: false
1172
+ };
1173
+ case "inline-sidebar-displayed-open": return {
1174
+ isInlineDisplayed: true,
1175
+ isSidebarDisplayed: true,
1176
+ isSidebarOpen: true
1177
+ };
1178
+ case "sidebar-displayed-closed": return {
1179
+ isInlineDisplayed: false,
1180
+ isSidebarDisplayed: true,
1181
+ isSidebarOpen: false
1182
+ };
1183
+ case "sidebar-displayed-open": return {
1184
+ isInlineDisplayed: false,
1185
+ isSidebarDisplayed: true,
1186
+ isSidebarOpen: true
1187
+ };
1188
+ default: return {
1189
+ isInlineDisplayed: false,
1190
+ isSidebarDisplayed: false,
1191
+ isSidebarOpen: false
1192
+ };
1193
+ }
1194
+ }
1195
+ function parseContexts(contextLevels, options) {
1196
+ const contextTreeLevels = [];
1197
+ for (const contextLevel of contextLevels) for (const contextItemToParse of contextLevel.context) contextTreeLevels.push(parseContextItem(contextItemToParse, options));
1198
+ return contextTreeLevels;
1199
+ }
1200
+ function parseFilterContexts(filterContextLevels, options) {
1201
+ const filterContextTreeLevels = [];
1202
+ for (const filterContextLevel of filterContextLevels) for (const contextItemToParse of filterContextLevel.context) filterContextTreeLevels.push({
1203
+ ...parseContextItem(contextItemToParse, options),
1204
+ filterType: contextItemToParse.filterType ?? "property",
1205
+ ...parseFilterContextDisplay(contextItemToParse.filterOption)
1206
+ });
1207
+ return filterContextTreeLevels;
1208
+ }
1209
+ function parseWebsiteTree(websiteTree, context, type, options, slugPrefix) {
1210
+ if (!websiteTree.properties) throw new Error(`Website properties not found (website uuid “${websiteTree.uuid}”)`, { cause: websiteTree });
1211
+ if (type === "website" && websiteTree.items?.resource == null) throw new Error(`Website pages not found (website uuid “${websiteTree.uuid}”)`, { cause: websiteTree });
1212
+ const resources = normalizeWebsiteResources(websiteTree.items?.resource);
1213
+ const sidebar = parseSidebar(resources, options);
1214
+ const properties = parseWebsiteProperties(websiteTree.properties.property, websiteTree, sidebar, options);
1215
+ return {
1216
+ uuid: websiteTree.uuid,
1217
+ type,
1218
+ belongsTo: context.belongsTo,
1219
+ metadata: context.metadata,
1220
+ publicationDateTime: websiteTree.publicationDateTime ?? null,
1221
+ identification: parseIdentification(websiteTree.identification, options),
1222
+ creators: websiteTree.creators ? parsePersonList(websiteTree.creators.creator, options) : [],
1223
+ license: parseLicense(websiteTree.availability),
1224
+ items: parseWebpages(resources, options, context, slugPrefix),
1225
+ properties
1226
+ };
1227
+ }
1228
+ function parseWebsite(data, options) {
1229
+ const rawOchre = data.result.ochre;
1230
+ const metadataLanguages = parseMetadataLanguages(rawOchre);
1231
+ const languages = resolveLanguages(options?.languages ?? [], metadataLanguages);
1232
+ const parserOptions = { languages };
1233
+ const defaultLanguage = resolveDefaultLanguage(rawOchre, languages);
1234
+ const websiteTree = rawOchre.tree[0];
1235
+ if (websiteTree == null) throw new Error("Website tree not found", { cause: data });
1236
+ return parseWebsiteTree(websiteTree, {
1237
+ belongsTo: {
1238
+ uuid: rawOchre.uuidBelongsTo,
1239
+ abbreviation: rawOchre.belongsTo
1240
+ },
1241
+ metadata: parseMetadata(rawOchre, parserOptions, defaultLanguage)
1242
+ }, "website", parserOptions);
1243
+ }
1244
+ //#endregion
1245
+ export { parseBounds, parseWebpageView, parseWebsite };