ochre-sdk 1.3.2 → 1.3.4

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.
@@ -2,7 +2,7 @@ import { requestOchre } from "./request.mjs";
2
2
  import { parseRequestedLanguages, resolveContentLanguages } from "../parsers/languages.mjs";
3
3
  import { parseSetItems } from "../parsers/index.mjs";
4
4
  import { XMLSetItemsData } from "../xml/schemas.mjs";
5
- import { stringLiteral } from "../xquery.mjs";
5
+ import { getContentLanguage, stringLiteral } from "../xquery.mjs";
6
6
  import { compileContainerItemsQuery } from "../query.mjs";
7
7
  //#region src/fetchers/container-items.ts
8
8
  function hasArray(items) {
@@ -149,6 +149,7 @@ async function fetchContainerItems(parameters) {
149
149
  scopeUuids,
150
150
  belongsToCollectionScopeUuids,
151
151
  queries,
152
+ contentLanguage: getContentLanguage(options),
152
153
  body: ({ items, omitSupplemental }) => ` let $totalCount := count(${items})
153
154
  ${buildOrderedItemsClause(sort)}
154
155
  let $pagedItems := subsequence($orderedItems, ${startPosition}, ${pageSize})
@@ -4,14 +4,16 @@ import { gallerySchema } from "../schemas.mjs";
4
4
  import { parseRequestedLanguages, resolveContentLanguages } from "../parsers/languages.mjs";
5
5
  import { parseGallery } from "../parsers/index.mjs";
6
6
  import { XMLGalleryData } from "../xml/schemas.mjs";
7
- import { OCHRE_RESPONSE_PADDING, compileOchreQuery, stringLiteral } from "../xquery.mjs";
7
+ import { OCHRE_RESPONSE_PADDING, compileOchreQuery, getContentLanguage, stringLiteral } from "../xquery.mjs";
8
8
  import * as v from "valibot";
9
9
  //#region src/fetchers/gallery.ts
10
10
  function buildXQuery(parameters) {
11
- const { uuid, filter, page, perPage } = parameters;
11
+ const { uuid, filter, page, perPage, contentLanguage } = parameters;
12
12
  const start = (page - 1) * perPage + 1;
13
13
  const filterLiteral = stringLiteral(filter?.trim() ?? "");
14
- return compileOchreQuery({ body: ({ omitSupplemental }) => `<ochre>${OCHRE_RESPONSE_PADDING}{
14
+ return compileOchreQuery({
15
+ contentLanguage,
16
+ body: ({ omitSupplemental }) => `<ochre>${OCHRE_RESPONSE_PADDING}{
15
17
  for $q in doc()/ochre[@uuid=${stringLiteral(uuid)}]
16
18
  let $filter := ${filterLiteral}
17
19
  let $resources := $q//items/resource
@@ -27,7 +29,8 @@ function buildXQuery(parameters) {
27
29
  subsequence($filtered, ${start}, ${perPage})
28
30
  )`)}
29
31
  }</gallery>
30
- }</ochre>` });
32
+ }</ochre>`
33
+ });
31
34
  }
