ochre-sdk 0.19.3 → 0.19.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -274,8 +274,8 @@ function parseStringDocumentItem(item) {
274
274
  for (const link of itemLinks) if ("resource" in link) {
275
275
  const linkResource = Array.isArray(link.resource) ? link.resource[0] : link.resource;
276
276
  let linkContent = null;
277
- if (linkResource.content != null) linkContent = parseFakeString(linkResource.content).replaceAll("<", String.raw`\<`).replaceAll("{", String.raw`\{`);
278
- switch (linkResource.type) {
277
+ if (linkResource?.content != null) linkContent = parseFakeString(linkResource.content).replaceAll("<", String.raw`\<`).replaceAll("{", String.raw`\{`);
278
+ switch (linkResource?.type) {
279
279
  case "IIIF":
280
280
  case "image": if (linkResource.rend === "inline") return applyWhitespaceToResult(`<InlineImage uuid="${linkResource.uuid}" ${linkContent !== null ? `content="${linkContent}"` : ""} height={${linkResource.height?.toString() ?? "null"}} width={${linkResource.width?.toString() ?? "null"}} />`, itemWhitespace);
281
281
  else if (linkResource.publicationDateTime != null) {
@@ -328,28 +328,28 @@ function parseStringDocumentItem(item) {
328
328
  }
329
329
  } else if ("spatialUnit" in link) {
330
330
  const linkSpatialUnit = Array.isArray(link.spatialUnit) ? link.spatialUnit[0] : link.spatialUnit;
331
- if (linkSpatialUnit.publicationDateTime != null) return applyWhitespaceToResult(`<InternalLink uuid="${linkSpatialUnit.uuid}">${itemString}</InternalLink>`, itemWhitespace);
331
+ if (linkSpatialUnit?.publicationDateTime != null) return applyWhitespaceToResult(`<InternalLink uuid="${linkSpatialUnit.uuid}">${itemString}</InternalLink>`, itemWhitespace);
332
332
  else return applyWhitespaceToResult(`<TooltipSpan>${itemString}</TooltipSpan>`, itemWhitespace);
333
333
  } else if ("concept" in link) {
334
334
  const linkConcept = Array.isArray(link.concept) ? link.concept[0] : link.concept;
335
- if (linkConcept.publicationDateTime != null) return applyWhitespaceToResult(`<InternalLink uuid="${linkConcept.uuid}">${itemString}</InternalLink>`, itemWhitespace);
335
+ if (linkConcept?.publicationDateTime != null) return applyWhitespaceToResult(`<InternalLink uuid="${linkConcept.uuid}">${itemString}</InternalLink>`, itemWhitespace);
336
336
  else return applyWhitespaceToResult(`<TooltipSpan>${itemString}</TooltipSpan>`, itemWhitespace);
337
337
  } else if ("set" in link) {
338
338
  const linkSet = Array.isArray(link.set) ? link.set[0] : link.set;
339
- if (linkSet.publicationDateTime != null) return applyWhitespaceToResult(`<InternalLink uuid="${linkSet.uuid}">${itemString}</InternalLink>`, itemWhitespace);
339
+ if (linkSet?.publicationDateTime != null) return applyWhitespaceToResult(`<InternalLink uuid="${linkSet.uuid}">${itemString}</InternalLink>`, itemWhitespace);
340
340
  else return applyWhitespaceToResult(`<TooltipSpan>${itemString}</TooltipSpan>`, itemWhitespace);
341
341
  } else if ("person" in link) {
342
342
  const linkPerson = Array.isArray(link.person) ? link.person[0] : link.person;
343
- const linkContent = linkPerson.identification ? [
343
+ const linkContent = linkPerson?.identification != null ? [
344
344
  "string",
345
345
  "number",
346
346
  "boolean"
347
347
  ].includes(typeof linkPerson.identification.label) ? parseFakeString(linkPerson.identification.label) : parseStringContent(linkPerson.identification.label) : null;
348
- if (linkPerson.publicationDateTime != null) return applyWhitespaceToResult(`<InternalLink uuid="${linkPerson.uuid}">${itemString}</InternalLink>`, itemWhitespace);
348
+ if (linkPerson?.publicationDateTime != null) return applyWhitespaceToResult(`<InternalLink uuid="${linkPerson.uuid}">${itemString}</InternalLink>`, itemWhitespace);
349
349
  else return applyWhitespaceToResult(`<TooltipSpan${linkContent !== null ? ` content="${linkContent}"` : ""}>${itemString}</TooltipSpan>`, itemWhitespace);
350
350
  } else if ("bibliography" in link) {
351
351
  const linkBibliography = Array.isArray(link.bibliography) ? link.bibliography[0] : link.bibliography;
352
- if (linkBibliography.publicationDateTime != null) return applyWhitespaceToResult(`<InternalLink uuid="${linkBibliography.uuid}">${itemString}</InternalLink>`, itemWhitespace);
352
+ if (linkBibliography?.publicationDateTime != null) return applyWhitespaceToResult(`<InternalLink uuid="${linkBibliography.uuid}">${itemString}</InternalLink>`, itemWhitespace);
353
353
  else return applyWhitespaceToResult(`<TooltipSpan>${itemString}</TooltipSpan>`, itemWhitespace);
354
354
  }
355
355
  }
@@ -722,755 +722,406 @@ const boundsSchema = z.string().transform((str, ctx) => {
722
722
  }).pipe(z.tuple([z.tuple([z.number(), z.number()]), z.tuple([z.number(), z.number()])], { message: "Must contain exactly 2 coordinate pairs" }));
723
723
 
724
724
  //#endregion
725
- //#region src/utils/getters.ts
726
- const DEFAULT_OPTIONS = { includeNestedProperties: false };
725
+ //#region src/utils/parse/index.ts
727
726
  /**
728
- * Finds a property by its UUID in an array of properties
727
+ * Parses raw identification data into the standardized Identification type
729
728
  *
730
- * @param properties - Array of properties to search through
731
- * @param uuid - The UUID to search for
732
- * @param options - Search options, including whether to include nested properties
733
- * @returns The matching Property object, or null if not found
729
+ * @param identification - Raw identification data from OCHRE format
730
+ * @returns Parsed Identification object with label and abbreviation
734
731
  */
735
- function getPropertyByUuid(properties, uuid, options = DEFAULT_OPTIONS) {
736
- const { includeNestedProperties } = options;
737
- const property = properties.find((property) => property.uuid === uuid);
738
- if (property) return property;
739
- if (includeNestedProperties) {
740
- for (const property of properties) if (property.properties.length > 0) {
741
- const nestedResult = getPropertyByUuid(property.properties, uuid, { includeNestedProperties });
742
- if (nestedResult) return nestedResult;
732
+ function parseIdentification(identification) {
733
+ try {
734
+ const result = {
735
+ label: parseFakeStringOrContent(identification.label),
736
+ abbreviation: "",
737
+ code: identification.code ?? null
738
+ };
739
+ for (const key of Object.keys(identification)) {
740
+ if (key === "label" || key === "code") continue;
741
+ const raw = identification[key];
742
+ result[key] = parseFakeStringOrContent(raw);
743
743
  }
744
+ return result;
745
+ } catch (error) {
746
+ console.error(error);
747
+ return {
748
+ label: "",
749
+ abbreviation: "",
750
+ code: null
751
+ };
744
752
  }
745
- return null;
746
753
  }
747
754
  /**
748
- * Retrieves all values for a property with the given UUID
755
+ * Parses raw language data into an array of language codes
749
756
  *
750
- * @param properties - Array of properties to search through
751
- * @param uuid - The UUID to search for
752
- * @param options - Search options, including whether to include nested properties
753
- * @returns Array of property values as strings, or null if property not found
757
+ * @param language - Raw language data, either single or array
758
+ * @returns Array of language codes as strings
754
759
  */
755
- function getPropertyValuesByUuid(properties, uuid, options = DEFAULT_OPTIONS) {
756
- const { includeNestedProperties } = options;
757
- const property = properties.find((property) => property.uuid === uuid);
758
- if (property) return property.values.map((value) => value.content);
759
- if (includeNestedProperties) {
760
- for (const property of properties) if (property.properties.length > 0) {
761
- const nestedResult = getPropertyValuesByUuid(property.properties, uuid, { includeNestedProperties });
762
- if (nestedResult) return nestedResult;
763
- }
764
- }
765
- return null;
760
+ function parseLanguages(language) {
761
+ if (language == null) return ["eng"];
762
+ if (typeof language === "string") return [language];
763
+ if (Array.isArray(language)) return language.map((lang) => typeof lang === "object" ? parseStringContent(lang) : lang);
764
+ else return [parseStringContent(language)];
766
765
  }
767
766
  /**
768
- * Gets the first value of a property with the given UUID
767
+ * Parses raw metadata into the standardized Metadata type
769
768
  *
770
- * @param properties - Array of properties to search through
771
- * @param uuid - The UUID to search for
772
- * @param options - Search options, including whether to include nested properties
773
- * @returns The first property value as string, or null if property not found
769
+ * @param metadata - Raw metadata from OCHRE format
770
+ * @returns Parsed Metadata object
774
771
  */
775
- function getPropertyValueByUuid(properties, uuid, options = DEFAULT_OPTIONS) {
776
- const { includeNestedProperties } = options;
777
- const values = getPropertyValuesByUuid(properties, uuid, { includeNestedProperties });
778
- if (values !== null && values.length > 0) return values[0];
779
- if (includeNestedProperties) {
780
- for (const property of properties) if (property.properties.length > 0) {
781
- const nestedResult = getPropertyValueByUuid(property.properties, uuid, { includeNestedProperties });
782
- if (nestedResult !== null) return nestedResult;
783
- }
784
- }
785
- return null;
772
+ function parseMetadata(metadata) {
773
+ let identification = {
774
+ label: "",
775
+ abbreviation: "",
776
+ code: null
777
+ };
778
+ if (metadata.item) if (metadata.item.label || metadata.item.abbreviation) identification = {
779
+ label: metadata.item.label ? parseStringContent(metadata.item.label) : "",
780
+ abbreviation: metadata.item.abbreviation ? parseStringContent(metadata.item.abbreviation) : "",
781
+ code: metadata.item.identification.code ?? null
782
+ };
783
+ else identification = parseIdentification(metadata.item.identification);
784
+ const projectId = metadata.project?.identification ? parseIdentification(metadata.project.identification) : null;
785
+ return {
786
+ project: projectId ? {
787
+ identification: {
788
+ ...projectId,
789
+ website: metadata.project?.identification.website ?? null
790
+ },
791
+ dateFormat: metadata.project?.dateFormat ?? null,
792
+ page: metadata.project?.page ?? null
793
+ } : null,
794
+ collection: metadata.collection ? {
795
+ identification: parseIdentification(metadata.collection.identification),
796
+ page: metadata.collection.page
797
+ } : null,
798
+ publication: metadata.publication ? {
799
+ identification: parseIdentification(metadata.publication.identification),
800
+ page: metadata.publication.page
801
+ } : null,
802
+ item: metadata.item ? {
803
+ identification,
804
+ category: metadata.item.category,
805
+ type: metadata.item.type,
806
+ maxLength: metadata.item.maxLength ?? null
807
+ } : null,
808
+ dataset: parseFakeStringOrContent(metadata.dataset),
809
+ publisher: parseFakeStringOrContent(metadata.publisher),
810
+ languages: parseLanguages(metadata.language),
811
+ identifier: parseFakeStringOrContent(metadata.identifier),
812
+ description: parseFakeStringOrContent(metadata.description)
813
+ };
786
814
  }
787
815
  /**
788
- * Finds a property by its label in an array of properties
816
+ * Parses raw context item data into the standardized ContextItem type
789
817
  *
790
- * @param properties - Array of properties to search through
791
- * @param label - The label to search for
792
- * @param options - Search options, including whether to include nested properties
793
- * @returns The matching Property object, or null if not found
818
+ * @param contextItem - Raw context item data from OCHRE format
819
+ * @returns Parsed ContextItem object
794
820
  */
795
- function getPropertyByLabel(properties, label, options = DEFAULT_OPTIONS) {
796
- const { includeNestedProperties } = options;
797
- const property = properties.find((property) => property.label === label);
798
- if (property) return property;
799
- if (includeNestedProperties) {
800
- for (const property of properties) if (property.properties.length > 0) {
801
- const nestedResult = getPropertyByLabel(property.properties, label, { includeNestedProperties });
802
- if (nestedResult) return nestedResult;
803
- }
804
- }
805
- return null;
821
+ function parseContextItem(contextItem) {
822
+ return {
823
+ uuid: contextItem.uuid,
824
+ publicationDateTime: parseOptionalDate(contextItem.publicationDateTime),
825
+ number: contextItem.n,
826
+ content: parseFakeString(contextItem.content)
827
+ };
806
828
  }
807
829
  /**
808
- * Finds a property by its label and all values in an array of properties
830
+ * Parses raw context data into the standardized Context type
809
831
  *
810
- * @param properties - Array of properties to search through
811
- * @param label - The label to search for
812
- * @param values - The values to search for
813
- * @param options - Search options, including whether to include nested properties
814
- * @returns The matching Property object, or null if not found or all values do not match
832
+ * @param context - Raw context data from OCHRE format
833
+ * @returns Parsed Context object
815
834
  */
816
- function getPropertyByLabelAndValues(properties, label, values, options = DEFAULT_OPTIONS) {
817
- const { includeNestedProperties } = options;
818
- const property = properties.find((property) => property.label === label && deepEqual(property.values, values));
819
- if (property) return property;
820
- if (includeNestedProperties) {
821
- for (const property of properties) if (property.properties.length > 0) {
822
- const nestedResult = getPropertyByLabelAndValues(property.properties, label, values, { includeNestedProperties });
823
- if (nestedResult) return nestedResult;
824
- }
825
- }
826
- return null;
835
+ function parseContext(context) {
836
+ return {
837
+ nodes: ensureArray(context.context).map((ctx) => ({
838
+ tree: parseContextItem(ctx.tree),
839
+ project: parseContextItem(ctx.project),
840
+ spatialUnit: "spatialUnit" in ctx && ctx.spatialUnit ? ensureArray(ctx.spatialUnit).map((element) => parseContextItem(element)) : []
841
+ })),
842
+ displayPath: context.displayPath
843
+ };
827
844
  }
828
845
  /**
829
- * Finds a property by its label and value in an array of properties
846
+ * Parses raw license data into the standardized License type
830
847
  *
831
- * @param properties - Array of properties to search through
832
- * @param label - The label to search for
833
- * @param value - The value to search for
834
- * @param options - Search options, including whether to include nested properties
835
- * @returns The matching Property object, or null if not found or value does not match
848
+ * @param license - Raw license data from OCHRE format
849
+ * @returns Parsed License object or null if invalid
836
850
  */
837
- function getPropertyByLabelAndValue(properties, label, value, options = DEFAULT_OPTIONS) {
838
- const { includeNestedProperties } = options;
839
- const property = properties.find((property) => property.label === label && property.values.some((v) => v.content === value));
840
- if (property) return property;
841
- if (includeNestedProperties) {
842
- for (const property of properties) if (property.properties.length > 0) {
843
- const nestedResult = getPropertyByLabelAndValue(property.properties, label, value, { includeNestedProperties });
844
- if (nestedResult) return nestedResult;
845
- }
846
- }
847
- return null;
851
+ function parseLicense(license) {
852
+ if (typeof license.license === "string") return null;
853
+ return {
854
+ content: license.license.content,
855
+ url: license.license.target
856
+ };
848
857
  }
849
858
  /**
850
- * Retrieves all values for a property with the given label
859
+ * Parses raw person data into the standardized Person type
851
860
  *
852
- * @param properties - Array of properties to search through
853
- * @param label - The label to search for
854
- * @param options - Search options, including whether to include nested properties
855
- * @returns Array of property values as strings, or null if property not found
861
+ * @param person - Raw person data from OCHRE format
862
+ * @returns Parsed Person object
856
863
  */
857
- function getPropertyValuesByLabel(properties, label, options = DEFAULT_OPTIONS) {
858
- const { includeNestedProperties } = options;
859
- const property = properties.find((property) => property.label === label);
860
- if (property) return property.values.map((value) => value.content);
861
- if (includeNestedProperties) {
862
- for (const property of properties) if (property.properties.length > 0) {
863
- const nestedResult = getPropertyValuesByLabel(property.properties, label, { includeNestedProperties });
864
- if (nestedResult) return nestedResult;
865
- }
866
- }
867
- return null;
864
+ function parsePerson(person, metadata, persistentUrl, belongsTo) {
865
+ return {
866
+ uuid: person.uuid,
867
+ category: "person",
868
+ belongsTo: belongsTo ?? null,
869
+ metadata: metadata ?? null,
870
+ publicationDateTime: parseOptionalDate(person.publicationDateTime),
871
+ persistentUrl: persistentUrl ?? null,
872
+ type: person.type ?? null,
873
+ number: person.n ?? null,
874
+ context: person.context ? parseContext(person.context) : null,
875
+ date: person.date ?? null,
876
+ identification: person.identification ? parseIdentification(person.identification) : null,
877
+ availability: person.availability ? parseLicense(person.availability) : null,
878
+ image: person.image ? parseImage(person.image) : null,
879
+ address: person.address ? {
880
+ country: person.address.country ?? null,
881
+ city: person.address.city ?? null,
882
+ state: person.address.state ?? null
883
+ } : null,
884
+ description: person.description ? parseFakeStringOrContent(person.description) : null,
885
+ coordinates: parseCoordinates(person.coordinates),
886
+ content: person.content != null ? parseFakeString(person.content) : null,
887
+ notes: person.notes ? parseNotes(ensureArray(person.notes.note)) : [],
888
+ links: person.links ? parseLinks(ensureArray(person.links)) : [],
889
+ events: person.events ? parseEvents(ensureArray(person.events.event)) : [],
890
+ properties: person.properties ? parseProperties(ensureArray(person.properties.property)) : [],
891
+ bibliographies: person.bibliographies ? parseBibliographies(ensureArray(person.bibliographies.bibliography)) : []
892
+ };
868
893
  }
869
894
  /**
870
- * Gets the first value of a property with the given label
895
+ * Parses raw person data into the standardized Person type
871
896
  *
872
- * @param properties - Array of properties to search through
873
- * @param label - The label to search for
874
- * @param options - Search options, including whether to include nested properties
875
- * @returns The first property value as string, or null if property not found
897
+ * @param persons - Array of raw person data from OCHRE format
898
+ * @returns Array of parsed Person objects
876
899
  */
877
- function getPropertyValueByLabel(properties, label, options = DEFAULT_OPTIONS) {
878
- const { includeNestedProperties } = options;
879
- const values = getPropertyValuesByLabel(properties, label, { includeNestedProperties });
880
- if (values !== null && values.length > 0) return values[0];
881
- if (includeNestedProperties) {
882
- for (const property of properties) if (property.properties.length > 0) {
883
- const nestedResult = getPropertyValueByLabel(property.properties, label, { includeNestedProperties });
884
- if (nestedResult !== null) return nestedResult;
885
- }
886
- }
887
- return null;
900
+ function parsePersons(persons) {
901
+ return persons.map((person) => parsePerson(person));
888
902
  }
889
903
  /**
890
- * Gets all unique properties from an array of properties
904
+ * Parses an array of raw links into standardized Link objects
891
905
  *
892
- * @param properties - Array of properties to get unique properties from
893
- * @param options - Search options, including whether to include nested properties
894
- * @returns Array of unique properties
906
+ * @param linkRaw - Raw OCHRE link
907
+ * @returns Parsed Link object
895
908
  */
896
- function getUniqueProperties(properties, options = DEFAULT_OPTIONS) {
897
- const { includeNestedProperties } = options;
898
- const uniqueProperties = new Array();
899
- for (const property of properties) {
900
- if (uniqueProperties.some((p) => p.uuid === property.uuid)) continue;
901
- uniqueProperties.push(property);
902
- if (property.properties.length > 0 && includeNestedProperties) {
903
- const nestedProperties = getUniqueProperties(property.properties, { includeNestedProperties: true });
904
- for (const property of nestedProperties) {
905
- if (uniqueProperties.some((p) => p.uuid === property.uuid)) continue;
906
- uniqueProperties.push(property);
907
- }
908
- }
909
- }
910
- return uniqueProperties;
909
+ function parseLink(linkRaw) {
910
+ const categoryKey = [
911
+ "resource",
912
+ "spatialUnit",
913
+ "concept",
914
+ "set",
915
+ "tree",
916
+ "person",
917
+ "bibliography",
918
+ "propertyVariable",
919
+ "propertyValue"
920
+ ].find((key) => key in linkRaw);
921
+ if (!categoryKey) throw new Error(`Invalid link provided: ${JSON.stringify(linkRaw, null, 2)}`);
922
+ const links = linkRaw[categoryKey];
923
+ if (links == null) return [];
924
+ return ensureArray(links).map((link) => {
925
+ const returnLink = {
926
+ category: categoryKey,
927
+ content: "content" in link && link.content != null ? parseFakeString(link.content) : null,
928
+ href: "href" in link && link.href != null ? link.href : null,
929
+ fileFormat: "fileFormat" in link && link.fileFormat != null ? link.fileFormat : null,
930
+ fileSize: "fileSize" in link && link.fileSize != null ? link.fileSize : null,
931
+ uuid: link.uuid ?? null,
932
+ type: link.type ?? null,
933
+ identification: link.identification ? parseIdentification(link.identification) : null,
934
+ description: "description" in link && link.description != null ? parseStringContent(link.description) : null,
935
+ image: null,
936
+ bibliographies: "bibliography" in linkRaw && linkRaw.bibliography != null ? parseBibliographies(ensureArray(linkRaw.bibliography)) : null,
937
+ publicationDateTime: parseOptionalDate(link.publicationDateTime)
938
+ };
939
+ if ("height" in link && link.height != null && link.width != null && link.heightPreview != null && link.widthPreview != null) returnLink.image = {
940
+ isInline: link.rend === "inline",
941
+ isPrimary: link.isPrimary ?? false,
942
+ heightPreview: link.heightPreview,
943
+ widthPreview: link.widthPreview,
944
+ height: link.height,
945
+ width: link.width
946
+ };
947
+ return returnLink;
948
+ });
911
949
  }
912
950
  /**
913
- * Gets all unique property labels from an array of properties
951
+ * Parses an array of raw links into standardized Link objects
914
952
  *
915
- * @param properties - Array of properties to get unique property labels from
916
- * @param options - Search options, including whether to include nested properties
917
- * @returns Array of unique property labels
918
- */
919
- function getUniquePropertyLabels(properties, options = DEFAULT_OPTIONS) {
920
- const { includeNestedProperties } = options;
921
- const uniquePropertyLabels = new Array();
922
- for (const property of properties) {
923
- if (uniquePropertyLabels.includes(property.label)) continue;
924
- uniquePropertyLabels.push(property.label);
925
- if (property.properties.length > 0 && includeNestedProperties) {
926
- const nestedProperties = getUniquePropertyLabels(property.properties, { includeNestedProperties: true });
927
- for (const property of nestedProperties) {
928
- if (uniquePropertyLabels.includes(property)) continue;
929
- uniquePropertyLabels.push(property);
930
- }
931
- }
932
- }
933
- return uniquePropertyLabels;
934
- }
935
- /**
936
- * Get the leaf property values from an array of property values
937
- * @param propertyValues - The array of property values to get the leaf property values from
938
- * @returns The array of leaf property values
953
+ * @param links - Array of raw OCHRE links
954
+ * @returns Array of parsed Link objects
939
955
  */
940
- function getLeafPropertyValues(propertyValues) {
941
- return propertyValues.filter((value) => value.hierarchy.isLeaf);
956
+ function parseLinks(links) {
957
+ const result = [];
958
+ for (const link of links) result.push(...parseLink(link));
959
+ return result;
942
960
  }
943
961
  /**
944
- * Filters a property based on a label and value criteria
962
+ * Parses raw document content into a standardized Document structure
945
963
  *
946
- * @param property - The property to filter
947
- * @param filter - Filter criteria containing label and value to match
948
- * @param filter.label - The label to filter by
949
- * @param filter.value - The value to filter by
950
- * @param options - Search options, including whether to include nested properties
951
- * @returns True if the property matches the filter criteria, false otherwise
964
+ * @param document - Raw document content in OCHRE format
965
+ * @param language - Language code to use for content selection (defaults to "eng")
966
+ * @returns Parsed Document object with content and footnotes
952
967
  */
953
- function filterProperties(property, filter, options = DEFAULT_OPTIONS) {
954
- const { includeNestedProperties } = options;
955
- if (filter.label.toLocaleLowerCase("en-US") === "all fields" || property.label.toLocaleLowerCase("en-US") === filter.label.toLocaleLowerCase("en-US")) {
956
- let isFound = property.values.some((value) => {
957
- if (value.content === null) return false;
958
- if (typeof value.content === "string") {
959
- if (typeof filter.value !== "string") return false;
960
- return value.content.toLocaleLowerCase("en-US").includes(filter.value.toLocaleLowerCase("en-US"));
961
- }
962
- if (typeof value.content === "number") {
963
- if (typeof filter.value !== "number") return false;
964
- return value.content === filter.value;
965
- }
966
- if (typeof value.content === "boolean") {
967
- if (typeof filter.value !== "boolean") return false;
968
- return value.content === filter.value;
969
- }
970
- return false;
971
- });
972
- if (!isFound && includeNestedProperties) isFound = property.properties.some((property) => filterProperties(property, filter, { includeNestedProperties: true }));
973
- return isFound;
974
- }
975
- return false;
968
+ function parseDocument(document, language = "eng") {
969
+ const doc = Array.isArray(document) ? document.find((d) => d.lang === language) : document;
970
+ if (isFakeString(doc.string)) return parseEmail(parseFakeString(doc.string));
971
+ let result = "";
972
+ for (const item of ensureArray(doc.string)) result += parseStringDocumentItem(item);
973
+ return result;
976
974
  }
977
-
978
- //#endregion
979
- //#region src/utils/helpers.ts
980
975
  /**
981
- * The default API version to use
982
- *
983
- * @remarks
984
- * Version 1 of the OCHRE API is deprecated and will be removed in the future.
985
- * It points to the old Tamino server.
976
+ * Parses raw image data into a standardized Image structure
986
977
  *
987
- * Version 2 of the OCHRE API is the current version and is the default.
988
- * It points to the new MarkLogic server.
989
- */
990
- const DEFAULT_API_VERSION = 2;
991
- /**
992
- * The default page size to use for fetching paginated items
993
- */
994
- const DEFAULT_PAGE_SIZE = 48;
995
- /**
996
- * Flatten the properties of an item
997
- * @param item - The item whose properties to flatten
998
- * @returns The item with the properties flattened
978
+ * @param image - Raw image data in OCHRE format
979
+ * @returns Parsed Image object or null if invalid
999
980
  */
1000
- function flattenItemProperties(item) {
1001
- const allProperties = [];
1002
- if ("properties" in item) allProperties.push(...item.properties);
1003
- if ("observations" in item) for (const observation of item.observations) allProperties.push(...observation.properties);
1004
- if ("interpretations" in item) for (const interpretation of item.interpretations) allProperties.push(...interpretation.properties);
1005
- if ("bibliographies" in item) for (const bibliography of item.bibliographies) allProperties.push(...bibliography.properties);
981
+ function parseImage(image) {
1006
982
  return {
1007
- ...item,
1008
- properties: flattenProperties(allProperties)
983
+ publicationDateTime: parseOptionalDate(image.publicationDateTime),
984
+ identification: image.identification ? parseIdentification(image.identification) : null,
985
+ url: image.href ?? (image.htmlImgSrcPrefix == null && image.content != null ? parseFakeString(image.content) : null),
986
+ htmlPrefix: image.htmlImgSrcPrefix ?? null,
987
+ content: image.htmlImgSrcPrefix != null && image.content != null ? parseFakeString(image.content) : null,
988
+ widthPreview: image.widthPreview ?? null,
989
+ heightPreview: image.heightPreview ?? null,
990
+ width: image.width ?? null,
991
+ height: image.height ?? null
1009
992
  };
1010
993
  }
1011
-
1012
- //#endregion
1013
- //#region src/utils/parse.ts
1014
994
  /**
1015
- * Extracts CSS style properties for a given presentation variant.
995
+ * Parses raw notes into standardized Note objects
1016
996
  *
1017
- * @param properties - Array of properties to parse
1018
- * @param cssVariant - CSS variant to parse
1019
- * @returns Array of CSS styles
997
+ * @param notes - Array of raw notes in OCHRE format
998
+ * @param language - Language code for content selection (defaults to "eng")
999
+ * @returns Array of parsed Note objects
1020
1000
  */
1021
- function parseCssStylesFromProperties(properties, cssVariant) {
1022
- const cssProperties = getPropertyByLabelAndValue(properties, "presentation", cssVariant != null ? `css-${cssVariant}` : "css")?.properties ?? [];
1023
- const styles = [];
1024
- for (const property of cssProperties) {
1025
- const value = property.values[0]?.content?.toString();
1026
- if (value != null) styles.push({
1027
- label: property.label,
1028
- value
1001
+ function parseNotes(notes, language = "eng") {
1002
+ const result = [];
1003
+ for (const note of notes) {
1004
+ if (typeof note === "string") {
1005
+ if (note !== "") result.push({
1006
+ number: -1,
1007
+ title: null,
1008
+ date: null,
1009
+ authors: [],
1010
+ content: note
1011
+ });
1012
+ continue;
1013
+ }
1014
+ const notesToParse = note.content != null ? ensureArray(note.content) : [];
1015
+ if (notesToParse.length === 0) continue;
1016
+ const noteWithLanguage = notesToParse.find((item) => item.lang === language) ?? notesToParse[0];
1017
+ if (!noteWithLanguage) throw new Error(`Note does not have a valid content item: ${JSON.stringify(note, null, 2)}`);
1018
+ const content = isFakeString(noteWithLanguage.string) ? parseEmail(parseFakeString(noteWithLanguage.string)) : parseEmail(parseDocument(noteWithLanguage));
1019
+ result.push({
1020
+ number: note.noteNo,
1021
+ title: noteWithLanguage.title != null ? parseFakeString(noteWithLanguage.title) : null,
1022
+ date: note.date ?? null,
1023
+ authors: note.authors ? parsePersons(ensureArray(note.authors.author)) : [],
1024
+ content
1029
1025
  });
1030
1026
  }
1031
- return styles;
1027
+ return result;
1032
1028
  }
1033
1029
  /**
1034
- * Parses responsive CSS styles (default, tablet, mobile) from properties.
1030
+ * Parses raw coordinates data into a standardized array of Coordinate structures
1035
1031
  *
1036
- * @param properties - Array of properties to parse
1037
- * @returns Object containing responsive CSS styles
1032
+ * @param coordinates - Raw coordinates data in OCHRE format
1033
+ * @returns Parsed array of Coordinate objects
1038
1034
  */
1039
- function parseResponsiveCssStyles(properties) {
1040
- return {
1041
- default: parseCssStylesFromProperties(properties),
1042
- tablet: parseCssStylesFromProperties(properties, "tablet"),
1043
- mobile: parseCssStylesFromProperties(properties, "mobile")
1044
- };
1045
- }
1046
- /**
1047
- * Parses all context option arrays from an options object.
1048
- *
1049
- * @param options - Options object containing context options
1050
- * @param options.flattenContexts - Flatten contexts
1051
- * @param options.suppressContexts - Suppress contexts
1052
- * @param options.filterContexts - Filter contexts
1053
- * @param options.sortContexts - Sort contexts
1054
- * @param options.detailContexts - Detail contexts
1055
- * @param options.downloadContexts - Download contexts
1056
- * @param options.labelContexts - Label contexts
1057
- * @param options.prominentContexts - Prominent contexts
1058
- * @returns Parsed context options
1059
- */
1060
- function parseAllOptionContexts(options) {
1061
- function handleContexts(v) {
1062
- return parseContexts(v != null ? ensureArray(v) : []);
1063
- }
1064
- return {
1065
- flatten: handleContexts(options.flattenContexts),
1066
- suppress: handleContexts(options.suppressContexts),
1067
- filter: handleContexts(options.filterContexts),
1068
- sort: handleContexts(options.sortContexts),
1069
- detail: handleContexts(options.detailContexts),
1070
- download: handleContexts(options.downloadContexts),
1071
- label: handleContexts(options.labelContexts),
1072
- prominent: handleContexts(options.prominentContexts)
1073
- };
1074
- }
1075
- /**
1076
- * Parses raw identification data into the standardized Identification type
1077
- *
1078
- * @param identification - Raw identification data from OCHRE format
1079
- * @returns Parsed Identification object with label and abbreviation
1080
- */
1081
- function parseIdentification(identification) {
1082
- try {
1083
- const result = {
1084
- label: parseFakeStringOrContent(identification.label),
1085
- abbreviation: "",
1086
- code: identification.code ?? null
1087
- };
1088
- for (const key of Object.keys(identification)) {
1089
- if (key === "label" || key === "code") continue;
1090
- const raw = identification[key];
1091
- result[key] = parseFakeStringOrContent(raw);
1035
+ function parseCoordinates(coordinates) {
1036
+ if (coordinates == null) return [];
1037
+ const result = [];
1038
+ for (const coord of ensureArray(coordinates.coord)) {
1039
+ const source = "source" in coord && coord.source ? coord.source.context === "self" ? {
1040
+ context: "self",
1041
+ uuid: coord.source.label.uuid,
1042
+ label: parseStringContent(coord.source.label)
1043
+ } : coord.source.context === "related" ? {
1044
+ context: "related",
1045
+ uuid: coord.source.label.uuid,
1046
+ label: parseStringContent(coord.source.label),
1047
+ value: parseStringContent(coord.source.value)
1048
+ } : {
1049
+ context: "inherited",
1050
+ uuid: coord.source.label.uuid,
1051
+ item: {
1052
+ uuid: coord.source.item.label.uuid,
1053
+ label: parseStringContent(coord.source.item.label)
1054
+ },
1055
+ label: parseStringContent(coord.source.label)
1056
+ } : null;
1057
+ switch (coord.type) {
1058
+ case "point":
1059
+ result.push({
1060
+ type: coord.type,
1061
+ latitude: coord.latitude,
1062
+ longitude: coord.longitude,
1063
+ altitude: coord.altitude ?? null,
1064
+ source
1065
+ });
1066
+ break;
1067
+ case "plane":
1068
+ result.push({
1069
+ type: coord.type,
1070
+ minimum: {
1071
+ latitude: coord.minimum.latitude,
1072
+ longitude: coord.minimum.longitude
1073
+ },
1074
+ maximum: {
1075
+ latitude: coord.maximum.latitude,
1076
+ longitude: coord.maximum.longitude
1077
+ },
1078
+ source
1079
+ });
1080
+ break;
1092
1081
  }
1093
- return result;
1094
- } catch (error) {
1095
- console.error(error);
1096
- return {
1097
- label: "",
1098
- abbreviation: "",
1099
- code: null
1100
- };
1101
1082
  }
1083
+ return result;
1102
1084
  }
1103
1085
  /**
1104
- * Parses raw language data into an array of language codes
1105
- *
1106
- * @param language - Raw language data, either single or array
1107
- * @returns Array of language codes as strings
1108
- */
1109
- function parseLanguages(language) {
1110
- if (language == null) return ["eng"];
1111
- if (typeof language === "string") return [language];
1112
- if (Array.isArray(language)) return language.map((lang) => typeof lang === "object" ? parseStringContent(lang) : lang);
1113
- else return [parseStringContent(language)];
1114
- }
1115
- /**
1116
- * Parses raw metadata into the standardized Metadata type
1117
- *
1118
- * @param metadata - Raw metadata from OCHRE format
1119
- * @returns Parsed Metadata object
1120
- */
1121
- function parseMetadata(metadata) {
1122
- let identification = {
1123
- label: "",
1124
- abbreviation: "",
1125
- code: null
1126
- };
1127
- if (metadata.item) if (metadata.item.label || metadata.item.abbreviation) identification = {
1128
- label: metadata.item.label ? parseStringContent(metadata.item.label) : "",
1129
- abbreviation: metadata.item.abbreviation ? parseStringContent(metadata.item.abbreviation) : "",
1130
- code: metadata.item.identification.code ?? null
1131
- };
1132
- else identification = parseIdentification(metadata.item.identification);
1133
- const projectId = metadata.project?.identification ? parseIdentification(metadata.project.identification) : null;
1134
- return {
1135
- project: projectId ? {
1136
- identification: {
1137
- ...projectId,
1138
- website: metadata.project?.identification.website ?? null
1139
- },
1140
- dateFormat: metadata.project?.dateFormat ?? null,
1141
- page: metadata.project?.page ?? null
1142
- } : null,
1143
- collection: metadata.collection ? {
1144
- identification: parseIdentification(metadata.collection.identification),
1145
- page: metadata.collection.page
1146
- } : null,
1147
- publication: metadata.publication ? {
1148
- identification: parseIdentification(metadata.publication.identification),
1149
- page: metadata.publication.page
1150
- } : null,
1151
- item: metadata.item ? {
1152
- identification,
1153
- category: metadata.item.category,
1154
- type: metadata.item.type,
1155
- maxLength: metadata.item.maxLength ?? null
1156
- } : null,
1157
- dataset: parseFakeStringOrContent(metadata.dataset),
1158
- publisher: parseFakeStringOrContent(metadata.publisher),
1159
- languages: parseLanguages(metadata.language),
1160
- identifier: parseFakeStringOrContent(metadata.identifier),
1161
- description: parseFakeStringOrContent(metadata.description)
1162
- };
1163
- }
1164
- /**
1165
- * Parses raw context item data into the standardized ContextItem type
1166
- *
1167
- * @param contextItem - Raw context item data from OCHRE format
1168
- * @returns Parsed ContextItem object
1169
- */
1170
- function parseContextItem(contextItem) {
1171
- return {
1172
- uuid: contextItem.uuid,
1173
- publicationDateTime: parseOptionalDate(contextItem.publicationDateTime),
1174
- number: contextItem.n,
1175
- content: parseFakeString(contextItem.content)
1176
- };
1177
- }
1178
- /**
1179
- * Parses raw context data into the standardized Context type
1086
+ * Parses a raw observation into a standardized Observation structure
1180
1087
  *
1181
- * @param context - Raw context data from OCHRE format
1182
- * @returns Parsed Context object
1088
+ * @param observation - Raw observation data in OCHRE format
1089
+ * @returns Parsed Observation object
1183
1090
  */
1184
- function parseContext(context) {
1091
+ function parseObservation(observation) {
1185
1092
  return {
1186
- nodes: ensureArray(context.context).map((ctx) => ({
1187
- tree: parseContextItem(ctx.tree),
1188
- project: parseContextItem(ctx.project),
1189
- spatialUnit: "spatialUnit" in ctx && ctx.spatialUnit ? ensureArray(ctx.spatialUnit).map((element) => parseContextItem(element)) : []
1190
- })),
1191
- displayPath: context.displayPath
1093
+ number: observation.observationNo,
1094
+ date: observation.date ?? null,
1095
+ observers: observation.observers != null ? isFakeString(observation.observers) ? parseFakeString(observation.observers).split(";").map((observer) => observer.trim()) : parsePersons(ensureArray(observation.observers)) : [],
1096
+ notes: observation.notes ? parseNotes(ensureArray(observation.notes.note)) : [],
1097
+ links: observation.links ? parseLinks(ensureArray(observation.links)) : [],
1098
+ properties: observation.properties ? parseProperties(ensureArray(observation.properties.property)) : [],
1099
+ bibliographies: observation.bibliographies ? parseBibliographies(ensureArray(observation.bibliographies.bibliography)) : []
1192
1100
  };
1193
1101
  }
1194
1102
  /**
1195
- * Parses raw license data into the standardized License type
1103
+ * Parses an array of raw observations into standardized Observation objects
1196
1104
  *
1197
- * @param license - Raw license data from OCHRE format
1198
- * @returns Parsed License object or null if invalid
1105
+ * @param observations - Array of raw observations in OCHRE format
1106
+ * @returns Array of parsed Observation objects
1199
1107
  */
1200
- function parseLicense(license) {
1201
- if (typeof license.license === "string") return null;
1202
- return {
1203
- content: license.license.content,
1204
- url: license.license.target
1205
- };
1108
+ function parseObservations(observations) {
1109
+ return observations.map((obs) => parseObservation(obs));
1206
1110
  }
1207
1111
  /**
1208
- * Parses raw person data into the standardized Person type
1112
+ * Parses an array of raw events into standardized Event objects
1209
1113
  *
1210
- * @param person - Raw person data from OCHRE format
1211
- * @returns Parsed Person object
1114
+ * @param events - Array of raw events in OCHRE format
1115
+ * @returns Array of parsed Event objects
1212
1116
  */
1213
- function parsePerson(person, metadata, persistentUrl, belongsTo) {
1214
- return {
1215
- uuid: person.uuid,
1216
- category: "person",
1217
- belongsTo: belongsTo ?? null,
1218
- metadata: metadata ?? null,
1219
- publicationDateTime: parseOptionalDate(person.publicationDateTime),
1220
- persistentUrl: persistentUrl ?? null,
1221
- type: person.type ?? null,
1222
- number: person.n ?? null,
1223
- context: person.context ? parseContext(person.context) : null,
1224
- date: person.date ?? null,
1225
- identification: person.identification ? parseIdentification(person.identification) : null,
1226
- availability: person.availability ? parseLicense(person.availability) : null,
1227
- image: person.image ? parseImage(person.image) : null,
1228
- address: person.address ? {
1229
- country: person.address.country ?? null,
1230
- city: person.address.city ?? null,
1231
- state: person.address.state ?? null
1232
- } : null,
1233
- description: person.description ? parseFakeStringOrContent(person.description) : null,
1234
- coordinates: parseCoordinates(person.coordinates),
1235
- content: person.content != null ? parseFakeString(person.content) : null,
1236
- notes: person.notes ? parseNotes(ensureArray(person.notes.note)) : [],
1237
- links: person.links ? parseLinks(ensureArray(person.links)) : [],
1238
- events: person.events ? parseEvents(ensureArray(person.events.event)) : [],
1239
- properties: person.properties ? parseProperties(ensureArray(person.properties.property)) : [],
1240
- bibliographies: person.bibliographies ? parseBibliographies(ensureArray(person.bibliographies.bibliography)) : []
1241
- };
1242
- }
1243
- /**
1244
- * Parses raw person data into the standardized Person type
1245
- *
1246
- * @param persons - Array of raw person data from OCHRE format
1247
- * @returns Array of parsed Person objects
1248
- */
1249
- function parsePersons(persons) {
1250
- return persons.map((person) => parsePerson(person));
1251
- }
1252
- /**
1253
- * Parses an array of raw links into standardized Link objects
1254
- *
1255
- * @param linkRaw - Raw OCHRE link
1256
- * @returns Parsed Link object
1257
- */
1258
- function parseLink(linkRaw) {
1259
- const categoryKey = [
1260
- "resource",
1261
- "spatialUnit",
1262
- "concept",
1263
- "set",
1264
- "tree",
1265
- "person",
1266
- "bibliography",
1267
- "propertyVariable",
1268
- "propertyValue"
1269
- ].find((key) => key in linkRaw);
1270
- if (!categoryKey) throw new Error(`Invalid link provided: ${JSON.stringify(linkRaw, null, 2)}`);
1271
- const links = linkRaw[categoryKey];
1272
- if (links == null) return [];
1273
- return ensureArray(links).map((link) => {
1274
- const returnLink = {
1275
- category: categoryKey,
1276
- content: "content" in link && link.content != null ? parseFakeString(link.content) : null,
1277
- href: "href" in link && link.href != null ? link.href : null,
1278
- fileFormat: "fileFormat" in link && link.fileFormat != null ? link.fileFormat : null,
1279
- fileSize: "fileSize" in link && link.fileSize != null ? link.fileSize : null,
1280
- uuid: link.uuid ?? null,
1281
- type: link.type ?? null,
1282
- identification: link.identification ? parseIdentification(link.identification) : null,
1283
- description: "description" in link && link.description != null ? parseStringContent(link.description) : null,
1284
- image: null,
1285
- bibliographies: "bibliography" in linkRaw && linkRaw.bibliography != null ? parseBibliographies(ensureArray(linkRaw.bibliography)) : null,
1286
- publicationDateTime: parseOptionalDate(link.publicationDateTime)
1287
- };
1288
- if ("height" in link && link.height != null && link.width != null && link.heightPreview != null && link.widthPreview != null) returnLink.image = {
1289
- isInline: link.rend === "inline",
1290
- isPrimary: link.isPrimary ?? false,
1291
- heightPreview: link.heightPreview,
1292
- widthPreview: link.widthPreview,
1293
- height: link.height,
1294
- width: link.width
1295
- };
1296
- return returnLink;
1297
- });
1298
- }
1299
- /**
1300
- * Parses an array of raw links into standardized Link objects
1301
- *
1302
- * @param links - Array of raw OCHRE links
1303
- * @returns Array of parsed Link objects
1304
- */
1305
- function parseLinks(links) {
1306
- const result = [];
1307
- for (const link of links) result.push(...parseLink(link));
1308
- return result;
1309
- }
1310
- /**
1311
- * Parses raw document content into a standardized Document structure
1312
- *
1313
- * @param document - Raw document content in OCHRE format
1314
- * @param language - Language code to use for content selection (defaults to "eng")
1315
- * @returns Parsed Document object with content and footnotes
1316
- */
1317
- function parseDocument(document, language = "eng") {
1318
- const doc = Array.isArray(document) ? document.find((d) => d.lang === language) : document;
1319
- if (isFakeString(doc.string)) return parseEmail(parseFakeString(doc.string));
1320
- let result = "";
1321
- for (const item of ensureArray(doc.string)) result += parseStringDocumentItem(item);
1322
- return result;
1323
- }
1324
- /**
1325
- * Parses raw image data into a standardized Image structure
1326
- *
1327
- * @param image - Raw image data in OCHRE format
1328
- * @returns Parsed Image object or null if invalid
1329
- */
1330
- function parseImage(image) {
1331
- return {
1332
- publicationDateTime: parseOptionalDate(image.publicationDateTime),
1333
- identification: image.identification ? parseIdentification(image.identification) : null,
1334
- url: image.href ?? (image.htmlImgSrcPrefix == null && image.content != null ? parseFakeString(image.content) : null),
1335
- htmlPrefix: image.htmlImgSrcPrefix ?? null,
1336
- content: image.htmlImgSrcPrefix != null && image.content != null ? parseFakeString(image.content) : null,
1337
- widthPreview: image.widthPreview ?? null,
1338
- heightPreview: image.heightPreview ?? null,
1339
- width: image.width ?? null,
1340
- height: image.height ?? null
1341
- };
1342
- }
1343
- /**
1344
- * Parses raw notes into standardized Note objects
1345
- *
1346
- * @param notes - Array of raw notes in OCHRE format
1347
- * @param language - Language code for content selection (defaults to "eng")
1348
- * @returns Array of parsed Note objects
1349
- */
1350
- function parseNotes(notes, language = "eng") {
1351
- const result = [];
1352
- for (const note of notes) {
1353
- if (typeof note === "string") {
1354
- if (note !== "") result.push({
1355
- number: -1,
1356
- title: null,
1357
- date: null,
1358
- authors: [],
1359
- content: note
1360
- });
1361
- continue;
1362
- }
1363
- const notesToParse = note.content != null ? ensureArray(note.content) : [];
1364
- if (notesToParse.length === 0) continue;
1365
- const noteWithLanguage = notesToParse.find((item) => item.lang === language) ?? notesToParse[0];
1366
- if (!noteWithLanguage) throw new Error(`Note does not have a valid content item: ${JSON.stringify(note, null, 2)}`);
1367
- const content = isFakeString(noteWithLanguage.string) ? parseEmail(parseFakeString(noteWithLanguage.string)) : parseEmail(parseDocument(noteWithLanguage));
1368
- result.push({
1369
- number: note.noteNo,
1370
- title: noteWithLanguage.title != null ? parseFakeString(noteWithLanguage.title) : null,
1371
- date: note.date ?? null,
1372
- authors: note.authors ? parsePersons(ensureArray(note.authors.author)) : [],
1373
- content
1374
- });
1375
- }
1376
- return result;
1377
- }
1378
- /**
1379
- * Parses raw coordinates data into a standardized array of Coordinate structures
1380
- *
1381
- * @param coordinates - Raw coordinates data in OCHRE format
1382
- * @returns Parsed array of Coordinate objects
1383
- */
1384
- function parseCoordinates(coordinates) {
1385
- if (coordinates == null) return [];
1386
- const result = [];
1387
- for (const coord of ensureArray(coordinates.coord)) {
1388
- const source = "source" in coord && coord.source ? coord.source.context === "self" ? {
1389
- context: "self",
1390
- uuid: coord.source.label.uuid,
1391
- label: parseStringContent(coord.source.label)
1392
- } : coord.source.context === "related" ? {
1393
- context: "related",
1394
- uuid: coord.source.label.uuid,
1395
- label: parseStringContent(coord.source.label),
1396
- value: parseStringContent(coord.source.value)
1397
- } : {
1398
- context: "inherited",
1399
- uuid: coord.source.label.uuid,
1400
- item: {
1401
- uuid: coord.source.item.label.uuid,
1402
- label: parseStringContent(coord.source.item.label)
1403
- },
1404
- label: parseStringContent(coord.source.label)
1405
- } : null;
1406
- switch (coord.type) {
1407
- case "point":
1408
- result.push({
1409
- type: coord.type,
1410
- latitude: coord.latitude,
1411
- longitude: coord.longitude,
1412
- altitude: coord.altitude ?? null,
1413
- source
1414
- });
1415
- break;
1416
- case "plane":
1417
- result.push({
1418
- type: coord.type,
1419
- minimum: {
1420
- latitude: coord.minimum.latitude,
1421
- longitude: coord.minimum.longitude
1422
- },
1423
- maximum: {
1424
- latitude: coord.maximum.latitude,
1425
- longitude: coord.maximum.longitude
1426
- },
1427
- source
1428
- });
1429
- break;
1430
- }
1431
- }
1432
- return result;
1433
- }
1434
- /**
1435
- * Parses a raw observation into a standardized Observation structure
1436
- *
1437
- * @param observation - Raw observation data in OCHRE format
1438
- * @returns Parsed Observation object
1439
- */
1440
- function parseObservation(observation) {
1441
- return {
1442
- number: observation.observationNo,
1443
- date: observation.date ?? null,
1444
- observers: observation.observers != null ? isFakeString(observation.observers) ? parseFakeString(observation.observers).split(";").map((observer) => observer.trim()) : parsePersons(ensureArray(observation.observers)) : [],
1445
- notes: observation.notes ? parseNotes(ensureArray(observation.notes.note)) : [],
1446
- links: observation.links ? parseLinks(ensureArray(observation.links)) : [],
1447
- properties: observation.properties ? parseProperties(ensureArray(observation.properties.property)) : [],
1448
- bibliographies: observation.bibliographies ? parseBibliographies(ensureArray(observation.bibliographies.bibliography)) : []
1449
- };
1450
- }
1451
- /**
1452
- * Parses an array of raw observations into standardized Observation objects
1453
- *
1454
- * @param observations - Array of raw observations in OCHRE format
1455
- * @returns Array of parsed Observation objects
1456
- */
1457
- function parseObservations(observations) {
1458
- return observations.map((obs) => parseObservation(obs));
1459
- }
1460
- /**
1461
- * Parses an array of raw events into standardized Event objects
1462
- *
1463
- * @param events - Array of raw events in OCHRE format
1464
- * @returns Array of parsed Event objects
1465
- */
1466
- function parseEvents(events) {
1467
- return events.map((event) => ({
1468
- dateTime: event.endDateTime != null ? `${event.dateTime}/${event.endDateTime}` : event.dateTime ?? null,
1469
- label: parseStringContent(event.label),
1470
- location: event.location ? {
1471
- uuid: event.location.uuid,
1472
- publicationDateTime: parseOptionalDate(event.location.publicationDateTime),
1473
- content: parseStringContent(event.location)
1117
+ function parseEvents(events) {
1118
+ return events.map((event) => ({
1119
+ dateTime: event.endDateTime != null ? `${event.dateTime}/${event.endDateTime}` : event.dateTime ?? null,
1120
+ label: parseStringContent(event.label),
1121
+ location: event.location ? {
1122
+ uuid: event.location.uuid,
1123
+ publicationDateTime: parseOptionalDate(event.location.publicationDateTime),
1124
+ content: parseStringContent(event.location)
1474
1125
  } : null,
1475
1126
  agent: event.agent ? {
1476
1127
  uuid: event.agent.uuid,
@@ -2132,37 +1783,6 @@ function parseConcept(concept, metadata, persistentUrl, belongsTo) {
2132
1783
  };
2133
1784
  }
2134
1785
  /**
2135
- * Parses raw webpage resources into standardized WebElement or Webpage objects
2136
- *
2137
- * @param webpageResources - Array of raw webpage resources in OCHRE format
2138
- * @param type - Type of resource to parse ("element" or "page")
2139
- * @returns Array of parsed WebElement or Webpage objects
2140
- */
2141
- const parseWebpageResources = (webpageResources, type) => {
2142
- const returnElements = [];
2143
- for (const resource of webpageResources) {
2144
- if (getPropertyByLabelAndValue(resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [], "presentation", type) === null) continue;
2145
- switch (type) {
2146
- case "element": {
2147
- const element = parseWebElement(resource);
2148
- returnElements.push(element);
2149
- break;
2150
- }
2151
- case "page": {
2152
- const webpage = parseWebpage(resource);
2153
- if (webpage) returnElements.push(webpage);
2154
- break;
2155
- }
2156
- case "block": {
2157
- const block = parseWebBlock(resource);
2158
- if (block) returnElements.push(block);
2159
- break;
2160
- }
2161
- }
2162
- }
2163
- return returnElements;
2164
- };
2165
- /**
2166
1786
  * Parses raw concept data into standardized Concept objects
2167
1787
  *
2168
1788
  * @param concepts - Array of raw concept data in OCHRE format
@@ -2171,88 +1791,908 @@ const parseWebpageResources = (webpageResources, type) => {
2171
1791
  function parseConcepts(concepts) {
2172
1792
  return concepts.map((concept) => parseConcept(concept));
2173
1793
  }
2174
- function parseBounds(bounds) {
2175
- const result = boundsSchema.safeParse(bounds);
2176
- if (!result.success) throw new Error(`Invalid bounds: ${result.error.message}`);
2177
- return result.data;
1794
+
1795
+ //#endregion
1796
+ //#region src/utils/fetchers/gallery.ts
1797
+ /**
1798
+ * Fetches and parses a gallery from the OCHRE API
1799
+ *
1800
+ * @param uuid - The UUID of the gallery
1801
+ * @param filter - The filter to apply to the gallery
1802
+ * @param page - The page number to fetch
1803
+ * @param pageSize - The number of items per page
1804
+ * @param options - The options for the fetch
1805
+ * @param options.fetch - The fetch function to use
1806
+ * @param options.version - The version of the OCHRE API to use
1807
+ * @returns The parsed gallery or an error message if the fetch/parse fails
1808
+ */
1809
+ async function fetchGallery(uuid, filter, page, pageSize, options) {
1810
+ try {
1811
+ const { uuid: parsedUuid, filter: parsedFilter, page: parsedPage, pageSize: parsedPageSize } = gallerySchema.parse({
1812
+ uuid,
1813
+ filter,
1814
+ page,
1815
+ pageSize
1816
+ });
1817
+ const response = await (options?.fetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(`
1818
+ for $q in input()/ochre[@uuid='${parsedUuid}']
1819
+ let $filtered := $q//items/resource[contains(lower-case(identification/label), lower-case('${parsedFilter}'))]
1820
+ let $maxLength := count($filtered)
1821
+ return <gallery maxLength='{$maxLength}'>
1822
+ {$q/metadata/project}
1823
+ {$q/metadata/item}
1824
+ {$filtered[position() >= ${((parsedPage - 1) * parsedPageSize + 1).toString()} and position() < ${(parsedPage * parsedPageSize + 1).toString()}]}
1825
+ </gallery>
1826
+ `)}&format=json`);
1827
+ if (!response.ok) throw new Error("Error fetching gallery items, please try again later.");
1828
+ const data = await response.json();
1829
+ if (!("gallery" in data.result)) throw new Error("Failed to fetch gallery");
1830
+ return {
1831
+ item: {
1832
+ identification: parseIdentification(data.result.gallery.item.identification),
1833
+ projectIdentification: parseIdentification(data.result.gallery.project.identification),
1834
+ resources: parseResources(data.result.gallery.resource ? Array.isArray(data.result.gallery.resource) ? data.result.gallery.resource : [data.result.gallery.resource] : []),
1835
+ maxLength: data.result.gallery.maxLength
1836
+ },
1837
+ error: null
1838
+ };
1839
+ } catch (error) {
1840
+ console.error(error);
1841
+ return {
1842
+ item: null,
1843
+ error: error instanceof Error ? error.message : "Failed to fetch gallery"
1844
+ };
1845
+ }
2178
1846
  }
1847
+
1848
+ //#endregion
1849
+ //#region src/utils/helpers.ts
2179
1850
  /**
2180
- * Parses raw web element properties into a standardized WebElementComponent structure
1851
+ * The default API version to use
2181
1852
  *
2182
- * @param componentProperty - Raw component property data in OCHRE format
2183
- * @param elementResource - Raw element resource data in OCHRE format
2184
- * @returns Parsed WebElementComponent object
1853
+ * @remarks
1854
+ * Version 1 of the OCHRE API is deprecated and will be removed in the future.
1855
+ * It points to the old Tamino server.
1856
+ *
1857
+ * Version 2 of the OCHRE API is the current version and is the default.
1858
+ * It points to the new MarkLogic server.
2185
1859
  */
2186
- function parseWebElementProperties(componentProperty, elementResource) {
2187
- const unparsedComponentName = componentProperty.values[0].content;
2188
- const { data: componentName } = componentSchema.safeParse(unparsedComponentName);
2189
- let properties = null;
2190
- const links = elementResource.links ? parseLinks(ensureArray(elementResource.links)) : [];
2191
- switch (componentName) {
2192
- case "3d-viewer": {
2193
- const resourceLink = links.find((link) => link.category === "resource" && link.fileFormat === "model/obj");
2194
- if (resourceLink?.uuid == null) throw new Error(`Resource link not found for the following component: “${componentName}”`);
2195
- let isInteractive = getPropertyValueByLabel(componentProperty.properties, "is-interactive");
2196
- isInteractive ??= true;
2197
- let isControlsDisplayed = getPropertyValueByLabel(componentProperty.properties, "controls-displayed");
2198
- isControlsDisplayed ??= true;
2199
- properties = {
2200
- component: "3d-viewer",
2201
- linkUuid: resourceLink.uuid,
2202
- fileSize: resourceLink.fileSize,
2203
- isInteractive,
2204
- isControlsDisplayed
2205
- };
2206
- break;
2207
- }
2208
- case "advanced-search": {
2209
- const boundElementPropertyUuid = getPropertyByLabel(componentProperty.properties, "bound-element")?.values[0]?.uuid ?? null;
2210
- const linkToProperty = getPropertyByLabel(componentProperty.properties, "link-to");
2211
- const href = linkToProperty?.values[0]?.href ?? linkToProperty?.values[0]?.slug ?? null;
2212
- if (boundElementPropertyUuid == null && href == null) throw new Error(`Bound element or href not found for the following component: “${componentName}”`);
2213
- properties = {
2214
- component: "advanced-search",
2215
- boundElementUuid: boundElementPropertyUuid,
2216
- href
2217
- };
2218
- break;
2219
- }
2220
- case "annotated-document": {
2221
- const documentLink = links.find((link) => link.type === "internalDocument");
2222
- if (documentLink?.uuid == null) throw new Error(`Document link not found for the following component: “${componentName}”`);
2223
- properties = {
2224
- component: "annotated-document",
2225
- linkUuid: documentLink.uuid
2226
- };
2227
- break;
2228
- }
2229
- case "annotated-image": {
2230
- const imageLinks = links.filter((link) => link.type === "image" || link.type === "IIIF");
2231
- if (imageLinks.length === 0 || imageLinks[0].uuid == null) throw new Error(`Image link not found for the following component: “${componentName}”`);
2232
- let isFilterInputDisplayed = getPropertyValueByLabel(componentProperty.properties, "filter-input-displayed");
2233
- isFilterInputDisplayed ??= true;
2234
- let isOptionsDisplayed = getPropertyValueByLabel(componentProperty.properties, "options-displayed");
2235
- isOptionsDisplayed ??= true;
2236
- let isAnnotationHighlightsDisplayed = getPropertyValueByLabel(componentProperty.properties, "annotation-highlights-displayed");
2237
- isAnnotationHighlightsDisplayed ??= true;
2238
- let isAnnotationTooltipsDisplayed = getPropertyValueByLabel(componentProperty.properties, "annotation-tooltips-displayed");
2239
- isAnnotationTooltipsDisplayed ??= true;
2240
- properties = {
2241
- component: "annotated-image",
2242
- linkUuid: imageLinks[0].uuid,
2243
- isFilterInputDisplayed,
2244
- isOptionsDisplayed,
2245
- isAnnotationHighlightsDisplayed,
2246
- isAnnotationTooltipsDisplayed
2247
- };
2248
- break;
1860
+ const DEFAULT_API_VERSION = 2;
1861
+ /**
1862
+ * The default page size to use for fetching paginated items
1863
+ */
1864
+ const DEFAULT_PAGE_SIZE = 48;
1865
+ /**
1866
+ * Flatten the properties of an item
1867
+ * @param item - The item whose properties to flatten
1868
+ * @returns The item with the properties flattened
1869
+ */
1870
+ function flattenItemProperties(item) {
1871
+ const allProperties = [];
1872
+ if ("properties" in item) allProperties.push(...item.properties);
1873
+ if ("observations" in item) for (const observation of item.observations) allProperties.push(...observation.properties);
1874
+ if ("interpretations" in item) for (const interpretation of item.interpretations) allProperties.push(...interpretation.properties);
1875
+ if ("bibliographies" in item) for (const bibliography of item.bibliographies) allProperties.push(...bibliography.properties);
1876
+ return {
1877
+ ...item,
1878
+ properties: flattenProperties(allProperties)
1879
+ };
1880
+ }
1881
+
1882
+ //#endregion
1883
+ //#region src/utils/fetchers/uuid.ts
1884
+ /**
1885
+ * Fetches raw OCHRE data by UUID from the OCHRE API
1886
+ *
1887
+ * @param uuid - The UUID of the OCHRE item to fetch
1888
+ * @returns A tuple containing either [null, OchreData] on success or [error message, null] on failure
1889
+ * @internal
1890
+ */
1891
+ async function fetchByUuid(uuid, options) {
1892
+ try {
1893
+ const version = options?.version ?? DEFAULT_API_VERSION;
1894
+ const parsedUuid = uuidSchema.parse(uuid);
1895
+ const response = await (options?.fetch ?? fetch)(version === 2 ? `https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?uuid=${parsedUuid}&format=json&lang="*"` : `https://ochre.lib.uchicago.edu/ochre?uuid=${parsedUuid}&format=json&lang="*"`);
1896
+ if (!response.ok) throw new Error("Failed to fetch OCHRE data");
1897
+ const dataRaw = await response.json();
1898
+ if (version === 2 && (!("result" in dataRaw) || !("ochre" in dataRaw.result)) || version !== 2 && !("ochre" in dataRaw)) throw new Error("Invalid OCHRE data: API response missing 'ochre' key");
1899
+ return [null, "result" in dataRaw && !Array.isArray(dataRaw.result) && "ochre" in dataRaw.result ? dataRaw.result : "ochre" in dataRaw ? dataRaw : null];
1900
+ } catch (error) {
1901
+ return [error instanceof Error ? error.message : "Unknown error", null];
1902
+ }
1903
+ }
1904
+
1905
+ //#endregion
1906
+ //#region src/utils/fetchers/item.ts
1907
+ /**
1908
+ * Fetches and parses an OCHRE item from the OCHRE API
1909
+ *
1910
+ * @param uuid - The UUID of the OCHRE item to fetch
1911
+ * @returns Object containing the parsed OCHRE item, or an error message if the fetch/parse fails
1912
+ */
1913
+ async function fetchItem(uuid, category, itemCategories, options) {
1914
+ try {
1915
+ const version = options?.version ?? DEFAULT_API_VERSION;
1916
+ const [error, data] = await fetchByUuid(uuid, {
1917
+ fetch,
1918
+ version
1919
+ });
1920
+ if (error !== null) throw new Error(error);
1921
+ const categoryKey = category ?? getItemCategory(Object.keys(data.ochre));
1922
+ const belongsTo = {
1923
+ uuid: data.ochre.uuidBelongsTo,
1924
+ abbreviation: parseFakeString(data.ochre.belongsTo)
1925
+ };
1926
+ const metadata = parseMetadata(data.ochre.metadata);
1927
+ let item;
1928
+ switch (categoryKey) {
1929
+ case "resource":
1930
+ if (!("resource" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'resource' key");
1931
+ item = parseResource(data.ochre.resource, metadata, data.ochre.persistentUrl, belongsTo);
1932
+ break;
1933
+ case "spatialUnit":
1934
+ if (!("spatialUnit" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'spatialUnit' key");
1935
+ item = parseSpatialUnit(data.ochre.spatialUnit, metadata, data.ochre.persistentUrl, belongsTo);
1936
+ break;
1937
+ case "concept":
1938
+ if (!("concept" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'concept' key");
1939
+ item = parseConcept(data.ochre.concept, metadata, data.ochre.persistentUrl, belongsTo);
1940
+ break;
1941
+ case "period":
1942
+ if (!("period" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'period' key");
1943
+ item = parsePeriod(data.ochre.period, metadata, data.ochre.persistentUrl, {
1944
+ uuid: data.ochre.uuidBelongsTo,
1945
+ abbreviation: parseFakeString(data.ochre.belongsTo)
1946
+ });
1947
+ break;
1948
+ case "bibliography":
1949
+ if (!("bibliography" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'bibliography' key");
1950
+ item = parseBibliography(data.ochre.bibliography, metadata, data.ochre.persistentUrl, belongsTo);
1951
+ break;
1952
+ case "person":
1953
+ if (!("person" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'person' key");
1954
+ item = parsePerson(data.ochre.person, metadata, data.ochre.persistentUrl, belongsTo);
1955
+ break;
1956
+ case "propertyVariable":
1957
+ if (!("propertyVariable" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'propertyVariable' key");
1958
+ item = parsePropertyVariable(data.ochre.propertyVariable, metadata, data.ochre.persistentUrl, belongsTo);
1959
+ break;
1960
+ case "propertyValue":
1961
+ if (!("propertyValue" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'propertyValue' key");
1962
+ item = parsePropertyValue(data.ochre.propertyValue, metadata, data.ochre.persistentUrl, belongsTo);
1963
+ break;
1964
+ case "text":
1965
+ if (!("text" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'text' key");
1966
+ item = parseText(data.ochre.text, metadata, data.ochre.persistentUrl, belongsTo);
1967
+ break;
1968
+ case "set":
1969
+ if (!("set" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'set' key");
1970
+ item = parseSet(data.ochre.set, itemCategories, metadata, data.ochre.persistentUrl, belongsTo);
1971
+ break;
1972
+ case "tree":
1973
+ if (!("tree" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'tree' key");
1974
+ item = parseTree(data.ochre.tree, itemCategories, metadata, data.ochre.persistentUrl, belongsTo);
1975
+ break;
1976
+ default: throw new Error("Invalid category");
2249
1977
  }
2250
- case "audio-player": {
2251
- const audioLink = links.find((link) => link.type === "audio");
2252
- if (audioLink?.uuid == null) throw new Error(`Audio link not found for the following component: “${componentName}”`);
2253
- let isSpeedControlsDisplayed = getPropertyValueByLabel(componentProperty.properties, "speed-controls-displayed");
2254
- isSpeedControlsDisplayed ??= true;
2255
- let isVolumeControlsDisplayed = getPropertyValueByLabel(componentProperty.properties, "volume-controls-displayed");
1978
+ return {
1979
+ error: null,
1980
+ item
1981
+ };
1982
+ } catch (error) {
1983
+ return {
1984
+ error: error instanceof Error ? error.message : "Unknown error",
1985
+ item: void 0
1986
+ };
1987
+ }
1988
+ }
1989
+
1990
+ //#endregion
1991
+ //#region src/utils/fetchers/items-by-property-values.ts
1992
+ /**
1993
+ * Build an XQuery string to fetch items by property values from the OCHRE API
1994
+ * @param params - The parameters for the fetch
1995
+ * @param params.projectScopeUuid - The UUID of the project scope
1996
+ * @param params.belongsToCollectionScopeUuids - An array of collection scope UUIDs to filter by
1997
+ * @param params.propertyVariableUuids - An array of property variable UUIDs to fetch
1998
+ * @param params.propertyValues - An array of property values to fetch
1999
+ * @param params.itemCategory - The category of the items to fetch
2000
+ * @param params.page - The page number (1-indexed)
2001
+ * @param params.pageSize - The number of items per page
2002
+ * @param params.includeChildItems - Whether to include child items of the same category
2003
+ * @param options - Options for the fetch
2004
+ * @param options.version - The version of the OCHRE API to use
2005
+ * @returns An XQuery string
2006
+ */
2007
+ function buildXQuery$1(params, options) {
2008
+ const version = options?.version ?? DEFAULT_API_VERSION;
2009
+ const { propertyVariableUuids, propertyValues, projectScopeUuid, belongsToCollectionScopeUuids, itemCategory, page, pageSize, includeChildItems = false } = params;
2010
+ const startPos = (page - 1) * pageSize + 1;
2011
+ const endPos = page * pageSize;
2012
+ let belongsToCollectionScopeFilter = "";
2013
+ if (belongsToCollectionScopeUuids.length > 0) belongsToCollectionScopeFilter = `[.//properties[property[label/@uuid="${BELONGS_TO_COLLECTION_UUID}" and value/(${belongsToCollectionScopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ")})]]`;
2014
+ const propertyVariables = propertyVariableUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
2015
+ const propertyValuesFilters = propertyValues.map(({ dataType, value }) => {
2016
+ if (dataType === "IDREF") return `value[@uuid="${value}"]`;
2017
+ if (dataType === "date" || dataType === "dateTime" || dataType === "time" || dataType === "integer" || dataType === "decimal" || dataType === "boolean") return `value[@rawValue="${value}"]`;
2018
+ return `value[.="${value}"]`;
2019
+ }).join(" or ");
2020
+ return `<ochre>{${`let $match-uuids := distinct-values(
2021
+ ${version === 2 ? "doc()" : "input()"}/ochre[@uuidBelongsTo="${projectScopeUuid}"]
2022
+ ${belongsToCollectionScopeFilter}
2023
+ //property[label[${propertyVariables}]][${propertyValuesFilters}]]
2024
+ /@uuid
2025
+ )
2026
+
2027
+ let $items := ${version === 2 ? "doc()" : "input()"}/ochre[@uuidBelongsTo="${projectScopeUuid}"]
2028
+ /${itemCategory ?? "*"}[@uuid = $match-uuids
2029
+ or bibliographies/bibliography/@uuid = $match-uuids]
2030
+
2031
+ let $unique-uuids := distinct-values($items/@uuid)
2032
+ let $totalCount := count($unique-uuids)
2033
+
2034
+ return <items totalCount="{$totalCount}" page="${page}" pageSize="${pageSize}">{
2035
+ for $uuid in $unique-uuids[position() ge ${startPos} and position() le ${endPos}]
2036
+ let $item := ($items[@uuid = $uuid])[1]
2037
+ let $category := local-name($item)
2038
+ return element { node-name($item) } {
2039
+ $item/@*, ${includeChildItems ? "$item/node()" : "$item/node()[not(local-name(.) = $category)]"}
2040
+ }
2041
+ }</items>`}}</ochre>`;
2042
+ }
2043
+ /**
2044
+ * Fetches and parses items by property values from the OCHRE API
2045
+ *
2046
+ * @param params - The parameters for the fetch
2047
+ * @param params.projectScopeUuid - The UUID of the project scope
2048
+ * @param params.belongsToCollectionScopeUuids - The collection scope UUIDs to filter by
2049
+ * @param params.propertyVariableUuids - The property variable UUIDs to query by
2050
+ * @param params.propertyValues - The property values to query by
2051
+ * @param params.page - The page number (1-indexed)
2052
+ * @param params.pageSize - The number of items per page
2053
+ * @param params.itemCategory - The category of the items to fetch
2054
+ * @param params.includeChildItems - Whether to include child items of the same category
2055
+ * @param categoryParams - The category parameters for the fetch
2056
+ * @param categoryParams.category - The category of the items to fetch
2057
+ * @param categoryParams.itemCategories - The categories of the items to fetch
2058
+ * @param options - Options for the fetch
2059
+ * @param options.fetch - The fetch function to use
2060
+ * @param options.version - The version of the OCHRE API to use
2061
+ * @returns The parsed items by property values or null if the fetch/parse fails
2062
+ */
2063
+ async function fetchItemsByPropertyValues(params, categoryParams, options) {
2064
+ try {
2065
+ const version = options?.version ?? DEFAULT_API_VERSION;
2066
+ const { propertyVariableUuids, propertyValues, projectScopeUuid, belongsToCollectionScopeUuids, page, pageSize = DEFAULT_PAGE_SIZE, itemCategory, includeChildItems } = params;
2067
+ const { category, itemCategories } = categoryParams ?? {};
2068
+ const xquery = buildXQuery$1({
2069
+ projectScopeUuid,
2070
+ belongsToCollectionScopeUuids,
2071
+ propertyVariableUuids,
2072
+ propertyValues,
2073
+ itemCategory,
2074
+ includeChildItems,
2075
+ page,
2076
+ pageSize
2077
+ }, { version });
2078
+ const response = await (options?.fetch ?? fetch)(version === 2 ? `https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?xquery=${encodeURIComponent(xquery)}&format=json&lang="*"` : `https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(xquery)}&format=json&lang="*"`);
2079
+ if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`);
2080
+ const data = await response.json();
2081
+ if (Array.isArray(data.result) || Object.keys(data.result.ochre).length === 0) throw new Error("No items found");
2082
+ if (category != null && data.result.ochre.items[category] == null) throw new Error(`No items found for category: ${category}`);
2083
+ if ((category === "set" || category === "tree") && itemCategories != null && "items" in data.result.ochre.items[category] && Array.isArray(data.result.ochre.items[category].items) && data.result.ochre.items[category].items.every((item) => item.category !== itemCategories)) throw new Error(`No items found for category: ${category} and item categories: ${itemCategories}`);
2084
+ const items = [];
2085
+ if ("resource" in data.result.ochre.items && data.result.ochre.items.resource != null) {
2086
+ const resources = parseResources(Array.isArray(data.result.ochre.items.resource) ? data.result.ochre.items.resource : [data.result.ochre.items.resource]);
2087
+ items.push(...resources);
2088
+ }
2089
+ if ("spatialUnit" in data.result.ochre.items && data.result.ochre.items.spatialUnit != null) {
2090
+ const spatialUnits = parseSpatialUnits(Array.isArray(data.result.ochre.items.spatialUnit) ? data.result.ochre.items.spatialUnit : [data.result.ochre.items.spatialUnit]);
2091
+ items.push(...spatialUnits);
2092
+ }
2093
+ if ("concept" in data.result.ochre.items && data.result.ochre.items.concept != null) {
2094
+ const concepts = parseConcepts(Array.isArray(data.result.ochre.items.concept) ? data.result.ochre.items.concept : [data.result.ochre.items.concept]);
2095
+ items.push(...concepts);
2096
+ }
2097
+ if ("period" in data.result.ochre.items && data.result.ochre.items.period != null) {
2098
+ const periods = parsePeriods(Array.isArray(data.result.ochre.items.period) ? data.result.ochre.items.period : [data.result.ochre.items.period]);
2099
+ items.push(...periods);
2100
+ }
2101
+ if ("bibliography" in data.result.ochre.items && data.result.ochre.items.bibliography != null) {
2102
+ const bibliographies = parseBibliographies(Array.isArray(data.result.ochre.items.bibliography) ? data.result.ochre.items.bibliography : [data.result.ochre.items.bibliography]);
2103
+ items.push(...bibliographies);
2104
+ }
2105
+ if ("person" in data.result.ochre.items && data.result.ochre.items.person != null) {
2106
+ const persons = parsePersons(Array.isArray(data.result.ochre.items.person) ? data.result.ochre.items.person : [data.result.ochre.items.person]);
2107
+ items.push(...persons);
2108
+ }
2109
+ if ("propertyVariable" in data.result.ochre.items && data.result.ochre.items.propertyVariable != null) {
2110
+ const propertyVariables = parsePropertyVariables(Array.isArray(data.result.ochre.items.propertyVariable) ? data.result.ochre.items.propertyVariable : [data.result.ochre.items.propertyVariable]);
2111
+ items.push(...propertyVariables);
2112
+ }
2113
+ if ("propertyValue" in data.result.ochre.items && data.result.ochre.items.propertyValue != null) {
2114
+ const propertyValues = parsePropertyValues(Array.isArray(data.result.ochre.items.propertyValue) ? data.result.ochre.items.propertyValue : [data.result.ochre.items.propertyValue]);
2115
+ items.push(...propertyValues);
2116
+ }
2117
+ if ("text" in data.result.ochre.items && data.result.ochre.items.text != null) {
2118
+ const texts = parseTexts(Array.isArray(data.result.ochre.items.text) ? data.result.ochre.items.text : [data.result.ochre.items.text]);
2119
+ items.push(...texts);
2120
+ }
2121
+ if ("set" in data.result.ochre.items && data.result.ochre.items.set != null) {
2122
+ const sets = parseSets(Array.isArray(data.result.ochre.items.set) ? data.result.ochre.items.set : [data.result.ochre.items.set]);
2123
+ items.push(...sets);
2124
+ }
2125
+ if ("tree" in data.result.ochre.items && data.result.ochre.items.tree != null) {
2126
+ const trees = parseTrees(Array.isArray(data.result.ochre.items.tree) ? data.result.ochre.items.tree : [data.result.ochre.items.tree]);
2127
+ items.push(...trees);
2128
+ }
2129
+ return {
2130
+ totalCount: data.result.ochre.items.totalCount,
2131
+ page: data.result.ochre.items.page,
2132
+ pageSize: data.result.ochre.items.pageSize,
2133
+ items,
2134
+ error: null
2135
+ };
2136
+ } catch (error) {
2137
+ console.error(error);
2138
+ return {
2139
+ totalCount: null,
2140
+ page: null,
2141
+ pageSize: null,
2142
+ items: null,
2143
+ error: error instanceof Error ? error.message : "Failed to fetch items by property values"
2144
+ };
2145
+ }
2146
+ }
2147
+
2148
+ //#endregion
2149
+ //#region src/utils/fetchers/property-values-by-property-variables.ts
2150
+ /**
2151
+ * Schema for a single property value query item in the OCHRE API response
2152
+ */
2153
+ const propertyValueQueryItemSchema = z.object({
2154
+ uuid: z.string(),
2155
+ itemUuid: z.string().optional(),
2156
+ dataType: z.string(),
2157
+ rawValue: z.union([
2158
+ z.string(),
2159
+ z.number(),
2160
+ z.boolean()
2161
+ ]).optional(),
2162
+ content: z.union([
2163
+ z.string(),
2164
+ z.number(),
2165
+ z.boolean(),
2166
+ richTextStringSchema,
2167
+ z.array(richTextStringSchema)
2168
+ ]).optional()
2169
+ }).transform((val) => {
2170
+ const returnValue = {
2171
+ itemUuid: val.itemUuid != null && val.itemUuid !== "" ? val.itemUuid : null,
2172
+ dataType: val.dataType,
2173
+ content: null,
2174
+ label: null
2175
+ };
2176
+ switch (val.dataType) {
2177
+ case "IDREF":
2178
+ returnValue.content = val.uuid !== "" ? val.uuid : null;
2179
+ returnValue.label = val.content != null && val.content !== "" ? typeof val.content === "object" ? parseStringContent({ content: val.content }) : parseFakeString(val.content) : null;
2180
+ break;
2181
+ case "integer":
2182
+ case "decimal":
2183
+ case "time":
2184
+ returnValue.content = val.rawValue != null && val.rawValue !== "" ? Number(val.rawValue) : null;
2185
+ returnValue.label = val.content != null && val.content !== "" ? val.content.toString() : null;
2186
+ break;
2187
+ case "boolean":
2188
+ returnValue.content = val.rawValue != null && val.rawValue !== "" ? Boolean(val.rawValue) : null;
2189
+ returnValue.label = val.content != null && val.content !== "" ? val.content.toString() : null;
2190
+ break;
2191
+ default:
2192
+ returnValue.content = val.rawValue != null && val.rawValue !== "" ? val.rawValue.toString() : null;
2193
+ returnValue.label = val.content != null && val.content !== "" ? val.content.toString() : null;
2194
+ break;
2195
+ }
2196
+ return returnValue;
2197
+ });
2198
+ /**
2199
+ * Schema for the property values by property variables OCHRE API response
2200
+ */
2201
+ const responseSchema = z.object({ result: z.union([z.object({ ochre: z.object({ propertyValue: z.union([propertyValueQueryItemSchema, z.array(propertyValueQueryItemSchema)]) }) }), z.array(z.unknown()).length(0)]) });
2202
+ /**
2203
+ * Build an XQuery string to fetch property values by property variables from the OCHRE API
2204
+ * @param params - The parameters for the fetch
2205
+ * @param params.projectScopeUuid - The UUID of the project scope
2206
+ * @param params.belongsToCollectionScopeUuids - An array of collection scope UUIDs to filter by
2207
+ * @param params.propertyVariableUuids - An array of property variable UUIDs to fetch
2208
+ * @param options - Options for the fetch
2209
+ * @param options.version - The version of the OCHRE API to use
2210
+ * @returns An XQuery string
2211
+ */
2212
+ function buildXQuery(params, options) {
2213
+ const version = options?.version ?? DEFAULT_API_VERSION;
2214
+ const { projectScopeUuid, belongsToCollectionScopeUuids, propertyVariableUuids } = params;
2215
+ let collectionScopeFilter = "";
2216
+ if (belongsToCollectionScopeUuids.length > 0) collectionScopeFilter = `//properties[property[label/@uuid="${BELONGS_TO_COLLECTION_UUID}" and value/(${belongsToCollectionScopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ")})]]`;
2217
+ const propertyVariableFilters = propertyVariableUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
2218
+ return `<ochre>{${`let $matching-props := ${version === 2 ? "doc()" : "input()"}/ochre[@uuidBelongsTo="${projectScopeUuid}"]
2219
+ ${collectionScopeFilter}
2220
+ /*[not(self::set)]
2221
+ //property[label/(${propertyVariableFilters})]
2222
+
2223
+ for $v in $matching-props/value
2224
+ let $item-uuid := $v/ancestor::*[parent::ochre]/@uuid
2225
+ return <propertyValue uuid="{$v/@uuid}" rawValue="{$v/@rawValue}" dataType="{$v/@dataType}" itemUuid="{$item-uuid}">{
2226
+ if ($v/content) then $v/content else $v/text()
2227
+ }</propertyValue>`}}</ochre>`;
2228
+ }
2229
+ /**
2230
+ * Fetches and parses property values by property variables from the OCHRE API
2231
+ *
2232
+ * @param params - The parameters for the fetch
2233
+ * @param params.projectScopeUuid - The UUID of the project scope
2234
+ * @param params.belongsToCollectionScopeUuids - The collection scope UUIDs to filter by
2235
+ * @param params.propertyVariableUuids - The property variable UUIDs to query by
2236
+ * @param options - Options for the fetch
2237
+ * @param options.fetch - The fetch function to use
2238
+ * @param options.version - The version of the OCHRE API to use
2239
+ * @returns The parsed property values by property variables or null if the fetch/parse fails
2240
+ */
2241
+ async function fetchPropertyValuesByPropertyVariables(params, options) {
2242
+ try {
2243
+ const version = options?.version ?? DEFAULT_API_VERSION;
2244
+ const { belongsToCollectionScopeUuids, propertyVariableUuids, projectScopeUuid } = params;
2245
+ const xquery = buildXQuery({
2246
+ projectScopeUuid,
2247
+ belongsToCollectionScopeUuids,
2248
+ propertyVariableUuids
2249
+ }, { version });
2250
+ const response = await (options?.fetch ?? fetch)(version === 2 ? `https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?xquery=${encodeURIComponent(xquery)}&format=json&lang="*"` : `https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(xquery)}&format=json&lang="*"`);
2251
+ if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`);
2252
+ const data = await response.json();
2253
+ const parsedResultRaw = responseSchema.parse(data);
2254
+ if (Array.isArray(parsedResultRaw.result)) throw new TypeError("No items found");
2255
+ const parsedItems = Array.isArray(parsedResultRaw.result.ochre.propertyValue) ? parsedResultRaw.result.ochre.propertyValue : [parsedResultRaw.result.ochre.propertyValue];
2256
+ const groupedItemsMap = /* @__PURE__ */ new Map();
2257
+ for (const item of parsedItems) {
2258
+ const existing = groupedItemsMap.get(item.content);
2259
+ if (existing == null) groupedItemsMap.set(item.content, {
2260
+ dataType: item.dataType,
2261
+ content: item.content,
2262
+ label: item.label,
2263
+ itemUuids: new Set([item.itemUuid])
2264
+ });
2265
+ else existing.itemUuids.add(item.itemUuid);
2266
+ }
2267
+ const groupedItems = [];
2268
+ for (const group of groupedItemsMap.values()) groupedItems.push({
2269
+ count: group.itemUuids.size,
2270
+ dataType: group.dataType,
2271
+ content: group.content,
2272
+ label: group.label
2273
+ });
2274
+ return {
2275
+ items: groupedItems.toSorted((a, b) => {
2276
+ if (a.count !== b.count) return b.count - a.count;
2277
+ if (a.label !== b.label) return a.label?.localeCompare(b.label ?? "") ?? 0;
2278
+ return a.content?.toString().localeCompare(b.content?.toString() ?? "") ?? 0;
2279
+ }),
2280
+ error: null
2281
+ };
2282
+ } catch (error) {
2283
+ console.error(error);
2284
+ return {
2285
+ items: null,
2286
+ error: error instanceof Error ? error.message : "Failed to fetch property values by property variables"
2287
+ };
2288
+ }
2289
+ }
2290
+
2291
+ //#endregion
2292
+ //#region src/utils/getters.ts
2293
+ const DEFAULT_OPTIONS = { includeNestedProperties: false };
2294
+ /**
2295
+ * Finds a property by its UUID in an array of properties
2296
+ *
2297
+ * @param properties - Array of properties to search through
2298
+ * @param uuid - The UUID to search for
2299
+ * @param options - Search options, including whether to include nested properties
2300
+ * @returns The matching Property object, or null if not found
2301
+ */
2302
+ function getPropertyByUuid(properties, uuid, options = DEFAULT_OPTIONS) {
2303
+ const { includeNestedProperties } = options;
2304
+ const property = properties.find((property) => property.uuid === uuid);
2305
+ if (property) return property;
2306
+ if (includeNestedProperties) {
2307
+ for (const property of properties) if (property.properties.length > 0) {
2308
+ const nestedResult = getPropertyByUuid(property.properties, uuid, { includeNestedProperties });
2309
+ if (nestedResult) return nestedResult;
2310
+ }
2311
+ }
2312
+ return null;
2313
+ }
2314
+ /**
2315
+ * Retrieves all values for a property with the given UUID
2316
+ *
2317
+ * @param properties - Array of properties to search through
2318
+ * @param uuid - The UUID to search for
2319
+ * @param options - Search options, including whether to include nested properties
2320
+ * @returns Array of property values as strings, or null if property not found
2321
+ */
2322
+ function getPropertyValuesByUuid(properties, uuid, options = DEFAULT_OPTIONS) {
2323
+ const { includeNestedProperties } = options;
2324
+ const property = properties.find((property) => property.uuid === uuid);
2325
+ if (property) return property.values.map((value) => value.content);
2326
+ if (includeNestedProperties) {
2327
+ for (const property of properties) if (property.properties.length > 0) {
2328
+ const nestedResult = getPropertyValuesByUuid(property.properties, uuid, { includeNestedProperties });
2329
+ if (nestedResult) return nestedResult;
2330
+ }
2331
+ }
2332
+ return null;
2333
+ }
2334
+ /**
2335
+ * Gets the first value of a property with the given UUID
2336
+ *
2337
+ * @param properties - Array of properties to search through
2338
+ * @param uuid - The UUID to search for
2339
+ * @param options - Search options, including whether to include nested properties
2340
+ * @returns The first property value as string, or null if property not found
2341
+ */
2342
+ function getPropertyValueByUuid(properties, uuid, options = DEFAULT_OPTIONS) {
2343
+ const { includeNestedProperties } = options;
2344
+ const values = getPropertyValuesByUuid(properties, uuid, { includeNestedProperties });
2345
+ if (values !== null && values.length > 0) return values[0];
2346
+ if (includeNestedProperties) {
2347
+ for (const property of properties) if (property.properties.length > 0) {
2348
+ const nestedResult = getPropertyValueByUuid(property.properties, uuid, { includeNestedProperties });
2349
+ if (nestedResult !== null) return nestedResult;
2350
+ }
2351
+ }
2352
+ return null;
2353
+ }
2354
+ /**
2355
+ * Finds a property by its label in an array of properties
2356
+ *
2357
+ * @param properties - Array of properties to search through
2358
+ * @param label - The label to search for
2359
+ * @param options - Search options, including whether to include nested properties
2360
+ * @returns The matching Property object, or null if not found
2361
+ */
2362
+ function getPropertyByLabel(properties, label, options = DEFAULT_OPTIONS) {
2363
+ const { includeNestedProperties } = options;
2364
+ const property = properties.find((property) => property.label === label);
2365
+ if (property) return property;
2366
+ if (includeNestedProperties) {
2367
+ for (const property of properties) if (property.properties.length > 0) {
2368
+ const nestedResult = getPropertyByLabel(property.properties, label, { includeNestedProperties });
2369
+ if (nestedResult) return nestedResult;
2370
+ }
2371
+ }
2372
+ return null;
2373
+ }
2374
+ /**
2375
+ * Finds a property by its label and all values in an array of properties
2376
+ *
2377
+ * @param properties - Array of properties to search through
2378
+ * @param label - The label to search for
2379
+ * @param values - The values to search for
2380
+ * @param options - Search options, including whether to include nested properties
2381
+ * @returns The matching Property object, or null if not found or all values do not match
2382
+ */
2383
+ function getPropertyByLabelAndValues(properties, label, values, options = DEFAULT_OPTIONS) {
2384
+ const { includeNestedProperties } = options;
2385
+ const property = properties.find((property) => property.label === label && deepEqual(property.values, values));
2386
+ if (property) return property;
2387
+ if (includeNestedProperties) {
2388
+ for (const property of properties) if (property.properties.length > 0) {
2389
+ const nestedResult = getPropertyByLabelAndValues(property.properties, label, values, { includeNestedProperties });
2390
+ if (nestedResult) return nestedResult;
2391
+ }
2392
+ }
2393
+ return null;
2394
+ }
2395
+ /**
2396
+ * Finds a property by its label and value in an array of properties
2397
+ *
2398
+ * @param properties - Array of properties to search through
2399
+ * @param label - The label to search for
2400
+ * @param value - The value to search for
2401
+ * @param options - Search options, including whether to include nested properties
2402
+ * @returns The matching Property object, or null if not found or value does not match
2403
+ */
2404
+ function getPropertyByLabelAndValue(properties, label, value, options = DEFAULT_OPTIONS) {
2405
+ const { includeNestedProperties } = options;
2406
+ const property = properties.find((property) => property.label === label && property.values.some((v) => v.content === value));
2407
+ if (property) return property;
2408
+ if (includeNestedProperties) {
2409
+ for (const property of properties) if (property.properties.length > 0) {
2410
+ const nestedResult = getPropertyByLabelAndValue(property.properties, label, value, { includeNestedProperties });
2411
+ if (nestedResult) return nestedResult;
2412
+ }
2413
+ }
2414
+ return null;
2415
+ }
2416
+ /**
2417
+ * Retrieves all values for a property with the given label
2418
+ *
2419
+ * @param properties - Array of properties to search through
2420
+ * @param label - The label to search for
2421
+ * @param options - Search options, including whether to include nested properties
2422
+ * @returns Array of property values as strings, or null if property not found
2423
+ */
2424
+ function getPropertyValuesByLabel(properties, label, options = DEFAULT_OPTIONS) {
2425
+ const { includeNestedProperties } = options;
2426
+ const property = properties.find((property) => property.label === label);
2427
+ if (property) return property.values.map((value) => value.content);
2428
+ if (includeNestedProperties) {
2429
+ for (const property of properties) if (property.properties.length > 0) {
2430
+ const nestedResult = getPropertyValuesByLabel(property.properties, label, { includeNestedProperties });
2431
+ if (nestedResult) return nestedResult;
2432
+ }
2433
+ }
2434
+ return null;
2435
+ }
2436
+ /**
2437
+ * Gets the first value of a property with the given label
2438
+ *
2439
+ * @param properties - Array of properties to search through
2440
+ * @param label - The label to search for
2441
+ * @param options - Search options, including whether to include nested properties
2442
+ * @returns The first property value as string, or null if property not found
2443
+ */
2444
+ function getPropertyValueByLabel(properties, label, options = DEFAULT_OPTIONS) {
2445
+ const { includeNestedProperties } = options;
2446
+ const values = getPropertyValuesByLabel(properties, label, { includeNestedProperties });
2447
+ if (values !== null && values.length > 0) return values[0];
2448
+ if (includeNestedProperties) {
2449
+ for (const property of properties) if (property.properties.length > 0) {
2450
+ const nestedResult = getPropertyValueByLabel(property.properties, label, { includeNestedProperties });
2451
+ if (nestedResult !== null) return nestedResult;
2452
+ }
2453
+ }
2454
+ return null;
2455
+ }
2456
+ /**
2457
+ * Gets all unique properties from an array of properties
2458
+ *
2459
+ * @param properties - Array of properties to get unique properties from
2460
+ * @param options - Search options, including whether to include nested properties
2461
+ * @returns Array of unique properties
2462
+ */
2463
+ function getUniqueProperties(properties, options = DEFAULT_OPTIONS) {
2464
+ const { includeNestedProperties } = options;
2465
+ const uniqueProperties = new Array();
2466
+ for (const property of properties) {
2467
+ if (uniqueProperties.some((p) => p.uuid === property.uuid)) continue;
2468
+ uniqueProperties.push(property);
2469
+ if (property.properties.length > 0 && includeNestedProperties) {
2470
+ const nestedProperties = getUniqueProperties(property.properties, { includeNestedProperties: true });
2471
+ for (const property of nestedProperties) {
2472
+ if (uniqueProperties.some((p) => p.uuid === property.uuid)) continue;
2473
+ uniqueProperties.push(property);
2474
+ }
2475
+ }
2476
+ }
2477
+ return uniqueProperties;
2478
+ }
2479
+ /**
2480
+ * Gets all unique property labels from an array of properties
2481
+ *
2482
+ * @param properties - Array of properties to get unique property labels from
2483
+ * @param options - Search options, including whether to include nested properties
2484
+ * @returns Array of unique property labels
2485
+ */
2486
+ function getUniquePropertyLabels(properties, options = DEFAULT_OPTIONS) {
2487
+ const { includeNestedProperties } = options;
2488
+ const uniquePropertyLabels = new Array();
2489
+ for (const property of properties) {
2490
+ if (uniquePropertyLabels.includes(property.label)) continue;
2491
+ uniquePropertyLabels.push(property.label);
2492
+ if (property.properties.length > 0 && includeNestedProperties) {
2493
+ const nestedProperties = getUniquePropertyLabels(property.properties, { includeNestedProperties: true });
2494
+ for (const property of nestedProperties) {
2495
+ if (uniquePropertyLabels.includes(property)) continue;
2496
+ uniquePropertyLabels.push(property);
2497
+ }
2498
+ }
2499
+ }
2500
+ return uniquePropertyLabels;
2501
+ }
2502
+ /**
2503
+ * Get the leaf property values from an array of property values
2504
+ * @param propertyValues - The array of property values to get the leaf property values from
2505
+ * @returns The array of leaf property values
2506
+ */
2507
+ function getLeafPropertyValues(propertyValues) {
2508
+ return propertyValues.filter((value) => value.hierarchy.isLeaf);
2509
+ }
2510
+ /**
2511
+ * Filters a property based on a label and value criteria
2512
+ *
2513
+ * @param property - The property to filter
2514
+ * @param filter - Filter criteria containing label and value to match
2515
+ * @param filter.label - The label to filter by
2516
+ * @param filter.value - The value to filter by
2517
+ * @param options - Search options, including whether to include nested properties
2518
+ * @returns True if the property matches the filter criteria, false otherwise
2519
+ */
2520
+ function filterProperties(property, filter, options = DEFAULT_OPTIONS) {
2521
+ const { includeNestedProperties } = options;
2522
+ if (filter.label.toLocaleLowerCase("en-US") === "all fields" || property.label.toLocaleLowerCase("en-US") === filter.label.toLocaleLowerCase("en-US")) {
2523
+ let isFound = property.values.some((value) => {
2524
+ if (value.content === null) return false;
2525
+ if (typeof value.content === "string") {
2526
+ if (typeof filter.value !== "string") return false;
2527
+ return value.content.toLocaleLowerCase("en-US").includes(filter.value.toLocaleLowerCase("en-US"));
2528
+ }
2529
+ if (typeof value.content === "number") {
2530
+ if (typeof filter.value !== "number") return false;
2531
+ return value.content === filter.value;
2532
+ }
2533
+ if (typeof value.content === "boolean") {
2534
+ if (typeof filter.value !== "boolean") return false;
2535
+ return value.content === filter.value;
2536
+ }
2537
+ return false;
2538
+ });
2539
+ if (!isFound && includeNestedProperties) isFound = property.properties.some((property) => filterProperties(property, filter, { includeNestedProperties: true }));
2540
+ return isFound;
2541
+ }
2542
+ return false;
2543
+ }
2544
+
2545
+ //#endregion
2546
+ //#region src/utils/parse/website.ts
2547
+ /**
2548
+ * Extracts CSS style properties for a given presentation variant.
2549
+ *
2550
+ * @param properties - Array of properties to parse
2551
+ * @param cssVariant - CSS variant to parse
2552
+ * @returns Array of CSS styles
2553
+ */
2554
+ function parseCssStylesFromProperties(properties, cssVariant) {
2555
+ const cssProperties = getPropertyByLabelAndValue(properties, "presentation", cssVariant != null ? `css-${cssVariant}` : "css")?.properties ?? [];
2556
+ const styles = [];
2557
+ for (const property of cssProperties) {
2558
+ const value = property.values[0]?.content?.toString();
2559
+ if (value != null) styles.push({
2560
+ label: property.label,
2561
+ value
2562
+ });
2563
+ }
2564
+ return styles;
2565
+ }
2566
+ /**
2567
+ * Parses responsive CSS styles (default, tablet, mobile) from properties.
2568
+ *
2569
+ * @param properties - Array of properties to parse
2570
+ * @returns Object containing responsive CSS styles
2571
+ */
2572
+ function parseResponsiveCssStyles(properties) {
2573
+ return {
2574
+ default: parseCssStylesFromProperties(properties),
2575
+ tablet: parseCssStylesFromProperties(properties, "tablet"),
2576
+ mobile: parseCssStylesFromProperties(properties, "mobile")
2577
+ };
2578
+ }
2579
+ /**
2580
+ * Parses raw bounds data into a standardized bounds structure
2581
+ *
2582
+ * @param bounds - Raw bounds data in OCHRE format
2583
+ * @returns Parsed bounds object
2584
+ */
2585
+ function parseBounds(bounds) {
2586
+ const result = boundsSchema.safeParse(bounds);
2587
+ if (!result.success) throw new Error(`Invalid bounds: ${result.error.message}`);
2588
+ return result.data;
2589
+ }
2590
+ /**
2591
+ * Parses all context option arrays from an options object.
2592
+ *
2593
+ * @param options - Options object containing context options
2594
+ * @param options.flattenContexts - Flatten contexts
2595
+ * @param options.suppressContexts - Suppress contexts
2596
+ * @param options.filterContexts - Filter contexts
2597
+ * @param options.sortContexts - Sort contexts
2598
+ * @param options.detailContexts - Detail contexts
2599
+ * @param options.downloadContexts - Download contexts
2600
+ * @param options.labelContexts - Label contexts
2601
+ * @param options.prominentContexts - Prominent contexts
2602
+ * @returns Parsed context options
2603
+ */
2604
+ function parseAllOptionContexts(options) {
2605
+ function handleContexts(v) {
2606
+ return parseContexts(v != null ? ensureArray(v) : []);
2607
+ }
2608
+ return {
2609
+ flatten: handleContexts(options.flattenContexts),
2610
+ suppress: handleContexts(options.suppressContexts),
2611
+ filter: handleContexts(options.filterContexts),
2612
+ sort: handleContexts(options.sortContexts),
2613
+ detail: handleContexts(options.detailContexts),
2614
+ download: handleContexts(options.downloadContexts),
2615
+ label: handleContexts(options.labelContexts),
2616
+ prominent: handleContexts(options.prominentContexts)
2617
+ };
2618
+ }
2619
+ /**
2620
+ * Parses raw web element properties into a standardized WebElementComponent structure
2621
+ *
2622
+ * @param componentProperty - Raw component property data in OCHRE format
2623
+ * @param elementResource - Raw element resource data in OCHRE format
2624
+ * @returns Parsed WebElementComponent object
2625
+ */
2626
+ function parseWebElementProperties(componentProperty, elementResource) {
2627
+ const unparsedComponentName = componentProperty.values[0].content;
2628
+ const { data: componentName } = componentSchema.safeParse(unparsedComponentName);
2629
+ let properties = null;
2630
+ const links = elementResource.links ? parseLinks(ensureArray(elementResource.links)) : [];
2631
+ switch (componentName) {
2632
+ case "3d-viewer": {
2633
+ const resourceLink = links.find((link) => link.category === "resource" && link.fileFormat === "model/obj");
2634
+ if (resourceLink?.uuid == null) throw new Error(`Resource link not found for the following component: “${componentName}”`);
2635
+ let isInteractive = getPropertyValueByLabel(componentProperty.properties, "is-interactive");
2636
+ isInteractive ??= true;
2637
+ let isControlsDisplayed = getPropertyValueByLabel(componentProperty.properties, "controls-displayed");
2638
+ isControlsDisplayed ??= true;
2639
+ properties = {
2640
+ component: "3d-viewer",
2641
+ linkUuid: resourceLink.uuid,
2642
+ fileSize: resourceLink.fileSize,
2643
+ isInteractive,
2644
+ isControlsDisplayed
2645
+ };
2646
+ break;
2647
+ }
2648
+ case "advanced-search": {
2649
+ const boundElementPropertyUuid = getPropertyByLabel(componentProperty.properties, "bound-element")?.values[0]?.uuid ?? null;
2650
+ const linkToProperty = getPropertyByLabel(componentProperty.properties, "link-to");
2651
+ const href = linkToProperty?.values[0]?.href ?? linkToProperty?.values[0]?.slug ?? null;
2652
+ if (boundElementPropertyUuid == null && href == null) throw new Error(`Bound element or href not found for the following component: “${componentName}”`);
2653
+ properties = {
2654
+ component: "advanced-search",
2655
+ boundElementUuid: boundElementPropertyUuid,
2656
+ href
2657
+ };
2658
+ break;
2659
+ }
2660
+ case "annotated-document": {
2661
+ const documentLink = links.find((link) => link.type === "internalDocument");
2662
+ if (documentLink?.uuid == null) throw new Error(`Document link not found for the following component: “${componentName}”`);
2663
+ properties = {
2664
+ component: "annotated-document",
2665
+ linkUuid: documentLink.uuid
2666
+ };
2667
+ break;
2668
+ }
2669
+ case "annotated-image": {
2670
+ const imageLinks = links.filter((link) => link.type === "image" || link.type === "IIIF");
2671
+ if (imageLinks.length === 0 || imageLinks[0].uuid == null) throw new Error(`Image link not found for the following component: “${componentName}”`);
2672
+ let isFilterInputDisplayed = getPropertyValueByLabel(componentProperty.properties, "filter-input-displayed");
2673
+ isFilterInputDisplayed ??= true;
2674
+ let isOptionsDisplayed = getPropertyValueByLabel(componentProperty.properties, "options-displayed");
2675
+ isOptionsDisplayed ??= true;
2676
+ let isAnnotationHighlightsDisplayed = getPropertyValueByLabel(componentProperty.properties, "annotation-highlights-displayed");
2677
+ isAnnotationHighlightsDisplayed ??= true;
2678
+ let isAnnotationTooltipsDisplayed = getPropertyValueByLabel(componentProperty.properties, "annotation-tooltips-displayed");
2679
+ isAnnotationTooltipsDisplayed ??= true;
2680
+ properties = {
2681
+ component: "annotated-image",
2682
+ linkUuid: imageLinks[0].uuid,
2683
+ isFilterInputDisplayed,
2684
+ isOptionsDisplayed,
2685
+ isAnnotationHighlightsDisplayed,
2686
+ isAnnotationTooltipsDisplayed
2687
+ };
2688
+ break;
2689
+ }
2690
+ case "audio-player": {
2691
+ const audioLink = links.find((link) => link.type === "audio");
2692
+ if (audioLink?.uuid == null) throw new Error(`Audio link not found for the following component: “${componentName}”`);
2693
+ let isSpeedControlsDisplayed = getPropertyValueByLabel(componentProperty.properties, "speed-controls-displayed");
2694
+ isSpeedControlsDisplayed ??= true;
2695
+ let isVolumeControlsDisplayed = getPropertyValueByLabel(componentProperty.properties, "volume-controls-displayed");
2256
2696
  isVolumeControlsDisplayed ??= true;
2257
2697
  let isSeekBarDisplayed = getPropertyValueByLabel(componentProperty.properties, "seek-bar-displayed");
2258
2698
  isSeekBarDisplayed ??= true;
@@ -2604,1172 +3044,741 @@ function parseWebElementProperties(componentProperty, elementResource) {
2604
3044
  endIcon
2605
3045
  });
2606
3046
  }
2607
- if (queries.length === 0) throw new Error(`No queries found for the following component: “${componentName}”`);
2608
- const displayedProperties = getPropertyByLabel(componentProperty.properties, "use-property");
2609
- let itemVariant = getPropertyValueByLabel(componentProperty.properties, "item-variant");
2610
- itemVariant ??= "detailed";
2611
- let paginationVariant = getPropertyValueByLabel(componentProperty.properties, "pagination-variant");
2612
- paginationVariant ??= "default";
2613
- let layout = getPropertyValueByLabel(componentProperty.properties, "layout");
2614
- layout ??= "image-start";
2615
- properties = {
2616
- component: "query",
2617
- itemCategory,
2618
- queries,
2619
- displayedProperties: displayedProperties?.values.filter((value) => value.uuid !== null).map((value) => ({
2620
- uuid: value.uuid,
2621
- label: value.content?.toString() ?? ""
2622
- })) ?? null,
2623
- itemVariant,
2624
- paginationVariant,
2625
- layout
2626
- };
2627
- break;
2628
- }
2629
- case "table": {
2630
- const tableLink = links.find((link) => link.category === "set");
2631
- if (tableLink?.uuid == null) throw new Error(`Table link not found for the following component: “${componentName}”`);
2632
- properties = {
2633
- component: "table",
2634
- linkUuid: tableLink.uuid
2635
- };
2636
- break;
2637
- }
2638
- case "search-bar": {
2639
- let queryVariant = null;
2640
- queryVariant = getPropertyValueByLabel(componentProperty.properties, "query-variant");
2641
- queryVariant ??= "submit";
2642
- const boundElementUuid = getPropertyByLabel(componentProperty.properties, "bound-element")?.values[0]?.uuid ?? null;
2643
- const linkToProperty = getPropertyByLabel(componentProperty.properties, "link-to");
2644
- const href = linkToProperty?.values[0]?.href ?? linkToProperty?.values[0]?.slug ?? null;
2645
- if (!boundElementUuid && !href) throw new Error(`Bound element or href not found for the following component: “${componentName}”`);
2646
- let placeholder = getPropertyValueByLabel(componentProperty.properties, "placeholder-text");
2647
- placeholder ??= null;
2648
- let baseFilterQueries = getPropertyValueByLabel(componentProperty.properties, "base-filter-queries");
2649
- baseFilterQueries ??= null;
2650
- properties = {
2651
- component: "search-bar",
2652
- queryVariant,
2653
- placeholder,
2654
- baseFilterQueries: baseFilterQueries?.replaceAll(String.raw`\{`, "{").replaceAll(String.raw`\}`, "}") ?? null,
2655
- boundElementUuid,
2656
- href
2657
- };
2658
- break;
2659
- }
2660
- case "text": {
2661
- const content = elementResource.document && "content" in elementResource.document ? parseDocument(elementResource.document.content) : null;
2662
- if (!content) throw new Error(`Content not found for the following component: “${componentName}”`);
2663
- let variantName = "block";
2664
- let variant;
2665
- const variantProperty = getPropertyByLabel(componentProperty.properties, "variant");
2666
- if (variantProperty !== null) {
2667
- variantName = variantProperty.values[0].content;
2668
- if (variantName === "paragraph" || variantName === "label" || variantName === "heading" || variantName === "display") {
2669
- let size = getPropertyValueByLabel(variantProperty.properties, "size");
2670
- size ??= "md";
2671
- variant = {
2672
- name: variantName,
2673
- size
2674
- };
2675
- } else variant = { name: variantName };
2676
- } else variant = { name: variantName };
2677
- let headingLevel = getPropertyValueByLabel(componentProperty.properties, "heading-level");
2678
- headingLevel ??= null;
2679
- properties = {
2680
- component: "text",
2681
- variant,
2682
- headingLevel,
2683
- content
2684
- };
2685
- break;
2686
- }
2687
- case "timeline": {
2688
- const timelineLink = links.find((link) => link.category === "tree");
2689
- if (timelineLink?.uuid == null) throw new Error(`Timeline link not found for the following component: “${componentName}”`);
2690
- properties = {
2691
- component: "timeline",
2692
- linkUuid: timelineLink.uuid
2693
- };
2694
- break;
2695
- }
2696
- case "video": {
2697
- const videoLink = links.find((link) => link.type === "video");
2698
- if (videoLink?.uuid == null) throw new Error(`Video link not found for the following component: “${componentName}”`);
2699
- let isChaptersDisplayed = getPropertyValueByLabel(componentProperty.properties, "chapters-displayed");
2700
- isChaptersDisplayed ??= true;
2701
- properties = {
2702
- component: "video",
2703
- linkUuid: videoLink.uuid,
2704
- isChaptersDisplayed
2705
- };
2706
- break;
2707
- }
2708
- default:
2709
- console.warn(`Invalid or non-implemented component name “${unparsedComponentName?.toString() ?? "(unknown)"}” for the following element: “${parseStringContent(elementResource.identification.label)}”`);
2710
- break;
2711
- }
2712
- if (properties === null) throw new Error(`Properties not found for the following component: “${componentName}”`);
2713
- return properties;
2714
- }
2715
- function parseWebTitle(properties, identification, overrides) {
2716
- const title = {
2717
- label: identification.label,
2718
- variant: "default",
2719
- properties: {
2720
- isNameDisplayed: overrides?.isNameDisplayed ?? false,
2721
- isDescriptionDisplayed: overrides?.isDescriptionDisplayed ?? false,
2722
- isDateDisplayed: overrides?.isDateDisplayed ?? false,
2723
- isCreatorsDisplayed: overrides?.isCreatorsDisplayed ?? false,
2724
- isCountDisplayed: overrides?.isCountDisplayed ?? false
2725
- }
2726
- };
2727
- const titleProperties = getPropertyByLabelAndValue(properties, "presentation", "title")?.properties ?? [];
2728
- if (titleProperties.length > 0) {
2729
- title.variant = getPropertyValueByLabel(titleProperties, "variant") ?? "default";
2730
- title.properties.isNameDisplayed = getPropertyValueByLabel(titleProperties, "name-displayed") ?? false;
2731
- title.properties.isDescriptionDisplayed = getPropertyValueByLabel(titleProperties, "description-displayed") ?? false;
2732
- title.properties.isDateDisplayed = getPropertyValueByLabel(titleProperties, "date-displayed") ?? false;
2733
- title.properties.isCreatorsDisplayed = getPropertyValueByLabel(titleProperties, "creators-displayed") ?? false;
2734
- title.properties.isCountDisplayed = getPropertyValueByLabel(titleProperties, "count-displayed") ?? false;
2735
- }
2736
- return title;
2737
- }
2738
- /**
2739
- * Parses raw web element data into a standardized WebElement structure
2740
- *
2741
- * @param elementResource - Raw element resource data in OCHRE format
2742
- * @returns Parsed WebElement object
2743
- */
2744
- function parseWebElement(elementResource) {
2745
- const identification = parseIdentification(elementResource.identification);
2746
- const elementProperties = elementResource.properties?.property ? parseProperties(Array.isArray(elementResource.properties.property) ? elementResource.properties.property : [elementResource.properties.property]) : [];
2747
- const presentationProperty = getPropertyByLabel(elementProperties, "presentation");
2748
- if (presentationProperty === null) throw new Error(`Presentation property not found for element “${identification.label}”`);
2749
- const componentProperty = getPropertyByLabel(presentationProperty.properties, "component");
2750
- if (componentProperty === null) throw new Error(`Component for element “${identification.label}” not found`);
2751
- const properties = parseWebElementProperties(componentProperty, elementResource);
2752
- const cssStyles = parseResponsiveCssStyles(elementProperties);
2753
- const title = parseWebTitle(elementProperties, identification, {
2754
- isNameDisplayed: properties.component === "annotated-image" || properties.component === "annotated-document" || properties.component === "collection",
2755
- isCountDisplayed: properties.component === "collection" && properties.variant === "full"
2756
- });
2757
- return {
2758
- uuid: elementResource.uuid,
2759
- type: "element",
2760
- title,
2761
- cssStyles,
2762
- ...properties
2763
- };
2764
- }
2765
- /**
2766
- * Parses raw webpage data into a standardized Webpage structure
2767
- *
2768
- * @param webpageResource - Raw webpage resource data in OCHRE format
2769
- * @returns Parsed Webpage object
2770
- */
2771
- function parseWebpage(webpageResource, slugPrefix) {
2772
- const webpageProperties = webpageResource.properties ? parseProperties(ensureArray(webpageResource.properties.property)) : [];
2773
- if (webpageProperties.length === 0 || getPropertyValueByLabel(webpageProperties, "presentation") !== "page") return null;
2774
- const identification = parseIdentification(webpageResource.identification);
2775
- const slug = webpageResource.slug?.replace(/^\$[^-]*-/, "") ?? null;
2776
- if (slug == null) throw new Error(`Slug not found for page “${identification.label}”`);
2777
- const returnWebpage = {
2778
- uuid: webpageResource.uuid,
2779
- type: "page",
2780
- title: identification.label,
2781
- slug: slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug,
2782
- publicationDateTime: parseOptionalDate(webpageResource.publicationDateTime),
2783
- items: [],
2784
- properties: {
2785
- width: "default",
2786
- variant: "default",
2787
- isBreadcrumbsDisplayed: false,
2788
- isSidebarDisplayed: true,
2789
- isDisplayedInNavbar: true,
2790
- isNavbarSearchBarDisplayed: true,
2791
- backgroundImage: null,
2792
- cssStyles: {
2793
- default: [],
2794
- tablet: [],
2795
- mobile: []
2796
- }
2797
- },
2798
- webpages: []
2799
- };
2800
- const imageLink = (webpageResource.links != null ? parseLinks(ensureArray(webpageResource.links)) : []).find((link) => link.type === "image" || link.type === "IIIF");
2801
- const webpageResources = webpageResource.resource != null ? ensureArray(webpageResource.resource) : [];
2802
- const items = [];
2803
- for (const resource of webpageResources) {
2804
- const resourceType = getPropertyValueByLabel(resource.properties != null ? parseProperties(ensureArray(resource.properties.property)) : [], "presentation");
2805
- if (resourceType === null) continue;
2806
- switch (resourceType) {
2807
- case "segment": {
2808
- const segment = parseWebSegment(resource);
2809
- if (segment) items.push(segment);
2810
- break;
2811
- }
2812
- case "element": {
2813
- const element = parseWebElement(resource);
2814
- items.push(element);
2815
- break;
2816
- }
2817
- case "block": {
2818
- const block = parseWebBlock(resource);
2819
- if (block) items.push(block);
2820
- break;
2821
- }
3047
+ if (queries.length === 0) throw new Error(`No queries found for the following component: “${componentName}”`);
3048
+ const displayedProperties = getPropertyByLabel(componentProperty.properties, "use-property");
3049
+ let itemVariant = getPropertyValueByLabel(componentProperty.properties, "item-variant");
3050
+ itemVariant ??= "detailed";
3051
+ let paginationVariant = getPropertyValueByLabel(componentProperty.properties, "pagination-variant");
3052
+ paginationVariant ??= "default";
3053
+ let layout = getPropertyValueByLabel(componentProperty.properties, "layout");
3054
+ layout ??= "image-start";
3055
+ properties = {
3056
+ component: "query",
3057
+ itemCategory,
3058
+ queries,
3059
+ displayedProperties: displayedProperties?.values.filter((value) => value.uuid !== null).map((value) => ({
3060
+ uuid: value.uuid,
3061
+ label: value.content?.toString() ?? ""
3062
+ })) ?? null,
3063
+ itemVariant,
3064
+ paginationVariant,
3065
+ layout
3066
+ };
3067
+ break;
2822
3068
  }
2823
- }
2824
- returnWebpage.webpages = webpageResource.resource != null ? parseWebpageResources(ensureArray(webpageResource.resource), "page") : [];
2825
- const webpageSubProperties = getPropertyByLabelAndValue(webpageProperties, "presentation", "page")?.properties ?? [];
2826
- if (webpageSubProperties.length > 0) {
2827
- returnWebpage.properties.isDisplayedInNavbar = getPropertyValueByLabel(webpageSubProperties, "displayed-in-navbar") ?? true;
2828
- returnWebpage.properties.width = getPropertyValueByLabel(webpageSubProperties, "width") ?? "default";
2829
- returnWebpage.properties.variant = getPropertyValueByLabel(webpageSubProperties, "variant") ?? "default";
2830
- returnWebpage.properties.isSidebarDisplayed = getPropertyValueByLabel(webpageSubProperties, "sidebar-displayed") ?? true;
2831
- returnWebpage.properties.isBreadcrumbsDisplayed = getPropertyValueByLabel(webpageSubProperties, "breadcrumbs-displayed") ?? false;
2832
- returnWebpage.properties.isNavbarSearchBarDisplayed = getPropertyValueByLabel(webpageSubProperties, "navbar-search-bar-displayed") ?? true;
2833
- }
2834
- if (imageLink?.uuid != null) returnWebpage.properties.backgroundImage = {
2835
- uuid: imageLink.uuid,
2836
- label: imageLink.identification?.label ?? null,
2837
- description: imageLink.description ?? null,
2838
- width: imageLink.image?.width ?? 0,
2839
- height: imageLink.image?.height ?? 0,
2840
- quality: "high"
2841
- };
2842
- returnWebpage.properties.cssStyles = parseResponsiveCssStyles(webpageProperties);
2843
- return returnWebpage;
2844
- }
2845
- /**
2846
- * Parses raw webpage resources into an array of Webpage objects
2847
- *
2848
- * @param webpageResources - Array of raw webpage resources in OCHRE format
2849
- * @returns Array of parsed Webpage objects
2850
- */
2851
- function parseWebpages(webpageResources, slugPrefix) {
2852
- const returnPages = [];
2853
- for (const webpageResource of webpageResources) {
2854
- const webpage = parseWebpage(webpageResource, slugPrefix);
2855
- if (webpage !== null) returnPages.push(webpage);
2856
- }
2857
- return returnPages;
2858
- }
2859
- /**
2860
- * Parses raw segment resource into a standardized WebSegment object
2861
- *
2862
- * @param segmentResource - Raw segment resource in OCHRE format
2863
- * @returns Parsed WebSegment object
2864
- */
2865
- function parseWebSegment(segmentResource, slugPrefix) {
2866
- const webpageProperties = segmentResource.properties ? parseProperties(ensureArray(segmentResource.properties.property)) : [];
2867
- if (webpageProperties.length === 0 || getPropertyValueByLabel(webpageProperties, "presentation") !== "segment") return null;
2868
- const identification = parseIdentification(segmentResource.identification);
2869
- const slug = segmentResource.identification.abbreviation != null ? parseFakeStringOrContent(segmentResource.identification.abbreviation) : null;
2870
- if (slug == null) throw new Error(`Slug not found for segment “${identification.label}”`);
2871
- const returnSegment = {
2872
- uuid: segmentResource.uuid,
2873
- type: "segment",
2874
- title: identification.label,
2875
- slug,
2876
- publicationDateTime: parseOptionalDate(segmentResource.publicationDateTime),
2877
- items: []
2878
- };
2879
- returnSegment.items = parseWebSegmentItems(segmentResource.resource ? ensureArray(segmentResource.resource) : [], slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug);
2880
- return returnSegment;
2881
- }
2882
- /**
2883
- * Parses raw segment resources into an array of WebSegment objects
2884
- *
2885
- * @param segmentResources - Array of raw segment resources in OCHRE format
2886
- * @returns Array of parsed WebSegment objects
2887
- */
2888
- function parseSegments(segmentResources, slugPrefix) {
2889
- const returnSegments = [];
2890
- for (const segmentResource of segmentResources) {
2891
- const segment = parseWebSegment(segmentResource, slugPrefix);
2892
- if (segment !== null) returnSegments.push(segment);
2893
- }
2894
- return returnSegments;
2895
- }
2896
- /**
2897
- * Parses raw segment item into a standardized WebSegmentItem object
2898
- *
2899
- * @param segmentItemResource - Raw segment item resource in OCHRE format
2900
- * @returns Parsed WebSegmentItem object
2901
- */
2902
- function parseWebSegmentItem(segmentItemResource, slugPrefix) {
2903
- const webpageProperties = segmentItemResource.properties ? parseProperties(ensureArray(segmentItemResource.properties.property)) : [];
2904
- if (webpageProperties.length === 0 || getPropertyValueByLabel(webpageProperties, "presentation") !== "segment-item") return null;
2905
- const identification = parseIdentification(segmentItemResource.identification);
2906
- const slug = segmentItemResource.identification.abbreviation != null ? parseFakeStringOrContent(segmentItemResource.identification.abbreviation) : null;
2907
- if (slug == null) throw new Error(`Slug not found for segment item “${identification.label}”`);
2908
- const returnSegmentItem = {
2909
- uuid: segmentItemResource.uuid,
2910
- type: "segment-item",
2911
- title: identification.label,
2912
- slug,
2913
- publicationDateTime: parseOptionalDate(segmentItemResource.publicationDateTime),
2914
- items: []
2915
- };
2916
- const resources = segmentItemResource.resource ? ensureArray(segmentItemResource.resource) : [];
2917
- returnSegmentItem.items.push(...parseWebpages(resources, slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug), ...parseSegments(resources, slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug));
2918
- return returnSegmentItem;
2919
- }
2920
- /**
2921
- * Parses raw segment items into an array of WebSegmentItem objects
2922
- *
2923
- * @param segmentItems - Array of raw segment items in OCHRE format
2924
- * @returns Array of parsed WebSegmentItem objects
2925
- */
2926
- function parseWebSegmentItems(segmentItems, slugPrefix) {
2927
- const returnItems = [];
2928
- for (const segmentItem of segmentItems) {
2929
- const segmentItemParsed = parseWebSegmentItem(segmentItem, slugPrefix);
2930
- if (segmentItemParsed !== null) returnItems.push(segmentItemParsed);
2931
- }
2932
- return returnItems;
2933
- }
2934
- /**
2935
- * Parses raw sidebar data into a standardized Sidebar structure
2936
- *
2937
- * @param resources - Array of raw sidebar resources in OCHRE format
2938
- * @returns Parsed Sidebar object
2939
- */
2940
- function parseSidebar(resources) {
2941
- let returnSidebar = null;
2942
- const items = [];
2943
- const title = {
2944
- label: "",
2945
- variant: "default",
2946
- properties: {
2947
- isNameDisplayed: false,
2948
- isDescriptionDisplayed: false,
2949
- isDateDisplayed: false,
2950
- isCreatorsDisplayed: false,
2951
- isCountDisplayed: false
3069
+ case "table": {
3070
+ const tableLink = links.find((link) => link.category === "set");
3071
+ if (tableLink?.uuid == null) throw new Error(`Table link not found for the following component: “${componentName}”`);
3072
+ properties = {
3073
+ component: "table",
3074
+ linkUuid: tableLink.uuid
3075
+ };
3076
+ break;
2952
3077
  }
2953
- };
2954
- let layout = "start";
2955
- let mobileLayout = "default";
2956
- const cssStyles = {
2957
- default: [],
2958
- tablet: [],
2959
- mobile: []
2960
- };
2961
- const sidebarResource = resources.find((resource) => {
2962
- const resourceProperties = resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [];
2963
- return getPropertyValueByLabel(resourceProperties, "presentation") === "element" && getPropertyValueByLabel(resourceProperties[0]?.properties ?? [], "component") === "sidebar";
2964
- });
2965
- if (sidebarResource != null) {
2966
- title.label = parseFakeStringOrContent(sidebarResource.identification.label);
2967
- const sidebarBaseProperties = sidebarResource.properties ? parseProperties(ensureArray(sidebarResource.properties.property)) : [];
2968
- 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 ?? [];
2969
- const sidebarLayoutProperty = sidebarProperties.find((property) => property.label === "layout");
2970
- if (sidebarLayoutProperty) layout = sidebarLayoutProperty.values[0].content;
2971
- const sidebarMobileLayoutProperty = sidebarProperties.find((property) => property.label === "layout-mobile");
2972
- if (sidebarMobileLayoutProperty) mobileLayout = sidebarMobileLayoutProperty.values[0].content;
2973
- const parsedCssStyles = parseResponsiveCssStyles(sidebarBaseProperties);
2974
- cssStyles.default = parsedCssStyles.default;
2975
- cssStyles.tablet = parsedCssStyles.tablet;
2976
- cssStyles.mobile = parsedCssStyles.mobile;
2977
- const titleProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0].content === "title")?.properties;
2978
- if (titleProperties) {
2979
- const titleVariant = getPropertyValueByLabel(titleProperties, "variant");
2980
- if (titleVariant) title.variant = titleVariant;
2981
- title.properties.isNameDisplayed = getPropertyValueByLabel(titleProperties, "name-displayed") === true;
2982
- title.properties.isDescriptionDisplayed = getPropertyValueByLabel(titleProperties, "description-displayed") === true;
2983
- title.properties.isDateDisplayed = getPropertyValueByLabel(titleProperties, "date-displayed") === true;
2984
- title.properties.isCreatorsDisplayed = getPropertyValueByLabel(titleProperties, "creators-displayed") === true;
2985
- title.properties.isCountDisplayed = getPropertyValueByLabel(titleProperties, "count-displayed") === true;
3078
+ case "search-bar": {
3079
+ let queryVariant = null;
3080
+ queryVariant = getPropertyValueByLabel(componentProperty.properties, "query-variant");
3081
+ queryVariant ??= "submit";
3082
+ const boundElementUuid = getPropertyByLabel(componentProperty.properties, "bound-element")?.values[0]?.uuid ?? null;
3083
+ const linkToProperty = getPropertyByLabel(componentProperty.properties, "link-to");
3084
+ const href = linkToProperty?.values[0]?.href ?? linkToProperty?.values[0]?.slug ?? null;
3085
+ if (!boundElementUuid && !href) throw new Error(`Bound element or href not found for the following component: “${componentName}”`);
3086
+ let placeholder = getPropertyValueByLabel(componentProperty.properties, "placeholder-text");
3087
+ placeholder ??= null;
3088
+ let baseFilterQueries = getPropertyValueByLabel(componentProperty.properties, "base-filter-queries");
3089
+ baseFilterQueries ??= null;
3090
+ properties = {
3091
+ component: "search-bar",
3092
+ queryVariant,
3093
+ placeholder,
3094
+ baseFilterQueries: baseFilterQueries?.replaceAll(String.raw`\{`, "{").replaceAll(String.raw`\}`, "}") ?? null,
3095
+ boundElementUuid,
3096
+ href
3097
+ };
3098
+ break;
3099
+ }
3100
+ case "text": {
3101
+ const content = elementResource.document && "content" in elementResource.document ? parseDocument(elementResource.document.content) : null;
3102
+ if (!content) throw new Error(`Content not found for the following component: “${componentName}”`);
3103
+ let variantName = "block";
3104
+ let variant;
3105
+ const variantProperty = getPropertyByLabel(componentProperty.properties, "variant");
3106
+ if (variantProperty !== null) {
3107
+ variantName = variantProperty.values[0].content;
3108
+ if (variantName === "paragraph" || variantName === "label" || variantName === "heading" || variantName === "display") {
3109
+ let size = getPropertyValueByLabel(variantProperty.properties, "size");
3110
+ size ??= "md";
3111
+ variant = {
3112
+ name: variantName,
3113
+ size
3114
+ };
3115
+ } else variant = { name: variantName };
3116
+ } else variant = { name: variantName };
3117
+ let headingLevel = getPropertyValueByLabel(componentProperty.properties, "heading-level");
3118
+ headingLevel ??= null;
3119
+ properties = {
3120
+ component: "text",
3121
+ variant,
3122
+ headingLevel,
3123
+ content
3124
+ };
3125
+ break;
3126
+ }
3127
+ case "timeline": {
3128
+ const timelineLink = links.find((link) => link.category === "tree");
3129
+ if (timelineLink?.uuid == null) throw new Error(`Timeline link not found for the following component: “${componentName}”`);
3130
+ properties = {
3131
+ component: "timeline",
3132
+ linkUuid: timelineLink.uuid
3133
+ };
3134
+ break;
3135
+ }
3136
+ case "video": {
3137
+ const videoLink = links.find((link) => link.type === "video");
3138
+ if (videoLink?.uuid == null) throw new Error(`Video link not found for the following component: “${componentName}”`);
3139
+ let isChaptersDisplayed = getPropertyValueByLabel(componentProperty.properties, "chapters-displayed");
3140
+ isChaptersDisplayed ??= true;
3141
+ properties = {
3142
+ component: "video",
3143
+ linkUuid: videoLink.uuid,
3144
+ isChaptersDisplayed
3145
+ };
3146
+ break;
2986
3147
  }
2987
- const sidebarResources = sidebarResource.resource ? ensureArray(sidebarResource.resource) : [];
2988
- for (const resource of sidebarResources) {
2989
- const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [], "presentation");
2990
- if (resourceType === null) continue;
2991
- switch (resourceType) {
2992
- case "element": {
2993
- const element = parseWebElement(resource);
2994
- items.push(element);
2995
- break;
2996
- }
2997
- case "block": {
2998
- const block = parseWebBlock(resource);
2999
- if (block) items.push(block);
3000
- break;
3001
- }
3002
- }
3148
+ default:
3149
+ console.warn(`Invalid or non-implemented component name “${unparsedComponentName?.toString() ?? "(unknown)"}” for the following element: “${parseStringContent(elementResource.identification.label)}”`);
3150
+ break;
3151
+ }
3152
+ if (properties === null) throw new Error(`Properties not found for the following component: “${componentName}”`);
3153
+ return properties;
3154
+ }
3155
+ function parseWebTitle(properties, identification, overrides) {
3156
+ const title = {
3157
+ label: identification.label,
3158
+ variant: "default",
3159
+ properties: {
3160
+ isNameDisplayed: overrides?.isNameDisplayed ?? false,
3161
+ isDescriptionDisplayed: overrides?.isDescriptionDisplayed ?? false,
3162
+ isDateDisplayed: overrides?.isDateDisplayed ?? false,
3163
+ isCreatorsDisplayed: overrides?.isCreatorsDisplayed ?? false,
3164
+ isCountDisplayed: overrides?.isCountDisplayed ?? false
3003
3165
  }
3166
+ };
3167
+ const titleProperties = getPropertyByLabelAndValue(properties, "presentation", "title")?.properties ?? [];
3168
+ if (titleProperties.length > 0) {
3169
+ title.variant = getPropertyValueByLabel(titleProperties, "variant") ?? "default";
3170
+ title.properties.isNameDisplayed = getPropertyValueByLabel(titleProperties, "name-displayed") ?? false;
3171
+ title.properties.isDescriptionDisplayed = getPropertyValueByLabel(titleProperties, "description-displayed") ?? false;
3172
+ title.properties.isDateDisplayed = getPropertyValueByLabel(titleProperties, "date-displayed") ?? false;
3173
+ title.properties.isCreatorsDisplayed = getPropertyValueByLabel(titleProperties, "creators-displayed") ?? false;
3174
+ title.properties.isCountDisplayed = getPropertyValueByLabel(titleProperties, "count-displayed") ?? false;
3004
3175
  }
3005
- if (items.length > 0) returnSidebar = {
3006
- isDisplayed: true,
3007
- items,
3176
+ return title;
3177
+ }
3178
+ /**
3179
+ * Parses raw web element data into a standardized WebElement structure
3180
+ *
3181
+ * @param elementResource - Raw element resource data in OCHRE format
3182
+ * @returns Parsed WebElement object
3183
+ */
3184
+ function parseWebElement(elementResource) {
3185
+ const identification = parseIdentification(elementResource.identification);
3186
+ const elementProperties = elementResource.properties?.property ? parseProperties(Array.isArray(elementResource.properties.property) ? elementResource.properties.property : [elementResource.properties.property]) : [];
3187
+ const presentationProperty = getPropertyByLabel(elementProperties, "presentation");
3188
+ if (presentationProperty === null) throw new Error(`Presentation property not found for element “${identification.label}”`);
3189
+ const componentProperty = getPropertyByLabel(presentationProperty.properties, "component");
3190
+ if (componentProperty === null) throw new Error(`Component for element “${identification.label}” not found`);
3191
+ const properties = parseWebElementProperties(componentProperty, elementResource);
3192
+ const cssStyles = parseResponsiveCssStyles(elementProperties);
3193
+ const title = parseWebTitle(elementProperties, identification, {
3194
+ isNameDisplayed: properties.component === "annotated-image" || properties.component === "annotated-document" || properties.component === "collection",
3195
+ isCountDisplayed: properties.component === "collection" && properties.variant === "full"
3196
+ });
3197
+ return {
3198
+ uuid: elementResource.uuid,
3199
+ type: "element",
3008
3200
  title,
3009
- layout,
3010
- mobileLayout,
3011
- cssStyles
3201
+ cssStyles,
3202
+ ...properties
3012
3203
  };
3013
- return returnSidebar;
3014
3204
  }
3015
3205
  /**
3016
- * Parses raw text element data for accordion layout with items support
3206
+ * Parses raw webpage resources into standardized WebElement or Webpage objects
3017
3207
  *
3018
- * @param elementResource - Raw element resource data in OCHRE format
3019
- * @returns Parsed text WebElement with items array
3208
+ * @param webpageResources - Array of raw webpage resources in OCHRE format
3209
+ * @param type - Type of resource to parse ("element" or "page")
3210
+ * @returns Array of parsed WebElement or Webpage objects
3020
3211
  */
3021
- function parseWebElementForAccordion(elementResource) {
3022
- const textElement = parseWebElement(elementResource);
3023
- const childResources = elementResource.resource ? ensureArray(elementResource.resource) : [];
3024
- const items = [];
3025
- for (const resource of childResources) {
3026
- const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [], "presentation");
3027
- if (resourceType === null) continue;
3028
- switch (resourceType) {
3212
+ const parseWebpageResources = (webpageResources, type) => {
3213
+ const returnElements = [];
3214
+ for (const resource of webpageResources) {
3215
+ if (getPropertyByLabelAndValue(resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [], "presentation", type) === null) continue;
3216
+ switch (type) {
3029
3217
  case "element": {
3030
3218
  const element = parseWebElement(resource);
3031
- items.push(element);
3219
+ returnElements.push(element);
3220
+ break;
3221
+ }
3222
+ case "page": {
3223
+ const webpage = parseWebpage(resource);
3224
+ if (webpage) returnElements.push(webpage);
3032
3225
  break;
3033
3226
  }
3034
3227
  case "block": {
3035
3228
  const block = parseWebBlock(resource);
3036
- if (block) items.push(block);
3229
+ if (block) returnElements.push(block);
3037
3230
  break;
3038
3231
  }
3039
3232
  }
3040
3233
  }
3041
- return {
3042
- ...textElement,
3043
- items
3044
- };
3045
- }
3234
+ return returnElements;
3235
+ };
3046
3236
  /**
3047
- * Parses raw block data into a standardized WebBlock structure
3237
+ * Parses raw webpage data into a standardized Webpage structure
3048
3238
  *
3049
- * @param blockResource - Raw block resource data in OCHRE format
3050
- * @returns Parsed WebBlock object
3239
+ * @param webpageResource - Raw webpage resource data in OCHRE format
3240
+ * @returns Parsed Webpage object
3051
3241
  */
3052
- function parseWebBlock(blockResource) {
3053
- const blockProperties = blockResource.properties ? parseProperties(ensureArray(blockResource.properties.property)) : [];
3054
- const returnBlock = {
3055
- uuid: blockResource.uuid,
3056
- type: "block",
3057
- title: parseWebTitle(blockProperties, parseIdentification(blockResource.identification)),
3242
+ function parseWebpage(webpageResource, slugPrefix) {
3243
+ const webpageProperties = webpageResource.properties ? parseProperties(ensureArray(webpageResource.properties.property)) : [];
3244
+ if (webpageProperties.length === 0 || getPropertyValueByLabel(webpageProperties, "presentation") !== "page") return null;
3245
+ const identification = parseIdentification(webpageResource.identification);
3246
+ const slug = webpageResource.slug?.replace(/^\$[^-]*-/, "") ?? null;
3247
+ if (slug == null) throw new Error(`Slug not found for page “${identification.label}”`);
3248
+ const returnWebpage = {
3249
+ uuid: webpageResource.uuid,
3250
+ type: "page",
3251
+ title: identification.label,
3252
+ slug: slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug,
3253
+ publicationDateTime: parseOptionalDate(webpageResource.publicationDateTime),
3058
3254
  items: [],
3059
3255
  properties: {
3060
- default: {
3061
- layout: "vertical",
3062
- spacing: null,
3063
- gap: null
3064
- },
3065
- mobile: null,
3066
- tablet: null
3256
+ width: "default",
3257
+ variant: "default",
3258
+ isBreadcrumbsDisplayed: false,
3259
+ isSidebarDisplayed: true,
3260
+ isDisplayedInNavbar: true,
3261
+ isNavbarSearchBarDisplayed: true,
3262
+ backgroundImage: null,
3263
+ cssStyles: {
3264
+ default: [],
3265
+ tablet: [],
3266
+ mobile: []
3267
+ }
3067
3268
  },
3068
- cssStyles: {
3069
- default: [],
3070
- tablet: [],
3071
- mobile: []
3072
- }
3269
+ webpages: []
3073
3270
  };
3074
- const blockMainProperties = getPropertyByLabelAndValue(blockProperties, "presentation", "block")?.properties ?? [];
3075
- if (blockMainProperties.length > 0) {
3076
- returnBlock.properties.default.layout = getPropertyValueByLabel(blockMainProperties, "layout") ?? "vertical";
3077
- if (returnBlock.properties.default.layout === "accordion") {
3078
- returnBlock.properties.default.isAccordionEnabled = getPropertyValueByLabel(blockMainProperties, "accordion-enabled") ?? true;
3079
- returnBlock.properties.default.isAccordionExpandedByDefault = getPropertyValueByLabel(blockMainProperties, "accordion-expanded") ?? true;
3080
- returnBlock.properties.default.isAccordionSidebarDisplayed = getPropertyValueByLabel(blockMainProperties, "accordion-sidebar-displayed") ?? false;
3081
- }
3082
- returnBlock.properties.default.spacing = getPropertyValueByLabel(blockMainProperties, "spacing") ?? null;
3083
- returnBlock.properties.default.gap = getPropertyValueByLabel(blockMainProperties, "gap") ?? null;
3084
- const tabletOverwriteProperty = getPropertyByLabel(blockMainProperties, "overwrite-tablet");
3085
- if (tabletOverwriteProperty !== null) {
3086
- const tabletOverwriteProperties = tabletOverwriteProperty.properties;
3087
- const propertiesTablet = {
3088
- layout: getPropertyValueByLabel(tabletOverwriteProperties, "layout") ?? void 0,
3089
- spacing: getPropertyValueByLabel(tabletOverwriteProperties, "spacing") ?? void 0,
3090
- gap: getPropertyValueByLabel(tabletOverwriteProperties, "gap") ?? void 0,
3091
- isAccordionEnabled: void 0,
3092
- isAccordionExpandedByDefault: void 0,
3093
- isAccordionSidebarDisplayed: void 0
3094
- };
3095
- if (propertiesTablet.layout === "accordion" || returnBlock.properties.default.layout === "accordion") {
3096
- propertiesTablet.isAccordionEnabled = getPropertyValueByLabel(tabletOverwriteProperties, "accordion-enabled") ?? void 0;
3097
- propertiesTablet.isAccordionExpandedByDefault = getPropertyValueByLabel(tabletOverwriteProperties, "accordion-expanded") ?? void 0;
3098
- propertiesTablet.isAccordionSidebarDisplayed = getPropertyValueByLabel(tabletOverwriteProperties, "accordion-sidebar-displayed") ?? void 0;
3271
+ const imageLink = (webpageResource.links != null ? parseLinks(ensureArray(webpageResource.links)) : []).find((link) => link.type === "image" || link.type === "IIIF");
3272
+ const webpageResources = webpageResource.resource != null ? ensureArray(webpageResource.resource) : [];
3273
+ const items = [];
3274
+ for (const resource of webpageResources) {
3275
+ const resourceType = getPropertyValueByLabel(resource.properties != null ? parseProperties(ensureArray(resource.properties.property)) : [], "presentation");
3276
+ if (resourceType === null) continue;
3277
+ switch (resourceType) {
3278
+ case "segment": {
3279
+ const segment = parseWebSegment(resource);
3280
+ if (segment) items.push(segment);
3281
+ break;
3099
3282
  }
3100
- if (Object.values(propertiesTablet).every((value) => value != null)) returnBlock.properties.tablet = propertiesTablet;
3101
- }
3102
- const mobileOverwriteProperty = getPropertyByLabel(blockMainProperties, "overwrite-tablet");
3103
- if (mobileOverwriteProperty !== null) {
3104
- const mobileOverwriteProperties = mobileOverwriteProperty.properties;
3105
- const propertiesMobile = {
3106
- layout: getPropertyValueByLabel(mobileOverwriteProperties, "layout") ?? void 0,
3107
- spacing: getPropertyValueByLabel(mobileOverwriteProperties, "spacing") ?? void 0,
3108
- gap: getPropertyValueByLabel(mobileOverwriteProperties, "gap") ?? void 0,
3109
- isAccordionEnabled: void 0,
3110
- isAccordionExpandedByDefault: void 0,
3111
- isAccordionSidebarDisplayed: void 0
3112
- };
3113
- if (propertiesMobile.layout === "accordion" || returnBlock.properties.default.layout === "accordion") {
3114
- propertiesMobile.isAccordionEnabled = getPropertyValueByLabel(mobileOverwriteProperties, "accordion-enabled") ?? void 0;
3115
- propertiesMobile.isAccordionExpandedByDefault = getPropertyValueByLabel(mobileOverwriteProperties, "accordion-expanded") ?? void 0;
3116
- propertiesMobile.isAccordionSidebarDisplayed = getPropertyValueByLabel(mobileOverwriteProperties, "accordion-sidebar-displayed") ?? void 0;
3283
+ case "element": {
3284
+ const element = parseWebElement(resource);
3285
+ items.push(element);
3286
+ break;
3117
3287
  }
3118
- if (Object.values(propertiesMobile).every((value) => value != null)) returnBlock.properties.mobile = propertiesMobile;
3119
- }
3120
- }
3121
- const blockResources = blockResource.resource ? ensureArray(blockResource.resource) : [];
3122
- if (returnBlock.properties.default.layout === "accordion") {
3123
- const accordionItems = [];
3124
- for (const resource of blockResources) {
3125
- const resourceProperties = resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [];
3126
- const resourceType = getPropertyValueByLabel(resourceProperties, "presentation");
3127
- if (resourceType !== "element") throw new Error(`Accordion only accepts elements, but got “${resourceType}” for the following resource: “${parseStringContent(resource.identification.label)}”`);
3128
- const componentType = getPropertyValueByLabel(getPropertyByLabel(resourceProperties, "presentation")?.properties ?? [], "component");
3129
- if (componentType !== "text") throw new Error(`Accordion only accepts text components, but got “${componentType}” for the following resource: “${parseStringContent(resource.identification.label)}”`);
3130
- const element = parseWebElementForAccordion(resource);
3131
- accordionItems.push(element);
3132
- }
3133
- returnBlock.items = accordionItems;
3134
- } else {
3135
- const blockItems = [];
3136
- for (const resource of blockResources) {
3137
- const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [], "presentation");
3138
- if (resourceType === null) continue;
3139
- switch (resourceType) {
3140
- case "element": {
3141
- const element = parseWebElement(resource);
3142
- blockItems.push(element);
3143
- break;
3144
- }
3145
- case "block": {
3146
- const block = parseWebBlock(resource);
3147
- if (block) blockItems.push(block);
3148
- break;
3149
- }
3288
+ case "block": {
3289
+ const block = parseWebBlock(resource);
3290
+ if (block) items.push(block);
3291
+ break;
3150
3292
  }
3151
3293
  }
3152
- returnBlock.items = blockItems;
3153
3294
  }
3154
- returnBlock.cssStyles = parseResponsiveCssStyles(blockProperties);
3155
- return returnBlock;
3295
+ returnWebpage.webpages = webpageResource.resource != null ? parseWebpageResources(ensureArray(webpageResource.resource), "page") : [];
3296
+ const webpageSubProperties = getPropertyByLabelAndValue(webpageProperties, "presentation", "page")?.properties ?? [];
3297
+ if (webpageSubProperties.length > 0) {
3298
+ returnWebpage.properties.isDisplayedInNavbar = getPropertyValueByLabel(webpageSubProperties, "displayed-in-navbar") ?? true;
3299
+ returnWebpage.properties.width = getPropertyValueByLabel(webpageSubProperties, "width") ?? "default";
3300
+ returnWebpage.properties.variant = getPropertyValueByLabel(webpageSubProperties, "variant") ?? "default";
3301
+ returnWebpage.properties.isSidebarDisplayed = getPropertyValueByLabel(webpageSubProperties, "sidebar-displayed") ?? true;
3302
+ returnWebpage.properties.isBreadcrumbsDisplayed = getPropertyValueByLabel(webpageSubProperties, "breadcrumbs-displayed") ?? false;
3303
+ returnWebpage.properties.isNavbarSearchBarDisplayed = getPropertyValueByLabel(webpageSubProperties, "navbar-search-bar-displayed") ?? true;
3304
+ }
3305
+ if (imageLink?.uuid != null) returnWebpage.properties.backgroundImage = {
3306
+ uuid: imageLink.uuid,
3307
+ label: imageLink.identification?.label ?? null,
3308
+ description: imageLink.description ?? null,
3309
+ width: imageLink.image?.width ?? 0,
3310
+ height: imageLink.image?.height ?? 0,
3311
+ quality: "high"
3312
+ };
3313
+ returnWebpage.properties.cssStyles = parseResponsiveCssStyles(webpageProperties);
3314
+ return returnWebpage;
3156
3315
  }
3157
3316
  /**
3158
- * Parses raw website properties into a standardized Website properties structure
3317
+ * Parses raw webpage resources into an array of Webpage objects
3159
3318
  *
3160
- * @param properties - Array of raw website properties in OCHRE format
3161
- * @returns Parsed WebsiteProperties object
3319
+ * @param webpageResources - Array of raw webpage resources in OCHRE format
3320
+ * @returns Array of parsed Webpage objects
3162
3321
  */
3163
- function parseWebsiteProperties(properties, websiteTree, sidebar) {
3164
- const websiteProperties = getPropertyByLabel(parseProperties(properties), "presentation")?.properties ?? [];
3165
- let type = getPropertyValueByLabel(websiteProperties, "webUI");
3166
- type ??= "traditional";
3167
- let status = getPropertyValueByLabel(websiteProperties, "status");
3168
- status ??= "development";
3169
- let privacy = getPropertyValueByLabel(websiteProperties, "privacy");
3170
- privacy ??= "public";
3171
- const returnProperties = {
3172
- type,
3173
- status,
3174
- privacy,
3175
- contact: null,
3176
- theme: {
3177
- isThemeToggleDisplayed: true,
3178
- defaultTheme: "system"
3179
- },
3180
- icon: {
3181
- logoUuid: null,
3182
- faviconUuid: null,
3183
- appleTouchIconUuid: null
3184
- },
3185
- navbar: {
3186
- isDisplayed: true,
3187
- variant: "default",
3188
- alignment: "start",
3189
- isProjectDisplayed: true,
3190
- searchBarBoundElementUuid: null,
3191
- items: null
3192
- },
3193
- footer: {
3194
- isDisplayed: true,
3195
- items: null
3196
- },
3197
- sidebar,
3198
- itemPage: {
3199
- isMainContentDisplayed: true,
3200
- isDescriptionDisplayed: true,
3201
- isDocumentDisplayed: true,
3202
- isNotesDisplayed: true,
3203
- isEventsDisplayed: true,
3204
- isPeriodsDisplayed: true,
3205
- isPropertiesDisplayed: true,
3206
- isBibliographyDisplayed: true,
3207
- isPropertyValuesGrouped: true,
3208
- iiifViewer: "universal-viewer"
3209
- },
3210
- options: {
3211
- contexts: null,
3212
- scopes: null,
3213
- labels: { title: null }
3214
- }
3215
- };
3216
- const contactProperty = getPropertyByLabel(websiteProperties, "contact");
3217
- if (contactProperty !== null) {
3218
- const contactContent = contactProperty.values[0]?.content?.toString().split(";");
3219
- if (contactContent?.length === 2) returnProperties.contact = {
3220
- name: contactContent[0],
3221
- email: contactContent[1] ?? null
3222
- };
3223
- else throw new Error(`Contact property must be in the format “name;email”, but got “${contactProperty.values[0]?.content}”`);
3224
- }
3225
- returnProperties.theme.isThemeToggleDisplayed = getPropertyValueByLabel(websiteProperties, "supports-theme-toggle") ?? true;
3226
- returnProperties.theme.defaultTheme = getPropertyValueByLabel(websiteProperties, "default-theme") ?? "system";
3227
- returnProperties.icon.logoUuid = getPropertyByLabel(websiteProperties, "logo")?.values[0]?.uuid ?? null;
3228
- returnProperties.navbar.isDisplayed = getPropertyValueByLabel(websiteProperties, "navbar-displayed") ?? true;
3229
- returnProperties.navbar.variant = getPropertyValueByLabel(websiteProperties, "navbar-variant") ?? "default";
3230
- returnProperties.navbar.alignment = getPropertyValueByLabel(websiteProperties, "navbar-alignment") ?? "start";
3231
- returnProperties.navbar.isProjectDisplayed = getPropertyValueByLabel(websiteProperties, "navbar-project-displayed") ?? true;
3232
- returnProperties.navbar.searchBarBoundElementUuid = getPropertyByLabel(websiteProperties, "bound-element-navbar-search-bar")?.values[0]?.uuid ?? null;
3233
- returnProperties.footer.isDisplayed = getPropertyValueByLabel(websiteProperties, "footer-displayed") ?? true;
3234
- const itemPageTypeProperty = getPropertyByLabelAndValue(websiteProperties, "page-type", "item-page");
3235
- if (itemPageTypeProperty !== null) {
3236
- returnProperties.itemPage.isMainContentDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-main-content-displayed") ?? true;
3237
- returnProperties.itemPage.isDescriptionDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-description-displayed") ?? true;
3238
- returnProperties.itemPage.isDocumentDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-document-displayed") ?? true;
3239
- returnProperties.itemPage.isNotesDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-notes-displayed") ?? true;
3240
- returnProperties.itemPage.isEventsDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-events-displayed") ?? true;
3241
- returnProperties.itemPage.isPeriodsDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-periods-displayed") ?? true;
3242
- returnProperties.itemPage.isPropertiesDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-properties-displayed") ?? true;
3243
- returnProperties.itemPage.isBibliographyDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-bibliography-displayed") ?? true;
3244
- returnProperties.itemPage.isPropertyValuesGrouped = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-property-values-grouped") ?? true;
3245
- returnProperties.itemPage.iiifViewer = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-iiif-viewer") ?? "universal-viewer";
3246
- }
3247
- if ("options" in websiteTree && websiteTree.options) {
3248
- returnProperties.options.scopes = websiteTree.options.scopes != null ? ensureArray(websiteTree.options.scopes.scope).map((scope) => ({
3249
- uuid: scope.uuid.content,
3250
- type: scope.uuid.type,
3251
- identification: parseIdentification(scope.identification)
3252
- })) : null;
3253
- returnProperties.options.contexts = parseAllOptionContexts(websiteTree.options);
3254
- if ("notes" in websiteTree.options && websiteTree.options.notes != null) {
3255
- const labelNotes = parseNotes(ensureArray(websiteTree.options.notes.note));
3256
- returnProperties.options.labels.title = labelNotes.find((note) => note.title === "Title label")?.content ?? null;
3257
- }
3258
- }
3259
- return returnProperties;
3260
- }
3261
- function parseContexts(contexts) {
3262
- const contextsParsed = [];
3263
- for (const mainContext of contexts) for (const contextItemToParse of ensureArray(mainContext.context)) {
3264
- const levelsToParse = ensureArray(contextItemToParse.levels.level);
3265
- let type = "";
3266
- const levels = levelsToParse.map((level) => {
3267
- let variableUuid = "";
3268
- let valueUuid = null;
3269
- if (typeof level === "string") {
3270
- const splitLevel = level.split(", ");
3271
- variableUuid = splitLevel[0];
3272
- valueUuid = splitLevel[1] === "null" ? null : splitLevel[1];
3273
- } else {
3274
- const splitLevel = level.content.split(", ");
3275
- type = level.dataType;
3276
- variableUuid = splitLevel[0];
3277
- valueUuid = splitLevel[1] === "null" ? null : splitLevel[1];
3278
- }
3279
- return {
3280
- variableUuid,
3281
- valueUuid
3282
- };
3283
- });
3284
- contextsParsed.push({
3285
- context: levels,
3286
- type,
3287
- identification: parseIdentification(contextItemToParse.identification)
3288
- });
3322
+ function parseWebpages(webpageResources, slugPrefix) {
3323
+ const returnPages = [];
3324
+ for (const webpageResource of webpageResources) {
3325
+ const webpage = parseWebpage(webpageResource, slugPrefix);
3326
+ if (webpage !== null) returnPages.push(webpage);
3289
3327
  }
3290
- return contextsParsed;
3328
+ return returnPages;
3291
3329
  }
3292
- function parseWebsite(websiteTree, metadata, belongsTo, { version = DEFAULT_API_VERSION } = {}) {
3293
- if (!websiteTree.properties) throw new Error("Website properties not found");
3294
- if (typeof websiteTree.items === "string" || !("resource" in websiteTree.items)) throw new Error("Website pages not found");
3295
- const resources = ensureArray(websiteTree.items.resource);
3296
- const items = [...parseWebpages(resources), ...parseSegments(resources)];
3297
- const sidebar = parseSidebar(resources);
3298
- const properties = parseWebsiteProperties(ensureArray(websiteTree.properties.property), websiteTree, sidebar);
3299
- return {
3300
- uuid: websiteTree.uuid,
3301
- version,
3302
- belongsTo: belongsTo ?? null,
3303
- metadata: parseMetadata(metadata),
3304
- publicationDateTime: parseOptionalDate(websiteTree.publicationDateTime),
3305
- identification: parseIdentification(websiteTree.identification),
3306
- creators: websiteTree.creators ? parsePersons(ensureArray(websiteTree.creators.creator)) : [],
3307
- license: parseLicense(websiteTree.availability),
3308
- items,
3309
- properties
3330
+ /**
3331
+ * Parses raw segment resource into a standardized WebSegment object
3332
+ *
3333
+ * @param segmentResource - Raw segment resource in OCHRE format
3334
+ * @returns Parsed WebSegment object
3335
+ */
3336
+ function parseWebSegment(segmentResource, slugPrefix) {
3337
+ const webpageProperties = segmentResource.properties ? parseProperties(ensureArray(segmentResource.properties.property)) : [];
3338
+ if (webpageProperties.length === 0 || getPropertyValueByLabel(webpageProperties, "presentation") !== "segment") return null;
3339
+ const identification = parseIdentification(segmentResource.identification);
3340
+ const slug = segmentResource.identification.abbreviation != null ? parseFakeStringOrContent(segmentResource.identification.abbreviation) : null;
3341
+ if (slug == null) throw new Error(`Slug not found for segment “${identification.label}”`);
3342
+ const returnSegment = {
3343
+ uuid: segmentResource.uuid,
3344
+ type: "segment",
3345
+ title: identification.label,
3346
+ slug,
3347
+ publicationDateTime: parseOptionalDate(segmentResource.publicationDateTime),
3348
+ items: []
3310
3349
  };
3350
+ returnSegment.items = parseWebSegmentItems(segmentResource.resource ? ensureArray(segmentResource.resource) : [], slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug);
3351
+ return returnSegment;
3311
3352
  }
3312
-
3313
- //#endregion
3314
- //#region src/utils/fetchers/gallery.ts
3315
3353
  /**
3316
- * Fetches and parses a gallery from the OCHRE API
3354
+ * Parses raw segment resources into an array of WebSegment objects
3317
3355
  *
3318
- * @param uuid - The UUID of the gallery
3319
- * @param filter - The filter to apply to the gallery
3320
- * @param page - The page number to fetch
3321
- * @param pageSize - The number of items per page
3322
- * @param options - The options for the fetch
3323
- * @param options.fetch - The fetch function to use
3324
- * @param options.version - The version of the OCHRE API to use
3325
- * @returns The parsed gallery or an error message if the fetch/parse fails
3356
+ * @param segmentResources - Array of raw segment resources in OCHRE format
3357
+ * @returns Array of parsed WebSegment objects
3326
3358
  */
3327
- async function fetchGallery(uuid, filter, page, pageSize, options) {
3328
- try {
3329
- const { uuid: parsedUuid, filter: parsedFilter, page: parsedPage, pageSize: parsedPageSize } = gallerySchema.parse({
3330
- uuid,
3331
- filter,
3332
- page,
3333
- pageSize
3334
- });
3335
- const response = await (options?.fetch ?? fetch)(`https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(`
3336
- for $q in input()/ochre[@uuid='${parsedUuid}']
3337
- let $filtered := $q//items/resource[contains(lower-case(identification/label), lower-case('${parsedFilter}'))]
3338
- let $maxLength := count($filtered)
3339
- return <gallery maxLength='{$maxLength}'>
3340
- {$q/metadata/project}
3341
- {$q/metadata/item}
3342
- {$filtered[position() >= ${((parsedPage - 1) * parsedPageSize + 1).toString()} and position() < ${(parsedPage * parsedPageSize + 1).toString()}]}
3343
- </gallery>
3344
- `)}&format=json`);
3345
- if (!response.ok) throw new Error("Error fetching gallery items, please try again later.");
3346
- const data = await response.json();
3347
- if (!("gallery" in data.result)) throw new Error("Failed to fetch gallery");
3348
- return {
3349
- item: {
3350
- identification: parseIdentification(data.result.gallery.item.identification),
3351
- projectIdentification: parseIdentification(data.result.gallery.project.identification),
3352
- resources: parseResources(data.result.gallery.resource ? Array.isArray(data.result.gallery.resource) ? data.result.gallery.resource : [data.result.gallery.resource] : []),
3353
- maxLength: data.result.gallery.maxLength
3354
- },
3355
- error: null
3356
- };
3357
- } catch (error) {
3358
- console.error(error);
3359
- return {
3360
- item: null,
3361
- error: error instanceof Error ? error.message : "Failed to fetch gallery"
3362
- };
3359
+ function parseSegments(segmentResources, slugPrefix) {
3360
+ const returnSegments = [];
3361
+ for (const segmentResource of segmentResources) {
3362
+ const segment = parseWebSegment(segmentResource, slugPrefix);
3363
+ if (segment !== null) returnSegments.push(segment);
3363
3364
  }
3365
+ return returnSegments;
3364
3366
  }
3365
-
3366
- //#endregion
3367
- //#region src/utils/fetchers/uuid.ts
3368
3367
  /**
3369
- * Fetches raw OCHRE data by UUID from the OCHRE API
3368
+ * Parses raw segment item into a standardized WebSegmentItem object
3370
3369
  *
3371
- * @param uuid - The UUID of the OCHRE item to fetch
3372
- * @returns A tuple containing either [null, OchreData] on success or [error message, null] on failure
3373
- * @internal
3370
+ * @param segmentItemResource - Raw segment item resource in OCHRE format
3371
+ * @returns Parsed WebSegmentItem object
3374
3372
  */
3375
- async function fetchByUuid(uuid, options) {
3376
- try {
3377
- const version = options?.version ?? DEFAULT_API_VERSION;
3378
- const parsedUuid = uuidSchema.parse(uuid);
3379
- const response = await (options?.fetch ?? fetch)(version === 2 ? `https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?uuid=${parsedUuid}&format=json&lang="*"` : `https://ochre.lib.uchicago.edu/ochre?uuid=${parsedUuid}&format=json&lang="*"`);
3380
- if (!response.ok) throw new Error("Failed to fetch OCHRE data");
3381
- const dataRaw = await response.json();
3382
- if (version === 2 && (!("result" in dataRaw) || !("ochre" in dataRaw.result)) || version !== 2 && !("ochre" in dataRaw)) throw new Error("Invalid OCHRE data: API response missing 'ochre' key");
3383
- return [null, "result" in dataRaw && !Array.isArray(dataRaw.result) && "ochre" in dataRaw.result ? dataRaw.result : "ochre" in dataRaw ? dataRaw : null];
3384
- } catch (error) {
3385
- return [error instanceof Error ? error.message : "Unknown error", null];
3386
- }
3373
+ function parseWebSegmentItem(segmentItemResource, slugPrefix) {
3374
+ const webpageProperties = segmentItemResource.properties ? parseProperties(ensureArray(segmentItemResource.properties.property)) : [];
3375
+ if (webpageProperties.length === 0 || getPropertyValueByLabel(webpageProperties, "presentation") !== "segment-item") return null;
3376
+ const identification = parseIdentification(segmentItemResource.identification);
3377
+ const slug = segmentItemResource.identification.abbreviation != null ? parseFakeStringOrContent(segmentItemResource.identification.abbreviation) : null;
3378
+ if (slug == null) throw new Error(`Slug not found for segment item “${identification.label}”`);
3379
+ const returnSegmentItem = {
3380
+ uuid: segmentItemResource.uuid,
3381
+ type: "segment-item",
3382
+ title: identification.label,
3383
+ slug,
3384
+ publicationDateTime: parseOptionalDate(segmentItemResource.publicationDateTime),
3385
+ items: []
3386
+ };
3387
+ const resources = segmentItemResource.resource ? ensureArray(segmentItemResource.resource) : [];
3388
+ returnSegmentItem.items.push(...parseWebpages(resources, slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug), ...parseSegments(resources, slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug));
3389
+ return returnSegmentItem;
3387
3390
  }
3388
-
3389
- //#endregion
3390
- //#region src/utils/fetchers/item.ts
3391
3391
  /**
3392
- * Fetches and parses an OCHRE item from the OCHRE API
3392
+ * Parses raw segment items into an array of WebSegmentItem objects
3393
3393
  *
3394
- * @param uuid - The UUID of the OCHRE item to fetch
3395
- * @returns Object containing the parsed OCHRE item, or an error message if the fetch/parse fails
3394
+ * @param segmentItems - Array of raw segment items in OCHRE format
3395
+ * @returns Array of parsed WebSegmentItem objects
3396
3396
  */
3397
- async function fetchItem(uuid, category, itemCategories, options) {
3398
- try {
3399
- const version = options?.version ?? DEFAULT_API_VERSION;
3400
- const [error, data] = await fetchByUuid(uuid, {
3401
- fetch,
3402
- version
3403
- });
3404
- if (error !== null) throw new Error(error);
3405
- const categoryKey = category ?? getItemCategory(Object.keys(data.ochre));
3406
- const belongsTo = {
3407
- uuid: data.ochre.uuidBelongsTo,
3408
- abbreviation: parseFakeString(data.ochre.belongsTo)
3409
- };
3410
- const metadata = parseMetadata(data.ochre.metadata);
3411
- let item;
3412
- switch (categoryKey) {
3413
- case "resource":
3414
- if (!("resource" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'resource' key");
3415
- item = parseResource(data.ochre.resource, metadata, data.ochre.persistentUrl, belongsTo);
3416
- break;
3417
- case "spatialUnit":
3418
- if (!("spatialUnit" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'spatialUnit' key");
3419
- item = parseSpatialUnit(data.ochre.spatialUnit, metadata, data.ochre.persistentUrl, belongsTo);
3420
- break;
3421
- case "concept":
3422
- if (!("concept" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'concept' key");
3423
- item = parseConcept(data.ochre.concept, metadata, data.ochre.persistentUrl, belongsTo);
3424
- break;
3425
- case "period":
3426
- if (!("period" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'period' key");
3427
- item = parsePeriod(data.ochre.period, metadata, data.ochre.persistentUrl, {
3428
- uuid: data.ochre.uuidBelongsTo,
3429
- abbreviation: parseFakeString(data.ochre.belongsTo)
3430
- });
3431
- break;
3432
- case "bibliography":
3433
- if (!("bibliography" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'bibliography' key");
3434
- item = parseBibliography(data.ochre.bibliography, metadata, data.ochre.persistentUrl, belongsTo);
3435
- break;
3436
- case "person":
3437
- if (!("person" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'person' key");
3438
- item = parsePerson(data.ochre.person, metadata, data.ochre.persistentUrl, belongsTo);
3439
- break;
3440
- case "propertyVariable":
3441
- if (!("propertyVariable" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'propertyVariable' key");
3442
- item = parsePropertyVariable(data.ochre.propertyVariable, metadata, data.ochre.persistentUrl, belongsTo);
3443
- break;
3444
- case "propertyValue":
3445
- if (!("propertyValue" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'propertyValue' key");
3446
- item = parsePropertyValue(data.ochre.propertyValue, metadata, data.ochre.persistentUrl, belongsTo);
3447
- break;
3448
- case "text":
3449
- if (!("text" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'text' key");
3450
- item = parseText(data.ochre.text, metadata, data.ochre.persistentUrl, belongsTo);
3451
- break;
3452
- case "set":
3453
- if (!("set" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'set' key");
3454
- item = parseSet(data.ochre.set, itemCategories, metadata, data.ochre.persistentUrl, belongsTo);
3455
- break;
3456
- case "tree":
3457
- if (!("tree" in data.ochre)) throw new Error("Invalid OCHRE data: API response missing 'tree' key");
3458
- item = parseTree(data.ochre.tree, itemCategories, metadata, data.ochre.persistentUrl, belongsTo);
3459
- break;
3460
- default: throw new Error("Invalid category");
3461
- }
3462
- return {
3463
- error: null,
3464
- item
3465
- };
3466
- } catch (error) {
3467
- return {
3468
- error: error instanceof Error ? error.message : "Unknown error",
3469
- item: void 0
3470
- };
3397
+ function parseWebSegmentItems(segmentItems, slugPrefix) {
3398
+ const returnItems = [];
3399
+ for (const segmentItem of segmentItems) {
3400
+ const segmentItemParsed = parseWebSegmentItem(segmentItem, slugPrefix);
3401
+ if (segmentItemParsed !== null) returnItems.push(segmentItemParsed);
3471
3402
  }
3403
+ return returnItems;
3472
3404
  }
3473
-
3474
- //#endregion
3475
- //#region src/utils/fetchers/items-by-property-values.ts
3476
3405
  /**
3477
- * Build an XQuery string to fetch items by property values from the OCHRE API
3478
- * @param params - The parameters for the fetch
3479
- * @param params.projectScopeUuid - The UUID of the project scope
3480
- * @param params.belongsToCollectionScopeUuids - An array of collection scope UUIDs to filter by
3481
- * @param params.propertyVariableUuids - An array of property variable UUIDs to fetch
3482
- * @param params.propertyValues - An array of property values to fetch
3483
- * @param params.itemCategory - The category of the items to fetch
3484
- * @param params.page - The page number (1-indexed)
3485
- * @param params.pageSize - The number of items per page
3486
- * @param params.includeChildItems - Whether to include child items of the same category
3487
- * @param options - Options for the fetch
3488
- * @param options.version - The version of the OCHRE API to use
3489
- * @returns An XQuery string
3406
+ * Parses raw sidebar data into a standardized Sidebar structure
3407
+ *
3408
+ * @param resources - Array of raw sidebar resources in OCHRE format
3409
+ * @returns Parsed Sidebar object
3490
3410
  */
3491
- function buildXQuery$1(params, options) {
3492
- const version = options?.version ?? DEFAULT_API_VERSION;
3493
- const { propertyVariableUuids, propertyValues, projectScopeUuid, belongsToCollectionScopeUuids, itemCategory, page, pageSize, includeChildItems = false } = params;
3494
- const startPos = (page - 1) * pageSize + 1;
3495
- const endPos = page * pageSize;
3496
- let belongsToCollectionScopeFilter = "";
3497
- if (belongsToCollectionScopeUuids.length > 0) belongsToCollectionScopeFilter = `[.//properties[property[label/@uuid="${BELONGS_TO_COLLECTION_UUID}" and value/(${belongsToCollectionScopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ")})]]`;
3498
- const propertyVariables = propertyVariableUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
3499
- const propertyValuesFilters = propertyValues.map(({ dataType, value }) => {
3500
- if (dataType === "IDREF") return `value[@uuid="${value}"]`;
3501
- if (dataType === "date" || dataType === "dateTime" || dataType === "time" || dataType === "integer" || dataType === "decimal" || dataType === "boolean") return `value[@rawValue="${value}"]`;
3502
- return `value[.="${value}"]`;
3503
- }).join(" or ");
3504
- return `<ochre>{${`let $match-uuids := distinct-values(
3505
- ${version === 2 ? "doc()" : "input()"}/ochre[@uuidBelongsTo="${projectScopeUuid}"]
3506
- ${belongsToCollectionScopeFilter}
3507
- //property[label[${propertyVariables}]][${propertyValuesFilters}]]
3508
- /@uuid
3509
- )
3510
-
3511
- let $items := ${version === 2 ? "doc()" : "input()"}/ochre[@uuidBelongsTo="${projectScopeUuid}"]
3512
- /${itemCategory ?? "*"}[@uuid = $match-uuids
3513
- or bibliographies/bibliography/@uuid = $match-uuids]
3514
-
3515
- let $unique-uuids := distinct-values($items/@uuid)
3516
- let $totalCount := count($unique-uuids)
3517
-
3518
- return <items totalCount="{$totalCount}" page="${page}" pageSize="${pageSize}">{
3519
- for $uuid in $unique-uuids[position() ge ${startPos} and position() le ${endPos}]
3520
- let $item := ($items[@uuid = $uuid])[1]
3521
- let $category := local-name($item)
3522
- return element { node-name($item) } {
3523
- $item/@*, ${includeChildItems ? "$item/node()" : "$item/node()[not(local-name(.) = $category)]"}
3524
- }
3525
- }</items>`}}</ochre>`;
3411
+ function parseSidebar(resources) {
3412
+ let returnSidebar = null;
3413
+ const items = [];
3414
+ const title = {
3415
+ label: "",
3416
+ variant: "default",
3417
+ properties: {
3418
+ isNameDisplayed: false,
3419
+ isDescriptionDisplayed: false,
3420
+ isDateDisplayed: false,
3421
+ isCreatorsDisplayed: false,
3422
+ isCountDisplayed: false
3423
+ }
3424
+ };
3425
+ let layout = "start";
3426
+ let mobileLayout = "default";
3427
+ const cssStyles = {
3428
+ default: [],
3429
+ tablet: [],
3430
+ mobile: []
3431
+ };
3432
+ const sidebarResource = resources.find((resource) => {
3433
+ const resourceProperties = resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [];
3434
+ return getPropertyValueByLabel(resourceProperties, "presentation") === "element" && getPropertyValueByLabel(resourceProperties[0]?.properties ?? [], "component") === "sidebar";
3435
+ });
3436
+ if (sidebarResource != null) {
3437
+ title.label = parseFakeStringOrContent(sidebarResource.identification.label);
3438
+ const sidebarBaseProperties = sidebarResource.properties ? parseProperties(ensureArray(sidebarResource.properties.property)) : [];
3439
+ 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 ?? [];
3440
+ const sidebarLayoutProperty = sidebarProperties.find((property) => property.label === "layout");
3441
+ if (sidebarLayoutProperty) layout = sidebarLayoutProperty.values[0].content;
3442
+ const sidebarMobileLayoutProperty = sidebarProperties.find((property) => property.label === "layout-mobile");
3443
+ if (sidebarMobileLayoutProperty) mobileLayout = sidebarMobileLayoutProperty.values[0].content;
3444
+ const parsedCssStyles = parseResponsiveCssStyles(sidebarBaseProperties);
3445
+ cssStyles.default = parsedCssStyles.default;
3446
+ cssStyles.tablet = parsedCssStyles.tablet;
3447
+ cssStyles.mobile = parsedCssStyles.mobile;
3448
+ const titleProperties = sidebarBaseProperties.find((property) => property.label === "presentation" && property.values[0].content === "title")?.properties;
3449
+ if (titleProperties) {
3450
+ const titleVariant = getPropertyValueByLabel(titleProperties, "variant");
3451
+ if (titleVariant) title.variant = titleVariant;
3452
+ title.properties.isNameDisplayed = getPropertyValueByLabel(titleProperties, "name-displayed") === true;
3453
+ title.properties.isDescriptionDisplayed = getPropertyValueByLabel(titleProperties, "description-displayed") === true;
3454
+ title.properties.isDateDisplayed = getPropertyValueByLabel(titleProperties, "date-displayed") === true;
3455
+ title.properties.isCreatorsDisplayed = getPropertyValueByLabel(titleProperties, "creators-displayed") === true;
3456
+ title.properties.isCountDisplayed = getPropertyValueByLabel(titleProperties, "count-displayed") === true;
3457
+ }
3458
+ const sidebarResources = sidebarResource.resource ? ensureArray(sidebarResource.resource) : [];
3459
+ for (const resource of sidebarResources) {
3460
+ const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [], "presentation");
3461
+ if (resourceType === null) continue;
3462
+ switch (resourceType) {
3463
+ case "element": {
3464
+ const element = parseWebElement(resource);
3465
+ items.push(element);
3466
+ break;
3467
+ }
3468
+ case "block": {
3469
+ const block = parseWebBlock(resource);
3470
+ if (block) items.push(block);
3471
+ break;
3472
+ }
3473
+ }
3474
+ }
3475
+ }
3476
+ if (items.length > 0) returnSidebar = {
3477
+ isDisplayed: true,
3478
+ items,
3479
+ title,
3480
+ layout,
3481
+ mobileLayout,
3482
+ cssStyles
3483
+ };
3484
+ return returnSidebar;
3526
3485
  }
3527
3486
  /**
3528
- * Fetches and parses items by property values from the OCHRE API
3487
+ * Parses raw text element data for accordion layout with items support
3529
3488
  *
3530
- * @param params - The parameters for the fetch
3531
- * @param params.projectScopeUuid - The UUID of the project scope
3532
- * @param params.belongsToCollectionScopeUuids - The collection scope UUIDs to filter by
3533
- * @param params.propertyVariableUuids - The property variable UUIDs to query by
3534
- * @param params.propertyValues - The property values to query by
3535
- * @param params.page - The page number (1-indexed)
3536
- * @param params.pageSize - The number of items per page
3537
- * @param params.itemCategory - The category of the items to fetch
3538
- * @param params.includeChildItems - Whether to include child items of the same category
3539
- * @param categoryParams - The category parameters for the fetch
3540
- * @param categoryParams.category - The category of the items to fetch
3541
- * @param categoryParams.itemCategories - The categories of the items to fetch
3542
- * @param options - Options for the fetch
3543
- * @param options.fetch - The fetch function to use
3544
- * @param options.version - The version of the OCHRE API to use
3545
- * @returns The parsed items by property values or null if the fetch/parse fails
3489
+ * @param elementResource - Raw element resource data in OCHRE format
3490
+ * @returns Parsed text WebElement with items array
3546
3491
  */
3547
- async function fetchItemsByPropertyValues(params, categoryParams, options) {
3548
- try {
3549
- const version = options?.version ?? DEFAULT_API_VERSION;
3550
- const { propertyVariableUuids, propertyValues, projectScopeUuid, belongsToCollectionScopeUuids, page, pageSize = DEFAULT_PAGE_SIZE, itemCategory, includeChildItems } = params;
3551
- const { category, itemCategories } = categoryParams ?? {};
3552
- const xquery = buildXQuery$1({
3553
- projectScopeUuid,
3554
- belongsToCollectionScopeUuids,
3555
- propertyVariableUuids,
3556
- propertyValues,
3557
- itemCategory,
3558
- includeChildItems,
3559
- page,
3560
- pageSize
3561
- }, { version });
3562
- const response = await (options?.fetch ?? fetch)(version === 2 ? `https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?xquery=${encodeURIComponent(xquery)}&format=json&lang="*"` : `https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(xquery)}&format=json&lang="*"`);
3563
- if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`);
3564
- const data = await response.json();
3565
- if (Array.isArray(data.result) || Object.keys(data.result.ochre).length === 0) throw new Error("No items found");
3566
- if (category != null && data.result.ochre.items[category] == null) throw new Error(`No items found for category: ${category}`);
3567
- if ((category === "set" || category === "tree") && itemCategories != null && "items" in data.result.ochre.items[category] && Array.isArray(data.result.ochre.items[category].items) && data.result.ochre.items[category].items.every((item) => item.category !== itemCategories)) throw new Error(`No items found for category: ${category} and item categories: ${itemCategories}`);
3568
- const items = [];
3569
- if ("resource" in data.result.ochre.items && data.result.ochre.items.resource != null) {
3570
- const resources = parseResources(Array.isArray(data.result.ochre.items.resource) ? data.result.ochre.items.resource : [data.result.ochre.items.resource]);
3571
- items.push(...resources);
3572
- }
3573
- if ("spatialUnit" in data.result.ochre.items && data.result.ochre.items.spatialUnit != null) {
3574
- const spatialUnits = parseSpatialUnits(Array.isArray(data.result.ochre.items.spatialUnit) ? data.result.ochre.items.spatialUnit : [data.result.ochre.items.spatialUnit]);
3575
- items.push(...spatialUnits);
3576
- }
3577
- if ("concept" in data.result.ochre.items && data.result.ochre.items.concept != null) {
3578
- const concepts = parseConcepts(Array.isArray(data.result.ochre.items.concept) ? data.result.ochre.items.concept : [data.result.ochre.items.concept]);
3579
- items.push(...concepts);
3580
- }
3581
- if ("period" in data.result.ochre.items && data.result.ochre.items.period != null) {
3582
- const periods = parsePeriods(Array.isArray(data.result.ochre.items.period) ? data.result.ochre.items.period : [data.result.ochre.items.period]);
3583
- items.push(...periods);
3584
- }
3585
- if ("bibliography" in data.result.ochre.items && data.result.ochre.items.bibliography != null) {
3586
- const bibliographies = parseBibliographies(Array.isArray(data.result.ochre.items.bibliography) ? data.result.ochre.items.bibliography : [data.result.ochre.items.bibliography]);
3587
- items.push(...bibliographies);
3588
- }
3589
- if ("person" in data.result.ochre.items && data.result.ochre.items.person != null) {
3590
- const persons = parsePersons(Array.isArray(data.result.ochre.items.person) ? data.result.ochre.items.person : [data.result.ochre.items.person]);
3591
- items.push(...persons);
3592
- }
3593
- if ("propertyVariable" in data.result.ochre.items && data.result.ochre.items.propertyVariable != null) {
3594
- const propertyVariables = parsePropertyVariables(Array.isArray(data.result.ochre.items.propertyVariable) ? data.result.ochre.items.propertyVariable : [data.result.ochre.items.propertyVariable]);
3595
- items.push(...propertyVariables);
3596
- }
3597
- if ("propertyValue" in data.result.ochre.items && data.result.ochre.items.propertyValue != null) {
3598
- const propertyValues = parsePropertyValues(Array.isArray(data.result.ochre.items.propertyValue) ? data.result.ochre.items.propertyValue : [data.result.ochre.items.propertyValue]);
3599
- items.push(...propertyValues);
3600
- }
3601
- if ("text" in data.result.ochre.items && data.result.ochre.items.text != null) {
3602
- const texts = parseTexts(Array.isArray(data.result.ochre.items.text) ? data.result.ochre.items.text : [data.result.ochre.items.text]);
3603
- items.push(...texts);
3604
- }
3605
- if ("set" in data.result.ochre.items && data.result.ochre.items.set != null) {
3606
- const sets = parseSets(Array.isArray(data.result.ochre.items.set) ? data.result.ochre.items.set : [data.result.ochre.items.set]);
3607
- items.push(...sets);
3608
- }
3609
- if ("tree" in data.result.ochre.items && data.result.ochre.items.tree != null) {
3610
- const trees = parseTrees(Array.isArray(data.result.ochre.items.tree) ? data.result.ochre.items.tree : [data.result.ochre.items.tree]);
3611
- items.push(...trees);
3492
+ function parseWebElementForAccordion(elementResource) {
3493
+ const textElement = parseWebElement(elementResource);
3494
+ const childResources = elementResource.resource ? ensureArray(elementResource.resource) : [];
3495
+ const items = [];
3496
+ for (const resource of childResources) {
3497
+ const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [], "presentation");
3498
+ if (resourceType === null) continue;
3499
+ switch (resourceType) {
3500
+ case "element": {
3501
+ const element = parseWebElement(resource);
3502
+ items.push(element);
3503
+ break;
3504
+ }
3505
+ case "block": {
3506
+ const block = parseWebBlock(resource);
3507
+ if (block) items.push(block);
3508
+ break;
3509
+ }
3612
3510
  }
3613
- return {
3614
- totalCount: data.result.ochre.items.totalCount,
3615
- page: data.result.ochre.items.page,
3616
- pageSize: data.result.ochre.items.pageSize,
3617
- items,
3618
- error: null
3619
- };
3620
- } catch (error) {
3621
- console.error(error);
3622
- return {
3623
- totalCount: null,
3624
- page: null,
3625
- pageSize: null,
3626
- items: null,
3627
- error: error instanceof Error ? error.message : "Failed to fetch items by property values"
3628
- };
3629
3511
  }
3512
+ return {
3513
+ ...textElement,
3514
+ items
3515
+ };
3630
3516
  }
3631
-
3632
- //#endregion
3633
- //#region src/utils/fetchers/property-values-by-property-variables.ts
3634
3517
  /**
3635
- * Schema for a single property value query item in the OCHRE API response
3518
+ * Parses raw block data into a standardized WebBlock structure
3519
+ *
3520
+ * @param blockResource - Raw block resource data in OCHRE format
3521
+ * @returns Parsed WebBlock object
3636
3522
  */
3637
- const propertyValueQueryItemSchema = z.object({
3638
- uuid: z.string(),
3639
- itemUuid: z.string().optional(),
3640
- dataType: z.string(),
3641
- rawValue: z.union([
3642
- z.string(),
3643
- z.number(),
3644
- z.boolean()
3645
- ]).optional(),
3646
- content: z.union([
3647
- z.string(),
3648
- z.number(),
3649
- z.boolean(),
3650
- richTextStringSchema,
3651
- z.array(richTextStringSchema)
3652
- ]).optional()
3653
- }).transform((val) => {
3654
- const returnValue = {
3655
- itemUuid: val.itemUuid != null && val.itemUuid !== "" ? val.itemUuid : null,
3656
- dataType: val.dataType,
3657
- content: null,
3658
- label: null
3523
+ function parseWebBlock(blockResource) {
3524
+ const blockProperties = blockResource.properties ? parseProperties(ensureArray(blockResource.properties.property)) : [];
3525
+ const returnBlock = {
3526
+ uuid: blockResource.uuid,
3527
+ type: "block",
3528
+ title: parseWebTitle(blockProperties, parseIdentification(blockResource.identification)),
3529
+ items: [],
3530
+ properties: {
3531
+ default: {
3532
+ layout: "vertical",
3533
+ spacing: null,
3534
+ gap: null
3535
+ },
3536
+ mobile: null,
3537
+ tablet: null
3538
+ },
3539
+ cssStyles: {
3540
+ default: [],
3541
+ tablet: [],
3542
+ mobile: []
3543
+ }
3659
3544
  };
3660
- switch (val.dataType) {
3661
- case "IDREF":
3662
- returnValue.content = val.uuid !== "" ? val.uuid : null;
3663
- returnValue.label = val.content != null && val.content !== "" ? typeof val.content === "object" ? parseStringContent({ content: val.content }) : parseFakeString(val.content) : null;
3664
- break;
3665
- case "integer":
3666
- case "decimal":
3667
- case "time":
3668
- returnValue.content = val.rawValue != null && val.rawValue !== "" ? Number(val.rawValue) : null;
3669
- returnValue.label = val.content != null && val.content !== "" ? val.content.toString() : null;
3670
- break;
3671
- case "boolean":
3672
- returnValue.content = val.rawValue != null && val.rawValue !== "" ? Boolean(val.rawValue) : null;
3673
- returnValue.label = val.content != null && val.content !== "" ? val.content.toString() : null;
3674
- break;
3675
- default:
3676
- returnValue.content = val.rawValue != null && val.rawValue !== "" ? val.rawValue.toString() : null;
3677
- returnValue.label = val.content != null && val.content !== "" ? val.content.toString() : null;
3678
- break;
3545
+ const blockMainProperties = getPropertyByLabelAndValue(blockProperties, "presentation", "block")?.properties ?? [];
3546
+ if (blockMainProperties.length > 0) {
3547
+ returnBlock.properties.default.layout = getPropertyValueByLabel(blockMainProperties, "layout") ?? "vertical";
3548
+ if (returnBlock.properties.default.layout === "accordion") {
3549
+ returnBlock.properties.default.isAccordionEnabled = getPropertyValueByLabel(blockMainProperties, "accordion-enabled") ?? true;
3550
+ returnBlock.properties.default.isAccordionExpandedByDefault = getPropertyValueByLabel(blockMainProperties, "accordion-expanded") ?? true;
3551
+ returnBlock.properties.default.isAccordionSidebarDisplayed = getPropertyValueByLabel(blockMainProperties, "accordion-sidebar-displayed") ?? false;
3552
+ }
3553
+ returnBlock.properties.default.spacing = getPropertyValueByLabel(blockMainProperties, "spacing") ?? null;
3554
+ returnBlock.properties.default.gap = getPropertyValueByLabel(blockMainProperties, "gap") ?? null;
3555
+ const tabletOverwriteProperty = getPropertyByLabel(blockMainProperties, "overwrite-tablet");
3556
+ if (tabletOverwriteProperty !== null) {
3557
+ const tabletOverwriteProperties = tabletOverwriteProperty.properties;
3558
+ const propertiesTablet = {
3559
+ layout: getPropertyValueByLabel(tabletOverwriteProperties, "layout") ?? void 0,
3560
+ spacing: getPropertyValueByLabel(tabletOverwriteProperties, "spacing") ?? void 0,
3561
+ gap: getPropertyValueByLabel(tabletOverwriteProperties, "gap") ?? void 0,
3562
+ isAccordionEnabled: void 0,
3563
+ isAccordionExpandedByDefault: void 0,
3564
+ isAccordionSidebarDisplayed: void 0
3565
+ };
3566
+ if (propertiesTablet.layout === "accordion" || returnBlock.properties.default.layout === "accordion") {
3567
+ propertiesTablet.isAccordionEnabled = getPropertyValueByLabel(tabletOverwriteProperties, "accordion-enabled") ?? void 0;
3568
+ propertiesTablet.isAccordionExpandedByDefault = getPropertyValueByLabel(tabletOverwriteProperties, "accordion-expanded") ?? void 0;
3569
+ propertiesTablet.isAccordionSidebarDisplayed = getPropertyValueByLabel(tabletOverwriteProperties, "accordion-sidebar-displayed") ?? void 0;
3570
+ }
3571
+ if (Object.values(propertiesTablet).every((value) => value != null)) returnBlock.properties.tablet = propertiesTablet;
3572
+ }
3573
+ const mobileOverwriteProperty = getPropertyByLabel(blockMainProperties, "overwrite-tablet");
3574
+ if (mobileOverwriteProperty !== null) {
3575
+ const mobileOverwriteProperties = mobileOverwriteProperty.properties;
3576
+ const propertiesMobile = {
3577
+ layout: getPropertyValueByLabel(mobileOverwriteProperties, "layout") ?? void 0,
3578
+ spacing: getPropertyValueByLabel(mobileOverwriteProperties, "spacing") ?? void 0,
3579
+ gap: getPropertyValueByLabel(mobileOverwriteProperties, "gap") ?? void 0,
3580
+ isAccordionEnabled: void 0,
3581
+ isAccordionExpandedByDefault: void 0,
3582
+ isAccordionSidebarDisplayed: void 0
3583
+ };
3584
+ if (propertiesMobile.layout === "accordion" || returnBlock.properties.default.layout === "accordion") {
3585
+ propertiesMobile.isAccordionEnabled = getPropertyValueByLabel(mobileOverwriteProperties, "accordion-enabled") ?? void 0;
3586
+ propertiesMobile.isAccordionExpandedByDefault = getPropertyValueByLabel(mobileOverwriteProperties, "accordion-expanded") ?? void 0;
3587
+ propertiesMobile.isAccordionSidebarDisplayed = getPropertyValueByLabel(mobileOverwriteProperties, "accordion-sidebar-displayed") ?? void 0;
3588
+ }
3589
+ if (Object.values(propertiesMobile).every((value) => value != null)) returnBlock.properties.mobile = propertiesMobile;
3590
+ }
3591
+ }
3592
+ const blockResources = blockResource.resource ? ensureArray(blockResource.resource) : [];
3593
+ if (returnBlock.properties.default.layout === "accordion") {
3594
+ const accordionItems = [];
3595
+ for (const resource of blockResources) {
3596
+ const resourceProperties = resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [];
3597
+ const resourceType = getPropertyValueByLabel(resourceProperties, "presentation");
3598
+ if (resourceType !== "element") throw new Error(`Accordion only accepts elements, but got “${resourceType}” for the following resource: “${parseStringContent(resource.identification.label)}”`);
3599
+ const componentType = getPropertyValueByLabel(getPropertyByLabel(resourceProperties, "presentation")?.properties ?? [], "component");
3600
+ if (componentType !== "text") throw new Error(`Accordion only accepts text components, but got “${componentType}” for the following resource: “${parseStringContent(resource.identification.label)}”`);
3601
+ const element = parseWebElementForAccordion(resource);
3602
+ accordionItems.push(element);
3603
+ }
3604
+ returnBlock.items = accordionItems;
3605
+ } else {
3606
+ const blockItems = [];
3607
+ for (const resource of blockResources) {
3608
+ const resourceType = getPropertyValueByLabel(resource.properties ? parseProperties(ensureArray(resource.properties.property)) : [], "presentation");
3609
+ if (resourceType === null) continue;
3610
+ switch (resourceType) {
3611
+ case "element": {
3612
+ const element = parseWebElement(resource);
3613
+ blockItems.push(element);
3614
+ break;
3615
+ }
3616
+ case "block": {
3617
+ const block = parseWebBlock(resource);
3618
+ if (block) blockItems.push(block);
3619
+ break;
3620
+ }
3621
+ }
3622
+ }
3623
+ returnBlock.items = blockItems;
3679
3624
  }
3680
- return returnValue;
3681
- });
3682
- /**
3683
- * Schema for the property values by property variables OCHRE API response
3684
- */
3685
- const responseSchema = z.object({ result: z.union([z.object({ ochre: z.object({ propertyValue: z.union([propertyValueQueryItemSchema, z.array(propertyValueQueryItemSchema)]) }) }), z.array(z.unknown()).length(0)]) });
3686
- /**
3687
- * Build an XQuery string to fetch property values by property variables from the OCHRE API
3688
- * @param params - The parameters for the fetch
3689
- * @param params.projectScopeUuid - The UUID of the project scope
3690
- * @param params.belongsToCollectionScopeUuids - An array of collection scope UUIDs to filter by
3691
- * @param params.propertyVariableUuids - An array of property variable UUIDs to fetch
3692
- * @param options - Options for the fetch
3693
- * @param options.version - The version of the OCHRE API to use
3694
- * @returns An XQuery string
3695
- */
3696
- function buildXQuery(params, options) {
3697
- const version = options?.version ?? DEFAULT_API_VERSION;
3698
- const { projectScopeUuid, belongsToCollectionScopeUuids, propertyVariableUuids } = params;
3699
- let collectionScopeFilter = "";
3700
- if (belongsToCollectionScopeUuids.length > 0) collectionScopeFilter = `//properties[property[label/@uuid="${BELONGS_TO_COLLECTION_UUID}" and value/(${belongsToCollectionScopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ")})]]`;
3701
- const propertyVariableFilters = propertyVariableUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
3702
- return `<ochre>{${`let $matching-props := ${version === 2 ? "doc()" : "input()"}/ochre[@uuidBelongsTo="${projectScopeUuid}"]
3703
- ${collectionScopeFilter}
3704
- /*[not(self::set)]
3705
- //property[label/(${propertyVariableFilters})]
3706
-
3707
- for $v in $matching-props/value
3708
- let $item-uuid := $v/ancestor::*[parent::ochre]/@uuid
3709
- return <propertyValue uuid="{$v/@uuid}" rawValue="{$v/@rawValue}" dataType="{$v/@dataType}" itemUuid="{$item-uuid}">{
3710
- if ($v/content) then $v/content else $v/text()
3711
- }</propertyValue>`}}</ochre>`;
3625
+ returnBlock.cssStyles = parseResponsiveCssStyles(blockProperties);
3626
+ return returnBlock;
3712
3627
  }
3713
3628
  /**
3714
- * Fetches and parses property values by property variables from the OCHRE API
3629
+ * Parses raw website properties into a standardized Website properties structure
3715
3630
  *
3716
- * @param params - The parameters for the fetch
3717
- * @param params.projectScopeUuid - The UUID of the project scope
3718
- * @param params.belongsToCollectionScopeUuids - The collection scope UUIDs to filter by
3719
- * @param params.propertyVariableUuids - The property variable UUIDs to query by
3720
- * @param options - Options for the fetch
3721
- * @param options.fetch - The fetch function to use
3722
- * @param options.version - The version of the OCHRE API to use
3723
- * @returns The parsed property values by property variables or null if the fetch/parse fails
3631
+ * @param properties - Array of raw website properties in OCHRE format
3632
+ * @returns Parsed WebsiteProperties object
3724
3633
  */
3725
- async function fetchPropertyValuesByPropertyVariables(params, options) {
3726
- try {
3727
- const version = options?.version ?? DEFAULT_API_VERSION;
3728
- const { belongsToCollectionScopeUuids, propertyVariableUuids, projectScopeUuid } = params;
3729
- const xquery = buildXQuery({
3730
- projectScopeUuid,
3731
- belongsToCollectionScopeUuids,
3732
- propertyVariableUuids
3733
- }, { version });
3734
- const response = await (options?.fetch ?? fetch)(version === 2 ? `https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?xquery=${encodeURIComponent(xquery)}&format=json&lang="*"` : `https://ochre.lib.uchicago.edu/ochre?xquery=${encodeURIComponent(xquery)}&format=json&lang="*"`);
3735
- if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`);
3736
- const data = await response.json();
3737
- const parsedResultRaw = responseSchema.parse(data);
3738
- if (Array.isArray(parsedResultRaw.result)) throw new TypeError("No items found");
3739
- const parsedItems = Array.isArray(parsedResultRaw.result.ochre.propertyValue) ? parsedResultRaw.result.ochre.propertyValue : [parsedResultRaw.result.ochre.propertyValue];
3740
- const groupedItemsMap = /* @__PURE__ */ new Map();
3741
- for (const item of parsedItems) {
3742
- const existing = groupedItemsMap.get(item.content);
3743
- if (existing == null) groupedItemsMap.set(item.content, {
3744
- dataType: item.dataType,
3745
- content: item.content,
3746
- label: item.label,
3747
- itemUuids: new Set([item.itemUuid])
3748
- });
3749
- else existing.itemUuids.add(item.itemUuid);
3634
+ function parseWebsiteProperties(properties, websiteTree, sidebar) {
3635
+ const websiteProperties = getPropertyByLabel(parseProperties(properties), "presentation")?.properties ?? [];
3636
+ let type = getPropertyValueByLabel(websiteProperties, "webUI");
3637
+ type ??= "traditional";
3638
+ let status = getPropertyValueByLabel(websiteProperties, "status");
3639
+ status ??= "development";
3640
+ let privacy = getPropertyValueByLabel(websiteProperties, "privacy");
3641
+ privacy ??= "public";
3642
+ const returnProperties = {
3643
+ type,
3644
+ status,
3645
+ privacy,
3646
+ contact: null,
3647
+ theme: {
3648
+ isThemeToggleDisplayed: true,
3649
+ defaultTheme: "system"
3650
+ },
3651
+ icon: {
3652
+ logoUuid: null,
3653
+ faviconUuid: null,
3654
+ appleTouchIconUuid: null
3655
+ },
3656
+ navbar: {
3657
+ isDisplayed: true,
3658
+ variant: "default",
3659
+ alignment: "start",
3660
+ isProjectDisplayed: true,
3661
+ searchBarBoundElementUuid: null,
3662
+ items: null
3663
+ },
3664
+ footer: {
3665
+ isDisplayed: true,
3666
+ items: null
3667
+ },
3668
+ sidebar,
3669
+ itemPage: {
3670
+ isMainContentDisplayed: true,
3671
+ isDescriptionDisplayed: true,
3672
+ isDocumentDisplayed: true,
3673
+ isNotesDisplayed: true,
3674
+ isEventsDisplayed: true,
3675
+ isPeriodsDisplayed: true,
3676
+ isPropertiesDisplayed: true,
3677
+ isBibliographyDisplayed: true,
3678
+ isPropertyValuesGrouped: true,
3679
+ iiifViewer: "universal-viewer"
3680
+ },
3681
+ options: {
3682
+ contexts: null,
3683
+ scopes: null,
3684
+ labels: { title: null }
3750
3685
  }
3751
- const groupedItems = [];
3752
- for (const group of groupedItemsMap.values()) groupedItems.push({
3753
- count: group.itemUuids.size,
3754
- dataType: group.dataType,
3755
- content: group.content,
3756
- label: group.label
3757
- });
3758
- return {
3759
- items: groupedItems.toSorted((a, b) => {
3760
- if (a.count !== b.count) return b.count - a.count;
3761
- if (a.label !== b.label) return a.label?.localeCompare(b.label ?? "") ?? 0;
3762
- return a.content?.toString().localeCompare(b.content?.toString() ?? "") ?? 0;
3763
- }),
3764
- error: null
3765
- };
3766
- } catch (error) {
3767
- console.error(error);
3768
- return {
3769
- items: null,
3770
- error: error instanceof Error ? error.message : "Failed to fetch property values by property variables"
3686
+ };
3687
+ const contactProperty = getPropertyByLabel(websiteProperties, "contact");
3688
+ if (contactProperty !== null) {
3689
+ const contactContent = contactProperty.values[0]?.content?.toString().split(";");
3690
+ if (contactContent?.length === 2) returnProperties.contact = {
3691
+ name: contactContent[0],
3692
+ email: contactContent[1] ?? null
3771
3693
  };
3694
+ else throw new Error(`Contact property must be in the format “name;email”, but got “${contactProperty.values[0]?.content}”`);
3695
+ }
3696
+ returnProperties.theme.isThemeToggleDisplayed = getPropertyValueByLabel(websiteProperties, "supports-theme-toggle") ?? true;
3697
+ returnProperties.theme.defaultTheme = getPropertyValueByLabel(websiteProperties, "default-theme") ?? "system";
3698
+ returnProperties.icon.logoUuid = getPropertyByLabel(websiteProperties, "logo")?.values[0]?.uuid ?? null;
3699
+ returnProperties.navbar.isDisplayed = getPropertyValueByLabel(websiteProperties, "navbar-displayed") ?? true;
3700
+ returnProperties.navbar.variant = getPropertyValueByLabel(websiteProperties, "navbar-variant") ?? "default";
3701
+ returnProperties.navbar.alignment = getPropertyValueByLabel(websiteProperties, "navbar-alignment") ?? "start";
3702
+ returnProperties.navbar.isProjectDisplayed = getPropertyValueByLabel(websiteProperties, "navbar-project-displayed") ?? true;
3703
+ returnProperties.navbar.searchBarBoundElementUuid = getPropertyByLabel(websiteProperties, "bound-element-navbar-search-bar")?.values[0]?.uuid ?? null;
3704
+ returnProperties.footer.isDisplayed = getPropertyValueByLabel(websiteProperties, "footer-displayed") ?? true;
3705
+ const itemPageTypeProperty = getPropertyByLabelAndValue(websiteProperties, "page-type", "item-page");
3706
+ if (itemPageTypeProperty !== null) {
3707
+ returnProperties.itemPage.isMainContentDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-main-content-displayed") ?? true;
3708
+ returnProperties.itemPage.isDescriptionDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-description-displayed") ?? true;
3709
+ returnProperties.itemPage.isDocumentDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-document-displayed") ?? true;
3710
+ returnProperties.itemPage.isNotesDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-notes-displayed") ?? true;
3711
+ returnProperties.itemPage.isEventsDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-events-displayed") ?? true;
3712
+ returnProperties.itemPage.isPeriodsDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-periods-displayed") ?? true;
3713
+ returnProperties.itemPage.isPropertiesDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-properties-displayed") ?? true;
3714
+ returnProperties.itemPage.isBibliographyDisplayed = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-bibliography-displayed") ?? true;
3715
+ returnProperties.itemPage.isPropertyValuesGrouped = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-property-values-grouped") ?? true;
3716
+ returnProperties.itemPage.iiifViewer = getPropertyValueByLabel(itemPageTypeProperty.properties, "item-page-iiif-viewer") ?? "universal-viewer";
3772
3717
  }
3718
+ if ("options" in websiteTree && websiteTree.options) {
3719
+ returnProperties.options.scopes = websiteTree.options.scopes != null ? ensureArray(websiteTree.options.scopes.scope).map((scope) => ({
3720
+ uuid: scope.uuid.content,
3721
+ type: scope.uuid.type,
3722
+ identification: parseIdentification(scope.identification)
3723
+ })) : null;
3724
+ returnProperties.options.contexts = parseAllOptionContexts(websiteTree.options);
3725
+ if ("notes" in websiteTree.options && websiteTree.options.notes != null) {
3726
+ const labelNotes = parseNotes(ensureArray(websiteTree.options.notes.note));
3727
+ returnProperties.options.labels.title = labelNotes.find((note) => note.title === "Title label")?.content ?? null;
3728
+ }
3729
+ }
3730
+ return returnProperties;
3731
+ }
3732
+ function parseContexts(contexts) {
3733
+ const contextsParsed = [];
3734
+ for (const mainContext of contexts) for (const contextItemToParse of ensureArray(mainContext.context)) {
3735
+ const levelsToParse = ensureArray(contextItemToParse.levels.level);
3736
+ let type = "";
3737
+ const levels = levelsToParse.map((level) => {
3738
+ let variableUuid = "";
3739
+ let valueUuid = null;
3740
+ if (typeof level === "string") {
3741
+ const splitLevel = level.split(", ");
3742
+ variableUuid = splitLevel[0];
3743
+ valueUuid = splitLevel[1] === "null" ? null : splitLevel[1];
3744
+ } else {
3745
+ const splitLevel = level.content.split(", ");
3746
+ type = level.dataType;
3747
+ variableUuid = splitLevel[0];
3748
+ valueUuid = splitLevel[1] === "null" ? null : splitLevel[1];
3749
+ }
3750
+ return {
3751
+ variableUuid,
3752
+ valueUuid
3753
+ };
3754
+ });
3755
+ contextsParsed.push({
3756
+ context: levels,
3757
+ type,
3758
+ identification: parseIdentification(contextItemToParse.identification)
3759
+ });
3760
+ }
3761
+ return contextsParsed;
3762
+ }
3763
+ function parseWebsite(websiteTree, metadata, belongsTo, { version = DEFAULT_API_VERSION } = {}) {
3764
+ if (!websiteTree.properties) throw new Error("Website properties not found");
3765
+ if (typeof websiteTree.items === "string" || !("resource" in websiteTree.items)) throw new Error("Website pages not found");
3766
+ const resources = ensureArray(websiteTree.items.resource);
3767
+ const items = [...parseWebpages(resources), ...parseSegments(resources)];
3768
+ const sidebar = parseSidebar(resources);
3769
+ const properties = parseWebsiteProperties(ensureArray(websiteTree.properties.property), websiteTree, sidebar);
3770
+ return {
3771
+ uuid: websiteTree.uuid,
3772
+ version,
3773
+ belongsTo: belongsTo ?? null,
3774
+ metadata: parseMetadata(metadata),
3775
+ publicationDateTime: parseOptionalDate(websiteTree.publicationDateTime),
3776
+ identification: parseIdentification(websiteTree.identification),
3777
+ creators: websiteTree.creators ? parsePersons(ensureArray(websiteTree.creators.creator)) : [],
3778
+ license: parseLicense(websiteTree.availability),
3779
+ items,
3780
+ properties
3781
+ };
3773
3782
  }
3774
3783
 
3775
3784
  //#endregion