ochre-sdk 1.0.78 → 1.1.0

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.
Files changed (84) hide show
  1. package/README.md +31 -3
  2. package/dist/_virtual/_rolldown/runtime.mjs +13 -0
  3. package/dist/categories.d.mts +249 -0
  4. package/dist/categories.mjs +259 -0
  5. package/dist/constants.d.mts +14 -0
  6. package/dist/constants.mjs +18 -1
  7. package/dist/errors.d.mts +23 -0
  8. package/dist/{utilities.mjs → errors.mjs} +33 -84
  9. package/dist/fetchers/gallery.mjs +15 -51
  10. package/dist/fetchers/item-children.mjs +20 -66
  11. package/dist/fetchers/item-links.mjs +26 -79
  12. package/dist/fetchers/item-ocr-data.d.mts +2 -2
  13. package/dist/fetchers/item-ocr-data.mjs +11 -15
  14. package/dist/fetchers/item.d.mts +0 -15
  15. package/dist/fetchers/item.mjs +20 -72
  16. package/dist/fetchers/request.d.mts +70 -0
  17. package/dist/fetchers/request.mjs +100 -0
  18. package/dist/fetchers/set/items.mjs +27 -73
  19. package/dist/fetchers/set/property-values.d.mts +2 -3
  20. package/dist/fetchers/set/property-values.mjs +96 -130
  21. package/dist/fetchers/website-metadata.mjs +35 -57
  22. package/dist/fetchers/website.d.mts +2 -3
  23. package/dist/fetchers/website.mjs +22 -31
  24. package/dist/getters.d.mts +78 -148
  25. package/dist/getters.mjs +127 -208
  26. package/dist/helpers.d.mts +0 -4
  27. package/dist/helpers.mjs +19 -6
  28. package/dist/index.d.mts +8 -6
  29. package/dist/index.mjs +6 -4
  30. package/dist/ocr.d.mts +37 -0
  31. package/dist/ocr.mjs +52 -0
  32. package/dist/parsers/helpers.d.mts +21 -1
  33. package/dist/parsers/helpers.mjs +26 -6
  34. package/dist/parsers/index.d.mts +0 -8
  35. package/dist/parsers/index.mjs +118 -259
  36. package/dist/parsers/languages.d.mts +72 -0
  37. package/dist/parsers/languages.mjs +132 -0
  38. package/dist/parsers/multilingual.d.mts +49 -74
  39. package/dist/parsers/multilingual.mjs +88 -189
  40. package/dist/parsers/property-token.d.mts +34 -0
  41. package/dist/parsers/property-token.mjs +29 -0
  42. package/dist/parsers/string.d.mts +19 -0
  43. package/dist/parsers/string.mjs +45 -25
  44. package/dist/parsers/website/bounds.d.mts +10 -0
  45. package/dist/parsers/website/bounds.mjs +28 -0
  46. package/dist/parsers/website/components.d.mts +91 -0
  47. package/dist/parsers/website/components.mjs +681 -0
  48. package/dist/parsers/website/index.d.mts +0 -7
  49. package/dist/parsers/website/index.mjs +92 -1153
  50. package/dist/parsers/website/links.d.mts +36 -0
  51. package/dist/parsers/website/links.mjs +58 -0
  52. package/dist/parsers/website/messages.d.mts +24 -0
  53. package/dist/parsers/website/messages.mjs +31 -0
  54. package/dist/parsers/website/options.d.mts +6 -0
  55. package/dist/parsers/website/options.mjs +114 -0
  56. package/dist/parsers/website/properties.d.mts +12 -0
  57. package/dist/parsers/website/properties.mjs +158 -0
  58. package/dist/parsers/website/reader.d.mts +54 -4
  59. package/dist/parsers/website/reader.mjs +65 -20
  60. package/dist/parsers/website/slug.d.mts +64 -0
  61. package/dist/parsers/website/slug.mjs +82 -0
  62. package/dist/parsers/website/styles.d.mts +28 -0
  63. package/dist/parsers/website/styles.mjs +103 -0
  64. package/dist/parsers/website/walk.d.mts +68 -0
  65. package/dist/parsers/website/walk.mjs +116 -0
  66. package/dist/query.d.mts +66 -18
  67. package/dist/query.mjs +202 -48
  68. package/dist/reflection.d.mts +64 -0
  69. package/dist/reflection.mjs +79 -0
  70. package/dist/schemas.d.mts +7 -0
  71. package/dist/schemas.mjs +12 -3
  72. package/dist/types/index.d.mts +1 -31
  73. package/dist/types/utilities.d.mts +9 -0
  74. package/dist/types/utilities.mjs +1 -0
  75. package/dist/types/website.d.mts +49 -58
  76. package/dist/xml/metadata.d.mts +16 -0
  77. package/dist/xml/metadata.mjs +32 -11
  78. package/dist/xml/schemas.d.mts +5970 -3
  79. package/dist/xml/schemas.mjs +43 -45
  80. package/dist/xml/types.d.mts +13 -30
  81. package/dist/xquery.d.mts +46 -0
  82. package/dist/xquery.mjs +66 -0
  83. package/package.json +3 -3
  84. package/dist/utilities.d.mts +0 -54