32
35
  async function fetchGallery(parameters, options) {
33
36
  try {
@@ -38,7 +41,8 @@ async function fetchGallery(parameters, options) {
38
41
  uuid,
39
42
  filter,
40
43
  page,
41
- perPage
44
+ perPage,
45
+ contentLanguage: getContentLanguage(options)
42
46
  }),
43
47
  schema: XMLGalleryData,
44
48
  label: "OCHRE gallery",
@@ -5,13 +5,14 @@ import { uuidSchema } from "../schemas.mjs";
5
5
  import { parseRequestedLanguages, resolveContentLanguages } from "../parsers/languages.mjs";
6
6
  import { parseLinkedItems } from "../parsers/index.mjs";
7
7
  import { XMLItemLinksData } from "../xml/schemas.mjs";
8
- import { OCHRE_RESPONSE_PADDING, compileOchreQuery, stringLiteral } from "../xquery.mjs";
8
+ import { OCHRE_RESPONSE_PADDING, compileOchreQuery, getContentLanguage, stringLiteral } from "../xquery.mjs";
9
9
  import * as v from "valibot";
10
10
  //#region src/fetchers/item-children.ts
11
- function buildXQuery(uuid, category) {
11
+ function buildXQuery(uuid, category, contentLanguage) {
12
12
  const categories = category == null ? OCHRE_COLLECTION_CATEGORIES : typeof category === "string" ? [category] : category;
13
13
  const collectionQueries = Array.from(categories, (possibleCategory) => `cts:search(fn:collection("ochre/${possibleCategory}")/ochre, $uuid-query)`);
14
14
  return compileOchreQuery({
15
+ contentLanguage,
15
16
  declarations: [`declare function local:item-children($nodes as node()*) as node()* {
16
17
  for $node in $nodes
17
18
  return
@@ -59,7 +60,7 @@ async function fetchItemChildren(uuid, options) {
59
60
  const parsedUuid = v.parse(uuidSchema, uuid);
60
61
  const requestedLanguages = parseRequestedLanguages(options?.languages);
61
62
  const output = await requestOchre({
62
- xquery: buildXQuery(parsedUuid, options?.category),
63
+ xquery: buildXQuery(parsedUuid, options?.category, getContentLanguage(options)),
63
64
  schema: XMLItemLinksData,
64
65
  label: "OCHRE item children",
65
66
  options
@@ -5,7 +5,7 @@ import { uuidSchema } from "../schemas.mjs";
5
5
  import { parseRequestedLanguages, resolveContentLanguages } from "../parsers/languages.mjs";
6
6
  import { parseLinkedItems } from "../parsers/index.mjs";
7
7
  import { XMLItemLinksData } from "../xml/schemas.mjs";
8
- import { OCHRE_RESPONSE_PADDING, compileOchreQuery, stringLiteral } from "../xquery.mjs";
8
+ import { OCHRE_RESPONSE_PADDING, compileOchreQuery, getContentLanguage, stringLiteral } from "../xquery.mjs";
9
9
  import * as v from "valibot";
10
10
  //#region src/fetchers/item-links.ts
11
11
  /**
@@ -14,7 +14,7 @@ import * as v from "valibot";
14
14
  * @param uuid - The UUID of the OCHRE item whose links should be fetched
15
15
  * @returns An XQuery string
16
16
  */
17
- function buildXQuery(uuid) {
17
+ function buildXQuery(uuid, contentLanguage) {
18
18
  const linkedItems = `for $link at $position in $link-nodes
19
19
  let $uuid := $link/@uuid/string()
20
20
  let $category := name($link)
@@ -24,7 +24,9 @@ function buildXQuery(uuid) {
24
24
  return `if (${ITEM_CATEGORY_ALIASES[category].map((alias) => `$category = ${stringLiteral(alias)}`).join(" or ")}) then fn:collection("ochre/${category}")/ochre/${category}[@uuid = $uuid]`;
25
25
  }).join("\n else ")}
26
26
  else ()`;
27
- return compileOchreQuery({ body: ({ omitSupplemental }) => `<ochre>${OCHRE_RESPONSE_PADDING}{let $item-uuid := ${stringLiteral(uuid)}
27
+ return compileOchreQuery({
28
+ contentLanguage,
29
+ body: ({ omitSupplemental }) => `<ochre>${OCHRE_RESPONSE_PADDING}{let $item-uuid := ${stringLiteral(uuid)}
28
30
 
29
31
  let $source-items := (
30
32
  ${OCHRE_COLLECTION_CATEGORIES.map((category) => ` fn:collection("ochre/${category}")/ochre[@uuid = $item-uuid]/${category}`).join(",\n")}
@@ -39,14 +41,15 @@ let $link-nodes := (
39
41
  return
40
42
  <items>{
41
43
  ${omitSupplemental(linkedItems)}
42
- }</items>}</ochre>` });
44
+ }</items>}</ochre>`
45
+ });
43
46
  }
44
47
  async function fetchItemLinks(uuid, options) {
45
48
  try {
46
49
  const parsedUuid = v.parse(uuidSchema, uuid);
47
50
  const requestedLanguages = parseRequestedLanguages(options?.languages);
48
51
  const output = await requestOchre({
49
- xquery: buildXQuery(parsedUuid),
52
+ xquery: buildXQuery(parsedUuid, getContentLanguage(options)),
50
53
  schema: XMLItemLinksData,
51
54
  label: "OCHRE item links",
52
55
  options
@@ -5,7 +5,7 @@ import { uuidSchema } from "../schemas.mjs";
5
5
  import { parseRequestedLanguages } from "../parsers/languages.mjs";
6
6
  import { parseItem } from "../parsers/index.mjs";
7
7
  import { XMLData } from "../xml/schemas.mjs";
8
- import { OCHRE_RESPONSE_PADDING, compileOchreQuery, stringLiteral } from "../xquery.mjs";
8
+ import { OCHRE_RESPONSE_PADDING, compileOchreQuery, getContentLanguage, stringLiteral } from "../xquery.mjs";
9
9
  import { parseWebpageView } from "../parsers/website/index.mjs";
10
10
  import * as v from "valibot";
11
11
  //#region src/fetchers/item.ts
@@ -28,10 +28,11 @@ function assertItemCategoryAllowed(category, containedItemCategory) {
28
28
  * @param parameters - The parameters for the fetch
29
29
  * @param parameters.uuid - The UUID of the OCHRE item to fetch
30
30
  * @param parameters.shouldOmitEmbeddedItems - Whether to drop the embedded item hierarchy
31
+ * @param parameters.contentLanguage - Keep only this language of every multilingual field
31
32
  * @returns An XQuery string
32
33
  */
33
34
  function buildXQuery(parameters) {
34
- const { uuid, shouldOmitEmbeddedItems } = parameters;
35
+ const { uuid, shouldOmitEmbeddedItems, contentLanguage } = parameters;
35
36
  const letClauses = [`let $ochre := doc(${stringLiteral(uuid)})/ochre`];
36
37
  let itemNodesExpression = "$ochre/node()";
37
38
  if (shouldOmitEmbeddedItems) {
@@ -46,13 +47,16 @@ ${ITEM_CATEGORIES_WITH_EMBEDDED_ITEMS.map((category) => ` $ochre/${category}`).
46
47
  else $node
47
48
  )`;
48
49
  }
49
- return compileOchreQuery({ body: ({ omitSupplemental }) => `${letClauses.join("\n")}
50
+ return compileOchreQuery({
51
+ contentLanguage,
52
+ body: ({ omitSupplemental }) => `${letClauses.join("\n")}
50
53
  return
51
54
  if (empty($ochre)) then <ochre>${OCHRE_RESPONSE_PADDING}</ochre>
52
55
  else element ochre {
53
56
  $ochre/@*,
54
57
  ${omitSupplemental(itemNodesExpression)}
55
- }` });
58
+ }`
59
+ });
56
60
  }
57
61
  function omitEmbeddedItems(item) {
58
62
  const { items: _items, ...itemWithoutEmbeddedItems } = item;
@@ -67,7 +71,8 @@ async function fetchItem(uuid, options) {
67
71
  const output = await requestOchre({
68
72
  xquery: buildXQuery({
69
73
  uuid: parsedUuid,
70
- shouldOmitEmbeddedItems
74
+ shouldOmitEmbeddedItems,
75
+ contentLanguage: getContentLanguage(options)
71
76
  }),
72
77
  schema: XMLData,
73
78
  label: "OCHRE item",
@@ -6,7 +6,7 @@ import { parseRequiredContentLike } from "../../parsers/helpers.mjs";
6
6
  import { parseRequestedLanguages, resolveContentLanguages } from "../../parsers/languages.mjs";
7
7
  import { parseCoordinates, parseIdentification } from "../../parsers/index.mjs";
8
8
  import { XMLSetItemLocationsData } from "../../xml/schemas.mjs";
9
- import { stringLiteral } from "../../xquery.mjs";
9
+ import { getContentLanguage, stringLiteral } from "../../xquery.mjs";
10
10
  import { compileContainerItemsQuery } from "../../query.mjs";
11
11
  import * as v from "valibot";
12
12
  //#region src/fetchers/set/item-locations.ts
@@ -19,6 +19,7 @@ async function fetchSetItemLocations(parameters, options) {
19
19
  scopeUuids: setScopeUuids,
20
20
  belongsToCollectionScopeUuids,
21
21
  queries,
22
+ contentLanguage: getContentLanguage(options),
22
23
  body: ({ items }) => ` let $propertyVariableUuids := (${Array.from(propertyVariableUuids, (uuid) => stringLiteral(uuid)).join(", ")})
23
24
 
24
25
  return <items totalCount="{count(${items})}">{
@@ -48,8 +48,8 @@ function normalizePropertyValueDataType(dataType) {
48
48
  function sortPropertyValues(values) {
49
49
  return values.toSorted((a, b) => {
50
50
  if (a.count !== b.count) return b.count - a.count;
51
- const label = a.label?.getText() ?? null;
52
- const otherLabel = b.label?.getText() ?? null;
51
+ const label = a.label?.getText(a.label.getDefaultLanguage()) ?? null;
52
+ const otherLabel = b.label?.getText(b.label.getDefaultLanguage()) ?? null;
53
53
  if (label !== otherLabel) return label?.localeCompare(otherLabel ?? "") ?? 0;
54
54
  return a.content?.toString().localeCompare(b.content?.toString() ?? "") ?? 0;
55
55
  });
@@ -26,7 +26,7 @@ function parseWebsiteMetadata(data, options) {
26
26
  const websiteTree = rawOchre.tree[0];
27
27
  if (websiteTree == null) throw new Error("Website tree not found", { cause: data });
28
28
  const identification = parseIdentification(websiteTree.identification, options);
29
- const websiteName = identification.label.getText().trim();
29
+ const websiteName = identification.label.getText(identification.label.getDefaultLanguage()).trim();
30
30
  const metadataDescription = (parseStringLike(rawOchre.metadata.description) ?? "").trim();
31
31
  const properties = parseSimplifiedProperties(websiteTree.properties, options);
32
32
  const reader = websitePresentationReader(properties);
package/dist/getters.mjs CHANGED
@@ -27,7 +27,7 @@ function withDefaultOptions(options) {
27
27
  };
28
28
  }
29
29
  function getPropertyVariableLabel(property) {
30
- return typeof property.variable.label === "string" ? property.variable.label : property.variable.label.getText();
30
+ return typeof property.variable.label === "string" ? property.variable.label : property.variable.label.getText(property.variable.label.getDefaultLanguage());
31
31
  }
32
32
  function hasPropertyValue(property, value) {
33
33
  for (const candidateValue of property.values) if (deepEqual(candidateValue, value)) return true;
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MultilingualOptions, MultilingualString, MultilingualStringEntries, MultilingualStringEntry, MultilingualStringInput, MultilingualStringJSON, MultilingualStringObject, MultilingualStringText } from "./parsers/multilingual.mjs";
2
- import { AccordionWebBlock, ContextTree, ContextTreeFilterLevel, ContextTreeFilterVariant, ContextTreeLevel, ContextTreeLevelItem, ProtectedWebsite, ResponsiveStyles, Scope, Style, StylesheetCategory, StylesheetItem, WebAccordionItem, WebBlock, WebBlockByLayout, WebBlockItem, WebBlockLayout, WebElement, WebElementComponent, WebElementComponentName, WebElementComponentOf, WebElementOf, WebIiifViewer, WebImage, WebLoadingVariant, WebOptions, WebSectionDisplay, WebSectionVariant, WebSidebar, WebTitle, Webpage, Website, WebsiteMetadata, WebsitePrivacy, WebsitePropertyQuery, WebsitePropertyQueryNode, WebsiteSegment, WebsiteType } from "./types/website.mjs";
2
+ import { AccordionWebBlock, ContextTree, ContextTreeFilterLevel, ContextTreeFilterVariant, ContextTreeLevel, ContextTreeLevelItem, ProtectedWebsite, ResponsiveStyles, Scope, Style, StylesheetCategory, StylesheetItem, WebAccordionItem, WebBlock, WebBlockByLayout, WebBlockItem, WebBlockLayout, WebElement, WebElementComponent, WebElementComponentName, WebElementComponentOf, WebElementOf, WebIiifViewer, WebImage, WebLoadingVariant, WebOptions, WebRelationVariant, WebSectionDisplay, WebSectionVariant, WebSidebar, WebTitle, Webpage, Website, WebsiteMetadata, WebsitePrivacy, WebsitePropertyQuery, WebsitePropertyQueryNode, WebsiteSegment, WebsiteType } from "./types/website.mjs";
3
3
  import { AnyBibliography, AnyConcept, AnyItem, AnyPeriod, AnyPerson, AnyPropertyValue, AnyPropertyVariable, AnyResource, AnySet, AnySpatialUnit, AnyText, AnyTree, BaseItem, BaseItemLink, BelongsTo, Bibliography, BibliographyEntryInfo, BibliographyItemLink, BibliographySourceDocument, Concept, ConceptItemLink, ContainedItemCategory, ContainedItemCategoryFromOption, ContainedItemCategoryOption, Context, ContextItem, ContextItemCategory, ContextNode, Coordinates, CoordinatesSource, DictionaryUnitItemLink, EmbeddedBibliography, EmbeddedConcept, EmbeddedItem, EmbeddedPeriod, EmbeddedPerson, EmbeddedPropertyValue, EmbeddedPropertyVariable, EmbeddedResource, EmbeddedSet, EmbeddedSpatialUnit, EmbeddedText, EmbeddedTree, Event, Gallery, Heading, HeadingItemCategory, Identification, Image, ImageMap, ImageMapArea, Interpretation, Item, ItemCategory, ItemCategoryFromOption, ItemCategoryOption, ItemCategoryWithEmbeddedItems, ItemContainerCategory, ItemLink, ItemLinkCategory, ItemLinks, ItemPayloadKind, ItemProperty, ItemWithoutEmbeddedItems, LanguageCodes, License, Metadata, Note, Observation, OcrString, Period, PeriodItemLink, Person, PersonItemLink, Property, PropertyLike, PropertyRelation, PropertyValue, PropertyValueContent, PropertyValueDataType, PropertyValueItemLink, PropertyValueQueryItem, PropertyVariable, PropertyVariableItemLink, Query, QueryGroup, QueryLeaf, QueryablePropertyValueDataType, RecursiveItemCategory, Resource, ResourceItemLink, Section, Set, SetAttributeValueQueryItem, SetBibliography, SetConcept, SetItem, SetItemCategory, SetItemLink, SetItemLocation, SetItemProperty, SetItemSimplifiedProperty, SetItemsSort, SetItemsSortDirection, SetPeriod, SetResource, SetSpatialUnit, SetStyle, SetStyleProperty, SetTree, SimplifiedProperty, SpatialUnit, SpatialUnitItemLink, Text, TextItemLink, TopLevelItem, Tree, TreeItemCategory, TreeItemLink } from "./types/index.mjs";
4
4
  import { DEFAULT_PAGE_SIZE } from "./constants.mjs";
5
5
  import { fetchGallery } from "./fetchers/gallery.mjs";
@@ -16,4 +16,4 @@ import { fetchWebsite } from "./fetchers/website.mjs";
16
16
  import { PropertyOptions, PropertySelector, getLeafPropertyValues, getProperty, getPropertyValue, getPropertyValues, getUniqueProperties, getUniquePropertyVariableLabels, isPropertyMatchingFilter, normalizePropertyVariableLabel } from "./getters.mjs";
17
17
  import { flattenItemProperties } from "./helpers.mjs";
18
18
  import { defineLanguages } from "./parsers/languages.mjs";
19
- export { type AccordionWebBlock, type AnyBibliography, type AnyConcept, type AnyItem, type AnyPeriod, type AnyPerson, type AnyPropertyValue, type AnyPropertyVariable, type AnyResource, type AnySet, type AnySpatialUnit, type AnyText, type AnyTree, type BaseItem, type BaseItemLink, type BelongsTo, type Bibliography, type BibliographyEntryInfo, type BibliographyItemLink, type BibliographySourceDocument, type Concept, type ConceptItemLink, type ContainedItemCategory, type ContainedItemCategoryFromOption, type ContainedItemCategoryOption, type Context, type ContextItem, type ContextItemCategory, type ContextNode, type ContextTree, type ContextTreeFilterLevel, type ContextTreeFilterVariant, type ContextTreeLevel, type ContextTreeLevelItem, type Coordinates, type CoordinatesSource, DEFAULT_PAGE_SIZE, type DictionaryUnitItemLink, type EmbeddedBibliography, type EmbeddedConcept, type EmbeddedItem, type EmbeddedPeriod, type EmbeddedPerson, type EmbeddedPropertyValue, type EmbeddedPropertyVariable, type EmbeddedResource, type EmbeddedSet, type EmbeddedSpatialUnit, type EmbeddedText, type EmbeddedTree, type Event, type Gallery, type Heading, type HeadingItemCategory, type Identification, type Image, type ImageMap, type ImageMapArea, type Interpretation, type Item, type ItemCategory, type ItemCategoryFromOption, type ItemCategoryOption, type ItemCategoryWithEmbeddedItems, type ItemContainerCategory, type ItemLink, type ItemLinkCategory, type ItemLinks, type ItemPayloadKind, type ItemProperty, type ItemWithoutEmbeddedItems, type LanguageCodes, type License, type Metadata, type MultilingualOptions, MultilingualString, type MultilingualStringEntries, type MultilingualStringEntry, type MultilingualStringInput, type MultilingualStringJSON, type MultilingualStringObject, type MultilingualStringText, type Note, type Observation, type OcrString, type Period, type PeriodItemLink, type Person, type PersonItemLink, type Property, type PropertyLike, type PropertyOptions, type PropertyRelation, type PropertySelector, type PropertyValue, type PropertyValueContent, type PropertyValueDataType, type PropertyValueItemLink, type PropertyValueQueryItem, type PropertyVariable, type PropertyVariableItemLink, type ProtectedWebsite, type Query, type QueryGroup, type QueryLeaf, type QueryablePropertyValueDataType, type RecursiveItemCategory, type Resource, type ResourceItemLink, type ResponsiveStyles, type Scope, type Section, type Set, type SetAttributeValueQueryItem, type SetBibliography, type SetConcept, type SetItem, type SetItemCategory, type SetItemLink, type SetItemLocation, type SetItemProperty, type SetItemSimplifiedProperty, type SetItemsSort, type SetItemsSortDirection, type SetPeriod, type SetResource, type SetSpatialUnit, type SetStyle, type SetStyleProperty, type SetTree, type SimplifiedProperty, type SpatialUnit, type SpatialUnitItemLink, type Style, type StylesheetCategory, type StylesheetItem, type Text, type TextItemLink, type TopLevelItem, type Tree, type TreeItemCategory, type TreeItemLink, type WebAccordionItem, type WebBlock, type WebBlockByLayout, type WebBlockItem, type WebBlockLayout, type WebElement, type WebElementComponent, type WebElementComponentName, type WebElementComponentOf, type WebElementOf, type WebIiifViewer, type WebImage, type WebLoadingVariant, type WebOptions, type WebSectionDisplay, type WebSectionVariant, type WebSidebar, type WebTitle, type Webpage, type Website, type WebsiteMetadata, type WebsitePrivacy, type WebsitePropertyQuery, type WebsitePropertyQueryNode, type WebsiteSegment, type WebsiteType, defineLanguages, fetchGallery, fetchItem, fetchItemChildren, fetchItemLinks, fetchItemOcrData, fetchSetItemLocations, fetchSetItems, fetchSetPropertyValues, fetchTreeItems, fetchWebsite, fetchWebsiteMetadata, flattenItemProperties, getLeafPropertyValues, getProperty, getPropertyValue, getPropertyValues, getUniqueProperties, getUniquePropertyVariableLabels, isPropertyMatchingFilter, normalizePropertyVariableLabel };
19
+ export { type AccordionWebBlock, type AnyBibliography, type AnyConcept, type AnyItem, type AnyPeriod, type AnyPerson, type AnyPropertyValue, type AnyPropertyVariable, type AnyResource, type AnySet, type AnySpatialUnit, type AnyText, type AnyTree, type BaseItem, type BaseItemLink, type BelongsTo, type Bibliography, type BibliographyEntryInfo, type BibliographyItemLink, type BibliographySourceDocument, type Concept, type ConceptItemLink, type ContainedItemCategory, type ContainedItemCategoryFromOption, type ContainedItemCategoryOption, type Context, type ContextItem, type ContextItemCategory, type ContextNode, type ContextTree, type ContextTreeFilterLevel, type ContextTreeFilterVariant, type ContextTreeLevel, type ContextTreeLevelItem, type Coordinates, type CoordinatesSource, DEFAULT_PAGE_SIZE, type DictionaryUnitItemLink, type EmbeddedBibliography, type EmbeddedConcept, type EmbeddedItem, type EmbeddedPeriod, type EmbeddedPerson, type EmbeddedPropertyValue, type EmbeddedPropertyVariable, type EmbeddedResource, type EmbeddedSet, type EmbeddedSpatialUnit, type EmbeddedText, type EmbeddedTree, type Event, type Gallery, type Heading, type HeadingItemCategory, type Identification, type Image, type ImageMap, type ImageMapArea, type Interpretation, type Item, type ItemCategory, type ItemCategoryFromOption, type ItemCategoryOption, type ItemCategoryWithEmbeddedItems, type ItemContainerCategory, type ItemLink, type ItemLinkCategory, type ItemLinks, type ItemPayloadKind, type ItemProperty, type ItemWithoutEmbeddedItems, type LanguageCodes, type License, type Metadata, type MultilingualOptions, MultilingualString, type MultilingualStringEntries, type MultilingualStringEntry, type MultilingualStringInput, type MultilingualStringJSON, type MultilingualStringObject, type MultilingualStringText, type Note, type Observation, type OcrString, type Period, type PeriodItemLink, type Person, type PersonItemLink, type Property, type PropertyLike, type PropertyOptions, type PropertyRelation, type PropertySelector, type PropertyValue, type PropertyValueContent, type PropertyValueDataType, type PropertyValueItemLink, type PropertyValueQueryItem, type PropertyVariable, type PropertyVariableItemLink, type ProtectedWebsite, type Query, type QueryGroup, type QueryLeaf, type QueryablePropertyValueDataType, type RecursiveItemCategory, type Resource, type ResourceItemLink, type ResponsiveStyles, type Scope, type Section, type Set, type SetAttributeValueQueryItem, type SetBibliography, type SetConcept, type SetItem, type SetItemCategory, type SetItemLink, type SetItemLocation, type SetItemProperty, type SetItemSimplifiedProperty, type SetItemsSort, type SetItemsSortDirection, type SetPeriod, type SetResource, type SetSpatialUnit, type SetStyle, type SetStyleProperty, type SetTree, type SimplifiedProperty, type SpatialUnit, type SpatialUnitItemLink, type Style, type StylesheetCategory, type StylesheetItem, type Text, type TextItemLink, type TopLevelItem, type Tree, type TreeItemCategory, type TreeItemLink, type WebAccordionItem, type WebBlock, type WebBlockByLayout, type WebBlockItem, type WebBlockLayout, type WebElement, type WebElementComponent, type WebElementComponentName, type WebElementComponentOf, type WebElementOf, type WebIiifViewer, type WebImage, type WebLoadingVariant, type WebOptions, type WebRelationVariant, type WebSectionDisplay, type WebSectionVariant, type WebSidebar, type WebTitle, type Webpage, type Website, type WebsiteMetadata, type WebsitePrivacy, type WebsitePropertyQuery, type WebsitePropertyQueryNode, type WebsiteSegment, type WebsiteType, defineLanguages, fetchGallery, fetchItem, fetchItemChildren, fetchItemLinks, fetchItemOcrData, fetchSetItemLocations, fetchSetItems, fetchSetPropertyValues, fetchTreeItems, fetchWebsite, fetchWebsiteMetadata, flattenItemProperties, getLeafPropertyValues, getProperty, getPropertyValue, getPropertyValues, getUniqueProperties, getUniquePropertyVariableLabels, isPropertyMatchingFilter, normalizePropertyVariableLabel };
@@ -5,6 +5,17 @@ import { XMLContent, XMLString } from "../xml/types.mjs";
5
5
  export type FetchFunction = (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>;
6
6
  export type FetchBaseOptions<TLanguages extends ReadonlyArray<string> | undefined = undefined> = {
7
7
  languages?: TLanguages;
8
+ /**
9
+ * Fetch only this language of every multilingual field, instead of every
10
+ * language OCHRE holds. A field without it keeps its `fallbackLanguage`
11
+ * content, else its content in any language; aliases are always kept.
12
+ */
13
+ language?: string;
14
+ /**
15
+ * The language shown for fields that have no `language` content, typically
16
+ * the website's default language (defaults to `"eng"`)
17
+ */
18
+ fallbackLanguage?: string;
8
19
  fetch?: FetchFunction;
9
20
  signal?: AbortSignal;
10
21
  timeoutMilliseconds?: number;
@@ -58,15 +58,19 @@ function parseRequiredContentLike(value, options) {
58
58
  return parseContentLike(value, options) ?? MultilingualString.empty(options.languages, { defaultLanguage: options.defaultLanguage });
59
59
  }
60
60
  function parseContentLikeText(value, options) {
61
- return parseContentLike(value, options)?.getText().trim() ?? "";
61
+ const content = parseContentLike(value, options);
62
+ return content?.getText(content.getDefaultLanguage()).trim() ?? "";
62
63
  }
63
64
  function parseStringContent(value, options = FALLBACK_PARSER_OPTIONS) {
64
65
  if (value == null) return "";
65
66
  if (typeof value === "string") return value;
66
- if (isXMLContent(value)) return parseXMLContent(value, {
67
- languages: options.languages,
68
- defaultLanguage: options.defaultLanguage
69
- }).getText();
67
+ if (isXMLContent(value)) {
68
+ const content = parseXMLContent(value, {
69
+ languages: options.languages,
70
+ defaultLanguage: options.defaultLanguage
71
+ });
72
+ return content.getText(content.getDefaultLanguage());
73
+ }
70
74
  return parseXMLString(value).text;
71
75
  }
72
76
  //#endregion
@@ -457,7 +457,7 @@ function parsePropertyDataType(dataType) {
457
457
  function parsePropertyValueContent(value, options) {
458
458
  const dataType = parsePropertyDataType(value.dataType);
459
459
  const rawLabel = value.content != null ? parseRequiredContentLike(value, options) : value.payload != null && value.payload !== "" ? multilingualFromText(value.payload, options) : null;
460
- const contentText = readPropertyValueText(value, () => rawLabel?.getText() ?? null);
460
+ const contentText = readPropertyValueText(value, () => rawLabel?.getText(rawLabel.getDefaultLanguage()) ?? null);
461
461
  const common = {
462
462
  hierarchy: {
463
463
  isLeaf: value.inherited == null || !value.inherited,
@@ -109,11 +109,11 @@ export declare class MultilingualString<T extends ReadonlyArray<string> = Readon
109
109
  /**
110
110
  * Get text in a specific language, falling back when it has none
111
111
  */
112
- getText(language?: T[number]): string;
112
+ getText(language: T[number]): string;
113
113
  /**
114
114
  * Get rich text in a specific language, falling back when it has none
115
115
  */
116
- getRichText(language?: T[number]): string;
116
+ getRichText(language: T[number]): string;
117
117
  /**
118
118
  * Get text in a specific language, with no fallback
119
119
  */
@@ -133,16 +133,16 @@ export declare class MultilingualString<T extends ReadonlyArray<string> = Readon
133
133
  * can carry several entries for one language, and this returns all of them
134
134
  * rather than only the primary. Each entry carries both `text` and
135
135
  * `richText`, so a caller rendering a rich field reads them from here.
136
- * @param language - The language to read, or undefined for the default
136
+ * @param language - The language to read
137
137
  * @returns The entries, or an empty array when no language has any
138
138
  */
139
- getEntries(language?: T[number]): Array<MultilingualStringEntry>;
139
+ getEntries(language: T[number]): Array<MultilingualStringEntry>;
140
140
  /**
141
141
  * Get the text of every entry for a language, falling back when it has none
142
- * @param language - The language to read, or undefined for the default
142
+ * @param language - The language to read
143
143
  * @returns The texts, or an empty array when no language has any
144
144
  */
145
- getTexts(language?: T[number]): Array<string>;
145
+ getTexts(language: T[number]): Array<string>;
146
146
  /**
147
147
  * Get the text of every entry for a language, with no fallback
148
148
  * @param language - The language to read
@@ -259,10 +259,6 @@ export declare class MultilingualString<T extends ReadonlyArray<string> = Readon
259
259
  * @returns A new multilingual string
260
260
  */
261
261
  filterEntries(shouldKeep: (entry: MultilingualStringEntry, language: T[number]) => boolean): MultilingualString<T>;
262
- /**
263
- * Get the string representation, using the default language
264
- */
265
- toString(): string;
266
262
  /**
267
263
  * Get the JSON representation
268
264
  *
@@ -228,7 +228,7 @@ var MultilingualString = class MultilingualString {
228
228
  * can carry several entries for one language, and this returns all of them
229
229
  * rather than only the primary. Each entry carries both `text` and
230
230
  * `richText`, so a caller rendering a rich field reads them from here.
231
- * @param language - The language to read, or undefined for the default
231
+ * @param language - The language to read
232
232
  * @returns The entries, or an empty array when no language has any
233
233
  */
234
234
  getEntries(language) {
@@ -236,7 +236,7 @@ var MultilingualString = class MultilingualString {
236
236
  }
237
237
  /**
238
238
  * Get the text of every entry for a language, falling back when it has none
239
- * @param language - The language to read, or undefined for the default
239
+ * @param language - The language to read
240
240
  * @returns The texts, or an empty array when no language has any
241
241
  */
242
242
  getTexts(language) {
@@ -423,12 +423,6 @@ var MultilingualString = class MultilingualString {
423
423
  }, newAvailableLanguages);
424
424
  }
425
425
  /**
426
- * Get the string representation, using the default language
427
- */
428
- toString() {
429
- return this.getText();
430
- }
431
- /**
432
426
  * Get the JSON representation
433
427
  *
434
428
  * Carries `defaultLanguage`, because it is not derivable from the content:
@@ -85,6 +85,23 @@ function parseCollectionDisplayedProperties(reader) {
85
85
  label: value.label
86
86
  }]);
87
87
  }
88
+ /**
89
+ * Parses the "customize-special" "relations" properties, each pairing the
90
+ * property variables it links with "use-property" with a display "variant".
91
+ */
92
+ function parseCollectionRelations(reader) {
93
+ const relations = [];
94
+ const relationReaders = reader.nestedAllByValue("customize-special", "relations");
95
+ for (const relationReader of relationReaders) {
96
+ const variant = relationReader.value("variant");
97
+ const properties = parseCollectionDisplayedProperties(relationReader) ?? [];
98
+ for (const property of properties) relations.push({
99
+ ...property,
100
+ variant
101
+ });
102
+ }
103
+ return relations;
104
+ }
88
105
  const THREE_D_VIEWER_COMPONENT_DEFAULTS = {
89
106
  isInteractive: true,
90
107
  isControlsDisplayed: true
@@ -261,6 +278,7 @@ function parseCollectionComponent(parameters) {
261
278
  component: "collection",
262
279
  linkUuids: setLinks.map((link) => link.uuid),
263
280
  displayedProperties: parseCollectionDisplayedProperties(componentReader),
281
+ relations: parseCollectionRelations(componentReader),
264
282
  ...COLLECTION_PROPERTY_DEFAULTS,
265
283
  ...propertyOverrides,
266
284
  image: {
@@ -472,9 +490,12 @@ function parseQueryComponent(parameters) {
472
490
  }
473
491
  if (items.length === 0) throw new Error(formatComponentError("No queries found", "query", elementResource), { cause: componentProperty });
474
492
  const componentOptions = parseWebsiteOptions(elementResource.options, options);
475
- const collectionProperties = parseCollectionPropertyOverrides(componentReader.nestedByValue("sub-component-override", "collection"));
493
+ const overrideReader = componentReader.nestedByValue("sub-component-override", "collection");
494
+ const collectionProperties = parseCollectionPropertyOverrides(overrideReader);
476
495
  const displayedProperties = parseCollectionDisplayedProperties(componentReader);
477
496
  if (displayedProperties != null) collectionProperties.displayedProperties = displayedProperties;
497
+ const relations = parseCollectionRelations(overrideReader);
498
+ if (relations.length > 0) collectionProperties.relations = relations;
478
499
  return {
479
500
  component: "query",
480
501
  linkUuids: setLinks.map((link) => link.uuid),
@@ -104,7 +104,7 @@ function parseWebsiteOptions(rawOptions, options) {
104
104
  };
105
105
  const notes = parseNotes(rawOptions?.notes, options);
106
106
  for (const note of notes) {
107
- if (note.title?.getText() !== "Title label") continue;
107
+ if (note.title?.getText(note.title.getDefaultLanguage()) !== "Title label") continue;
108
108
  parsedOptions.labels.title = note.content;
109
109
  break;
110
110
  }
@@ -76,6 +76,7 @@ function parseWebsiteProperties(properties, websiteTree, sidebar, options, paren
76
76
  isPersistentIdentifierDisplayed: parent?.itemPage.isPersistentIdentifierDisplayed ?? true,
77
77
  iiifViewer: parent?.itemPage.iiifViewer ?? "universal-viewer"
78
78
  },
79
+ relations: { variant: parent?.relations.variant ?? "list" },
79
80
  options: {
80
81
  contextTree: parent?.options.contextTree ?? null,
81
82
  scopes: parent?.options.scopes ?? null,
@@ -117,7 +118,7 @@ function parseWebsiteProperties(properties, websiteTree, sidebar, options, paren
117
118
  websiteReader.readAllUuids(returnProperties.navbar, { searchBarBoundElementUuid: "bound-element-navbar-search-bar" });
118
119
  websiteReader.readAll(returnProperties.footer, { isDisplayed: "footer-displayed" });
119
120
  websiteReader.readAllUuids(returnProperties.footer, { logoUuid: "footer-logo" });
120
- const itemPageReader = websiteReader.nestedByValue("page-type", "item-page");
121
+ const itemPageReader = websiteReader.nestedByValue("customize-special", "item-page");
121
122
  if (itemPageReader.size > 0) {
122
123
  for (const [key, slug] of [
123
124
  ["description", "description"],
@@ -142,6 +143,7 @@ function parseWebsiteProperties(properties, websiteTree, sidebar, options, paren
142
143
  iiifViewer: "item-page-iiif-viewer"
143
144
  });
144
145
  }
146
+ websiteReader.nestedByValue("customize-special", "relations").readAll(returnProperties.relations, { variant: "variant" });
145
147
  if (websiteTree.options != null) {
146
148
  const parsedOptions = parseWebsiteOptions(websiteTree.options, options);
147
149
  returnProperties.options.scopes = (parsedOptions.scopes != null && parsedOptions.scopes.length > 0 ? parsedOptions : returnProperties.options).scopes;
@@ -70,6 +70,7 @@ export declare class WebsitePresentationReader<T extends LanguageCodes> {
70
70
  readAllUuids<O extends object>(target: O, labels: Partial<Record<UuidKeys<O>, string>>): void;
71
71
  nested(label: string): WebsitePresentationReader<T>;
72
72
  nestedByValue(label: string, value: WebsitePropertyContent<T>): WebsitePresentationReader<T>;
73
+ nestedAllByValue(label: string, value: WebsitePropertyContent<T>): Array<WebsitePresentationReader<T>>;
73
74
  get size(): number;
74
75
  }
75
76
  export declare function websitePresentationReader<T extends LanguageCodes>(properties: ReadonlyArray<SimplifiedProperty<T>>): WebsitePresentationReader<T>;
@@ -103,6 +103,14 @@ var WebsitePresentationReader = class WebsitePresentationReader {
103
103
  nestedByValue(label, value) {
104
104
  return new WebsitePresentationReader(this.propertyByValue(label, value)?.properties ?? []);
105
105
  }
106
+ nestedAllByValue(label, value) {
107
+ const readers = [];
108
+ for (const property of this.sourceProperties) if (getProperty([property], {
109
+ label,
110
+ valueContent: value
111
+ }) != null) readers.push(new WebsitePresentationReader(property.properties));
112
+ return readers;
113
+ }
106
114
  get size() {
107
115
  return this.sourceProperties.length;
108
116
  }
package/dist/query.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { PropertyRelation, Query } from "./types/index.mjs";
2
- import { OchreQueryContext } from "./xquery.mjs";
2
+ import { OchreContentLanguage, OchreQueryContext } from "./xquery.mjs";
3
3
  //#region src/query.d.ts
4
4
  export declare function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids: ReadonlyArray<string>, belongsToCollectionPropertyVariableUuid: string): string | null;
5
5
  /**
@@ -75,6 +75,7 @@ export type ItemsContainer = keyof typeof ITEMS_CONTAINERS;
75
75
  * @param parameters.queries - The query tree to compile, or null to match every item
76
76
  * @param parameters.declarations - Extra prolog declarations, placed before the compiled prolog
77
77
  * @param parameters.body - Builds the body from the name of the variable holding the items
78
+ * @param parameters.contentLanguage - Keep only this language of every multilingual field in the result
78
79
  * @returns A complete XQuery document
79
80
  * @internal
80
81
  */
@@ -87,6 +88,7 @@ export declare function compileContainerItemsQuery(parameters: {
87
88
  body: (context: OchreQueryContext & {
88
89
  items: string;
89
90
  }) => string;
91
+ contentLanguage?: OchreContentLanguage | null;
90
92
  }): string;
91
93
  /**
92
94
  * Reduce a property-value facet query tree to the leaves that filter items
package/dist/query.mjs CHANGED
@@ -14,32 +14,25 @@ const CTS_INCLUDES_STOP_WORDS = /* @__PURE__ */ new Set([
14
14
  const CTS_INCLUDES_TOKEN_WORD_REGEX = /^\p{L}+$/u;
15
15
  const CTS_INCLUDES_TOKEN_REGEX = /[\p{L}\p{N}*?]+/gu;
16
16
  const CTS_EXACT_TEXT_TOKEN_REGEX = /[\p{L}\p{N}]+/gu;
17
- const CONTENT_TARGET_CONTENT_ELEMENT_PATHS = {
18
- title: [
19
- "identification",
20
- "label",
21
- "content"
22
- ],
23
- description: ["description", "content"],
17
+ const CONTENT_TARGET_FIELD_ELEMENT_PATHS = {
18
+ title: ["identification", "label"],
19
+ description: ["description"],
24
20
  image: [
25
21
  "image",
26
22
  "identification",
27
- "label",
28
- "content"
23
+ "label"
29
24
  ],
30
25
  periods: [
31
26
  "periods",
32
27
  "period",
33
28
  "identification",
34
- "label",
35
- "content"
29
+ "label"
36
30
  ],
37
31
  bibliography: [
38
32
  "bibliographies",
39
33
  "bibliography",
40
34
  "identification",
41
- "label",
42
- "content"
35
+ "label"
43
36
  ]
44
37
  };
45
38
  const HELPER_SEARCH_VALUE_REFERENCE = "$value";
@@ -246,36 +239,51 @@ function buildValueNotIdReferenceQuery() {
246
239
  value: searchValue("IDREF")
247
240
  }));
248
241
  }
249
- function buildRichTextContentQueryExpression(parameters) {
250
- const { value, matchMode, isCaseSensitive, language } = parameters;
251
- return buildAndCtsQueryExpressionInternal([buildContentLanguageQuery(language), matchMode === "exact" ? buildRichTextExactQueryExpression({
252
- value,
253
- isCaseSensitive,
254
- language
255
- }) : buildCtsWordQueryExpression({
256
- value,
257
- matchMode,
258
- isCaseSensitive,
259
- queryFamily: "text",
260
- language
261
- })]);
242
+ /**
243
+ * Match a field's `content` in a language, falling back to another language
244
+ * only for fields that carry no content in the first one
245
+ *
246
+ * Placed inside the field element (a label, a note, a property value), so the
247
+ * missing-language check is scoped to that one field. Without a distinct
248
+ * fallback it is the single-language query this always compiled to.
249
+ */
250
+ function buildLocalizedContentQueryExpression(parameters) {
251
+ const { language, fallbackLanguage, buildMatchQuery } = parameters;
252
+ function buildContentQuery(contentLanguage) {
253
+ return buildNestedElementQuery(["content"], buildAndCtsQueryExpressionInternal([buildContentLanguageQuery(contentLanguage), buildMatchQuery(contentLanguage)]));
254
+ }
255
+ if (fallbackLanguage == null || fallbackLanguage === language) return buildContentQuery(language);
256
+ return buildOrCtsQueryExpressionInternal([buildContentQuery(language), buildAndCtsQueryExpressionInternal([buildNotCtsQueryExpression(buildNestedElementQuery(["content"], buildContentLanguageQuery(language))), buildContentQuery(fallbackLanguage)])]);
262
257
  }
263
- function buildValueContentInnerQuery(parameters) {
264
- const { language, value, matchMode, isCaseSensitive } = parameters;
265
- return buildNestedElementQuery(["content"], buildRichTextContentQueryExpression({
258
+ function buildRichTextContentQueryExpression(parameters) {
259
+ const { value, matchMode, isCaseSensitive, language, fallbackLanguage } = parameters;
260
+ return buildLocalizedContentQueryExpression({
266
261
  language,
267
- value,
268
- matchMode,
269
- isCaseSensitive
270
- }));
262
+ fallbackLanguage,
263
+ buildMatchQuery: (contentLanguage) => matchMode === "exact" ? buildRichTextExactQueryExpression({
264
+ value,
265
+ isCaseSensitive,
266
+ language: contentLanguage
267
+ }) : buildCtsWordQueryExpression({
268
+ value,
269
+ matchMode,
270
+ isCaseSensitive,
271
+ queryFamily: "text",
272
+ language: contentLanguage
273
+ })
274
+ });
271
275
  }
272
276
  function buildValueContentExactInnerQuery(parameters) {
273
- const { language, value, isCaseSensitive } = parameters;
274
- return buildNestedElementQuery(["content"], buildAndCtsQueryExpressionInternal([buildContentLanguageQuery(language), buildCtsElementValueQueryExpression({
275
- elementName: "string",
276
- value,
277
- isCaseSensitive
278
- })]));
277
+ const { language, fallbackLanguage, value, isCaseSensitive } = parameters;
278
+ return buildLocalizedContentQueryExpression({
279
+ language,
280
+ fallbackLanguage,
281
+ buildMatchQuery: () => buildCtsElementValueQueryExpression({
282
+ elementName: "string",
283
+ value,
284
+ isCaseSensitive
285
+ })
286
+ });
279
287
  }
280
288
  function buildValueDirectTextInnerQuery(parameters) {
281
289
  const { value, matchMode, isCaseSensitive } = parameters;
@@ -310,26 +318,23 @@ function buildValueRawValueInnerQuery(parameters) {
310
318
  });
311
319
  }
312
320
  function buildNotesQueryExpression(parameters) {
313
- const { value, matchMode, isCaseSensitive, language } = parameters;
314
- return buildNestedElementQuery([
315
- "notes",
316
- "note",
317
- "content"
318
- ], buildRichTextContentQueryExpression({
321
+ const { value, matchMode, isCaseSensitive, language, fallbackLanguage } = parameters;
322
+ return buildNestedElementQuery(["notes", "note"], buildRichTextContentQueryExpression({
319
323
  value,
320
324
  matchMode,
321
325
  isCaseSensitive,
322
- language
326
+ language,
327
+ fallbackLanguage
323
328
  }));
324
329
  }
325
330
  function buildContentTargetQueryExpression(parameters) {
326
- const { target, value, matchMode, isCaseSensitive, language } = parameters;
327
- const contentElementPath = CONTENT_TARGET_CONTENT_ELEMENT_PATHS[target];
328
- return buildNestedElementQuery(contentElementPath, buildRichTextContentQueryExpression({
331
+ const { target, value, matchMode, isCaseSensitive, language, fallbackLanguage } = parameters;
332
+ return buildNestedElementQuery(CONTENT_TARGET_FIELD_ELEMENT_PATHS[target], buildRichTextContentQueryExpression({
329
333
  value,
330
334
  matchMode,
331
335
  isCaseSensitive,
332
- language
336
+ language,
337
+ fallbackLanguage
333
338
  }));
334
339
  }
335
340
  function buildPropertyQueryExpression(parameters) {
@@ -381,16 +386,18 @@ function buildPropertyPresenceQueryExpression(parameters) {
381
386
  });
382
387
  }
383
388
  function buildPropertyStringQueryExpression(parameters) {
384
- const { propertyVariable, propertyRelation, value, matchMode, isCaseSensitive, language } = parameters;
389
+ const { propertyVariable, propertyRelation, value, matchMode, isCaseSensitive, language, fallbackLanguage } = parameters;
385
390
  return buildPropertyTextMatchQueryExpression({
386
391
  propertyVariable,
387
392
  propertyRelation,
388
393
  contentQueryExpression: matchMode === "exact" ? buildValueContentExactInnerQuery({
389
394
  language,
395
+ fallbackLanguage,
390
396
  value,
391
397
  isCaseSensitive
392
- }) : buildValueContentInnerQuery({
398
+ }) : buildRichTextContentQueryExpression({
393
399
  language,
400
+ fallbackLanguage,
394
401
  value,
395
402
  matchMode,
396
403
  isCaseSensitive
@@ -429,8 +436,9 @@ function buildPropertyAllQueryExpression(parameters) {
429
436
  propertyVariable: query.propertyVariable,
430
437
  propertyRelation: query.propertyRelation,
431
438
  valueFilters: [buildValueNotIdReferenceQuery()],
432
- contentQueryExpression: buildValueContentInnerQuery({
439
+ contentQueryExpression: buildRichTextContentQueryExpression({
433
440
  language: query.language,
441
+ fallbackLanguage: query.fallbackLanguage,
434
442
  value,
435
443
  matchMode,
436
444
  isCaseSensitive: query.isCaseSensitive
@@ -470,18 +478,20 @@ function buildPropertyDateRangeQueryExpression(query) {
470
478
  });
471
479
  }
472
480
  function buildItemStringQueryExpression(parameters) {
473
- const { value, matchMode, isCaseSensitive, language } = parameters;
481
+ const { value, matchMode, isCaseSensitive, language, fallbackLanguage } = parameters;
474
482
  return buildOrCtsQueryExpressionInternal([buildContentTargetQueryExpression({
475
483
  target: "title",
476
484
  value,
477
485
  matchMode,
478
486
  isCaseSensitive,
479
- language
487
+ language,
488
+ fallbackLanguage
480
489
  }), buildPropertyStringQueryExpression({
481
490
  value,
482
491
  matchMode,
483
492
  isCaseSensitive,
484
- language
493
+ language,
494
+ fallbackLanguage
485
495
  })]);
486
496
  }
487
497
  function tokenizeOcrExactValue(value) {
@@ -614,13 +624,15 @@ function buildLeafValueQueryExpression(parameters) {
614
624
  value,
615
625
  matchMode,
616
626
  isCaseSensitive: query.isCaseSensitive,
617
- language: query.language
627
+ language: query.language,
628
+ fallbackLanguage: query.fallbackLanguage
618
629
  });
619
630
  case "notes": return buildNotesQueryExpression({
620
631
  value,
621
632
  matchMode,
622
633
  isCaseSensitive: query.isCaseSensitive,
623
- language: query.language
634
+ language: query.language,
635
+ fallbackLanguage: query.fallbackLanguage
624
636
  });
625
637
  case "title":
626
638
  case "description":
@@ -631,7 +643,8 @@ function buildLeafValueQueryExpression(parameters) {
631
643
  value,
632
644
  matchMode,
633
645
  isCaseSensitive: query.isCaseSensitive,
634
- language: query.language
646
+ language: query.language,
647
+ fallbackLanguage: query.fallbackLanguage
635
648
  });
636
649
  case "property": switch (query.dataType) {
637
650
  case "all": return buildPropertyAllQueryExpression({
@@ -650,7 +663,8 @@ function buildLeafValueQueryExpression(parameters) {
650
663
  value,
651
664
  matchMode,
652
665
  isCaseSensitive: query.isCaseSensitive,
653
- language: query.language
666
+ language: query.language,
667
+ fallbackLanguage: query.fallbackLanguage
654
668
  });
655
669
  case "integer":
656
670
  case "decimal":
@@ -728,7 +742,8 @@ function getLeafHelperKey(parameters) {
728
742
  query.target,
729
743
  value,
730
744
  query.isCaseSensitive ? "case-sensitive" : "case-insensitive",
731
- query.language
745
+ query.language,
746
+ query.fallbackLanguage ?? ""
732
747
  ].join("|");
733
748
  case "property": return [
734
749
  "leaf",
@@ -739,7 +754,8 @@ function getLeafHelperKey(parameters) {
739
754
  query.propertyRelation ?? "",
740
755
  value,
741
756
  query.isCaseSensitive ? "case-sensitive" : "case-insensitive",
742
- query.language
757
+ query.language,
758
+ query.fallbackLanguage ?? ""
743
759
  ].join("|");
744
760
  }
745
761
  }
@@ -775,6 +791,7 @@ function getIncludesLeafHelperKey(parameters) {
775
791
  query.target,
776
792
  query.isCaseSensitive ? "case-sensitive" : "case-insensitive",
777
793
  query.language,
794
+ query.fallbackLanguage ?? "",
778
795
  isWildcarded ? "wildcarded" : "unwildcarded",
779
796
  isStemmed ? "stemmed" : "unstemmed"
780
797
  ].join("|");
@@ -786,6 +803,7 @@ function getIncludesLeafHelperKey(parameters) {
786
803
  query.propertyRelation ?? "",
787
804
  query.isCaseSensitive ? "case-sensitive" : "case-insensitive",
788
805
  query.language,
806
+ query.fallbackLanguage ?? "",
789
807
  isWildcarded ? "wildcarded" : "unwildcarded",
790
808
  isStemmed ? "stemmed" : "unstemmed"
791
809
  ].join("|");
@@ -876,7 +894,7 @@ function getCompatibleIncludesGroupLeaves(query) {
876
894
  if (!("or" in query) || query.or.length <= 1) return null;
877
895
  const leafQueries = [];
878
896
  for (const childQuery of query.or) {
879
- if (!isQueryLeaf(childQuery) || childQuery.target === "ocr") return null;
897
+ if (!isQueryLeaf(childQuery) || childQuery.target === "ocr" || childQuery.fallbackLanguage != null && childQuery.fallbackLanguage !== childQuery.language) return null;
880
898
  leafQueries.push(childQuery);
881
899
  }
882
900
  const firstQuery = leafQueries[0];
@@ -941,7 +959,26 @@ function buildCtsItemsPlan(queryExpression) {
941
959
  return {
942
960
  kind: "search",
943
961
  itemPredicates: [],
944
- queryExpressions: [queryExpression]
962
+ queryExpressions: [queryExpression],
963
+ resultPredicates: []
964
+ };
965
+ }
966
+ /**
967
+ * Plan a leaf that falls back to a second language for fields missing the first
968
+ *
969
+ * The strict query needs a `cts:not-query` scoped to one field, which CTS
970
+ * resolves against the Set's single fragment and so rules out every item the
971
+ * moment any item holds the first language. The search therefore runs a
972
+ * superset (the leaf in either language), and `cts:contains` then applies the
973
+ * strict query to each matched item projection, where the negation is exact.
974
+ */
975
+ function buildFallbackItemsPlan(parameters) {
976
+ const { supersetQueryExpression, strictQueryExpression } = parameters;
977
+ return {
978
+ kind: "search",
979
+ itemPredicates: [],
980
+ queryExpressions: [supersetQueryExpression],
981
+ resultPredicates: [`[cts:contains(., ${strictQueryExpression})]`]
945
982
  };
946
983
  }
947
984
  /**
@@ -958,7 +995,8 @@ function buildNegatedItemsPlan(queryExpression) {
958
995
  return {
959
996
  kind: "search",
960
997
  itemPredicates: [`[not(cts:contains(., ${queryExpression}))]`],
961
- queryExpressions: []
998
+ queryExpressions: [],
999
+ resultPredicates: []
962
1000
  };
963
1001
  }
964
1002
  /**
@@ -976,6 +1014,9 @@ function flattenItemsPlans(childPlans, kind) {
976
1014
  }
977
1015
  return flattenedPlans;
978
1016
  }
1017
+ function appendUniquePredicates(predicates, addedPredicates) {
1018
+ for (const predicate of addedPredicates) if (!predicates.includes(predicate)) predicates.push(predicate);
1019
+ }
979
1020
  /**
980
1021
  * Fold the children of an `and` group into one plan
981
1022
  *
@@ -987,7 +1028,8 @@ function buildAndItemsPlan(childPlans) {
987
1028
  const mergedPlan = {
988
1029
  kind: "search",
989
1030
  itemPredicates: [],
990
- queryExpressions: []
1031
+ queryExpressions: [],
1032
+ resultPredicates: []
991
1033
  };
992
1034
  const unfoldablePlans = [];
993
1035
  for (const childPlan of flattenItemsPlans(childPlans, "intersect")) {
@@ -995,11 +1037,12 @@ function buildAndItemsPlan(childPlans) {
995
1037
  unfoldablePlans.push(childPlan);
996
1038
  continue;
997
1039
  }
998
- for (const itemPredicate of childPlan.itemPredicates) if (!mergedPlan.itemPredicates.includes(itemPredicate)) mergedPlan.itemPredicates.push(itemPredicate);
1040
+ appendUniquePredicates(mergedPlan.itemPredicates, childPlan.itemPredicates);
1041
+ appendUniquePredicates(mergedPlan.resultPredicates, childPlan.resultPredicates);
999
1042
  mergedPlan.queryExpressions.push(...childPlan.queryExpressions);
1000
1043
  }
1001
1044
  if (unfoldablePlans.length === 0) return mergedPlan;
1002
- const intersectedPlans = mergedPlan.itemPredicates.length === 0 && mergedPlan.queryExpressions.length === 0 ? unfoldablePlans : [mergedPlan, ...unfoldablePlans];
1045
+ const intersectedPlans = mergedPlan.itemPredicates.length === 0 && mergedPlan.queryExpressions.length === 0 && mergedPlan.resultPredicates.length === 0 ? unfoldablePlans : [mergedPlan, ...unfoldablePlans];
1003
1046
  return intersectedPlans.length === 1 ? intersectedPlans[0] ?? mergedPlan : {
1004
1047
  kind: "intersect",
1005
1048
  children: intersectedPlans
@@ -1015,7 +1058,7 @@ function buildOrItemsPlan(childPlans) {
1015
1058
  const mergedQueryExpressions = [];
1016
1059
  const unionedPlans = [];
1017
1060
  for (const childPlan of flattenItemsPlans(childPlans, "union")) {
1018
- if (childPlan.kind === "search" && childPlan.itemPredicates.length === 0) {
1061
+ if (childPlan.kind === "search" && childPlan.itemPredicates.length === 0 && childPlan.resultPredicates.length === 0) {
1019
1062
  mergedQueryExpressions.push(buildAndCtsQueryExpressionInternal(childPlan.queryExpressions));
1020
1063
  continue;
1021
1064
  }
@@ -1024,7 +1067,8 @@ function buildOrItemsPlan(childPlans) {
1024
1067
  if (mergedQueryExpressions.length > 0) unionedPlans.unshift({
1025
1068
  kind: "search",
1026
1069
  itemPredicates: [],
1027
- queryExpressions: [buildOrCtsQueryExpressionInternal(mergedQueryExpressions)]
1070
+ queryExpressions: [buildOrCtsQueryExpressionInternal(mergedQueryExpressions)],
1071
+ resultPredicates: []
1028
1072
  });
1029
1073
  if (unionedPlans.length === 0) return buildCtsItemsPlan("cts:false-query()");
1030
1074
  return unionedPlans.length === 1 ? unionedPlans[0] ?? buildCtsItemsPlan("cts:false-query()") : {
@@ -1039,11 +1083,24 @@ function buildItemsPlan(context, query) {
1039
1083
  return {
1040
1084
  kind: "search",
1041
1085
  itemPredicates: [query.isNegated === true ? `[not(@uuid = ${bindingName})]` : `[@uuid = ${bindingName}]`],
1042
- queryExpressions: []
1086
+ queryExpressions: [],
1087
+ resultPredicates: []
1043
1088
  };
1044
1089
  }
1045
1090
  const queryExpression = buildLeafQueryExpression(context, query);
1046
- return query.isNegated === true ? buildNegatedItemsPlan(queryExpression) : buildCtsItemsPlan(queryExpression);
1091
+ if (query.isNegated === true) return buildNegatedItemsPlan(queryExpression);
1092
+ if (query.fallbackLanguage == null || query.fallbackLanguage === query.language) return buildCtsItemsPlan(queryExpression);
1093
+ return buildFallbackItemsPlan({
1094
+ supersetQueryExpression: buildOrCtsQueryExpressionInternal([buildLeafQueryExpression(context, {
1095
+ ...query,
1096
+ fallbackLanguage: void 0
1097
+ }), buildLeafQueryExpression(context, {
1098
+ ...query,
1099
+ language: query.fallbackLanguage,
1100
+ fallbackLanguage: void 0
1101
+ })]),
1102
+ strictQueryExpression: queryExpression
1103
+ });
1047
1104
  }
1048
1105
  const optimizedIncludesGroupQueries = getCompatibleIncludesGroupLeaves(query);
1049
1106
  if (optimizedIncludesGroupQueries != null) return buildCtsItemsPlan(buildIncludesGroupQueryExpression(context, optimizedIncludesGroupQueries));
@@ -1062,7 +1119,8 @@ function buildItemsPlanExpression(parameters) {
1062
1119
  if (plan.kind === "search") {
1063
1120
  const itemsExpression = `${baseItemsExpression}${plan.itemPredicates.join("")}`;
1064
1121
  const queryName = queryNamesByPlan.get(plan);
1065
- return queryName == null ? itemsExpression : `cts:search(${itemsExpression}, ${queryName})`;
1122
+ const searchedExpression = queryName == null ? itemsExpression : `cts:search(${itemsExpression}, ${queryName})`;
1123
+ return plan.resultPredicates.length === 0 ? searchedExpression : `(${searchedExpression})${plan.resultPredicates.join("")}`;
1066
1124
  }
1067
1125
  return `(${Array.from(plan.children, (childPlan) => buildItemsPlanExpression({
1068
1126
  plan: childPlan,
@@ -1150,7 +1208,8 @@ function buildQueryPlan(parameters) {
1150
1208
  const plan = queries == null ? {
1151
1209
  kind: "search",
1152
1210
  itemPredicates: [],
1153
- queryExpressions: []
1211
+ queryExpressions: [],
1212
+ resultPredicates: []
1154
1213
  } : buildItemsPlan(context, queries);
1155
1214
  const searchPlans = [];
1156
1215
  collectItemsSearchPlans(plan, searchPlans);
@@ -1202,11 +1261,12 @@ function buildQueryPlan(parameters) {
1202
1261
  * @param parameters.queries - The query tree to compile, or null to match every item
1203
1262
  * @param parameters.declarations - Extra prolog declarations, placed before the compiled prolog
1204
1263
  * @param parameters.body - Builds the body from the name of the variable holding the items
1264
+ * @param parameters.contentLanguage - Keep only this language of every multilingual field in the result
1205
1265
  * @returns A complete XQuery document
1206
1266
  * @internal
1207
1267
  */
1208
1268
  function compileContainerItemsQuery(parameters) {
1209
- const { container, scopeUuids, belongsToCollectionScopeUuids, queries, declarations = [], body } = parameters;
1269
+ const { container, scopeUuids, belongsToCollectionScopeUuids, queries, declarations = [], body, contentLanguage } = parameters;
1210
1270
  const { scopeVariable, itemsExpression } = ITEMS_CONTAINERS[container];
1211
1271
  const plan = buildQueryPlan({
1212
1272
  queries,
@@ -1225,7 +1285,8 @@ ${body({
1225
1285
  ...context,
1226
1286
  items: plan.itemsVariable
1227
1287
  })}
1228
- }</ochre>`
1288
+ }</ochre>`,
1289
+ contentLanguage
1229
1290
  });
1230
1291
  }
1231
1292
  /**
package/dist/schemas.mjs CHANGED
@@ -84,6 +84,7 @@ const standardQueryFields = {
84
84
  matchMode: v.picklist(["includes", "exact"]),
85
85
  isCaseSensitive: v.boolean(),
86
86
  language: defaultString("eng"),
87
+ fallbackLanguage: v.optional(v.string()),
87
88
  isNegated: defaultBoolean(false)
88
89
  };
89
90
  const propertyRelationSchema = v.picklist(["related", "inverse"]);
@@ -853,6 +853,10 @@ export type SetItemsSort = {
853
853
  * layer holds a single OCR word, so `includes` matches every search term as its
854
854
  * own word and `exact` matches the terms as an adjacent run of whole words. OCR
855
855
  * text carries no language, so `ocr` leaves take no `language`.
856
+ *
857
+ * `fallbackLanguage` searches a field in that language only when the field has
858
+ * no content in `language`, so a match follows the text a reader of `language`
859
+ * is shown. Leave it unset to search `language` alone.
856
860
  */
857
861
  export type QueryLeaf = {
858
862
  target: "property";
@@ -865,6 +869,7 @@ export type QueryLeaf = {
865
869
  matchMode: "includes" | "exact";
866
870
  isCaseSensitive: boolean;
867
871
  language: string;
872
+ fallbackLanguage?: string;
868
873
  isNegated?: boolean;
869
874
  } | {
870
875
  target: "property";
@@ -877,6 +882,7 @@ export type QueryLeaf = {
877
882
  matchMode: "includes" | "exact";
878
883
  isCaseSensitive: boolean;
879
884
  language: string;
885
+ fallbackLanguage?: string;
880
886
  isNegated?: boolean;
881
887
  } | {
882
888
  target: "property";
@@ -889,6 +895,7 @@ export type QueryLeaf = {
889
895
  matchMode: "includes" | "exact";
890
896
  isCaseSensitive: boolean;
891
897
  language: string;
898
+ fallbackLanguage?: string;
892
899
  isNegated?: boolean;
893
900
  } | {
894
901
  target: "property";
@@ -901,6 +908,7 @@ export type QueryLeaf = {
901
908
  matchMode: "includes" | "exact";
902
909
  isCaseSensitive: boolean;
903
910
  language: string;
911
+ fallbackLanguage?: string;
904
912
  isNegated?: boolean;
905
913
  } | {
906
914
  target: "property";
@@ -911,6 +919,7 @@ export type QueryLeaf = {
911
919
  matchMode: "includes" | "exact";
912
920
  isCaseSensitive: boolean;
913
921
  language: string;
922
+ fallbackLanguage?: string;
914
923
  isNegated?: boolean;
915
924
  } | {
916
925
  target: "string";
@@ -918,6 +927,7 @@ export type QueryLeaf = {
918
927
  matchMode: "includes" | "exact";
919
928
  isCaseSensitive: boolean;
920
929
  language: string;
930
+ fallbackLanguage?: string;
921
931
  isNegated?: boolean;
922
932
  } | {
923
933
  target: "ocr";
@@ -931,6 +941,7 @@ export type QueryLeaf = {
931
941
  matchMode: "includes" | "exact";
932
942
  isCaseSensitive: boolean;
933
943
  language: string;
944
+ fallbackLanguage?: string;
934
945
  isNegated?: boolean;
935
946
  };
936
947
  /**
@@ -87,6 +87,10 @@ export type WebLoadingVariant = "spinner" | "skeleton" | "animation" | "none";
87
87
  * How an item page groups a repeated section
88
88
  */
89
89
  export type WebSectionVariant = "discrete" | "tabs-horizontal" | "tabs-vertical" | "tabular";
90
+ /**
91
+ * How a website displays an item's related items
92
+ */
93
+ export type WebRelationVariant = "list" | "dropdown-list" | "grid" | "gallery" | "inline";
90
94
  /**
91
95
  * The IIIF viewer a website renders images with
92
96
  */
@@ -206,6 +210,9 @@ export type Website<T extends LanguageCodes = LanguageCodes> = {
206
210
  isPersistentIdentifierDisplayed: boolean;
207
211
  iiifViewer: WebIiifViewer;
208
212
  };
213
+ relations: {
214
+ variant: WebRelationVariant;
215
+ };
209
216
  options: Prettify<WebOptions<T> & {
210
217
  stylesheets: {
211
218
  properties: Array<StylesheetItem>;
@@ -336,6 +343,11 @@ export type WebElementComponent<T extends LanguageCodes = LanguageCodes> = {
336
343
  uuid: string;
337
344
  label: MultilingualString<T> | null;
338
345
  }> | null;
346
+ relations: Array<{
347
+ uuid: string;
348
+ label: MultilingualString<T> | null;
349
+ variant: WebRelationVariant | null;
350
+ }>;
339
351
  variant: "slide" | "table" | "card" | "tile" | "showcase";
340
352
  paginationVariant: "default" | "numeric";
341
353
  loadingVariant: WebLoadingVariant;
package/dist/xquery.d.mts CHANGED
@@ -17,6 +17,25 @@ export declare function stringLiteral(value: string): string;
17
17
  * @internal
18
18
  */
19
19
  export declare const OCHRE_RESPONSE_PADDING = "<!--ochre-sdk: keeps an empty result distinct from a failed query-->";
20
+ /**
21
+ * Which language a query should return, and what to show where it is missing
22
+ */
23
+ export type OchreContentLanguage = {
24
+ language: string;
25
+ fallbackLanguage: string;
26
+ };
27
+ /**
28
+ * Resolve the content language a fetch asked for
29
+ * @param options - The fetch options
30
+ * @param options.language - The language to keep
31
+ * @param options.fallbackLanguage - The language shown where `language` is missing
32
+ * @returns The content language, or null to keep every language
33
+ * @internal
34
+ */
35
+ export declare function getContentLanguage(options?: {
36
+ language?: string;
37
+ fallbackLanguage?: string;
38
+ }): OchreContentLanguage | null;
20
39
  /**
21
40
  * What a query body can ask the surrounding document for
22
41
  *
@@ -47,11 +66,13 @@ export type OchreQueryContext = {
47
66
  * @param parameters - The document parameters
48
67
  * @param parameters.declarations - Prolog declarations, emitted in order before the supplemental helper
49
68
  * @param parameters.body - Builds the query body
69
+ * @param parameters.contentLanguage - Keep only this language of every multilingual field in the result
50
70
  * @returns A complete XQuery document
51
71
  * @internal
52
72
  */
53
73
  export declare function compileOchreQuery(parameters: {
54
74
  declarations?: ReadonlyArray<string>;
55
75
  body: (context: OchreQueryContext) => string;
76
+ contentLanguage?: OchreContentLanguage | null;
56
77
  }): string;
57
78
  //#endregion
package/dist/xquery.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_LANGUAGES } from "./constants.mjs";
1
2
  //#region src/xquery.ts
2
3
  /**
3
4
  * Build a string literal for an XQuery string
@@ -46,6 +47,63 @@ const SUPPLEMENTAL_XQUERY_PROLOG = `declare function local:omit-supplemental($no
46
47
  }
47
48
  else $node
48
49
  };`;
50
+ /**
51
+ * XQuery prolog declaring `local:select-language`, which keeps one language of
52
+ * every multilingual field in a node sequence
53
+ *
54
+ * A field's `content` children are reduced to the one in `$language`, else the
55
+ * one in `$fallback`, else the first in any language, which is the choice the
56
+ * API's own `lang` parameter makes. `zxx` content is always kept, since it holds
57
+ * a field's aliases rather than a translation; the API's `lang` parameter drops
58
+ * it, which is why the SDK selects languages itself. Leaf elements are
59
+ * returned by reference; testing for multilingual descendants before copying a
60
+ * subtree measured slower than copying it.
61
+ */
62
+ const LANGUAGE_XQUERY_PROLOG = `declare function local:select-language($nodes as node()*, $language as xs:string, $fallback as xs:string) as node()* {
63
+ for $node in $nodes
64
+ return
65
+ typeswitch ($node)
66
+ case element() return
67
+ if (empty($node/*))
68
+ then $node
69
+ else
70
+ let $contents := $node/content[@xml:lang]
71
+ let $selected :=
72
+ if (empty($contents))
73
+ then ()
74
+ else (
75
+ $contents[@xml:lang = $language],
76
+ $contents[@xml:lang = $fallback],
77
+ $contents[@xml:lang != "zxx"]
78
+ )[1]
79
+ return element { node-name($node) } {
80
+ $node/@*,
81
+ for $child in $node/node()
82
+ return
83
+ if ($child instance of element(content) and exists($child/@xml:lang))
84
+ then
85
+ if ($child is $selected or $child/@xml:lang = "zxx")
86
+ then $child
87
+ else ()
88
+ else local:select-language($child, $language, $fallback)
89
+ }
90
+ default return $node
91
+ };`;
92
+ /**
93
+ * Resolve the content language a fetch asked for
94
+ * @param options - The fetch options
95
+ * @param options.language - The language to keep
96
+ * @param options.fallbackLanguage - The language shown where `language` is missing
97
+ * @returns The content language, or null to keep every language
98
+ * @internal
99
+ */
100
+ function getContentLanguage(options) {
101
+ if (options?.language == null) return null;
102
+ return {
103
+ language: options.language,
104
+ fallbackLanguage: options.fallbackLanguage ?? DEFAULT_LANGUAGES[0]
105
+ };
106
+ }
49
107
  const NOT_SUPPLEMENTAL_PREDICATE = "[not(ancestor-or-self::*[@supplemental = \"true\"])]";
50
108
  /**
51
109
  * Compile a complete XQuery document for the OCHRE API
@@ -57,21 +115,27 @@ const NOT_SUPPLEMENTAL_PREDICATE = "[not(ancestor-or-self::*[@supplemental = \"t
57
115
  * @param parameters - The document parameters
58
116
  * @param parameters.declarations - Prolog declarations, emitted in order before the supplemental helper
59
117
  * @param parameters.body - Builds the query body
118
+ * @param parameters.contentLanguage - Keep only this language of every multilingual field in the result
60
119
  * @returns A complete XQuery document
61
120
  * @internal
62
121
  */
63
122
  function compileOchreQuery(parameters) {
64
- const { declarations = [], body } = parameters;
65
- return `${[
123
+ const { declarations = [], body, contentLanguage } = parameters;
124
+ const prologDeclarations = [
66
125
  "xquery version \"1.0-ml\";",
67
126
  ...declarations,
68
- SUPPLEMENTAL_XQUERY_PROLOG
69
- ].join("\n\n")}
70
-
71
- ${body({
127
+ SUPPLEMENTAL_XQUERY_PROLOG,
128
+ ...contentLanguage == null ? [] : [LANGUAGE_XQUERY_PROLOG]
129
+ ];
130
+ const bodyExpression = body({
72
131
  omitSupplemental: (expression) => `local:omit-supplemental(${expression})`,
73
132
  notSupplemental: NOT_SUPPLEMENTAL_PREDICATE
74
- })}`;
133
+ });
134
+ return `${prologDeclarations.join("\n\n")}
135
+
136
+ ${contentLanguage == null ? bodyExpression : `local:select-language((
137
+ ${bodyExpression}
138
+ ), ${stringLiteral(contentLanguage.language)}, ${stringLiteral(contentLanguage.fallbackLanguage)})`}`;
75
139
  }
76
140
  //#endregion
77
- export { OCHRE_RESPONSE_PADDING, compileOchreQuery, stringLiteral };
141
+ export { OCHRE_RESPONSE_PADDING, compileOchreQuery, getContentLanguage, stringLiteral };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ochre-sdk",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Node.js library for working with OCHRE (Online Cultural and Historical Research Environment) data",
@@ -45,20 +45,20 @@
45
45
  "dist"
46
46
  ],
47
47
  "dependencies": {
48
- "fast-equals": "^6.0.4",
48
+ "fast-equals": "^6.1.0",
49
49
  "fast-xml-parser": "^5.11.2",
50
50
  "valibot": "^1.5.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@antfu/eslint-config": "^9.5.1",
54
- "@types/node": "^24.19.0",
54
+ "@types/node": "^24.19.1",
55
55
  "bumpp": "^12.3.0",
56
- "eslint": "^10.11.0",
56
+ "eslint": "^10.12.0",
57
57
  "eslint-plugin-erasable-syntax-only": "^0.7.2",
58
58
  "eslint-plugin-slop": "^0.1.3",
59
59
  "eslint-plugin-sonarjs": "^4.2.2",
60
60
  "knip": "^6.39.0",
61
- "oxfmt": "^0.71.0",
61
+ "oxfmt": "^0.72.0",
62
62
  "tsdown": "^0.23.0",
63
63
  "typescript": "^6.0.3",
64
64
  "vitest": "^5.0.3"