ochre-sdk 1.0.72 → 1.0.74
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/README.md +50 -0
- package/dist/constants.mjs +0 -5
- package/dist/fetchers/gallery.mjs +11 -5
- package/dist/fetchers/item-children.mjs +4 -2
- package/dist/fetchers/item-links.mjs +10 -5
- package/dist/fetchers/item-ocr-data.d.mts +37 -0
- package/dist/fetchers/item-ocr-data.mjs +166 -0
- package/dist/fetchers/item.mjs +43 -58
- package/dist/fetchers/set/items.mjs +14 -16
- package/dist/fetchers/set/property-values.mjs +12 -18
- package/dist/fetchers/website-metadata.mjs +7 -3
- package/dist/fetchers/website.mjs +22 -5
- package/dist/getters.d.mts +6 -2
- package/dist/helpers.d.mts +1 -5
- package/dist/helpers.mjs +1 -5
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/parsers/index.d.mts +3 -10
- package/dist/parsers/index.mjs +1 -75
- package/dist/parsers/multilingual.d.mts +12 -4
- package/dist/parsers/website/index.mjs +13 -12
- package/dist/query.d.mts +21 -29
- package/dist/query.mjs +219 -86
- package/dist/schemas.d.mts +7 -8
- package/dist/schemas.mjs +7 -9
- package/dist/types/index.d.mts +27 -56
- package/dist/utilities.d.mts +25 -1
- package/dist/utilities.mjs +41 -1
- package/dist/xml/schemas.d.mts +2 -7
- package/dist/xml/schemas.mjs +1 -39
- package/dist/xml/types.d.mts +1 -44
- package/package.json +5 -4
- package/dist/fetchers/ocr-matches.d.mts +0 -44
- package/dist/fetchers/ocr-matches.mjs +0 -134
package/dist/query.mjs
CHANGED
|
@@ -12,11 +12,6 @@ const CTS_INCLUDES_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
12
12
|
const CTS_INCLUDES_TOKEN_WORD_REGEX = /^\p{L}+$/u;
|
|
13
13
|
const CTS_INCLUDES_TOKEN_REGEX = /[\p{L}\p{N}*?]+/gu;
|
|
14
14
|
const CTS_EXACT_TEXT_TOKEN_REGEX = /[\p{L}\p{N}]+/gu;
|
|
15
|
-
/**
|
|
16
|
-
* Error message for OCR queries nested inside an OR group
|
|
17
|
-
* @internal
|
|
18
|
-
*/
|
|
19
|
-
const OCR_DISJUNCTION_ERROR_MESSAGE = "OCR queries cannot be nested inside an OR group because they are resolved by a document join instead of a CTS query";
|
|
20
15
|
const CONTENT_TARGET_CONTENT_ELEMENT_PATHS = {
|
|
21
16
|
title: [
|
|
22
17
|
"identification",
|
|
@@ -204,9 +199,6 @@ function buildNestedElementQuery(elementNames, queryExpression) {
|
|
|
204
199
|
function buildNotCtsQueryExpression(queryExpression) {
|
|
205
200
|
return `cts:not-query(${queryExpression})`;
|
|
206
201
|
}
|
|
207
|
-
function buildCtsNearQueryExpression(queryExpressions) {
|
|
208
|
-
return `cts:near-query((${queryExpressions.join(", ")}), ${queryExpressions.length - 1}, ("ordered"))`;
|
|
209
|
-
}
|
|
210
202
|
function buildAndCtsQueryExpressionInternal(queryExpressions) {
|
|
211
203
|
if (queryExpressions.length === 0) return "cts:true-query()";
|
|
212
204
|
if (queryExpressions.length === 1) return queryExpressions[0] ?? "cts:true-query()";
|
|
@@ -480,62 +472,81 @@ function buildItemStringQueryExpression(parameters) {
|
|
|
480
472
|
language
|
|
481
473
|
})]);
|
|
482
474
|
}
|
|
483
|
-
function
|
|
484
|
-
const { value, matchMode, isCaseSensitive } = parameters;
|
|
485
|
-
const rawTerms = (isCaseSensitive ? value : value.toLowerCase()).match(matchMode === "exact" ? CTS_EXACT_TEXT_TOKEN_REGEX : CTS_INCLUDES_TOKEN_REGEX) ?? [];
|
|
475
|
+
function tokenizeOcrPhraseValue(value) {
|
|
486
476
|
const terms = [];
|
|
487
|
-
for (const term of
|
|
477
|
+
for (const term of value.split(/\s+/u)) if (term !== "") terms.push(term);
|
|
488
478
|
return terms;
|
|
489
479
|
}
|
|
490
480
|
/**
|
|
491
|
-
*
|
|
481
|
+
* Word queries against the OCR layer cannot carry a stemming option: the OCHRE
|
|
482
|
+
* database has unstemmed word searches turned off, and asking an element word
|
|
483
|
+
* query for `unstemmed` fails with `XDMP-WORDSEARCH`. Omitting the option
|
|
484
|
+
* altogether resolves the term against the database default instead.
|
|
485
|
+
*/
|
|
486
|
+
function buildOcrWordQueryExpression(parameters) {
|
|
487
|
+
const { value, isCaseSensitive } = parameters;
|
|
488
|
+
const options = [
|
|
489
|
+
isCaseSensitive ? "case-sensitive" : "case-insensitive",
|
|
490
|
+
"diacritic-insensitive",
|
|
491
|
+
"punctuation-insensitive",
|
|
492
|
+
"whitespace-insensitive"
|
|
493
|
+
];
|
|
494
|
+
if (hasWildcardCharacters(value)) options.push("wildcarded");
|
|
495
|
+
return `cts:element-word-query(xs:QName("string"), ${stringLiteral(value)}, (${options.map((option) => stringLiteral(option)).join(", ")}))`;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Compile an OCR text search into a query over the `<ocr>` layer of a Resource
|
|
499
|
+
* document
|
|
492
500
|
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
501
|
+
* Every `<string>` node in that layer holds a single OCR word, so `includes`
|
|
502
|
+
* matches each search term as its own word anywhere in the layer, and `exact`
|
|
503
|
+
* matches the terms as a run of adjacent whole string values. A phrase cannot
|
|
504
|
+
* be a word query here: word positions do not carry across the `<string>`
|
|
505
|
+
* boundaries, which makes `cts:near-query` the only phrase mechanism, and its
|
|
506
|
+
* distance is a total span rather than a pairwise gap.
|
|
496
507
|
*/
|
|
497
|
-
function
|
|
498
|
-
const { value, matchMode, isCaseSensitive } =
|
|
499
|
-
|
|
508
|
+
function buildOcrQueryExpression(query) {
|
|
509
|
+
const { value, matchMode, isCaseSensitive } = query;
|
|
510
|
+
if (matchMode === "exact") {
|
|
511
|
+
const terms = tokenizeOcrPhraseValue(value);
|
|
512
|
+
if (terms.length === 0) return "cts:false-query()";
|
|
513
|
+
const termQueryExpressions = Array.from(terms, (term) => buildCtsElementValueQueryExpression({
|
|
514
|
+
elementName: "string",
|
|
515
|
+
value: term,
|
|
516
|
+
isCaseSensitive
|
|
517
|
+
}));
|
|
518
|
+
return buildNestedElementQuery(["ocr"], termQueryExpressions.length === 1 ? termQueryExpressions[0] ?? "cts:false-query()" : `cts:near-query((${termQueryExpressions.join(", ")}), ${termQueryExpressions.length - 1}, ("ordered"))`);
|
|
519
|
+
}
|
|
520
|
+
const terms = tokenizeIncludesSearchValue({
|
|
500
521
|
value,
|
|
501
|
-
matchMode,
|
|
502
522
|
isCaseSensitive
|
|
503
523
|
});
|
|
504
|
-
|
|
505
|
-
return Array.from(terms, (term) =>
|
|
506
|
-
elementName: "string",
|
|
524
|
+
if (terms.length === 0) return "cts:false-query()";
|
|
525
|
+
return buildNestedElementQuery(["ocr"], buildAndCtsQueryExpressionInternal(Array.from(terms, (term) => buildOcrWordQueryExpression({
|
|
507
526
|
value: term,
|
|
508
527
|
isCaseSensitive
|
|
509
|
-
})
|
|
510
|
-
value: term,
|
|
511
|
-
matchMode,
|
|
512
|
-
isCaseSensitive,
|
|
513
|
-
queryFamily: "text"
|
|
514
|
-
}));
|
|
528
|
+
}))));
|
|
515
529
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const bindingKey = [
|
|
530
|
+
/**
|
|
531
|
+
* Bind the UUIDs of the Resource documents whose OCR layer matches a query,
|
|
532
|
+
* reusing the binding when the same search is requested more than once
|
|
533
|
+
*/
|
|
534
|
+
function registerOcrBinding(context, query) {
|
|
535
|
+
const key = [
|
|
523
536
|
query.value,
|
|
524
537
|
query.matchMode,
|
|
525
538
|
query.isCaseSensitive ? "case-sensitive" : "case-insensitive"
|
|
526
539
|
].join("|");
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const itemPredicate = query.isNegated === true ? `[not(@uuid = ${variableName})]` : `[@uuid = ${variableName}]`;
|
|
538
|
-
if (!context.itemPredicates.includes(itemPredicate)) context.itemPredicates.push(itemPredicate);
|
|
540
|
+
const existingName = context.ocrBindingNamesByKey.get(key);
|
|
541
|
+
if (existingName != null) return existingName;
|
|
542
|
+
const name = `$ocrItemUuids${context.ocrBindings.length + 1}`;
|
|
543
|
+
const queryExpression = buildOcrQueryExpression(query);
|
|
544
|
+
context.ocrBindingNamesByKey.set(key, name);
|
|
545
|
+
context.ocrBindings.push({
|
|
546
|
+
name,
|
|
547
|
+
expression: queryExpression === "cts:false-query()" ? "()" : `cts:search(/ochre/resource, ${queryExpression})/@uuid/string()`
|
|
548
|
+
});
|
|
549
|
+
return name;
|
|
539
550
|
}
|
|
540
551
|
function getLeafSearchValue(query) {
|
|
541
552
|
switch (query.target) {
|
|
@@ -618,10 +629,8 @@ function createQueryCompilerContext() {
|
|
|
618
629
|
nextHelperSerial: 1,
|
|
619
630
|
helperNamesByKey: /* @__PURE__ */ new Map(),
|
|
620
631
|
helperDeclarations: [],
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
ocrBindings: [],
|
|
624
|
-
itemPredicates: []
|
|
632
|
+
ocrBindingNamesByKey: /* @__PURE__ */ new Map(),
|
|
633
|
+
ocrBindings: []
|
|
625
634
|
};
|
|
626
635
|
}
|
|
627
636
|
function registerConstantHelper(parameters) {
|
|
@@ -818,19 +827,6 @@ function getQueryGroupChildren(query) {
|
|
|
818
827
|
function getQueryGroupOperator(query) {
|
|
819
828
|
return "and" in query ? "and" : "or";
|
|
820
829
|
}
|
|
821
|
-
function isDisjunctiveQueryGroup(query) {
|
|
822
|
-
return "or" in query && query.or.length > 1;
|
|
823
|
-
}
|
|
824
|
-
/**
|
|
825
|
-
* Whether a query tree nests an OCR leaf inside an OR group
|
|
826
|
-
* @internal
|
|
827
|
-
*/
|
|
828
|
-
function hasOcrQueryInDisjunction(query, isInDisjunction = false) {
|
|
829
|
-
if (isQueryLeaf(query)) return query.target === "ocr" && isInDisjunction;
|
|
830
|
-
const isChildInDisjunction = isInDisjunction || isDisjunctiveQueryGroup(query);
|
|
831
|
-
for (const childQuery of getQueryGroupChildren(query)) if (hasOcrQueryInDisjunction(childQuery, isChildInDisjunction)) return true;
|
|
832
|
-
return false;
|
|
833
|
-
}
|
|
834
830
|
function getCompatibleIncludesGroupLeaves(query) {
|
|
835
831
|
if (!("or" in query) || query.or.length <= 1) return null;
|
|
836
832
|
const leafQueries = [];
|
|
@@ -896,21 +892,121 @@ function buildIncludesGroupQueryExpression(context, queries) {
|
|
|
896
892
|
bodyExpression: buildOrCtsQueryExpressionInternal(exactMemberHelpers.map((helper) => helper.callExpression))
|
|
897
893
|
}).callExpression, tokenizedQueryExpression]);
|
|
898
894
|
}
|
|
899
|
-
function
|
|
895
|
+
function buildCtsItemsPlan(queryExpression) {
|
|
896
|
+
return {
|
|
897
|
+
kind: "search",
|
|
898
|
+
itemPredicates: [],
|
|
899
|
+
queryExpressions: [queryExpression]
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Splice the children of same-kind child plans into their parent, so that a
|
|
904
|
+
* nested group of the same operator does not cost an extra search
|
|
905
|
+
*/
|
|
906
|
+
function flattenItemsPlans(childPlans, kind) {
|
|
907
|
+
const flattenedPlans = [];
|
|
908
|
+
for (const childPlan of childPlans) {
|
|
909
|
+
if (childPlan.kind === kind) {
|
|
910
|
+
flattenedPlans.push(...childPlan.children);
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
flattenedPlans.push(childPlan);
|
|
914
|
+
}
|
|
915
|
+
return flattenedPlans;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Fold the children of an `and` group into one plan
|
|
919
|
+
*
|
|
920
|
+
* Conjunction is the direction the item path predicates already run in, so
|
|
921
|
+
* every child that is a plain search collapses into a single search, and only
|
|
922
|
+
* the children that resolved to a union stay separate.
|
|
923
|
+
*/
|
|
924
|
+
function buildAndItemsPlan(childPlans) {
|
|
925
|
+
const mergedPlan = {
|
|
926
|
+
kind: "search",
|
|
927
|
+
itemPredicates: [],
|
|
928
|
+
queryExpressions: []
|
|
929
|
+
};
|
|
930
|
+
const unfoldablePlans = [];
|
|
931
|
+
for (const childPlan of flattenItemsPlans(childPlans, "intersect")) {
|
|
932
|
+
if (childPlan.kind !== "search") {
|
|
933
|
+
unfoldablePlans.push(childPlan);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
for (const itemPredicate of childPlan.itemPredicates) if (!mergedPlan.itemPredicates.includes(itemPredicate)) mergedPlan.itemPredicates.push(itemPredicate);
|
|
937
|
+
mergedPlan.queryExpressions.push(...childPlan.queryExpressions);
|
|
938
|
+
}
|
|
939
|
+
if (unfoldablePlans.length === 0) return mergedPlan;
|
|
940
|
+
const intersectedPlans = mergedPlan.itemPredicates.length === 0 && mergedPlan.queryExpressions.length === 0 ? unfoldablePlans : [mergedPlan, ...unfoldablePlans];
|
|
941
|
+
return intersectedPlans.length === 1 ? intersectedPlans[0] ?? mergedPlan : {
|
|
942
|
+
kind: "intersect",
|
|
943
|
+
children: intersectedPlans
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Fold the children of an `or` group into one plan
|
|
948
|
+
*
|
|
949
|
+
* Item path predicates cannot be disjoined, so a child carrying one becomes its
|
|
950
|
+
* own arm of a node union. Everything else is still a single CTS query.
|
|
951
|
+
*/
|
|
952
|
+
function buildOrItemsPlan(childPlans) {
|
|
953
|
+
const mergedQueryExpressions = [];
|
|
954
|
+
const unionedPlans = [];
|
|
955
|
+
for (const childPlan of flattenItemsPlans(childPlans, "union")) {
|
|
956
|
+
if (childPlan.kind === "search" && childPlan.itemPredicates.length === 0) {
|
|
957
|
+
mergedQueryExpressions.push(buildAndCtsQueryExpressionInternal(childPlan.queryExpressions));
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
unionedPlans.push(childPlan);
|
|
961
|
+
}
|
|
962
|
+
if (mergedQueryExpressions.length > 0) unionedPlans.unshift({
|
|
963
|
+
kind: "search",
|
|
964
|
+
itemPredicates: [],
|
|
965
|
+
queryExpressions: [buildOrCtsQueryExpressionInternal(mergedQueryExpressions)]
|
|
966
|
+
});
|
|
967
|
+
if (unionedPlans.length === 0) return buildCtsItemsPlan("cts:false-query()");
|
|
968
|
+
return unionedPlans.length === 1 ? unionedPlans[0] ?? buildCtsItemsPlan("cts:false-query()") : {
|
|
969
|
+
kind: "union",
|
|
970
|
+
children: unionedPlans
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function buildItemsPlan(context, query) {
|
|
900
974
|
if (isQueryLeaf(query)) {
|
|
901
975
|
if (query.target === "ocr") {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
976
|
+
const bindingName = registerOcrBinding(context, query);
|
|
977
|
+
return {
|
|
978
|
+
kind: "search",
|
|
979
|
+
itemPredicates: [query.isNegated === true ? `[not(@uuid = ${bindingName})]` : `[@uuid = ${bindingName}]`],
|
|
980
|
+
queryExpressions: []
|
|
981
|
+
};
|
|
905
982
|
}
|
|
906
983
|
const queryExpression = buildLeafQueryExpression(context, query);
|
|
907
|
-
return query.isNegated === true ? buildNotCtsQueryExpression(queryExpression) : queryExpression;
|
|
984
|
+
return buildCtsItemsPlan(query.isNegated === true ? buildNotCtsQueryExpression(queryExpression) : queryExpression);
|
|
908
985
|
}
|
|
909
986
|
const optimizedIncludesGroupQueries = getCompatibleIncludesGroupLeaves(query);
|
|
910
|
-
if (optimizedIncludesGroupQueries != null) return buildIncludesGroupQueryExpression(context, optimizedIncludesGroupQueries);
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
987
|
+
if (optimizedIncludesGroupQueries != null) return buildCtsItemsPlan(buildIncludesGroupQueryExpression(context, optimizedIncludesGroupQueries));
|
|
988
|
+
const childPlans = Array.from(getQueryGroupChildren(query), (childQuery) => buildItemsPlan(context, childQuery));
|
|
989
|
+
return getQueryGroupOperator(query) === "and" ? buildAndItemsPlan(childPlans) : buildOrItemsPlan(childPlans);
|
|
990
|
+
}
|
|
991
|
+
function collectItemsSearchPlans(plan, searchPlans) {
|
|
992
|
+
if (plan.kind === "search") {
|
|
993
|
+
searchPlans.push(plan);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
for (const childPlan of plan.children) collectItemsSearchPlans(childPlan, searchPlans);
|
|
997
|
+
}
|
|
998
|
+
function buildItemsPlanExpression(parameters) {
|
|
999
|
+
const { plan, baseItemsExpression, queryNamesByPlan } = parameters;
|
|
1000
|
+
if (plan.kind === "search") {
|
|
1001
|
+
const itemsExpression = `${baseItemsExpression}${plan.itemPredicates.join("")}`;
|
|
1002
|
+
const queryName = queryNamesByPlan.get(plan);
|
|
1003
|
+
return queryName == null ? itemsExpression : `cts:search(${itemsExpression}, ${queryName})`;
|
|
1004
|
+
}
|
|
1005
|
+
return `(${Array.from(plan.children, (childPlan) => buildItemsPlanExpression({
|
|
1006
|
+
plan: childPlan,
|
|
1007
|
+
baseItemsExpression,
|
|
1008
|
+
queryNamesByPlan
|
|
1009
|
+
})).join(plan.kind === "union" ? " | " : " intersect ")})`;
|
|
914
1010
|
}
|
|
915
1011
|
function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, belongsToCollectionPropertyVariableUuid) {
|
|
916
1012
|
if (belongsToCollectionScopeUuids.length === 0) return null;
|
|
@@ -923,22 +1019,59 @@ function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids,
|
|
|
923
1019
|
}))))
|
|
924
1020
|
});
|
|
925
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Compile a query tree into the XQuery `let` clauses that bind `$items` to the
|
|
1024
|
+
* matching Set items
|
|
1025
|
+
*
|
|
1026
|
+
* Most queries compile to a single `cts:search` over the Set item projections.
|
|
1027
|
+
* An `ocr` leaf cannot: the projections drop the `<ocr>` layer, so it resolves
|
|
1028
|
+
* to a search over the Resource documents whose matching UUIDs are joined back
|
|
1029
|
+
* in as an item path predicate. Path predicates only ever AND, so an `ocr` leaf
|
|
1030
|
+
* that sits under an `or` becomes its own arm of a node union instead, and one
|
|
1031
|
+
* that sits under an `and` alongside a union becomes an intersection.
|
|
1032
|
+
*
|
|
1033
|
+
* The searchable path has to stay inline in `cts:search`: binding it to a
|
|
1034
|
+
* variable first makes every query XDMP-UNSEARCHABLE.
|
|
1035
|
+
* @param parameters - The parameters for the compilation
|
|
1036
|
+
* @param parameters.queries - Recursive query tree to compile, if any
|
|
1037
|
+
* @param parameters.baseItemsExpression - The inline XQuery path selecting the items to search
|
|
1038
|
+
* @param parameters.scopeQueryExpression - An optional CTS query ANDed into every compiled search
|
|
1039
|
+
* @returns The prolog declaring the query helpers, and the `let` clauses binding `$items`
|
|
1040
|
+
*/
|
|
926
1041
|
function buildQueryPlan(parameters) {
|
|
927
|
-
const { queries } = parameters;
|
|
928
|
-
if (queries == null) return {
|
|
929
|
-
prolog: "",
|
|
930
|
-
queryExpression: null,
|
|
931
|
-
ocrBindings: [],
|
|
932
|
-
itemPredicates: ""
|
|
933
|
-
};
|
|
1042
|
+
const { queries, baseItemsExpression, scopeQueryExpression } = parameters;
|
|
934
1043
|
const context = createQueryCompilerContext();
|
|
935
|
-
const
|
|
1044
|
+
const plan = queries == null ? {
|
|
1045
|
+
kind: "search",
|
|
1046
|
+
itemPredicates: [],
|
|
1047
|
+
queryExpressions: []
|
|
1048
|
+
} : buildItemsPlan(context, queries);
|
|
1049
|
+
const searchPlans = [];
|
|
1050
|
+
collectItemsSearchPlans(plan, searchPlans);
|
|
1051
|
+
const boundSearchPlans = [];
|
|
1052
|
+
for (const searchPlan of searchPlans) {
|
|
1053
|
+
const queryExpression = buildAndCtsQueryExpression([...searchPlan.queryExpressions, ...scopeQueryExpression == null ? [] : [scopeQueryExpression]]);
|
|
1054
|
+
if (queryExpression != null) boundSearchPlans.push({
|
|
1055
|
+
plan: searchPlan,
|
|
1056
|
+
queryExpression
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
const queryNamesByPlan = /* @__PURE__ */ new Map();
|
|
1060
|
+
const letClauses = Array.from(context.ocrBindings, (binding) => `let ${binding.name} := ${binding.expression}`);
|
|
1061
|
+
for (const [index, boundSearchPlan] of boundSearchPlans.entries()) {
|
|
1062
|
+
const queryName = boundSearchPlans.length === 1 ? "$query" : `$query${index + 1}`;
|
|
1063
|
+
queryNamesByPlan.set(boundSearchPlan.plan, queryName);
|
|
1064
|
+
letClauses.push(`let ${queryName} := ${boundSearchPlan.queryExpression}`);
|
|
1065
|
+
}
|
|
1066
|
+
letClauses.push(`let $items := ${buildItemsPlanExpression({
|
|
1067
|
+
plan,
|
|
1068
|
+
baseItemsExpression,
|
|
1069
|
+
queryNamesByPlan
|
|
1070
|
+
})}`);
|
|
936
1071
|
return {
|
|
937
1072
|
prolog: context.helperDeclarations.join("\n\n"),
|
|
938
|
-
|
|
939
|
-
ocrBindings: context.ocrBindings,
|
|
940
|
-
itemPredicates: context.itemPredicates.join("")
|
|
1073
|
+
itemsClause: letClauses.join("\n ")
|
|
941
1074
|
};
|
|
942
1075
|
}
|
|
943
1076
|
//#endregion
|
|
944
|
-
export {
|
|
1077
|
+
export { buildBelongsToCollectionQueryExpression, buildQueryPlan };
|
package/dist/schemas.d.mts
CHANGED
|
@@ -38,7 +38,7 @@ declare const renderOptionsSchema: v.SchemaWithPipe<readonly [v.StringSchema<und
|
|
|
38
38
|
declare const setPropertyValuesParametersSchema: v.ObjectSchema<{
|
|
39
39
|
readonly setScopeUuids: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "Invalid pseudo-UUID">]>, undefined>, v.MinLengthAction<string[], 1, "At least one set scope UUID is required">]>;
|
|
40
40
|
readonly belongsToCollectionScopeUuids: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "Invalid pseudo-UUID">]>, undefined>, readonly []>;
|
|
41
|
-
readonly queries: v.OptionalSchema<v.NullableSchema<v.
|
|
41
|
+
readonly queries: v.OptionalSchema<v.NullableSchema<v.GenericSchema<unknown, Query>, undefined>, null>;
|
|
42
42
|
readonly attributes: v.OptionalSchema<v.ObjectSchema<{
|
|
43
43
|
readonly bibliographies: v.GenericSchema<unknown, boolean>;
|
|
44
44
|
readonly periods: v.GenericSchema<unknown, boolean>;
|
|
@@ -49,15 +49,14 @@ declare const setPropertyValuesParametersSchema: v.ObjectSchema<{
|
|
|
49
49
|
readonly isLimitedToLeafPropertyValues: v.GenericSchema<unknown, boolean>;
|
|
50
50
|
}, undefined>;
|
|
51
51
|
/**
|
|
52
|
-
* Schema for validating OCR
|
|
52
|
+
* Schema for validating the parameters for the item OCR data fetching function
|
|
53
53
|
* @internal
|
|
54
54
|
*/
|
|
55
|
-
declare const
|
|
56
|
-
readonly
|
|
57
|
-
readonly value: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.
|
|
55
|
+
declare const itemOcrDataParametersSchema: v.ObjectSchema<{
|
|
56
|
+
readonly uuid: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "Invalid pseudo-UUID">]>;
|
|
57
|
+
readonly value: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "A search value is required">]>;
|
|
58
58
|
readonly matchMode: v.OptionalSchema<v.PicklistSchema<["includes", "exact"], undefined>, "includes">;
|
|
59
59
|
readonly isCaseSensitive: v.GenericSchema<unknown, boolean>;
|
|
60
|
-
readonly maxMatchesPerItem: v.OptionalSchema<v.GenericSchema<unknown, number>, 50>;
|
|
61
60
|
}, undefined>;
|
|
62
61
|
/**
|
|
63
62
|
* Schema for validating Set items parameters
|
|
@@ -66,7 +65,7 @@ declare const ocrMatchesParametersSchema: v.ObjectSchema<{
|
|
|
66
65
|
declare const setItemsParametersSchema: v.ObjectSchema<{
|
|
67
66
|
readonly setScopeUuids: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "Invalid pseudo-UUID">]>, undefined>, v.MinLengthAction<string[], 1, "At least one set scope UUID is required">]>;
|
|
68
67
|
readonly belongsToCollectionScopeUuids: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.CheckAction<string, "Invalid pseudo-UUID">]>, undefined>, readonly []>;
|
|
69
|
-
readonly queries: v.OptionalSchema<v.NullableSchema<v.
|
|
68
|
+
readonly queries: v.OptionalSchema<v.NullableSchema<v.GenericSchema<unknown, Query>, undefined>, null>;
|
|
70
69
|
readonly sort: v.OptionalSchema<v.VariantSchema<"target", [v.StrictObjectSchema<{
|
|
71
70
|
readonly target: v.LiteralSchema<"none", undefined>;
|
|
72
71
|
}, undefined>, v.StrictObjectSchema<{
|
|
@@ -86,4 +85,4 @@ declare const setItemsParametersSchema: v.ObjectSchema<{
|
|
|
86
85
|
readonly pageSize: v.OptionalSchema<v.GenericSchema<unknown, number>, 48>;
|
|
87
86
|
}, undefined>;
|
|
88
87
|
//#endregion
|
|
89
|
-
export { componentSchema, gallerySchema, iso639_3Schema,
|
|
88
|
+
export { componentSchema, gallerySchema, iso639_3Schema, itemOcrDataParametersSchema, renderOptionsSchema, setItemsParametersSchema, setPropertyValuesParametersSchema, uuidSchema };
|
package/dist/schemas.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isPseudoUuid } from "./utilities.mjs";
|
|
2
2
|
import "./helpers.mjs";
|
|
3
|
-
import { OCR_DISJUNCTION_ERROR_MESSAGE, hasOcrQueryInDisjunction } from "./query.mjs";
|
|
4
3
|
import * as v from "valibot";
|
|
5
4
|
//#region src/schemas.ts
|
|
6
5
|
const positiveNumber = (message) => v.pipe(v.number(), v.minValue(1, message));
|
|
@@ -175,7 +174,7 @@ const setQuerySchema = v.lazy(() => v.union([
|
|
|
175
174
|
* Schema for validating Set queries
|
|
176
175
|
* @internal
|
|
177
176
|
*/
|
|
178
|
-
const setQueriesSchema = v.optional(v.nullable(
|
|
177
|
+
const setQueriesSchema = v.optional(v.nullable(setQuerySchema), null);
|
|
179
178
|
/**
|
|
180
179
|
* Schema for validating Set items sort
|
|
181
180
|
* @internal
|
|
@@ -222,15 +221,14 @@ const setPropertyValuesParametersSchema = v.object({
|
|
|
222
221
|
isLimitedToLeafPropertyValues: defaultBoolean(false)
|
|
223
222
|
});
|
|
224
223
|
/**
|
|
225
|
-
* Schema for validating OCR
|
|
224
|
+
* Schema for validating the parameters for the item OCR data fetching function
|
|
226
225
|
* @internal
|
|
227
226
|
*/
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
value: v.pipe(v.string(), v.
|
|
227
|
+
const itemOcrDataParametersSchema = v.object({
|
|
228
|
+
uuid: uuidSchema,
|
|
229
|
+
value: v.pipe(v.string(), v.check((value) => value.trim() !== "", "A search value is required")),
|
|
231
230
|
matchMode: v.optional(v.picklist(["includes", "exact"]), "includes"),
|
|
232
|
-
isCaseSensitive: defaultBoolean(false)
|
|
233
|
-
maxMatchesPerItem: v.optional(positiveNumber("Max matches per item must be positive"), 50)
|
|
231
|
+
isCaseSensitive: defaultBoolean(false)
|
|
234
232
|
});
|
|
235
233
|
/**
|
|
236
234
|
* Schema for validating Set items parameters
|
|
@@ -245,4 +243,4 @@ const setItemsParametersSchema = v.object({
|
|
|
245
243
|
pageSize: v.optional(positiveNumber("Page size must be positive"), 48)
|
|
246
244
|
});
|
|
247
245
|
//#endregion
|
|
248
|
-
export { componentSchema, gallerySchema, iso639_3Schema,
|
|
246
|
+
export { componentSchema, gallerySchema, iso639_3Schema, itemOcrDataParametersSchema, renderOptionsSchema, setItemsParametersSchema, setPropertyValuesParametersSchema, uuidSchema };
|
package/dist/types/index.d.mts
CHANGED
|
@@ -263,58 +263,29 @@ type ImageMap = {
|
|
|
263
263
|
height: number;
|
|
264
264
|
};
|
|
265
265
|
/**
|
|
266
|
-
*
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
y
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
*
|
|
274
|
-
*/
|
|
275
|
-
type OcrWord = {
|
|
276
|
-
content: string;
|
|
277
|
-
x: number;
|
|
278
|
-
y: number;
|
|
279
|
-
width: number;
|
|
280
|
-
height: number;
|
|
281
|
-
vertices: Array<OcrPoint>;
|
|
282
|
-
};
|
|
283
|
-
/**
|
|
284
|
-
* Text line of an OCR text block in OCHRE
|
|
285
|
-
*/
|
|
286
|
-
type OcrTextLine = {
|
|
287
|
-
content: string;
|
|
288
|
-
words: Array<OcrWord>;
|
|
289
|
-
};
|
|
290
|
-
/**
|
|
291
|
-
* Text block of an OCR page in OCHRE
|
|
292
|
-
*/
|
|
293
|
-
type OcrTextBlock = {
|
|
294
|
-
lines: Array<OcrTextLine>;
|
|
295
|
-
};
|
|
296
|
-
/**
|
|
297
|
-
* OCR page of a resource in OCHRE
|
|
298
|
-
*/
|
|
299
|
-
type OcrPage = {
|
|
300
|
-
number: number | null;
|
|
301
|
-
fileName: string | null;
|
|
302
|
-
width: number | null;
|
|
303
|
-
height: number | null;
|
|
304
|
-
blocks: Array<OcrTextBlock>;
|
|
305
|
-
};
|
|
306
|
-
/**
|
|
307
|
-
* A run of adjacent OCR words matching a search value in OCHRE
|
|
266
|
+
* Positioned OCR string in OCHRE
|
|
267
|
+
*
|
|
268
|
+
* OCHRE gives no guarantee about the node hierarchy inside a Resource's
|
|
269
|
+
* `<ocr>` layer, so only `<string>` nodes are parsed, at whatever depth they
|
|
270
|
+
* occur. `x` and `y` come from `HPOS` and `VPOS` and locate the top-left
|
|
271
|
+
* corner of the string's box, while `vertices` is its full bounding polygon,
|
|
272
|
+
* which is not necessarily rectangular. Every geometry attribute is optional
|
|
273
|
+
* in the source, so each one is null when absent or unparseable.
|
|
308
274
|
*
|
|
309
|
-
* `
|
|
310
|
-
*
|
|
275
|
+
* `resourceUuid` is the Resource that owns the OCR layer, which differs from
|
|
276
|
+
* the requested item when the OCR lives on a child Resource.
|
|
311
277
|
*/
|
|
312
|
-
type
|
|
313
|
-
uuid: string;
|
|
278
|
+
type OcrString = {
|
|
314
279
|
resourceUuid: string | null;
|
|
315
|
-
page: Omit<OcrPage, "blocks">;
|
|
316
280
|
content: string;
|
|
317
|
-
|
|
281
|
+
x: number | null;
|
|
282
|
+
y: number | null;
|
|
283
|
+
width: number | null;
|
|
284
|
+
height: number | null;
|
|
285
|
+
vertices: Array<{
|
|
286
|
+
x: number;
|
|
287
|
+
y: number;
|
|
288
|
+
}>;
|
|
318
289
|
};
|
|
319
290
|
/**
|
|
320
291
|
* Note in OCHRE
|
|
@@ -734,7 +705,6 @@ type Resource<T extends LanguageCodes = LanguageCodes, U extends ItemPayloadKind
|
|
|
734
705
|
image: Image<T> | null;
|
|
735
706
|
document: MultilingualString<T> | null;
|
|
736
707
|
imageMap: ImageMap | null;
|
|
737
|
-
ocr: Array<OcrPage>;
|
|
738
708
|
coordinates: Array<Coordinates<T>>;
|
|
739
709
|
periods: Array<Period<T, "embedded">>;
|
|
740
710
|
links: ItemLinks<T>;
|
|
@@ -845,12 +815,13 @@ type SetItemsSort = {
|
|
|
845
815
|
/**
|
|
846
816
|
* Represents a leaf query for Set items
|
|
847
817
|
*
|
|
848
|
-
* The `ocr` target matches the OCR text layer of
|
|
849
|
-
*
|
|
850
|
-
* than a CTS term,
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
818
|
+
* The `ocr` target matches the OCR text layer of Resource items. Set item
|
|
819
|
+
* projections do not carry `<ocr>`, so it is resolved by a document join rather
|
|
820
|
+
* than by a CTS term, and it composes with `and`, `or`, and `isNegated` at the
|
|
821
|
+
* cost of one extra search per distinct OCR value. Each `<string>` in that
|
|
822
|
+
* layer holds a single OCR word, so `includes` matches every search term as its
|
|
823
|
+
* own word and `exact` matches the terms as an adjacent run of whole words. OCR
|
|
824
|
+
* text carries no language, so `ocr` leaves take no `language`.
|
|
854
825
|
*/
|
|
855
826
|
type QueryLeaf = {
|
|
856
827
|
target: "property";
|
|
@@ -944,4 +915,4 @@ type QueryGroup = {
|
|
|
944
915
|
*/
|
|
945
916
|
type Query = QueryLeaf | QueryGroup;
|
|
946
917
|
//#endregion
|
|
947
|
-
export { 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,
|
|
918
|
+
export { 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, Prettify, 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, SetItemProperty, SetItemSimplifiedProperty, SetItemsSort, SetItemsSortDirection, SetPeriod, SetResource, SetSpatialUnit, SetTree, SimplifiedProperty, SpatialUnit, SpatialUnitItemLink, Text, TextItemLink, TopLevelItem, Tree, TreeItemCategory, TreeItemLink };
|
package/dist/utilities.d.mts
CHANGED
|
@@ -20,6 +20,30 @@ declare function isPseudoUuid(value: string): boolean;
|
|
|
20
20
|
* @returns The escaped string literal
|
|
21
21
|
*/
|
|
22
22
|
declare function stringLiteral(value: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* XQuery prolog declaring `local:omit-supplemental`, which drops every element
|
|
25
|
+
* carrying `supplemental="true"` from a node sequence, at any depth.
|
|
26
|
+
*
|
|
27
|
+
* Subtrees without a supplemental descendant are returned by reference, so
|
|
28
|
+
* nodes are only copied along the path leading to an omitted element. The
|
|
29
|
+
* lookahead walks the attribute axis (`//@supplemental`) rather than testing
|
|
30
|
+
* every element, which measures around three times faster on large documents.
|
|
31
|
+
*
|
|
32
|
+
* Must be declared before any query body that calls {@link omitSupplemental}.
|
|
33
|
+
*/
|
|
34
|
+
declare const SUPPLEMENTAL_XQUERY_PROLOG = "declare function local:omit-supplemental($nodes as node()*) as node()* {\n for $node in $nodes\n return\n if ($node instance of element())\n then\n if ($node/@supplemental = \"true\")\n then ()\n else if (empty($node//@supplemental[. = \"true\"]))\n then $node\n else element { node-name($node) } {\n $node/@*,\n local:omit-supplemental($node/node())\n }\n else $node\n};";
|
|
35
|
+
/**
|
|
36
|
+
* Wrap an XQuery node expression so supplemental nodes are omitted from it
|
|
37
|
+
* @param expression - The XQuery expression returning the nodes to filter
|
|
38
|
+
* @returns The wrapped XQuery expression
|
|
39
|
+
*/
|
|
40
|
+
declare function omitSupplemental(expression: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* XQuery predicate keeping only nodes that are neither supplemental themselves
|
|
43
|
+
* nor nested inside a supplemental node. Use it when aggregating over nodes
|
|
44
|
+
* instead of returning them.
|
|
45
|
+
*/
|
|
46
|
+
declare const NOT_SUPPLEMENTAL_PREDICATE = "[not(ancestor-or-self::*[@supplemental = \"true\"])]";
|
|
23
47
|
/**
|
|
24
48
|
* Flatten a properties array
|
|
25
49
|
* @param properties - The properties to flatten
|
|
@@ -28,4 +52,4 @@ declare function stringLiteral(value: string): string;
|
|
|
28
52
|
*/
|
|
29
53
|
declare function flattenProperties<T extends LanguageCodes = LanguageCodes>(properties: ReadonlyArray<Property<T> | SetItemProperty<T>>): Array<SetItemProperty<T>>;
|
|
30
54
|
//#endregion
|
|
31
|
-
export { createSchemaValidationError, flattenProperties, getErrorOutput, isPseudoUuid, stringLiteral };
|
|
55
|
+
export { NOT_SUPPLEMENTAL_PREDICATE, SUPPLEMENTAL_XQUERY_PROLOG, createSchemaValidationError, flattenProperties, getErrorOutput, isPseudoUuid, omitSupplemental, stringLiteral };
|