@digitalculture/ochre-sdk 0.11.20 → 0.11.21

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 (3) hide show
  1. package/dist/index.d.ts +1185 -0
  2. package/dist/index.js +3119 -0
  3. package/package.json +1 -1
package/dist/index.js ADDED
@@ -0,0 +1,3119 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/schemas.ts
4
+ /**
5
+ * Schema for validating UUIDs
6
+ * @internal
7
+ */
8
+ const uuidSchema = z.uuid({ error: "Invalid UUID provided" });
9
+ /**
10
+ * Schema for validating website properties
11
+ * @internal
12
+ */
13
+ const websiteSchema = z.object({
14
+ type: z.enum([
15
+ "traditional",
16
+ "digital-collection",
17
+ "plum",
18
+ "cedar",
19
+ "elm",
20
+ "maple",
21
+ "oak",
22
+ "palm"
23
+ ], { error: "Invalid website type" }),
24
+ status: z.enum([
25
+ "development",
26
+ "preview",
27
+ "production"
28
+ ], { error: "Invalid website status" }),
29
+ privacy: z.enum([
30
+ "public",
31
+ "password",
32
+ "private"
33
+ ], { error: "Invalid website privacy" })
34
+ });
35
+ /**
36
+ * Valid component types for web elements
37
+ * @internal
38
+ */
39
+ const componentSchema = z.enum([
40
+ "annotated-document",
41
+ "annotated-image",
42
+ "audio-player",
43
+ "bibliography",
44
+ "button",
45
+ "collection",
46
+ "empty-space",
47
+ "entries",
48
+ "iframe",
49
+ "iiif-viewer",
50
+ "image",
51
+ "image-gallery",
52
+ "map",
53
+ "network-graph",
54
+ "query",
55
+ "search-bar",
56
+ "table",
57
+ "text",
58
+ "timeline",
59
+ "video"
60
+ ], { error: "Invalid component" });
61
+ /**
62
+ * Schema for validating data categories
63
+ * @internal
64
+ */
65
+ const categorySchema = z.enum([
66
+ "resource",
67
+ "spatialUnit",
68
+ "concept",
69
+ "period",
70
+ "bibliography",
71
+ "person",
72
+ "propertyValue",
73
+ "set",
74
+ "tree"
75
+ ]);
76
+ /**
77
+ * Schema for validating property value content types
78
+ * @internal
79
+ */
80
+ const propertyValueContentTypeSchema = z.enum([
81
+ "string",
82
+ "integer",
83
+ "decimal",
84
+ "boolean",
85
+ "date",
86
+ "dateTime",
87
+ "time",
88
+ "coordinate",
89
+ "IDREF"
90
+ ]);
91
+ /**
92
+ * Schema for validating gallery parameters
93
+ * @internal
94
+ */
95
+ const gallerySchema = z.object({
96
+ uuid: z.uuid({ error: "Invalid UUID" }),
97
+ filter: z.string().optional(),
98
+ page: z.number().positive({ error: "Page must be positive" }),
99
+ perPage: z.number().positive({ error: "Per page must be positive" })
100
+ }).strict();
101
+ /**
102
+ * Schema for validating and parsing render options
103
+ * @internal
104
+ */
105
+ const renderOptionsSchema = z.string().transform((str) => str.split(" ")).pipe(z.array(z.enum([
106
+ "bold",
107
+ "italic",
108
+ "underline"
109
+ ])));
110
+ /**
111
+ * Schema for validating and parsing whitespace options
112
+ * @internal
113
+ */
114
+ const whitespaceSchema = z.string().transform((str) => str.split(" ")).pipe(z.array(z.enum([
115
+ "newline",
116
+ "trailing",
117
+ "leading"
118
+ ])));
119
+ /**
120
+ * Schema for validating email addresses
121
+ * @internal
122
+ */
123
+ const emailSchema = z.email({ error: "Invalid email" });
124
+
125
+ //#endregion
126
+ //#region src/utils/getters.ts
127
+ const DEFAULT_OPTIONS = { includeNestedProperties: false };
128
+ /**
129
+ * Finds a property by its UUID in an array of properties
130
+ *
131
+ * @param properties - Array of properties to search through
132
+ * @param uuid - The UUID to search for
133
+ * @param options - Search options, including whether to include nested properties
134
+ * @returns The matching Property object, or null if not found
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * const property = getPropertyByUuid(properties, "123e4567-e89b-12d3-a456-426614174000", { includeNestedProperties: true });
139
+ * if (property) {
140
+ * console.log(property.values);
141
+ * }
142
+ * ```
143
+ */
144
+ function getPropertyByUuid(properties, uuid, options = DEFAULT_OPTIONS) {
145
+ const { includeNestedProperties } = options;
146
+ const property = properties.find((property$1) => property$1.uuid === uuid);
147
+ if (property) return property;
148
+ if (includeNestedProperties) {
149
+ for (const property$1 of properties) if (property$1.properties.length > 0) {
150
+ const nestedResult = getPropertyByUuid(property$1.properties, uuid, { includeNestedProperties });
151
+ if (nestedResult) return nestedResult;
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+ /**
157
+ * Retrieves all values for a property with the given UUID
158
+ *
159
+ * @param properties - Array of properties to search through
160
+ * @param uuid - The UUID to search for
161
+ * @param options - Search options, including whether to include nested properties
162
+ * @returns Array of property values as strings, or null if property not found
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const values = getPropertyValuesByUuid(properties, "123e4567-e89b-12d3-a456-426614174000");
167
+ * if (values) {
168
+ * for (const value of values) {
169
+ * console.log(value);
170
+ * }
171
+ * }
172
+ * ```
173
+ */
174
+ function getPropertyValuesByUuid(properties, uuid, options = DEFAULT_OPTIONS) {
175
+ const { includeNestedProperties } = options;
176
+ const property = properties.find((property$1) => property$1.uuid === uuid);
177
+ if (property) return property.values.map((value) => value.content);
178
+ if (includeNestedProperties) {
179
+ for (const property$1 of properties) if (property$1.properties.length > 0) {
180
+ const nestedResult = getPropertyValuesByUuid(property$1.properties, uuid, { includeNestedProperties });
181
+ if (nestedResult) return nestedResult;
182
+ }
183
+ }
184
+ return null;
185
+ }
186
+ /**
187
+ * Gets the first value of a property with the given UUID
188
+ *
189
+ * @param properties - Array of properties to search through
190
+ * @param uuid - The UUID to search for
191
+ * @param options - Search options, including whether to include nested properties
192
+ * @returns The first property value as string, or null if property not found
193
+ *
194
+ * @example
195
+ * ```ts
196
+ * const title = getPropertyValueByUuid(properties, "123e4567-e89b-12d3-a456-426614174000");
197
+ * if (title) {
198
+ * console.log(`Document title: ${title}`);
199
+ * }
200
+ * ```
201
+ */
202
+ function getPropertyValueByUuid(properties, uuid, options = DEFAULT_OPTIONS) {
203
+ const { includeNestedProperties } = options;
204
+ const values = getPropertyValuesByUuid(properties, uuid, { includeNestedProperties });
205
+ if (values !== null && values.length > 0) return values[0];
206
+ if (includeNestedProperties) {
207
+ for (const property of properties) if (property.properties.length > 0) {
208
+ const nestedResult = getPropertyValueByUuid(property.properties, uuid, { includeNestedProperties });
209
+ if (nestedResult !== null) return nestedResult;
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+ /**
215
+ * Finds a property by its label in an array of properties
216
+ *
217
+ * @param properties - Array of properties to search through
218
+ * @param label - The label to search for
219
+ * @param options - Search options, including whether to include nested properties
220
+ * @returns The matching Property object, or null if not found
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * const property = getPropertyByLabel(properties, "author", { includeNestedProperties: true });
225
+ * if (property) {
226
+ * console.log(property.values);
227
+ * }
228
+ * ```
229
+ */
230
+ function getPropertyByLabel(properties, label, options = DEFAULT_OPTIONS) {
231
+ const { includeNestedProperties } = options;
232
+ const property = properties.find((property$1) => property$1.label === label);
233
+ if (property) return property;
234
+ if (includeNestedProperties) {
235
+ for (const property$1 of properties) if (property$1.properties.length > 0) {
236
+ const nestedResult = getPropertyByLabel(property$1.properties, label, { includeNestedProperties });
237
+ if (nestedResult) return nestedResult;
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+ /**
243
+ * Retrieves all values for a property with the given label
244
+ *
245
+ * @param properties - Array of properties to search through
246
+ * @param label - The label to search for
247
+ * @param options - Search options, including whether to include nested properties
248
+ * @returns Array of property values as strings, or null if property not found
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * const values = getPropertyValuesByLabel(properties, "keywords");
253
+ * if (values) {
254
+ * for (const value of values) {
255
+ * console.log(value);
256
+ * }
257
+ * }
258
+ * ```
259
+ */
260
+ function getPropertyValuesByLabel(properties, label, options = DEFAULT_OPTIONS) {
261
+ const { includeNestedProperties } = options;
262
+ const property = properties.find((property$1) => property$1.label === label);
263
+ if (property) return property.values.map((value) => value.content);
264
+ if (includeNestedProperties) {
265
+ for (const property$1 of properties) if (property$1.properties.length > 0) {
266
+ const nestedResult = getPropertyValuesByLabel(property$1.properties, label, { includeNestedProperties });
267
+ if (nestedResult) return nestedResult;
268
+ }
269
+ }
270
+ return null;
271
+ }
272
+ /**
273
+ * Gets the first value of a property with the given label
274
+ *
275
+ * @param properties - Array of properties to search through
276
+ * @param label - The label to search for
277
+ * @param options - Search options, including whether to include nested properties
278
+ * @returns The first property value as string, or null if property not found
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * const title = getPropertyValueByLabel(properties, "title");
283
+ * if (title) {
284
+ * console.log(`Document title: ${title}`);
285
+ * }
286
+ * ```
287
+ */
288
+ function getPropertyValueByLabel(properties, label, options = DEFAULT_OPTIONS) {
289
+ const { includeNestedProperties } = options;
290
+ const values = getPropertyValuesByLabel(properties, label, { includeNestedProperties });
291
+ if (values !== null && values.length > 0) return values[0];
292
+ if (includeNestedProperties) {
293
+ for (const property of properties) if (property.properties.length > 0) {
294
+ const nestedResult = getPropertyValueByLabel(property.properties, label, { includeNestedProperties });
295
+ if (nestedResult !== null) return nestedResult;
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+ /**
301
+ * Gets all unique properties from an array of properties
302
+ *
303
+ * @param properties - Array of properties to get unique properties from
304
+ * @param options - Search options, including whether to include nested properties
305
+ * @returns Array of unique properties
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * const properties = getAllUniqueProperties(properties, { includeNestedProperties: true });
310
+ * console.log(`Available properties: ${properties.map((p) => p.label).join(", ")}`);
311
+ * ```
312
+ */
313
+ function getUniqueProperties(properties, options = DEFAULT_OPTIONS) {
314
+ const { includeNestedProperties } = options;
315
+ const uniqueProperties = new Array();
316
+ for (const property of properties) {
317
+ if (uniqueProperties.some((p) => p.uuid === property.uuid)) continue;
318
+ uniqueProperties.push(property);
319
+ if (property.properties.length > 0 && includeNestedProperties) {
320
+ const nestedProperties = getUniqueProperties(property.properties, { includeNestedProperties: true });
321
+ for (const property$1 of nestedProperties) {
322
+ if (uniqueProperties.some((p) => p.uuid === property$1.uuid)) continue;
323
+ uniqueProperties.push(property$1);
324
+ }
325
+ }
326
+ }
327
+ return uniqueProperties;
328
+ }
329
+ /**
330
+ * Gets all unique property labels from an array of properties
331
+ *
332
+ * @param properties - Array of properties to get unique property labels from
333
+ * @param options - Search options, including whether to include nested properties
334
+ * @returns Array of unique property labels
335
+ *
336
+ * @example
337
+ * ```ts
338
+ * const properties = getAllUniquePropertyLabels(properties, { includeNestedProperties: true });
339
+ * console.log(`Available properties: ${properties.join(", ")}`);
340
+ * ```
341
+ */
342
+ function getUniquePropertyLabels(properties, options = DEFAULT_OPTIONS) {
343
+ const { includeNestedProperties } = options;
344
+ const uniquePropertyLabels = new Array();
345
+ for (const property of properties) {
346
+ if (uniquePropertyLabels.includes(property.label)) continue;
347
+ uniquePropertyLabels.push(property.label);
348
+ if (property.properties.length > 0 && includeNestedProperties) {
349
+ const nestedProperties = getUniquePropertyLabels(property.properties, { includeNestedProperties: true });
350
+ for (const property$1 of nestedProperties) {
351
+ if (uniquePropertyLabels.includes(property$1)) continue;
352
+ uniquePropertyLabels.push(property$1);
353
+ }
354
+ }
355
+ }
356
+ return uniquePropertyLabels;
357
+ }
358
+ /**
359
+ * Filters a property based on a label and value criteria
360
+ *
361
+ * @param property - The property to filter
362
+ * @param filter - Filter criteria containing label and value to match
363
+ * @param filter.label - The label to filter by
364
+ * @param filter.value - The value to filter by
365
+ * @param options - Search options, including whether to include nested properties
366
+ * @returns True if the property matches the filter criteria, false otherwise
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * const matches = filterProperties(property, {
371
+ * label: "category",
372
+ * value: "book"
373
+ * });
374
+ * if (matches) {
375
+ * console.log("Property matches filter criteria");
376
+ * }
377
+ * ```
378
+ */
379
+ function filterProperties(property, filter, options = DEFAULT_OPTIONS) {
380
+ const { includeNestedProperties } = options;
381
+ if (filter.label.toLocaleLowerCase("en-US") === "all fields" || property.label.toLocaleLowerCase("en-US") === filter.label.toLocaleLowerCase("en-US")) {
382
+ let isFound = property.values.some((value) => {
383
+ if (value.content === null) return false;
384
+ if (typeof value.content === "string") {
385
+ if (typeof filter.value !== "string") return false;
386
+ return value.content.toLocaleLowerCase("en-US").includes(filter.value.toLocaleLowerCase("en-US"));
387
+ }
388
+ if (typeof value.content === "number") {
389
+ if (typeof filter.value !== "number") return false;
390
+ return value.content === filter.value;
391
+ }
392
+ if (typeof value.content === "boolean") {
393
+ if (typeof filter.value !== "boolean") return false;
394
+ return value.content === filter.value;
395
+ }
396
+ return false;
397
+ });
398
+ if (!isFound && includeNestedProperties) isFound = property.properties.some((property$1) => filterProperties(property$1, filter, { includeNestedProperties: true }));
399
+ return isFound;
400
+ }
401
+ return false;
402
+ }
403
+
404
+ //#endregion
405
+ //#region src/utils/string.ts
406
+ const PRESENTATION_ITEM_UUID = "f1c131b6-1498-48a4-95bf-a9edae9fd518";
407
+ const TEXT_ANNOTATION_UUID = "b9ca2732-78f4-416e-b77f-dae7647e68a9";
408
+ const TEXT_ANNOTATION_TEXT_STYLING_UUID = "3e6f86ab-df81-45ae-8257-e2867357df56";
409
+ const TEXT_ANNOTATION_TEXT_STYLING_VARIANT_UUID = "e1647bef-d801-4100-bdde-d081c422f763";
410
+ /**
411
+ * Finds a string item in an array by language code
412
+ *
413
+ * @param content - Array of string items to search
414
+ * @param language - Language code to search for
415
+ * @returns Matching string item or null if not found
416
+ * @internal
417
+ */
418
+ function getStringItemByLanguage(content, language) {
419
+ return content.find((item) => item.lang === language) ?? null;
420
+ }
421
+ /**
422
+ * Parses email addresses in a string into HTML links
423
+ *
424
+ * @param string - Input string to parse
425
+ * @returns String with emails converted to HTML links
426
+ *
427
+ * @example
428
+ * ```ts
429
+ * const parsed = parseEmail("Contact us at info@example.com");
430
+ * // Returns: "Contact us at <ExternalLink href="mailto:info@example.com">info@example.com</ExternalLink>"
431
+ * ```
432
+ */
433
+ function parseEmail(string) {
434
+ const splitString = string.split(" ");
435
+ const returnSplitString = [];
436
+ for (const string$1 of splitString) {
437
+ const cleanString = string$1.replaceAll(/(?<=\s|^)[([{]+|[)\]}]+(?=\s|$)/g, "").replaceAll(/[!),:;?\]]/g, "").replace(/\.$/, "");
438
+ const index = string$1.indexOf(cleanString);
439
+ const before = string$1.slice(0, index);
440
+ const after = string$1.slice(index + cleanString.length);
441
+ if (emailSchema.safeParse(cleanString).success) {
442
+ returnSplitString.push(`${before}<ExternalLink href="mailto:${cleanString}">${cleanString}</ExternalLink>${after}`);
443
+ continue;
444
+ }
445
+ returnSplitString.push(string$1);
446
+ }
447
+ return returnSplitString.join(" ");
448
+ }
449
+ /**
450
+ * Applies text rendering options (bold, italic, underline) to a string
451
+ *
452
+ * @param contentString - The string content to render
453
+ * @param renderString - Space-separated string of render options
454
+ * @returns String with markdown formatting applied
455
+ * @internal
456
+ */
457
+ function parseRenderOptions(contentString, renderString) {
458
+ let returnString = contentString;
459
+ const result = renderOptionsSchema.safeParse(renderString);
460
+ if (!result.success) {
461
+ console.warn(`Invalid render options string provided: “${renderString}”`);
462
+ return contentString;
463
+ }
464
+ for (const option of result.data) switch (option) {
465
+ case "bold":
466
+ returnString = `**${returnString}**`;
467
+ break;
468
+ case "italic":
469
+ returnString = `*${returnString}*`;
470
+ break;
471
+ case "underline":
472
+ returnString = `_${returnString}_`;
473
+ break;
474
+ }
475
+ return returnString.replaceAll("&#39;", "'");
476
+ }
477
+ /**
478
+ * Applies whitespace options to a string (newline, trailing, leading)
479
+ *
480
+ * @param contentString - The string content to modify
481
+ * @param whitespace - Space-separated string of whitespace options
482
+ * @returns String with whitespace modifications applied
483
+ * @internal
484
+ */
485
+ function parseWhitespace(contentString, whitespace) {
486
+ let returnString = contentString;
487
+ const result = whitespaceSchema.safeParse(whitespace);
488
+ if (!result.success) {
489
+ console.warn(`Invalid whitespace string provided: “${whitespace}”`);
490
+ return contentString;
491
+ }
492
+ for (const option of result.data) switch (option) {
493
+ case "newline":
494
+ returnString = `<br />\n${returnString}`;
495
+ break;
496
+ case "trailing":
497
+ returnString = `${returnString} `;
498
+ break;
499
+ case "leading":
500
+ returnString = ` ${returnString}`;
501
+ break;
502
+ }
503
+ return returnString.replaceAll("&#39;", "'");
504
+ }
505
+ /**
506
+ * Converts a FakeString (string|number|boolean) to a proper string
507
+ *
508
+ * @param string - FakeString value to convert
509
+ * @returns Converted string value
510
+ *
511
+ * @example
512
+ * ```ts
513
+ * parseFakeString(true); // Returns "Yes"
514
+ * parseFakeString(123); // Returns "123"
515
+ * parseFakeString("test"); // Returns "test"
516
+ * ```
517
+ */
518
+ function parseFakeString(string) {
519
+ return String(string).replaceAll("&#39;", "'").replaceAll("{", String.raw`\{`).replaceAll("}", String.raw`\}`);
520
+ }
521
+ /**
522
+ * Parses an OchreStringItem into a formatted string
523
+ *
524
+ * @param item - OchreStringItem to parse
525
+ * @returns Formatted string with applied rendering and whitespace
526
+ */
527
+ function parseStringItem(item) {
528
+ let returnString = "";
529
+ switch (typeof item.string) {
530
+ case "string":
531
+ case "number":
532
+ case "boolean":
533
+ returnString = parseFakeString(item.string);
534
+ break;
535
+ case "object": {
536
+ const stringItems = Array.isArray(item.string) ? item.string : [item.string];
537
+ for (const stringItem of stringItems) if (typeof stringItem === "string" || typeof stringItem === "number" || typeof stringItem === "boolean") returnString += parseFakeString(stringItem);
538
+ else if ("string" in stringItem) returnString += parseStringDocumentItem(stringItem);
539
+ else {
540
+ const renderedText = stringItem.content == null ? "" : stringItem.rend != null ? parseRenderOptions(parseFakeString(stringItem.content), stringItem.rend) : parseFakeString(stringItem.content);
541
+ const whitespacedText = stringItem.whitespace != null ? parseWhitespace(renderedText, stringItem.whitespace) : renderedText;
542
+ returnString += whitespacedText;
543
+ }
544
+ break;
545
+ }
546
+ default:
547
+ returnString = "";
548
+ break;
549
+ }
550
+ return returnString;
551
+ }
552
+ /**
553
+ * Parses rich text content into a formatted string with links and annotations
554
+ *
555
+ * @param item - Rich text item to parse
556
+ * @returns Formatted string with HTML/markdown elements
557
+ */
558
+ function parseStringDocumentItem(item) {
559
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") return parseEmail(parseFakeString(item));
560
+ if ("whitespace" in item && !("content" in item) && !("string" in item)) if (item.whitespace === "newline") return " \n";
561
+ else return parseWhitespace("", item.whitespace);
562
+ if ("links" in item) {
563
+ let itemString = "";
564
+ if (typeof item.string === "object") itemString = parseStringContent(item.string);
565
+ else itemString = parseFakeString(item.string).replaceAll("<", String.raw`\<`).replaceAll("{", String.raw`\{`);
566
+ const itemLinks = Array.isArray(item.links) ? item.links : [item.links];
567
+ for (const link of itemLinks) if ("resource" in link) {
568
+ const linkResource = Array.isArray(link.resource) ? link.resource[0] : link.resource;
569
+ let linkContent = null;
570
+ if (linkResource.content != null) linkContent = parseFakeString(linkResource.content).replaceAll("<", String.raw`\<`).replaceAll("{", String.raw`\{`);
571
+ switch (linkResource.type) {
572
+ case "image": if (linkResource.rend === "inline") return `<InlineImage uuid="${linkResource.uuid}" ${linkContent !== null ? `content="${linkContent}"` : ""} height={${linkResource.height?.toString() ?? "null"}} width={${linkResource.width?.toString() ?? "null"}} />`;
573
+ else if (linkResource.publicationDateTime != null) return `<InternalLink uuid="${linkResource.uuid}">${itemString}</InternalLink>`;
574
+ else return `<TooltipSpan${linkContent !== null ? ` content="${linkContent}"` : ""}>${itemString}</TooltipSpan>`;
575
+ case "internalDocument": if ("properties" in item && item.properties != null) {
576
+ const itemProperty = Array.isArray(item.properties.property) ? item.properties.property[0] : item.properties.property;
577
+ if (itemProperty != null) {
578
+ const itemPropertyLabelUuid = itemProperty.label.uuid;
579
+ const itemPropertyValueUuid = typeof itemProperty.value === "object" && "uuid" in itemProperty.value && itemProperty.value.uuid != null ? itemProperty.value.uuid : null;
580
+ if (itemPropertyLabelUuid === PRESENTATION_ITEM_UUID && itemPropertyValueUuid === TEXT_ANNOTATION_UUID) {
581
+ if ((itemProperty.property != null ? Array.isArray(itemProperty.property) ? itemProperty.property[0] : itemProperty.property : null) != null) return `<Annotation type="hover-card" uuid="${linkResource.uuid}">${itemString}</Annotation>`;
582
+ }
583
+ return `<InternalLink uuid="${linkResource.uuid}" properties="${itemPropertyLabelUuid}"${itemPropertyValueUuid !== null ? ` value="${itemPropertyValueUuid}"` : ""}>${itemString}</InternalLink>`;
584
+ } else return `<InternalLink uuid="${linkResource.uuid}">${itemString}</InternalLink>`;
585
+ } else return `<InternalLink uuid="${linkResource.uuid}">${itemString}</InternalLink>`;
586
+ case "externalDocument": if (linkResource.publicationDateTime != null) return `<ExternalLink href="https:\\/\\/ochre.lib.uchicago.edu/ochre?uuid=${linkResource.uuid}&load" ${linkContent !== null ? `content="${linkContent}"` : ""}>${itemString}</ExternalLink>`;
587
+ else return `<TooltipSpan${linkContent !== null ? ` content="${linkContent}"` : ""}>${itemString}</TooltipSpan>`;
588
+ case "webpage": return `<ExternalLink href="${linkResource.href}" ${linkContent !== null ? `content="${linkContent}"` : ""}>${itemString}</ExternalLink>`;
589
+ default: return "";
590
+ }
591
+ } else if ("concept" in link) {
592
+ const linkConcept = Array.isArray(link.concept) ? link.concept[0] : link.concept;
593
+ if (linkConcept.publicationDateTime != null) return `<InternalLink uuid="${linkConcept.uuid}">${itemString}</InternalLink>`;
594
+ else return `<TooltipSpan>${itemString}</TooltipSpan>`;
595
+ } else if ("set" in link) {
596
+ const linkSet = Array.isArray(link.set) ? link.set[0] : link.set;
597
+ if (linkSet.publicationDateTime != null) return `<InternalLink uuid="${linkSet.uuid}">${itemString}</InternalLink>`;
598
+ else return `<TooltipSpan>${itemString}</TooltipSpan>`;
599
+ } else if ("person" in link) {
600
+ const linkPerson = Array.isArray(link.person) ? link.person[0] : link.person;
601
+ const linkContent = linkPerson.identification ? [
602
+ "string",
603
+ "number",
604
+ "boolean"
605
+ ].includes(typeof linkPerson.identification.label) ? parseFakeString(linkPerson.identification.label) : parseStringContent(linkPerson.identification.label) : null;
606
+ if (linkPerson.publicationDateTime != null) return `<InternalLink uuid="${linkPerson.uuid}">${itemString}</InternalLink>`;
607
+ else return `<TooltipSpan${linkContent !== null ? ` content="${linkContent}"` : ""}>${itemString}</TooltipSpan>`;
608
+ } else if ("bibliography" in link) {
609
+ const linkBibliography = Array.isArray(link.bibliography) ? link.bibliography[0] : link.bibliography;
610
+ if (linkBibliography.publicationDateTime != null) return `<InternalLink uuid="${linkBibliography.uuid}">${itemString}</InternalLink>`;
611
+ else return `<TooltipSpan>${itemString}</TooltipSpan>`;
612
+ } else if ("properties" in item && item.properties != null) {
613
+ const itemProperty = Array.isArray(item.properties.property) ? item.properties.property[0] : item.properties.property;
614
+ if (itemProperty != null) {
615
+ const itemPropertyLabelUuid = itemProperty.label.uuid;
616
+ const itemPropertyValueUuid = typeof itemProperty.value === "object" && "uuid" in itemProperty.value && itemProperty.value.uuid != null ? itemProperty.value.uuid : null;
617
+ if (itemPropertyLabelUuid === PRESENTATION_ITEM_UUID && itemPropertyValueUuid === TEXT_ANNOTATION_UUID) {
618
+ const textAnnotationProperty = itemProperty.property != null ? Array.isArray(itemProperty.property) ? itemProperty.property[0] : itemProperty.property : null;
619
+ if (textAnnotationProperty != null) {
620
+ if ((typeof textAnnotationProperty.value === "object" && "uuid" in textAnnotationProperty.value && textAnnotationProperty.value.uuid != null ? textAnnotationProperty.value.uuid : null) === TEXT_ANNOTATION_TEXT_STYLING_UUID && textAnnotationProperty.property != null) {
621
+ const textStylingType = "text-styling";
622
+ let textStylingVariant = "default";
623
+ let textStylingSize = "md";
624
+ let textStylingCss = [];
625
+ const textStylingProperties = Array.isArray(textAnnotationProperty.property) ? textAnnotationProperty.property : [textAnnotationProperty.property];
626
+ if (textStylingProperties.length > 0) {
627
+ const textStylingVariantProperty = textStylingProperties.find((property) => property.label.uuid === TEXT_ANNOTATION_TEXT_STYLING_VARIANT_UUID);
628
+ if (textStylingVariantProperty != null) {
629
+ const textStylingPropertyVariant = parseFakeString(textStylingVariantProperty.value.content);
630
+ const textStylingSizeProperty = textStylingVariantProperty.property != null ? Array.isArray(textStylingVariantProperty.property) ? textStylingVariantProperty.property[0] : textStylingVariantProperty.property : null;
631
+ if (textStylingSizeProperty != null) textStylingSize = parseFakeString(textStylingSizeProperty.value.content);
632
+ textStylingVariant = textStylingPropertyVariant;
633
+ }
634
+ const textStylingCssProperties = textStylingProperties.filter((property) => property.label.uuid !== TEXT_ANNOTATION_TEXT_STYLING_VARIANT_UUID);
635
+ if (textStylingCssProperties.length > 0) textStylingCss = textStylingCssProperties.map((property) => ({
636
+ label: parseFakeString(property.label.content),
637
+ value: parseFakeString(property.value.content)
638
+ }));
639
+ }
640
+ return `<Annotation type="${textStylingType}" variant="${textStylingVariant}" size="${textStylingSize}"${textStylingCss.length > 0 ? ` cssStyles={{default: ${JSON.stringify(textStylingCss)}, tablet: [], mobile: []}}` : ""}>${itemString}</Annotation>`;
641
+ }
642
+ }
643
+ }
644
+ }
645
+ }
646
+ }
647
+ let returnString = "";
648
+ if ("string" in item) {
649
+ const stringItems = Array.isArray(item.string) ? item.string : [item.string];
650
+ for (const stringItem of stringItems) returnString += parseStringDocumentItem(stringItem);
651
+ if ("whitespace" in item && item.whitespace != null) returnString = parseWhitespace(parseEmail(returnString), item.whitespace);
652
+ return returnString.replaceAll("&#39;", "'");
653
+ } else {
654
+ returnString = parseFakeString(item.content);
655
+ if (item.rend != null) returnString = parseRenderOptions(parseEmail(returnString), item.rend);
656
+ if (item.whitespace != null) returnString = parseWhitespace(parseEmail(returnString), item.whitespace);
657
+ }
658
+ return returnString;
659
+ }
660
+ /**
661
+ * Parses raw string content into a formatted string
662
+ *
663
+ * @param content - Raw string content to parse
664
+ * @param language - Optional language code for content selection (defaults to "eng")
665
+ * @returns Parsed and formatted string
666
+ */
667
+ function parseStringContent(content, language = "eng") {
668
+ switch (typeof content.content) {
669
+ case "string":
670
+ case "number":
671
+ case "boolean":
672
+ if (content.rend != null) return parseRenderOptions(parseFakeString(content.content), content.rend);
673
+ return parseFakeString(content.content);
674
+ case "object": if (Array.isArray(content.content)) {
675
+ const stringItem = getStringItemByLanguage(content.content, language);
676
+ if (stringItem) return parseStringItem(stringItem);
677
+ else {
678
+ const returnStringItem = content.content[0];
679
+ if (!returnStringItem) throw new Error(`No string item found for language “${language}” in the following content:\n${JSON.stringify(content.content)}.`);
680
+ return parseStringItem(returnStringItem);
681
+ }
682
+ } else return parseStringItem(content.content);
683
+ default: return String(content.content);
684
+ }
685
+ }
686
+
687
+ //#endregion
688
+ //#region src/utils/helpers.ts
689
+ /**
690
+ * Get the category of an item from the OCHRE API response
691
+ * @param keys - The keys of the OCHRE API response
692
+ * @returns The category of the item
693
+ * @internal
694
+ */
695
+ function getItemCategory(keys) {
696
+ const categoryFound = keys.find((key) => categorySchema.safeParse(key).success);
697
+ if (!categoryFound) {
698
+ const unknownKey = keys.find((key) => ![
699
+ "uuid",
700
+ "uuidBelongsTo",
701
+ "belongsTo",
702
+ "publicationDateTime",
703
+ "metadata",
704
+ "languages"
705
+ ].includes(key));
706
+ throw new Error(`Invalid OCHRE data; found unexpected "${unknownKey}" key`);
707
+ }
708
+ return categorySchema.parse(categoryFound);
709
+ }
710
+
711
+ //#endregion
712
+ //#region src/utils/parse.ts
713
+ /**
714
+ * Parses raw identification data into the standardized Identification type
715
+ *
716
+ * @param identification - Raw identification data from OCHRE format
717
+ * @returns Parsed Identification object with label and abbreviation
718
+ */
719
+ function parseIdentification(identification) {
720
+ try {
721
+ const returnIdentification = {
722
+ label: [
723
+ "string",
724
+ "number",
725
+ "boolean"
726
+ ].includes(typeof identification.label) ? parseFakeString(identification.label) : parseStringContent(identification.label),
727
+ abbreviation: "",
728
+ code: identification.code ?? null
729
+ };
730
+ for (const key of Object.keys(identification).filter((key$1) => key$1 !== "label" && key$1 !== "code")) returnIdentification[key] = typeof identification[key] === "string" ? parseFakeString(identification[key]) : parseStringContent(identification[key]);
731
+ return returnIdentification;
732
+ } catch (error) {
733
+ console.error(error);
734
+ return {
735
+ label: "",
736
+ abbreviation: "",
737
+ code: null
738
+ };
739
+ }
740
+ }
741
+ /**
742
+ * Parses raw language data into an array of language codes
743
+ *
744
+ * @param language - Raw language data, either single or array
745
+ * @returns Array of language codes as strings
746
+ */
747
+ function parseLanguages(language) {
748
+ if (language == null) return ["eng"];
749
+ if (Array.isArray(language)) return language.map((lang) => parseStringContent(lang));
750
+ else return [parseStringContent(language)];
751
+ }
752
+ /**
753
+ * Parses raw metadata into the standardized Metadata type
754
+ *
755
+ * @param metadata - Raw metadata from OCHRE format
756
+ * @returns Parsed Metadata object
757
+ */
758
+ function parseMetadata(metadata) {
759
+ let identification = {
760
+ label: "",
761
+ abbreviation: "",
762
+ code: null
763
+ };
764
+ if (metadata.item) if (metadata.item.label || metadata.item.abbreviation) {
765
+ let label = "";
766
+ let abbreviation = "";
767
+ let code = null;
768
+ if (metadata.item.label) label = parseStringContent(metadata.item.label);
769
+ if (metadata.item.abbreviation) abbreviation = parseStringContent(metadata.item.abbreviation);
770
+ if (metadata.item.identification.code) code = metadata.item.identification.code;
771
+ identification = {
772
+ label,
773
+ abbreviation,
774
+ code
775
+ };
776
+ } else identification = parseIdentification(metadata.item.identification);
777
+ let projectIdentification = null;
778
+ const baseProjectIdentification = metadata.project?.identification ? parseIdentification(metadata.project.identification) : null;
779
+ if (baseProjectIdentification) projectIdentification = {
780
+ ...baseProjectIdentification,
781
+ website: metadata.project?.identification.website ?? null
782
+ };
783
+ return {
784
+ project: projectIdentification ? { identification: projectIdentification } : null,
785
+ item: metadata.item ? {
786
+ identification,
787
+ category: metadata.item.category,
788
+ type: metadata.item.type,
789
+ maxLength: metadata.item.maxLength ?? null
790
+ } : null,
791
+ dataset: parseStringContent(metadata.dataset),
792
+ publisher: parseStringContent(metadata.publisher),
793
+ languages: parseLanguages(metadata.language),
794
+ identifier: parseStringContent(metadata.identifier),
795
+ description: parseStringContent(metadata.description)
796
+ };
797
+ }
798
+ /**
799
+ * Parses raw context item data into the standardized ContextItem type
800
+ *
801
+ * @param contextItem - Raw context item data from OCHRE format
802
+ * @returns Parsed ContextItem object
803
+ */
804
+ function parseContextItem(contextItem) {
805
+ return {
806
+ uuid: contextItem.uuid,
807
+ publicationDateTime: contextItem.publicationDateTime != null ? new Date(contextItem.publicationDateTime) : null,
808
+ number: contextItem.n,
809
+ content: parseFakeString(contextItem.content)
810
+ };
811
+ }
812
+ /**
813
+ * Parses raw context data into the standardized Context type
814
+ *
815
+ * @param context - Raw context data from OCHRE format
816
+ * @returns Parsed Context object
817
+ */
818
+ function parseContext(context) {
819
+ return {
820
+ nodes: (Array.isArray(context.context) ? context.context : [context.context]).map((context$1) => {
821
+ const spatialUnit = [];
822
+ if ("spatialUnit" in context$1 && context$1.spatialUnit) {
823
+ const contextsToParse = Array.isArray(context$1.spatialUnit) ? context$1.spatialUnit : [context$1.spatialUnit];
824
+ for (const contextItem of contextsToParse) spatialUnit.push(parseContextItem(contextItem));
825
+ }
826
+ return {
827
+ tree: parseContextItem(context$1.tree),
828
+ project: parseContextItem(context$1.project),
829
+ spatialUnit
830
+ };
831
+ }),
832
+ displayPath: context.displayPath
833
+ };
834
+ }
835
+ /**
836
+ * Parses raw license data into the standardized License type
837
+ *
838
+ * @param license - Raw license data from OCHRE format
839
+ * @returns Parsed License object or null if invalid
840
+ */
841
+ function parseLicense(license) {
842
+ if (typeof license.license === "string") return null;
843
+ return {
844
+ content: license.license.content,
845
+ url: license.license.target
846
+ };
847
+ }
848
+ /**
849
+ * Parses raw person data into the standardized Person type
850
+ *
851
+ * @param person - Raw person data from OCHRE format
852
+ * @returns Parsed Person object
853
+ */
854
+ function parsePerson(person) {
855
+ return {
856
+ uuid: person.uuid,
857
+ category: "person",
858
+ publicationDateTime: person.publicationDateTime != null ? new Date(person.publicationDateTime) : null,
859
+ type: person.type ?? null,
860
+ number: person.n ?? null,
861
+ context: person.context ? parseContext(person.context) : null,
862
+ date: person.date ?? null,
863
+ identification: person.identification ? parseIdentification(person.identification) : null,
864
+ availability: person.availability ? parseLicense(person.availability) : null,
865
+ image: person.image ? parseImage(person.image) : null,
866
+ address: person.address ? {
867
+ country: person.address.country ?? null,
868
+ city: person.address.city ?? null,
869
+ state: person.address.state ?? null
870
+ } : null,
871
+ description: person.description ? typeof person.description === "string" || typeof person.description === "number" || typeof person.description === "boolean" ? parseFakeString(person.description) : parseStringContent(person.description) : null,
872
+ coordinates: parseCoordinates(person.coordinates),
873
+ content: person.content != null ? parseFakeString(person.content) : null,
874
+ notes: person.notes ? parseNotes(Array.isArray(person.notes.note) ? person.notes.note : [person.notes.note]) : [],
875
+ events: person.events ? parseEvents(Array.isArray(person.events.event) ? person.events.event : [person.events.event]) : [],
876
+ properties: person.properties ? parseProperties(Array.isArray(person.properties.property) ? person.properties.property : [person.properties.property]) : [],
877
+ bibliographies: person.bibliographies ? parseBibliographies(Array.isArray(person.bibliographies.bibliography) ? person.bibliographies.bibliography : [person.bibliographies.bibliography]) : []
878
+ };
879
+ }
880
+ /**
881
+ * Parses raw person data into the standardized Person type
882
+ *
883
+ * @param persons - Array of raw person data from OCHRE format
884
+ * @returns Array of parsed Person objects
885
+ */
886
+ function parsePersons(persons) {
887
+ const returnPersons = [];
888
+ for (const person of persons) returnPersons.push(parsePerson(person));
889
+ return returnPersons;
890
+ }
891
+ /**
892
+ * Parses an array of raw links into standardized Link objects
893
+ *
894
+ * @param linkRaw - Raw OCHRE link
895
+ * @returns Parsed Link object
896
+ */
897
+ function parseLink(linkRaw) {
898
+ const links = "resource" in linkRaw ? linkRaw.resource : "spatialUnit" in linkRaw ? linkRaw.spatialUnit : "concept" in linkRaw ? linkRaw.concept : "set" in linkRaw ? linkRaw.set : "tree" in linkRaw ? linkRaw.tree : "person" in linkRaw ? linkRaw.person : "bibliography" in linkRaw ? linkRaw.bibliography : "propertyValue" in linkRaw ? linkRaw.propertyValue : null;
899
+ if (!links) throw new Error(`Invalid link provided: ${JSON.stringify(linkRaw, null, 2)}`);
900
+ const linksToParse = Array.isArray(links) ? links : [links];
901
+ const returnLinks = [];
902
+ for (const link of linksToParse) {
903
+ const returnLink = {
904
+ category: "resource" in linkRaw ? "resource" : "spatialUnit" in linkRaw ? "spatialUnit" : "concept" in linkRaw ? "concept" : "set" in linkRaw ? "set" : "person" in linkRaw ? "person" : "tree" in linkRaw ? "tree" : "bibliography" in linkRaw ? "bibliography" : "propertyValue" in linkRaw ? "propertyValue" : null,
905
+ content: "content" in link ? link.content != null ? parseFakeString(link.content) : null : null,
906
+ href: "href" in link && link.href != null ? link.href : null,
907
+ fileFormat: "fileFormat" in link && link.fileFormat != null ? link.fileFormat : null,
908
+ fileSize: "fileSize" in link && link.fileSize != null ? link.fileSize : null,
909
+ uuid: link.uuid ?? null,
910
+ type: link.type ?? null,
911
+ identification: link.identification ? parseIdentification(link.identification) : null,
912
+ description: "description" in link && link.description != null ? parseStringContent(link.description) : null,
913
+ image: null,
914
+ bibliographies: "bibliography" in linkRaw ? parseBibliographies(Array.isArray(linkRaw.bibliography) ? linkRaw.bibliography : [linkRaw.bibliography]) : null,
915
+ publicationDateTime: link.publicationDateTime != null ? new Date(link.publicationDateTime) : null
916
+ };
917
+ if ("height" in link && link.height != null && link.width != null && link.heightPreview != null && link.widthPreview != null) returnLink.image = {
918
+ isInline: link.rend === "inline",
919
+ isPrimary: link.isPrimary ?? false,
920
+ heightPreview: link.heightPreview,
921
+ widthPreview: link.widthPreview,
922
+ height: link.height,
923
+ width: link.width
924
+ };
925
+ returnLinks.push(returnLink);
926
+ }
927
+ return returnLinks;
928
+ }
929
+ /**
930
+ * Parses an array of raw links into standardized Link objects
931
+ *
932
+ * @param links - Array of raw OCHRE links
933
+ * @returns Array of parsed Link objects
934
+ */
935
+ function parseLinks(links) {
936
+ const returnLinks = [];
937
+ for (const link of links) returnLinks.push(...parseLink(link));
938
+ return returnLinks;
939
+ }
940
+ /**
941
+ * Parses raw document content into a standardized Document structure
942
+ *
943
+ * @param document - Raw document content in OCHRE format
944
+ * @param language - Language code to use for content selection (defaults to "eng")
945
+ * @returns Parsed Document object with content and footnotes
946
+ */
947
+ function parseDocument(document, language = "eng") {
948
+ let returnString = "";
949
+ const documentWithLanguage = Array.isArray(document) ? document.find((doc) => doc.lang === language) : document;
950
+ if (typeof documentWithLanguage.string === "string" || typeof documentWithLanguage.string === "number" || typeof documentWithLanguage.string === "boolean") returnString += parseEmail(parseFakeString(documentWithLanguage.string));
951
+ else {
952
+ const documentItems = Array.isArray(documentWithLanguage.string) ? documentWithLanguage.string : [documentWithLanguage.string];
953
+ for (const item of documentItems) returnString += parseStringDocumentItem(item);
954
+ }
955
+ return returnString;
956
+ }
957
+ /**
958
+ * Parses raw image data into a standardized Image structure
959
+ *
960
+ * @param image - Raw image data in OCHRE format
961
+ * @returns Parsed Image object or null if invalid
962
+ */
963
+ function parseImage(image) {
964
+ return {
965
+ publicationDateTime: image.publicationDateTime != null ? new Date(image.publicationDateTime) : null,
966
+ identification: image.identification ? parseIdentification(image.identification) : null,
967
+ url: image.href ?? (image.htmlImgSrcPrefix == null && image.content != null ? parseFakeString(image.content) : null),
968
+ htmlPrefix: image.htmlImgSrcPrefix ?? null,
969
+ content: image.htmlImgSrcPrefix != null && image.content != null ? parseFakeString(image.content) : null,
970
+ widthPreview: image.widthPreview ?? null,
971
+ heightPreview: image.heightPreview ?? null,
972
+ width: image.width ?? null,
973
+ height: image.height ?? null
974
+ };
975
+ }
976
+ /**
977
+ * Parses raw notes into standardized Note objects
978
+ *
979
+ * @param notes - Array of raw notes in OCHRE format
980
+ * @param language - Language code for content selection (defaults to "eng")
981
+ * @returns Array of parsed Note objects
982
+ */
983
+ function parseNotes(notes, language = "eng") {
984
+ const returnNotes = [];
985
+ for (const note of notes) {
986
+ if (typeof note === "string") {
987
+ if (note === "") continue;
988
+ returnNotes.push({
989
+ number: -1,
990
+ title: null,
991
+ date: null,
992
+ authors: [],
993
+ content: note
994
+ });
995
+ continue;
996
+ }
997
+ let content = "";
998
+ const notesToParse = note.content != null ? Array.isArray(note.content) ? note.content : [note.content] : [];
999
+ if (notesToParse.length === 0) continue;
1000
+ let noteWithLanguage = notesToParse.find((item) => item.lang === language);
1001
+ if (!noteWithLanguage) {
1002
+ noteWithLanguage = notesToParse[0];
1003
+ if (!noteWithLanguage) throw new Error(`Note does not have a valid content item: ${JSON.stringify(note, null, 2)}`);
1004
+ }
1005
+ if (typeof noteWithLanguage.string === "string" || typeof noteWithLanguage.string === "number" || typeof noteWithLanguage.string === "boolean") content = parseEmail(parseFakeString(noteWithLanguage.string));
1006
+ else content = parseEmail(parseDocument(noteWithLanguage));
1007
+ returnNotes.push({
1008
+ number: note.noteNo,
1009
+ title: noteWithLanguage.title != null ? parseFakeString(noteWithLanguage.title) : null,
1010
+ date: note.date ?? null,
1011
+ authors: note.authors ? parsePersons(Array.isArray(note.authors.author) ? note.authors.author : [note.authors.author]) : [],
1012
+ content
1013
+ });
1014
+ }
1015
+ return returnNotes;
1016
+ }
1017
+ /**
1018
+ * Parses raw coordinates data into a standardized Coordinates structure
1019
+ *
1020
+ * @param coordinates - Raw coordinates data in OCHRE format
1021
+ * @returns Parsed Coordinates object
1022
+ */
1023
+ function parseCoordinates(coordinates) {
1024
+ if (coordinates == null) return [];
1025
+ const returnCoordinates = [];
1026
+ const coordsToParse = Array.isArray(coordinates.coord) ? coordinates.coord : [coordinates.coord];
1027
+ for (const coord of coordsToParse) {
1028
+ const source = "source" in coord && coord.source ? coord.source.context === "self" ? {
1029
+ context: "self",
1030
+ uuid: coord.source.label.uuid,
1031
+ label: parseStringContent(coord.source.label)
1032
+ } : coord.source.context === "related" ? {
1033
+ context: "related",
1034
+ uuid: coord.source.label.uuid,
1035
+ label: parseStringContent(coord.source.label),
1036
+ value: parseStringContent(coord.source.value)
1037
+ } : {
1038
+ context: "inherited",
1039
+ uuid: coord.source.label.uuid,
1040
+ item: {
1041
+ uuid: coord.source.item.label.uuid,
1042
+ label: parseStringContent(coord.source.item.label)
1043
+ },
1044
+ label: parseStringContent(coord.source.label)
1045
+ } : null;
1046
+ switch (coord.type) {
1047
+ case "point":
1048
+ returnCoordinates.push({
1049
+ type: coord.type,
1050
+ latitude: coord.latitude,
1051
+ longitude: coord.longitude,
1052
+ altitude: coord.altitude ?? null,
1053
+ source
1054
+ });
1055
+ break;
1056
+ case "plane":
1057
+ returnCoordinates.push({
1058
+ type: coord.type,
1059
+ minimum: {
1060
+ latitude: coord.minimum.latitude,
1061
+ longitude: coord.minimum.longitude
1062
+ },
1063
+ maximum: {
1064
+ latitude: coord.maximum.latitude,
1065
+ longitude: coord.maximum.longitude
1066
+ },
1067
+ source
1068
+ });
1069
+ break;
1070
+ }
1071
+ }
1072
+ return returnCoordinates;
1073
+ }
1074
+ /**
1075
+ * Parses a raw observation into a standardized Observation structure
1076
+ *
1077
+ * @param observation - Raw observation data in OCHRE format
1078
+ * @returns Parsed Observation object
1079
+ */
1080
+ function parseObservation(observation) {
1081
+ return {
1082
+ number: observation.observationNo,
1083
+ date: observation.date ?? null,
1084
+ observers: observation.observers != null ? typeof observation.observers === "string" || typeof observation.observers === "number" || typeof observation.observers === "boolean" ? parseFakeString(observation.observers).split(";").map((observer) => observer.trim()) : parsePersons(Array.isArray(observation.observers) ? observation.observers : [observation.observers]) : [],
1085
+ notes: observation.notes ? parseNotes(Array.isArray(observation.notes.note) ? observation.notes.note : [observation.notes.note]) : [],
1086
+ links: observation.links ? parseLinks(Array.isArray(observation.links) ? observation.links : [observation.links]) : [],
1087
+ properties: observation.properties ? parseProperties(Array.isArray(observation.properties.property) ? observation.properties.property : [observation.properties.property]) : [],
1088
+ bibliographies: observation.bibliographies ? parseBibliographies(Array.isArray(observation.bibliographies.bibliography) ? observation.bibliographies.bibliography : [observation.bibliographies.bibliography]) : []
1089
+ };
1090
+ }
1091
+ /**
1092
+ * Parses an array of raw observations into standardized Observation objects
1093
+ *
1094
+ * @param observations - Array of raw observations in OCHRE format
1095
+ * @returns Array of parsed Observation objects
1096
+ */
1097
+ function parseObservations(observations) {
1098
+ const returnObservations = [];
1099
+ for (const observation of observations) returnObservations.push(parseObservation(observation));
1100
+ return returnObservations;
1101
+ }
1102
+ /**
1103
+ * Parses an array of raw events into standardized Event objects
1104
+ *
1105
+ * @param events - Array of raw events in OCHRE format
1106
+ * @returns Array of parsed Event objects
1107
+ */
1108
+ function parseEvents(events) {
1109
+ const returnEvents = [];
1110
+ for (const event of events) returnEvents.push({
1111
+ date: event.dateTime != null ? new Date(event.dateTime) : null,
1112
+ label: parseStringContent(event.label),
1113
+ location: event.location ? {
1114
+ uuid: event.location.uuid,
1115
+ content: parseStringContent(event.location)
1116
+ } : null,
1117
+ agent: event.agent ? {
1118
+ uuid: event.agent.uuid,
1119
+ content: parseStringContent(event.agent)
1120
+ } : null,
1121
+ comment: event.comment ? parseStringContent(event.comment) : null,
1122
+ value: event.value ? parseFakeString(event.value) : null
1123
+ });
1124
+ return returnEvents;
1125
+ }
1126
+ function parseProperty(property, language = "eng") {
1127
+ const values = ("value" in property && property.value ? Array.isArray(property.value) ? property.value : [property.value] : []).map((value) => {
1128
+ let content = null;
1129
+ let label = null;
1130
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1131
+ content = parseFakeString(value);
1132
+ return {
1133
+ content,
1134
+ label: null,
1135
+ dataType: "string",
1136
+ isUncertain: false,
1137
+ category: "value",
1138
+ type: null,
1139
+ uuid: null,
1140
+ publicationDateTime: null,
1141
+ unit: null,
1142
+ href: null,
1143
+ slug: null
1144
+ };
1145
+ } else {
1146
+ let parsedType = "string";
1147
+ if (value.dataType != null) {
1148
+ const { data, error } = propertyValueContentTypeSchema.safeParse(value.dataType);
1149
+ if (error) throw new Error(`Invalid property value content type: "${value.dataType}"`);
1150
+ parsedType = data;
1151
+ }
1152
+ switch (parsedType) {
1153
+ case "integer":
1154
+ case "decimal":
1155
+ case "time":
1156
+ if (value.rawValue != null) {
1157
+ content = Number(value.rawValue);
1158
+ label = value.content ? parseStringContent({ content: value.content }) : null;
1159
+ } else {
1160
+ content = Number(value.content);
1161
+ label = null;
1162
+ }
1163
+ break;
1164
+ case "boolean":
1165
+ if (value.rawValue != null) {
1166
+ content = value.rawValue === "true";
1167
+ label = value.content ? parseStringContent({ content: value.content }) : null;
1168
+ } else {
1169
+ content = value.content === true;
1170
+ label = null;
1171
+ }
1172
+ break;
1173
+ default:
1174
+ if ("slug" in value && value.slug != null) content = parseFakeString(value.slug);
1175
+ else if (value.content != null) if (value.rawValue != null) {
1176
+ content = parseFakeString(value.rawValue);
1177
+ label = value.content ? parseStringContent({ content: value.content }) : null;
1178
+ } else {
1179
+ content = parseStringContent({ content: value.content });
1180
+ label = null;
1181
+ }
1182
+ break;
1183
+ }
1184
+ return {
1185
+ content,
1186
+ dataType: parsedType,
1187
+ isUncertain: value.isUncertain ?? false,
1188
+ label,
1189
+ category: value.category ?? null,
1190
+ type: value.type ?? null,
1191
+ uuid: value.uuid ?? null,
1192
+ publicationDateTime: value.publicationDateTime != null ? new Date(value.publicationDateTime) : null,
1193
+ unit: value.unit ?? null,
1194
+ href: value.href ?? null,
1195
+ slug: value.slug ?? null
1196
+ };
1197
+ }
1198
+ });
1199
+ return {
1200
+ uuid: property.label.uuid,
1201
+ label: parseStringContent(property.label, language).replace(/\s*\.{3}$/, "").trim(),
1202
+ values,
1203
+ comment: property.comment != null ? parseStringContent(property.comment) : null,
1204
+ properties: property.property ? parseProperties(Array.isArray(property.property) ? property.property : [property.property]) : []
1205
+ };
1206
+ }
1207
+ /**
1208
+ * Parses raw properties into standardized Property objects
1209
+ *
1210
+ * @param properties - Array of raw properties in OCHRE format
1211
+ * @param language - Language code for content selection (defaults to "eng")
1212
+ * @returns Array of parsed Property objects
1213
+ */
1214
+ function parseProperties(properties, language = "eng") {
1215
+ const returnProperties = [];
1216
+ for (const property of properties) returnProperties.push(parseProperty(property, language));
1217
+ return returnProperties;
1218
+ }
1219
+ /**
1220
+ * Parses raw interpretations into standardized Interpretation objects
1221
+ *
1222
+ * @param interpretations - Array of raw interpretations in OCHRE format
1223
+ * @returns Array of parsed Interpretation objects
1224
+ */
1225
+ function parseInterpretations(interpretations) {
1226
+ const returnInterpretations = [];
1227
+ for (const interpretation of interpretations) returnInterpretations.push({
1228
+ date: interpretation.date,
1229
+ number: interpretation.interpretationNo,
1230
+ properties: interpretation.properties ? parseProperties(Array.isArray(interpretation.properties.property) ? interpretation.properties.property : [interpretation.properties.property]) : [],
1231
+ bibliographies: interpretation.bibliographies ? parseBibliographies(Array.isArray(interpretation.bibliographies.bibliography) ? interpretation.bibliographies.bibliography : [interpretation.bibliographies.bibliography]) : []
1232
+ });
1233
+ return returnInterpretations;
1234
+ }
1235
+ /**
1236
+ * Parses raw image map data into a standardized ImageMap structure
1237
+ *
1238
+ * @param imageMap - Raw image map data in OCHRE format
1239
+ * @returns Parsed ImageMap object
1240
+ */
1241
+ function parseImageMap(imageMap) {
1242
+ const returnImageMap = {
1243
+ area: [],
1244
+ width: imageMap.width,
1245
+ height: imageMap.height
1246
+ };
1247
+ const imageMapAreasToParse = Array.isArray(imageMap.area) ? imageMap.area : [imageMap.area];
1248
+ for (const area of imageMapAreasToParse) returnImageMap.area.push({
1249
+ uuid: area.uuid,
1250
+ publicationDateTime: area.publicationDateTime != null ? new Date(area.publicationDateTime) : null,
1251
+ type: area.type,
1252
+ title: parseFakeString(area.title),
1253
+ shape: area.shape === "rect" ? "rectangle" : area.shape === "circle" ? "circle" : "polygon",
1254
+ coords: area.coords.split(",").map((coord) => Number.parseInt(coord)),
1255
+ slug: area.slug ? parseFakeString(area.slug) : null
1256
+ });
1257
+ return returnImageMap;
1258
+ }
1259
+ /**
1260
+ * Parses raw period data into a standardized Period structure
1261
+ *
1262
+ * @param period - Raw period data in OCHRE format
1263
+ * @returns Parsed Period object
1264
+ */
1265
+ function parsePeriod(period) {
1266
+ return {
1267
+ uuid: period.uuid,
1268
+ category: "period",
1269
+ publicationDateTime: period.publicationDateTime != null ? new Date(period.publicationDateTime) : null,
1270
+ type: period.type ?? null,
1271
+ number: period.n ?? null,
1272
+ identification: parseIdentification(period.identification),
1273
+ description: period.description ? parseStringContent(period.description) : null
1274
+ };
1275
+ }
1276
+ /**
1277
+ * Parses an array of raw periods into standardized Period objects
1278
+ *
1279
+ * @param periods - Array of raw periods in OCHRE format
1280
+ * @returns Array of parsed Period objects
1281
+ */
1282
+ function parsePeriods(periods) {
1283
+ const returnPeriods = [];
1284
+ for (const period of periods) returnPeriods.push(parsePeriod(period));
1285
+ return returnPeriods;
1286
+ }
1287
+ /**
1288
+ * Parses raw bibliography data into a standardized Bibliography structure
1289
+ *
1290
+ * @param bibliography - Raw bibliography data in OCHRE format
1291
+ * @returns Parsed Bibliography object
1292
+ */
1293
+ function parseBibliography(bibliography) {
1294
+ let resource = null;
1295
+ if (bibliography.source?.resource) resource = {
1296
+ uuid: bibliography.source.resource.uuid,
1297
+ publicationDateTime: bibliography.source.resource.publicationDateTime ? new Date(bibliography.source.resource.publicationDateTime) : null,
1298
+ type: bibliography.source.resource.type,
1299
+ identification: parseIdentification(bibliography.source.resource.identification)
1300
+ };
1301
+ let shortCitation = null;
1302
+ let longCitation = null;
1303
+ if (bibliography.citationFormatSpan) try {
1304
+ shortCitation = JSON.parse(`"${bibliography.citationFormatSpan}"`).replaceAll("&lt;", "<").replaceAll("&gt;", ">");
1305
+ } catch {
1306
+ shortCitation = bibliography.citationFormatSpan;
1307
+ }
1308
+ if (bibliography.referenceFormatDiv) try {
1309
+ longCitation = JSON.parse(`"${bibliography.referenceFormatDiv}"`).replaceAll("&lt;", "<").replaceAll("&gt;", ">");
1310
+ } catch {
1311
+ longCitation = bibliography.referenceFormatDiv;
1312
+ }
1313
+ return {
1314
+ uuid: bibliography.uuid ?? null,
1315
+ zoteroId: bibliography.zoteroId ?? null,
1316
+ category: "bibliography",
1317
+ publicationDateTime: bibliography.publicationDateTime != null ? new Date(bibliography.publicationDateTime) : null,
1318
+ type: bibliography.type ?? null,
1319
+ number: bibliography.n ?? null,
1320
+ identification: bibliography.identification ? parseIdentification(bibliography.identification) : null,
1321
+ projectIdentification: bibliography.project?.identification ? parseIdentification(bibliography.project.identification) : null,
1322
+ context: bibliography.context ? parseContext(bibliography.context) : null,
1323
+ citation: {
1324
+ details: bibliography.citationDetails ?? null,
1325
+ format: bibliography.citationFormat ?? null,
1326
+ short: shortCitation,
1327
+ long: longCitation
1328
+ },
1329
+ publicationInfo: {
1330
+ publishers: bibliography.publicationInfo?.publishers ? parsePersons(Array.isArray(bibliography.publicationInfo.publishers.publishers.person) ? bibliography.publicationInfo.publishers.publishers.person : [bibliography.publicationInfo.publishers.publishers.person]) : [],
1331
+ startDate: bibliography.publicationInfo?.startDate ? new Date(bibliography.publicationInfo.startDate.year, bibliography.publicationInfo.startDate.month, bibliography.publicationInfo.startDate.day) : null
1332
+ },
1333
+ entryInfo: bibliography.entryInfo ? {
1334
+ startIssue: parseFakeString(bibliography.entryInfo.startIssue),
1335
+ startVolume: parseFakeString(bibliography.entryInfo.startVolume)
1336
+ } : null,
1337
+ source: {
1338
+ resource,
1339
+ documentUrl: bibliography.sourceDocument ? `https://ochre.lib.uchicago.edu/ochre?uuid=${bibliography.sourceDocument.uuid}&load` : null
1340
+ },
1341
+ periods: bibliography.periods ? parsePeriods(Array.isArray(bibliography.periods.period) ? bibliography.periods.period : [bibliography.periods.period]) : [],
1342
+ authors: bibliography.authors ? parsePersons(Array.isArray(bibliography.authors.person) ? bibliography.authors.person : [bibliography.authors.person]) : [],
1343
+ properties: bibliography.properties ? parseProperties(Array.isArray(bibliography.properties.property) ? bibliography.properties.property : [bibliography.properties.property]) : []
1344
+ };
1345
+ }
1346
+ /**
1347
+ * Parses an array of raw bibliographies into standardized Bibliography objects
1348
+ *
1349
+ * @param bibliographies - Array of raw bibliographies in OCHRE format
1350
+ * @returns Array of parsed Bibliography objects
1351
+ */
1352
+ function parseBibliographies(bibliographies) {
1353
+ const returnBibliographies = [];
1354
+ for (const bibliography of bibliographies) returnBibliographies.push(parseBibliography(bibliography));
1355
+ return returnBibliographies;
1356
+ }
1357
+ /**
1358
+ * Parses raw property value data into a standardized PropertyValue structure
1359
+ *
1360
+ * @param propertyValue - Raw property value data in OCHRE format
1361
+ * @returns Parsed PropertyValue object
1362
+ */
1363
+ function parsePropertyValue(propertyValue) {
1364
+ return {
1365
+ uuid: propertyValue.uuid,
1366
+ category: "propertyValue",
1367
+ number: propertyValue.n,
1368
+ publicationDateTime: propertyValue.publicationDateTime ? new Date(propertyValue.publicationDateTime) : null,
1369
+ context: propertyValue.context ? parseContext(propertyValue.context) : null,
1370
+ availability: propertyValue.availability ? parseLicense(propertyValue.availability) : null,
1371
+ identification: parseIdentification(propertyValue.identification),
1372
+ date: propertyValue.date ?? null,
1373
+ creators: propertyValue.creators ? parsePersons(Array.isArray(propertyValue.creators.creator) ? propertyValue.creators.creator : [propertyValue.creators.creator]) : [],
1374
+ description: propertyValue.description ? typeof propertyValue.description === "string" || typeof propertyValue.description === "number" || typeof propertyValue.description === "boolean" ? parseFakeString(propertyValue.description) : parseStringContent(propertyValue.description) : "",
1375
+ notes: propertyValue.notes ? parseNotes(Array.isArray(propertyValue.notes.note) ? propertyValue.notes.note : [propertyValue.notes.note]) : [],
1376
+ links: propertyValue.links ? parseLinks(Array.isArray(propertyValue.links) ? propertyValue.links : [propertyValue.links]) : []
1377
+ };
1378
+ }
1379
+ /**
1380
+ * Parses an array of raw property values into standardized PropertyValue objects
1381
+ *
1382
+ * @param propertyValues - Array of raw property values in OCHRE format
1383
+ * @returns Array of parsed PropertyValue objects
1384
+ */
1385
+ function parsePropertyValues(propertyValues) {
1386
+ const returnPropertyValues = [];
1387
+ for (const propertyValue of propertyValues) returnPropertyValues.push(parsePropertyValue(propertyValue));
1388
+ return returnPropertyValues;
1389
+ }
1390
+ /**
1391
+ * Parses a raw tree structure into a standardized Tree object
1392
+ *
1393
+ * @param tree - Raw tree data in OCHRE format
1394
+ * @returns Parsed Tree object or null if invalid
1395
+ */
1396
+ function parseTree(tree, itemCategory, itemSubCategory) {
1397
+ if (typeof tree.items === "string") throw new TypeError("Invalid OCHRE data: Tree has no items");
1398
+ let creators = [];
1399
+ if (tree.creators) creators = parsePersons(Array.isArray(tree.creators.creator) ? tree.creators.creator : [tree.creators.creator]);
1400
+ let date = null;
1401
+ if (tree.date != null) date = tree.date;
1402
+ const parsedItemCategory = itemSubCategory ?? getItemCategory(Object.keys(tree.items));
1403
+ let items = [];
1404
+ switch (parsedItemCategory) {
1405
+ case "resource":
1406
+ if (!("resource" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no resources");
1407
+ items = parseResources(Array.isArray(tree.items.resource) ? tree.items.resource : [tree.items.resource]);
1408
+ break;
1409
+ case "spatialUnit":
1410
+ if (!("spatialUnit" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no spatial units");
1411
+ items = parseSpatialUnits(Array.isArray(tree.items.spatialUnit) ? tree.items.spatialUnit : [tree.items.spatialUnit]);
1412
+ break;
1413
+ case "concept":
1414
+ if (!("concept" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no concepts");
1415
+ items = parseConcepts(Array.isArray(tree.items.concept) ? tree.items.concept : [tree.items.concept]);
1416
+ break;
1417
+ case "period":
1418
+ if (!("period" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no periods");
1419
+ items = parsePeriods(Array.isArray(tree.items.period) ? tree.items.period : [tree.items.period]);
1420
+ break;
1421
+ case "bibliography":
1422
+ if (!("bibliography" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no bibliographies");
1423
+ items = parseBibliographies(Array.isArray(tree.items.bibliography) ? tree.items.bibliography : [tree.items.bibliography]);
1424
+ break;
1425
+ case "person":
1426
+ if (!("person" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no persons");
1427
+ items = parsePersons(Array.isArray(tree.items.person) ? tree.items.person : [tree.items.person]);
1428
+ break;
1429
+ case "propertyValue":
1430
+ if (!("propertyValue" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no property values");
1431
+ items = parsePropertyValues(Array.isArray(tree.items.propertyValue) ? tree.items.propertyValue : [tree.items.propertyValue]);
1432
+ break;
1433
+ case "set": {
1434
+ if (!("set" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no sets");
1435
+ const setItems = [];
1436
+ for (const item of Array.isArray(tree.items.set) ? tree.items.set : [tree.items.set]) setItems.push(parseSet(item, itemSubCategory));
1437
+ items = setItems;
1438
+ break;
1439
+ }
1440
+ default: throw new Error("Invalid OCHRE data: Tree has no items or is malformed");
1441
+ }
1442
+ return {
1443
+ uuid: tree.uuid,
1444
+ category: "tree",
1445
+ publicationDateTime: new Date(tree.publicationDateTime),
1446
+ identification: parseIdentification(tree.identification),
1447
+ creators,
1448
+ license: parseLicense(tree.availability),
1449
+ date,
1450
+ type: tree.type,
1451
+ number: tree.n,
1452
+ items,
1453
+ properties: tree.properties ? parseProperties(Array.isArray(tree.properties.property) ? tree.properties.property : [tree.properties.property]) : []
1454
+ };
1455
+ }
1456
+ /**
1457
+ * Parses raw set data into a standardized Set structure
1458
+ *
1459
+ * @param set - Raw set data in OCHRE format
1460
+ * @returns Parsed Set object
1461
+ */
1462
+ function parseSet(set, itemCategory) {
1463
+ if (typeof set.items === "string") throw new TypeError("Invalid OCHRE data: Set has no items");
1464
+ const parsedItemCategory = itemCategory ?? getItemCategory(Object.keys(set.items));
1465
+ let items = [];
1466
+ switch (parsedItemCategory) {
1467
+ case "resource":
1468
+ if (!("resource" in set.items)) throw new Error("Invalid OCHRE data: Set has no resources");
1469
+ items = parseResources(Array.isArray(set.items.resource) ? set.items.resource : [set.items.resource]);
1470
+ break;
1471
+ case "spatialUnit":
1472
+ if (!("spatialUnit" in set.items)) throw new Error("Invalid OCHRE data: Set has no spatial units");
1473
+ items = parseSpatialUnits(Array.isArray(set.items.spatialUnit) ? set.items.spatialUnit : [set.items.spatialUnit]);
1474
+ break;
1475
+ case "concept":
1476
+ if (!("concept" in set.items)) throw new Error("Invalid OCHRE data: Set has no concepts");
1477
+ items = parseConcepts(Array.isArray(set.items.concept) ? set.items.concept : [set.items.concept]);
1478
+ break;
1479
+ case "period":
1480
+ if (!("period" in set.items)) throw new Error("Invalid OCHRE data: Set has no periods");
1481
+ items = parsePeriods(Array.isArray(set.items.period) ? set.items.period : [set.items.period]);
1482
+ break;
1483
+ case "bibliography":
1484
+ if (!("bibliography" in set.items)) throw new Error("Invalid OCHRE data: Set has no bibliographies");
1485
+ items = parseBibliographies(Array.isArray(set.items.bibliography) ? set.items.bibliography : [set.items.bibliography]);
1486
+ break;
1487
+ case "person":
1488
+ if (!("person" in set.items)) throw new Error("Invalid OCHRE data: Set has no persons");
1489
+ items = parsePersons(Array.isArray(set.items.person) ? set.items.person : [set.items.person]);
1490
+ break;
1491
+ case "propertyValue":
1492
+ if (!("propertyValue" in set.items)) throw new Error("Invalid OCHRE data: Set has no property values");
1493
+ items = parsePropertyValues(Array.isArray(set.items.propertyValue) ? set.items.propertyValue : [set.items.propertyValue]);
1494
+ break;
1495
+ default: throw new Error("Invalid OCHRE data: Set has no items or is malformed");
1496
+ }
1497
+ return {
1498
+ uuid: set.uuid,
1499
+ category: "set",
1500
+ itemCategory,
1501
+ publicationDateTime: set.publicationDateTime ? new Date(set.publicationDateTime) : null,
1502
+ date: set.date ?? null,
1503
+ license: parseLicense(set.availability),
1504
+ identification: parseIdentification(set.identification),
1505
+ isSuppressingBlanks: set.suppressBlanks ?? false,
1506
+ description: set.description ? [
1507
+ "string",
1508
+ "number",
1509
+ "boolean"
1510
+ ].includes(typeof set.description) ? parseFakeString(set.description) : parseStringContent(set.description) : "",
1511
+ creators: set.creators ? parsePersons(Array.isArray(set.creators.creator) ? set.creators.creator : [set.creators.creator]) : [],
1512
+ type: set.type,
1513
+ number: set.n,
1514
+ items
1515
+ };
1516
+ }
1517
+ /**
1518
+ * Parses raw resource data into a standardized Resource structure
1519
+ *
1520
+ * @param resource - Raw resource data in OCHRE format
1521
+ * @returns Parsed Resource object
1522
+ */
1523
+ function parseResource(resource) {
1524
+ return {
1525
+ uuid: resource.uuid,
1526
+ category: "resource",
1527
+ publicationDateTime: resource.publicationDateTime ? new Date(resource.publicationDateTime) : null,
1528
+ type: resource.type,
1529
+ number: resource.n,
1530
+ fileFormat: resource.fileFormat ?? null,
1531
+ fileSize: resource.fileSize ?? null,
1532
+ context: "context" in resource && resource.context ? parseContext(resource.context) : null,
1533
+ license: "availability" in resource && resource.availability ? parseLicense(resource.availability) : null,
1534
+ copyright: "copyright" in resource && resource.copyright != null ? parseStringContent(resource.copyright) : null,
1535
+ watermark: "watermark" in resource && resource.watermark != null ? parseStringContent(resource.watermark) : null,
1536
+ identification: parseIdentification(resource.identification),
1537
+ date: resource.date ?? null,
1538
+ image: resource.image ? parseImage(resource.image) : null,
1539
+ creators: resource.creators ? parsePersons(Array.isArray(resource.creators.creator) ? resource.creators.creator : [resource.creators.creator]) : [],
1540
+ notes: resource.notes ? parseNotes(Array.isArray(resource.notes.note) ? resource.notes.note : [resource.notes.note]) : [],
1541
+ description: resource.description ? [
1542
+ "string",
1543
+ "number",
1544
+ "boolean"
1545
+ ].includes(typeof resource.description) ? parseFakeString(resource.description) : parseStringContent(resource.description) : "",
1546
+ coordinates: resource.coordinates ? parseCoordinates(resource.coordinates) : [],
1547
+ document: resource.document && "content" in resource.document ? parseDocument(resource.document.content) : null,
1548
+ href: resource.href ?? null,
1549
+ imageMap: resource.imagemap ? parseImageMap(resource.imagemap) : null,
1550
+ periods: resource.periods ? parsePeriods(Array.isArray(resource.periods.period) ? resource.periods.period : [resource.periods.period]) : [],
1551
+ links: resource.links ? parseLinks(Array.isArray(resource.links) ? resource.links : [resource.links]) : [],
1552
+ reverseLinks: resource.reverseLinks ? parseLinks(Array.isArray(resource.reverseLinks) ? resource.reverseLinks : [resource.reverseLinks]) : [],
1553
+ properties: resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : [],
1554
+ bibliographies: resource.bibliographies ? parseBibliographies(Array.isArray(resource.bibliographies.bibliography) ? resource.bibliographies.bibliography : [resource.bibliographies.bibliography]) : [],
1555
+ resources: resource.resource ? parseResources(Array.isArray(resource.resource) ? resource.resource : [resource.resource]) : []
1556
+ };
1557
+ }
1558
+ /**
1559
+ * Parses raw resource data into a standardized Resource structure
1560
+ *
1561
+ * @param resources - Raw resource data in OCHRE format
1562
+ * @returns Parsed Resource object
1563
+ */
1564
+ function parseResources(resources) {
1565
+ const returnResources = [];
1566
+ const resourcesToParse = Array.isArray(resources) ? resources : [resources];
1567
+ for (const resource of resourcesToParse) returnResources.push(parseResource(resource));
1568
+ return returnResources;
1569
+ }
1570
+ /**
1571
+ * Parses raw spatial units into standardized SpatialUnit objects
1572
+ *
1573
+ * @param spatialUnit - Raw spatial unit in OCHRE format
1574
+ * @returns Parsed SpatialUnit object
1575
+ */
1576
+ function parseSpatialUnit(spatialUnit) {
1577
+ return {
1578
+ uuid: spatialUnit.uuid,
1579
+ category: "spatialUnit",
1580
+ publicationDateTime: spatialUnit.publicationDateTime != null ? new Date(spatialUnit.publicationDateTime) : null,
1581
+ number: spatialUnit.n,
1582
+ context: "context" in spatialUnit && spatialUnit.context ? parseContext(spatialUnit.context) : null,
1583
+ license: "availability" in spatialUnit && spatialUnit.availability ? parseLicense(spatialUnit.availability) : null,
1584
+ identification: parseIdentification(spatialUnit.identification),
1585
+ image: spatialUnit.image ? parseImage(spatialUnit.image) : null,
1586
+ description: spatialUnit.description ? [
1587
+ "string",
1588
+ "number",
1589
+ "boolean"
1590
+ ].includes(typeof spatialUnit.description) ? parseFakeString(spatialUnit.description) : parseStringContent(spatialUnit.description) : "",
1591
+ coordinates: parseCoordinates(spatialUnit.coordinates),
1592
+ mapData: spatialUnit.mapData ?? null,
1593
+ observations: "observations" in spatialUnit && spatialUnit.observations ? parseObservations(Array.isArray(spatialUnit.observations.observation) ? spatialUnit.observations.observation : [spatialUnit.observations.observation]) : spatialUnit.observation ? [parseObservation(spatialUnit.observation)] : [],
1594
+ events: "events" in spatialUnit && spatialUnit.events ? parseEvents(Array.isArray(spatialUnit.events.event) ? spatialUnit.events.event : [spatialUnit.events.event]) : [],
1595
+ properties: "properties" in spatialUnit && spatialUnit.properties ? parseProperties(Array.isArray(spatialUnit.properties.property) ? spatialUnit.properties.property : [spatialUnit.properties.property]) : [],
1596
+ bibliographies: spatialUnit.bibliographies ? parseBibliographies(Array.isArray(spatialUnit.bibliographies.bibliography) ? spatialUnit.bibliographies.bibliography : [spatialUnit.bibliographies.bibliography]) : []
1597
+ };
1598
+ }
1599
+ /**
1600
+ * Parses an array of raw spatial units into standardized SpatialUnit objects
1601
+ *
1602
+ * @param spatialUnits - Array of raw spatial units in OCHRE format
1603
+ * @returns Array of parsed SpatialUnit objects
1604
+ */
1605
+ function parseSpatialUnits(spatialUnits) {
1606
+ const returnSpatialUnits = [];
1607
+ const spatialUnitsToParse = Array.isArray(spatialUnits) ? spatialUnits : [spatialUnits];
1608
+ for (const spatialUnit of spatialUnitsToParse) returnSpatialUnits.push(parseSpatialUnit(spatialUnit));
1609
+ return returnSpatialUnits;
1610
+ }
1611
+ /**
1612
+ * Parses a raw concept into a standardized Concept object
1613
+ *
1614
+ * @param concept - Raw concept data in OCHRE format
1615
+ * @returns Parsed Concept object
1616
+ */
1617
+ function parseConcept(concept) {
1618
+ return {
1619
+ uuid: concept.uuid,
1620
+ category: "concept",
1621
+ publicationDateTime: concept.publicationDateTime ? new Date(concept.publicationDateTime) : null,
1622
+ number: concept.n,
1623
+ license: "availability" in concept && concept.availability ? parseLicense(concept.availability) : null,
1624
+ context: "context" in concept && concept.context ? parseContext(concept.context) : null,
1625
+ identification: parseIdentification(concept.identification),
1626
+ image: concept.image ? parseImage(concept.image) : null,
1627
+ description: concept.description ? parseStringContent(concept.description) : null,
1628
+ interpretations: concept.interpretations ? parseInterpretations(Array.isArray(concept.interpretations.interpretation) ? concept.interpretations.interpretation : [concept.interpretations.interpretation]) : [],
1629
+ properties: concept.properties ? parseProperties(Array.isArray(concept.properties.property) ? concept.properties.property : [concept.properties.property]) : [],
1630
+ bibliographies: concept.bibliographies ? parseBibliographies(Array.isArray(concept.bibliographies.bibliography) ? concept.bibliographies.bibliography : [concept.bibliographies.bibliography]) : []
1631
+ };
1632
+ }
1633
+ /**
1634
+ * Parses raw webpage resources into standardized WebElement or Webpage objects
1635
+ *
1636
+ * @param webpageResources - Array of raw webpage resources in OCHRE format
1637
+ * @param type - Type of resource to parse ("element" or "page")
1638
+ * @returns Array of parsed WebElement or Webpage objects
1639
+ */
1640
+ const parseWebpageResources = (webpageResources, type) => {
1641
+ const returnElements = [];
1642
+ for (const resource of webpageResources) {
1643
+ if (!(resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : []).find((property) => property.label === "presentation" && property.values[0].content === type)) continue;
1644
+ switch (type) {
1645
+ case "element": {
1646
+ const element = parseWebElement(resource);
1647
+ returnElements.push(element);
1648
+ break;
1649
+ }
1650
+ case "page": {
1651
+ const webpage = parseWebpage(resource);
1652
+ if (webpage) returnElements.push(webpage);
1653
+ break;
1654
+ }
1655
+ case "block": {
1656
+ const block = parseWebBlock(resource);
1657
+ if (block) returnElements.push(block);
1658
+ break;
1659
+ }
1660
+ }
1661
+ }
1662
+ return returnElements;
1663
+ };
1664
+ /**
1665
+ * Parses raw concept data into standardized Concept objects
1666
+ *
1667
+ * @param concepts - Array of raw concept data in OCHRE format
1668
+ * @returns Array of parsed Concept objects
1669
+ */
1670
+ function parseConcepts(concepts) {
1671
+ const returnConcepts = [];
1672
+ const conceptsToParse = Array.isArray(concepts) ? concepts : [concepts];
1673
+ for (const concept of conceptsToParse) returnConcepts.push(parseConcept(concept));
1674
+ return returnConcepts;
1675
+ }
1676
+ /**
1677
+ * Parses raw web element properties into a standardized WebElementComponent structure
1678
+ *
1679
+ * @param componentProperty - Raw component property data in OCHRE format
1680
+ * @param elementResource - Raw element resource data in OCHRE format
1681
+ * @returns Parsed WebElementComponent object
1682
+ */
1683
+ function parseWebElementProperties(componentProperty, elementResource) {
1684
+ const unparsedComponentName = componentProperty.values[0].content;
1685
+ const { data: componentName } = componentSchema.safeParse(unparsedComponentName);
1686
+ const properties = { component: componentName };
1687
+ const links = elementResource.links ? parseLinks(Array.isArray(elementResource.links) ? elementResource.links : [elementResource.links]) : [];
1688
+ switch (componentName) {
1689
+ case "annotated-document": {
1690
+ const documentLink = links.find((link) => link.type === "internalDocument");
1691
+ if (!documentLink) throw new Error(`Document link not found for the following component: “${componentName}”`);
1692
+ properties.documentId = documentLink.uuid;
1693
+ break;
1694
+ }
1695
+ case "annotated-image": {
1696
+ const imageLinks = links.filter((link) => link.type === "image" || link.type === "IIIF");
1697
+ if (imageLinks.length === 0) throw new Error(`Image link not found for the following component: “${componentName}”`);
1698
+ const isFilterDisplayed = getPropertyValueByLabel(componentProperty.properties, "filter-displayed") === true;
1699
+ const isOptionsDisplayed = getPropertyValueByLabel(componentProperty.properties, "options-displayed") !== false;
1700
+ const isAnnotationHighlightsDisplayed = getPropertyValueByLabel(componentProperty.properties, "annotation-highlights-displayed") !== false;
1701
+ const isAnnotationTooltipsDisplayed = getPropertyValueByLabel(componentProperty.properties, "annotation-tooltips-displayed") !== false;
1702
+ properties.imageUuid = imageLinks[0].uuid;
1703
+ properties.isFilterDisplayed = isFilterDisplayed;
1704
+ properties.isOptionsDisplayed = isOptionsDisplayed;
1705
+ properties.isAnnotationHighlightsDisplayed = isAnnotationHighlightsDisplayed;
1706
+ properties.isAnnotationTooltipsDisplayed = isAnnotationTooltipsDisplayed;
1707
+ break;
1708
+ }
1709
+ case "audio-player": {
1710
+ const audioLink = links.find((link) => link.type === "audio");
1711
+ if (!audioLink) throw new Error(`Audio link not found for the following component: “${componentName}”`);
1712
+ let isSpeedControlsDisplayed = false;
1713
+ const isSpeedControlsDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "speed-controls-displayed");
1714
+ if (isSpeedControlsDisplayedProperty !== null) isSpeedControlsDisplayed = isSpeedControlsDisplayedProperty === true;
1715
+ let isVolumeControlsDisplayed = false;
1716
+ const isVolumeControlsDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "volume-controls-displayed");
1717
+ if (isVolumeControlsDisplayedProperty !== null) isVolumeControlsDisplayed = isVolumeControlsDisplayedProperty === true;
1718
+ let isSeekBarDisplayed = false;
1719
+ const isSeekBarDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "seek-bar-displayed");
1720
+ if (isSeekBarDisplayedProperty !== null) isSeekBarDisplayed = isSeekBarDisplayedProperty === true;
1721
+ properties.audioId = audioLink.uuid;
1722
+ properties.isSpeedControlsDisplayed = isSpeedControlsDisplayed;
1723
+ properties.isVolumeControlsDisplayed = isVolumeControlsDisplayed;
1724
+ properties.isSeekBarDisplayed = isSeekBarDisplayed;
1725
+ break;
1726
+ }
1727
+ case "bibliography": {
1728
+ const itemLinks = links.filter((link) => link.category !== "bibliography");
1729
+ const bibliographyLink = links.find((link) => link.category === "bibliography");
1730
+ if (itemLinks.length === 0 && bibliographyLink?.bibliographies == null) throw new Error(`No links found for the following component: “${componentName}”`);
1731
+ let layout = getPropertyValueByLabel(componentProperty.properties, "layout");
1732
+ layout ??= "long";
1733
+ let isSourceDocumentDisplayed = true;
1734
+ const isSourceDocumentDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "source-document-displayed");
1735
+ if (isSourceDocumentDisplayedProperty !== null) isSourceDocumentDisplayed = isSourceDocumentDisplayedProperty === true;
1736
+ properties.itemUuids = itemLinks.map((link) => link.uuid).filter((uuid) => uuid !== null);
1737
+ properties.bibliographies = bibliographyLink?.bibliographies ?? [];
1738
+ properties.layout = layout;
1739
+ properties.isSourceDocumentDisplayed = isSourceDocumentDisplayed;
1740
+ break;
1741
+ }
1742
+ case "button": {
1743
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1744
+ variant ??= "default";
1745
+ let isExternal = false;
1746
+ const navigateToProperty = getPropertyByLabel(componentProperty.properties, "navigate-to");
1747
+ let href = navigateToProperty?.values[0]?.href ?? navigateToProperty?.values[0]?.slug ?? null;
1748
+ if (href === null) {
1749
+ const linkToProperty = getPropertyByLabel(componentProperty.properties, "link-to");
1750
+ href = linkToProperty?.values[0]?.href ?? linkToProperty?.values[0]?.slug ?? null;
1751
+ if (href === null) throw new Error(`Properties “navigate-to” or “link-to” not found for the following component: “${componentName}”`);
1752
+ else isExternal = true;
1753
+ }
1754
+ let startIcon = null;
1755
+ const startIconProperty = getPropertyValueByLabel(componentProperty.properties, "start-icon");
1756
+ if (startIconProperty !== null) startIcon = startIconProperty;
1757
+ let endIcon = null;
1758
+ const endIconProperty = getPropertyValueByLabel(componentProperty.properties, "end-icon");
1759
+ if (endIconProperty !== null) endIcon = endIconProperty;
1760
+ let image = null;
1761
+ const imageLink = links.find((link) => link.type === "image" || link.type === "IIIF");
1762
+ if (imageLink != null) image = {
1763
+ url: `https://ochre.lib.uchicago.edu/ochre?uuid=${imageLink.uuid}&load`,
1764
+ label: imageLink.identification?.label ?? null,
1765
+ width: imageLink.image?.width ?? 0,
1766
+ height: imageLink.image?.height ?? 0,
1767
+ description: imageLink.description ?? null
1768
+ };
1769
+ properties.variant = variant;
1770
+ properties.href = href;
1771
+ properties.isExternal = isExternal;
1772
+ properties.label = elementResource.document && "content" in elementResource.document ? parseDocument(elementResource.document.content) : null;
1773
+ properties.startIcon = startIcon;
1774
+ properties.endIcon = endIcon;
1775
+ properties.image = image;
1776
+ break;
1777
+ }
1778
+ case "collection": {
1779
+ const collectionLink = links.find((link) => link.category === "set");
1780
+ if (!collectionLink) throw new Error(`Collection link not found for the following component: “${componentName}”`);
1781
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1782
+ variant ??= "full";
1783
+ let itemVariant = getPropertyValueByLabel(componentProperty.properties, "item-variant");
1784
+ itemVariant ??= "detailed";
1785
+ let paginationVariant = getPropertyValueByLabel(componentProperty.properties, "pagination-variant");
1786
+ paginationVariant ??= "default";
1787
+ let isSortDisplayed = false;
1788
+ const isSortDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "sort-displayed");
1789
+ if (isSortDisplayedProperty !== null) isSortDisplayed = isSortDisplayedProperty === true;
1790
+ let isFilterDisplayed = false;
1791
+ const isFilterDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "filter-displayed");
1792
+ if (isFilterDisplayedProperty !== null) isFilterDisplayed = isFilterDisplayedProperty === true;
1793
+ let filterSort = getPropertyValueByLabel(componentProperty.properties, "filter-sort");
1794
+ filterSort ??= "default";
1795
+ let layout = getPropertyValueByLabel(componentProperty.properties, "layout");
1796
+ layout ??= "image-start";
1797
+ properties.collectionId = collectionLink.uuid;
1798
+ properties.variant = variant;
1799
+ properties.itemVariant = itemVariant;
1800
+ properties.paginationVariant = paginationVariant;
1801
+ properties.isSortDisplayed = isSortDisplayed;
1802
+ properties.isFilterDisplayed = isFilterDisplayed;
1803
+ properties.filterSort = filterSort;
1804
+ properties.layout = layout;
1805
+ break;
1806
+ }
1807
+ case "empty-space": {
1808
+ const height = getPropertyValueByLabel(componentProperty.properties, "height");
1809
+ const width = getPropertyValueByLabel(componentProperty.properties, "width");
1810
+ properties.height = height;
1811
+ properties.width = width;
1812
+ break;
1813
+ }
1814
+ case "entries": {
1815
+ const entriesLink = links.find((link) => link.category === "tree" || link.category === "set");
1816
+ if (!entriesLink) throw new Error(`Entries link not found for the following component: “${componentName}”`);
1817
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1818
+ variant ??= "entry";
1819
+ let isFilterDisplayed = false;
1820
+ const isFilterDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "filter-displayed");
1821
+ if (isFilterDisplayedProperty !== null) isFilterDisplayed = isFilterDisplayedProperty === true;
1822
+ properties.entriesId = entriesLink.uuid;
1823
+ properties.variant = variant;
1824
+ properties.isFilterDisplayed = isFilterDisplayed;
1825
+ break;
1826
+ }
1827
+ case "iframe": {
1828
+ const href = links.find((link) => link.type === "webpage")?.href;
1829
+ if (!href) throw new Error(`URL not found for the following component: “${componentName}”`);
1830
+ const height = getPropertyValueByLabel(componentProperty.properties, "height");
1831
+ const width = getPropertyValueByLabel(componentProperty.properties, "width");
1832
+ properties.href = href;
1833
+ properties.height = height;
1834
+ properties.width = width;
1835
+ break;
1836
+ }
1837
+ case "iiif-viewer": {
1838
+ const manifestLink = links.find((link) => link.type === "IIIF");
1839
+ if (!manifestLink) throw new Error(`Manifest link not found for the following component: “${componentName}”`);
1840
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1841
+ variant ??= "universal-viewer";
1842
+ properties.iiifId = manifestLink.uuid;
1843
+ properties.variant = variant;
1844
+ break;
1845
+ }
1846
+ case "image": {
1847
+ if (links.length === 0) throw new Error(`No links found for the following component: “${componentName}”`);
1848
+ let imageQuality = getPropertyValueByLabel(componentProperty.properties, "quality");
1849
+ imageQuality ??= "high";
1850
+ const images = [];
1851
+ for (const link of links) images.push({
1852
+ url: `https://ochre.lib.uchicago.edu/ochre?uuid=${link.uuid}${imageQuality === "high" && (link.type === "image" || link.type === "IIIF") ? "&load" : "&preview"}`,
1853
+ label: link.identification?.label ?? null,
1854
+ width: link.image?.width ?? 0,
1855
+ height: link.image?.height ?? 0,
1856
+ description: link.description ?? null
1857
+ });
1858
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1859
+ variant ??= "default";
1860
+ let captionLayout = getPropertyValueByLabel(componentProperty.properties, "layout-caption");
1861
+ captionLayout ??= "bottom";
1862
+ let width = null;
1863
+ const widthProperty = getPropertyValueByLabel(componentProperty.properties, "width");
1864
+ if (widthProperty !== null) {
1865
+ if (typeof widthProperty === "number") width = widthProperty;
1866
+ else if (typeof widthProperty === "string") width = Number.parseFloat(widthProperty);
1867
+ }
1868
+ let height = null;
1869
+ const heightProperty = getPropertyValueByLabel(componentProperty.properties, "height");
1870
+ if (heightProperty !== null) {
1871
+ if (typeof heightProperty === "number") height = heightProperty;
1872
+ else if (typeof heightProperty === "string") height = Number.parseFloat(heightProperty);
1873
+ }
1874
+ let isFullWidth = true;
1875
+ const isFullWidthProperty = getPropertyValueByLabel(componentProperty.properties, "is-full-width");
1876
+ if (isFullWidthProperty !== null) isFullWidth = isFullWidthProperty === true;
1877
+ let isFullHeight = true;
1878
+ const isFullHeightProperty = getPropertyValueByLabel(componentProperty.properties, "is-full-height");
1879
+ if (isFullHeightProperty !== null) isFullHeight = isFullHeightProperty === true;
1880
+ let captionSource = getPropertyValueByLabel(componentProperty.properties, "caption-source");
1881
+ captionSource ??= "name";
1882
+ let altTextSource = getPropertyValueByLabel(componentProperty.properties, "alt-text-source");
1883
+ altTextSource ??= "name";
1884
+ let isTransparentBackground = false;
1885
+ const isTransparentBackgroundProperty = getPropertyValueByLabel(componentProperty.properties, "is-transparent");
1886
+ if (isTransparentBackgroundProperty !== null) isTransparentBackground = isTransparentBackgroundProperty === true;
1887
+ let isCover = false;
1888
+ const isCoverProperty = getPropertyValueByLabel(componentProperty.properties, "is-cover");
1889
+ if (isCoverProperty !== null) isCover = isCoverProperty === true;
1890
+ const variantProperty = getPropertyByLabel(componentProperty.properties, "variant");
1891
+ let carouselOptions = null;
1892
+ if (images.length > 1) {
1893
+ let secondsPerImage = 5;
1894
+ if (variantProperty?.values[0].content === "carousel") {
1895
+ const secondsPerImageProperty = getPropertyValueByLabel(variantProperty.properties, "seconds-per-image");
1896
+ if (secondsPerImageProperty !== null) {
1897
+ if (typeof secondsPerImageProperty === "number") secondsPerImage = secondsPerImageProperty;
1898
+ else if (typeof secondsPerImageProperty === "string") secondsPerImage = Number.parseFloat(secondsPerImageProperty);
1899
+ }
1900
+ }
1901
+ carouselOptions = { secondsPerImage };
1902
+ }
1903
+ let heroOptions = null;
1904
+ if (variantProperty?.values[0].content === "hero") {
1905
+ const isBackgroundImageDisplayedProperty = getPropertyValueByLabel(variantProperty.properties, "background-image-displayed");
1906
+ const isDocumentDisplayedProperty = getPropertyValueByLabel(variantProperty.properties, "document-displayed");
1907
+ const isLinkDisplayedProperty = getPropertyValueByLabel(variantProperty.properties, "link-displayed");
1908
+ heroOptions = {
1909
+ isBackgroundImageDisplayed: isBackgroundImageDisplayedProperty !== false,
1910
+ isDocumentDisplayed: isDocumentDisplayedProperty !== false,
1911
+ isLinkDisplayed: isLinkDisplayedProperty !== false
1912
+ };
1913
+ }
1914
+ properties.images = images;
1915
+ properties.variant = variant;
1916
+ properties.width = width;
1917
+ properties.height = height;
1918
+ properties.isFullWidth = isFullWidth;
1919
+ properties.isFullHeight = isFullHeight;
1920
+ properties.imageQuality = imageQuality;
1921
+ properties.captionLayout = captionLayout;
1922
+ properties.captionSource = captionSource;
1923
+ properties.altTextSource = altTextSource;
1924
+ properties.isTransparentBackground = isTransparentBackground;
1925
+ properties.isCover = isCover;
1926
+ properties.carouselOptions = carouselOptions;
1927
+ properties.heroOptions = heroOptions;
1928
+ break;
1929
+ }
1930
+ case "image-gallery": {
1931
+ const galleryLink = links.find((link) => link.category === "tree" || link.category === "set");
1932
+ if (!galleryLink) throw new Error(`Image gallery link not found for the following component: “${componentName}”`);
1933
+ const isFilterDisplayed = getPropertyValueByLabel(componentProperty.properties, "filter-displayed") === true;
1934
+ properties.galleryId = galleryLink.uuid;
1935
+ properties.isFilterDisplayed = isFilterDisplayed;
1936
+ break;
1937
+ }
1938
+ case "map": {
1939
+ const mapLink = links.find((link) => link.category === "set" || link.category === "tree");
1940
+ if (!mapLink) throw new Error(`Map link not found for the following component: “${componentName}”`);
1941
+ let isInteractive = true;
1942
+ const isInteractiveProperty = getPropertyValueByLabel(componentProperty.properties, "is-interactive");
1943
+ if (isInteractiveProperty !== null) isInteractive = isInteractiveProperty === true;
1944
+ let isClustered = false;
1945
+ const isClusteredProperty = getPropertyValueByLabel(componentProperty.properties, "is-clustered");
1946
+ if (isClusteredProperty !== null) isClustered = isClusteredProperty === true;
1947
+ let isUsingPins = false;
1948
+ const isUsingPinsProperty = getPropertyValueByLabel(componentProperty.properties, "is-using-pins");
1949
+ if (isUsingPinsProperty !== null) isUsingPins = isUsingPinsProperty === true;
1950
+ let customBasemap = null;
1951
+ const customBasemapProperty = getPropertyValueByLabel(componentProperty.properties, "custom-basemap");
1952
+ if (customBasemapProperty !== null) customBasemap = customBasemapProperty;
1953
+ let isControlsDisplayed = false;
1954
+ const isControlsDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "controls-displayed");
1955
+ if (isControlsDisplayedProperty !== null) isControlsDisplayed = isControlsDisplayedProperty === true;
1956
+ let isFullHeight = false;
1957
+ const isFullHeightProperty = getPropertyValueByLabel(componentProperty.properties, "is-full-height");
1958
+ if (isFullHeightProperty !== null) isFullHeight = isFullHeightProperty === true;
1959
+ properties.mapId = mapLink.uuid;
1960
+ properties.isInteractive = isInteractive;
1961
+ properties.isClustered = isClustered;
1962
+ properties.isUsingPins = isUsingPins;
1963
+ properties.customBasemap = customBasemap;
1964
+ properties.isControlsDisplayed = isControlsDisplayed;
1965
+ properties.isFullHeight = isFullHeight;
1966
+ break;
1967
+ }
1968
+ case "network-graph": break;
1969
+ case "query": {
1970
+ const queries = [];
1971
+ const queryProperties = componentProperty.properties;
1972
+ if (queryProperties.length === 0) throw new Error(`Query properties not found for the following component: “${componentName}”`);
1973
+ for (const query of queryProperties) {
1974
+ const querySubProperties = query.properties;
1975
+ const label = getPropertyValueByLabel(querySubProperties, "query-prompt");
1976
+ if (label === null) throw new Error(`Query prompt not found for the following component: “${componentName}”`);
1977
+ const propertyUuids = querySubProperties.find((property) => property.label === "use-property")?.values.map((value) => value.uuid).filter((uuid) => uuid !== null) ?? [];
1978
+ const startIcon = getPropertyValueByLabel(querySubProperties, "start-icon");
1979
+ const endIcon = getPropertyValueByLabel(querySubProperties, "end-icon");
1980
+ queries.push({
1981
+ label: String(label),
1982
+ propertyUuids,
1983
+ startIcon: startIcon !== null ? String(startIcon) : null,
1984
+ endIcon: endIcon !== null ? String(endIcon) : null
1985
+ });
1986
+ }
1987
+ properties.queries = queries;
1988
+ break;
1989
+ }
1990
+ case "table": {
1991
+ const tableLink = links.find((link) => link.category === "set");
1992
+ if (!tableLink) throw new Error(`Table link not found for the following component: “${componentName}”`);
1993
+ properties.tableId = tableLink.uuid;
1994
+ break;
1995
+ }
1996
+ case "search-bar": {
1997
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1998
+ variant ??= "default";
1999
+ const placeholder = getPropertyValueByLabel(componentProperty.properties, "placeholder-text");
2000
+ const baseQuery = getPropertyValueByLabel(componentProperty.properties, "base-query");
2001
+ properties.variant = variant;
2002
+ properties.placeholder = placeholder !== null ? String(placeholder) : null;
2003
+ properties.baseQuery = baseQuery !== null ? String(baseQuery).replaceAll(String.raw`\{`, "{").replaceAll(String.raw`\}`, "}") : null;
2004
+ break;
2005
+ }
2006
+ case "text": {
2007
+ const content = elementResource.document && "content" in elementResource.document ? parseDocument(elementResource.document.content) : null;
2008
+ if (!content) throw new Error(`Content not found for the following component: “${componentName}”`);
2009
+ let variantName = "block";
2010
+ let variant;
2011
+ const variantProperty = getPropertyByLabel(componentProperty.properties, "variant");
2012
+ if (variantProperty !== null) {
2013
+ variantName = variantProperty.values[0].content;
2014
+ if (variantName === "paragraph" || variantName === "label" || variantName === "heading" || variantName === "display") {
2015
+ const size = getPropertyValueByLabel(variantProperty.properties, "size");
2016
+ variant = {
2017
+ name: variantName,
2018
+ size: size !== null ? size : "md"
2019
+ };
2020
+ } else variant = { name: variantName };
2021
+ } else variant = { name: variantName };
2022
+ properties.variant = variant;
2023
+ properties.content = content;
2024
+ break;
2025
+ }
2026
+ case "timeline": {
2027
+ const timelineLink = links.find((link) => link.category === "tree");
2028
+ if (!timelineLink) throw new Error(`Timeline link not found for the following component: “${componentName}”`);
2029
+ properties.timelineId = timelineLink.uuid;
2030
+ break;
2031
+ }
2032
+ case "video": {
2033
+ const videoLink = links.find((link) => link.type === "video");
2034
+ if (!videoLink) throw new Error(`Video link not found for the following component: “${componentName}”`);
2035
+ let isChaptersDislayed = getPropertyValueByLabel(componentProperty.properties, "chapters-displayed");
2036
+ isChaptersDislayed ??= true;
2037
+ properties.videoId = videoLink.uuid;
2038
+ properties.isChaptersDislayed = isChaptersDislayed === true;
2039
+ break;
2040
+ }
2041
+ default:
2042
+ console.warn(`Invalid or non-implemented component name “${String(unparsedComponentName)}” for the following element: “${parseStringContent(elementResource.identification.label)}”`);
2043
+ break;
2044
+ }
2045
+ return properties;
2046
+ }
2047
+ function parseWebTitle(properties, identification, overrides = {}) {
2048
+ const titleProperties = properties.find((property) => property.label === "presentation" && property.values[0].content === "title")?.properties;
2049
+ let variant = "default";
2050
+ let isNameDisplayed = overrides.isNameDisplayed ?? false;
2051
+ let isDescriptionDisplayed = false;
2052
+ let isDateDisplayed = false;
2053
+ let isCreatorsDisplayed = false;
2054
+ let isCountDisplayed = overrides.isCountDisplayed ?? false;
2055
+ if (titleProperties) {
2056
+ const titleVariant = getPropertyValueByLabel(titleProperties, "variant");
2057
+ if (titleVariant) variant = titleVariant;
2058
+ isNameDisplayed = getPropertyValueByLabel(titleProperties, "name-displayed") === true;
2059
+ isDescriptionDisplayed = getPropertyValueByLabel(titleProperties, "description-displayed") === true;
2060
+ isDateDisplayed = getPropertyValueByLabel(titleProperties, "date-displayed") === true;
2061
+ isCreatorsDisplayed = getPropertyValueByLabel(titleProperties, "creators-displayed") === true;
2062
+ isCountDisplayed = getPropertyValueByLabel(titleProperties, "count-displayed") === true;
2063
+ }
2064
+ return {
2065
+ label: identification.label,
2066
+ variant,
2067
+ properties: {
2068
+ isNameDisplayed,
2069
+ isDescriptionDisplayed,
2070
+ isDateDisplayed,
2071
+ isCreatorsDisplayed,
2072
+ isCountDisplayed
2073
+ }
2074
+ };
2075
+ }
2076
+ /**
2077
+ * Parses raw web element data into a standardized WebElement structure
2078
+ *
2079
+ * @param elementResource - Raw element resource data in OCHRE format
2080
+ * @returns Parsed WebElement object
2081
+ */
2082
+ function parseWebElement(elementResource) {
2083
+ const identification = parseIdentification(elementResource.identification);
2084
+ const presentationProperty = (elementResource.properties?.property ? parseProperties(Array.isArray(elementResource.properties.property) ? elementResource.properties.property : [elementResource.properties.property]) : []).find((property) => property.label === "presentation");
2085
+ if (!presentationProperty) throw new Error(`Presentation property not found for element “${identification.label}”`);
2086
+ const componentProperty = presentationProperty.properties.find((property) => property.label === "component");
2087
+ if (!componentProperty) throw new Error(`Component for element “${identification.label}” not found`);
2088
+ const properties = parseWebElementProperties(componentProperty, elementResource);
2089
+ const elementResourceProperties = elementResource.properties?.property ? parseProperties(Array.isArray(elementResource.properties.property) ? elementResource.properties.property : [elementResource.properties.property]) : [];
2090
+ const cssProperties = elementResourceProperties.find((property) => property.label === "presentation" && property.values[0].content === "css")?.properties ?? [];
2091
+ const cssStyles = [];
2092
+ for (const property of cssProperties) {
2093
+ const cssStyle = property.values[0].content;
2094
+ cssStyles.push({
2095
+ label: property.label,
2096
+ value: cssStyle
2097
+ });
2098
+ }
2099
+ const tabletCssProperties = elementResourceProperties.find((property) => property.label === "presentation" && property.values[0].content === "css-tablet")?.properties ?? [];
2100
+ const cssStylesTablet = [];
2101
+ for (const property of tabletCssProperties) {
2102
+ const cssStyle = property.values[0].content;
2103
+ cssStylesTablet.push({
2104
+ label: property.label,
2105
+ value: cssStyle
2106
+ });
2107
+ }
2108
+ const mobileCssProperties = elementResourceProperties.find((property) => property.label === "presentation" && property.values[0].content === "css-mobile")?.properties ?? [];
2109
+ const cssStylesMobile = [];
2110
+ for (const property of mobileCssProperties) {
2111
+ const cssStyle = property.values[0].content;
2112
+ cssStylesMobile.push({
2113
+ label: property.label,
2114
+ value: cssStyle
2115
+ });
2116
+ }
2117
+ const title = parseWebTitle(elementResourceProperties, identification, {
2118
+ isNameDisplayed: [
2119
+ "annotated-image",
2120
+ "annotated-document",
2121
+ "collection"
2122
+ ].includes(properties.component),
2123
+ isCountDisplayed: properties.component === "collection" && properties.variant === "full"
2124
+ });
2125
+ return {
2126
+ uuid: elementResource.uuid,
2127
+ type: "element",
2128
+ title,
2129
+ cssStyles: {
2130
+ default: cssStyles,
2131
+ tablet: cssStylesTablet,
2132
+ mobile: cssStylesMobile
2133
+ },
2134
+ ...properties
2135
+ };
2136
+ }
2137
+ /**
2138
+ * Parses raw webpage data into a standardized Webpage structure
2139
+ *
2140
+ * @param webpageResource - Raw webpage resource data in OCHRE format
2141
+ * @returns Parsed Webpage object
2142
+ */
2143
+ function parseWebpage(webpageResource) {
2144
+ const webpageProperties = webpageResource.properties ? parseProperties(Array.isArray(webpageResource.properties.property) ? webpageResource.properties.property : [webpageResource.properties.property]) : [];
2145
+ if (webpageProperties.length === 0 || webpageProperties.find((property) => property.label === "presentation")?.values[0]?.content !== "page") return null;
2146
+ const identification = parseIdentification(webpageResource.identification);
2147
+ const slug = webpageResource.slug;
2148
+ if (slug === void 0) throw new Error(`Slug not found for page “${identification.label}”`);
2149
+ const imageLink = (webpageResource.links ? parseLinks(Array.isArray(webpageResource.links) ? webpageResource.links : [webpageResource.links]) : []).find((link) => link.type === "image" || link.type === "IIIF");
2150
+ const webpageResources = webpageResource.resource ? Array.isArray(webpageResource.resource) ? webpageResource.resource : [webpageResource.resource] : [];
2151
+ const items = [];
2152
+ for (const resource of webpageResources) {
2153
+ const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : [], "presentation");
2154
+ if (resourceType == null) continue;
2155
+ switch (resourceType) {
2156
+ case "element": {
2157
+ const element = parseWebElement(resource);
2158
+ items.push(element);
2159
+ break;
2160
+ }
2161
+ case "block": {
2162
+ const block = parseWebBlock(resource);
2163
+ if (block) items.push(block);
2164
+ break;
2165
+ }
2166
+ }
2167
+ }
2168
+ const webpages = webpageResource.resource ? parseWebpageResources(Array.isArray(webpageResource.resource) ? webpageResource.resource : [webpageResource.resource], "page") : [];
2169
+ let displayedInHeader = true;
2170
+ let width = "default";
2171
+ let variant = "default";
2172
+ let isSidebarDisplayed = true;
2173
+ let isBreadcrumbsDisplayed = false;
2174
+ const webpageSubProperties = webpageProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "page")?.properties;
2175
+ if (webpageSubProperties) {
2176
+ const headerProperty = webpageSubProperties.find((property) => property.label === "header")?.values[0];
2177
+ if (headerProperty) displayedInHeader = headerProperty.content === true;
2178
+ const widthProperty = webpageSubProperties.find((property) => property.label === "width")?.values[0];
2179
+ if (widthProperty) width = widthProperty.content;
2180
+ const variantProperty = webpageSubProperties.find((property) => property.label === "variant")?.values[0];
2181
+ if (variantProperty) variant = variantProperty.content;
2182
+ const isSidebarDisplayedProperty = webpageSubProperties.find((property) => property.label === "sidebar-visible")?.values[0];
2183
+ if (isSidebarDisplayedProperty) isSidebarDisplayed = isSidebarDisplayedProperty.content === true;
2184
+ const isBreadcrumbsDisplayedProperty = webpageSubProperties.find((property) => property.label === "breadcrumbs-visible")?.values[0];
2185
+ if (isBreadcrumbsDisplayedProperty) isBreadcrumbsDisplayed = isBreadcrumbsDisplayedProperty.content === true;
2186
+ }
2187
+ const cssStyleSubProperties = webpageProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css")?.properties;
2188
+ const cssStyles = [];
2189
+ if (cssStyleSubProperties) for (const property of cssStyleSubProperties) cssStyles.push({
2190
+ label: property.label,
2191
+ value: property.values[0].content
2192
+ });
2193
+ const tabletCssStyleSubProperties = webpageProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css-tablet")?.properties;
2194
+ const cssStylesTablet = [];
2195
+ if (tabletCssStyleSubProperties) for (const property of tabletCssStyleSubProperties) cssStylesTablet.push({
2196
+ label: property.label,
2197
+ value: property.values[0].content
2198
+ });
2199
+ const mobileCssStyleSubProperties = webpageProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css-mobile")?.properties;
2200
+ const cssStylesMobile = [];
2201
+ if (mobileCssStyleSubProperties) for (const property of mobileCssStyleSubProperties) cssStylesMobile.push({
2202
+ label: property.label,
2203
+ value: property.values[0].content
2204
+ });
2205
+ return {
2206
+ title: identification.label,
2207
+ slug,
2208
+ items,
2209
+ properties: {
2210
+ displayedInHeader,
2211
+ width,
2212
+ variant,
2213
+ backgroundImageUrl: imageLink ? `https://ochre.lib.uchicago.edu/ochre?uuid=${imageLink.uuid}&load` : null,
2214
+ isSidebarDisplayed,
2215
+ isBreadcrumbsDisplayed,
2216
+ cssStyles: {
2217
+ default: cssStyles,
2218
+ tablet: cssStylesTablet,
2219
+ mobile: cssStylesMobile
2220
+ }
2221
+ },
2222
+ webpages
2223
+ };
2224
+ }
2225
+ /**
2226
+ * Parses raw webpage resources into an array of Webpage objects
2227
+ *
2228
+ * @param webpageResources - Array of raw webpage resources in OCHRE format
2229
+ * @returns Array of parsed Webpage objects
2230
+ */
2231
+ function parseWebpages(webpageResources) {
2232
+ const returnPages = [];
2233
+ const pagesToParse = Array.isArray(webpageResources) ? webpageResources : [webpageResources];
2234
+ for (const page of pagesToParse) {
2235
+ const webpage = parseWebpage(page);
2236
+ if (webpage) returnPages.push(webpage);
2237
+ }
2238
+ return returnPages;
2239
+ }
2240
+ /**
2241
+ * Parses raw sidebar data into a standardized Sidebar structure
2242
+ *
2243
+ * @param resources - Array of raw sidebar resources in OCHRE format
2244
+ * @returns Parsed Sidebar object
2245
+ */
2246
+ function parseSidebar(resources) {
2247
+ let sidebar = null;
2248
+ const sidebarElements = [];
2249
+ const sidebarTitle = {
2250
+ label: "",
2251
+ variant: "default",
2252
+ properties: {
2253
+ isNameDisplayed: false,
2254
+ isDescriptionDisplayed: false,
2255
+ isDateDisplayed: false,
2256
+ isCreatorsDisplayed: false,
2257
+ isCountDisplayed: false
2258
+ }
2259
+ };
2260
+ let sidebarLayout = "start";
2261
+ let sidebarMobileLayout = "default";
2262
+ const sidebarCssStyles = [];
2263
+ const sidebarCssStylesTablet = [];
2264
+ const sidebarCssStylesMobile = [];
2265
+ const sidebarResource = resources.find((resource) => {
2266
+ return (resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : []).some((property) => property.label === "presentation" && property.values[0]?.content === "element" && property.properties[0]?.label === "component" && property.properties[0].values[0]?.content === "sidebar");
2267
+ });
2268
+ if (sidebarResource) {
2269
+ sidebarTitle.label = typeof sidebarResource.identification.label === "string" || typeof sidebarResource.identification.label === "number" || typeof sidebarResource.identification.label === "boolean" ? parseFakeString(sidebarResource.identification.label) : parseStringContent(sidebarResource.identification.label);
2270
+ const sidebarBaseProperties = sidebarResource.properties ? parseProperties(Array.isArray(sidebarResource.properties.property) ? sidebarResource.properties.property : [sidebarResource.properties.property]) : [];
2271
+ const sidebarProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "element")?.properties.find((property) => property.label === "component" && property.values[0]?.content === "sidebar")?.properties ?? [];
2272
+ const sidebarLayoutProperty = sidebarProperties.find((property) => property.label === "layout");
2273
+ if (sidebarLayoutProperty) sidebarLayout = sidebarLayoutProperty.values[0].content;
2274
+ const sidebarMobileLayoutProperty = sidebarProperties.find((property) => property.label === "layout-mobile");
2275
+ if (sidebarMobileLayoutProperty) sidebarMobileLayout = sidebarMobileLayoutProperty.values[0].content;
2276
+ const cssProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0].content === "css")?.properties ?? [];
2277
+ for (const property of cssProperties) {
2278
+ const cssStyle = property.values[0].content;
2279
+ sidebarCssStyles.push({
2280
+ label: property.label,
2281
+ value: cssStyle
2282
+ });
2283
+ }
2284
+ const tabletCssProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0].content === "css-tablet")?.properties ?? [];
2285
+ for (const property of tabletCssProperties) {
2286
+ const cssStyle = property.values[0].content;
2287
+ sidebarCssStylesTablet.push({
2288
+ label: property.label,
2289
+ value: cssStyle
2290
+ });
2291
+ }
2292
+ const mobileCssProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0].content === "css-mobile")?.properties ?? [];
2293
+ for (const property of mobileCssProperties) {
2294
+ const cssStyle = property.values[0].content;
2295
+ sidebarCssStylesMobile.push({
2296
+ label: property.label,
2297
+ value: cssStyle
2298
+ });
2299
+ }
2300
+ const titleProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0].content === "title")?.properties;
2301
+ if (titleProperties) {
2302
+ const titleVariant = getPropertyValueByLabel(titleProperties, "variant");
2303
+ if (titleVariant) sidebarTitle.variant = titleVariant;
2304
+ sidebarTitle.properties.isNameDisplayed = getPropertyValueByLabel(titleProperties, "name-displayed") === true;
2305
+ sidebarTitle.properties.isDescriptionDisplayed = getPropertyValueByLabel(titleProperties, "description-displayed") === true;
2306
+ sidebarTitle.properties.isDateDisplayed = getPropertyValueByLabel(titleProperties, "date-displayed") === true;
2307
+ sidebarTitle.properties.isCreatorsDisplayed = getPropertyValueByLabel(titleProperties, "creators-displayed") === true;
2308
+ sidebarTitle.properties.isCountDisplayed = getPropertyValueByLabel(titleProperties, "count-displayed") === true;
2309
+ }
2310
+ const sidebarResources = sidebarResource.resource ? Array.isArray(sidebarResource.resource) ? sidebarResource.resource : [sidebarResource.resource] : [];
2311
+ for (const resource of sidebarResources) {
2312
+ const element = parseWebElement(resource);
2313
+ sidebarElements.push(element);
2314
+ }
2315
+ }
2316
+ if (sidebarElements.length > 0) sidebar = {
2317
+ elements: sidebarElements,
2318
+ title: sidebarTitle,
2319
+ layout: sidebarLayout,
2320
+ mobileLayout: sidebarMobileLayout,
2321
+ cssStyles: {
2322
+ default: sidebarCssStyles,
2323
+ tablet: sidebarCssStylesTablet,
2324
+ mobile: sidebarCssStylesMobile
2325
+ }
2326
+ };
2327
+ return sidebar;
2328
+ }
2329
+ /**
2330
+ * Parses raw text element data for accordion layout with items support
2331
+ *
2332
+ * @param elementResource - Raw element resource data in OCHRE format
2333
+ * @returns Parsed text WebElement with items array
2334
+ */
2335
+ function parseWebElementForAccordion(elementResource) {
2336
+ const textElement = parseWebElement(elementResource);
2337
+ const childResources = elementResource.resource ? Array.isArray(elementResource.resource) ? elementResource.resource : [elementResource.resource] : [];
2338
+ const items = [];
2339
+ for (const resource of childResources) {
2340
+ const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : [], "presentation");
2341
+ if (resourceType == null) continue;
2342
+ switch (resourceType) {
2343
+ case "element": {
2344
+ const element = parseWebElement(resource);
2345
+ items.push(element);
2346
+ break;
2347
+ }
2348
+ case "block": {
2349
+ const block = parseWebBlock(resource);
2350
+ if (block) items.push(block);
2351
+ break;
2352
+ }
2353
+ }
2354
+ }
2355
+ return {
2356
+ ...textElement,
2357
+ items
2358
+ };
2359
+ }
2360
+ /**
2361
+ * Parses raw block data into a standardized WebBlock structure
2362
+ *
2363
+ * @param blockResource - Raw block resource data in OCHRE format
2364
+ * @returns Parsed WebBlock object
2365
+ */
2366
+ function parseWebBlock(blockResource) {
2367
+ const blockProperties = blockResource.properties ? parseProperties(Array.isArray(blockResource.properties.property) ? blockResource.properties.property : [blockResource.properties.property]) : [];
2368
+ const returnBlock = {
2369
+ uuid: blockResource.uuid,
2370
+ type: "block",
2371
+ title: parseWebTitle(blockProperties, parseIdentification(blockResource.identification)),
2372
+ items: [],
2373
+ properties: {
2374
+ default: {
2375
+ layout: "vertical",
2376
+ spacing: void 0,
2377
+ gap: void 0,
2378
+ alignItems: "start",
2379
+ justifyContent: "stretch"
2380
+ },
2381
+ mobile: null,
2382
+ tablet: null
2383
+ },
2384
+ cssStyles: {
2385
+ default: [],
2386
+ tablet: [],
2387
+ mobile: []
2388
+ }
2389
+ };
2390
+ const blockMainProperties = blockProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "block")?.properties;
2391
+ if (blockMainProperties) {
2392
+ const layoutProperty = blockMainProperties.find((property) => property.label === "layout")?.values[0];
2393
+ if (layoutProperty) returnBlock.properties.default.layout = layoutProperty.content;
2394
+ if (returnBlock.properties.default.layout === "accordion") {
2395
+ const isAccordionEnabledProperty = blockMainProperties.find((property) => property.label === "accordion-enabled")?.values[0];
2396
+ if (isAccordionEnabledProperty) returnBlock.properties.default.isAccordionEnabled = isAccordionEnabledProperty.content === true;
2397
+ else returnBlock.properties.default.isAccordionEnabled = true;
2398
+ const isAccordionExpandedByDefaultProperty = blockMainProperties.find((property) => property.label === "accordion-expanded")?.values[0];
2399
+ if (isAccordionExpandedByDefaultProperty) returnBlock.properties.default.isAccordionExpandedByDefault = isAccordionExpandedByDefaultProperty.content === true;
2400
+ else returnBlock.properties.default.isAccordionExpandedByDefault = false;
2401
+ const isAccordionSidebarDisplayedProperty = blockMainProperties.find((property) => property.label === "accordion-sidebar-displayed")?.values[0];
2402
+ if (isAccordionSidebarDisplayedProperty) returnBlock.properties.default.isAccordionSidebarDisplayed = isAccordionSidebarDisplayedProperty.content === true;
2403
+ else returnBlock.properties.default.isAccordionSidebarDisplayed = false;
2404
+ }
2405
+ const spacingProperty = blockMainProperties.find((property) => property.label === "spacing")?.values[0];
2406
+ if (spacingProperty) returnBlock.properties.default.spacing = spacingProperty.content;
2407
+ const gapProperty = blockMainProperties.find((property) => property.label === "gap")?.values[0];
2408
+ if (gapProperty) returnBlock.properties.default.gap = gapProperty.content;
2409
+ const alignItemsProperty = blockMainProperties.find((property) => property.label === "align-items")?.values[0];
2410
+ if (alignItemsProperty) returnBlock.properties.default.alignItems = alignItemsProperty.content;
2411
+ const justifyContentProperty = blockMainProperties.find((property) => property.label === "justify-content")?.values[0];
2412
+ if (justifyContentProperty) returnBlock.properties.default.justifyContent = justifyContentProperty.content;
2413
+ const tabletOverwriteProperty = blockMainProperties.find((property) => property.label === "overwrite-tablet");
2414
+ if (tabletOverwriteProperty) {
2415
+ const tabletOverwriteProperties = tabletOverwriteProperty.properties;
2416
+ const propertiesTablet = {};
2417
+ const layoutProperty$1 = tabletOverwriteProperties.find((property) => property.label === "layout")?.values[0];
2418
+ if (layoutProperty$1) propertiesTablet.layout = layoutProperty$1.content;
2419
+ if (propertiesTablet.layout === "accordion" || returnBlock.properties.default.layout === "accordion") {
2420
+ const isAccordionEnabledProperty = tabletOverwriteProperties.find((property) => property.label === "accordion-enabled")?.values[0];
2421
+ if (isAccordionEnabledProperty) propertiesTablet.isAccordionEnabled = isAccordionEnabledProperty.content === true;
2422
+ const isAccordionExpandedByDefaultProperty = tabletOverwriteProperties.find((property) => property.label === "accordion-expanded")?.values[0];
2423
+ if (isAccordionExpandedByDefaultProperty) propertiesTablet.isAccordionExpandedByDefault = isAccordionExpandedByDefaultProperty.content === true;
2424
+ const isAccordionSidebarDisplayedProperty = tabletOverwriteProperties.find((property) => property.label === "accordion-sidebar-displayed")?.values[0];
2425
+ if (isAccordionSidebarDisplayedProperty) propertiesTablet.isAccordionSidebarDisplayed = isAccordionSidebarDisplayedProperty.content === true;
2426
+ }
2427
+ const spacingProperty$1 = tabletOverwriteProperties.find((property) => property.label === "spacing")?.values[0];
2428
+ if (spacingProperty$1) propertiesTablet.spacing = spacingProperty$1.content;
2429
+ const gapProperty$1 = tabletOverwriteProperties.find((property) => property.label === "gap")?.values[0];
2430
+ if (gapProperty$1) propertiesTablet.gap = gapProperty$1.content;
2431
+ const alignItemsProperty$1 = tabletOverwriteProperties.find((property) => property.label === "align-items")?.values[0];
2432
+ if (alignItemsProperty$1) propertiesTablet.alignItems = alignItemsProperty$1.content;
2433
+ const justifyContentProperty$1 = tabletOverwriteProperties.find((property) => property.label === "justify-content")?.values[0];
2434
+ if (justifyContentProperty$1) propertiesTablet.justifyContent = justifyContentProperty$1.content;
2435
+ returnBlock.properties.tablet = propertiesTablet;
2436
+ }
2437
+ const mobileOverwriteProperty = blockMainProperties.find((property) => property.label === "overwrite-mobile");
2438
+ if (mobileOverwriteProperty) {
2439
+ const mobileOverwriteProperties = mobileOverwriteProperty.properties;
2440
+ const propertiesMobile = {};
2441
+ const layoutProperty$1 = mobileOverwriteProperties.find((property) => property.label === "layout")?.values[0];
2442
+ if (layoutProperty$1) propertiesMobile.layout = layoutProperty$1.content;
2443
+ if (propertiesMobile.layout === "accordion" || returnBlock.properties.default.layout === "accordion") {
2444
+ const isAccordionEnabledProperty = mobileOverwriteProperties.find((property) => property.label === "accordion-enabled")?.values[0];
2445
+ if (isAccordionEnabledProperty) propertiesMobile.isAccordionEnabled = isAccordionEnabledProperty.content === true;
2446
+ const isAccordionExpandedByDefaultProperty = mobileOverwriteProperties.find((property) => property.label === "accordion-expanded")?.values[0];
2447
+ if (isAccordionExpandedByDefaultProperty) propertiesMobile.isAccordionExpandedByDefault = isAccordionExpandedByDefaultProperty.content === true;
2448
+ const isAccordionSidebarDisplayedProperty = mobileOverwriteProperties.find((property) => property.label === "accordion-sidebar-displayed")?.values[0];
2449
+ if (isAccordionSidebarDisplayedProperty) propertiesMobile.isAccordionSidebarDisplayed = isAccordionSidebarDisplayedProperty.content === true;
2450
+ }
2451
+ const spacingProperty$1 = mobileOverwriteProperties.find((property) => property.label === "spacing")?.values[0];
2452
+ if (spacingProperty$1) propertiesMobile.spacing = spacingProperty$1.content;
2453
+ const gapProperty$1 = mobileOverwriteProperties.find((property) => property.label === "gap")?.values[0];
2454
+ if (gapProperty$1) propertiesMobile.gap = gapProperty$1.content;
2455
+ const alignItemsProperty$1 = mobileOverwriteProperties.find((property) => property.label === "align-items")?.values[0];
2456
+ if (alignItemsProperty$1) propertiesMobile.alignItems = alignItemsProperty$1.content;
2457
+ const justifyContentProperty$1 = mobileOverwriteProperties.find((property) => property.label === "justify-content")?.values[0];
2458
+ if (justifyContentProperty$1) propertiesMobile.justifyContent = justifyContentProperty$1.content;
2459
+ returnBlock.properties.mobile = propertiesMobile;
2460
+ }
2461
+ }
2462
+ const blockResources = blockResource.resource ? Array.isArray(blockResource.resource) ? blockResource.resource : [blockResource.resource] : [];
2463
+ if (returnBlock.properties.default.layout === "accordion") {
2464
+ const accordionItems = [];
2465
+ for (const resource of blockResources) {
2466
+ const resourceProperties = resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : [];
2467
+ const resourceType = getPropertyValueByLabel(resourceProperties, "presentation");
2468
+ if (resourceType !== "element") throw new Error(`Accordion only accepts elements, but got “${resourceType}” for the following resource: “${parseStringContent(resource.identification.label)}”`);
2469
+ const componentType = (resourceProperties.find((property) => property.label === "presentation")?.properties.find((property) => property.label === "component"))?.values[0]?.content;
2470
+ if (componentType !== "text") throw new Error(`Accordion only accepts text components, but got “${componentType}” for the following resource: “${parseStringContent(resource.identification.label)}”`);
2471
+ const element = parseWebElementForAccordion(resource);
2472
+ accordionItems.push(element);
2473
+ }
2474
+ returnBlock.items = accordionItems;
2475
+ } else {
2476
+ const blockItems = [];
2477
+ for (const resource of blockResources) {
2478
+ const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : [], "presentation");
2479
+ if (resourceType == null) continue;
2480
+ switch (resourceType) {
2481
+ case "element": {
2482
+ const element = parseWebElement(resource);
2483
+ blockItems.push(element);
2484
+ break;
2485
+ }
2486
+ case "block": {
2487
+ const block = parseWebBlock(resource);
2488
+ if (block) blockItems.push(block);
2489
+ break;
2490
+ }
2491
+ }
2492
+ }
2493
+ returnBlock.items = blockItems;
2494
+ }
2495
+ const blockCssStyles = blockProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css")?.properties;
2496
+ if (blockCssStyles) for (const property of blockCssStyles) returnBlock.cssStyles.default.push({
2497
+ label: property.label,
2498
+ value: property.values[0].content
2499
+ });
2500
+ const blockTabletCssStyles = blockProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css-tablet")?.properties;
2501
+ if (blockTabletCssStyles) for (const property of blockTabletCssStyles) returnBlock.cssStyles.tablet.push({
2502
+ label: property.label,
2503
+ value: property.values[0].content
2504
+ });
2505
+ const blockMobileCssStyles = blockProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css-mobile")?.properties;
2506
+ if (blockMobileCssStyles) for (const property of blockMobileCssStyles) returnBlock.cssStyles.mobile.push({
2507
+ label: property.label,
2508
+ value: property.values[0].content
2509
+ });
2510
+ return returnBlock;
2511
+ }
2512
+ /**
2513
+ * Parses raw website properties into a standardized WebsiteProperties structure
2514
+ *
2515
+ * @param properties - Array of raw website properties in OCHRE format
2516
+ * @returns Parsed WebsiteProperties object
2517
+ */
2518
+ function parseWebsiteProperties(properties) {
2519
+ const websiteProperties = parseProperties(properties).find((property) => property.label === "presentation")?.properties;
2520
+ if (!websiteProperties) throw new Error("Presentation property not found");
2521
+ let type = websiteProperties.find((property) => property.label === "webUI")?.values[0]?.content;
2522
+ type ??= "traditional";
2523
+ let status = websiteProperties.find((property) => property.label === "status")?.values[0]?.content;
2524
+ status ??= "development";
2525
+ let privacy = websiteProperties.find((property) => property.label === "privacy")?.values[0]?.content;
2526
+ privacy ??= "public";
2527
+ const result = websiteSchema.safeParse({
2528
+ type,
2529
+ status,
2530
+ privacy
2531
+ });
2532
+ if (!result.success) throw new Error(`Invalid website properties: ${result.error.message}`);
2533
+ let contact = null;
2534
+ const contactProperty = websiteProperties.find((property) => property.label === "contact");
2535
+ if (contactProperty) {
2536
+ const [name, email] = (contactProperty.values[0]?.content).split(";");
2537
+ contact = {
2538
+ name,
2539
+ email: email ?? null
2540
+ };
2541
+ }
2542
+ const logoUuid = websiteProperties.find((property) => property.label === "logo")?.values[0]?.uuid ?? null;
2543
+ let isHeaderDisplayed = true;
2544
+ let headerVariant = "default";
2545
+ let headerAlignment = "start";
2546
+ let isHeaderProjectDisplayed = true;
2547
+ let isFooterDisplayed = true;
2548
+ let isSidebarDisplayed = false;
2549
+ let iiifViewer = "universal-viewer";
2550
+ let supportsThemeToggle = true;
2551
+ let defaultTheme = null;
2552
+ const headerProperty = websiteProperties.find((property) => property.label === "navbar-visible")?.values[0];
2553
+ if (headerProperty) isHeaderDisplayed = headerProperty.content === true;
2554
+ const headerVariantProperty = websiteProperties.find((property) => property.label === "navbar-variant")?.values[0];
2555
+ if (headerVariantProperty) headerVariant = headerVariantProperty.content;
2556
+ const headerAlignmentProperty = websiteProperties.find((property) => property.label === "navbar-alignment")?.values[0];
2557
+ if (headerAlignmentProperty) headerAlignment = headerAlignmentProperty.content;
2558
+ const isHeaderProjectDisplayedProperty = websiteProperties.find((property) => property.label === "navbar-project-visible")?.values[0];
2559
+ if (isHeaderProjectDisplayedProperty) isHeaderProjectDisplayed = isHeaderProjectDisplayedProperty.content === true;
2560
+ const footerProperty = websiteProperties.find((property) => property.label === "footer-visible")?.values[0];
2561
+ if (footerProperty) isFooterDisplayed = footerProperty.content === true;
2562
+ const sidebarProperty = websiteProperties.find((property) => property.label === "sidebar-visible")?.values[0];
2563
+ if (sidebarProperty) isSidebarDisplayed = sidebarProperty.content === true;
2564
+ const iiifViewerProperty = websiteProperties.find((property) => property.label === "iiif-viewer")?.values[0];
2565
+ if (iiifViewerProperty) iiifViewer = iiifViewerProperty.content;
2566
+ const supportsThemeToggleProperty = websiteProperties.find((property) => property.label === "supports-theme-toggle")?.values[0];
2567
+ if (supportsThemeToggleProperty) supportsThemeToggle = supportsThemeToggleProperty.content === true;
2568
+ const defaultThemeProperty = websiteProperties.find((property) => property.label === "default-theme")?.values[0];
2569
+ if (defaultThemeProperty) defaultTheme = defaultThemeProperty.content;
2570
+ const { type: validatedType, status: validatedStatus, privacy: validatedPrivacy } = result.data;
2571
+ return {
2572
+ type: validatedType,
2573
+ privacy: validatedPrivacy,
2574
+ status: validatedStatus,
2575
+ contact,
2576
+ isHeaderDisplayed,
2577
+ headerVariant,
2578
+ headerAlignment,
2579
+ isHeaderProjectDisplayed,
2580
+ isFooterDisplayed,
2581
+ isSidebarDisplayed,
2582
+ iiifViewer,
2583
+ supportsThemeToggle,
2584
+ defaultTheme,
2585
+ logoUrl: logoUuid !== null ? `https://ochre.lib.uchicago.edu/ochre?uuid=${logoUuid}&load` : null
2586
+ };
2587
+ }
2588
+ function parseContexts(contexts) {
2589
+ const contextsParsed = [];
2590
+ for (const mainContext of contexts) {
2591
+ const contextItemsToParse = Array.isArray(mainContext.context) ? mainContext.context : [mainContext.context];
2592
+ for (const contextItemToParse of contextItemsToParse) {
2593
+ const levelsToParse = Array.isArray(contextItemToParse.levels.level) ? contextItemToParse.levels.level : [contextItemToParse.levels.level];
2594
+ let type = "";
2595
+ const levels = levelsToParse.map((level) => {
2596
+ let variableUuid = "";
2597
+ let valueUuid = null;
2598
+ if (typeof level === "string") {
2599
+ const splitLevel = level.split(", ");
2600
+ variableUuid = splitLevel[0];
2601
+ valueUuid = splitLevel[1] === "null" ? null : splitLevel[1];
2602
+ } else {
2603
+ const splitLevel = level.content.split(", ");
2604
+ type = level.dataType;
2605
+ variableUuid = splitLevel[0];
2606
+ valueUuid = splitLevel[1] === "null" ? null : splitLevel[1];
2607
+ }
2608
+ return {
2609
+ variableUuid,
2610
+ valueUuid
2611
+ };
2612
+ });
2613
+ contextsParsed.push({
2614
+ context: levels,
2615
+ type,
2616
+ identification: parseIdentification(contextItemToParse.identification)
2617
+ });
2618
+ }
2619
+ }
2620
+ return contextsParsed;
2621
+ }
2622
+ function parseWebsite(websiteTree, projectName, website) {
2623
+ if (!websiteTree.properties) throw new Error("Website properties not found");
2624
+ const properties = parseWebsiteProperties(Array.isArray(websiteTree.properties.property) ? websiteTree.properties.property : [websiteTree.properties.property]);
2625
+ if (typeof websiteTree.items === "string" || !("resource" in websiteTree.items)) throw new Error("Website pages not found");
2626
+ const resources = Array.isArray(websiteTree.items.resource) ? websiteTree.items.resource : [websiteTree.items.resource];
2627
+ const pages = parseWebpages(resources);
2628
+ const sidebar = parseSidebar(resources);
2629
+ let globalOptions = { contexts: {
2630
+ flatten: [],
2631
+ filter: [],
2632
+ sort: [],
2633
+ detail: [],
2634
+ download: [],
2635
+ label: [],
2636
+ suppress: [],
2637
+ prominent: []
2638
+ } };
2639
+ if (websiteTree.websiteOptions) {
2640
+ const flattenContextsRaw = websiteTree.websiteOptions.flattenContexts != null ? Array.isArray(websiteTree.websiteOptions.flattenContexts) ? websiteTree.websiteOptions.flattenContexts : [websiteTree.websiteOptions.flattenContexts] : [];
2641
+ const suppressContextsRaw = websiteTree.websiteOptions.suppressContexts != null ? Array.isArray(websiteTree.websiteOptions.suppressContexts) ? websiteTree.websiteOptions.suppressContexts : [websiteTree.websiteOptions.suppressContexts] : [];
2642
+ const filterContextsRaw = websiteTree.websiteOptions.filterContexts != null ? Array.isArray(websiteTree.websiteOptions.filterContexts) ? websiteTree.websiteOptions.filterContexts : [websiteTree.websiteOptions.filterContexts] : [];
2643
+ const sortContextsRaw = websiteTree.websiteOptions.sortContexts != null ? Array.isArray(websiteTree.websiteOptions.sortContexts) ? websiteTree.websiteOptions.sortContexts : [websiteTree.websiteOptions.sortContexts] : [];
2644
+ const detailContextsRaw = websiteTree.websiteOptions.detailContexts != null ? Array.isArray(websiteTree.websiteOptions.detailContexts) ? websiteTree.websiteOptions.detailContexts : [websiteTree.websiteOptions.detailContexts] : [];
2645
+ const downloadContextsRaw = websiteTree.websiteOptions.downloadContexts != null ? Array.isArray(websiteTree.websiteOptions.downloadContexts) ? websiteTree.websiteOptions.downloadContexts : [websiteTree.websiteOptions.downloadContexts] : [];
2646
+ const labelContextsRaw = websiteTree.websiteOptions.labelContexts != null ? Array.isArray(websiteTree.websiteOptions.labelContexts) ? websiteTree.websiteOptions.labelContexts : [websiteTree.websiteOptions.labelContexts] : [];
2647
+ const prominentContextsRaw = websiteTree.websiteOptions.prominentContexts != null ? Array.isArray(websiteTree.websiteOptions.prominentContexts) ? websiteTree.websiteOptions.prominentContexts : [websiteTree.websiteOptions.prominentContexts] : [];
2648
+ globalOptions = { contexts: {
2649
+ flatten: parseContexts(flattenContextsRaw),
2650
+ filter: parseContexts(filterContextsRaw),
2651
+ sort: parseContexts(sortContextsRaw),
2652
+ detail: parseContexts(detailContextsRaw),
2653
+ download: parseContexts(downloadContextsRaw),
2654
+ label: parseContexts(labelContextsRaw),
2655
+ suppress: parseContexts(suppressContextsRaw),
2656
+ prominent: parseContexts(prominentContextsRaw)
2657
+ } };
2658
+ }
2659
+ return {
2660
+ uuid: websiteTree.uuid,
2661
+ publicationDateTime: websiteTree.publicationDateTime ? new Date(websiteTree.publicationDateTime) : null,
2662
+ identification: parseIdentification(websiteTree.identification),
2663
+ project: {
2664
+ name: parseFakeString(projectName),
2665
+ website: website !== null ? parseFakeString(website) : null
2666
+ },
2667
+ creators: websiteTree.creators ? parsePersons(Array.isArray(websiteTree.creators.creator) ? websiteTree.creators.creator : [websiteTree.creators.creator]) : [],
2668
+ license: parseLicense(websiteTree.availability),
2669
+ sidebar,
2670
+ pages,
2671
+ properties,
2672
+ searchOptions: {
2673
+ filters: websiteTree.searchOptions?.filterUuids != null ? (Array.isArray(websiteTree.searchOptions.filterUuids.uuid) ? websiteTree.searchOptions.filterUuids.uuid : [websiteTree.searchOptions.filterUuids.uuid]).map((uuid) => ({
2674
+ uuid: uuid.content,
2675
+ type: uuid.type
2676
+ })) : [],
2677
+ attributeFilters: {
2678
+ bibliographies: websiteTree.searchOptions?.filterUuids?.filterBibliography ?? false,
2679
+ periods: websiteTree.searchOptions?.filterUuids?.filterPeriods ?? false
2680
+ },
2681
+ scopes: websiteTree.searchOptions?.scopes != null ? (Array.isArray(websiteTree.searchOptions.scopes.scope) ? websiteTree.searchOptions.scopes.scope : [websiteTree.searchOptions.scopes.scope]).map((scope) => ({
2682
+ uuid: scope.uuid.content,
2683
+ type: scope.uuid.type,
2684
+ identification: parseIdentification(scope.identification)
2685
+ })) : []
2686
+ },
2687
+ globalOptions
2688
+ };
2689
+ }
2690
+
2691
+ //#endregion
2692
+ //#region src/utils/fetchers/gallery.ts
2693
+ /**
2694
+ * Fetches and parses a gallery from the OCHRE API
2695
+ *
2696
+ * @param uuid - The UUID of the gallery
2697
+ * @param filter - The filter to apply to the gallery
2698
+ * @param page - The page number to fetch
2699
+ * @param perPage - The number of items per page
2700
+ * @returns The parsed gallery or null if the fetch/parse fails
2701
+ *
2702
+ * @example
2703
+ * ```ts
2704
+ * const gallery = await fetchGallery("9c4da06b-f15e-40af-a747-0933eaf3587e", "1978", 1, 12);
2705
+ * if (gallery === null) {
2706
+ * console.error("Failed to fetch gallery");
2707
+ * return;
2708
+ * }
2709
+ * console.log(`Fetched gallery: ${gallery.identification.label}`);
2710
+ * console.log(`Contains ${gallery.resources.length.toLocaleString()} resources`);
2711
+ * ```
2712
+ *
2713
+ * @remarks
2714
+ * The returned gallery includes:
2715
+ * - Gallery metadata and identification
2716
+ * - Project identification
2717
+ * - Resources (gallery items)
2718
+ */
2719
+ async function fetchGallery(uuid, filter, page, perPage, customFetch) {
2720
+ try {
2721
+ const { uuid: parsedUuid, filter: parsedFilter, page: parsedPage, perPage: parsedPerPage } = gallerySchema.parse({
2722
+ uuid,
2723
+ filter,
2724
+ page,
2725
+ perPage
2726
+ });
2727
+ const response = await (customFetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(`
2728
+ for $q in input()/ochre[@uuid='${parsedUuid}']
2729
+ let $filtered := $q//items/resource[contains(lower-case(identification/label), lower-case('${parsedFilter}'))]
2730
+ let $maxLength := count($filtered)
2731
+ return <gallery maxLength='{$maxLength}'>
2732
+ {$q/metadata/project}
2733
+ {$q/metadata/item}
2734
+ {$filtered[position() >= ${((parsedPage - 1) * parsedPerPage + 1).toString()} and position() < ${(parsedPage * parsedPerPage + 1).toString()}]}
2735
+ </gallery>
2736
+ `)}&format=json`);
2737
+ if (!response.ok) throw new Error("Error fetching gallery items, please try again later.");
2738
+ const data = await response.json();
2739
+ if (!("gallery" in data.result)) throw new Error("Failed to fetch gallery");
2740
+ return {
2741
+ item: {
2742
+ identification: parseIdentification(data.result.gallery.item.identification),
2743
+ projectIdentification: parseIdentification(data.result.gallery.project.identification),
2744
+ resources: parseResources(data.result.gallery.resource ? Array.isArray(data.result.gallery.resource) ? data.result.gallery.resource : [data.result.gallery.resource] : []),
2745
+ maxLength: data.result.gallery.maxLength
2746
+ },
2747
+ error: null
2748
+ };
2749
+ } catch (error) {
2750
+ console.error(error);
2751
+ return {
2752
+ item: null,
2753
+ error: error instanceof Error ? error.message : "Failed to fetch gallery"
2754
+ };
2755
+ }
2756
+ }
2757
+
2758
+ //#endregion
2759
+ //#region src/utils/fetchers/uuid.ts
2760
+ /**
2761
+ * Fetches raw OCHRE data by UUID from the OCHRE API
2762
+ *
2763
+ * @param uuid - The UUID of the OCHRE item to fetch
2764
+ * @returns A tuple containing either [null, OchreData] on success or [error message, null] on failure
2765
+ *
2766
+ * @example
2767
+ * ```ts
2768
+ * const [error, data] = await fetchByUuid("123e4567-e89b-12d3-a456-426614174000");
2769
+ * if (error !== null) {
2770
+ * console.error(`Failed to fetch: ${error}`);
2771
+ * return;
2772
+ * }
2773
+ * // Process data...
2774
+ * ```
2775
+ *
2776
+ * @internal
2777
+ */
2778
+ async function fetchByUuid(uuid, customFetch) {
2779
+ try {
2780
+ const parsedUuid = uuidSchema.parse(uuid);
2781
+ const response = await (customFetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre?uuid=${parsedUuid}&format=json&lang="*"`);
2782
+ if (!response.ok) throw new Error("Failed to fetch OCHRE data");
2783
+ const dataRaw = await response.json();
2784
+ if (!("ochre" in dataRaw)) throw new Error("Invalid OCHRE data: API response missing 'ochre' key");
2785
+ return [null, dataRaw];
2786
+ } catch (error) {
2787
+ return [error instanceof Error ? error.message : "Unknown error", null];
2788
+ }
2789
+ }
2790
+
2791
+ //#endregion
2792
+ //#region src/utils/fetchers/item.ts
2793
+ /**
2794
+ * Fetches and parses an OCHRE item from the OCHRE API
2795
+ *
2796
+ * @param uuid - The UUID of the OCHRE item to fetch
2797
+ * @returns Object containing the parsed OCHRE item and its metadata, or null if the fetch/parse fails
2798
+ *
2799
+ * @example
2800
+ * ```ts
2801
+ * const result = await fetchItem("123e4567-e89b-12d3-a456-426614174000");
2802
+ * if (result === null) {
2803
+ * console.error("Failed to fetch OCHRE item");
2804
+ * return;
2805
+ * }
2806
+ * const { metadata, belongsTo, item, category } = result;
2807
+ * console.log(`Fetched OCHRE item: ${item.identification.label} with category ${category}`);
2808
+ * ```
2809
+ *
2810
+ * Or, if you want to fetch a specific category, you can do so by passing the category as an argument:
2811
+ * ```ts
2812
+ * const result = await fetchItem("123e4567-e89b-12d3-a456-426614174000", "resource");
2813
+ * const { metadata, belongsTo, item, category } = result;
2814
+ * console.log(item.category); // "resource"
2815
+ * ```
2816
+ *
2817
+ * @remarks
2818
+ * The returned OCHRE item includes:
2819
+ * - Item metadata
2820
+ * - Item belongsTo information
2821
+ * - Item content
2822
+ * - Item category
2823
+ *
2824
+ * If the fetch/parse fails, the returned object will have an `error` property.
2825
+ */
2826
+ async function fetchItem(uuid, category, setCategory, customFetch) {
2827
+ try {
2828
+ const [error, data] = await fetchByUuid(uuid, customFetch);
2829
+ if (error !== null) throw new Error(error);
2830
+ const categoryKey = getItemCategory(Object.keys(data.ochre));
2831
+ let item;
2832
+ switch (categoryKey) {
2833
+ case "resource":
2834
+ if (!("resource" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'resource' key");
2835
+ item = parseResource(data.ochre.resource);
2836
+ break;
2837
+ case "spatialUnit":
2838
+ if (!("spatialUnit" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'spatialUnit' key");
2839
+ item = parseSpatialUnit(data.ochre.spatialUnit);
2840
+ break;
2841
+ case "concept":
2842
+ if (!("concept" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'concept' key");
2843
+ item = parseConcept(data.ochre.concept);
2844
+ break;
2845
+ case "period":
2846
+ if (!("period" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'period' key");
2847
+ item = parsePeriod(data.ochre.period);
2848
+ break;
2849
+ case "bibliography":
2850
+ if (!("bibliography" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'bibliography' key");
2851
+ item = parseBibliography(data.ochre.bibliography);
2852
+ break;
2853
+ case "person":
2854
+ if (!("person" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'person' key");
2855
+ item = parsePerson(data.ochre.person);
2856
+ break;
2857
+ case "propertyValue":
2858
+ if (!("propertyValue" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'propertyValue' key");
2859
+ item = parsePropertyValue(data.ochre.propertyValue);
2860
+ break;
2861
+ case "set":
2862
+ if (!("set" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'set' key");
2863
+ item = parseSet(data.ochre.set, setCategory);
2864
+ break;
2865
+ case "tree":
2866
+ if (!("tree" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'tree' key");
2867
+ item = parseTree(data.ochre.tree, category, setCategory);
2868
+ break;
2869
+ default: throw new Error("Invalid category");
2870
+ }
2871
+ return {
2872
+ error: null,
2873
+ metadata: parseMetadata(data.ochre.metadata),
2874
+ belongsTo: {
2875
+ uuid: data.ochre.uuidBelongsTo,
2876
+ abbreviation: parseFakeString(data.ochre.belongsTo)
2877
+ },
2878
+ item,
2879
+ category
2880
+ };
2881
+ } catch (error) {
2882
+ return {
2883
+ error: error instanceof Error ? error.message : "Unknown error",
2884
+ metadata: void 0,
2885
+ belongsTo: void 0,
2886
+ item: void 0,
2887
+ category: void 0
2888
+ };
2889
+ }
2890
+ }
2891
+
2892
+ //#endregion
2893
+ //#region src/utils/fetchers/property-query.ts
2894
+ const PROJECT_SCOPE = "0c0aae37-7246-495b-9547-e25dbf5b99a3";
2895
+ const BELONG_TO_COLLECTION_UUID = "30054cb2-909a-4f34-8db9-8fe7369d691d";
2896
+ const UNASSIGNED_UUID = "e28e29af-b663-c0ac-ceb6-11a688fca0dd";
2897
+ /**
2898
+ * Check if a string is a valid UUID
2899
+ * @param value - The string to check
2900
+ * @returns True if the string is a valid UUID, false otherwise
2901
+ */
2902
+ function isUUID(value) {
2903
+ return /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value);
2904
+ }
2905
+ /**
2906
+ * Schema for a single item in the OCHRE API response
2907
+ */
2908
+ const responseItemSchema = z.object({
2909
+ property: z.string().refine(isUUID),
2910
+ category: z.object({
2911
+ uuid: z.string().refine(isUUID),
2912
+ content: z.string()
2913
+ }),
2914
+ value: z.object({
2915
+ uuid: z.string().refine(isUUID).optional(),
2916
+ category: z.string().optional(),
2917
+ type: z.string().optional(),
2918
+ dataType: z.string(),
2919
+ publicationDateTime: z.iso.datetime().optional(),
2920
+ content: z.string().optional(),
2921
+ rawValue: z.string().optional()
2922
+ })
2923
+ });
2924
+ /**
2925
+ * Schema for the OCHRE API response
2926
+ */
2927
+ const responseSchema = z.object({ result: z.object({ item: z.union([responseItemSchema, z.array(responseItemSchema)]) }) });
2928
+ /**
2929
+ * Build an XQuery string to fetch properties from the OCHRE API
2930
+ * @param scopeUuids - An array of scope UUIDs to filter by
2931
+ * @param propertyUuids - An array of property UUIDs to fetch
2932
+ * @returns An XQuery string
2933
+ */
2934
+ function buildXQuery(scopeUuids, propertyUuids) {
2935
+ let collectionScopeFilter = "";
2936
+ if (scopeUuids.length > 0) collectionScopeFilter = `[properties/property[label/@uuid="${BELONG_TO_COLLECTION_UUID}"][value[${scopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ")}]]]`;
2937
+ const propertyFilters = propertyUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
2938
+ return `for $q in input()/ochre[@uuidBelongsTo="${PROJECT_SCOPE}"]/*${collectionScopeFilter}/properties//property[label[${propertyFilters}]]
2939
+ return <item>
2940
+ <property>{xs:string($q/label/@uuid)}</property>
2941
+ <value> {$q/*[2]/@*} {$q/*[2]/content[1]/string/text()} </value>
2942
+ <category> {$q/ancestor::node()[local-name(.)="properties"]/../@uuid} {local-name($q/ancestor::node()[local-name(.)="properties"]/../self::node())} </category>
2943
+ </item>`;
2944
+ }
2945
+ /**
2946
+ * Fetches and parses a property query from the OCHRE API
2947
+ *
2948
+ * @param scopeUuids - The scope UUIDs to filter by
2949
+ * @param propertyUuids - The property UUIDs to query by
2950
+ * @param customFetch - A custom fetch function to use instead of the default fetch
2951
+ * @returns The parsed property query or null if the fetch/parse fails
2952
+ *
2953
+ * @example
2954
+ * ```ts
2955
+ * const propertyQuery = await fetchPropertyQuery(["0c0aae37-7246-495b-9547-e25dbf5b99a3"], ["9c4da06b-f15e-40af-a747-0933eaf3587e"]);
2956
+ * if (propertyQuery === null) {
2957
+ * console.error("Failed to fetch property query");
2958
+ * return;
2959
+ * }
2960
+ * console.log(`Fetched property query: ${propertyQuery.item}`);
2961
+ * ```
2962
+ *
2963
+ * @remarks
2964
+ * The returned property query includes:
2965
+ * - Property items
2966
+ */
2967
+ async function fetchPropertyQuery(scopeUuids, propertyUuids, customFetch) {
2968
+ try {
2969
+ const xquery = buildXQuery(scopeUuids, propertyUuids);
2970
+ const response = await (customFetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(xquery)}&format=json`);
2971
+ if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`);
2972
+ const data = await response.json();
2973
+ const parsedResultRaw = responseSchema.parse(data);
2974
+ const parsedItems = Array.isArray(parsedResultRaw.result.item) ? parsedResultRaw.result.item : [parsedResultRaw.result.item];
2975
+ const items = {};
2976
+ for (const item of parsedItems) {
2977
+ const categoryUuid = item.category.uuid;
2978
+ const valueUuid = item.value.uuid;
2979
+ const valueContent = item.value.rawValue ?? item.value.content ?? "";
2980
+ if (valueContent in items) items[valueContent].resultUuids.push(categoryUuid);
2981
+ else items[valueContent] = {
2982
+ value: {
2983
+ uuid: valueUuid ?? null,
2984
+ category: item.value.category ?? null,
2985
+ type: item.value.type ?? null,
2986
+ dataType: item.value.dataType,
2987
+ publicationDateTime: item.value.publicationDateTime ?? null,
2988
+ content: item.value.rawValue ?? item.value.content ?? "",
2989
+ label: item.value.rawValue != null && item.value.content != null ? item.value.content : null
2990
+ },
2991
+ resultUuids: [categoryUuid]
2992
+ };
2993
+ }
2994
+ return {
2995
+ items: Object.values(items).filter((result) => result.value.uuid !== UNASSIGNED_UUID).toSorted((a, b) => {
2996
+ const aValue = a.value.label ?? a.value.content;
2997
+ const bValue = b.value.label ?? b.value.content;
2998
+ return aValue.localeCompare(bValue, "en-US");
2999
+ }),
3000
+ error: null
3001
+ };
3002
+ } catch (error) {
3003
+ console.error(error);
3004
+ return {
3005
+ items: null,
3006
+ error: error instanceof Error ? error.message : "Failed to fetch property query"
3007
+ };
3008
+ }
3009
+ }
3010
+
3011
+ //#endregion
3012
+ //#region src/utils/fetchers/uuid-metadata.ts
3013
+ /**
3014
+ * Fetches raw OCHRE metadata by UUID from the OCHRE API
3015
+ *
3016
+ * @param uuid - The UUID of the OCHRE item to fetch
3017
+ * @returns An object containing the OCHRE metadata or an error message
3018
+ *
3019
+ * @example
3020
+ * ```ts
3021
+ * const { item, error } = await fetchByUuidMetadata("123e4567-e89b-12d3-a456-426614174000");
3022
+ * if (error !== null) {
3023
+ * console.error(`Failed to fetch: ${error}`);
3024
+ * return;
3025
+ * }
3026
+ * // Process data...
3027
+ * ```
3028
+ */
3029
+ async function fetchByUuidMetadata(uuid, customFetch) {
3030
+ try {
3031
+ const parsedUuid = uuidSchema.parse(uuid);
3032
+ const response = await (customFetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(`for $q in input()/ochre[@uuid='${parsedUuid}']/metadata return ($q/item, $q/project)`)}&format=json`);
3033
+ if (!response.ok) throw new Error("Failed to fetch metadata");
3034
+ const data = await response.json();
3035
+ const projectIdentification = {
3036
+ ...parseIdentification(data.result.project.identification),
3037
+ website: data.result.project.identification.website ?? null
3038
+ };
3039
+ return {
3040
+ item: {
3041
+ item: {
3042
+ uuid,
3043
+ name: parseIdentification(data.result.item.identification).label,
3044
+ type: data.result.item.type
3045
+ },
3046
+ project: {
3047
+ name: projectIdentification.label,
3048
+ website: projectIdentification.website ?? null
3049
+ }
3050
+ },
3051
+ error: null
3052
+ };
3053
+ } catch (error) {
3054
+ return {
3055
+ item: null,
3056
+ error: error instanceof Error ? error.message : "Unknown error"
3057
+ };
3058
+ }
3059
+ }
3060
+
3061
+ //#endregion
3062
+ //#region src/utils/fetchers/website.ts
3063
+ const KNOWN_ABBREVIATIONS = {
3064
+ "uchicago-node": "60a1e386-7e53-4e14-b8cf-fb4ed953d57e",
3065
+ "uchicago-node-staging": "62b60a47-fad5-49d7-a06a-2fa059f6e79a",
3066
+ "guerrilla-television": "fad1e1bd-989d-4159-b195-4c32adc5cdc7",
3067
+ "mapping-chicagoland": "8db5e83e-0c06-48b7-b4ac-a060d9bb5689",
3068
+ "hannah-papanek": "20b2c919-021f-4774-b2c3-2f1ae5b910e7",
3069
+ mepa: "85ddaa5a-535b-4809-8714-855d2d812a3e",
3070
+ ssmc: "8ff977dd-d440-40f5-ad93-8ad7e2d39e74",
3071
+ "sosc-core-at-smart": "db26c953-9b2a-4691-a909-5e8726b531d7"
3072
+ };
3073
+ /**
3074
+ * Fetches and parses a website configuration from the OCHRE API
3075
+ *
3076
+ * @param abbreviation - The abbreviation identifier for the website
3077
+ * @returns The parsed website configuration or null if the fetch/parse fails
3078
+ *
3079
+ * @example
3080
+ * ```ts
3081
+ * const website = await fetchWebsite("guerrilla-television");
3082
+ * if (website === null) {
3083
+ * console.error("Failed to fetch website");
3084
+ * return;
3085
+ * }
3086
+ * console.log(`Fetched website: ${website.identification.label}`);
3087
+ * console.log(`Contains ${website.pages.length.toLocaleString()} pages`);
3088
+ * ```
3089
+ *
3090
+ * @remarks
3091
+ * The returned website configuration includes:
3092
+ * - Website metadata and identification
3093
+ * - Page structure and content
3094
+ * - Layout and styling properties
3095
+ * - Navigation configuration
3096
+ * - Sidebar elements
3097
+ * - Project information
3098
+ * - Creator details
3099
+ *
3100
+ * The abbreviation is case-insensitive and should match the website's configured abbreviation in OCHRE.
3101
+ */
3102
+ async function fetchWebsite(abbreviation, customFetch) {
3103
+ try {
3104
+ const uuid = KNOWN_ABBREVIATIONS[abbreviation.toLocaleLowerCase("en-US")];
3105
+ const response = await (customFetch ?? fetch)(uuid != null ? `https://ochre.lib.uchicago.edu/ochre?uuid=${uuid}&format=json` : `https://ochre.lib.uchicago.edu/ochre?xquery=for $q in input()/ochre[tree[@type='lesson'][identification/abbreviation='${abbreviation.toLocaleLowerCase("en-US")}']] return $q&format=json`);
3106
+ if (!response.ok) throw new Error("Failed to fetch website");
3107
+ const data = await response.json();
3108
+ const result = "result" in data && !Array.isArray(data.result) ? data.result : !("result" in data) ? data : null;
3109
+ if (result == null || !("tree" in result.ochre)) throw new Error("Failed to fetch website");
3110
+ const projectIdentification = result.ochre.metadata.project?.identification ? parseIdentification(result.ochre.metadata.project.identification) : null;
3111
+ return [null, parseWebsite(result.ochre.tree, projectIdentification?.label ?? "", result.ochre.metadata.project?.identification.website ?? null)];
3112
+ } catch (error) {
3113
+ console.error(error);
3114
+ return [error instanceof Error ? error.message : "Unknown error", null];
3115
+ }
3116
+ }
3117
+
3118
+ //#endregion
3119
+ export { fetchByUuidMetadata, fetchGallery, fetchItem, fetchPropertyQuery, fetchWebsite, filterProperties, getPropertyByLabel, getPropertyByUuid, getPropertyValueByLabel, getPropertyValueByUuid, getPropertyValuesByLabel, getPropertyValuesByUuid, getUniqueProperties, getUniquePropertyLabels };