ochre-sdk 0.22.15 → 0.22.18

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/index.d.mts CHANGED
@@ -914,6 +914,7 @@ type Website = {
914
914
  isBibliographyDisplayed: boolean;
915
915
  isPropertyValuesGrouped: boolean;
916
916
  isPublicationDateTimeDisplayed: boolean;
917
+ isPersistentIdentifierDisplayed: boolean;
917
918
  iiifViewer: "universal-viewer" | "clover";
918
919
  };
919
920
  options: {
package/dist/index.mjs CHANGED
@@ -2155,7 +2155,7 @@ const CTS_INCLUDES_STOP_WORDS = new Set([
2155
2155
  ]);
2156
2156
  const CTS_INCLUDES_TOKEN_WORD_REGEX = /^\p{L}+$/u;
2157
2157
  const CTS_INCLUDES_TOKEN_REGEX = /[\p{L}\p{N}*?]+/gu;
2158
- const CTS_INCLUDES_TOKEN_SPLIT_REGEX = /\W+/u;
2158
+ const CTS_EXACT_TEXT_TOKEN_REGEX = /[\p{L}\p{N}]+/gu;
2159
2159
  const CONTENT_TARGET_CONTENT_ELEMENT_PATHS = {
2160
2160
  title: [
2161
2161
  "identification",
@@ -2198,9 +2198,9 @@ function tokenizeIncludesSearchValue(params) {
2198
2198
  }
2199
2199
  return terms;
2200
2200
  }
2201
- function tokenizeExactPhraseSearchValue(params) {
2201
+ function tokenizeExactTextSearchValue(params) {
2202
2202
  const { value, isCaseSensitive } = params;
2203
- const rawTerms = (isCaseSensitive ? value : value.toLowerCase()).split(CTS_INCLUDES_TOKEN_SPLIT_REGEX);
2203
+ const rawTerms = (isCaseSensitive ? value : value.toLowerCase()).match(CTS_EXACT_TEXT_TOKEN_REGEX) ?? [];
2204
2204
  const terms = [];
2205
2205
  for (const term of rawTerms) if (term !== "") terms.push(term);
2206
2206
  return terms;
@@ -2215,7 +2215,23 @@ function shouldUseStemmedTextSearch(value) {
2215
2215
  const wildcardStrippedValue = getWildcardStrippedValue(value);
2216
2216
  return wildcardStrippedValue.length >= 3 && CTS_INCLUDES_TOKEN_WORD_REGEX.test(wildcardStrippedValue);
2217
2217
  }
2218
- function buildCtsMatchOptionsExpression(params) {
2218
+ function shouldUseFullValueFallbackForIncludes(params) {
2219
+ const { value, isCaseSensitive, terms } = params;
2220
+ if (terms.length <= 1) return false;
2221
+ const tokenSource = isCaseSensitive ? value : value.toLowerCase();
2222
+ if (/[^\p{L}\p{N}\s*?]/u.test(tokenSource)) return true;
2223
+ const rawSpaceTerms = tokenSource.trim().split(/\s+/u).filter(Boolean);
2224
+ if (rawSpaceTerms.length !== terms.length) return true;
2225
+ for (const rawTerm of rawSpaceTerms) {
2226
+ const wildcardStrippedTerm = getWildcardStrippedValue(rawTerm);
2227
+ if (hasWildcardCharacters(rawTerm)) return true;
2228
+ if (!CTS_INCLUDES_TOKEN_WORD_REGEX.test(wildcardStrippedTerm)) return true;
2229
+ if (CTS_INCLUDES_STOP_WORDS.has(rawTerm.toLowerCase())) return true;
2230
+ }
2231
+ for (const [index, rawTerm] of rawSpaceTerms.entries()) if (rawTerm !== (terms[index] ?? "")) return true;
2232
+ return false;
2233
+ }
2234
+ function buildWordQueryOptionsExpression(params) {
2219
2235
  const { matchMode, isCaseSensitive, queryFamily, language, isWildcarded } = params;
2220
2236
  const { isStemmed } = params;
2221
2237
  const options = [
@@ -2228,14 +2244,25 @@ function buildCtsMatchOptionsExpression(params) {
2228
2244
  else if (queryFamily === "text") {
2229
2245
  options.push(isStemmed ? "stemmed" : "unstemmed", isWildcarded ? "wildcarded" : "unwildcarded");
2230
2246
  if (isStemmed && language != null && language !== "") options.push(`lang=${language}`);
2231
- } else options.push("unstemmed", isWildcarded ? "wildcarded" : "unwildcarded");
2247
+ }
2232
2248
  return `(${options.map((option) => stringLiteral(option)).join(", ")})`;
2233
2249
  }
2250
+ function buildRichTextPhraseOptionsExpression(params) {
2251
+ const { isCaseSensitive } = params;
2252
+ return `(${[
2253
+ isCaseSensitive ? "case-sensitive" : "case-insensitive",
2254
+ "diacritic-sensitive",
2255
+ "punctuation-insensitive",
2256
+ "whitespace-insensitive",
2257
+ "unstemmed",
2258
+ "unwildcarded"
2259
+ ].map((option) => stringLiteral(option)).join(", ")})`;
2260
+ }
2234
2261
  function buildCtsWordQueryExpression(params) {
2235
2262
  const { value, matchMode, isCaseSensitive, queryFamily, language } = params;
2236
2263
  const isWildcarded = matchMode === "includes" && hasWildcardCharacters(value);
2237
2264
  const isStemmed = matchMode === "includes" && queryFamily === "text" && !isWildcarded && shouldUseStemmedTextSearch(value);
2238
- return `cts:word-query(${stringLiteral(value)}, ${buildCtsMatchOptionsExpression({
2265
+ return `cts:word-query(${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
2239
2266
  matchMode,
2240
2267
  isCaseSensitive,
2241
2268
  queryFamily,
@@ -2244,11 +2271,43 @@ function buildCtsWordQueryExpression(params) {
2244
2271
  isStemmed
2245
2272
  })})`;
2246
2273
  }
2274
+ function buildRichTextPhraseQueryExpression(params) {
2275
+ const { value, isCaseSensitive } = params;
2276
+ return `cts:word-query(${stringLiteral(value)}, ${buildRichTextPhraseOptionsExpression({ isCaseSensitive })})`;
2277
+ }
2278
+ function buildCtsNearQueryExpression(params) {
2279
+ const { queryExpressions, distance, isOrdered = false } = params;
2280
+ const options = isOrdered ? `, (${stringLiteral("ordered")})` : "";
2281
+ return `cts:near-query((${queryExpressions.join(", ")}), ${distance}${options})`;
2282
+ }
2283
+ function buildRichTextExactQueryExpression(params) {
2284
+ const { value, isCaseSensitive, language } = params;
2285
+ const phraseQuery = buildRichTextPhraseQueryExpression({
2286
+ value,
2287
+ isCaseSensitive
2288
+ });
2289
+ const terms = tokenizeExactTextSearchValue({
2290
+ value,
2291
+ isCaseSensitive
2292
+ });
2293
+ if (terms.length <= 1) return phraseQuery;
2294
+ return buildOrCtsQueryExpressionInternal([phraseQuery, buildCtsNearQueryExpression({
2295
+ queryExpressions: terms.map((term) => buildCtsWordQueryExpression({
2296
+ value: term,
2297
+ matchMode: "exact",
2298
+ isCaseSensitive,
2299
+ queryFamily: "text",
2300
+ language
2301
+ })),
2302
+ distance: 2,
2303
+ isOrdered: true
2304
+ })]);
2305
+ }
2247
2306
  function buildCtsElementWordQueryExpression(params) {
2248
2307
  const { elementName, value, matchMode, isCaseSensitive, queryFamily, language } = params;
2249
2308
  const isWildcarded = matchMode === "includes" && hasWildcardCharacters(value);
2250
2309
  const isStemmed = matchMode === "includes" && queryFamily === "text" && !isWildcarded && shouldUseStemmedTextSearch(value);
2251
- return `cts:element-word-query(xs:QName("${elementName}"), ${stringLiteral(value)}, ${buildCtsMatchOptionsExpression({
2310
+ return `cts:element-word-query(xs:QName("${elementName}"), ${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
2252
2311
  matchMode,
2253
2312
  isCaseSensitive,
2254
2313
  queryFamily,
@@ -2261,7 +2320,7 @@ function buildCtsElementAttributeWordQueryExpression(params) {
2261
2320
  const { elementName, attributeName, value, matchMode, isCaseSensitive, queryFamily, language } = params;
2262
2321
  const isWildcarded = matchMode === "includes" && hasWildcardCharacters(value);
2263
2322
  const isStemmed = matchMode === "includes" && queryFamily === "text" && !isWildcarded && shouldUseStemmedTextSearch(value);
2264
- return `cts:element-attribute-word-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${stringLiteral(value)}, ${buildCtsMatchOptionsExpression({
2323
+ return `cts:element-attribute-word-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
2265
2324
  matchMode,
2266
2325
  isCaseSensitive,
2267
2326
  queryFamily,
@@ -2272,14 +2331,14 @@ function buildCtsElementAttributeWordQueryExpression(params) {
2272
2331
  }
2273
2332
  function buildCtsElementValueQueryExpression(params) {
2274
2333
  const { elementName, value, isCaseSensitive } = params;
2275
- return `cts:element-value-query(xs:QName("${elementName}"), ${stringLiteral(value)}, ${buildCtsMatchOptionsExpression({
2334
+ return `cts:element-value-query(xs:QName("${elementName}"), ${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
2276
2335
  matchMode: "exact",
2277
2336
  isCaseSensitive
2278
2337
  })})`;
2279
2338
  }
2280
2339
  function buildCtsElementAttributeValueQueryExpression(params) {
2281
2340
  const { elementName, attributeName, value, isCaseSensitive } = params;
2282
- return `cts:element-attribute-value-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${stringLiteral(value)}, ${buildCtsMatchOptionsExpression({
2341
+ return `cts:element-attribute-value-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${stringLiteral(value)}, ${buildWordQueryOptionsExpression({
2283
2342
  matchMode: "exact",
2284
2343
  isCaseSensitive
2285
2344
  })})`;
@@ -2288,39 +2347,6 @@ function buildPlainElementAttributeValueQueryExpression(params) {
2288
2347
  const { elementName, attributeName, value } = params;
2289
2348
  return `cts:element-attribute-value-query(xs:QName("${elementName}"), xs:QName("${attributeName}"), ${stringLiteral(value)})`;
2290
2349
  }
2291
- function buildSearchableContentTextQueryExpression(params) {
2292
- const { value, matchMode, isCaseSensitive, language } = params;
2293
- if (matchMode === "exact") {
2294
- const phraseTerms = tokenizeExactPhraseSearchValue({
2295
- value,
2296
- isCaseSensitive
2297
- });
2298
- if (phraseTerms.length > 1) return buildAndCtsQueryExpressionInternal(phraseTerms.map((term) => buildCtsWordQueryExpression({
2299
- value: term,
2300
- matchMode,
2301
- isCaseSensitive
2302
- })));
2303
- return buildCtsWordQueryExpression({
2304
- value,
2305
- matchMode,
2306
- isCaseSensitive
2307
- });
2308
- }
2309
- return buildOrCtsQueryExpressionInternal([buildCtsElementWordQueryExpression({
2310
- elementName: "string",
2311
- value,
2312
- matchMode,
2313
- isCaseSensitive,
2314
- queryFamily: "text",
2315
- language
2316
- }), buildCtsWordQueryExpression({
2317
- value,
2318
- matchMode,
2319
- isCaseSensitive,
2320
- queryFamily: "text",
2321
- language
2322
- })]);
2323
- }
2324
2350
  function buildNestedElementQuery(elementNames, queryExpression) {
2325
2351
  let wrappedQueryExpression = queryExpression;
2326
2352
  for (const elementName of elementNames.toReversed()) wrappedQueryExpression = `cts:element-query(xs:QName("${elementName}"), ${wrappedQueryExpression})`;
@@ -2371,14 +2397,28 @@ function buildValueNotIdRefQuery() {
2371
2397
  value: "IDREF"
2372
2398
  }));
2373
2399
  }
2374
- function buildValueContentInnerQuery(params) {
2375
- const { language, value, matchMode, isCaseSensitive } = params;
2376
- return buildNestedElementQuery(["content"], buildAndCtsQueryExpressionInternal([buildContentLanguageQuery(language), buildSearchableContentTextQueryExpression({
2400
+ function buildRichTextContentQueryExpression(params) {
2401
+ const { value, matchMode, isCaseSensitive, language } = params;
2402
+ return buildAndCtsQueryExpressionInternal([buildContentLanguageQuery(language), matchMode === "exact" ? buildRichTextExactQueryExpression({
2403
+ value,
2404
+ isCaseSensitive,
2405
+ language
2406
+ }) : buildCtsWordQueryExpression({
2377
2407
  value,
2378
2408
  matchMode,
2379
2409
  isCaseSensitive,
2410
+ queryFamily: "text",
2380
2411
  language
2381
- })]));
2412
+ })]);
2413
+ }
2414
+ function buildValueContentInnerQuery(params) {
2415
+ const { language, value, matchMode, isCaseSensitive } = params;
2416
+ return buildNestedElementQuery(["content"], buildRichTextContentQueryExpression({
2417
+ language,
2418
+ value,
2419
+ matchMode,
2420
+ isCaseSensitive
2421
+ }));
2382
2422
  }
2383
2423
  function buildValueDirectTextInnerQuery(params) {
2384
2424
  const { value, matchMode, isCaseSensitive } = params;
@@ -2418,35 +2458,22 @@ function buildNotesQueryExpression(params) {
2418
2458
  "notes",
2419
2459
  "note",
2420
2460
  "content"
2421
- ], buildAndCtsQueryExpressionInternal([buildContentLanguageQuery(language), buildOrCtsQueryExpressionInternal([matchMode === "exact" ? buildCtsElementAttributeValueQueryExpression({
2422
- elementName: "content",
2423
- attributeName: "title",
2424
- value,
2425
- isCaseSensitive
2426
- }) : buildCtsElementAttributeWordQueryExpression({
2427
- elementName: "content",
2428
- attributeName: "title",
2461
+ ], buildRichTextContentQueryExpression({
2429
2462
  value,
2430
2463
  matchMode,
2431
2464
  isCaseSensitive,
2432
- queryFamily: "text",
2433
2465
  language
2434
- }), buildSearchableContentTextQueryExpression({
2435
- value,
2436
- matchMode,
2437
- isCaseSensitive,
2438
- language
2439
- })])]));
2466
+ }));
2440
2467
  }
2441
2468
  function buildContentTargetQueryExpression(params) {
2442
2469
  const { target, value, matchMode, isCaseSensitive, language } = params;
2443
2470
  const contentElementPath = CONTENT_TARGET_CONTENT_ELEMENT_PATHS[target];
2444
- return buildNestedElementQuery(contentElementPath, buildAndCtsQueryExpressionInternal([buildContentLanguageQuery(language), buildSearchableContentTextQueryExpression({
2471
+ return buildNestedElementQuery(contentElementPath, buildRichTextContentQueryExpression({
2445
2472
  value,
2446
2473
  matchMode,
2447
2474
  isCaseSensitive,
2448
2475
  language
2449
- })]));
2476
+ }));
2450
2477
  }
2451
2478
  function buildPropertyQueryExpression(params) {
2452
2479
  const { propertyVariable, queryExpression } = params;
@@ -2454,6 +2481,31 @@ function buildPropertyQueryExpression(params) {
2454
2481
  if (propertyVariable != null) propertyQueryExpressions.unshift(buildPropertyLabelQuery(propertyVariable));
2455
2482
  return buildNestedElementQuery(["properties", "property"], buildAndCtsQueryExpressionInternal(propertyQueryExpressions));
2456
2483
  }
2484
+ function buildPropertyTextMatchQueryExpression(params) {
2485
+ const { propertyVariable, valueFilters = [], contentQueryExpression, rawValueQueryExpression, bareValueQueryExpression } = params;
2486
+ const letBindings = [];
2487
+ const valueMatchReferences = [];
2488
+ if (contentQueryExpression != null) {
2489
+ letBindings.push(`let $contentQuery := ${contentQueryExpression}`);
2490
+ valueMatchReferences.push("$contentQuery");
2491
+ }
2492
+ if (rawValueQueryExpression != null) {
2493
+ letBindings.push(`let $rawValueQuery := ${rawValueQueryExpression}`);
2494
+ valueMatchReferences.push("$rawValueQuery");
2495
+ }
2496
+ if (bareValueQueryExpression != null) {
2497
+ letBindings.push(`let $bareValueQuery := ${bareValueQueryExpression}`);
2498
+ valueMatchReferences.push("$bareValueQuery");
2499
+ }
2500
+ const valueQueryExpressions = [...valueFilters];
2501
+ if (valueMatchReferences.length > 0) valueQueryExpressions.push(buildOrCtsQueryExpressionInternal(valueMatchReferences));
2502
+ const propertyQueryExpressions = [];
2503
+ if (propertyVariable != null) propertyQueryExpressions.push(buildPropertyLabelQuery(propertyVariable));
2504
+ propertyQueryExpressions.push(buildNestedElementQuery(["value"], buildAndCtsQueryExpressionInternal(valueQueryExpressions)));
2505
+ const propertyQueryExpression = buildNestedElementQuery(["properties", "property"], buildAndCtsQueryExpressionInternal(propertyQueryExpressions));
2506
+ if (letBindings.length === 0) return propertyQueryExpression;
2507
+ return `(${letBindings.join("\n ")}\n return ${propertyQueryExpression})`;
2508
+ }
2457
2509
  function buildPropertyPresenceQueryExpression(params) {
2458
2510
  return buildPropertyQueryExpression({
2459
2511
  propertyVariable: params.propertyVariable,
@@ -2462,14 +2514,15 @@ function buildPropertyPresenceQueryExpression(params) {
2462
2514
  }
2463
2515
  function buildPropertyStringQueryExpression(params) {
2464
2516
  const { propertyVariable, value, matchMode, isCaseSensitive, language } = params;
2465
- return buildPropertyQueryExpression({
2517
+ return buildPropertyTextMatchQueryExpression({
2466
2518
  propertyVariable,
2467
- queryExpression: buildNestedElementQuery(["value"], buildAndCtsQueryExpressionInternal([buildValueNotInheritedQuery(), buildValueContentInnerQuery({
2519
+ valueFilters: [buildValueNotInheritedQuery()],
2520
+ contentQueryExpression: buildValueContentInnerQuery({
2468
2521
  language,
2469
2522
  value,
2470
2523
  matchMode,
2471
2524
  isCaseSensitive
2472
- })]))
2525
+ })
2473
2526
  });
2474
2527
  }
2475
2528
  function buildPropertyScalarQueryExpression(params) {
@@ -2489,34 +2542,25 @@ function buildPropertyScalarQueryExpression(params) {
2489
2542
  }
2490
2543
  function buildPropertyAllQueryExpression(params) {
2491
2544
  const { query, value, matchMode } = params;
2492
- if (matchMode === "includes") return buildPropertyQueryExpression({
2493
- propertyVariable: query.propertyVariable,
2494
- queryExpression: buildNestedElementQuery(["value"], buildAndCtsQueryExpressionInternal([buildValueNotIdRefQuery(), buildOrCtsQueryExpressionInternal([
2495
- buildValueContentInnerQuery({
2496
- language: query.language,
2497
- value,
2498
- matchMode,
2499
- isCaseSensitive: query.isCaseSensitive
2500
- }),
2501
- buildValueRawValueInnerQuery({
2502
- value,
2503
- matchMode,
2504
- isCaseSensitive: query.isCaseSensitive
2505
- }),
2506
- buildValueDirectTextInnerQuery({
2507
- value,
2508
- matchMode,
2509
- isCaseSensitive: query.isCaseSensitive
2510
- })
2511
- ])]))
2512
- });
2513
- return buildPropertyQueryExpression({
2545
+ return buildPropertyTextMatchQueryExpression({
2514
2546
  propertyVariable: query.propertyVariable,
2515
- queryExpression: buildNestedElementQuery(["value"], buildAndCtsQueryExpressionInternal([buildValueNotIdRefQuery(), buildCtsWordQueryExpression({
2547
+ valueFilters: [buildValueNotIdRefQuery()],
2548
+ contentQueryExpression: buildValueContentInnerQuery({
2549
+ language: query.language,
2516
2550
  value,
2517
2551
  matchMode,
2518
2552
  isCaseSensitive: query.isCaseSensitive
2519
- })]))
2553
+ }),
2554
+ rawValueQueryExpression: buildValueRawValueInnerQuery({
2555
+ value,
2556
+ matchMode,
2557
+ isCaseSensitive: query.isCaseSensitive
2558
+ }),
2559
+ bareValueQueryExpression: buildValueDirectTextInnerQuery({
2560
+ value,
2561
+ matchMode,
2562
+ isCaseSensitive: query.isCaseSensitive
2563
+ })
2520
2564
  });
2521
2565
  }
2522
2566
  function buildPropertyIdRefQueryExpression(params) {
@@ -2623,35 +2667,181 @@ function buildLeafValueQueryExpression(params) {
2623
2667
  }
2624
2668
  }
2625
2669
  }
2626
- function buildLeafQueryExpression(query) {
2670
+ function indentBlock(value, spaces) {
2671
+ const prefix = " ".repeat(spaces);
2672
+ return value.split("\n").map((line) => line === "" ? line : `${prefix}${line}`).join("\n");
2673
+ }
2674
+ function createQueryCompilerContext() {
2675
+ return {
2676
+ nextHelperSerial: 1,
2677
+ helperNamesByKey: /* @__PURE__ */ new Map(),
2678
+ helperDeclarations: []
2679
+ };
2680
+ }
2681
+ function registerConstantHelper(params) {
2682
+ const { context, key, bodyExpression } = params;
2683
+ const existingName = context.helperNamesByKey.get(key);
2684
+ if (existingName != null) return {
2685
+ name: existingName,
2686
+ callExpression: `${existingName}()`
2687
+ };
2688
+ const helperName = `local:queryHelper${context.nextHelperSerial}`;
2689
+ context.nextHelperSerial += 1;
2690
+ context.helperNamesByKey.set(key, helperName);
2691
+ context.helperDeclarations.push(`declare function ${helperName}() as cts:query {\n${indentBlock(bodyExpression, 2)}\n};`);
2692
+ return {
2693
+ name: helperName,
2694
+ callExpression: `${helperName}()`
2695
+ };
2696
+ }
2697
+ function replaceSampleValueLiteral(expression, sampleValue, valueReference) {
2698
+ return expression.replaceAll(stringLiteral(sampleValue), valueReference);
2699
+ }
2700
+ function registerParameterizedHelper(params) {
2701
+ const { context, key, bodyExpression } = params;
2702
+ const existingName = context.helperNamesByKey.get(key);
2703
+ if (existingName != null) return {
2704
+ name: existingName,
2705
+ call: (valueExpression) => `${existingName}(${valueExpression})`
2706
+ };
2707
+ const helperName = `local:queryHelper${context.nextHelperSerial}`;
2708
+ context.nextHelperSerial += 1;
2709
+ context.helperNamesByKey.set(key, helperName);
2710
+ context.helperDeclarations.push(`declare function ${helperName}($value as xs:string) as cts:query {\n${indentBlock(bodyExpression, 2)}\n};`);
2711
+ return {
2712
+ name: helperName,
2713
+ call: (valueExpression) => `${helperName}(${valueExpression})`
2714
+ };
2715
+ }
2716
+ function getLeafHelperKey(params) {
2717
+ const { query, matchMode, value } = params;
2718
+ switch (query.target) {
2719
+ case "string":
2720
+ case "title":
2721
+ case "description":
2722
+ case "image":
2723
+ case "periods":
2724
+ case "bibliography":
2725
+ case "notes": return [
2726
+ "leaf",
2727
+ matchMode,
2728
+ query.target,
2729
+ value,
2730
+ query.isCaseSensitive ? "case-sensitive" : "case-insensitive",
2731
+ query.language
2732
+ ].join("|");
2733
+ case "property": return [
2734
+ "leaf",
2735
+ matchMode,
2736
+ query.target,
2737
+ query.dataType,
2738
+ query.propertyVariable ?? "",
2739
+ value,
2740
+ query.isCaseSensitive ? "case-sensitive" : "case-insensitive",
2741
+ query.language
2742
+ ].join("|");
2743
+ }
2744
+ }
2745
+ function registerLeafHelper(params) {
2746
+ const { context, query, matchMode, value } = params;
2747
+ return registerConstantHelper({
2748
+ context,
2749
+ key: getLeafHelperKey({
2750
+ query,
2751
+ matchMode,
2752
+ value
2753
+ }),
2754
+ bodyExpression: buildLeafValueQueryExpression({
2755
+ query,
2756
+ value,
2757
+ matchMode
2758
+ })
2759
+ });
2760
+ }
2761
+ function getIncludesLeafHelperKey(params) {
2762
+ const { query, value } = params;
2763
+ const isWildcarded = hasWildcardCharacters(value);
2764
+ const isStemmed = !isWildcarded && shouldUseStemmedTextSearch(value);
2765
+ switch (query.target) {
2766
+ case "string":
2767
+ case "title":
2768
+ case "description":
2769
+ case "image":
2770
+ case "periods":
2771
+ case "bibliography":
2772
+ case "notes": return [
2773
+ "includes-helper",
2774
+ query.target,
2775
+ query.isCaseSensitive ? "case-sensitive" : "case-insensitive",
2776
+ query.language,
2777
+ isWildcarded ? "wildcarded" : "unwildcarded",
2778
+ isStemmed ? "stemmed" : "unstemmed"
2779
+ ].join("|");
2780
+ case "property": return [
2781
+ "includes-helper",
2782
+ query.target,
2783
+ query.dataType,
2784
+ query.propertyVariable ?? "",
2785
+ query.isCaseSensitive ? "case-sensitive" : "case-insensitive",
2786
+ query.language,
2787
+ isWildcarded ? "wildcarded" : "unwildcarded",
2788
+ isStemmed ? "stemmed" : "unstemmed"
2789
+ ].join("|");
2790
+ }
2791
+ }
2792
+ function registerIncludesLeafHelper(params) {
2793
+ const { context, query, sampleValue } = params;
2794
+ return registerParameterizedHelper({
2795
+ context,
2796
+ key: getIncludesLeafHelperKey({
2797
+ query,
2798
+ value: sampleValue
2799
+ }),
2800
+ bodyExpression: replaceSampleValueLiteral(buildLeafValueQueryExpression({
2801
+ query,
2802
+ value: sampleValue,
2803
+ matchMode: "includes"
2804
+ }), sampleValue, "$value")
2805
+ });
2806
+ }
2807
+ function buildLeafQueryExpression(context, query) {
2627
2808
  if (query.target === "property" && query.dataType !== "date" && query.dataType !== "dateTime" && !("value" in query) && query.propertyVariable != null) return buildPropertyPresenceQueryExpression({ propertyVariable: query.propertyVariable });
2628
2809
  if (query.target === "property" && (query.dataType === "date" || query.dataType === "dateTime") && query.value == null) return buildPropertyDateRangeQueryExpression(query);
2629
2810
  const searchValue = getLeafSearchValue(query);
2630
2811
  if (searchValue == null) throw new Error("Missing searchable value for query leaf");
2631
- if (query.matchMode === "exact") return buildLeafValueQueryExpression({
2812
+ const exactHelper = registerLeafHelper({
2813
+ context,
2632
2814
  query,
2633
- value: searchValue,
2634
- matchMode: "exact"
2815
+ matchMode: "exact",
2816
+ value: searchValue
2635
2817
  });
2818
+ if (query.matchMode === "exact") return exactHelper.callExpression;
2636
2819
  const terms = tokenizeIncludesSearchValue({
2637
2820
  value: searchValue,
2638
2821
  isCaseSensitive: query.isCaseSensitive
2639
2822
  });
2640
- const fullValueQueryExpression = buildLeafValueQueryExpression({
2641
- query,
2642
- value: searchValue,
2643
- matchMode: "exact"
2644
- });
2645
2823
  if (terms.length === 0) return "cts:false-query()";
2646
- const termQueryExpressions = [];
2647
- for (const term of terms) termQueryExpressions.push(buildLeafValueQueryExpression({
2824
+ const includesHelper = registerIncludesLeafHelper({
2825
+ context,
2648
2826
  query,
2649
- value: term,
2650
- matchMode: "includes"
2651
- }));
2652
- const tokenizedQueryExpression = buildAndCtsQueryExpressionInternal(termQueryExpressions);
2653
- if (terms.length === 1) return tokenizedQueryExpression;
2654
- return buildOrCtsQueryExpressionInternal([fullValueQueryExpression, tokenizedQueryExpression]);
2827
+ sampleValue: terms[0] ?? ""
2828
+ });
2829
+ const tokenizedHelperCalls = [];
2830
+ for (const term of terms) {
2831
+ const termHelper = term === (terms[0] ?? "") ? includesHelper : registerIncludesLeafHelper({
2832
+ context,
2833
+ query,
2834
+ sampleValue: term
2835
+ });
2836
+ tokenizedHelperCalls.push(termHelper.call(stringLiteral(term)));
2837
+ }
2838
+ const tokenizedQueryExpression = buildAndCtsQueryExpressionInternal(tokenizedHelperCalls);
2839
+ if (!shouldUseFullValueFallbackForIncludes({
2840
+ value: searchValue,
2841
+ isCaseSensitive: query.isCaseSensitive,
2842
+ terms
2843
+ })) return tokenizedQueryExpression;
2844
+ return buildOrCtsQueryExpressionInternal([exactHelper.callExpression, tokenizedQueryExpression]);
2655
2845
  }
2656
2846
  function getGroupableIncludesValue(query) {
2657
2847
  if (query.matchMode !== "includes" || query.isNegated === true) return null;
@@ -2691,21 +2881,7 @@ function getCompatibleIncludesGroupLeaves(query) {
2691
2881
  for (const leafQuery of leafQueries) if (getGroupableIncludesValue(leafQuery) !== groupValue || leafQuery.isCaseSensitive !== firstQuery.isCaseSensitive || leafQuery.language !== firstQuery.language) return null;
2692
2882
  return leafQueries;
2693
2883
  }
2694
- function buildIncludesGroupMember(query) {
2695
- return {
2696
- buildTermQuery: (term) => buildLeafValueQueryExpression({
2697
- query,
2698
- value: term,
2699
- matchMode: "includes"
2700
- }),
2701
- buildFullValueQuery: (value) => buildLeafValueQueryExpression({
2702
- query,
2703
- value,
2704
- matchMode: "exact"
2705
- })
2706
- };
2707
- }
2708
- function buildIncludesGroupQueryExpression(queries) {
2884
+ function buildIncludesGroupQueryExpression(context, queries) {
2709
2885
  const firstQuery = queries[0];
2710
2886
  if (firstQuery == null) throw new Error("Cannot build an includes group without queries");
2711
2887
  const groupValue = getGroupableIncludesValue(firstQuery);
@@ -2715,28 +2891,56 @@ function buildIncludesGroupQueryExpression(queries) {
2715
2891
  isCaseSensitive: firstQuery.isCaseSensitive
2716
2892
  });
2717
2893
  if (terms.length === 0) return "cts:false-query()";
2718
- const members = queries.map((query) => buildIncludesGroupMember(query));
2719
- const perTermQueryExpressions = [];
2720
- const fullValueFieldQueryExpressions = [];
2721
- for (const member of members) fullValueFieldQueryExpressions.push(member.buildFullValueQuery(groupValue));
2894
+ const tokenizedHelperCalls = [];
2722
2895
  for (const term of terms) {
2723
- const fieldQueryExpressions = [];
2724
- for (const member of members) fieldQueryExpressions.push(member.buildTermQuery(term));
2725
- perTermQueryExpressions.push(buildOrCtsQueryExpressionInternal(fieldQueryExpressions));
2896
+ const memberHelpers = queries.map((query) => registerIncludesLeafHelper({
2897
+ context,
2898
+ query,
2899
+ sampleValue: term
2900
+ }));
2901
+ const termGroupHelper = registerParameterizedHelper({
2902
+ context,
2903
+ key: [
2904
+ "group",
2905
+ "includes",
2906
+ ...memberHelpers.map((helper) => helper.name)
2907
+ ].join("|"),
2908
+ bodyExpression: buildOrCtsQueryExpressionInternal(memberHelpers.map((helper) => helper.call("$value")))
2909
+ });
2910
+ tokenizedHelperCalls.push(termGroupHelper.call(stringLiteral(term)));
2726
2911
  }
2727
- const tokenizedGroupQueryExpression = buildAndCtsQueryExpressionInternal(perTermQueryExpressions);
2728
- if (terms.length === 1) return tokenizedGroupQueryExpression;
2729
- return buildOrCtsQueryExpressionInternal([buildOrCtsQueryExpressionInternal(fullValueFieldQueryExpressions), tokenizedGroupQueryExpression]);
2730
- }
2731
- function buildQueryNode(query) {
2912
+ const tokenizedQueryExpression = buildAndCtsQueryExpressionInternal(tokenizedHelperCalls);
2913
+ if (!shouldUseFullValueFallbackForIncludes({
2914
+ value: groupValue,
2915
+ isCaseSensitive: firstQuery.isCaseSensitive,
2916
+ terms
2917
+ })) return tokenizedQueryExpression;
2918
+ const exactMemberHelpers = queries.map((query) => registerLeafHelper({
2919
+ context,
2920
+ query,
2921
+ matchMode: "exact",
2922
+ value: groupValue
2923
+ }));
2924
+ return buildOrCtsQueryExpressionInternal([registerConstantHelper({
2925
+ context,
2926
+ key: [
2927
+ "group",
2928
+ "exact",
2929
+ groupValue,
2930
+ ...exactMemberHelpers.map((helper) => helper.name)
2931
+ ].join("|"),
2932
+ bodyExpression: buildOrCtsQueryExpressionInternal(exactMemberHelpers.map((helper) => helper.callExpression))
2933
+ }).callExpression, tokenizedQueryExpression]);
2934
+ }
2935
+ function buildQueryNode(context, query) {
2732
2936
  if (isQueryLeaf(query)) {
2733
- const queryExpression = buildLeafQueryExpression(query);
2937
+ const queryExpression = buildLeafQueryExpression(context, query);
2734
2938
  return query.isNegated === true ? buildNotCtsQueryExpression(queryExpression) : queryExpression;
2735
2939
  }
2736
2940
  const optimizedIncludesGroupQueries = getCompatibleIncludesGroupLeaves(query);
2737
- if (optimizedIncludesGroupQueries != null) return buildIncludesGroupQueryExpression(optimizedIncludesGroupQueries);
2941
+ if (optimizedIncludesGroupQueries != null) return buildIncludesGroupQueryExpression(context, optimizedIncludesGroupQueries);
2738
2942
  const childQueryExpressions = [];
2739
- for (const childQuery of getQueryGroupChildren(query)) childQueryExpressions.push(buildQueryNode(childQuery));
2943
+ for (const childQuery of getQueryGroupChildren(query)) childQueryExpressions.push(buildQueryNode(context, childQuery));
2740
2944
  return getQueryGroupOperator(query) === "and" ? buildAndCtsQueryExpressionInternal(childQueryExpressions) : buildOrCtsQueryExpressionInternal(childQueryExpressions);
2741
2945
  }
2742
2946
  function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, belongsToCollectionPropertyVariableUuid) {
@@ -2754,8 +2958,16 @@ function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids,
2754
2958
  }
2755
2959
  function buildQueryPlan(params) {
2756
2960
  const { queries } = params;
2757
- if (queries == null) return { queryExpression: null };
2758
- return { queryExpression: buildQueryNode(queries) };
2961
+ if (queries == null) return {
2962
+ prolog: "",
2963
+ queryExpression: null
2964
+ };
2965
+ const context = createQueryCompilerContext();
2966
+ const queryExpression = buildQueryNode(context, queries);
2967
+ return {
2968
+ prolog: context.helperDeclarations.join("\n\n"),
2969
+ queryExpression
2970
+ };
2759
2971
  }
2760
2972
  //#endregion
2761
2973
  //#region src/utils/fetchers/set/items.ts
@@ -2844,8 +3056,8 @@ function buildOrderedItemsClause(sort) {
2844
3056
  function buildXQuery$1(params) {
2845
3057
  const { queries, sort, setScopeUuids, belongsToCollectionScopeUuids, page, pageSize } = params;
2846
3058
  const startPosition = (page - 1) * pageSize + 1;
2847
- const endPosition = page * pageSize;
2848
- const baseItemsExpression = `doc()/ochre${`/set[(${setScopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ")})]/items/*`}`;
3059
+ const setScopeDeclaration = `declare variable $setScopeUuids := (${setScopeUuids.map((uuid) => stringLiteral(uuid)).join(", ")});`;
3060
+ const baseItemsExpression = "doc()/ochre/set[@uuid = $setScopeUuids]/items/*";
2849
3061
  const compiledQueryPlan = buildQueryPlan({ queries });
2850
3062
  const itemsQueryExpressions = [];
2851
3063
  const belongsToCollectionQueryExpression = buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, BELONGS_TO_COLLECTION_UUID);
@@ -2853,17 +3065,22 @@ function buildXQuery$1(params) {
2853
3065
  if (belongsToCollectionQueryExpression != null) itemsQueryExpressions.push(belongsToCollectionQueryExpression);
2854
3066
  const itemsQueryExpression = buildAndCtsQueryExpression(itemsQueryExpressions);
2855
3067
  const orderedItemsClause = buildOrderedItemsClause(sort);
2856
- return `<ochre>{${`${itemsQueryExpression == null ? `let $items := ${baseItemsExpression}` : `let $items := ${baseItemsExpression}[cts:contains(., ${itemsQueryExpression})]`}
3068
+ const xqueryDeclarations = ["xquery version \"1.0-ml\";", setScopeDeclaration];
3069
+ if (compiledQueryPlan.prolog !== "") xqueryDeclarations.push(compiledQueryPlan.prolog);
3070
+ const itemsClause = itemsQueryExpression == null ? `let $items := ${baseItemsExpression}` : `let $query := ${itemsQueryExpression}
3071
+ let $items := cts:search(${baseItemsExpression}, $query)`;
3072
+ return `${xqueryDeclarations.join("\n\n")}
2857
3073
 
3074
+ <ochre>{
3075
+ ${itemsClause}
2858
3076
  let $totalCount := count($items)
2859
3077
  ${orderedItemsClause}
3078
+ let $pagedItems := subsequence($orderedItems, ${startPosition}, ${pageSize})
2860
3079
 
2861
3080
  return <items totalCount="{$totalCount}" page="${page}" pageSize="${pageSize}">{
2862
- for $item in $orderedItems[position() ge ${startPosition} and position() le ${endPosition}]
2863
- return element { node-name($item) } {
2864
- $item/@*, $item/node()
2865
- }
2866
- }</items>`}}</ochre>`;
3081
+ $pagedItems
3082
+ }</items>
3083
+ }</ochre>`;
2867
3084
  }
2868
3085
  /**
2869
3086
  * Fetches and parses Set items from the OCHRE API
@@ -3154,9 +3371,9 @@ const responseSchema = z.object({ result: z.union([z.object({ ochre: z.object({
3154
3371
  */
3155
3372
  function buildXQuery(params) {
3156
3373
  const { setScopeUuids, belongsToCollectionScopeUuids, queries, propertyVariableUuids, attributes, isLimitedToLeafPropertyValues } = params;
3157
- let setScopeFilter = "/set/items/*";
3158
- if (setScopeUuids.length > 0) setScopeFilter = `/set[(${setScopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ")})]/items/*`;
3159
- const baseItemsExpression = `doc()/ochre${setScopeFilter}`;
3374
+ const setScopeValues = setScopeUuids.map((uuid) => stringLiteral(uuid));
3375
+ const setScopeDeclaration = setScopeValues.length > 0 ? `declare variable $setScopeUuids := (${setScopeValues.join(", ")});` : "";
3376
+ const baseItemsExpression = setScopeValues.length > 0 ? "doc()/ochre/set[@uuid = $setScopeUuids]/items/*" : "doc()/ochre/set/items/*";
3160
3377
  const compiledQueryPlan = buildQueryPlan({ queries: getItemFilterQueriesFromPropertyValueQueries(queries) });
3161
3378
  const itemsQueryExpressions = [];
3162
3379
  const belongsToCollectionQueryExpression = buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, BELONGS_TO_COLLECTION_UUID);
@@ -3166,6 +3383,9 @@ function buildXQuery(params) {
3166
3383
  const valueFilter = isLimitedToLeafPropertyValues ? "[not(@i)]" : "";
3167
3384
  const queryBlocks = [];
3168
3385
  const returnedSequences = [];
3386
+ const xqueryDeclarations = ["xquery version \"1.0-ml\";"];
3387
+ if (setScopeDeclaration !== "") xqueryDeclarations.push(setScopeDeclaration);
3388
+ if (compiledQueryPlan.prolog !== "") xqueryDeclarations.push(compiledQueryPlan.prolog);
3169
3389
  if (propertyVariableUuids.length > 0) {
3170
3390
  const propertyVariableFilters = propertyVariableUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
3171
3391
  queryBlocks.push(`let $matching-props := $items//property[label[${propertyVariableFilters}]]
@@ -3198,11 +3418,16 @@ let $property-values :=
3198
3418
  return <attributeValue attributeType="periods" itemUuid="{$item/@uuid}" content="{$label}" />`);
3199
3419
  returnedSequences.push("$period-values");
3200
3420
  }
3201
- return `<ochre>{${`${itemsQueryExpression == null ? `let $items := ${baseItemsExpression}` : `let $items := ${baseItemsExpression}[cts:contains(., ${itemsQueryExpression})]`}
3421
+ const itemsClause = itemsQueryExpression == null ? `let $items := ${baseItemsExpression}` : `let $query := ${itemsQueryExpression}
3422
+ let $items := cts:search(${baseItemsExpression}, $query)`;
3423
+ return `${xqueryDeclarations.join("\n\n")}
3202
3424
 
3425
+ <ochre>{
3426
+ ${itemsClause}
3203
3427
  ${queryBlocks.join("\n\n")}
3204
3428
 
3205
- return (${returnedSequences.join(", ")})`}}</ochre>`;
3429
+ return (${returnedSequences.join(", ")})
3430
+ }</ochre>`;
3206
3431
  }
3207
3432
  /**
3208
3433
  * Fetches and parses Set property values from the OCHRE API
@@ -4856,7 +5081,8 @@ function parseWebsiteProperties(properties, websiteTree, sidebar) {
4856
5081
  isPropertiesDisplayed: true,
4857
5082
  isBibliographyDisplayed: true,
4858
5083
  isPropertyValuesGrouped: true,
4859
- isPublicationDateTimeDisplayed: false,
5084
+ isPublicationDateTimeDisplayed: true,
5085
+ isPersistentIdentifierDisplayed: true,
4860
5086
  iiifViewer: "universal-viewer"
4861
5087
  },
4862
5088
  options: {
@@ -4899,7 +5125,8 @@ function parseWebsiteProperties(properties, websiteTree, sidebar) {
4899
5125
  returnProperties.itemPage.isPropertiesDisplayed = getPropertyValueContentByLabel(itemPageTypeProperty.properties, "item-page-properties-displayed") ?? true;
4900
5126
  returnProperties.itemPage.isBibliographyDisplayed = getPropertyValueContentByLabel(itemPageTypeProperty.properties, "item-page-bibliography-displayed") ?? true;
4901
5127
  returnProperties.itemPage.isPropertyValuesGrouped = getPropertyValueContentByLabel(itemPageTypeProperty.properties, "item-page-property-values-grouped") ?? true;
4902
- returnProperties.itemPage.isPublicationDateTimeDisplayed = getPropertyValueContentByLabel(itemPageTypeProperty.properties, "item-page-publication-date-time-displayed") ?? false;
5128
+ returnProperties.itemPage.isPublicationDateTimeDisplayed = getPropertyValueContentByLabel(itemPageTypeProperty.properties, "item-page-publication-date-time-displayed") ?? true;
5129
+ returnProperties.itemPage.isPersistentIdentifierDisplayed = getPropertyValueContentByLabel(itemPageTypeProperty.properties, "item-page-persistent-identifier-displayed") ?? true;
4903
5130
  returnProperties.itemPage.iiifViewer = getPropertyValueContentByLabel(itemPageTypeProperty.properties, "item-page-iiif-viewer") ?? "universal-viewer";
4904
5131
  }
4905
5132
  if ("options" in websiteTree && websiteTree.options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ochre-sdk",
3
- "version": "0.22.15",
3
+ "version": "0.22.18",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Node.js library for working with OCHRE (Online Cultural and Historical Research Environment) data",
@@ -52,7 +52,7 @@
52
52
  "@antfu/eslint-config": "^8.2.0",
53
53
  "@types/node": "^24.12.2",
54
54
  "bumpp": "^11.0.1",
55
- "eslint": "^10.2.0",
55
+ "eslint": "^10.2.1",
56
56
  "prettier": "^3.8.3",
57
57
  "tsdown": "^0.21.9",
58
58
  "typescript": "^6.0.3",