package/dist/query.mjs CHANGED
@@ -1,4 +1,6 @@
1
- import { stringLiteral } from "./utilities.mjs";
1
+ import { BELONGS_TO_COLLECTION_UUID } from "./constants.mjs";
2
+ import { compileOchreQuery, stringLiteral } from "./xquery.mjs";
3
+ import { OCR_WORD_CONTENT_ATTRIBUTE, OCR_WORD_QNAMES, buildOcrWordPath } from "./ocr.mjs";
2
4
  //#region src/query.ts
3
5
  const CTS_INCLUDES_STOP_WORDS = /* @__PURE__ */ new Set([
4
6
  "and",
@@ -40,6 +42,19 @@ const CONTENT_TARGET_CONTENT_ELEMENT_PATHS = {
40
42
  "content"
41
43
  ]
42
44
  };
45
+ const HELPER_SEARCH_VALUE_REFERENCE = "$value";
46
+ function searchValue(text) {
47
+ return {
48
+ text,
49
+ expression: stringLiteral(text)
50
+ };
51
+ }
52
+ function referencedSearchValue(text) {
53
+ return {
54
+ text,
55
+ expression: HELPER_SEARCH_VALUE_REFERENCE
56
+ };
57
+ }
43
58
  function tokenizeIncludesSearchValue(parameters) {
44
59
  const { value, isCaseSensitive } = parameters;
45
60
  const rawTerms = (isCaseSensitive ? value : value.toLowerCase()).match(CTS_INCLUDES_TOKEN_REGEX) ?? [];
@@ -116,9 +131,9 @@ function buildRichTextPhraseOptionsExpression(parameters) {
116
131
  }
117
132
  function buildCtsWordQueryExpression(parameters) {
118
133
  const { value, matchMode, isCaseSensitive, queryFamily, language } = parameters;
119
- const isWildcarded = matchMode === "includes" && hasWildcardCharacters(value);
120
- const isStemmed = matchMode === "includes" && queryFamily === "text" && !isWildcarded && shouldUseStemmedTextSearch(value);
121
- return `cts:word-query(${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
134
+ const isWildcarded = matchMode === "includes" && hasWildcardCharacters(value.text);
135
+ const isStemmed = matchMode === "includes" && queryFamily === "text" && !isWildcarded && shouldUseStemmedTextSearch(value.text);
136
+ return `cts:word-query(${value.expression}, ${buildWordQueryOptionsExpression({
122
137
  matchMode,
123
138
  isCaseSensitive,
124
139
  queryFamily,
@@ -129,7 +144,7 @@ function buildCtsWordQueryExpression(parameters) {
129
144
  }
130
145
  function buildRichTextPhraseQueryExpression(parameters) {
131
146
  const { value, isCaseSensitive } = parameters;
132
- return `cts:word-query(${stringLiteral(value)}, ${buildRichTextPhraseOptionsExpression({ isCaseSensitive })})`;
147
+ return `cts:word-query(${value.expression}, ${buildRichTextPhraseOptionsExpression({ isCaseSensitive })})`;
133
148
  }
134
149
  function buildRichTextExactQueryExpression(parameters) {
135
150
  const { value, isCaseSensitive } = parameters;
@@ -138,20 +153,20 @@ function buildRichTextExactQueryExpression(parameters) {
138
153
  isCaseSensitive
139
154
  });
140
155
  const terms = tokenizeExactTextSearchValue({
141
- value,
156
+ value: value.text,
142
157
  isCaseSensitive
143
158
  });
144
159
  if (terms.length <= 1) return phraseQuery;
145
160
  return buildOrCtsQueryExpressionInternal([phraseQuery, buildAndCtsQueryExpressionInternal(terms.map((term) => buildRichTextPhraseQueryExpression({
146
- value: term,
161
+ value: searchValue(term),
147
162
  isCaseSensitive
148
163
  })))]);
149
164
  }
150
165
  function buildCtsElementWordQueryExpression(parameters) {
151
166
  const { elementName, value, matchMode, isCaseSensitive, queryFamily, language } = parameters;
152
- const isWildcarded = matchMode === "includes" && hasWildcardCharacters(value);
153
- const isStemmed = matchMode === "includes" && queryFamily === "text" && !isWildcarded && shouldUseStemmedTextSearch(value);
154
- return `cts:element-word-query(xs:QName("${elementName}"), ${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
167
+ const isWildcarded = matchMode === "includes" && hasWildcardCharacters(value.text);
168
+ const isStemmed = matchMode === "includes" && queryFamily === "text" && !isWildcarded && shouldUseStemmedTextSearch(value.text);
169
+ return `cts:element-word-query(xs:QName("${elementName}"), ${value.expression}, ${buildWordQueryOptionsExpression({
155
170
  matchMode,
156
171
  isCaseSensitive,
157
172
  queryFamily,
@@ -162,9 +177,9 @@ function buildCtsElementWordQueryExpression(parameters) {
162
177
  }
163
178
  function buildCtsElementAttributeWordQueryExpression(parameters) {
164
179
  const { elementName, attributeName, value, matchMode, isCaseSensitive, queryFamily, language } = parameters;
165
- const isWildcarded = matchMode === "includes" && hasWildcardCharacters(value);
166
- const isStemmed = matchMode === "includes" && queryFamily === "text" && !isWildcarded && shouldUseStemmedTextSearch(value);
167
- return `cts:element-attribute-word-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
180
+ const isWildcarded = matchMode === "includes" && hasWildcardCharacters(value.text);
181
+ const isStemmed = matchMode === "includes" && queryFamily === "text" && !isWildcarded && shouldUseStemmedTextSearch(value.text);
182
+ return `cts:element-attribute-word-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${value.expression}, ${buildWordQueryOptionsExpression({
168
183
  matchMode,
169
184
  isCaseSensitive,
170
185
  queryFamily,
@@ -175,21 +190,21 @@ function buildCtsElementAttributeWordQueryExpression(parameters) {
175
190
  }
176
191
  function buildCtsElementValueQueryExpression(parameters) {
177
192
  const { elementName, value, isCaseSensitive } = parameters;
178
- return `cts:element-value-query(xs:QName("${elementName}"), ${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
193
+ return `cts:element-value-query(xs:QName("${elementName}"), ${value.expression}, ${buildWordQueryOptionsExpression({
179
194
  matchMode: "exact",
180
195
  isCaseSensitive
181
196
  })})`;
182
197
  }
183
198
  function buildCtsElementAttributeValueQueryExpression(parameters) {
184
199
  const { elementName, attributeName, value, isCaseSensitive } = parameters;
185
- return `cts:element-attribute-value-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
200
+ return `cts:element-attribute-value-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${value.expression}, ${buildWordQueryOptionsExpression({
186
201
  matchMode: "exact",
187
202
  isCaseSensitive
188
203
  })})`;
189
204
  }
190
205
  function buildPlainElementAttributeValueQueryExpression(parameters) {
191
206
  const { elementName, attributeName, value } = parameters;
192
- return `cts:element-attribute-value-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${stringLiteral(value)})`;
207
+ return `cts:element-attribute-value-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${value.expression})`;
193
208
  }
194
209
  function buildNestedElementQuery(elementNames, queryExpression) {
195
210
  let wrappedQueryExpression = queryExpression;
@@ -217,21 +232,21 @@ function buildContentLanguageQuery(language) {
217
232
  return buildPlainElementAttributeValueQueryExpression({
218
233
  elementName: "content",
219
234
  attributeName: "xml:lang",
220
- value: language
235
+ value: searchValue(language)
221
236
  });
222
237
  }
223
238
  function buildPropertyLabelQuery(propertyVariable) {
224
239
  return buildPlainElementAttributeValueQueryExpression({
225
240
  elementName: "label",
226
241
  attributeName: "uuid",
227
- value: propertyVariable
242
+ value: searchValue(propertyVariable)
228
243
  });
229
244
  }
230
245
  function buildValueNotIdReferenceQuery() {
231
246
  return buildNotCtsQueryExpression(buildPlainElementAttributeValueQueryExpression({
232
247
  elementName: "value",
233
248
  attributeName: "dataType",
234
- value: "IDREF"
249
+ value: searchValue("IDREF")
235
250
  }));
236
251
  }
237
252
  function buildRichTextContentQueryExpression(parameters) {
@@ -327,7 +342,7 @@ function buildPropertyQueryExpression(parameters) {
327
342
  if (propertyRelation != null) propertyQueryExpressions.unshift(buildPlainElementAttributeValueQueryExpression({
328
343
  elementName: "label",
329
344
  attributeName: "relation",
330
- value: propertyRelation
345
+ value: searchValue(propertyRelation)
331
346
  }));
332
347
  return buildNestedElementQuery(["properties", "property"], buildAndCtsQueryExpressionInternal(propertyQueryExpressions));
333
348
  }
@@ -354,7 +369,7 @@ function buildPropertyTextMatchQueryExpression(parameters) {
354
369
  if (propertyRelation != null) propertyQueryExpressions.push(buildPlainElementAttributeValueQueryExpression({
355
370
  elementName: "label",
356
371
  attributeName: "relation",
357
- value: propertyRelation
372
+ value: searchValue(propertyRelation)
358
373
  }));
359
374
  propertyQueryExpressions.push(buildNestedElementQuery(["value"], buildAndCtsQueryExpressionInternal(valueQueryExpressions)));
360
375
  const propertyQueryExpression = buildNestedElementQuery(["properties", "property"], buildAndCtsQueryExpressionInternal(propertyQueryExpressions));
@@ -472,7 +487,6 @@ function buildItemStringQueryExpression(parameters) {
472
487
  language
473
488
  })]);
474
489
  }
475
- const OCR_STRING_QNAMES = `(xs:QName("String"), fn:QName("http://www.loc.gov/standards/alto/ns-v2#", "string"))`;
476
490
  function tokenizeOcrExactValue(value) {
477
491
  const terms = [];
478
492
  for (const term of value.split(/\s+/u)) if (term !== "") terms.push(term);
@@ -492,12 +506,12 @@ function buildOcrWordQueryExpression(parameters) {
492
506
  "punctuation-insensitive",
493
507
  "whitespace-insensitive"
494
508
  ];
495
- if (hasWildcardCharacters(value)) options.push("wildcarded");
496
- return `cts:element-attribute-word-query(${OCR_STRING_QNAMES}, xs:QName("CONTENT"), ${stringLiteral(value)}, (${options.map((option) => stringLiteral(option)).join(", ")}))`;
509
+ if (hasWildcardCharacters(value.text)) options.push("wildcarded");
510
+ return `cts:element-attribute-word-query(${OCR_WORD_QNAMES}, xs:QName(${stringLiteral(OCR_WORD_CONTENT_ATTRIBUTE)}), ${value.expression}, (${options.map((option) => stringLiteral(option)).join(", ")}))`;
497
511
  }
498
512
  function buildOcrValueQueryExpression(parameters) {
499
513
  const { value, isCaseSensitive } = parameters;
500
- return `cts:element-attribute-value-query(${OCR_STRING_QNAMES}, xs:QName("CONTENT"), ${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
514
+ return `cts:element-attribute-value-query(${OCR_WORD_QNAMES}, xs:QName(${stringLiteral(OCR_WORD_CONTENT_ATTRIBUTE)}), ${value.expression}, ${buildWordQueryOptionsExpression({
501
515
  matchMode: "exact",
502
516
  isCaseSensitive
503
517
  })})`;
@@ -523,7 +537,7 @@ function buildOcrQueryExpression(query) {
523
537
  const terms = tokenizeOcrExactValue(value);
524
538
  if (terms.length === 0) return "cts:false-query()";
525
539
  return buildNestedElementQuery(["ocr"], buildAndCtsQueryExpressionInternal(Array.from(terms, (term) => buildOcrValueQueryExpression({
526
- value: term,
540
+ value: searchValue(term),
527
541
  isCaseSensitive
528
542
  }))));
529
543
  }
@@ -533,7 +547,7 @@ function buildOcrQueryExpression(query) {
533
547
  });
534
548
  if (terms.length === 0) return "cts:false-query()";
535
549
  return buildNestedElementQuery(["ocr"], buildAndCtsQueryExpressionInternal(Array.from(terms, (term) => buildOcrWordQueryExpression({
536
- value: term,
550
+ value: searchValue(term),
537
551
  isCaseSensitive
538
552
  }))));
539
553
  }
@@ -547,8 +561,8 @@ function registerOcrPhraseHelper(context) {
547
561
  context.helperNamesByKey.set(helperName, helperName);
548
562
  context.helperDeclarations.push(`declare function ${helperName}($resource as node(), $terms as xs:string*, $isCaseSensitive as xs:boolean) as xs:boolean {
549
563
  let $contents :=
550
- for $word in $resource//*[lower-case(local-name(.)) = "ocr"]//*[lower-case(local-name(.)) = "string"][@CONTENT]
551
- return if ($isCaseSensitive) then string($word/@CONTENT) else lower-case(string($word/@CONTENT))
564
+ for $word in ${buildOcrWordPath("$resource")}
565
+ return if ($isCaseSensitive) then string($word/@${OCR_WORD_CONTENT_ATTRIBUTE}) else lower-case(string($word/@${OCR_WORD_CONTENT_ATTRIBUTE}))
552
566
  let $needles :=
553
567
  for $term in $terms
554
568
  return if ($isCaseSensitive) then $term else lower-case($term)
@@ -578,7 +592,7 @@ function registerOcrBinding(context, query) {
578
592
  const queryExpression = buildOcrQueryExpression(query);
579
593
  const phraseTerms = query.matchMode === "exact" ? tokenizeOcrExactValue(query.value) : [];
580
594
  context.ocrBindingNamesByKey.set(key, name);
581
- const searchExpression = `cts:search(/ochre/resource, ${queryExpression})`;
595
+ const searchExpression = `cts:search(/ochre/resource, ${buildAndCtsQueryExpressionInternal([queryExpression, `cts:document-query(${context.baseItemsExpression}/@uuid/string())`])})`;
582
596
  const phraseHelperName = phraseTerms.length > 1 ? registerOcrPhraseHelper(context) : null;
583
597
  context.ocrBindings.push({
584
598
  name,
@@ -664,13 +678,14 @@ function indentBlock(value, spaces) {
664
678
  const prefix = " ".repeat(spaces);
665
679
  return value.split("\n").map((line) => line === "" ? line : `${prefix}${line}`).join("\n");
666
680
  }
667
- function createQueryCompilerContext() {
681
+ function createQueryCompilerContext(baseItemsExpression) {
668
682
  return {
669
683
  nextHelperSerial: 1,
670
684
  helperNamesByKey: /* @__PURE__ */ new Map(),
671
685
  helperDeclarations: [],
672
686
  ocrBindingNamesByKey: /* @__PURE__ */ new Map(),
673
- ocrBindings: []
687
+ ocrBindings: [],
688
+ baseItemsExpression
674
689
  };
675
690
  }
676
691
  function registerConstantHelper(parameters) {
@@ -689,9 +704,6 @@ function registerConstantHelper(parameters) {
689
704
  callExpression: `${helperName}()`
690
705
  };
691
706
  }
692
- function replaceSampleValueLiteral(expression, sampleValue, valueReference) {
693
- return expression.replaceAll(stringLiteral(sampleValue), () => valueReference);
694
- }
695
707
  function registerParameterizedHelper(parameters) {
696
708
  const { context, key, bodyExpression } = parameters;
697
709
  const existingName = context.helperNamesByKey.get(key);
@@ -749,7 +761,7 @@ function registerLeafHelper(parameters) {
749
761
  }),
750
762
  bodyExpression: buildLeafValueQueryExpression({
751
763
  query,
752
- value,
764
+ value: searchValue(value),
753
765
  matchMode
754
766
  })
755
767
  });
@@ -794,11 +806,11 @@ function registerIncludesLeafHelper(parameters) {
794
806
  query,
795
807
  value: sampleValue
796
808
  }),
797
- bodyExpression: replaceSampleValueLiteral(buildLeafValueQueryExpression({
809
+ bodyExpression: buildLeafValueQueryExpression({
798
810
  query,
799
- value: sampleValue,
811
+ value: referencedSearchValue(sampleValue),
800
812
  matchMode: "includes"
801
- }), sampleValue, "$value")
813
+ })
802
814
  });
803
815
  }
804
816
  function buildLeafQueryExpression(context, query) {
@@ -940,6 +952,23 @@ function buildCtsItemsPlan(queryExpression) {
940
952
  };
941
953
  }
942
954
  /**
955
+ * Plan a negated leaf as an item path predicate
956
+ *
957
+ * A Set holds every one of its items in one fragment, and CTS resolves
958
+ * `cts:not-query` from the fragment indexes without filtering the match down to
959
+ * the node it came from. Negating inside the search query would therefore drop
960
+ * every item of a Set as soon as one of them matched, and keep every item of a
961
+ * Set that held no match at all. `cts:contains` evaluates the leaf against one
962
+ * item projection instead, which is the scope the negation is asking about.
963
+ */
964
+ function buildNegatedItemsPlan(queryExpression) {
965
+ return {
966
+ kind: "search",
967
+ itemPredicates: [`[not(cts:contains(., ${queryExpression}))]`],
968
+ queryExpressions: []
969
+ };
970
+ }
971
+ /**
943
972
  * Splice the children of same-kind child plans into their parent, so that a
944
973
  * nested group of the same operator does not cost an extra search
945
974
  */
@@ -1021,7 +1050,7 @@ function buildItemsPlan(context, query) {
1021
1050
  };
1022
1051
  }
1023
1052
  const queryExpression = buildLeafQueryExpression(context, query);
1024
- return buildCtsItemsPlan(query.isNegated === true ? buildNotCtsQueryExpression(queryExpression) : queryExpression);
1053
+ return query.isNegated === true ? buildNegatedItemsPlan(queryExpression) : buildCtsItemsPlan(queryExpression);
1025
1054
  }
1026
1055
  const optimizedIncludesGroupQueries = getCompatibleIncludesGroupLeaves(query);
1027
1056
  if (optimizedIncludesGroupQueries != null) return buildCtsItemsPlan(buildIncludesGroupQueryExpression(context, optimizedIncludesGroupQueries));
@@ -1055,7 +1084,7 @@ function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids,
1055
1084
  queryExpression: buildNestedElementQuery(["value"], buildOrCtsQueryExpressionInternal(Array.from(belongsToCollectionScopeUuids, (uuid) => buildPlainElementAttributeValueQueryExpression({
1056
1085
  elementName: "value",
1057
1086
  attributeName: "uuid",
1058
- value: uuid
1087
+ value: searchValue(uuid)
1059
1088
  }))))
1060
1089
  });
1061
1090
  }
@@ -1066,9 +1095,12 @@ function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids,
1066
1095
  * Most queries compile to a single `cts:search` over the Set item projections.
1067
1096
  * An `ocr` leaf cannot: the projections drop the `<ocr>` layer, so it resolves
1068
1097
  * to a search over the Resource documents whose matching UUIDs are joined back
1069
- * in as an item path predicate. Path predicates only ever AND, so an `ocr` leaf
1070
- * that sits under an `or` becomes its own arm of a node union instead, and one
1071
- * that sits under an `and` alongside a union becomes an intersection.
1098
+ * in as an item path predicate. A negated leaf cannot either, because CTS
1099
+ * answers a negation for the whole fragment rather than for the item that
1100
+ * matched, so it is filtered per item instead. Path predicates only ever AND,
1101
+ * so such a leaf that sits under an `or` becomes its own arm of a node union
1102
+ * instead, and one that sits under an `and` alongside a union becomes an
1103
+ * intersection.
1072
1104
  *
1073
1105
  * The searchable path has to stay inline in `cts:search`: binding it to a
1074
1106
  * variable first makes every query XDMP-UNSEARCHABLE.
@@ -1078,9 +1110,34 @@ function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids,
1078
1110
  * @param parameters.scopeQueryExpression - An optional CTS query ANDed into every compiled search
1079
1111
  * @returns The prolog declaring the query helpers, and the `let` clauses binding `$items`
1080
1112
  */
1113
+ const ITEMS_VARIABLE = "$items";
1114
+ const SET_SCOPE_VARIABLE = "$setScopeUuids";
1115
+ /**
1116
+ * The XQuery path a Set item search runs over
1117
+ *
1118
+ * The path has to stay inline in `cts:search`: binding it to a variable first
1119
+ * materializes the sequence and makes every query `XDMP-UNSEARCHABLE`, even a
1120
+ * plain word query. It references {@link SET_SCOPE_VARIABLE}, which
1121
+ * {@link compileSetItemsQuery} declares.
1122
+ */
1123
+ const SET_ITEMS_EXPRESSION = `doc()/ochre/set[@uuid = ${SET_SCOPE_VARIABLE}]/items/*`;
1124
+ /**
1125
+ * Compile a query tree into the clauses that bind the matching Set items
1126
+ *
1127
+ * The returned `itemsClause` binds {@link ITEMS_VARIABLE} and has to be placed
1128
+ * inside an XQuery body, with `prolog` declared ahead of it.
1129
+ * {@link compileSetItemsQuery} does both and is what fetchers should use;
1130
+ * this is exposed for tests that assert on the compiled CTS.
1131
+ * @param parameters - The plan parameters
1132
+ * @param parameters.queries - The query tree to compile, or null to match every item
1133
+ * @param parameters.baseItemsExpression - The inline searchable path to filter
1134
+ * @param parameters.scopeQueryExpression - An extra query AND-ed into every search
1135
+ * @returns The prolog, the clauses binding the items, and the bound CTS queries
1136
+ * @internal
1137
+ */
1081
1138
  function buildQueryPlan(parameters) {
1082
1139
  const { queries, baseItemsExpression, scopeQueryExpression } = parameters;
1083
- const context = createQueryCompilerContext();
1140
+ const context = createQueryCompilerContext(baseItemsExpression);
1084
1141
  const plan = queries == null ? {
1085
1142
  kind: "search",
1086
1143
  itemPredicates: [],
@@ -1097,21 +1154,118 @@ function buildQueryPlan(parameters) {
1097
1154
  });
1098
1155
  }
1099
1156
  const queryNamesByPlan = /* @__PURE__ */ new Map();
1157
+ const queryBindings = [];
1100
1158
  const letClauses = Array.from(context.ocrBindings, (binding) => `let ${binding.name} := ${binding.expression}`);
1101
1159
  for (const [index, boundSearchPlan] of boundSearchPlans.entries()) {
1102
1160
  const queryName = boundSearchPlans.length === 1 ? "$query" : `$query${index + 1}`;
1103
1161
  queryNamesByPlan.set(boundSearchPlan.plan, queryName);
1162
+ queryBindings.push({
1163
+ name: queryName,
1164
+ expression: boundSearchPlan.queryExpression
1165
+ });
1104
1166
  letClauses.push(`let ${queryName} := ${boundSearchPlan.queryExpression}`);
1105
1167
  }
1106
- letClauses.push(`let $items := ${buildItemsPlanExpression({
1168
+ letClauses.push(`let ${ITEMS_VARIABLE} := ${buildItemsPlanExpression({
1107
1169
  plan,
1108
1170
  baseItemsExpression,
1109
1171
  queryNamesByPlan
1110
1172
  })}`);
1111
1173
  return {
1112
1174
  prolog: context.helperDeclarations.join("\n\n"),
1113
- itemsClause: letClauses.join("\n ")
1175
+ itemsClause: letClauses.join("\n "),
1176
+ itemsVariable: ITEMS_VARIABLE,
1177
+ queryBindings
1114
1178
  };
1115
1179
  }
1180
+ /**
1181
+ * Compile a Set item query into a complete XQuery document
1182
+ *
1183
+ * Owns everything a caller would otherwise have to know and restate: the
1184
+ * version declaration, the Set scope variable, the supplemental-stripping
1185
+ * prolog, the inline searchable path, where the compiled helper prolog goes and
1186
+ * that it is only declared when non-empty, the `<ochre>` wrapper, and the name
1187
+ * of the variable holding the matching items. The body receives that name.
1188
+ * @param parameters - The query parameters
1189
+ * @param parameters.setScopeUuids - The Set scope UUIDs to search within
1190
+ * @param parameters.belongsToCollectionScopeUuids - Collection scope UUIDs to narrow to
1191
+ * @param parameters.queries - The query tree to compile, or null to match every item
1192
+ * @param parameters.declarations - Extra prolog declarations, placed before the compiled prolog
1193
+ * @param parameters.body - Builds the body from the name of the variable holding the items
1194
+ * @returns A complete XQuery document
1195
+ * @internal
1196
+ */
1197
+ function compileSetItemsQuery(parameters) {
1198
+ const { setScopeUuids, belongsToCollectionScopeUuids, queries, declarations = [], body } = parameters;
1199
+ const plan = buildQueryPlan({
1200
+ queries,
1201
+ baseItemsExpression: SET_ITEMS_EXPRESSION,
1202
+ scopeQueryExpression: buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, BELONGS_TO_COLLECTION_UUID)
1203
+ });
1204
+ return compileOchreQuery({
1205
+ declarations: [
1206
+ ...declarations,
1207
+ `declare variable ${SET_SCOPE_VARIABLE} := (${Array.from(setScopeUuids, (uuid) => stringLiteral(uuid)).join(", ")});`,
1208
+ ...plan.prolog === "" ? [] : [plan.prolog]
1209
+ ],
1210
+ body: (context) => `<ochre>{
1211
+ ${plan.itemsClause}
1212
+ ${body({
1213
+ ...context,
1214
+ items: plan.itemsVariable
1215
+ })}
1216
+ }</ochre>`
1217
+ });
1218
+ }
1219
+ /**
1220
+ * Reduce a property-value facet query tree to the leaves that filter items
1221
+ *
1222
+ * A facet request carries property leaves that name a variable without naming
1223
+ * a value, which select what to aggregate rather than which items to keep.
1224
+ * Those are dropped, and groups left with a single child collapse into it.
1225
+ * @param queries - The query tree to reduce
1226
+ * @returns The reduced tree, or null when nothing filters items
1227
+ * @internal
1228
+ */
1229
+ function getItemFilterQueries(queries) {
1230
+ if (queries == null) return null;
1231
+ if (isQueryLeaf(queries)) {
1232
+ if (queries.target !== "property" || queries.dataType === "date" || queries.dataType === "dateTime") return queries;
1233
+ return "value" in queries && queries.value != null ? queries : null;
1234
+ }
1235
+ const filteredChildren = [];
1236
+ for (const childQuery of getQueryGroupChildren(queries)) {
1237
+ const filteredChildQuery = getItemFilterQueries(childQuery);
1238
+ if (filteredChildQuery != null) filteredChildren.push(filteredChildQuery);
1239
+ }
1240
+ if (filteredChildren.length <= 1) return filteredChildren[0] ?? null;
1241
+ return getQueryGroupOperator(queries) === "and" ? { and: filteredChildren } : { or: filteredChildren };
1242
+ }
1243
+ /**
1244
+ * Collect the property variables a query tree asks to be aggregated
1245
+ * @param queries - The query tree to walk
1246
+ * @returns One selector per distinct property variable and relation pair
1247
+ * @internal
1248
+ */
1249
+ function getPropertyFacetSelectors(queries) {
1250
+ if (queries == null) return [];
1251
+ const selectors = /* @__PURE__ */ new Map();
1252
+ const pendingQueries = [queries];
1253
+ while (pendingQueries.length > 0) {
1254
+ const query = pendingQueries.shift();
1255
+ if (query == null) continue;
1256
+ if (isQueryLeaf(query)) {
1257
+ if (query.target === "property" && query.propertyVariable != null) {
1258
+ const relation = query.propertyRelation ?? null;
1259
+ selectors.set(`${query.propertyVariable}|${relation}`, {
1260
+ uuid: query.propertyVariable,
1261
+ relation
1262
+ });
1263
+ }
1264
+ continue;
1265
+ }
1266
+ pendingQueries.push(...getQueryGroupChildren(query));
1267
+ }
1268
+ return selectors.values().toArray();
1269
+ }
1116
1270
  //#endregion
1117
- export { buildBelongsToCollectionQueryExpression, buildQueryPlan };
1271
+ export { buildBelongsToCollectionQueryExpression, buildQueryPlan, compileSetItemsQuery, getItemFilterQueries, getPropertyFacetSelectors };
@@ -0,0 +1,64 @@
1
+ //#region src/reflection.d.ts
2
+ /**
3
+ * Reading values whose shape is not known at compile time
4
+ *
5
+ * The SDK used to do this with five copies of a
6
+ * `value is Record<string, unknown>` predicate over
7
+ * `typeof value === "object" && value != null`. That predicate lies three ways:
8
+ * it is true for arrays, `Date`, `Map` and every other object; it claims a
9
+ * string index signature the value does not have, so every read produces
10
+ * `unknown` and callers add a second cast to get anything out of it; and being
11
+ * an `&&` of unrelated checks, its `false` branch does not mean "not a record",
12
+ * which is what a type predicate promises.
13
+ *
14
+ * These take `unknown` and hand back `unknown`. They assert nothing they have
15
+ * not checked, so the one unavoidable cast at the boundary lives here instead
16
+ * of being repeated at every call site. Where the shape *is* known, validate it
17
+ * with a valibot schema and `v.is` rather than reaching for these.
18
+ */
19
+ /**
20
+ * Whether a value is a non-null object
21
+ *
22
+ * Narrows to `object`, which is honest: arrays, `Date` and class instances are
23
+ * objects too. Callers that need to tell them apart still have to ask.
24
+ * @param value - The value to test
25
+ * @returns True when the value is a non-null object
26
+ * @internal
27
+ */
28
+ export declare function isObject(value: unknown): value is object;
29
+ /**
30
+ * Read one property from a value of unknown shape
31
+ *
32
+ * Uses `Reflect.get`, so there is no cast: a missing property, a non-object and
33
+ * a property holding `undefined` are all `undefined`, and the caller has to
34
+ * narrow whatever comes back.
35
+ * @param value - The value to read from
36
+ * @param key - The property to read
37
+ * @returns The property value, or undefined when there is none
38
+ * @internal
39
+ */
40
+ export declare function readProperty(value: unknown, key: PropertyKey): unknown;
41
+ /**
42
+ * Read the own enumerable string-keyed entries of a value of unknown shape
43
+ * @param value - The value to read from
44
+ * @returns The entries, or an empty array when the value is not an object
45
+ * @internal
46
+ */
47
+ export declare function readEntries(value: unknown): Array<[string, unknown]>;
48
+ /**
49
+ * Read a property that should hold an array, from a value of unknown shape
50
+ * @param value - The value to read from
51
+ * @param key - The property to read
52
+ * @returns The array, or an empty array when the property is absent or not one
53
+ * @internal
54
+ */
55
+ export declare function readArrayProperty(value: unknown, key: PropertyKey): ReadonlyArray<unknown>;
56
+ /**
57
+ * Read a property that should hold a string, from a value of unknown shape
58
+ * @param value - The value to read from
59
+ * @param key - The property to read
60
+ * @returns The string, or null when the property is absent or not one
61
+ * @internal
62
+ */
63
+ export declare function readStringProperty(value: unknown, key: PropertyKey): string | null;
64
+ //#endregion
@@ -0,0 +1,79 @@
1
+ //#region src/reflection.ts
2
+ /**
3
+ * Reading values whose shape is not known at compile time
4
+ *
5
+ * The SDK used to do this with five copies of a
6
+ * `value is Record<string, unknown>` predicate over
7
+ * `typeof value === "object" && value != null`. That predicate lies three ways:
8
+ * it is true for arrays, `Date`, `Map` and every other object; it claims a
9
+ * string index signature the value does not have, so every read produces
10
+ * `unknown` and callers add a second cast to get anything out of it; and being
11
+ * an `&&` of unrelated checks, its `false` branch does not mean "not a record",
12
+ * which is what a type predicate promises.
13
+ *
14
+ * These take `unknown` and hand back `unknown`. They assert nothing they have
15
+ * not checked, so the one unavoidable cast at the boundary lives here instead
16
+ * of being repeated at every call site. Where the shape *is* known, validate it
17
+ * with a valibot schema and `v.is` rather than reaching for these.
18
+ */
19
+ /**
20
+ * Whether a value is a non-null object
21
+ *
22
+ * Narrows to `object`, which is honest: arrays, `Date` and class instances are
23
+ * objects too. Callers that need to tell them apart still have to ask.
24
+ * @param value - The value to test
25
+ * @returns True when the value is a non-null object
26
+ * @internal
27
+ */
28
+ function isObject(value) {
29
+ return typeof value === "object" && value !== null;
30
+ }
31
+ /**
32
+ * Read one property from a value of unknown shape
33
+ *
34
+ * Uses `Reflect.get`, so there is no cast: a missing property, a non-object and
35
+ * a property holding `undefined` are all `undefined`, and the caller has to
36
+ * narrow whatever comes back.
37
+ * @param value - The value to read from
38
+ * @param key - The property to read
39
+ * @returns The property value, or undefined when there is none
40
+ * @internal
41
+ */
42
+ function readProperty(value, key) {
43
+ if (!isObject(value)) return;
44
+ return Reflect.get(value, key);
45
+ }
46
+ /**
47
+ * Read the own enumerable string-keyed entries of a value of unknown shape
48
+ * @param value - The value to read from
49
+ * @returns The entries, or an empty array when the value is not an object
50
+ * @internal
51
+ */
52
+ function readEntries(value) {
53
+ if (!isObject(value)) return [];
54
+ return Object.entries(value);
55
+ }
56
+ /**
57
+ * Read a property that should hold an array, from a value of unknown shape
58
+ * @param value - The value to read from
59
+ * @param key - The property to read
60
+ * @returns The array, or an empty array when the property is absent or not one
61
+ * @internal
62
+ */
63
+ function readArrayProperty(value, key) {
64
+ const property = readProperty(value, key);
65
+ return Array.isArray(property) ? property : [];
66
+ }
67
+ /**
68
+ * Read a property that should hold a string, from a value of unknown shape
69
+ * @param value - The value to read from
70
+ * @param key - The property to read
71
+ * @returns The string, or null when the property is absent or not one
72
+ * @internal
73
+ */
74
+ function readStringProperty(value, key) {
75
+ const property = readProperty(value, key);
76
+ return typeof property === "string" ? property : null;
77
+ }
78
+ //#endregion
79
+ export { isObject, readArrayProperty, readEntries, readProperty, readStringProperty };
@@ -1,6 +1,13 @@
1
1
  import { Query } from "./types/index.mjs";
2
2
  import * as v from "valibot";
3
3
  //#region src/schemas.d.ts
4
+ /**
5
+ * Validates a pseudo-UUID string
6
+ * @param value - The string to validate
7
+ * @returns True if the string is a valid pseudo-UUID, false otherwise
8
+ * @internal
9
+ */
10
+ export declare function isPseudoUuid(value: string): boolean;
4
11
  /**
5
12
  * Schema for validating UUIDs
6
13
  * @internal
package/dist/schemas.mjs CHANGED
@@ -1,7 +1,16 @@
1
- import { isPseudoUuid } from "./utilities.mjs";
2
- import "./helpers.mjs";
1
+ import "./constants.mjs";
3
2
  import * as v from "valibot";
4
3
  //#region src/schemas.ts
4
+ const PSEUDO_UUID_REGEX = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i;
5
+ /**
6
+ * Validates a pseudo-UUID string
7
+ * @param value - The string to validate
8
+ * @returns True if the string is a valid pseudo-UUID, false otherwise
9
+ * @internal
10
+ */
11
+ function isPseudoUuid(value) {
12
+ return PSEUDO_UUID_REGEX.test(value);
13
+ }
5
14
  const positiveNumber = (message) => v.pipe(v.number(), v.minValue(1, message));
6
15
  const defaultString = (value) => v.optional(v.string(), value);
7
16
  const defaultBoolean = (value) => v.optional(v.boolean(), value);
@@ -243,4 +252,4 @@ const setItemsParametersSchema = v.object({
243
252
  pageSize: v.optional(positiveNumber("Page size must be positive"), 48)
244
253
  });
245
254
  //#endregion
246
- export { componentSchema, gallerySchema, iso639_3Schema, itemOcrDataParametersSchema, renderOptionsSchema, setItemsParametersSchema, setPropertyValuesParametersSchema, uuidSchema };
255
+ export { componentSchema, gallerySchema, isPseudoUuid, iso639_3Schema, itemOcrDataParametersSchema, renderOptionsSchema, setItemsParametersSchema, setPropertyValuesParametersSchema, uuidSchema };