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/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 tokenizeOcrSearchValue(parameters) {
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 rawTerms) if (getWildcardStrippedValue(term) !== "") terms.push(term);
477
+ for (const term of value.split(/\s+/u)) if (term !== "") terms.push(term);
488
478
  return terms;
489
479
  }
490
480
  /**
491
- * Compile one CTS query expression per OCR search term, in word order
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
- * Highlighting matches each OCR word against these same per-term expressions,
494
- * so hit locations always agree with what the filter matched.
495
- * @internal
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 buildOcrTermQueryExpressions(parameters) {
498
- const { value, matchMode, isCaseSensitive } = parameters;
499
- const terms = tokenizeOcrSearchValue({
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
- const isWholeWordEquality = matchMode === "exact" && terms.length === 1;
505
- return Array.from(terms, (term) => isWholeWordEquality ? buildCtsElementValueQueryExpression({
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
- }) : buildCtsWordQueryExpression({
510
- value: term,
511
- matchMode,
512
- isCaseSensitive,
513
- queryFamily: "text"
514
- }));
528
+ }))));
515
529
  }
516
- function buildOcrQueryExpression(query) {
517
- const termQueryExpressions = buildOcrTermQueryExpressions(query);
518
- if (termQueryExpressions.length === 0) return "cts:false-query()";
519
- return buildNestedElementQuery(["ocr"], termQueryExpressions.length > 1 ? buildCtsNearQueryExpression(termQueryExpressions) : termQueryExpressions[0] ?? "cts:false-query()");
520
- }
521
- function registerOcrItemPredicate(context, query) {
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
- let variableName = context.ocrVariableNamesByKey.get(bindingKey);
528
- if (variableName == null) {
529
- variableName = `$ocrUuids${context.nextOcrSerial}`;
530
- context.nextOcrSerial += 1;
531
- context.ocrVariableNamesByKey.set(bindingKey, variableName);
532
- context.ocrBindings.push({
533
- name: variableName,
534
- expression: `for $ocrDocument in cts:search(doc(), ${buildOcrQueryExpression(query)})\n return document-uri($ocrDocument)`
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
- nextOcrSerial: 1,
622
- ocrVariableNamesByKey: /* @__PURE__ */ new Map(),
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 buildQueryNode(context, query, isInDisjunction) {
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
- if (isInDisjunction) throw new Error(OCR_DISJUNCTION_ERROR_MESSAGE, { cause: query });
903
- registerOcrItemPredicate(context, query);
904
- return "cts:true-query()";
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 isChildInDisjunction = isInDisjunction || isDisjunctiveQueryGroup(query);
912
- const childQueryExpressions = Array.from(getQueryGroupChildren(query), (childQuery) => buildQueryNode(context, childQuery, isChildInDisjunction));
913
- return (getQueryGroupOperator(query) === "and" ? buildAndCtsQueryExpressionInternal : buildOrCtsQueryExpressionInternal)(childQueryExpressions);
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 queryExpression = buildQueryNode(context, queries, false);
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
- queryExpression,
939
- ocrBindings: context.ocrBindings,
940
- itemPredicates: context.itemPredicates.join("")
1073
+ itemsClause: letClauses.join("\n ")
941
1074
  };
942
1075
  }
943
1076
  //#endregion
944
- export { OCR_DISJUNCTION_ERROR_MESSAGE, buildAndCtsQueryExpression, buildBelongsToCollectionQueryExpression, buildOcrTermQueryExpressions, buildQueryPlan, hasOcrQueryInDisjunction };
1077
+ export { buildBelongsToCollectionQueryExpression, buildQueryPlan };
@@ -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.SchemaWithPipe<readonly [v.GenericSchema<unknown, Query>, v.CheckAction<Query, "OCR queries cannot be nested inside an OR group because they are resolved by a document join instead of a CTS query">]>, undefined>, null>;
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 matches parameters
52
+ * Schema for validating the parameters for the item OCR data fetching function
53
53
  * @internal
54
54
  */
55
- declare const ocrMatchesParametersSchema: v.ObjectSchema<{
56
- readonly uuids: 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 UUID is required">]>;
57
- readonly value: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, "A search value is required">]>;
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.SchemaWithPipe<readonly [v.GenericSchema<unknown, Query>, v.CheckAction<Query, "OCR queries cannot be nested inside an OR group because they are resolved by a document join instead of a CTS query">]>, undefined>, null>;
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, ocrMatchesParametersSchema, renderOptionsSchema, setItemsParametersSchema, setPropertyValuesParametersSchema, uuidSchema };
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(v.pipe(setQuerySchema, v.check((query) => !hasOcrQueryInDisjunction(query), OCR_DISJUNCTION_ERROR_MESSAGE))), null);
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 matches parameters
224
+ * Schema for validating the parameters for the item OCR data fetching function
226
225
  * @internal
227
226
  */
228
- const ocrMatchesParametersSchema = v.object({
229
- uuids: v.pipe(v.array(uuidSchema), v.minLength(1, "At least one UUID is required")),
230
- value: v.pipe(v.string(), v.minLength(1, "A search value is required")),
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, ocrMatchesParametersSchema, renderOptionsSchema, setItemsParametersSchema, setPropertyValuesParametersSchema, uuidSchema };
246
+ export { componentSchema, gallerySchema, iso639_3Schema, itemOcrDataParametersSchema, renderOptionsSchema, setItemsParametersSchema, setPropertyValuesParametersSchema, uuidSchema };
@@ -263,58 +263,29 @@ type ImageMap = {
263
263
  height: number;
264
264
  };
265
265
  /**
266
- * Vertex of an OCR word's bounding polygon in OCHRE
267
- */
268
- type OcrPoint = {
269
- x: number;
270
- y: number;
271
- };
272
- /**
273
- * Word recognized within an OCR text line in OCHRE
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
- * `uuid` is the requested resource, while `resourceUuid` is the resource that
310
- * owns the OCR page — they differ when the OCR lives on a child page resource.
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 OcrMatch = {
313
- uuid: string;
278
+ type OcrString = {
314
279
  resourceUuid: string | null;
315
- page: Omit<OcrPage, "blocks">;
316
280
  content: string;
317
- words: Array<OcrWord>;
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 resources. Because OCR is not
849
- * carried by Set item projections, it is resolved by a document join rather
850
- * than a CTS term, which means `ocr` leaves compose with `and` and `isNegated`
851
- * but cannot be nested inside an `or` group. Both match modes are adjacency
852
- * based: `includes` allows stemming and wildcards, `exact` requires whole OCR
853
- * words. OCR carries no language, so `ocr` leaves take no `language`.
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, OcrMatch, OcrPage, OcrPoint, OcrTextBlock, OcrTextLine, OcrWord, 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 };
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 };
@@ -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 };