ochre-sdk 1.0.71 → 1.0.73

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,7 @@ 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";
15
+ const MAX_OCR_TEXT_CONDITIONS = 4;
20
16
  const CONTENT_TARGET_CONTENT_ELEMENT_PATHS = {
21
17
  title: [
22
18
  "identification",
@@ -204,9 +200,6 @@ function buildNestedElementQuery(elementNames, queryExpression) {
204
200
  function buildNotCtsQueryExpression(queryExpression) {
205
201
  return `cts:not-query(${queryExpression})`;
206
202
  }
207
- function buildCtsNearQueryExpression(queryExpressions) {
208
- return `cts:near-query((${queryExpressions.join(", ")}), ${queryExpressions.length - 1}, ("ordered"))`;
209
- }
210
203
  function buildAndCtsQueryExpressionInternal(queryExpressions) {
211
204
  if (queryExpressions.length === 0) return "cts:true-query()";
212
205
  if (queryExpressions.length === 1) return queryExpressions[0] ?? "cts:true-query()";
@@ -246,8 +239,7 @@ function buildRichTextContentQueryExpression(parameters) {
246
239
  const { value, matchMode, isCaseSensitive, language } = parameters;
247
240
  return buildAndCtsQueryExpressionInternal([buildContentLanguageQuery(language), matchMode === "exact" ? buildRichTextExactQueryExpression({
248
241
  value,
249
- isCaseSensitive,
250
- language
242
+ isCaseSensitive
251
243
  }) : buildCtsWordQueryExpression({
252
244
  value,
253
245
  matchMode,
@@ -480,62 +472,83 @@ 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) ?? [];
486
- const terms = [];
487
- for (const term of rawTerms) if (getWildcardStrippedValue(term) !== "") terms.push(term);
488
- return terms;
489
- }
490
- /**
491
- * Compile one CTS query expression per OCR search term, in word order
492
- *
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
496
- */
497
- function buildOcrTermQueryExpressions(parameters) {
498
- const { value, matchMode, isCaseSensitive } = parameters;
499
- const terms = tokenizeOcrSearchValue({
475
+ function buildOcrTextQueryExpression(query) {
476
+ const { value, matchMode, isCaseSensitive } = query;
477
+ const phraseQueryExpression = buildRichTextPhraseQueryExpression({
500
478
  value,
501
- matchMode,
502
479
  isCaseSensitive
503
480
  });
504
- const isWholeWordEquality = matchMode === "exact" && terms.length === 1;
505
- return Array.from(terms, (term) => isWholeWordEquality ? buildCtsElementValueQueryExpression({
506
- elementName: "string",
507
- value: term,
481
+ if (matchMode === "exact") return buildNestedElementQuery(["ocrText"], phraseQueryExpression);
482
+ const terms = tokenizeIncludesSearchValue({
483
+ value,
508
484
  isCaseSensitive
509
- }) : buildCtsWordQueryExpression({
485
+ });
486
+ if (terms.length === 0) return "cts:false-query()";
487
+ const tokenizedQueryExpression = buildAndCtsQueryExpressionInternal(Array.from(terms, (term) => buildCtsWordQueryExpression({
510
488
  value: term,
511
489
  matchMode,
512
490
  isCaseSensitive,
513
491
  queryFamily: "text"
514
- }));
515
- }
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()");
492
+ })));
493
+ return buildNestedElementQuery(["ocrText"], shouldUseFullValueFallbackForIncludes({
494
+ value,
495
+ isCaseSensitive,
496
+ terms
497
+ }) ? buildOrCtsQueryExpressionInternal([phraseQueryExpression, tokenizedQueryExpression]) : tokenizedQueryExpression);
520
498
  }
521
- function registerOcrItemPredicate(context, query) {
522
- const bindingKey = [
499
+ function getOcrTextConditionKey(query) {
500
+ return [
523
501
  query.value,
524
502
  query.matchMode,
525
503
  query.isCaseSensitive ? "case-sensitive" : "case-insensitive"
526
504
  ].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
- });
505
+ }
506
+ function registerOcrTextCondition(context, query) {
507
+ const key = getOcrTextConditionKey(query);
508
+ if (context.ocrTextConditionIndexesByKey.has(key)) return;
509
+ context.ocrTextConditionIndexesByKey.set(key, context.ocrTextConditions.length);
510
+ context.ocrTextConditions.push({
511
+ variableName: `$ocrTextUuids${context.ocrTextConditions.length + 1}`,
512
+ bindingExpression: `for $ocrTextDocument in cts:search(doc(), ${buildOcrTextQueryExpression(query)})\n return document-uri($ocrTextDocument)`
513
+ });
514
+ }
515
+ function collectOcrTextConditions(context, query) {
516
+ if (isQueryLeaf(query)) {
517
+ if (query.target === "ocrText") registerOcrTextCondition(context, query);
518
+ return;
519
+ }
520
+ for (const childQuery of getQueryGroupChildren(query)) collectOcrTextConditions(context, childQuery);
521
+ }
522
+ function isOcrTextLeafMatched(context, query, ocrTextValues) {
523
+ const conditionIndex = context.ocrTextConditionIndexesByKey.get(getOcrTextConditionKey(query));
524
+ const isMatched = conditionIndex != null && ocrTextValues[conditionIndex] === true;
525
+ return query.isNegated === true ? !isMatched : isMatched;
526
+ }
527
+ /**
528
+ * Enumerate every assignment of "this item is in the OCR text match set" across
529
+ * the compiled conditions, least significant position first
530
+ */
531
+ function getOcrTextValueCombinations(count) {
532
+ return Array.from({ length: 2 ** count }, (_, index) => Array.from({ length: count }, (_, position) => (index >> position & 1) === 1));
533
+ }
534
+ /**
535
+ * Resolve a query tree against one OCR text assignment, treating every CTS leaf
536
+ * as unknown. Only a definite `false` is actionable: it means the branch cannot
537
+ * match anything and can be dropped before it costs a `cts:search`.
538
+ */
539
+ function evaluateOcrTextBranch(context, query, ocrTextValues) {
540
+ if (isQueryLeaf(query)) return query.target === "ocrText" ? isOcrTextLeafMatched(context, query, ocrTextValues) : null;
541
+ const isAndGroup = "and" in query;
542
+ let result = isAndGroup;
543
+ for (const childQuery of getQueryGroupChildren(query)) {
544
+ const childResult = evaluateOcrTextBranch(context, childQuery, ocrTextValues);
545
+ if (childResult === !isAndGroup) return !isAndGroup;
546
+ if (childResult == null) result = null;
536
547
  }
537
- const itemPredicate = query.isNegated === true ? `[not(@uuid = ${variableName})]` : `[@uuid = ${variableName}]`;
538
- if (!context.itemPredicates.includes(itemPredicate)) context.itemPredicates.push(itemPredicate);
548
+ return result;
549
+ }
550
+ function buildOcrTextItemPredicates(context, ocrTextValues) {
551
+ return Array.from(context.ocrTextConditions, (condition, index) => ocrTextValues[index] === true ? `[@uuid = ${condition.variableName}]` : `[not(@uuid = ${condition.variableName})]`).join("");
539
552
  }
540
553
  function getLeafSearchValue(query) {
541
554
  switch (query.target) {
@@ -618,10 +631,8 @@ function createQueryCompilerContext() {
618
631
  nextHelperSerial: 1,
619
632
  helperNamesByKey: /* @__PURE__ */ new Map(),
620
633
  helperDeclarations: [],
621
- nextOcrSerial: 1,
622
- ocrVariableNamesByKey: /* @__PURE__ */ new Map(),
623
- ocrBindings: [],
624
- itemPredicates: []
634
+ ocrTextConditions: [],
635
+ ocrTextConditionIndexesByKey: /* @__PURE__ */ new Map()
625
636
  };
626
637
  }
627
638
  function registerConstantHelper(parameters) {
@@ -818,24 +829,11 @@ function getQueryGroupChildren(query) {
818
829
  function getQueryGroupOperator(query) {
819
830
  return "and" in query ? "and" : "or";
820
831
  }
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
832
  function getCompatibleIncludesGroupLeaves(query) {
835
833
  if (!("or" in query) || query.or.length <= 1) return null;
836
834
  const leafQueries = [];
837
835
  for (const childQuery of query.or) {
838
- if (!isQueryLeaf(childQuery) || childQuery.target === "ocr") return null;
836
+ if (!isQueryLeaf(childQuery) || childQuery.target === "ocrText") return null;
839
837
  leafQueries.push(childQuery);
840
838
  }
841
839
  const firstQuery = leafQueries[0];
@@ -896,20 +894,15 @@ function buildIncludesGroupQueryExpression(context, queries) {
896
894
  bodyExpression: buildOrCtsQueryExpressionInternal(exactMemberHelpers.map((helper) => helper.callExpression))
897
895
  }).callExpression, tokenizedQueryExpression]);
898
896
  }
899
- function buildQueryNode(context, query, isInDisjunction) {
897
+ function buildQueryNode(context, query, ocrTextValues) {
900
898
  if (isQueryLeaf(query)) {
901
- 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()";
905
- }
899
+ if (query.target === "ocrText") return isOcrTextLeafMatched(context, query, ocrTextValues) ? "cts:true-query()" : "cts:false-query()";
906
900
  const queryExpression = buildLeafQueryExpression(context, query);
907
901
  return query.isNegated === true ? buildNotCtsQueryExpression(queryExpression) : queryExpression;
908
902
  }
909
903
  const optimizedIncludesGroupQueries = getCompatibleIncludesGroupLeaves(query);
910
904
  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));
905
+ const childQueryExpressions = Array.from(getQueryGroupChildren(query), (childQuery) => buildQueryNode(context, childQuery, ocrTextValues));
913
906
  return (getQueryGroupOperator(query) === "and" ? buildAndCtsQueryExpressionInternal : buildOrCtsQueryExpressionInternal)(childQueryExpressions);
914
907
  }
915
908
  function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, belongsToCollectionPropertyVariableUuid) {
@@ -923,22 +916,47 @@ function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids,
923
916
  }))))
924
917
  });
