@digitalculture/ochre-sdk 0.11.20 → 0.11.22

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 +1186 -0
  2. package/dist/index.js +3120 -0
  3. package/package.json +3 -3
package/dist/index.js ADDED
@@ -0,0 +1,3120 @@
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
+ links: person.links ? parseLinks(Array.isArray(person.links.link) ? person.links.link : [person.links.link]) : [],
876
+ events: person.events ? parseEvents(Array.isArray(person.events.event) ? person.events.event : [person.events.event]) : [],
877
+ properties: person.properties ? parseProperties(Array.isArray(person.properties.property) ? person.properties.property : [person.properties.property]) : [],
878
+ bibliographies: person.bibliographies ? parseBibliographies(Array.isArray(person.bibliographies.bibliography) ? person.bibliographies.bibliography : [person.bibliographies.bibliography]) : []
879
+ };
880
+ }
881
+ /**
882
+ * Parses raw person data into the standardized Person type
883
+ *
884
+ * @param persons - Array of raw person data from OCHRE format
885
+ * @returns Array of parsed Person objects
886
+ */
887
+ function parsePersons(persons) {
888
+ const returnPersons = [];
889
+ for (const person of persons) returnPersons.push(parsePerson(person));
890
+ return returnPersons;
891
+ }
892
+ /**
893
+ * Parses an array of raw links into standardized Link objects
894
+ *
895
+ * @param linkRaw - Raw OCHRE link
896
+ * @returns Parsed Link object
897
+ */
898
+ function parseLink(linkRaw) {
899
+ 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;
900
+ if (!links) throw new Error(`Invalid link provided: ${JSON.stringify(linkRaw, null, 2)}`);
901
+ const linksToParse = Array.isArray(links) ? links : [links];
902
+ const returnLinks = [];
903
+ for (const link of linksToParse) {
904
+ const returnLink = {
905
+ 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,
906
+ content: "content" in link ? link.content != null ? parseFakeString(link.content) : null : null,
907
+ href: "href" in link && link.href != null ? link.href : null,
908
+ fileFormat: "fileFormat" in link && link.fileFormat != null ? link.fileFormat : null,
909
+ fileSize: "fileSize" in link && link.fileSize != null ? link.fileSize : null,
910
+ uuid: link.uuid ?? null,
911
+ type: link.type ?? null,
912
+ identification: link.identification ? parseIdentification(link.identification) : null,
913
+ description: "description" in link && link.description != null ? parseStringContent(link.description) : null,
914
+ image: null,
915
+ bibliographies: "bibliography" in linkRaw ? parseBibliographies(Array.isArray(linkRaw.bibliography) ? linkRaw.bibliography : [linkRaw.bibliography]) : null,
916
+ publicationDateTime: link.publicationDateTime != null ? new Date(link.publicationDateTime) : null
917
+ };
918
+ if ("height" in link && link.height != null && link.width != null && link.heightPreview != null && link.widthPreview != null) returnLink.image = {
919
+ isInline: link.rend === "inline",
920
+ isPrimary: link.isPrimary ?? false,
921
+ heightPreview: link.heightPreview,
922
+ widthPreview: link.widthPreview,
923
+ height: link.height,
924
+ width: link.width
925
+ };
926
+ returnLinks.push(returnLink);
927
+ }
928
+ return returnLinks;
929
+ }
930
+ /**
931
+ * Parses an array of raw links into standardized Link objects
932
+ *
933
+ * @param links - Array of raw OCHRE links
934
+ * @returns Array of parsed Link objects
935
+ */
936
+ function parseLinks(links) {
937
+ const returnLinks = [];
938
+ for (const link of links) returnLinks.push(...parseLink(link));
939
+ return returnLinks;
940
+ }
941
+ /**
942
+ * Parses raw document content into a standardized Document structure
943
+ *
944
+ * @param document - Raw document content in OCHRE format
945
+ * @param language - Language code to use for content selection (defaults to "eng")
946
+ * @returns Parsed Document object with content and footnotes
947
+ */
948
+ function parseDocument(document, language = "eng") {
949
+ let returnString = "";
950
+ const documentWithLanguage = Array.isArray(document) ? document.find((doc) => doc.lang === language) : document;
951
+ if (typeof documentWithLanguage.string === "string" || typeof documentWithLanguage.string === "number" || typeof documentWithLanguage.string === "boolean") returnString += parseEmail(parseFakeString(documentWithLanguage.string));
952
+ else {
953
+ const documentItems = Array.isArray(documentWithLanguage.string) ? documentWithLanguage.string : [documentWithLanguage.string];
954
+ for (const item of documentItems) returnString += parseStringDocumentItem(item);
955
+ }
956
+ return returnString;
957
+ }
958
+ /**
959
+ * Parses raw image data into a standardized Image structure
960
+ *
961
+ * @param image - Raw image data in OCHRE format
962
+ * @returns Parsed Image object or null if invalid
963
+ */
964
+ function parseImage(image) {
965
+ return {
966
+ publicationDateTime: image.publicationDateTime != null ? new Date(image.publicationDateTime) : null,
967
+ identification: image.identification ? parseIdentification(image.identification) : null,
968
+ url: image.href ?? (image.htmlImgSrcPrefix == null && image.content != null ? parseFakeString(image.content) : null),
969
+ htmlPrefix: image.htmlImgSrcPrefix ?? null,
970
+ content: image.htmlImgSrcPrefix != null && image.content != null ? parseFakeString(image.content) : null,
971
+ widthPreview: image.widthPreview ?? null,
972
+ heightPreview: image.heightPreview ?? null,
973
+ width: image.width ?? null,
974
+ height: image.height ?? null
975
+ };
976
+ }
977
+ /**
978
+ * Parses raw notes into standardized Note objects
979
+ *
980
+ * @param notes - Array of raw notes in OCHRE format
981
+ * @param language - Language code for content selection (defaults to "eng")
982
+ * @returns Array of parsed Note objects
983
+ */
984
+ function parseNotes(notes, language = "eng") {
985
+ const returnNotes = [];
986
+ for (const note of notes) {
987
+ if (typeof note === "string") {
988
+ if (note === "") continue;
989
+ returnNotes.push({
990
+ number: -1,
991
+ title: null,
992
+ date: null,
993
+ authors: [],
994
+ content: note
995
+ });
996
+ continue;
997
+ }
998
+ let content = "";
999
+ const notesToParse = note.content != null ? Array.isArray(note.content) ? note.content : [note.content] : [];
1000
+ if (notesToParse.length === 0) continue;
1001
+ let noteWithLanguage = notesToParse.find((item) => item.lang === language);
1002
+ if (!noteWithLanguage) {
1003
+ noteWithLanguage = notesToParse[0];
1004
+ if (!noteWithLanguage) throw new Error(`Note does not have a valid content item: ${JSON.stringify(note, null, 2)}`);
1005
+ }
1006
+ if (typeof noteWithLanguage.string === "string" || typeof noteWithLanguage.string === "number" || typeof noteWithLanguage.string === "boolean") content = parseEmail(parseFakeString(noteWithLanguage.string));
1007
+ else content = parseEmail(parseDocument(noteWithLanguage));
1008
+ returnNotes.push({
1009
+ number: note.noteNo,
1010
+ title: noteWithLanguage.title != null ? parseFakeString(noteWithLanguage.title) : null,
1011
+ date: note.date ?? null,
1012
+ authors: note.authors ? parsePersons(Array.isArray(note.authors.author) ? note.authors.author : [note.authors.author]) : [],
1013
+ content
1014
+ });
1015
+ }
1016
+ return returnNotes;
1017
+ }
1018
+ /**
1019
+ * Parses raw coordinates data into a standardized Coordinates structure
1020
+ *
1021
+ * @param coordinates - Raw coordinates data in OCHRE format
1022
+ * @returns Parsed Coordinates object
1023
+ */
1024
+ function parseCoordinates(coordinates) {
1025
+ if (coordinates == null) return [];
1026
+ const returnCoordinates = [];
1027
+ const coordsToParse = Array.isArray(coordinates.coord) ? coordinates.coord : [coordinates.coord];
1028
+ for (const coord of coordsToParse) {
1029
+ const source = "source" in coord && coord.source ? coord.source.context === "self" ? {
1030
+ context: "self",
1031
+ uuid: coord.source.label.uuid,
1032
+ label: parseStringContent(coord.source.label)
1033
+ } : coord.source.context === "related" ? {
1034
+ context: "related",
1035
+ uuid: coord.source.label.uuid,
1036
+ label: parseStringContent(coord.source.label),
1037
+ value: parseStringContent(coord.source.value)
1038
+ } : {
1039
+ context: "inherited",
1040
+ uuid: coord.source.label.uuid,
1041
+ item: {
1042
+ uuid: coord.source.item.label.uuid,
1043
+ label: parseStringContent(coord.source.item.label)
1044
+ },
1045
+ label: parseStringContent(coord.source.label)
1046
+ } : null;
1047
+ switch (coord.type) {
1048
+ case "point":
1049
+ returnCoordinates.push({
1050
+ type: coord.type,
1051
+ latitude: coord.latitude,
1052
+ longitude: coord.longitude,
1053
+ altitude: coord.altitude ?? null,
1054
+ source
1055
+ });
1056
+ break;
1057
+ case "plane":
1058
+ returnCoordinates.push({
1059
+ type: coord.type,
1060
+ minimum: {
1061
+ latitude: coord.minimum.latitude,
1062
+ longitude: coord.minimum.longitude
1063
+ },
1064
+ maximum: {
1065
+ latitude: coord.maximum.latitude,
1066
+ longitude: coord.maximum.longitude
1067
+ },
1068
+ source
1069
+ });
1070
+ break;
1071
+ }
1072
+ }
1073
+ return returnCoordinates;
1074
+ }
1075
+ /**
1076
+ * Parses a raw observation into a standardized Observation structure
1077
+ *
1078
+ * @param observation - Raw observation data in OCHRE format
1079
+ * @returns Parsed Observation object
1080
+ */
1081
+ function parseObservation(observation) {
1082
+ return {
1083
+ number: observation.observationNo,
1084
+ date: observation.date ?? null,
1085
+ 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]) : [],
1086
+ notes: observation.notes ? parseNotes(Array.isArray(observation.notes.note) ? observation.notes.note : [observation.notes.note]) : [],
1087
+ links: observation.links ? parseLinks(Array.isArray(observation.links) ? observation.links : [observation.links]) : [],
1088
+ properties: observation.properties ? parseProperties(Array.isArray(observation.properties.property) ? observation.properties.property : [observation.properties.property]) : [],
1089
+ bibliographies: observation.bibliographies ? parseBibliographies(Array.isArray(observation.bibliographies.bibliography) ? observation.bibliographies.bibliography : [observation.bibliographies.bibliography]) : []
1090
+ };
1091
+ }
1092
+ /**
1093
+ * Parses an array of raw observations into standardized Observation objects
1094
+ *
1095
+ * @param observations - Array of raw observations in OCHRE format
1096
+ * @returns Array of parsed Observation objects
1097
+ */
1098
+ function parseObservations(observations) {
1099
+ const returnObservations = [];
1100
+ for (const observation of observations) returnObservations.push(parseObservation(observation));
1101
+ return returnObservations;
1102
+ }
1103
+ /**
1104
+ * Parses an array of raw events into standardized Event objects
1105
+ *
1106
+ * @param events - Array of raw events in OCHRE format
1107
+ * @returns Array of parsed Event objects
1108
+ */
1109
+ function parseEvents(events) {
1110
+ const returnEvents = [];
1111
+ for (const event of events) returnEvents.push({
1112
+ date: event.dateTime != null ? new Date(event.dateTime) : null,
1113
+ label: parseStringContent(event.label),
1114
+ location: event.location ? {
1115
+ uuid: event.location.uuid,
1116
+ content: parseStringContent(event.location)
1117
+ } : null,
1118
+ agent: event.agent ? {
1119
+ uuid: event.agent.uuid,
1120
+ content: parseStringContent(event.agent)
1121
+ } : null,
1122
+ comment: event.comment ? parseStringContent(event.comment) : null,
1123
+ value: event.value ? parseFakeString(event.value) : null
1124
+ });
1125
+ return returnEvents;
1126
+ }
1127
+ function parseProperty(property, language = "eng") {
1128
+ const values = ("value" in property && property.value ? Array.isArray(property.value) ? property.value : [property.value] : []).map((value) => {
1129
+ let content = null;
1130
+ let label = null;
1131
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1132
+ content = parseFakeString(value);
1133
+ return {
1134
+ content,
1135
+ label: null,
1136
+ dataType: "string",
1137
+ isUncertain: false,
1138
+ category: "value",
1139
+ type: null,
1140
+ uuid: null,
1141
+ publicationDateTime: null,
1142
+ unit: null,
1143
+ href: null,
1144
+ slug: null
1145
+ };
1146
+ } else {
1147
+ let parsedType = "string";
1148
+ if (value.dataType != null) {
1149
+ const { data, error } = propertyValueContentTypeSchema.safeParse(value.dataType);
1150
+ if (error) throw new Error(`Invalid property value content type: "${value.dataType}"`);
1151
+ parsedType = data;
1152
+ }
1153
+ switch (parsedType) {
1154
+ case "integer":
1155
+ case "decimal":
1156
+ case "time":
1157
+ if (value.rawValue != null) {
1158
+ content = Number(value.rawValue);
1159
+ label = value.content ? parseStringContent({ content: value.content }) : null;
1160
+ } else {
1161
+ content = Number(value.content);
1162
+ label = null;
1163
+ }
1164
+ break;
1165
+ case "boolean":
1166
+ if (value.rawValue != null) {
1167
+ content = value.rawValue === "true";
1168
+ label = value.content ? parseStringContent({ content: value.content }) : null;
1169
+ } else {
1170
+ content = value.content === true;
1171
+ label = null;
1172
+ }
1173
+ break;
1174
+ default:
1175
+ if ("slug" in value && value.slug != null) content = parseFakeString(value.slug);
1176
+ else if (value.content != null) if (value.rawValue != null) {
1177
+ content = parseFakeString(value.rawValue);
1178
+ label = value.content ? parseStringContent({ content: value.content }) : null;
1179
+ } else {
1180
+ content = parseStringContent({ content: value.content });
1181
+ label = null;
1182
+ }
1183
+ break;
1184
+ }
1185
+ return {
1186
+ content,
1187
+ dataType: parsedType,
1188
+ isUncertain: value.isUncertain ?? false,
1189
+ label,
1190
+ category: value.category ?? null,
1191
+ type: value.type ?? null,
1192
+ uuid: value.uuid ?? null,
1193
+ publicationDateTime: value.publicationDateTime != null ? new Date(value.publicationDateTime) : null,
1194
+ unit: value.unit ?? null,
1195
+ href: value.href ?? null,
1196
+ slug: value.slug ?? null
1197
+ };
1198
+ }
1199
+ });
1200
+ return {
1201
+ uuid: property.label.uuid,
1202
+ label: parseStringContent(property.label, language).replace(/\s*\.{3}$/, "").trim(),
1203
+ values,
1204
+ comment: property.comment != null ? parseStringContent(property.comment) : null,
1205
+ properties: property.property ? parseProperties(Array.isArray(property.property) ? property.property : [property.property]) : []
1206
+ };
1207
+ }
1208
+ /**
1209
+ * Parses raw properties into standardized Property objects
1210
+ *
1211
+ * @param properties - Array of raw properties in OCHRE format
1212
+ * @param language - Language code for content selection (defaults to "eng")
1213
+ * @returns Array of parsed Property objects
1214
+ */
1215
+ function parseProperties(properties, language = "eng") {
1216
+ const returnProperties = [];
1217
+ for (const property of properties) returnProperties.push(parseProperty(property, language));
1218
+ return returnProperties;
1219
+ }
1220
+ /**
1221
+ * Parses raw interpretations into standardized Interpretation objects
1222
+ *
1223
+ * @param interpretations - Array of raw interpretations in OCHRE format
1224
+ * @returns Array of parsed Interpretation objects
1225
+ */
1226
+ function parseInterpretations(interpretations) {
1227
+ const returnInterpretations = [];
1228
+ for (const interpretation of interpretations) returnInterpretations.push({
1229
+ date: interpretation.date,
1230
+ number: interpretation.interpretationNo,
1231
+ properties: interpretation.properties ? parseProperties(Array.isArray(interpretation.properties.property) ? interpretation.properties.property : [interpretation.properties.property]) : [],
1232
+ bibliographies: interpretation.bibliographies ? parseBibliographies(Array.isArray(interpretation.bibliographies.bibliography) ? interpretation.bibliographies.bibliography : [interpretation.bibliographies.bibliography]) : []
1233
+ });
1234
+ return returnInterpretations;
1235
+ }
1236
+ /**
1237
+ * Parses raw image map data into a standardized ImageMap structure
1238
+ *
1239
+ * @param imageMap - Raw image map data in OCHRE format
1240
+ * @returns Parsed ImageMap object
1241
+ */
1242
+ function parseImageMap(imageMap) {
1243
+ const returnImageMap = {
1244
+ area: [],
1245
+ width: imageMap.width,
1246
+ height: imageMap.height
1247
+ };
1248
+ const imageMapAreasToParse = Array.isArray(imageMap.area) ? imageMap.area : [imageMap.area];
1249
+ for (const area of imageMapAreasToParse) returnImageMap.area.push({
1250
+ uuid: area.uuid,
1251
+ publicationDateTime: area.publicationDateTime != null ? new Date(area.publicationDateTime) : null,
1252
+ type: area.type,
1253
+ title: parseFakeString(area.title),
1254
+ shape: area.shape === "rect" ? "rectangle" : area.shape === "circle" ? "circle" : "polygon",
1255
+ coords: area.coords.split(",").map((coord) => Number.parseInt(coord)),
1256
+ slug: area.slug ? parseFakeString(area.slug) : null
1257
+ });
1258
+ return returnImageMap;
1259
+ }
1260
+ /**
1261
+ * Parses raw period data into a standardized Period structure
1262
+ *
1263
+ * @param period - Raw period data in OCHRE format
1264
+ * @returns Parsed Period object
1265
+ */
1266
+ function parsePeriod(period) {
1267
+ return {
1268
+ uuid: period.uuid,
1269
+ category: "period",
1270
+ publicationDateTime: period.publicationDateTime != null ? new Date(period.publicationDateTime) : null,
1271
+ type: period.type ?? null,
1272
+ number: period.n ?? null,
1273
+ identification: parseIdentification(period.identification),
1274
+ description: period.description ? parseStringContent(period.description) : null
1275
+ };
1276
+ }
1277
+ /**
1278
+ * Parses an array of raw periods into standardized Period objects
1279
+ *
1280
+ * @param periods - Array of raw periods in OCHRE format
1281
+ * @returns Array of parsed Period objects
1282
+ */
1283
+ function parsePeriods(periods) {
1284
+ const returnPeriods = [];
1285
+ for (const period of periods) returnPeriods.push(parsePeriod(period));
1286
+ return returnPeriods;
1287
+ }
1288
+ /**
1289
+ * Parses raw bibliography data into a standardized Bibliography structure
1290
+ *
1291
+ * @param bibliography - Raw bibliography data in OCHRE format
1292
+ * @returns Parsed Bibliography object
1293
+ */
1294
+ function parseBibliography(bibliography) {
1295
+ let resource = null;
1296
+ if (bibliography.source?.resource) resource = {
1297
+ uuid: bibliography.source.resource.uuid,
1298
+ publicationDateTime: bibliography.source.resource.publicationDateTime ? new Date(bibliography.source.resource.publicationDateTime) : null,
1299
+ type: bibliography.source.resource.type,
1300
+ identification: parseIdentification(bibliography.source.resource.identification)
1301
+ };
1302
+ let shortCitation = null;
1303
+ let longCitation = null;
1304
+ if (bibliography.citationFormatSpan) try {
1305
+ shortCitation = JSON.parse(`"${bibliography.citationFormatSpan}"`).replaceAll("&lt;", "<").replaceAll("&gt;", ">");
1306
+ } catch {
1307
+ shortCitation = bibliography.citationFormatSpan;
1308
+ }
1309
+ if (bibliography.referenceFormatDiv) try {
1310
+ longCitation = JSON.parse(`"${bibliography.referenceFormatDiv}"`).replaceAll("&lt;", "<").replaceAll("&gt;", ">");
1311
+ } catch {
1312
+ longCitation = bibliography.referenceFormatDiv;
1313
+ }
1314
+ return {
1315
+ uuid: bibliography.uuid ?? null,
1316
+ zoteroId: bibliography.zoteroId ?? null,
1317
+ category: "bibliography",
1318
+ publicationDateTime: bibliography.publicationDateTime != null ? new Date(bibliography.publicationDateTime) : null,
1319
+ type: bibliography.type ?? null,
1320
+ number: bibliography.n ?? null,
1321
+ identification: bibliography.identification ? parseIdentification(bibliography.identification) : null,
1322
+ projectIdentification: bibliography.project?.identification ? parseIdentification(bibliography.project.identification) : null,
1323
+ context: bibliography.context ? parseContext(bibliography.context) : null,
1324
+ citation: {
1325
+ details: bibliography.citationDetails ?? null,
1326
+ format: bibliography.citationFormat ?? null,
1327
+ short: shortCitation,
1328
+ long: longCitation
1329
+ },
1330
+ publicationInfo: {
1331
+ publishers: bibliography.publicationInfo?.publishers ? parsePersons(Array.isArray(bibliography.publicationInfo.publishers.publishers.person) ? bibliography.publicationInfo.publishers.publishers.person : [bibliography.publicationInfo.publishers.publishers.person]) : [],
1332
+ startDate: bibliography.publicationInfo?.startDate ? new Date(bibliography.publicationInfo.startDate.year, bibliography.publicationInfo.startDate.month, bibliography.publicationInfo.startDate.day) : null
1333
+ },
1334
+ entryInfo: bibliography.entryInfo ? {
1335
+ startIssue: parseFakeString(bibliography.entryInfo.startIssue),
1336
+ startVolume: parseFakeString(bibliography.entryInfo.startVolume)
1337
+ } : null,
1338
+ source: {
1339
+ resource,
1340
+ documentUrl: bibliography.sourceDocument ? `https://ochre.lib.uchicago.edu/ochre?uuid=${bibliography.sourceDocument.uuid}&load` : null
1341
+ },
1342
+ periods: bibliography.periods ? parsePeriods(Array.isArray(bibliography.periods.period) ? bibliography.periods.period : [bibliography.periods.period]) : [],
1343
+ authors: bibliography.authors ? parsePersons(Array.isArray(bibliography.authors.person) ? bibliography.authors.person : [bibliography.authors.person]) : [],
1344
+ properties: bibliography.properties ? parseProperties(Array.isArray(bibliography.properties.property) ? bibliography.properties.property : [bibliography.properties.property]) : []
1345
+ };
1346
+ }
1347
+ /**
1348
+ * Parses an array of raw bibliographies into standardized Bibliography objects
1349
+ *
1350
+ * @param bibliographies - Array of raw bibliographies in OCHRE format
1351
+ * @returns Array of parsed Bibliography objects
1352
+ */
1353
+ function parseBibliographies(bibliographies) {
1354
+ const returnBibliographies = [];
1355
+ for (const bibliography of bibliographies) returnBibliographies.push(parseBibliography(bibliography));
1356
+ return returnBibliographies;
1357
+ }
1358
+ /**
1359
+ * Parses raw property value data into a standardized PropertyValue structure
1360
+ *
1361
+ * @param propertyValue - Raw property value data in OCHRE format
1362
+ * @returns Parsed PropertyValue object
1363
+ */
1364
+ function parsePropertyValue(propertyValue) {
1365
+ return {
1366
+ uuid: propertyValue.uuid,
1367
+ category: "propertyValue",
1368
+ number: propertyValue.n,
1369
+ publicationDateTime: propertyValue.publicationDateTime ? new Date(propertyValue.publicationDateTime) : null,
1370
+ context: propertyValue.context ? parseContext(propertyValue.context) : null,
1371
+ availability: propertyValue.availability ? parseLicense(propertyValue.availability) : null,
1372
+ identification: parseIdentification(propertyValue.identification),
1373
+ date: propertyValue.date ?? null,
1374
+ creators: propertyValue.creators ? parsePersons(Array.isArray(propertyValue.creators.creator) ? propertyValue.creators.creator : [propertyValue.creators.creator]) : [],
1375
+ description: propertyValue.description ? typeof propertyValue.description === "string" || typeof propertyValue.description === "number" || typeof propertyValue.description === "boolean" ? parseFakeString(propertyValue.description) : parseStringContent(propertyValue.description) : "",
1376
+ notes: propertyValue.notes ? parseNotes(Array.isArray(propertyValue.notes.note) ? propertyValue.notes.note : [propertyValue.notes.note]) : [],
1377
+ links: propertyValue.links ? parseLinks(Array.isArray(propertyValue.links) ? propertyValue.links : [propertyValue.links]) : []
1378
+ };
1379
+ }
1380
+ /**
1381
+ * Parses an array of raw property values into standardized PropertyValue objects
1382
+ *
1383
+ * @param propertyValues - Array of raw property values in OCHRE format
1384
+ * @returns Array of parsed PropertyValue objects
1385
+ */
1386
+ function parsePropertyValues(propertyValues) {
1387
+ const returnPropertyValues = [];
1388
+ for (const propertyValue of propertyValues) returnPropertyValues.push(parsePropertyValue(propertyValue));
1389
+ return returnPropertyValues;
1390
+ }
1391
+ /**
1392
+ * Parses a raw tree structure into a standardized Tree object
1393
+ *
1394
+ * @param tree - Raw tree data in OCHRE format
1395
+ * @returns Parsed Tree object or null if invalid
1396
+ */
1397
+ function parseTree(tree, itemCategory, itemSubCategory) {
1398
+ if (typeof tree.items === "string") throw new TypeError("Invalid OCHRE data: Tree has no items");
1399
+ let creators = [];
1400
+ if (tree.creators) creators = parsePersons(Array.isArray(tree.creators.creator) ? tree.creators.creator : [tree.creators.creator]);
1401
+ let date = null;
1402
+ if (tree.date != null) date = tree.date;
1403
+ const parsedItemCategory = itemSubCategory ?? getItemCategory(Object.keys(tree.items));
1404
+ let items = [];
1405
+ switch (parsedItemCategory) {
1406
+ case "resource":
1407
+ if (!("resource" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no resources");
1408
+ items = parseResources(Array.isArray(tree.items.resource) ? tree.items.resource : [tree.items.resource]);
1409
+ break;
1410
+ case "spatialUnit":
1411
+ if (!("spatialUnit" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no spatial units");
1412
+ items = parseSpatialUnits(Array.isArray(tree.items.spatialUnit) ? tree.items.spatialUnit : [tree.items.spatialUnit]);
1413
+ break;
1414
+ case "concept":
1415
+ if (!("concept" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no concepts");
1416
+ items = parseConcepts(Array.isArray(tree.items.concept) ? tree.items.concept : [tree.items.concept]);
1417
+ break;
1418
+ case "period":
1419
+ if (!("period" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no periods");
1420
+ items = parsePeriods(Array.isArray(tree.items.period) ? tree.items.period : [tree.items.period]);
1421
+ break;
1422
+ case "bibliography":
1423
+ if (!("bibliography" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no bibliographies");
1424
+ items = parseBibliographies(Array.isArray(tree.items.bibliography) ? tree.items.bibliography : [tree.items.bibliography]);
1425
+ break;
1426
+ case "person":
1427
+ if (!("person" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no persons");
1428
+ items = parsePersons(Array.isArray(tree.items.person) ? tree.items.person : [tree.items.person]);
1429
+ break;
1430
+ case "propertyValue":
1431
+ if (!("propertyValue" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no property values");
1432
+ items = parsePropertyValues(Array.isArray(tree.items.propertyValue) ? tree.items.propertyValue : [tree.items.propertyValue]);
1433
+ break;
1434
+ case "set": {
1435
+ if (!("set" in tree.items)) throw new Error("Invalid OCHRE data: Tree has no sets");
1436
+ const setItems = [];
1437
+ for (const item of Array.isArray(tree.items.set) ? tree.items.set : [tree.items.set]) setItems.push(parseSet(item, itemSubCategory));
1438
+ items = setItems;
1439
+ break;
1440
+ }
1441
+ default: throw new Error("Invalid OCHRE data: Tree has no items or is malformed");
1442
+ }
1443
+ return {
1444
+ uuid: tree.uuid,
1445
+ category: "tree",
1446
+ publicationDateTime: new Date(tree.publicationDateTime),
1447
+ identification: parseIdentification(tree.identification),
1448
+ creators,
1449
+ license: parseLicense(tree.availability),
1450
+ date,
1451
+ type: tree.type,
1452
+ number: tree.n,
1453
+ items,
1454
+ properties: tree.properties ? parseProperties(Array.isArray(tree.properties.property) ? tree.properties.property : [tree.properties.property]) : []
1455
+ };
1456
+ }
1457
+ /**
1458
+ * Parses raw set data into a standardized Set structure
1459
+ *
1460
+ * @param set - Raw set data in OCHRE format
1461
+ * @returns Parsed Set object
1462
+ */
1463
+ function parseSet(set, itemCategory) {
1464
+ if (typeof set.items === "string") throw new TypeError("Invalid OCHRE data: Set has no items");
1465
+ const parsedItemCategory = itemCategory ?? getItemCategory(Object.keys(set.items));
1466
+ let items = [];
1467
+ switch (parsedItemCategory) {
1468
+ case "resource":
1469
+ if (!("resource" in set.items)) throw new Error("Invalid OCHRE data: Set has no resources");
1470
+ items = parseResources(Array.isArray(set.items.resource) ? set.items.resource : [set.items.resource]);
1471
+ break;
1472
+ case "spatialUnit":
1473
+ if (!("spatialUnit" in set.items)) throw new Error("Invalid OCHRE data: Set has no spatial units");
1474
+ items = parseSpatialUnits(Array.isArray(set.items.spatialUnit) ? set.items.spatialUnit : [set.items.spatialUnit]);
1475
+ break;
1476
+ case "concept":
1477
+ if (!("concept" in set.items)) throw new Error("Invalid OCHRE data: Set has no concepts");
1478
+ items = parseConcepts(Array.isArray(set.items.concept) ? set.items.concept : [set.items.concept]);
1479
+ break;
1480
+ case "period":
1481
+ if (!("period" in set.items)) throw new Error("Invalid OCHRE data: Set has no periods");
1482
+ items = parsePeriods(Array.isArray(set.items.period) ? set.items.period : [set.items.period]);
1483
+ break;
1484
+ case "bibliography":
1485
+ if (!("bibliography" in set.items)) throw new Error("Invalid OCHRE data: Set has no bibliographies");
1486
+ items = parseBibliographies(Array.isArray(set.items.bibliography) ? set.items.bibliography : [set.items.bibliography]);
1487
+ break;
1488
+ case "person":
1489
+ if (!("person" in set.items)) throw new Error("Invalid OCHRE data: Set has no persons");
1490
+ items = parsePersons(Array.isArray(set.items.person) ? set.items.person : [set.items.person]);
1491
+ break;
1492
+ case "propertyValue":
1493
+ if (!("propertyValue" in set.items)) throw new Error("Invalid OCHRE data: Set has no property values");
1494
+ items = parsePropertyValues(Array.isArray(set.items.propertyValue) ? set.items.propertyValue : [set.items.propertyValue]);
1495
+ break;
1496
+ default: throw new Error("Invalid OCHRE data: Set has no items or is malformed");
1497
+ }
1498
+ return {
1499
+ uuid: set.uuid,
1500
+ category: "set",
1501
+ itemCategory,
1502
+ publicationDateTime: set.publicationDateTime ? new Date(set.publicationDateTime) : null,
1503
+ date: set.date ?? null,
1504
+ license: parseLicense(set.availability),
1505
+ identification: parseIdentification(set.identification),
1506
+ isSuppressingBlanks: set.suppressBlanks ?? false,
1507
+ description: set.description ? [
1508
+ "string",
1509
+ "number",
1510
+ "boolean"
1511
+ ].includes(typeof set.description) ? parseFakeString(set.description) : parseStringContent(set.description) : "",
1512
+ creators: set.creators ? parsePersons(Array.isArray(set.creators.creator) ? set.creators.creator : [set.creators.creator]) : [],
1513
+ type: set.type,
1514
+ number: set.n,
1515
+ items
1516
+ };
1517
+ }
1518
+ /**
1519
+ * Parses raw resource data into a standardized Resource structure
1520
+ *
1521
+ * @param resource - Raw resource data in OCHRE format
1522
+ * @returns Parsed Resource object
1523
+ */
1524
+ function parseResource(resource) {
1525
+ return {
1526
+ uuid: resource.uuid,
1527
+ category: "resource",
1528
+ publicationDateTime: resource.publicationDateTime ? new Date(resource.publicationDateTime) : null,
1529
+ type: resource.type,
1530
+ number: resource.n,
1531
+ fileFormat: resource.fileFormat ?? null,
1532
+ fileSize: resource.fileSize ?? null,
1533
+ context: "context" in resource && resource.context ? parseContext(resource.context) : null,
1534
+ license: "availability" in resource && resource.availability ? parseLicense(resource.availability) : null,
1535
+ copyright: "copyright" in resource && resource.copyright != null ? parseStringContent(resource.copyright) : null,
1536
+ watermark: "watermark" in resource && resource.watermark != null ? parseStringContent(resource.watermark) : null,
1537
+ identification: parseIdentification(resource.identification),
1538
+ date: resource.date ?? null,
1539
+ image: resource.image ? parseImage(resource.image) : null,
1540
+ creators: resource.creators ? parsePersons(Array.isArray(resource.creators.creator) ? resource.creators.creator : [resource.creators.creator]) : [],
1541
+ notes: resource.notes ? parseNotes(Array.isArray(resource.notes.note) ? resource.notes.note : [resource.notes.note]) : [],
1542
+ description: resource.description ? [
1543
+ "string",
1544
+ "number",
1545
+ "boolean"
1546
+ ].includes(typeof resource.description) ? parseFakeString(resource.description) : parseStringContent(resource.description) : "",
1547
+ coordinates: resource.coordinates ? parseCoordinates(resource.coordinates) : [],
1548
+ document: resource.document && "content" in resource.document ? parseDocument(resource.document.content) : null,
1549
+ href: resource.href ?? null,
1550
+ imageMap: resource.imagemap ? parseImageMap(resource.imagemap) : null,
1551
+ periods: resource.periods ? parsePeriods(Array.isArray(resource.periods.period) ? resource.periods.period : [resource.periods.period]) : [],
1552
+ links: resource.links ? parseLinks(Array.isArray(resource.links) ? resource.links : [resource.links]) : [],
1553
+ reverseLinks: resource.reverseLinks ? parseLinks(Array.isArray(resource.reverseLinks) ? resource.reverseLinks : [resource.reverseLinks]) : [],
1554
+ properties: resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : [],
1555
+ bibliographies: resource.bibliographies ? parseBibliographies(Array.isArray(resource.bibliographies.bibliography) ? resource.bibliographies.bibliography : [resource.bibliographies.bibliography]) : [],
1556
+ resources: resource.resource ? parseResources(Array.isArray(resource.resource) ? resource.resource : [resource.resource]) : []
1557
+ };
1558
+ }
1559
+ /**
1560
+ * Parses raw resource data into a standardized Resource structure
1561
+ *
1562
+ * @param resources - Raw resource data in OCHRE format
1563
+ * @returns Parsed Resource object
1564
+ */
1565
+ function parseResources(resources) {
1566
+ const returnResources = [];
1567
+ const resourcesToParse = Array.isArray(resources) ? resources : [resources];
1568
+ for (const resource of resourcesToParse) returnResources.push(parseResource(resource));
1569
+ return returnResources;
1570
+ }
1571
+ /**
1572
+ * Parses raw spatial units into standardized SpatialUnit objects
1573
+ *
1574
+ * @param spatialUnit - Raw spatial unit in OCHRE format
1575
+ * @returns Parsed SpatialUnit object
1576
+ */
1577
+ function parseSpatialUnit(spatialUnit) {
1578
+ return {
1579
+ uuid: spatialUnit.uuid,
1580
+ category: "spatialUnit",
1581
+ publicationDateTime: spatialUnit.publicationDateTime != null ? new Date(spatialUnit.publicationDateTime) : null,
1582
+ number: spatialUnit.n,
1583
+ context: "context" in spatialUnit && spatialUnit.context ? parseContext(spatialUnit.context) : null,
1584
+ license: "availability" in spatialUnit && spatialUnit.availability ? parseLicense(spatialUnit.availability) : null,
1585
+ identification: parseIdentification(spatialUnit.identification),
1586
+ image: spatialUnit.image ? parseImage(spatialUnit.image) : null,
1587
+ description: spatialUnit.description ? [
1588
+ "string",
1589
+ "number",
1590
+ "boolean"
1591
+ ].includes(typeof spatialUnit.description) ? parseFakeString(spatialUnit.description) : parseStringContent(spatialUnit.description) : "",
1592
+ coordinates: parseCoordinates(spatialUnit.coordinates),
1593
+ mapData: spatialUnit.mapData ?? null,
1594
+ observations: "observations" in spatialUnit && spatialUnit.observations ? parseObservations(Array.isArray(spatialUnit.observations.observation) ? spatialUnit.observations.observation : [spatialUnit.observations.observation]) : spatialUnit.observation ? [parseObservation(spatialUnit.observation)] : [],
1595
+ events: "events" in spatialUnit && spatialUnit.events ? parseEvents(Array.isArray(spatialUnit.events.event) ? spatialUnit.events.event : [spatialUnit.events.event]) : [],
1596
+ properties: "properties" in spatialUnit && spatialUnit.properties ? parseProperties(Array.isArray(spatialUnit.properties.property) ? spatialUnit.properties.property : [spatialUnit.properties.property]) : [],
1597
+ bibliographies: spatialUnit.bibliographies ? parseBibliographies(Array.isArray(spatialUnit.bibliographies.bibliography) ? spatialUnit.bibliographies.bibliography : [spatialUnit.bibliographies.bibliography]) : []
1598
+ };
1599
+ }
1600
+ /**
1601
+ * Parses an array of raw spatial units into standardized SpatialUnit objects
1602
+ *
1603
+ * @param spatialUnits - Array of raw spatial units in OCHRE format
1604
+ * @returns Array of parsed SpatialUnit objects
1605
+ */
1606
+ function parseSpatialUnits(spatialUnits) {
1607
+ const returnSpatialUnits = [];
1608
+ const spatialUnitsToParse = Array.isArray(spatialUnits) ? spatialUnits : [spatialUnits];
1609
+ for (const spatialUnit of spatialUnitsToParse) returnSpatialUnits.push(parseSpatialUnit(spatialUnit));
1610
+ return returnSpatialUnits;
1611
+ }
1612
+ /**
1613
+ * Parses a raw concept into a standardized Concept object
1614
+ *
1615
+ * @param concept - Raw concept data in OCHRE format
1616
+ * @returns Parsed Concept object
1617
+ */
1618
+ function parseConcept(concept) {
1619
+ return {
1620
+ uuid: concept.uuid,
1621
+ category: "concept",
1622
+ publicationDateTime: concept.publicationDateTime ? new Date(concept.publicationDateTime) : null,
1623
+ number: concept.n,
1624
+ license: "availability" in concept && concept.availability ? parseLicense(concept.availability) : null,
1625
+ context: "context" in concept && concept.context ? parseContext(concept.context) : null,
1626
+ identification: parseIdentification(concept.identification),
1627
+ image: concept.image ? parseImage(concept.image) : null,
1628
+ description: concept.description ? parseStringContent(concept.description) : null,
1629
+ interpretations: concept.interpretations ? parseInterpretations(Array.isArray(concept.interpretations.interpretation) ? concept.interpretations.interpretation : [concept.interpretations.interpretation]) : [],
1630
+ properties: concept.properties ? parseProperties(Array.isArray(concept.properties.property) ? concept.properties.property : [concept.properties.property]) : [],
1631
+ bibliographies: concept.bibliographies ? parseBibliographies(Array.isArray(concept.bibliographies.bibliography) ? concept.bibliographies.bibliography : [concept.bibliographies.bibliography]) : []
1632
+ };
1633
+ }
1634
+ /**
1635
+ * Parses raw webpage resources into standardized WebElement or Webpage objects
1636
+ *
1637
+ * @param webpageResources - Array of raw webpage resources in OCHRE format
1638
+ * @param type - Type of resource to parse ("element" or "page")
1639
+ * @returns Array of parsed WebElement or Webpage objects
1640
+ */
1641
+ const parseWebpageResources = (webpageResources, type) => {
1642
+ const returnElements = [];
1643
+ for (const resource of webpageResources) {
1644
+ 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;
1645
+ switch (type) {
1646
+ case "element": {
1647
+ const element = parseWebElement(resource);
1648
+ returnElements.push(element);
1649
+ break;
1650
+ }
1651
+ case "page": {
1652
+ const webpage = parseWebpage(resource);
1653
+ if (webpage) returnElements.push(webpage);
1654
+ break;
1655
+ }
1656
+ case "block": {
1657
+ const block = parseWebBlock(resource);
1658
+ if (block) returnElements.push(block);
1659
+ break;
1660
+ }
1661
+ }
1662
+ }
1663
+ return returnElements;
1664
+ };
1665
+ /**
1666
+ * Parses raw concept data into standardized Concept objects
1667
+ *
1668
+ * @param concepts - Array of raw concept data in OCHRE format
1669
+ * @returns Array of parsed Concept objects
1670
+ */
1671
+ function parseConcepts(concepts) {
1672
+ const returnConcepts = [];
1673
+ const conceptsToParse = Array.isArray(concepts) ? concepts : [concepts];
1674
+ for (const concept of conceptsToParse) returnConcepts.push(parseConcept(concept));
1675
+ return returnConcepts;
1676
+ }
1677
+ /**
1678
+ * Parses raw web element properties into a standardized WebElementComponent structure
1679
+ *
1680
+ * @param componentProperty - Raw component property data in OCHRE format
1681
+ * @param elementResource - Raw element resource data in OCHRE format
1682
+ * @returns Parsed WebElementComponent object
1683
+ */
1684
+ function parseWebElementProperties(componentProperty, elementResource) {
1685
+ const unparsedComponentName = componentProperty.values[0].content;
1686
+ const { data: componentName } = componentSchema.safeParse(unparsedComponentName);
1687
+ const properties = { component: componentName };
1688
+ const links = elementResource.links ? parseLinks(Array.isArray(elementResource.links) ? elementResource.links : [elementResource.links]) : [];
1689
+ switch (componentName) {
1690
+ case "annotated-document": {
1691
+ const documentLink = links.find((link) => link.type === "internalDocument");
1692
+ if (!documentLink) throw new Error(`Document link not found for the following component: “${componentName}”`);
1693
+ properties.documentId = documentLink.uuid;
1694
+ break;
1695
+ }
1696
+ case "annotated-image": {
1697
+ const imageLinks = links.filter((link) => link.type === "image" || link.type === "IIIF");
1698
+ if (imageLinks.length === 0) throw new Error(`Image link not found for the following component: “${componentName}”`);
1699
+ const isFilterDisplayed = getPropertyValueByLabel(componentProperty.properties, "filter-displayed") === true;
1700
+ const isOptionsDisplayed = getPropertyValueByLabel(componentProperty.properties, "options-displayed") !== false;
1701
+ const isAnnotationHighlightsDisplayed = getPropertyValueByLabel(componentProperty.properties, "annotation-highlights-displayed") !== false;
1702
+ const isAnnotationTooltipsDisplayed = getPropertyValueByLabel(componentProperty.properties, "annotation-tooltips-displayed") !== false;
1703
+ properties.imageUuid = imageLinks[0].uuid;
1704
+ properties.isFilterDisplayed = isFilterDisplayed;
1705
+ properties.isOptionsDisplayed = isOptionsDisplayed;
1706
+ properties.isAnnotationHighlightsDisplayed = isAnnotationHighlightsDisplayed;
1707
+ properties.isAnnotationTooltipsDisplayed = isAnnotationTooltipsDisplayed;
1708
+ break;
1709
+ }
1710
+ case "audio-player": {
1711
+ const audioLink = links.find((link) => link.type === "audio");
1712
+ if (!audioLink) throw new Error(`Audio link not found for the following component: “${componentName}”`);
1713
+ let isSpeedControlsDisplayed = false;
1714
+ const isSpeedControlsDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "speed-controls-displayed");
1715
+ if (isSpeedControlsDisplayedProperty !== null) isSpeedControlsDisplayed = isSpeedControlsDisplayedProperty === true;
1716
+ let isVolumeControlsDisplayed = false;
1717
+ const isVolumeControlsDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "volume-controls-displayed");
1718
+ if (isVolumeControlsDisplayedProperty !== null) isVolumeControlsDisplayed = isVolumeControlsDisplayedProperty === true;
1719
+ let isSeekBarDisplayed = false;
1720
+ const isSeekBarDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "seek-bar-displayed");
1721
+ if (isSeekBarDisplayedProperty !== null) isSeekBarDisplayed = isSeekBarDisplayedProperty === true;
1722
+ properties.audioId = audioLink.uuid;
1723
+ properties.isSpeedControlsDisplayed = isSpeedControlsDisplayed;
1724
+ properties.isVolumeControlsDisplayed = isVolumeControlsDisplayed;
1725
+ properties.isSeekBarDisplayed = isSeekBarDisplayed;
1726
+ break;
1727
+ }
1728
+ case "bibliography": {
1729
+ const itemLinks = links.filter((link) => link.category !== "bibliography");
1730
+ const bibliographyLink = links.find((link) => link.category === "bibliography");
1731
+ if (itemLinks.length === 0 && bibliographyLink?.bibliographies == null) throw new Error(`No links found for the following component: “${componentName}”`);
1732
+ let layout = getPropertyValueByLabel(componentProperty.properties, "layout");
1733
+ layout ??= "long";
1734
+ let isSourceDocumentDisplayed = true;
1735
+ const isSourceDocumentDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "source-document-displayed");
1736
+ if (isSourceDocumentDisplayedProperty !== null) isSourceDocumentDisplayed = isSourceDocumentDisplayedProperty === true;
1737
+ properties.itemUuids = itemLinks.map((link) => link.uuid).filter((uuid) => uuid !== null);
1738
+ properties.bibliographies = bibliographyLink?.bibliographies ?? [];
1739
+ properties.layout = layout;
1740
+ properties.isSourceDocumentDisplayed = isSourceDocumentDisplayed;
1741
+ break;
1742
+ }
1743
+ case "button": {
1744
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1745
+ variant ??= "default";
1746
+ let isExternal = false;
1747
+ const navigateToProperty = getPropertyByLabel(componentProperty.properties, "navigate-to");
1748
+ let href = navigateToProperty?.values[0]?.href ?? navigateToProperty?.values[0]?.slug ?? null;
1749
+ if (href === null) {
1750
+ const linkToProperty = getPropertyByLabel(componentProperty.properties, "link-to");
1751
+ href = linkToProperty?.values[0]?.href ?? linkToProperty?.values[0]?.slug ?? null;
1752
+ if (href === null) throw new Error(`Properties “navigate-to” or “link-to” not found for the following component: “${componentName}”`);
1753
+ else isExternal = true;
1754
+ }
1755
+ let startIcon = null;
1756
+ const startIconProperty = getPropertyValueByLabel(componentProperty.properties, "start-icon");
1757
+ if (startIconProperty !== null) startIcon = startIconProperty;
1758
+ let endIcon = null;
1759
+ const endIconProperty = getPropertyValueByLabel(componentProperty.properties, "end-icon");
1760
+ if (endIconProperty !== null) endIcon = endIconProperty;
1761
+ let image = null;
1762
+ const imageLink = links.find((link) => link.type === "image" || link.type === "IIIF");
1763
+ if (imageLink != null) image = {
1764
+ url: `https://ochre.lib.uchicago.edu/ochre?uuid=${imageLink.uuid}&load`,
1765
+ label: imageLink.identification?.label ?? null,
1766
+ width: imageLink.image?.width ?? 0,
1767
+ height: imageLink.image?.height ?? 0,
1768
+ description: imageLink.description ?? null
1769
+ };
1770
+ properties.variant = variant;
1771
+ properties.href = href;
1772
+ properties.isExternal = isExternal;
1773
+ properties.label = elementResource.document && "content" in elementResource.document ? parseDocument(elementResource.document.content) : null;
1774
+ properties.startIcon = startIcon;
1775
+ properties.endIcon = endIcon;
1776
+ properties.image = image;
1777
+ break;
1778
+ }
1779
+ case "collection": {
1780
+ const collectionLink = links.find((link) => link.category === "set");
1781
+ if (!collectionLink) throw new Error(`Collection link not found for the following component: “${componentName}”`);
1782
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1783
+ variant ??= "full";
1784
+ let itemVariant = getPropertyValueByLabel(componentProperty.properties, "item-variant");
1785
+ itemVariant ??= "detailed";
1786
+ let paginationVariant = getPropertyValueByLabel(componentProperty.properties, "pagination-variant");
1787
+ paginationVariant ??= "default";
1788
+ let isSortDisplayed = false;
1789
+ const isSortDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "sort-displayed");
1790
+ if (isSortDisplayedProperty !== null) isSortDisplayed = isSortDisplayedProperty === true;
1791
+ let isFilterDisplayed = false;
1792
+ const isFilterDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "filter-displayed");
1793
+ if (isFilterDisplayedProperty !== null) isFilterDisplayed = isFilterDisplayedProperty === true;
1794
+ let filterSort = getPropertyValueByLabel(componentProperty.properties, "filter-sort");
1795
+ filterSort ??= "default";
1796
+ let layout = getPropertyValueByLabel(componentProperty.properties, "layout");
1797
+ layout ??= "image-start";
1798
+ properties.collectionId = collectionLink.uuid;
1799
+ properties.variant = variant;
1800
+ properties.itemVariant = itemVariant;
1801
+ properties.paginationVariant = paginationVariant;
1802
+ properties.isSortDisplayed = isSortDisplayed;
1803
+ properties.isFilterDisplayed = isFilterDisplayed;
1804
+ properties.filterSort = filterSort;
1805
+ properties.layout = layout;
1806
+ break;
1807
+ }
1808
+ case "empty-space": {
1809
+ const height = getPropertyValueByLabel(componentProperty.properties, "height");
1810
+ const width = getPropertyValueByLabel(componentProperty.properties, "width");
1811
+ properties.height = height;
1812
+ properties.width = width;
1813
+ break;
1814
+ }
1815
+ case "entries": {
1816
+ const entriesLink = links.find((link) => link.category === "tree" || link.category === "set");
1817
+ if (!entriesLink) throw new Error(`Entries link not found for the following component: “${componentName}”`);
1818
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1819
+ variant ??= "entry";
1820
+ let isFilterDisplayed = false;
1821
+ const isFilterDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "filter-displayed");
1822
+ if (isFilterDisplayedProperty !== null) isFilterDisplayed = isFilterDisplayedProperty === true;
1823
+ properties.entriesId = entriesLink.uuid;
1824
+ properties.variant = variant;
1825
+ properties.isFilterDisplayed = isFilterDisplayed;
1826
+ break;
1827
+ }
1828
+ case "iframe": {
1829
+ const href = links.find((link) => link.type === "webpage")?.href;
1830
+ if (!href) throw new Error(`URL not found for the following component: “${componentName}”`);
1831
+ const height = getPropertyValueByLabel(componentProperty.properties, "height");
1832
+ const width = getPropertyValueByLabel(componentProperty.properties, "width");
1833
+ properties.href = href;
1834
+ properties.height = height;
1835
+ properties.width = width;
1836
+ break;
1837
+ }
1838
+ case "iiif-viewer": {
1839
+ const manifestLink = links.find((link) => link.type === "IIIF");
1840
+ if (!manifestLink) throw new Error(`Manifest link not found for the following component: “${componentName}”`);
1841
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1842
+ variant ??= "universal-viewer";
1843
+ properties.iiifId = manifestLink.uuid;
1844
+ properties.variant = variant;
1845
+ break;
1846
+ }
1847
+ case "image": {
1848
+ if (links.length === 0) throw new Error(`No links found for the following component: “${componentName}”`);
1849
+ let imageQuality = getPropertyValueByLabel(componentProperty.properties, "quality");
1850
+ imageQuality ??= "high";
1851
+ const images = [];
1852
+ for (const link of links) images.push({
1853
+ url: `https://ochre.lib.uchicago.edu/ochre?uuid=${link.uuid}${imageQuality === "high" && (link.type === "image" || link.type === "IIIF") ? "&load" : "&preview"}`,
1854
+ label: link.identification?.label ?? null,
1855
+ width: link.image?.width ?? 0,
1856
+ height: link.image?.height ?? 0,
1857
+ description: link.description ?? null
1858
+ });
1859
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1860
+ variant ??= "default";
1861
+ let captionLayout = getPropertyValueByLabel(componentProperty.properties, "layout-caption");
1862
+ captionLayout ??= "bottom";
1863
+ let width = null;
1864
+ const widthProperty = getPropertyValueByLabel(componentProperty.properties, "width");
1865
+ if (widthProperty !== null) {
1866
+ if (typeof widthProperty === "number") width = widthProperty;
1867
+ else if (typeof widthProperty === "string") width = Number.parseFloat(widthProperty);
1868
+ }
1869
+ let height = null;
1870
+ const heightProperty = getPropertyValueByLabel(componentProperty.properties, "height");
1871
+ if (heightProperty !== null) {
1872
+ if (typeof heightProperty === "number") height = heightProperty;
1873
+ else if (typeof heightProperty === "string") height = Number.parseFloat(heightProperty);
1874
+ }
1875
+ let isFullWidth = true;
1876
+ const isFullWidthProperty = getPropertyValueByLabel(componentProperty.properties, "is-full-width");
1877
+ if (isFullWidthProperty !== null) isFullWidth = isFullWidthProperty === true;
1878
+ let isFullHeight = true;
1879
+ const isFullHeightProperty = getPropertyValueByLabel(componentProperty.properties, "is-full-height");
1880
+ if (isFullHeightProperty !== null) isFullHeight = isFullHeightProperty === true;
1881
+ let captionSource = getPropertyValueByLabel(componentProperty.properties, "caption-source");
1882
+ captionSource ??= "name";
1883
+ let altTextSource = getPropertyValueByLabel(componentProperty.properties, "alt-text-source");
1884
+ altTextSource ??= "name";
1885
+ let isTransparentBackground = false;
1886
+ const isTransparentBackgroundProperty = getPropertyValueByLabel(componentProperty.properties, "is-transparent");
1887
+ if (isTransparentBackgroundProperty !== null) isTransparentBackground = isTransparentBackgroundProperty === true;
1888
+ let isCover = false;
1889
+ const isCoverProperty = getPropertyValueByLabel(componentProperty.properties, "is-cover");
1890
+ if (isCoverProperty !== null) isCover = isCoverProperty === true;
1891
+ const variantProperty = getPropertyByLabel(componentProperty.properties, "variant");
1892
+ let carouselOptions = null;
1893
+ if (images.length > 1) {
1894
+ let secondsPerImage = 5;
1895
+ if (variantProperty?.values[0].content === "carousel") {
1896
+ const secondsPerImageProperty = getPropertyValueByLabel(variantProperty.properties, "seconds-per-image");
1897
+ if (secondsPerImageProperty !== null) {
1898
+ if (typeof secondsPerImageProperty === "number") secondsPerImage = secondsPerImageProperty;
1899
+ else if (typeof secondsPerImageProperty === "string") secondsPerImage = Number.parseFloat(secondsPerImageProperty);
1900
+ }
1901
+ }
1902
+ carouselOptions = { secondsPerImage };
1903
+ }
1904
+ let heroOptions = null;
1905
+ if (variantProperty?.values[0].content === "hero") {
1906
+ const isBackgroundImageDisplayedProperty = getPropertyValueByLabel(variantProperty.properties, "background-image-displayed");
1907
+ const isDocumentDisplayedProperty = getPropertyValueByLabel(variantProperty.properties, "document-displayed");
1908
+ const isLinkDisplayedProperty = getPropertyValueByLabel(variantProperty.properties, "link-displayed");
1909
+ heroOptions = {
1910
+ isBackgroundImageDisplayed: isBackgroundImageDisplayedProperty !== false,
1911
+ isDocumentDisplayed: isDocumentDisplayedProperty !== false,
1912
+ isLinkDisplayed: isLinkDisplayedProperty !== false
1913
+ };
1914
+ }
1915
+ properties.images = images;
1916
+ properties.variant = variant;
1917
+ properties.width = width;
1918
+ properties.height = height;
1919
+ properties.isFullWidth = isFullWidth;
1920
+ properties.isFullHeight = isFullHeight;
1921
+ properties.imageQuality = imageQuality;
1922
+ properties.captionLayout = captionLayout;
1923
+ properties.captionSource = captionSource;
1924
+ properties.altTextSource = altTextSource;
1925
+ properties.isTransparentBackground = isTransparentBackground;
1926
+ properties.isCover = isCover;
1927
+ properties.carouselOptions = carouselOptions;
1928
+ properties.heroOptions = heroOptions;
1929
+ break;
1930
+ }
1931
+ case "image-gallery": {
1932
+ const galleryLink = links.find((link) => link.category === "tree" || link.category === "set");
1933
+ if (!galleryLink) throw new Error(`Image gallery link not found for the following component: “${componentName}”`);
1934
+ const isFilterDisplayed = getPropertyValueByLabel(componentProperty.properties, "filter-displayed") === true;
1935
+ properties.galleryId = galleryLink.uuid;
1936
+ properties.isFilterDisplayed = isFilterDisplayed;
1937
+ break;
1938
+ }
1939
+ case "map": {
1940
+ const mapLink = links.find((link) => link.category === "set" || link.category === "tree");
1941
+ if (!mapLink) throw new Error(`Map link not found for the following component: “${componentName}”`);
1942
+ let isInteractive = true;
1943
+ const isInteractiveProperty = getPropertyValueByLabel(componentProperty.properties, "is-interactive");
1944
+ if (isInteractiveProperty !== null) isInteractive = isInteractiveProperty === true;
1945
+ let isClustered = false;
1946
+ const isClusteredProperty = getPropertyValueByLabel(componentProperty.properties, "is-clustered");
1947
+ if (isClusteredProperty !== null) isClustered = isClusteredProperty === true;
1948
+ let isUsingPins = false;
1949
+ const isUsingPinsProperty = getPropertyValueByLabel(componentProperty.properties, "is-using-pins");
1950
+ if (isUsingPinsProperty !== null) isUsingPins = isUsingPinsProperty === true;
1951
+ let customBasemap = null;
1952
+ const customBasemapProperty = getPropertyValueByLabel(componentProperty.properties, "custom-basemap");
1953
+ if (customBasemapProperty !== null) customBasemap = customBasemapProperty;
1954
+ let isControlsDisplayed = false;
1955
+ const isControlsDisplayedProperty = getPropertyValueByLabel(componentProperty.properties, "controls-displayed");
1956
+ if (isControlsDisplayedProperty !== null) isControlsDisplayed = isControlsDisplayedProperty === true;
1957
+ let isFullHeight = false;
1958
+ const isFullHeightProperty = getPropertyValueByLabel(componentProperty.properties, "is-full-height");
1959
+ if (isFullHeightProperty !== null) isFullHeight = isFullHeightProperty === true;
1960
+ properties.mapId = mapLink.uuid;
1961
+ properties.isInteractive = isInteractive;
1962
+ properties.isClustered = isClustered;
1963
+ properties.isUsingPins = isUsingPins;
1964
+ properties.customBasemap = customBasemap;
1965
+ properties.isControlsDisplayed = isControlsDisplayed;
1966
+ properties.isFullHeight = isFullHeight;
1967
+ break;
1968
+ }
1969
+ case "network-graph": break;
1970
+ case "query": {
1971
+ const queries = [];
1972
+ const queryProperties = componentProperty.properties;
1973
+ if (queryProperties.length === 0) throw new Error(`Query properties not found for the following component: “${componentName}”`);
1974
+ for (const query of queryProperties) {
1975
+ const querySubProperties = query.properties;
1976
+ const label = getPropertyValueByLabel(querySubProperties, "query-prompt");
1977
+ if (label === null) throw new Error(`Query prompt not found for the following component: “${componentName}”`);
1978
+ const propertyUuids = querySubProperties.find((property) => property.label === "use-property")?.values.map((value) => value.uuid).filter((uuid) => uuid !== null) ?? [];
1979
+ const startIcon = getPropertyValueByLabel(querySubProperties, "start-icon");
1980
+ const endIcon = getPropertyValueByLabel(querySubProperties, "end-icon");
1981
+ queries.push({
1982
+ label: String(label),
1983
+ propertyUuids,
1984
+ startIcon: startIcon !== null ? String(startIcon) : null,
1985
+ endIcon: endIcon !== null ? String(endIcon) : null
1986
+ });
1987
+ }
1988
+ properties.queries = queries;
1989
+ break;
1990
+ }
1991
+ case "table": {
1992
+ const tableLink = links.find((link) => link.category === "set");
1993
+ if (!tableLink) throw new Error(`Table link not found for the following component: “${componentName}”`);
1994
+ properties.tableId = tableLink.uuid;
1995
+ break;
1996
+ }
1997
+ case "search-bar": {
1998
+ let variant = getPropertyValueByLabel(componentProperty.properties, "variant");
1999
+ variant ??= "default";
2000
+ const placeholder = getPropertyValueByLabel(componentProperty.properties, "placeholder-text");
2001
+ const baseQuery = getPropertyValueByLabel(componentProperty.properties, "base-query");
2002
+ properties.variant = variant;
2003
+ properties.placeholder = placeholder !== null ? String(placeholder) : null;
2004
+ properties.baseQuery = baseQuery !== null ? String(baseQuery).replaceAll(String.raw`\{`, "{").replaceAll(String.raw`\}`, "}") : null;
2005
+ break;
2006
+ }
2007
+ case "text": {
2008
+ const content = elementResource.document && "content" in elementResource.document ? parseDocument(elementResource.document.content) : null;
2009
+ if (!content) throw new Error(`Content not found for the following component: “${componentName}”`);
2010
+ let variantName = "block";
2011
+ let variant;
2012
+ const variantProperty = getPropertyByLabel(componentProperty.properties, "variant");
2013
+ if (variantProperty !== null) {
2014
+ variantName = variantProperty.values[0].content;
2015
+ if (variantName === "paragraph" || variantName === "label" || variantName === "heading" || variantName === "display") {
2016
+ const size = getPropertyValueByLabel(variantProperty.properties, "size");
2017
+ variant = {
2018
+ name: variantName,
2019
+ size: size !== null ? size : "md"
2020
+ };
2021
+ } else variant = { name: variantName };
2022
+ } else variant = { name: variantName };
2023
+ properties.variant = variant;
2024
+ properties.content = content;
2025
+ break;
2026
+ }
2027
+ case "timeline": {
2028
+ const timelineLink = links.find((link) => link.category === "tree");
2029
+ if (!timelineLink) throw new Error(`Timeline link not found for the following component: “${componentName}”`);
2030
+ properties.timelineId = timelineLink.uuid;
2031
+ break;
2032
+ }
2033
+ case "video": {
2034
+ const videoLink = links.find((link) => link.type === "video");
2035
+ if (!videoLink) throw new Error(`Video link not found for the following component: “${componentName}”`);
2036
+ let isChaptersDislayed = getPropertyValueByLabel(componentProperty.properties, "chapters-displayed");
2037
+ isChaptersDislayed ??= true;
2038
+ properties.videoId = videoLink.uuid;
2039
+ properties.isChaptersDislayed = isChaptersDislayed === true;
2040
+ break;
2041
+ }
2042
+ default:
2043
+ console.warn(`Invalid or non-implemented component name “${String(unparsedComponentName)}” for the following element: “${parseStringContent(elementResource.identification.label)}”`);
2044
+ break;
2045
+ }
2046
+ return properties;
2047
+ }
2048
+ function parseWebTitle(properties, identification, overrides = {}) {
2049
+ const titleProperties = properties.find((property) => property.label === "presentation" && property.values[0].content === "title")?.properties;
2050
+ let variant = "default";
2051
+ let isNameDisplayed = overrides.isNameDisplayed ?? false;
2052
+ let isDescriptionDisplayed = false;
2053
+ let isDateDisplayed = false;
2054
+ let isCreatorsDisplayed = false;
2055
+ let isCountDisplayed = overrides.isCountDisplayed ?? false;
2056
+ if (titleProperties) {
2057
+ const titleVariant = getPropertyValueByLabel(titleProperties, "variant");
2058
+ if (titleVariant) variant = titleVariant;
2059
+ isNameDisplayed = getPropertyValueByLabel(titleProperties, "name-displayed") === true;
2060
+ isDescriptionDisplayed = getPropertyValueByLabel(titleProperties, "description-displayed") === true;
2061
+ isDateDisplayed = getPropertyValueByLabel(titleProperties, "date-displayed") === true;
2062
+ isCreatorsDisplayed = getPropertyValueByLabel(titleProperties, "creators-displayed") === true;
2063
+ isCountDisplayed = getPropertyValueByLabel(titleProperties, "count-displayed") === true;
2064
+ }
2065
+ return {
2066
+ label: identification.label,
2067
+ variant,
2068
+ properties: {
2069
+ isNameDisplayed,
2070
+ isDescriptionDisplayed,
2071
+ isDateDisplayed,
2072
+ isCreatorsDisplayed,
2073
+ isCountDisplayed
2074
+ }
2075
+ };
2076
+ }
2077
+ /**
2078
+ * Parses raw web element data into a standardized WebElement structure
2079
+ *
2080
+ * @param elementResource - Raw element resource data in OCHRE format
2081
+ * @returns Parsed WebElement object
2082
+ */
2083
+ function parseWebElement(elementResource) {
2084
+ const identification = parseIdentification(elementResource.identification);
2085
+ const presentationProperty = (elementResource.properties?.property ? parseProperties(Array.isArray(elementResource.properties.property) ? elementResource.properties.property : [elementResource.properties.property]) : []).find((property) => property.label === "presentation");
2086
+ if (!presentationProperty) throw new Error(`Presentation property not found for element “${identification.label}”`);
2087
+ const componentProperty = presentationProperty.properties.find((property) => property.label === "component");
2088
+ if (!componentProperty) throw new Error(`Component for element “${identification.label}” not found`);
2089
+ const properties = parseWebElementProperties(componentProperty, elementResource);
2090
+ const elementResourceProperties = elementResource.properties?.property ? parseProperties(Array.isArray(elementResource.properties.property) ? elementResource.properties.property : [elementResource.properties.property]) : [];
2091
+ const cssProperties = elementResourceProperties.find((property) => property.label === "presentation" && property.values[0].content === "css")?.properties ?? [];
2092
+ const cssStyles = [];
2093
+ for (const property of cssProperties) {
2094
+ const cssStyle = property.values[0].content;
2095
+ cssStyles.push({
2096
+ label: property.label,
2097
+ value: cssStyle
2098
+ });
2099
+ }
2100
+ const tabletCssProperties = elementResourceProperties.find((property) => property.label === "presentation" && property.values[0].content === "css-tablet")?.properties ?? [];
2101
+ const cssStylesTablet = [];
2102
+ for (const property of tabletCssProperties) {
2103
+ const cssStyle = property.values[0].content;
2104
+ cssStylesTablet.push({
2105
+ label: property.label,
2106
+ value: cssStyle
2107
+ });
2108
+ }
2109
+ const mobileCssProperties = elementResourceProperties.find((property) => property.label === "presentation" && property.values[0].content === "css-mobile")?.properties ?? [];
2110
+ const cssStylesMobile = [];
2111
+ for (const property of mobileCssProperties) {
2112
+ const cssStyle = property.values[0].content;
2113
+ cssStylesMobile.push({
2114
+ label: property.label,
2115
+ value: cssStyle
2116
+ });
2117
+ }
2118
+ const title = parseWebTitle(elementResourceProperties, identification, {
2119
+ isNameDisplayed: [
2120
+ "annotated-image",
2121
+ "annotated-document",
2122
+ "collection"
2123
+ ].includes(properties.component),
2124
+ isCountDisplayed: properties.component === "collection" && properties.variant === "full"
2125
+ });
2126
+ return {
2127
+ uuid: elementResource.uuid,
2128
+ type: "element",
2129
+ title,
2130
+ cssStyles: {
2131
+ default: cssStyles,
2132
+ tablet: cssStylesTablet,
2133
+ mobile: cssStylesMobile
2134
+ },
2135
+ ...properties
2136
+ };
2137
+ }
2138
+ /**
2139
+ * Parses raw webpage data into a standardized Webpage structure
2140
+ *
2141
+ * @param webpageResource - Raw webpage resource data in OCHRE format
2142
+ * @returns Parsed Webpage object
2143
+ */
2144
+ function parseWebpage(webpageResource) {
2145
+ const webpageProperties = webpageResource.properties ? parseProperties(Array.isArray(webpageResource.properties.property) ? webpageResource.properties.property : [webpageResource.properties.property]) : [];
2146
+ if (webpageProperties.length === 0 || webpageProperties.find((property) => property.label === "presentation")?.values[0]?.content !== "page") return null;
2147
+ const identification = parseIdentification(webpageResource.identification);
2148
+ const slug = webpageResource.slug;
2149
+ if (slug === void 0) throw new Error(`Slug not found for page “${identification.label}”`);
2150
+ const imageLink = (webpageResource.links ? parseLinks(Array.isArray(webpageResource.links) ? webpageResource.links : [webpageResource.links]) : []).find((link) => link.type === "image" || link.type === "IIIF");
2151
+ const webpageResources = webpageResource.resource ? Array.isArray(webpageResource.resource) ? webpageResource.resource : [webpageResource.resource] : [];
2152
+ const items = [];
2153
+ for (const resource of webpageResources) {
2154
+ const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : [], "presentation");
2155
+ if (resourceType == null) continue;
2156
+ switch (resourceType) {
2157
+ case "element": {
2158
+ const element = parseWebElement(resource);
2159
+ items.push(element);
2160
+ break;
2161
+ }
2162
+ case "block": {
2163
+ const block = parseWebBlock(resource);
2164
+ if (block) items.push(block);
2165
+ break;
2166
+ }
2167
+ }
2168
+ }
2169
+ const webpages = webpageResource.resource ? parseWebpageResources(Array.isArray(webpageResource.resource) ? webpageResource.resource : [webpageResource.resource], "page") : [];
2170
+ let displayedInHeader = true;
2171
+ let width = "default";
2172
+ let variant = "default";
2173
+ let isSidebarDisplayed = true;
2174
+ let isBreadcrumbsDisplayed = false;
2175
+ const webpageSubProperties = webpageProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "page")?.properties;
2176
+ if (webpageSubProperties) {
2177
+ const headerProperty = webpageSubProperties.find((property) => property.label === "header")?.values[0];
2178
+ if (headerProperty) displayedInHeader = headerProperty.content === true;
2179
+ const widthProperty = webpageSubProperties.find((property) => property.label === "width")?.values[0];
2180
+ if (widthProperty) width = widthProperty.content;
2181
+ const variantProperty = webpageSubProperties.find((property) => property.label === "variant")?.values[0];
2182
+ if (variantProperty) variant = variantProperty.content;
2183
+ const isSidebarDisplayedProperty = webpageSubProperties.find((property) => property.label === "sidebar-visible")?.values[0];
2184
+ if (isSidebarDisplayedProperty) isSidebarDisplayed = isSidebarDisplayedProperty.content === true;
2185
+ const isBreadcrumbsDisplayedProperty = webpageSubProperties.find((property) => property.label === "breadcrumbs-visible")?.values[0];
2186
+ if (isBreadcrumbsDisplayedProperty) isBreadcrumbsDisplayed = isBreadcrumbsDisplayedProperty.content === true;
2187
+ }
2188
+ const cssStyleSubProperties = webpageProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css")?.properties;
2189
+ const cssStyles = [];
2190
+ if (cssStyleSubProperties) for (const property of cssStyleSubProperties) cssStyles.push({
2191
+ label: property.label,
2192
+ value: property.values[0].content
2193
+ });
2194
+ const tabletCssStyleSubProperties = webpageProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css-tablet")?.properties;
2195
+ const cssStylesTablet = [];
2196
+ if (tabletCssStyleSubProperties) for (const property of tabletCssStyleSubProperties) cssStylesTablet.push({
2197
+ label: property.label,
2198
+ value: property.values[0].content
2199
+ });
2200
+ const mobileCssStyleSubProperties = webpageProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css-mobile")?.properties;
2201
+ const cssStylesMobile = [];
2202
+ if (mobileCssStyleSubProperties) for (const property of mobileCssStyleSubProperties) cssStylesMobile.push({
2203
+ label: property.label,
2204
+ value: property.values[0].content
2205
+ });
2206
+ return {
2207
+ title: identification.label,
2208
+ slug,
2209
+ items,
2210
+ properties: {
2211
+ displayedInHeader,
2212
+ width,
2213
+ variant,
2214
+ backgroundImageUrl: imageLink ? `https://ochre.lib.uchicago.edu/ochre?uuid=${imageLink.uuid}&load` : null,
2215
+ isSidebarDisplayed,
2216
+ isBreadcrumbsDisplayed,
2217
+ cssStyles: {
2218
+ default: cssStyles,
2219
+ tablet: cssStylesTablet,
2220
+ mobile: cssStylesMobile
2221
+ }
2222
+ },
2223
+ webpages
2224
+ };
2225
+ }
2226
+ /**
2227
+ * Parses raw webpage resources into an array of Webpage objects
2228
+ *
2229
+ * @param webpageResources - Array of raw webpage resources in OCHRE format
2230
+ * @returns Array of parsed Webpage objects
2231
+ */
2232
+ function parseWebpages(webpageResources) {
2233
+ const returnPages = [];
2234
+ const pagesToParse = Array.isArray(webpageResources) ? webpageResources : [webpageResources];
2235
+ for (const page of pagesToParse) {
2236
+ const webpage = parseWebpage(page);
2237
+ if (webpage) returnPages.push(webpage);
2238
+ }
2239
+ return returnPages;
2240
+ }
2241
+ /**
2242
+ * Parses raw sidebar data into a standardized Sidebar structure
2243
+ *
2244
+ * @param resources - Array of raw sidebar resources in OCHRE format
2245
+ * @returns Parsed Sidebar object
2246
+ */
2247
+ function parseSidebar(resources) {
2248
+ let sidebar = null;
2249
+ const sidebarElements = [];
2250
+ const sidebarTitle = {
2251
+ label: "",
2252
+ variant: "default",
2253
+ properties: {
2254
+ isNameDisplayed: false,
2255
+ isDescriptionDisplayed: false,
2256
+ isDateDisplayed: false,
2257
+ isCreatorsDisplayed: false,
2258
+ isCountDisplayed: false
2259
+ }
2260
+ };
2261
+ let sidebarLayout = "start";
2262
+ let sidebarMobileLayout = "default";
2263
+ const sidebarCssStyles = [];
2264
+ const sidebarCssStylesTablet = [];
2265
+ const sidebarCssStylesMobile = [];
2266
+ const sidebarResource = resources.find((resource) => {
2267
+ 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");
2268
+ });
2269
+ if (sidebarResource) {
2270
+ 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);
2271
+ const sidebarBaseProperties = sidebarResource.properties ? parseProperties(Array.isArray(sidebarResource.properties.property) ? sidebarResource.properties.property : [sidebarResource.properties.property]) : [];
2272
+ 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 ?? [];
2273
+ const sidebarLayoutProperty = sidebarProperties.find((property) => property.label === "layout");
2274
+ if (sidebarLayoutProperty) sidebarLayout = sidebarLayoutProperty.values[0].content;
2275
+ const sidebarMobileLayoutProperty = sidebarProperties.find((property) => property.label === "layout-mobile");
2276
+ if (sidebarMobileLayoutProperty) sidebarMobileLayout = sidebarMobileLayoutProperty.values[0].content;
2277
+ const cssProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0].content === "css")?.properties ?? [];
2278
+ for (const property of cssProperties) {
2279
+ const cssStyle = property.values[0].content;
2280
+ sidebarCssStyles.push({
2281
+ label: property.label,
2282
+ value: cssStyle
2283
+ });
2284
+ }
2285
+ const tabletCssProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0].content === "css-tablet")?.properties ?? [];
2286
+ for (const property of tabletCssProperties) {
2287
+ const cssStyle = property.values[0].content;
2288
+ sidebarCssStylesTablet.push({
2289
+ label: property.label,
2290
+ value: cssStyle
2291
+ });
2292
+ }
2293
+ const mobileCssProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0].content === "css-mobile")?.properties ?? [];
2294
+ for (const property of mobileCssProperties) {
2295
+ const cssStyle = property.values[0].content;
2296
+ sidebarCssStylesMobile.push({
2297
+ label: property.label,
2298
+ value: cssStyle
2299
+ });
2300
+ }
2301
+ const titleProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0].content === "title")?.properties;
2302
+ if (titleProperties) {
2303
+ const titleVariant = getPropertyValueByLabel(titleProperties, "variant");
2304
+ if (titleVariant) sidebarTitle.variant = titleVariant;
2305
+ sidebarTitle.properties.isNameDisplayed = getPropertyValueByLabel(titleProperties, "name-displayed") === true;
2306
+ sidebarTitle.properties.isDescriptionDisplayed = getPropertyValueByLabel(titleProperties, "description-displayed") === true;
2307
+ sidebarTitle.properties.isDateDisplayed = getPropertyValueByLabel(titleProperties, "date-displayed") === true;
2308
+ sidebarTitle.properties.isCreatorsDisplayed = getPropertyValueByLabel(titleProperties, "creators-displayed") === true;
2309
+ sidebarTitle.properties.isCountDisplayed = getPropertyValueByLabel(titleProperties, "count-displayed") === true;
2310
+ }
2311
+ const sidebarResources = sidebarResource.resource ? Array.isArray(sidebarResource.resource) ? sidebarResource.resource : [sidebarResource.resource] : [];
2312
+ for (const resource of sidebarResources) {
2313
+ const element = parseWebElement(resource);
2314
+ sidebarElements.push(element);
2315
+ }
2316
+ }
2317
+ if (sidebarElements.length > 0) sidebar = {
2318
+ elements: sidebarElements,
2319
+ title: sidebarTitle,
2320
+ layout: sidebarLayout,
2321
+ mobileLayout: sidebarMobileLayout,
2322
+ cssStyles: {
2323
+ default: sidebarCssStyles,
2324
+ tablet: sidebarCssStylesTablet,
2325
+ mobile: sidebarCssStylesMobile
2326
+ }
2327
+ };
2328
+ return sidebar;
2329
+ }
2330
+ /**
2331
+ * Parses raw text element data for accordion layout with items support
2332
+ *
2333
+ * @param elementResource - Raw element resource data in OCHRE format
2334
+ * @returns Parsed text WebElement with items array
2335
+ */
2336
+ function parseWebElementForAccordion(elementResource) {
2337
+ const textElement = parseWebElement(elementResource);
2338
+ const childResources = elementResource.resource ? Array.isArray(elementResource.resource) ? elementResource.resource : [elementResource.resource] : [];
2339
+ const items = [];
2340
+ for (const resource of childResources) {
2341
+ const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : [], "presentation");
2342
+ if (resourceType == null) continue;
2343
+ switch (resourceType) {
2344
+ case "element": {
2345
+ const element = parseWebElement(resource);
2346
+ items.push(element);
2347
+ break;
2348
+ }
2349
+ case "block": {
2350
+ const block = parseWebBlock(resource);
2351
+ if (block) items.push(block);
2352
+ break;
2353
+ }
2354
+ }
2355
+ }
2356
+ return {
2357
+ ...textElement,
2358
+ items
2359
+ };
2360
+ }
2361
+ /**
2362
+ * Parses raw block data into a standardized WebBlock structure
2363
+ *
2364
+ * @param blockResource - Raw block resource data in OCHRE format
2365
+ * @returns Parsed WebBlock object
2366
+ */
2367
+ function parseWebBlock(blockResource) {
2368
+ const blockProperties = blockResource.properties ? parseProperties(Array.isArray(blockResource.properties.property) ? blockResource.properties.property : [blockResource.properties.property]) : [];
2369
+ const returnBlock = {
2370
+ uuid: blockResource.uuid,
2371
+ type: "block",
2372
+ title: parseWebTitle(blockProperties, parseIdentification(blockResource.identification)),
2373
+ items: [],
2374
+ properties: {
2375
+ default: {
2376
+ layout: "vertical",
2377
+ spacing: void 0,
2378
+ gap: void 0,
2379
+ alignItems: "start",
2380
+ justifyContent: "stretch"
2381
+ },
2382
+ mobile: null,
2383
+ tablet: null
2384
+ },
2385
+ cssStyles: {
2386
+ default: [],
2387
+ tablet: [],
2388
+ mobile: []
2389
+ }
2390
+ };
2391
+ const blockMainProperties = blockProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "block")?.properties;
2392
+ if (blockMainProperties) {
2393
+ const layoutProperty = blockMainProperties.find((property) => property.label === "layout")?.values[0];
2394
+ if (layoutProperty) returnBlock.properties.default.layout = layoutProperty.content;
2395
+ if (returnBlock.properties.default.layout === "accordion") {
2396
+ const isAccordionEnabledProperty = blockMainProperties.find((property) => property.label === "accordion-enabled")?.values[0];
2397
+ if (isAccordionEnabledProperty) returnBlock.properties.default.isAccordionEnabled = isAccordionEnabledProperty.content === true;
2398
+ else returnBlock.properties.default.isAccordionEnabled = true;
2399
+ const isAccordionExpandedByDefaultProperty = blockMainProperties.find((property) => property.label === "accordion-expanded")?.values[0];
2400
+ if (isAccordionExpandedByDefaultProperty) returnBlock.properties.default.isAccordionExpandedByDefault = isAccordionExpandedByDefaultProperty.content === true;
2401
+ else returnBlock.properties.default.isAccordionExpandedByDefault = false;
2402
+ const isAccordionSidebarDisplayedProperty = blockMainProperties.find((property) => property.label === "accordion-sidebar-displayed")?.values[0];
2403
+ if (isAccordionSidebarDisplayedProperty) returnBlock.properties.default.isAccordionSidebarDisplayed = isAccordionSidebarDisplayedProperty.content === true;
2404
+ else returnBlock.properties.default.isAccordionSidebarDisplayed = false;
2405
+ }
2406
+ const spacingProperty = blockMainProperties.find((property) => property.label === "spacing")?.values[0];
2407
+ if (spacingProperty) returnBlock.properties.default.spacing = spacingProperty.content;
2408
+ const gapProperty = blockMainProperties.find((property) => property.label === "gap")?.values[0];
2409
+ if (gapProperty) returnBlock.properties.default.gap = gapProperty.content;
2410
+ const alignItemsProperty = blockMainProperties.find((property) => property.label === "align-items")?.values[0];
2411
+ if (alignItemsProperty) returnBlock.properties.default.alignItems = alignItemsProperty.content;
2412
+ const justifyContentProperty = blockMainProperties.find((property) => property.label === "justify-content")?.values[0];
2413
+ if (justifyContentProperty) returnBlock.properties.default.justifyContent = justifyContentProperty.content;
2414
+ const tabletOverwriteProperty = blockMainProperties.find((property) => property.label === "overwrite-tablet");
2415
+ if (tabletOverwriteProperty) {
2416
+ const tabletOverwriteProperties = tabletOverwriteProperty.properties;
2417
+ const propertiesTablet = {};
2418
+ const layoutProperty$1 = tabletOverwriteProperties.find((property) => property.label === "layout")?.values[0];
2419
+ if (layoutProperty$1) propertiesTablet.layout = layoutProperty$1.content;
2420
+ if (propertiesTablet.layout === "accordion" || returnBlock.properties.default.layout === "accordion") {
2421
+ const isAccordionEnabledProperty = tabletOverwriteProperties.find((property) => property.label === "accordion-enabled")?.values[0];
2422
+ if (isAccordionEnabledProperty) propertiesTablet.isAccordionEnabled = isAccordionEnabledProperty.content === true;
2423
+ const isAccordionExpandedByDefaultProperty = tabletOverwriteProperties.find((property) => property.label === "accordion-expanded")?.values[0];
2424
+ if (isAccordionExpandedByDefaultProperty) propertiesTablet.isAccordionExpandedByDefault = isAccordionExpandedByDefaultProperty.content === true;
2425
+ const isAccordionSidebarDisplayedProperty = tabletOverwriteProperties.find((property) => property.label === "accordion-sidebar-displayed")?.values[0];
2426
+ if (isAccordionSidebarDisplayedProperty) propertiesTablet.isAccordionSidebarDisplayed = isAccordionSidebarDisplayedProperty.content === true;
2427
+ }
2428
+ const spacingProperty$1 = tabletOverwriteProperties.find((property) => property.label === "spacing")?.values[0];
2429
+ if (spacingProperty$1) propertiesTablet.spacing = spacingProperty$1.content;
2430
+ const gapProperty$1 = tabletOverwriteProperties.find((property) => property.label === "gap")?.values[0];
2431
+ if (gapProperty$1) propertiesTablet.gap = gapProperty$1.content;
2432
+ const alignItemsProperty$1 = tabletOverwriteProperties.find((property) => property.label === "align-items")?.values[0];
2433
+ if (alignItemsProperty$1) propertiesTablet.alignItems = alignItemsProperty$1.content;
2434
+ const justifyContentProperty$1 = tabletOverwriteProperties.find((property) => property.label === "justify-content")?.values[0];
2435
+ if (justifyContentProperty$1) propertiesTablet.justifyContent = justifyContentProperty$1.content;
2436
+ returnBlock.properties.tablet = propertiesTablet;
2437
+ }
2438
+ const mobileOverwriteProperty = blockMainProperties.find((property) => property.label === "overwrite-mobile");
2439
+ if (mobileOverwriteProperty) {
2440
+ const mobileOverwriteProperties = mobileOverwriteProperty.properties;
2441
+ const propertiesMobile = {};
2442
+ const layoutProperty$1 = mobileOverwriteProperties.find((property) => property.label === "layout")?.values[0];
2443
+ if (layoutProperty$1) propertiesMobile.layout = layoutProperty$1.content;
2444
+ if (propertiesMobile.layout === "accordion" || returnBlock.properties.default.layout === "accordion") {
2445
+ const isAccordionEnabledProperty = mobileOverwriteProperties.find((property) => property.label === "accordion-enabled")?.values[0];
2446
+ if (isAccordionEnabledProperty) propertiesMobile.isAccordionEnabled = isAccordionEnabledProperty.content === true;
2447
+ const isAccordionExpandedByDefaultProperty = mobileOverwriteProperties.find((property) => property.label === "accordion-expanded")?.values[0];
2448
+ if (isAccordionExpandedByDefaultProperty) propertiesMobile.isAccordionExpandedByDefault = isAccordionExpandedByDefaultProperty.content === true;
2449
+ const isAccordionSidebarDisplayedProperty = mobileOverwriteProperties.find((property) => property.label === "accordion-sidebar-displayed")?.values[0];
2450
+ if (isAccordionSidebarDisplayedProperty) propertiesMobile.isAccordionSidebarDisplayed = isAccordionSidebarDisplayedProperty.content === true;
2451
+ }
2452
+ const spacingProperty$1 = mobileOverwriteProperties.find((property) => property.label === "spacing")?.values[0];
2453
+ if (spacingProperty$1) propertiesMobile.spacing = spacingProperty$1.content;
2454
+ const gapProperty$1 = mobileOverwriteProperties.find((property) => property.label === "gap")?.values[0];
2455
+ if (gapProperty$1) propertiesMobile.gap = gapProperty$1.content;
2456
+ const alignItemsProperty$1 = mobileOverwriteProperties.find((property) => property.label === "align-items")?.values[0];
2457
+ if (alignItemsProperty$1) propertiesMobile.alignItems = alignItemsProperty$1.content;
2458
+ const justifyContentProperty$1 = mobileOverwriteProperties.find((property) => property.label === "justify-content")?.values[0];
2459
+ if (justifyContentProperty$1) propertiesMobile.justifyContent = justifyContentProperty$1.content;
2460
+ returnBlock.properties.mobile = propertiesMobile;
2461
+ }
2462
+ }
2463
+ const blockResources = blockResource.resource ? Array.isArray(blockResource.resource) ? blockResource.resource : [blockResource.resource] : [];
2464
+ if (returnBlock.properties.default.layout === "accordion") {
2465
+ const accordionItems = [];
2466
+ for (const resource of blockResources) {
2467
+ const resourceProperties = resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : [];
2468
+ const resourceType = getPropertyValueByLabel(resourceProperties, "presentation");
2469
+ if (resourceType !== "element") throw new Error(`Accordion only accepts elements, but got “${resourceType}” for the following resource: “${parseStringContent(resource.identification.label)}”`);
2470
+ const componentType = (resourceProperties.find((property) => property.label === "presentation")?.properties.find((property) => property.label === "component"))?.values[0]?.content;
2471
+ if (componentType !== "text") throw new Error(`Accordion only accepts text components, but got “${componentType}” for the following resource: “${parseStringContent(resource.identification.label)}”`);
2472
+ const element = parseWebElementForAccordion(resource);
2473
+ accordionItems.push(element);
2474
+ }
2475
+ returnBlock.items = accordionItems;
2476
+ } else {
2477
+ const blockItems = [];
2478
+ for (const resource of blockResources) {
2479
+ const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(Array.isArray(resource.properties.property) ? resource.properties.property : [resource.properties.property]) : [], "presentation");
2480
+ if (resourceType == null) continue;
2481
+ switch (resourceType) {
2482
+ case "element": {
2483
+ const element = parseWebElement(resource);
2484
+ blockItems.push(element);
2485
+ break;
2486
+ }
2487
+ case "block": {
2488
+ const block = parseWebBlock(resource);
2489
+ if (block) blockItems.push(block);
2490
+ break;
2491
+ }
2492
+ }
2493
+ }
2494
+ returnBlock.items = blockItems;
2495
+ }
2496
+ const blockCssStyles = blockProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css")?.properties;
2497
+ if (blockCssStyles) for (const property of blockCssStyles) returnBlock.cssStyles.default.push({
2498
+ label: property.label,
2499
+ value: property.values[0].content
2500
+ });
2501
+ const blockTabletCssStyles = blockProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css-tablet")?.properties;
2502
+ if (blockTabletCssStyles) for (const property of blockTabletCssStyles) returnBlock.cssStyles.tablet.push({
2503
+ label: property.label,
2504
+ value: property.values[0].content
2505
+ });
2506
+ const blockMobileCssStyles = blockProperties.find((property) => property.label === "presentation" && property.values[0]?.content === "css-mobile")?.properties;
2507
+ if (blockMobileCssStyles) for (const property of blockMobileCssStyles) returnBlock.cssStyles.mobile.push({
2508
+ label: property.label,
2509
+ value: property.values[0].content
2510
+ });
2511
+ return returnBlock;
2512
+ }
2513
+ /**
2514
+ * Parses raw website properties into a standardized WebsiteProperties structure
2515
+ *
2516
+ * @param properties - Array of raw website properties in OCHRE format
2517
+ * @returns Parsed WebsiteProperties object
2518
+ */
2519
+ function parseWebsiteProperties(properties) {
2520
+ const websiteProperties = parseProperties(properties).find((property) => property.label === "presentation")?.properties;
2521
+ if (!websiteProperties) throw new Error("Presentation property not found");
2522
+ let type = websiteProperties.find((property) => property.label === "webUI")?.values[0]?.content;
2523
+ type ??= "traditional";
2524
+ let status = websiteProperties.find((property) => property.label === "status")?.values[0]?.content;
2525
+ status ??= "development";
2526
+ let privacy = websiteProperties.find((property) => property.label === "privacy")?.values[0]?.content;
2527
+ privacy ??= "public";
2528
+ const result = websiteSchema.safeParse({
2529
+ type,
2530
+ status,
2531
+ privacy
2532
+ });
2533
+ if (!result.success) throw new Error(`Invalid website properties: ${result.error.message}`);
2534
+ let contact = null;
2535
+ const contactProperty = websiteProperties.find((property) => property.label === "contact");
2536
+ if (contactProperty) {
2537
+ const [name, email] = (contactProperty.values[0]?.content).split(";");
2538
+ contact = {
2539
+ name,
2540
+ email: email ?? null
2541
+ };
2542
+ }
2543
+ const logoUuid = websiteProperties.find((property) => property.label === "logo")?.values[0]?.uuid ?? null;
2544
+ let isHeaderDisplayed = true;
2545
+ let headerVariant = "default";
2546
+ let headerAlignment = "start";
2547
+ let isHeaderProjectDisplayed = true;
2548
+ let isFooterDisplayed = true;
2549
+ let isSidebarDisplayed = false;
2550
+ let iiifViewer = "universal-viewer";
2551
+ let supportsThemeToggle = true;
2552
+ let defaultTheme = null;
2553
+ const headerProperty = websiteProperties.find((property) => property.label === "navbar-visible")?.values[0];
2554
+ if (headerProperty) isHeaderDisplayed = headerProperty.content === true;
2555
+ const headerVariantProperty = websiteProperties.find((property) => property.label === "navbar-variant")?.values[0];
2556
+ if (headerVariantProperty) headerVariant = headerVariantProperty.content;
2557
+ const headerAlignmentProperty = websiteProperties.find((property) => property.label === "navbar-alignment")?.values[0];
2558
+ if (headerAlignmentProperty) headerAlignment = headerAlignmentProperty.content;
2559
+ const isHeaderProjectDisplayedProperty = websiteProperties.find((property) => property.label === "navbar-project-visible")?.values[0];
2560
+ if (isHeaderProjectDisplayedProperty) isHeaderProjectDisplayed = isHeaderProjectDisplayedProperty.content === true;
2561
+ const footerProperty = websiteProperties.find((property) => property.label === "footer-visible")?.values[0];
2562
+ if (footerProperty) isFooterDisplayed = footerProperty.content === true;
2563
+ const sidebarProperty = websiteProperties.find((property) => property.label === "sidebar-visible")?.values[0];
2564
+ if (sidebarProperty) isSidebarDisplayed = sidebarProperty.content === true;
2565
+ const iiifViewerProperty = websiteProperties.find((property) => property.label === "iiif-viewer")?.values[0];
2566
+ if (iiifViewerProperty) iiifViewer = iiifViewerProperty.content;
2567
+ const supportsThemeToggleProperty = websiteProperties.find((property) => property.label === "supports-theme-toggle")?.values[0];
2568
+ if (supportsThemeToggleProperty) supportsThemeToggle = supportsThemeToggleProperty.content === true;
2569
+ const defaultThemeProperty = websiteProperties.find((property) => property.label === "default-theme")?.values[0];
2570
+ if (defaultThemeProperty) defaultTheme = defaultThemeProperty.content;
2571
+ const { type: validatedType, status: validatedStatus, privacy: validatedPrivacy } = result.data;
2572
+ return {
2573
+ type: validatedType,
2574
+ privacy: validatedPrivacy,
2575
+ status: validatedStatus,
2576
+ contact,
2577
+ isHeaderDisplayed,
2578
+ headerVariant,
2579
+ headerAlignment,
2580
+ isHeaderProjectDisplayed,
2581
+ isFooterDisplayed,
2582
+ isSidebarDisplayed,
2583
+ iiifViewer,
2584
+ supportsThemeToggle,
2585
+ defaultTheme,
2586
+ logoUrl: logoUuid !== null ? `https://ochre.lib.uchicago.edu/ochre?uuid=${logoUuid}&load` : null
2587
+ };
2588
+ }
2589
+ function parseContexts(contexts) {
2590
+ const contextsParsed = [];
2591
+ for (const mainContext of contexts) {
2592
+ const contextItemsToParse = Array.isArray(mainContext.context) ? mainContext.context : [mainContext.context];
2593
+ for (const contextItemToParse of contextItemsToParse) {
2594
+ const levelsToParse = Array.isArray(contextItemToParse.levels.level) ? contextItemToParse.levels.level : [contextItemToParse.levels.level];
2595
+ let type = "";
2596
+ const levels = levelsToParse.map((level) => {
2597
+ let variableUuid = "";
2598
+ let valueUuid = null;
2599
+ if (typeof level === "string") {
2600
+ const splitLevel = level.split(", ");
2601
+ variableUuid = splitLevel[0];
2602
+ valueUuid = splitLevel[1] === "null" ? null : splitLevel[1];
2603
+ } else {
2604
+ const splitLevel = level.content.split(", ");
2605
+ type = level.dataType;
2606
+ variableUuid = splitLevel[0];
2607
+ valueUuid = splitLevel[1] === "null" ? null : splitLevel[1];
2608
+ }
2609
+ return {
2610
+ variableUuid,
2611
+ valueUuid
2612
+ };
2613
+ });
2614
+ contextsParsed.push({
2615
+ context: levels,
2616
+ type,
2617
+ identification: parseIdentification(contextItemToParse.identification)
2618
+ });
2619
+ }
2620
+ }
2621
+ return contextsParsed;
2622
+ }
2623
+ function parseWebsite(websiteTree, projectName, website) {
2624
+ if (!websiteTree.properties) throw new Error("Website properties not found");
2625
+ const properties = parseWebsiteProperties(Array.isArray(websiteTree.properties.property) ? websiteTree.properties.property : [websiteTree.properties.property]);
2626
+ if (typeof websiteTree.items === "string" || !("resource" in websiteTree.items)) throw new Error("Website pages not found");
2627
+ const resources = Array.isArray(websiteTree.items.resource) ? websiteTree.items.resource : [websiteTree.items.resource];
2628
+ const pages = parseWebpages(resources);
2629
+ const sidebar = parseSidebar(resources);
2630
+ let globalOptions = { contexts: {
2631
+ flatten: [],
2632
+ filter: [],
2633
+ sort: [],
2634
+ detail: [],
2635
+ download: [],
2636
+ label: [],
2637
+ suppress: [],
2638
+ prominent: []
2639
+ } };
2640
+ if (websiteTree.websiteOptions) {
2641
+ const flattenContextsRaw = websiteTree.websiteOptions.flattenContexts != null ? Array.isArray(websiteTree.websiteOptions.flattenContexts) ? websiteTree.websiteOptions.flattenContexts : [websiteTree.websiteOptions.flattenContexts] : [];
2642
+ const suppressContextsRaw = websiteTree.websiteOptions.suppressContexts != null ? Array.isArray(websiteTree.websiteOptions.suppressContexts) ? websiteTree.websiteOptions.suppressContexts : [websiteTree.websiteOptions.suppressContexts] : [];
2643
+ const filterContextsRaw = websiteTree.websiteOptions.filterContexts != null ? Array.isArray(websiteTree.websiteOptions.filterContexts) ? websiteTree.websiteOptions.filterContexts : [websiteTree.websiteOptions.filterContexts] : [];
2644
+ const sortContextsRaw = websiteTree.websiteOptions.sortContexts != null ? Array.isArray(websiteTree.websiteOptions.sortContexts) ? websiteTree.websiteOptions.sortContexts : [websiteTree.websiteOptions.sortContexts] : [];
2645
+ const detailContextsRaw = websiteTree.websiteOptions.detailContexts != null ? Array.isArray(websiteTree.websiteOptions.detailContexts) ? websiteTree.websiteOptions.detailContexts : [websiteTree.websiteOptions.detailContexts] : [];
2646
+ const downloadContextsRaw = websiteTree.websiteOptions.downloadContexts != null ? Array.isArray(websiteTree.websiteOptions.downloadContexts) ? websiteTree.websiteOptions.downloadContexts : [websiteTree.websiteOptions.downloadContexts] : [];
2647
+ const labelContextsRaw = websiteTree.websiteOptions.labelContexts != null ? Array.isArray(websiteTree.websiteOptions.labelContexts) ? websiteTree.websiteOptions.labelContexts : [websiteTree.websiteOptions.labelContexts] : [];
2648
+ const prominentContextsRaw = websiteTree.websiteOptions.prominentContexts != null ? Array.isArray(websiteTree.websiteOptions.prominentContexts) ? websiteTree.websiteOptions.prominentContexts : [websiteTree.websiteOptions.prominentContexts] : [];
2649
+ globalOptions = { contexts: {
2650
+ flatten: parseContexts(flattenContextsRaw),
2651
+ filter: parseContexts(filterContextsRaw),
2652
+ sort: parseContexts(sortContextsRaw),
2653
+ detail: parseContexts(detailContextsRaw),
2654
+ download: parseContexts(downloadContextsRaw),
2655
+ label: parseContexts(labelContextsRaw),
2656
+ suppress: parseContexts(suppressContextsRaw),
2657
+ prominent: parseContexts(prominentContextsRaw)
2658
+ } };
2659
+ }
2660
+ return {
2661
+ uuid: websiteTree.uuid,
2662
+ publicationDateTime: websiteTree.publicationDateTime ? new Date(websiteTree.publicationDateTime) : null,
2663
+ identification: parseIdentification(websiteTree.identification),
2664
+ project: {
2665
+ name: parseFakeString(projectName),
2666
+ website: website !== null ? parseFakeString(website) : null
2667
+ },
2668
+ creators: websiteTree.creators ? parsePersons(Array.isArray(websiteTree.creators.creator) ? websiteTree.creators.creator : [websiteTree.creators.creator]) : [],
2669
+ license: parseLicense(websiteTree.availability),
2670
+ sidebar,
2671
+ pages,
2672
+ properties,
2673
+ searchOptions: {
2674
+ filters: websiteTree.searchOptions?.filterUuids != null ? (Array.isArray(websiteTree.searchOptions.filterUuids.uuid) ? websiteTree.searchOptions.filterUuids.uuid : [websiteTree.searchOptions.filterUuids.uuid]).map((uuid) => ({
2675
+ uuid: uuid.content,
2676
+ type: uuid.type
2677
+ })) : [],
2678
+ attributeFilters: {
2679
+ bibliographies: websiteTree.searchOptions?.filterUuids?.filterBibliography ?? false,
2680
+ periods: websiteTree.searchOptions?.filterUuids?.filterPeriods ?? false
2681
+ },
2682
+ scopes: websiteTree.searchOptions?.scopes != null ? (Array.isArray(websiteTree.searchOptions.scopes.scope) ? websiteTree.searchOptions.scopes.scope : [websiteTree.searchOptions.scopes.scope]).map((scope) => ({
2683
+ uuid: scope.uuid.content,
2684
+ type: scope.uuid.type,
2685
+ identification: parseIdentification(scope.identification)
2686
+ })) : []
2687
+ },
2688
+ globalOptions
2689
+ };
2690
+ }
2691
+
2692
+ //#endregion
2693
+ //#region src/utils/fetchers/gallery.ts
2694
+ /**
2695
+ * Fetches and parses a gallery from the OCHRE API
2696
+ *
2697
+ * @param uuid - The UUID of the gallery
2698
+ * @param filter - The filter to apply to the gallery
2699
+ * @param page - The page number to fetch
2700
+ * @param perPage - The number of items per page
2701
+ * @returns The parsed gallery or null if the fetch/parse fails
2702
+ *
2703
+ * @example
2704
+ * ```ts
2705
+ * const gallery = await fetchGallery("9c4da06b-f15e-40af-a747-0933eaf3587e", "1978", 1, 12);
2706
+ * if (gallery === null) {
2707
+ * console.error("Failed to fetch gallery");
2708
+ * return;
2709
+ * }
2710
+ * console.log(`Fetched gallery: ${gallery.identification.label}`);
2711
+ * console.log(`Contains ${gallery.resources.length.toLocaleString()} resources`);
2712
+ * ```
2713
+ *
2714
+ * @remarks
2715
+ * The returned gallery includes:
2716
+ * - Gallery metadata and identification
2717
+ * - Project identification
2718
+ * - Resources (gallery items)
2719
+ */
2720
+ async function fetchGallery(uuid, filter, page, perPage, customFetch) {
2721
+ try {
2722
+ const { uuid: parsedUuid, filter: parsedFilter, page: parsedPage, perPage: parsedPerPage } = gallerySchema.parse({
2723
+ uuid,
2724
+ filter,
2725
+ page,
2726
+ perPage
2727
+ });
2728
+ const response = await (customFetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(`
2729
+ for $q in input()/ochre[@uuid='${parsedUuid}']
2730
+ let $filtered := $q//items/resource[contains(lower-case(identification/label), lower-case('${parsedFilter}'))]
2731
+ let $maxLength := count($filtered)
2732
+ return <gallery maxLength='{$maxLength}'>
2733
+ {$q/metadata/project}
2734
+ {$q/metadata/item}
2735
+ {$filtered[position() >= ${((parsedPage - 1) * parsedPerPage + 1).toString()} and position() < ${(parsedPage * parsedPerPage + 1).toString()}]}
2736
+ </gallery>
2737
+ `)}&format=json`);
2738
+ if (!response.ok) throw new Error("Error fetching gallery items, please try again later.");
2739
+ const data = await response.json();
2740
+ if (!("gallery" in data.result)) throw new Error("Failed to fetch gallery");
2741
+ return {
2742
+ item: {
2743
+ identification: parseIdentification(data.result.gallery.item.identification),
2744
+ projectIdentification: parseIdentification(data.result.gallery.project.identification),
2745
+ resources: parseResources(data.result.gallery.resource ? Array.isArray(data.result.gallery.resource) ? data.result.gallery.resource : [data.result.gallery.resource] : []),
2746
+ maxLength: data.result.gallery.maxLength
2747
+ },
2748
+ error: null
2749
+ };
2750
+ } catch (error) {
2751
+ console.error(error);
2752
+ return {
2753
+ item: null,
2754
+ error: error instanceof Error ? error.message : "Failed to fetch gallery"
2755
+ };
2756
+ }
2757
+ }
2758
+
2759
+ //#endregion
2760
+ //#region src/utils/fetchers/uuid.ts
2761
+ /**
2762
+ * Fetches raw OCHRE data by UUID from the OCHRE API
2763
+ *
2764
+ * @param uuid - The UUID of the OCHRE item to fetch
2765
+ * @returns A tuple containing either [null, OchreData] on success or [error message, null] on failure
2766
+ *
2767
+ * @example
2768
+ * ```ts
2769
+ * const [error, data] = await fetchByUuid("123e4567-e89b-12d3-a456-426614174000");
2770
+ * if (error !== null) {
2771
+ * console.error(`Failed to fetch: ${error}`);
2772
+ * return;
2773
+ * }
2774
+ * // Process data...
2775
+ * ```
2776
+ *
2777
+ * @internal
2778
+ */
2779
+ async function fetchByUuid(uuid, customFetch) {
2780
+ try {
2781
+ const parsedUuid = uuidSchema.parse(uuid);
2782
+ const response = await (customFetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre?uuid=${parsedUuid}&format=json&lang="*"`);
2783
+ if (!response.ok) throw new Error("Failed to fetch OCHRE data");
2784
+ const dataRaw = await response.json();
2785
+ if (!("ochre" in dataRaw)) throw new Error("Invalid OCHRE data: API response missing 'ochre' key");
2786
+ return [null, dataRaw];
2787
+ } catch (error) {
2788
+ return [error instanceof Error ? error.message : "Unknown error", null];
2789
+ }
2790
+ }
2791
+
2792
+ //#endregion
2793
+ //#region src/utils/fetchers/item.ts
2794
+ /**
2795
+ * Fetches and parses an OCHRE item from the OCHRE API
2796
+ *
2797
+ * @param uuid - The UUID of the OCHRE item to fetch
2798
+ * @returns Object containing the parsed OCHRE item and its metadata, or null if the fetch/parse fails
2799
+ *
2800
+ * @example
2801
+ * ```ts
2802
+ * const result = await fetchItem("123e4567-e89b-12d3-a456-426614174000");
2803
+ * if (result === null) {
2804
+ * console.error("Failed to fetch OCHRE item");
2805
+ * return;
2806
+ * }
2807
+ * const { metadata, belongsTo, item, category } = result;
2808
+ * console.log(`Fetched OCHRE item: ${item.identification.label} with category ${category}`);
2809
+ * ```
2810
+ *
2811
+ * Or, if you want to fetch a specific category, you can do so by passing the category as an argument:
2812
+ * ```ts
2813
+ * const result = await fetchItem("123e4567-e89b-12d3-a456-426614174000", "resource");
2814
+ * const { metadata, belongsTo, item, category } = result;
2815
+ * console.log(item.category); // "resource"
2816
+ * ```
2817
+ *
2818
+ * @remarks
2819
+ * The returned OCHRE item includes:
2820
+ * - Item metadata
2821
+ * - Item belongsTo information
2822
+ * - Item content
2823
+ * - Item category
2824
+ *
2825
+ * If the fetch/parse fails, the returned object will have an `error` property.
2826
+ */
2827
+ async function fetchItem(uuid, category, setCategory, customFetch) {
2828
+ try {
2829
+ const [error, data] = await fetchByUuid(uuid, customFetch);
2830
+ if (error !== null) throw new Error(error);
2831
+ const categoryKey = getItemCategory(Object.keys(data.ochre));
2832
+ let item;
2833
+ switch (categoryKey) {
2834
+ case "resource":
2835
+ if (!("resource" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'resource' key");
2836
+ item = parseResource(data.ochre.resource);
2837
+ break;
2838
+ case "spatialUnit":
2839
+ if (!("spatialUnit" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'spatialUnit' key");
2840
+ item = parseSpatialUnit(data.ochre.spatialUnit);
2841
+ break;
2842
+ case "concept":
2843
+ if (!("concept" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'concept' key");
2844
+ item = parseConcept(data.ochre.concept);
2845
+ break;
2846
+ case "period":
2847
+ if (!("period" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'period' key");
2848
+ item = parsePeriod(data.ochre.period);
2849
+ break;
2850
+ case "bibliography":
2851
+ if (!("bibliography" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'bibliography' key");
2852
+ item = parseBibliography(data.ochre.bibliography);
2853
+ break;
2854
+ case "person":
2855
+ if (!("person" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'person' key");
2856
+ item = parsePerson(data.ochre.person);
2857
+ break;
2858
+ case "propertyValue":
2859
+ if (!("propertyValue" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'propertyValue' key");
2860
+ item = parsePropertyValue(data.ochre.propertyValue);
2861
+ break;
2862
+ case "set":
2863
+ if (!("set" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'set' key");
2864
+ item = parseSet(data.ochre.set, setCategory);
2865
+ break;
2866
+ case "tree":
2867
+ if (!("tree" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'tree' key");
2868
+ item = parseTree(data.ochre.tree, category, setCategory);
2869
+ break;
2870
+ default: throw new Error("Invalid category");
2871
+ }
2872
+ return {
2873
+ error: null,
2874
+ metadata: parseMetadata(data.ochre.metadata),
2875
+ belongsTo: {
2876
+ uuid: data.ochre.uuidBelongsTo,
2877
+ abbreviation: parseFakeString(data.ochre.belongsTo)
2878
+ },
2879
+ item,
2880
+ category
2881
+ };
2882
+ } catch (error) {
2883
+ return {
2884
+ error: error instanceof Error ? error.message : "Unknown error",
2885
+ metadata: void 0,
2886
+ belongsTo: void 0,
2887
+ item: void 0,
2888
+ category: void 0
2889
+ };
2890
+ }
2891
+ }
2892
+
2893
+ //#endregion
2894
+ //#region src/utils/fetchers/property-query.ts
2895
+ const PROJECT_SCOPE = "0c0aae37-7246-495b-9547-e25dbf5b99a3";
2896
+ const BELONG_TO_COLLECTION_UUID = "30054cb2-909a-4f34-8db9-8fe7369d691d";
2897
+ const UNASSIGNED_UUID = "e28e29af-b663-c0ac-ceb6-11a688fca0dd";
2898
+ /**
2899
+ * Check if a string is a valid UUID
2900
+ * @param value - The string to check
2901
+ * @returns True if the string is a valid UUID, false otherwise
2902
+ */
2903
+ function isUUID(value) {
2904
+ return /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value);
2905
+ }
2906
+ /**
2907
+ * Schema for a single item in the OCHRE API response
2908
+ */
2909
+ const responseItemSchema = z.object({
2910
+ property: z.string().refine(isUUID),
2911
+ category: z.object({
2912
+ uuid: z.string().refine(isUUID),
2913
+ content: z.string()
2914
+ }),
2915
+ value: z.object({
2916
+ uuid: z.string().refine(isUUID).optional(),
2917
+ category: z.string().optional(),
2918
+ type: z.string().optional(),
2919
+ dataType: z.string(),
2920
+ publicationDateTime: z.iso.datetime().optional(),
2921
+ content: z.string().optional(),
2922
+ rawValue: z.string().optional()
2923
+ })
2924
+ });
2925
+ /**
2926
+ * Schema for the OCHRE API response
2927
+ */
2928
+ const responseSchema = z.object({ result: z.object({ item: z.union([responseItemSchema, z.array(responseItemSchema)]) }) });
2929
+ /**
2930
+ * Build an XQuery string to fetch properties from the OCHRE API
2931
+ * @param scopeUuids - An array of scope UUIDs to filter by
2932
+ * @param propertyUuids - An array of property UUIDs to fetch
2933
+ * @returns An XQuery string
2934
+ */
2935
+ function buildXQuery(scopeUuids, propertyUuids) {
2936
+ let collectionScopeFilter = "";
2937
+ if (scopeUuids.length > 0) collectionScopeFilter = `[properties/property[label/@uuid="${BELONG_TO_COLLECTION_UUID}"][value[${scopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ")}]]]`;
2938
+ const propertyFilters = propertyUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
2939
+ return `for $q in input()/ochre[@uuidBelongsTo="${PROJECT_SCOPE}"]/*${collectionScopeFilter}/properties//property[label[${propertyFilters}]]
2940
+ return <item>
2941
+ <property>{xs:string($q/label/@uuid)}</property>
2942
+ <value> {$q/*[2]/@*} {$q/*[2]/content[1]/string/text()} </value>
2943
+ <category> {$q/ancestor::node()[local-name(.)="properties"]/../@uuid} {local-name($q/ancestor::node()[local-name(.)="properties"]/../self::node())} </category>
2944
+ </item>`;
2945
+ }
2946
+ /**
2947
+ * Fetches and parses a property query from the OCHRE API
2948
+ *
2949
+ * @param scopeUuids - The scope UUIDs to filter by
2950
+ * @param propertyUuids - The property UUIDs to query by
2951
+ * @param customFetch - A custom fetch function to use instead of the default fetch
2952
+ * @returns The parsed property query or null if the fetch/parse fails
2953
+ *
2954
+ * @example
2955
+ * ```ts
2956
+ * const propertyQuery = await fetchPropertyQuery(["0c0aae37-7246-495b-9547-e25dbf5b99a3"], ["9c4da06b-f15e-40af-a747-0933eaf3587e"]);
2957
+ * if (propertyQuery === null) {
2958
+ * console.error("Failed to fetch property query");
2959
+ * return;
2960
+ * }
2961
+ * console.log(`Fetched property query: ${propertyQuery.item}`);
2962
+ * ```
2963
+ *
2964
+ * @remarks
2965
+ * The returned property query includes:
2966
+ * - Property items
2967
+ */
2968
+ async function fetchPropertyQuery(scopeUuids, propertyUuids, customFetch) {
2969
+ try {
2970
+ const xquery = buildXQuery(scopeUuids, propertyUuids);
2971
+ const response = await (customFetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(xquery)}&format=json`);
2972
+ if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`);
2973
+ const data = await response.json();
2974
+ const parsedResultRaw = responseSchema.parse(data);
2975
+ const parsedItems = Array.isArray(parsedResultRaw.result.item) ? parsedResultRaw.result.item : [parsedResultRaw.result.item];
2976
+ const items = {};
2977
+ for (const item of parsedItems) {
2978
+ const categoryUuid = item.category.uuid;
2979
+ const valueUuid = item.value.uuid;
2980
+ const valueContent = item.value.rawValue ?? item.value.content ?? "";
2981
+ if (valueContent in items) items[valueContent].resultUuids.push(categoryUuid);
2982
+ else items[valueContent] = {
2983
+ value: {
2984
+ uuid: valueUuid ?? null,
2985
+ category: item.value.category ?? null,
2986
+ type: item.value.type ?? null,
2987
+ dataType: item.value.dataType,
2988
+ publicationDateTime: item.value.publicationDateTime ?? null,
2989
+ content: item.value.rawValue ?? item.value.content ?? "",
2990
+ label: item.value.rawValue != null && item.value.content != null ? item.value.content : null
2991
+ },
2992
+ resultUuids: [categoryUuid]
2993
+ };
2994
+ }
2995
+ return {
2996
+ items: Object.values(items).filter((result) => result.value.uuid !== UNASSIGNED_UUID).toSorted((a, b) => {
2997
+ const aValue = a.value.label ?? a.value.content;
2998
+ const bValue = b.value.label ?? b.value.content;
2999
+ return aValue.localeCompare(bValue, "en-US");
3000
+ }),
3001
+ error: null
3002
+ };
3003
+ } catch (error) {
3004
+ console.error(error);
3005
+ return {
3006
+ items: null,
3007
+ error: error instanceof Error ? error.message : "Failed to fetch property query"
3008
+ };
3009
+ }
3010
+ }
3011
+
3012
+ //#endregion
3013
+ //#region src/utils/fetchers/uuid-metadata.ts
3014
+ /**
3015
+ * Fetches raw OCHRE metadata by UUID from the OCHRE API
3016
+ *
3017
+ * @param uuid - The UUID of the OCHRE item to fetch
3018
+ * @returns An object containing the OCHRE metadata or an error message
3019
+ *
3020
+ * @example
3021
+ * ```ts
3022
+ * const { item, error } = await fetchByUuidMetadata("123e4567-e89b-12d3-a456-426614174000");
3023
+ * if (error !== null) {
3024
+ * console.error(`Failed to fetch: ${error}`);
3025
+ * return;
3026
+ * }
3027
+ * // Process data...
3028
+ * ```
3029
+ */
3030
+ async function fetchByUuidMetadata(uuid, customFetch) {
3031
+ try {
3032
+ const parsedUuid = uuidSchema.parse(uuid);
3033
+ 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`);
3034
+ if (!response.ok) throw new Error("Failed to fetch metadata");
3035
+ const data = await response.json();
3036
+ const projectIdentification = {
3037
+ ...parseIdentification(data.result.project.identification),
3038
+ website: data.result.project.identification.website ?? null
3039
+ };
3040
+ return {
3041
+ item: {
3042
+ item: {
3043
+ uuid,
3044
+ name: parseIdentification(data.result.item.identification).label,
3045
+ type: data.result.item.type
3046
+ },
3047
+ project: {
3048
+ name: projectIdentification.label,
3049
+ website: projectIdentification.website ?? null
3050
+ }
3051
+ },
3052
+ error: null
3053
+ };
3054
+ } catch (error) {
3055
+ return {
3056
+ item: null,
3057
+ error: error instanceof Error ? error.message : "Unknown error"
3058
+ };
3059
+ }
3060
+ }
3061
+
3062
+ //#endregion
3063
+ //#region src/utils/fetchers/website.ts
3064
+ const KNOWN_ABBREVIATIONS = {
3065
+ "uchicago-node": "60a1e386-7e53-4e14-b8cf-fb4ed953d57e",
3066
+ "uchicago-node-staging": "62b60a47-fad5-49d7-a06a-2fa059f6e79a",
3067
+ "guerrilla-television": "fad1e1bd-989d-4159-b195-4c32adc5cdc7",
3068
+ "mapping-chicagoland": "8db5e83e-0c06-48b7-b4ac-a060d9bb5689",
3069
+ "hannah-papanek": "20b2c919-021f-4774-b2c3-2f1ae5b910e7",
3070
+ mepa: "85ddaa5a-535b-4809-8714-855d2d812a3e",
3071
+ ssmc: "8ff977dd-d440-40f5-ad93-8ad7e2d39e74",
3072
+ "sosc-core-at-smart": "db26c953-9b2a-4691-a909-5e8726b531d7"
3073
+ };
3074
+ /**
3075
+ * Fetches and parses a website configuration from the OCHRE API
3076
+ *
3077
+ * @param abbreviation - The abbreviation identifier for the website
3078
+ * @returns The parsed website configuration or null if the fetch/parse fails
3079
+ *
3080
+ * @example
3081
+ * ```ts
3082
+ * const website = await fetchWebsite("guerrilla-television");
3083
+ * if (website === null) {
3084
+ * console.error("Failed to fetch website");
3085
+ * return;
3086
+ * }
3087
+ * console.log(`Fetched website: ${website.identification.label}`);
3088
+ * console.log(`Contains ${website.pages.length.toLocaleString()} pages`);
3089
+ * ```
3090
+ *
3091
+ * @remarks
3092
+ * The returned website configuration includes:
3093
+ * - Website metadata and identification
3094
+ * - Page structure and content
3095
+ * - Layout and styling properties
3096
+ * - Navigation configuration
3097
+ * - Sidebar elements
3098
+ * - Project information
3099
+ * - Creator details
3100
+ *
3101
+ * The abbreviation is case-insensitive and should match the website's configured abbreviation in OCHRE.
3102
+ */
3103
+ async function fetchWebsite(abbreviation, customFetch) {
3104
+ try {
3105
+ const uuid = KNOWN_ABBREVIATIONS[abbreviation.toLocaleLowerCase("en-US")];
3106
+ 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`);
3107
+ if (!response.ok) throw new Error("Failed to fetch website");
3108
+ const data = await response.json();
3109
+ const result = "result" in data && !Array.isArray(data.result) ? data.result : !("result" in data) ? data : null;
3110
+ if (result == null || !("tree" in result.ochre)) throw new Error("Failed to fetch website");
3111
+ const projectIdentification = result.ochre.metadata.project?.identification ? parseIdentification(result.ochre.metadata.project.identification) : null;
3112
+ return [null, parseWebsite(result.ochre.tree, projectIdentification?.label ?? "", result.ochre.metadata.project?.identification.website ?? null)];
3113
+ } catch (error) {
3114
+ console.error(error);
3115
+ return [error instanceof Error ? error.message : "Unknown error", null];
3116
+ }
3117
+ }
3118
+
3119
+ //#endregion
3120
+ export { fetchByUuidMetadata, fetchGallery, fetchItem, fetchPropertyQuery, fetchWebsite, filterProperties, getPropertyByLabel, getPropertyByUuid, getPropertyValueByLabel, getPropertyValueByUuid, getPropertyValuesByLabel, getPropertyValuesByUuid, getUniqueProperties, getUniquePropertyLabels };