ochre-sdk 1.3.2 → 1.3.3
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/fetchers/container-items.mjs +2 -1
- package/dist/fetchers/gallery.mjs +9 -5
- package/dist/fetchers/item-children.mjs +4 -3
- package/dist/fetchers/item-links.mjs +8 -5
- package/dist/fetchers/item.mjs +10 -5
- package/dist/fetchers/set/item-locations.mjs +2 -1
- package/dist/parsers/helpers.d.mts +11 -0
- package/dist/query.d.mts +3 -1
- package/dist/query.mjs +137 -76
- package/dist/schemas.mjs +1 -0
- package/dist/types/index.d.mts +11 -0
- package/dist/xquery.d.mts +21 -0
- package/dist/xquery.mjs +72 -8
- package/package.json +1 -1
|
@@ -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({
|
|
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({
|
|
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
|
package/dist/fetchers/item.mjs
CHANGED
|
@@ -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({
|
|
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})}">{
|
|
@@ -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;
|
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
|
|
18
|
-
title: [
|
|
19
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
264
|
-
const {
|
|
265
|
-
return
|
|
258
|
+
function buildRichTextContentQueryExpression(parameters) {
|
|
259
|
+
const { value, matchMode, isCaseSensitive, language, fallbackLanguage } = parameters;
|
|
260
|
+
return buildLocalizedContentQueryExpression({
|
|
266
261
|
language,
|
|
267
|
-
|
|
268
|
-
matchMode
|
|
269
|
-
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
}) :
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"]);
|
package/dist/types/index.d.mts
CHANGED
|
@@ -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
|
/**
|
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
|
-
|
|
123
|
+
const { declarations = [], body, contentLanguage } = parameters;
|
|
124
|
+
const prologDeclarations = [
|
|
66
125
|
"xquery version \"1.0-ml\";",
|
|
67
126
|
...declarations,
|
|
68
|
-
SUPPLEMENTAL_XQUERY_PROLOG
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 };
|