925
918
  }
919
+ /**
920
+ * Compile a query tree into the CTS searches that resolve it
921
+ *
922
+ * OCR text is not carried by Set item projections, so an `ocrText` leaf cannot
923
+ * be a CTS term: it resolves to a document join whose UUID list can only be
924
+ * applied as an item path predicate, and path predicates only ever AND. To keep
925
+ * `ocrText` composable with `or` anyway, the tree is split on each distinct OCR
926
+ * text condition, one branch per assignment of "this item is in that match
927
+ * set". Every branch is a plain CTS search, and their union is the result.
928
+ * Branches that the assignment already rules out are dropped, so a query whose
929
+ * OCR text leaves are all conjunctive still compiles to a single search.
930
+ */
926
931
  function buildQueryPlan(parameters) {
927
932
  const { queries } = parameters;
928
933
  if (queries == null) return {
929
934
  prolog: "",
930
- queryExpression: null,
931
- ocrBindings: [],
932
- itemPredicates: ""
935
+ ocrTextBindings: [],
936
+ branches: [{
937
+ itemPredicates: "",
938
+ queryExpression: null
939
+ }]
933
940
  };
934
941
  const context = createQueryCompilerContext();
935
- const queryExpression = buildQueryNode(context, queries, false);
942
+ collectOcrTextConditions(context, queries);
943
+ if (context.ocrTextConditions.length > MAX_OCR_TEXT_CONDITIONS) throw new Error(`A query cannot contain more than ${MAX_OCR_TEXT_CONDITIONS} distinct OCR text searches`, { cause: context.ocrTextConditions.length });
944
+ const branches = [];
945
+ for (const ocrTextValues of getOcrTextValueCombinations(context.ocrTextConditions.length)) {
946
+ if (evaluateOcrTextBranch(context, queries, ocrTextValues) === false) continue;
947
+ branches.push({
948
+ itemPredicates: buildOcrTextItemPredicates(context, ocrTextValues),
949
+ queryExpression: buildQueryNode(context, queries, ocrTextValues)
950
+ });
951
+ }
936
952
  return {
937
953
  prolog: context.helperDeclarations.join("\n\n"),
938
- queryExpression,
939
- ocrBindings: context.ocrBindings,
940
- itemPredicates: context.itemPredicates.join("")
954
+ ocrTextBindings: Array.from(context.ocrTextConditions, (condition) => ({
955
+ name: condition.variableName,
956
+ expression: condition.bindingExpression
957
+ })),
958
+ branches
941
959
  };
942
960
  }
943
961
  //#endregion
944
- export { OCR_DISJUNCTION_ERROR_MESSAGE, buildAndCtsQueryExpression, buildBelongsToCollectionQueryExpression, buildOcrTermQueryExpressions, buildQueryPlan, hasOcrQueryInDisjunction };
962
+ export { buildAndCtsQueryExpression, 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));
@@ -143,7 +142,7 @@ const setQueryLeafSchema = v.union([
143
142
  ...standardQueryFields
144
143
  }),
145
144
  v.strictObject({
146
- target: v.literal("ocr"),
145
+ target: v.literal("ocrText"),
147
146
  value: v.string(),
148
147
  matchMode: standardQueryFields.matchMode,
149
148
  isCaseSensitive: standardQueryFields.isCaseSensitive,
@@ -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,12 @@ 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 `ocrText` target matches the OCR text layer of Resource items. Because
819
+ * Set item projections do not carry `<ocrText>`, it is resolved by a document
820
+ * join rather than a CTS term, so it composes with `and`, `or`, and `isNegated`
821
+ * at the cost of one extra search branch per distinct OCR text value in a
822
+ * disjunction. OCR text carries no language, so `ocrText` leaves take no
823
+ * `language`.
854
824
  */
855
825
  type QueryLeaf = {
856
826
  target: "property";
@@ -918,7 +888,7 @@ type QueryLeaf = {
918
888
  language: string;
919
889
  isNegated?: boolean;
920
890
  } | {
921
- target: "ocr";
891
+ target: "ocrText";
922
892
  value: string;
923
893
  matchMode: "includes" | "exact";
924
894
  isCaseSensitive: boolean;
@@ -944,4 +914,4 @@ type QueryGroup = {
944
914
  */
945
915
  type Query = QueryLeaf | QueryGroup;
946
916
  //#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 };
917
+ 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 };
@@ -151,6 +151,46 @@ function stringLiteral(value) {
151
151
  return `"${value.replaceAll("\"", "\"\"")}"`;
152
152
  }
153
153
  /**
154
+ * XQuery prolog declaring `local:omit-supplemental`, which drops every element
155
+ * carrying `supplemental="true"` from a node sequence, at any depth.
156
+ *
157
+ * Subtrees without a supplemental descendant are returned by reference, so
158
+ * nodes are only copied along the path leading to an omitted element. The
159
+ * lookahead walks the attribute axis (`//@supplemental`) rather than testing
160
+ * every element, which measures around three times faster on large documents.
161
+ *
162
+ * Must be declared before any query body that calls {@link omitSupplemental}.
163
+ */
164
+ const SUPPLEMENTAL_XQUERY_PROLOG = `declare function local:omit-supplemental($nodes as node()*) as node()* {
165
+ for $node in $nodes
166
+ return
167
+ if ($node instance of element())
168
+ then
169
+ if ($node/@supplemental = "true")
170
+ then ()
171
+ else if (empty($node//@supplemental[. = "true"]))
172
+ then $node
173
+ else element { node-name($node) } {
174
+ $node/@*,
175
+ local:omit-supplemental($node/node())
176
+ }
177
+ else $node
178
+ };`;
179
+ /**
180
+ * Wrap an XQuery node expression so supplemental nodes are omitted from it
181
+ * @param expression - The XQuery expression returning the nodes to filter
182
+ * @returns The wrapped XQuery expression
183
+ */
184
+ function omitSupplemental(expression) {
185
+ return `local:omit-supplemental(${expression})`;
186
+ }
187
+ /**
188
+ * XQuery predicate keeping only nodes that are neither supplemental themselves
189
+ * nor nested inside a supplemental node. Use it when aggregating over nodes
190
+ * instead of returning them.
191
+ */
192
+ const NOT_SUPPLEMENTAL_PREDICATE = "[not(ancestor-or-self::*[@supplemental = \"true\"])]";
193
+ /**
154
194
  * Flatten a properties array
155
195
  * @param properties - The properties to flatten
156
196
  * @returns The flattened properties
@@ -169,4 +209,4 @@ function flattenProperties(properties) {
169
209
  return result;
170
210
  }
171
211
  //#endregion
172
- export { createSchemaValidationError, flattenProperties, getErrorOutput, isPseudoUuid, stringLiteral };
212
+ export { NOT_SUPPLEMENTAL_PREDICATE, SUPPLEMENTAL_XQUERY_PROLOG, createSchemaValidationError, flattenProperties, getErrorOutput, isPseudoUuid, omitSupplemental, stringLiteral };
@@ -1,11 +1,6 @@
1
- import { XMLData as XMLData$1, XMLDataItem as XMLDataItem$1, XMLGalleryData as XMLGalleryData$1, XMLItemLinksData as XMLItemLinksData$1, XMLLink as XMLLink$1, XMLOcrMatchesData as XMLOcrMatchesData$1, XMLSetItemsData as XMLSetItemsData$1, XMLWebsiteData as XMLWebsiteData$1 } from "./types.mjs";
1
+ import { XMLData as XMLData$1, XMLDataItem as XMLDataItem$1, XMLGalleryData as XMLGalleryData$1, XMLItemLinksData as XMLItemLinksData$1, XMLLink as XMLLink$1, XMLSetItemsData as XMLSetItemsData$1, XMLWebsiteData as XMLWebsiteData$1 } from "./types.mjs";
2
2
  import * as v from "valibot";
3
3
  //#region src/xml/schemas.d.ts
4
- /**
5
- * Schema for validating OCR matches fetched from the OCHRE API
6
- * @internal
7
- */
8
- declare const XMLOcrMatchesData: v.GenericSchema<unknown, XMLOcrMatchesData$1>;
9
4
  declare const XMLLink: v.GenericSchema<unknown, XMLLink$1>;
10
5
  declare const XMLDataItem: v.GenericSchema<unknown, XMLDataItem$1>;
11
6
  declare const XMLItemLinksData: v.GenericSchema<unknown, XMLItemLinksData$1>;
@@ -14,4 +9,4 @@ declare const XMLSetItemsData: v.GenericSchema<unknown, XMLSetItemsData$1>;
14
9
  declare const XMLData: v.GenericSchema<unknown, XMLData$1>;
15
10
  declare const XMLWebsiteData: v.GenericSchema<unknown, XMLWebsiteData$1>;
16
11
  //#endregion
17
- export { XMLData, XMLDataItem, XMLGalleryData, XMLItemLinksData, XMLLink, XMLOcrMatchesData, XMLSetItemsData, XMLWebsiteData };
12
+ export { XMLData, XMLDataItem, XMLGalleryData, XMLItemLinksData, XMLLink, XMLSetItemsData, XMLWebsiteData };