ochre-sdk 1.0.73 → 1.0.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -1
- package/dist/fetchers/set/items.mjs +7 -23
- package/dist/fetchers/set/property-values.mjs +7 -23
- package/dist/fetchers/website.mjs +2 -2
- package/dist/getters.d.mts +6 -2
- package/dist/parsers/multilingual.d.mts +12 -4
- package/dist/parsers/website/index.mjs +13 -12
- package/dist/query.d.mts +20 -19
- package/dist/query.mjs +222 -107
- package/dist/schemas.mjs +1 -1
- package/dist/types/index.d.mts +8 -7
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -121,6 +121,34 @@ const result = await fetchSetItems(
|
|
|
121
121
|
Use `fetchSetPropertyValues` with the same query shape when you need facet data
|
|
122
122
|
for a filtered result set.
|
|
123
123
|
|
|
124
|
+
### OCR Text Queries
|
|
125
|
+
|
|
126
|
+
The `ocr` target searches the OCR text layer of the Resource items in a Set. It takes no `language`, because OCR text carries none.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const queries: Query = {
|
|
130
|
+
and: [
|
|
131
|
+
{
|
|
132
|
+
target: "ocr",
|
|
133
|
+
value: "Cappaert",
|
|
134
|
+
matchMode: "includes",
|
|
135
|
+
isCaseSensitive: false,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
target: "title",
|
|
139
|
+
value: "Convocation",
|
|
140
|
+
matchMode: "includes",
|
|
141
|
+
isCaseSensitive: false,
|
|
142
|
+
language: "eng",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Every `<string>` node in that layer holds a single OCR word, so `includes` matches each search term as its own word anywhere in the layer, in any order, with `*` and `?` wildcards supported. `exact` instead matches the terms as a run of adjacent whole words, so `"THE COLLEGE"` matches a page carrying that phrase but not one where the two words merely appear apart. An item matches when the OCR layer of the item itself or of any of its child Resources matches.
|
|
149
|
+
|
|
150
|
+
Set item projections do not carry the OCR layer, so an `ocr` leaf is resolved by an extra index-only search over the Resource documents whose matching UUIDs are then joined back onto the Set items. It still composes with `and`, `or`, and `isNegated` like any other leaf, and repeating the same OCR search inside one tree only costs one search.
|
|
151
|
+
|
|
124
152
|
## OCR Data
|
|
125
153
|
|
|
126
154
|
A Resource may carry an `<ocr>` layer holding the positioned output of an OCR run. The node hierarchy inside that layer is irregular and is not parsed, but any `<string>` node within it, at any depth, is read as one positioned OCR string.
|
|
@@ -139,7 +167,7 @@ for (const ocrString of result.ocrStrings ?? []) {
|
|
|
139
167
|
|
|
140
168
|
`x` and `y` come from `HPOS` and `VPOS` and give the top-left corner of the box, `width` and `height` its size, and `vertices` its full bounding polygon, which is not always rectangular. Each geometry field is null when the source attribute is absent or unparseable. `resourceUuid` names the Resource that owns the OCR layer, which differs from the requested item when the OCR lives on a child Resource.
|
|
141
169
|
|
|
142
|
-
Matching defaults to case-insensitive `includes` and runs against each string's `CONTENT`. Because a `<string>` holds a single OCR word, a multi-word search value is split on whitespace and a string is returned when it matches any one term
|
|
170
|
+
Matching defaults to case-insensitive `includes` and runs against each string's `CONTENT`. Because a `<string>` holds a single OCR word, a multi-word search value is split on whitespace and a string is returned when it matches any one term. Requesting an item that does not exist is an error; an item with no OCR layer, or no matches, returns an empty array.
|
|
143
171
|
|
|
144
172
|
## Helpers And Types
|
|
145
173
|
|
|
@@ -4,7 +4,7 @@ import { iso639_3Schema, setItemsParametersSchema } from "../../schemas.mjs";
|
|
|
4
4
|
import { restoreXMLMetadata } from "../../xml/metadata.mjs";
|
|
5
5
|
import { parseSetItems } from "../../parsers/index.mjs";
|
|
6
6
|
import { XMLSetItemsData } from "../../xml/schemas.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { buildBelongsToCollectionQueryExpression, buildQueryPlan } from "../../query.mjs";
|
|
8
8
|
import * as v from "valibot";
|
|
9
9
|
import { XMLParser } from "fast-xml-parser";
|
|
10
10
|
//#region src/fetchers/set/items.ts
|
|
@@ -138,9 +138,11 @@ function buildXQuery(parameters) {
|
|
|
138
138
|
const { queries, sort, setScopeUuids, belongsToCollectionScopeUuids, page, pageSize } = parameters;
|
|
139
139
|
const startPosition = (page - 1) * pageSize + 1;
|
|
140
140
|
const setScopeDeclaration = `declare variable $setScopeUuids := (${setScopeUuids.map((uuid) => stringLiteral(uuid)).join(", ")});`;
|
|
141
|
-
const compiledQueryPlan = buildQueryPlan({
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
const compiledQueryPlan = buildQueryPlan({
|
|
142
|
+
queries,
|
|
143
|
+
baseItemsExpression: "doc()/ochre/set[@uuid = $setScopeUuids]/items/*",
|
|
144
|
+
scopeQueryExpression: buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, BELONGS_TO_COLLECTION_UUID)
|
|
145
|
+
});
|
|
144
146
|
const orderedItemsClause = buildOrderedItemsClause(sort);
|
|
145
147
|
const xqueryDeclarations = [
|
|
146
148
|
"xquery version \"1.0-ml\";",
|
|
@@ -148,28 +150,10 @@ function buildXQuery(parameters) {
|
|
|
148
150
|
SUPPLEMENTAL_XQUERY_PROLOG
|
|
149
151
|
];
|
|
150
152
|
if (compiledQueryPlan.prolog !== "") xqueryDeclarations.push(compiledQueryPlan.prolog);
|
|
151
|
-
const letClauses = Array.from(compiledQueryPlan.ocrTextBindings, (binding) => `let ${binding.name} := ${binding.expression}`);
|
|
152
|
-
const branchExpressions = [];
|
|
153
|
-
for (const [index, branch] of compiledQueryPlan.branches.entries()) {
|
|
154
|
-
const branchQueryExpressions = [];
|
|
155
|
-
if (branch.queryExpression != null) branchQueryExpressions.push(branch.queryExpression);
|
|
156
|
-
if (belongsToCollectionQueryExpression != null) branchQueryExpressions.push(belongsToCollectionQueryExpression);
|
|
157
|
-
const branchQueryExpression = buildAndCtsQueryExpression(branchQueryExpressions);
|
|
158
|
-
const branchItemsExpression = `${baseItemsExpression}${branch.itemPredicates}`;
|
|
159
|
-
if (branchQueryExpression == null) {
|
|
160
|
-
branchExpressions.push(branchItemsExpression);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
const queryVariableName = compiledQueryPlan.branches.length === 1 ? "$query" : `$query${index + 1}`;
|
|
164
|
-
letClauses.push(`let ${queryVariableName} := ${branchQueryExpression}`);
|
|
165
|
-
branchExpressions.push(`cts:search(${branchItemsExpression}, ${queryVariableName})`);
|
|
166
|
-
}
|
|
167
|
-
letClauses.push(`let $items := ${branchExpressions.length === 1 ? branchExpressions[0] : `(${branchExpressions.join(" | ")})`}`);
|
|
168
|
-
const itemsClause = letClauses.join("\n ");
|
|
169
153
|
return `${xqueryDeclarations.join("\n\n")}
|
|
170
154
|
|
|
171
155
|
<ochre>{
|
|
172
|
-
${itemsClause}
|
|
156
|
+
${compiledQueryPlan.itemsClause}
|
|
173
157
|
let $totalCount := count($items)
|
|
174
158
|
${orderedItemsClause}
|
|
175
159
|
let $pagedItems := subsequence($orderedItems, ${startPosition}, ${pageSize})
|
|
@@ -3,7 +3,7 @@ import { NOT_SUPPLEMENTAL_PREDICATE, createSchemaValidationError, getErrorOutput
|
|
|
3
3
|
import { MultilingualString } from "../../parsers/multilingual.mjs";
|
|
4
4
|
import { setPropertyValuesParametersSchema } from "../../schemas.mjs";
|
|
5
5
|
import { parseXMLContent } from "../../parsers/string.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { buildBelongsToCollectionQueryExpression, buildQueryPlan } from "../../query.mjs";
|
|
7
7
|
import * as v from "valibot";
|
|
8
8
|
import { XMLParser } from "fast-xml-parser";
|
|
9
9
|
//#region src/fetchers/set/property-values.ts
|
|
@@ -190,9 +190,11 @@ const responseSchema = v.object({ result: v.object({ ochre: v.object({
|
|
|
190
190
|
function buildXQuery(parameters) {
|
|
191
191
|
const { setScopeUuids, belongsToCollectionScopeUuids, queries, propertyFacetSelectors, attributes, isLimitedToLeafPropertyValues } = parameters;
|
|
192
192
|
const setScopeDeclaration = `declare variable $setScopeUuids := (${setScopeUuids.map((uuid) => stringLiteral(uuid)).join(", ")});`;
|
|
193
|
-
const compiledQueryPlan = buildQueryPlan({
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
const compiledQueryPlan = buildQueryPlan({
|
|
194
|
+
queries: getItemFilterQueriesFromPropertyValueQueries(queries),
|
|
195
|
+
baseItemsExpression: "doc()/ochre/set[@uuid = $setScopeUuids]/items/*",
|
|
196
|
+
scopeQueryExpression: buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, BELONGS_TO_COLLECTION_UUID)
|
|
197
|
+
});
|
|
196
198
|
const valueFilter = isLimitedToLeafPropertyValues ? "[not(@i)]" : "";
|
|
197
199
|
const queryBlocks = [];
|
|
198
200
|
const returnedSequences = [];
|
|
@@ -400,28 +402,10 @@ let $period-values :=
|
|
|
400
402
|
)`);
|
|
401
403
|
returnedSequences.push("$period-values");
|
|
402
404
|
}
|
|
403
|
-
const letClauses = Array.from(compiledQueryPlan.ocrTextBindings, (binding) => `let ${binding.name} := ${binding.expression}`);
|
|
404
|
-
const branchExpressions = [];
|
|
405
|
-
for (const [index, branch] of compiledQueryPlan.branches.entries()) {
|
|
406
|
-
const branchQueryExpressions = [];
|
|
407
|
-
if (branch.queryExpression != null) branchQueryExpressions.push(branch.queryExpression);
|
|
408
|
-
if (belongsToCollectionQueryExpression != null) branchQueryExpressions.push(belongsToCollectionQueryExpression);
|
|
409
|
-
const branchQueryExpression = buildAndCtsQueryExpression(branchQueryExpressions);
|
|
410
|
-
const branchItemsExpression = `${baseItemsExpression}${branch.itemPredicates}`;
|
|
411
|
-
if (branchQueryExpression == null) {
|
|
412
|
-
branchExpressions.push(branchItemsExpression);
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
const queryVariableName = compiledQueryPlan.branches.length === 1 ? "$query" : `$query${index + 1}`;
|
|
416
|
-
letClauses.push(`let ${queryVariableName} := ${branchQueryExpression}`);
|
|
417
|
-
branchExpressions.push(`cts:search(${branchItemsExpression}, ${queryVariableName})`);
|
|
418
|
-
}
|
|
419
|
-
letClauses.push(`let $items := ${branchExpressions.length === 1 ? branchExpressions[0] : `(${branchExpressions.join(" | ")})`}`);
|
|
420
|
-
const itemsClause = letClauses.join("\n ");
|
|
421
405
|
return `${xqueryDeclarations.join("\n\n")}
|
|
422
406
|
|
|
423
407
|
<ochre>{
|
|
424
|
-
${itemsClause}
|
|
408
|
+
${compiledQueryPlan.itemsClause}
|
|
425
409
|
${queryBlocks.join("\n\n")}
|
|
426
410
|
|
|
427
411
|
return (${returnedSequences.join(", ")})
|
|
@@ -6,7 +6,7 @@ import { parseWebsite } from "../parsers/website/index.mjs";
|
|
|
6
6
|
import * as v from "valibot";
|
|
7
7
|
import { XMLParser } from "fast-xml-parser";
|
|
8
8
|
//#region src/fetchers/website.ts
|
|
9
|
-
async function
|
|
9
|
+
async function areWebsiteCredentialsValid(uuid, credentials, fetcher) {
|
|
10
10
|
const security = typeof credentials === "string" ? { validate: credentials } : {
|
|
11
11
|
validate: credentials.password,
|
|
12
12
|
userOCHRE: credentials.username
|
|
@@ -69,7 +69,7 @@ async function fetchWebsite(abbreviation, options) {
|
|
|
69
69
|
error: null,
|
|
70
70
|
detailedError: null
|
|
71
71
|
};
|
|
72
|
-
if (!await
|
|
72
|
+
if (!await areWebsiteCredentialsValid(website.uuid, options.credentials, fetcher)) throw new Error("Invalid credentials for protected website");
|
|
73
73
|
}
|
|
74
74
|
return {
|
|
75
75
|
website,
|
package/dist/getters.d.mts
CHANGED
|
@@ -4,9 +4,13 @@ import { LanguageCodes, Property, PropertyLike, PropertyValueContent, SetItemPro
|
|
|
4
4
|
* Options for property search operations.
|
|
5
5
|
*/
|
|
6
6
|
type PropertyOptions = {
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
Whether to recursively search through nested properties.
|
|
9
|
+
*/
|
|
8
10
|
includeNestedProperties?: boolean;
|
|
9
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
Whether to limit property values to leaf values.
|
|
13
|
+
*/
|
|
10
14
|
limitToLeafPropertyValues?: boolean;
|
|
11
15
|
};
|
|
12
16
|
type PropertyContent<T extends LanguageCodes> = PropertyValueContent<T>["content"];
|
|
@@ -26,11 +26,17 @@ type MultilingualStringEntries<T extends ReadonlyArray<string> = ReadonlyArray<s
|
|
|
26
26
|
* Options for creating and working with multilingual strings
|
|
27
27
|
*/
|
|
28
28
|
type MultilingualOptions = {
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
Default language to use for fallbacks
|
|
31
|
+
*/
|
|
30
32
|
defaultLanguage?: string;
|
|
31
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
Available languages for this string
|
|
35
|
+
*/
|
|
32
36
|
availableLanguages?: ReadonlyArray<string>;
|
|
33
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
Alias values carried by OCHRE as zxx content
|
|
39
|
+
*/
|
|
34
40
|
aliases?: ReadonlyArray<string>;
|
|
35
41
|
};
|
|
36
42
|
type MultilingualContent<T extends ReadonlyArray<string>> = Partial<Record<T[number], ReadonlyArray<MultilingualStringEntry>>>;
|
|
@@ -78,7 +84,9 @@ declare class MultilingualString<T extends ReadonlyArray<string> = ReadonlyArray
|
|
|
78
84
|
/**
|
|
79
85
|
* Create a new multilingual string from an object of language codes to text.
|
|
80
86
|
*/
|
|
81
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
@internal
|
|
89
|
+
*/
|
|
82
90
|
constructor(init: MultilingualStringInternalInit<T>);
|
|
83
91
|
constructor(content: MultilingualStringObject<T>, languages: T, options?: MultilingualOptions);
|
|
84
92
|
constructor(content?: Partial<Record<string, MultilingualStringInput>>, languages?: undefined, options?: MultilingualOptions);
|
|
@@ -907,17 +907,18 @@ function parseWebpage(webpageResource, options, context, slugPrefix) {
|
|
|
907
907
|
returnWebpage.properties.isNavbarSearchBarDisplayed = pageReader.valueOr("navbar-search-bar-displayed", true);
|
|
908
908
|
const redirectValue = pageReader.valueNode("redirect-to");
|
|
909
909
|
const redirectTarget = parseWebsiteLinkTarget(redirectValue, context);
|
|
910
|
-
if (redirectTarget != null)
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
910
|
+
if (redirectTarget != null) {
|
|
911
|
+
if (redirectValue?.href == null && redirectValue?.uuid != null) returnWebpage.properties.redirect = {
|
|
912
|
+
type: "page",
|
|
913
|
+
slug: redirectTarget,
|
|
914
|
+
uuid: redirectValue.uuid
|
|
915
|
+
};
|
|
916
|
+
else returnWebpage.properties.redirect = {
|
|
917
|
+
type: "url",
|
|
918
|
+
href: redirectTarget,
|
|
919
|
+
isExternal: redirectTarget.startsWith("http")
|
|
920
|
+
};
|
|
921
|
+
} else if (redirectValue?.uuid != null) returnWebpage.properties.redirect = {
|
|
921
922
|
type: "item",
|
|
922
923
|
uuid: redirectValue.uuid,
|
|
923
924
|
pageType: "item"
|
|
@@ -1015,7 +1016,7 @@ function parseSidebar(resources, options, context) {
|
|
|
1015
1016
|
}
|
|
1016
1017
|
}
|
|
1017
1018
|
}
|
|
1018
|
-
if (items.length > 0
|
|
1019
|
+
if (title != null && items.length > 0) returnSidebar = {
|
|
1019
1020
|
isDisplayed: true,
|
|
1020
1021
|
items,
|
|
1021
1022
|
title,
|
package/dist/query.d.mts
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
import { Query } from "./types/index.mjs";
|
|
2
2
|
//#region src/query.d.ts
|
|
3
|
-
declare function buildAndCtsQueryExpression(queryExpressions: Array<string>): string | null;
|
|
4
3
|
declare function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids: Array<string>, belongsToCollectionPropertyVariableUuid: string): string | null;
|
|
5
4
|
/**
|
|
6
|
-
* Compile a query tree into the
|
|
5
|
+
* Compile a query tree into the XQuery `let` clauses that bind `$items` to the
|
|
6
|
+
* matching Set items
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* Most queries compile to a single `cts:search` over the Set item projections.
|
|
9
|
+
* An `ocr` leaf cannot: the projections drop the `<ocr>` layer, so it resolves
|
|
10
|
+
* to a search over the Resource documents whose matching UUIDs are joined back
|
|
11
|
+
* in as an item path predicate. Path predicates only ever AND, so an `ocr` leaf
|
|
12
|
+
* that sits under an `or` becomes its own arm of a node union instead, and one
|
|
13
|
+
* that sits under an `and` alongside a union becomes an intersection.
|
|
14
|
+
*
|
|
15
|
+
* The searchable path has to stay inline in `cts:search`: binding it to a
|
|
16
|
+
* variable first makes every query XDMP-UNSEARCHABLE.
|
|
17
|
+
* @param parameters - The parameters for the compilation
|
|
18
|
+
* @param parameters.queries - Recursive query tree to compile, if any
|
|
19
|
+
* @param parameters.baseItemsExpression - The inline XQuery path selecting the items to search
|
|
20
|
+
* @param parameters.scopeQueryExpression - An optional CTS query ANDed into every compiled search
|
|
21
|
+
* @returns The prolog declaring the query helpers, and the `let` clauses binding `$items`
|
|
16
22
|
*/
|
|
17
23
|
declare function buildQueryPlan(parameters: {
|
|
18
24
|
queries: Query | null;
|
|
25
|
+
baseItemsExpression: string;
|
|
26
|
+
scopeQueryExpression?: string | null;
|
|
19
27
|
}): {
|
|
20
28
|
prolog: string;
|
|
21
|
-
|
|
22
|
-
name: string;
|
|
23
|
-
expression: string;
|
|
24
|
-
}>;
|
|
25
|
-
branches: Array<{
|
|
26
|
-
itemPredicates: string;
|
|
27
|
-
queryExpression: string | null;
|
|
28
|
-
}>;
|
|
29
|
+
itemsClause: string;
|
|
29
30
|
};
|
|
30
31
|
//#endregion
|
|
31
|
-
export {
|
|
32
|
+
export { buildBelongsToCollectionQueryExpression, buildQueryPlan };
|
package/dist/query.mjs
CHANGED
|
@@ -12,7 +12,6 @@ const CTS_INCLUDES_STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
12
12
|
const CTS_INCLUDES_TOKEN_WORD_REGEX = /^\p{L}+$/u;
|
|
13
13
|
const CTS_INCLUDES_TOKEN_REGEX = /[\p{L}\p{N}*?]+/gu;
|
|
14
14
|
const CTS_EXACT_TEXT_TOKEN_REGEX = /[\p{L}\p{N}]+/gu;
|
|
15
|
-
const MAX_OCR_TEXT_CONDITIONS = 4;
|
|
16
15
|
const CONTENT_TARGET_CONTENT_ELEMENT_PATHS = {
|
|
17
16
|
title: [
|
|
18
17
|
"identification",
|
|
@@ -239,7 +238,8 @@ function buildRichTextContentQueryExpression(parameters) {
|
|
|
239
238
|
const { value, matchMode, isCaseSensitive, language } = parameters;
|
|
240
239
|
return buildAndCtsQueryExpressionInternal([buildContentLanguageQuery(language), matchMode === "exact" ? buildRichTextExactQueryExpression({
|
|
241
240
|
value,
|
|
242
|
-
isCaseSensitive
|
|
241
|
+
isCaseSensitive,
|
|
242
|
+
language
|
|
243
243
|
}) : buildCtsWordQueryExpression({
|
|
244
244
|
value,
|
|
245
245
|
matchMode,
|
|
@@ -472,83 +472,81 @@ function buildItemStringQueryExpression(parameters) {
|
|
|
472
472
|
language
|
|
473
473
|
})]);
|
|
474
474
|
}
|
|
475
|
-
function
|
|
475
|
+
function tokenizeOcrPhraseValue(value) {
|
|
476
|
+
const terms = [];
|
|
477
|
+
for (const term of value.split(/\s+/u)) if (term !== "") terms.push(term);
|
|
478
|
+
return terms;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Word queries against the OCR layer cannot carry a stemming option: the OCHRE
|
|
482
|
+
* database has unstemmed word searches turned off, and asking an element word
|
|
483
|
+
* query for `unstemmed` fails with `XDMP-WORDSEARCH`. Omitting the option
|
|
484
|
+
* altogether resolves the term against the database default instead.
|
|
485
|
+
*/
|
|
486
|
+
function buildOcrWordQueryExpression(parameters) {
|
|
487
|
+
const { value, isCaseSensitive } = parameters;
|
|
488
|
+
const options = [
|
|
489
|
+
isCaseSensitive ? "case-sensitive" : "case-insensitive",
|
|
490
|
+
"diacritic-insensitive",
|
|
491
|
+
"punctuation-insensitive",
|
|
492
|
+
"whitespace-insensitive"
|
|
493
|
+
];
|
|
494
|
+
if (hasWildcardCharacters(value)) options.push("wildcarded");
|
|
495
|
+
return `cts:element-word-query(xs:QName("string"), ${stringLiteral(value)}, (${options.map((option) => stringLiteral(option)).join(", ")}))`;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Compile an OCR text search into a query over the `<ocr>` layer of a Resource
|
|
499
|
+
* document
|
|
500
|
+
*
|
|
501
|
+
* Every `<string>` node in that layer holds a single OCR word, so `includes`
|
|
502
|
+
* matches each search term as its own word anywhere in the layer, and `exact`
|
|
503
|
+
* matches the terms as a run of adjacent whole string values. A phrase cannot
|
|
504
|
+
* be a word query here: word positions do not carry across the `<string>`
|
|
505
|
+
* boundaries, which makes `cts:near-query` the only phrase mechanism, and its
|
|
506
|
+
* distance is a total span rather than a pairwise gap.
|
|
507
|
+
*/
|
|
508
|
+
function buildOcrQueryExpression(query) {
|
|
476
509
|
const { value, matchMode, isCaseSensitive } = query;
|
|
477
|
-
|
|
478
|
-
value
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
510
|
+
if (matchMode === "exact") {
|
|
511
|
+
const terms = tokenizeOcrPhraseValue(value);
|
|
512
|
+
if (terms.length === 0) return "cts:false-query()";
|
|
513
|
+
const termQueryExpressions = Array.from(terms, (term) => buildCtsElementValueQueryExpression({
|
|
514
|
+
elementName: "string",
|
|
515
|
+
value: term,
|
|
516
|
+
isCaseSensitive
|
|
517
|
+
}));
|
|
518
|
+
return buildNestedElementQuery(["ocr"], termQueryExpressions.length === 1 ? termQueryExpressions[0] ?? "cts:false-query()" : `cts:near-query((${termQueryExpressions.join(", ")}), ${termQueryExpressions.length - 1}, ("ordered"))`);
|
|
519
|
+
}
|
|
482
520
|
const terms = tokenizeIncludesSearchValue({
|
|
483
521
|
value,
|
|
484
522
|
isCaseSensitive
|
|
485
523
|
});
|
|
486
524
|
if (terms.length === 0) return "cts:false-query()";
|
|
487
|
-
|
|
525
|
+
return buildNestedElementQuery(["ocr"], buildAndCtsQueryExpressionInternal(Array.from(terms, (term) => buildOcrWordQueryExpression({
|
|
488
526
|
value: term,
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
queryFamily: "text"
|
|
492
|
-
})));
|
|
493
|
-
return buildNestedElementQuery(["ocrText"], shouldUseFullValueFallbackForIncludes({
|
|
494
|
-
value,
|
|
495
|
-
isCaseSensitive,
|
|
496
|
-
terms
|
|
497
|
-
}) ? buildOrCtsQueryExpressionInternal([phraseQueryExpression, tokenizedQueryExpression]) : tokenizedQueryExpression);
|
|
527
|
+
isCaseSensitive
|
|
528
|
+
}))));
|
|
498
529
|
}
|
|
499
|
-
|
|
500
|
-
|
|
530
|
+
/**
|
|
531
|
+
* Bind the UUIDs of the Resource documents whose OCR layer matches a query,
|
|
532
|
+
* reusing the binding when the same search is requested more than once
|
|
533
|
+
*/
|
|
534
|
+
function registerOcrBinding(context, query) {
|
|
535
|
+
const key = [
|
|
501
536
|
query.value,
|
|
502
537
|
query.matchMode,
|
|
503
538
|
query.isCaseSensitive ? "case-sensitive" : "case-insensitive"
|
|
504
539
|
].join("|");
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
context.
|
|
510
|
-
context.
|
|
511
|
-
|
|
512
|
-
|
|
540
|
+
const existingName = context.ocrBindingNamesByKey.get(key);
|
|
541
|
+
if (existingName != null) return existingName;
|
|
542
|
+
const name = `$ocrItemUuids${context.ocrBindings.length + 1}`;
|
|
543
|
+
const queryExpression = buildOcrQueryExpression(query);
|
|
544
|
+
context.ocrBindingNamesByKey.set(key, name);
|
|
545
|
+
context.ocrBindings.push({
|
|
546
|
+
name,
|
|
547
|
+
expression: queryExpression === "cts:false-query()" ? "()" : `cts:search(/ochre/resource, ${queryExpression})/@uuid/string()`
|
|
513
548
|
});
|
|
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;
|
|
547
|
-
}
|
|
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("");
|
|
549
|
+
return name;
|
|
552
550
|
}
|
|
553
551
|
function getLeafSearchValue(query) {
|
|
554
552
|
switch (query.target) {
|
|
@@ -631,8 +629,8 @@ function createQueryCompilerContext() {
|
|
|
631
629
|
nextHelperSerial: 1,
|
|
632
630
|
helperNamesByKey: /* @__PURE__ */ new Map(),
|
|
633
631
|
helperDeclarations: [],
|
|
634
|
-
|
|
635
|
-
|
|
632
|
+
ocrBindingNamesByKey: /* @__PURE__ */ new Map(),
|
|
633
|
+
ocrBindings: []
|
|
636
634
|
};
|
|
637
635
|
}
|
|
638
636
|
function registerConstantHelper(parameters) {
|
|
@@ -833,7 +831,7 @@ function getCompatibleIncludesGroupLeaves(query) {
|
|
|
833
831
|
if (!("or" in query) || query.or.length <= 1) return null;
|
|
834
832
|
const leafQueries = [];
|
|
835
833
|
for (const childQuery of query.or) {
|
|
836
|
-
if (!isQueryLeaf(childQuery) || childQuery.target === "
|
|
834
|
+
if (!isQueryLeaf(childQuery) || childQuery.target === "ocr") return null;
|
|
837
835
|
leafQueries.push(childQuery);
|
|
838
836
|
}
|
|
839
837
|
const firstQuery = leafQueries[0];
|
|
@@ -894,16 +892,121 @@ function buildIncludesGroupQueryExpression(context, queries) {
|
|
|
894
892
|
bodyExpression: buildOrCtsQueryExpressionInternal(exactMemberHelpers.map((helper) => helper.callExpression))
|
|
895
893
|
}).callExpression, tokenizedQueryExpression]);
|
|
896
894
|
}
|
|
897
|
-
function
|
|
895
|
+
function buildCtsItemsPlan(queryExpression) {
|
|
896
|
+
return {
|
|
897
|
+
kind: "search",
|
|
898
|
+
itemPredicates: [],
|
|
899
|
+
queryExpressions: [queryExpression]
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Splice the children of same-kind child plans into their parent, so that a
|
|
904
|
+
* nested group of the same operator does not cost an extra search
|
|
905
|
+
*/
|
|
906
|
+
function flattenItemsPlans(childPlans, kind) {
|
|
907
|
+
const flattenedPlans = [];
|
|
908
|
+
for (const childPlan of childPlans) {
|
|
909
|
+
if (childPlan.kind === kind) {
|
|
910
|
+
flattenedPlans.push(...childPlan.children);
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
flattenedPlans.push(childPlan);
|
|
914
|
+
}
|
|
915
|
+
return flattenedPlans;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Fold the children of an `and` group into one plan
|
|
919
|
+
*
|
|
920
|
+
* Conjunction is the direction the item path predicates already run in, so
|
|
921
|
+
* every child that is a plain search collapses into a single search, and only
|
|
922
|
+
* the children that resolved to a union stay separate.
|
|
923
|
+
*/
|
|
924
|
+
function buildAndItemsPlan(childPlans) {
|
|
925
|
+
const mergedPlan = {
|
|
926
|
+
kind: "search",
|
|
927
|
+
itemPredicates: [],
|
|
928
|
+
queryExpressions: []
|
|
929
|
+
};
|
|
930
|
+
const unfoldablePlans = [];
|
|
931
|
+
for (const childPlan of flattenItemsPlans(childPlans, "intersect")) {
|
|
932
|
+
if (childPlan.kind !== "search") {
|
|
933
|
+
unfoldablePlans.push(childPlan);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
for (const itemPredicate of childPlan.itemPredicates) if (!mergedPlan.itemPredicates.includes(itemPredicate)) mergedPlan.itemPredicates.push(itemPredicate);
|
|
937
|
+
mergedPlan.queryExpressions.push(...childPlan.queryExpressions);
|
|
938
|
+
}
|
|
939
|
+
if (unfoldablePlans.length === 0) return mergedPlan;
|
|
940
|
+
const intersectedPlans = mergedPlan.itemPredicates.length === 0 && mergedPlan.queryExpressions.length === 0 ? unfoldablePlans : [mergedPlan, ...unfoldablePlans];
|
|
941
|
+
return intersectedPlans.length === 1 ? intersectedPlans[0] ?? mergedPlan : {
|
|
942
|
+
kind: "intersect",
|
|
943
|
+
children: intersectedPlans
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Fold the children of an `or` group into one plan
|
|
948
|
+
*
|
|
949
|
+
* Item path predicates cannot be disjoined, so a child carrying one becomes its
|
|
950
|
+
* own arm of a node union. Everything else is still a single CTS query.
|
|
951
|
+
*/
|
|
952
|
+
function buildOrItemsPlan(childPlans) {
|
|
953
|
+
const mergedQueryExpressions = [];
|
|
954
|
+
const unionedPlans = [];
|
|
955
|
+
for (const childPlan of flattenItemsPlans(childPlans, "union")) {
|
|
956
|
+
if (childPlan.kind === "search" && childPlan.itemPredicates.length === 0) {
|
|
957
|
+
mergedQueryExpressions.push(buildAndCtsQueryExpressionInternal(childPlan.queryExpressions));
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
unionedPlans.push(childPlan);
|
|
961
|
+
}
|
|
962
|
+
if (mergedQueryExpressions.length > 0) unionedPlans.unshift({
|
|
963
|
+
kind: "search",
|
|
964
|
+
itemPredicates: [],
|
|
965
|
+
queryExpressions: [buildOrCtsQueryExpressionInternal(mergedQueryExpressions)]
|
|
966
|
+
});
|
|
967
|
+
if (unionedPlans.length === 0) return buildCtsItemsPlan("cts:false-query()");
|
|
968
|
+
return unionedPlans.length === 1 ? unionedPlans[0] ?? buildCtsItemsPlan("cts:false-query()") : {
|
|
969
|
+
kind: "union",
|
|
970
|
+
children: unionedPlans
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function buildItemsPlan(context, query) {
|
|
898
974
|
if (isQueryLeaf(query)) {
|
|
899
|
-
if (query.target === "
|
|
975
|
+
if (query.target === "ocr") {
|
|
976
|
+
const bindingName = registerOcrBinding(context, query);
|
|
977
|
+
return {
|
|
978
|
+
kind: "search",
|
|
979
|
+
itemPredicates: [query.isNegated === true ? `[not(@uuid = ${bindingName})]` : `[@uuid = ${bindingName}]`],
|
|
980
|
+
queryExpressions: []
|
|
981
|
+
};
|
|
982
|
+
}
|
|
900
983
|
const queryExpression = buildLeafQueryExpression(context, query);
|
|
901
|
-
return query.isNegated === true ? buildNotCtsQueryExpression(queryExpression) : queryExpression;
|
|
984
|
+
return buildCtsItemsPlan(query.isNegated === true ? buildNotCtsQueryExpression(queryExpression) : queryExpression);
|
|
902
985
|
}
|
|
903
986
|
const optimizedIncludesGroupQueries = getCompatibleIncludesGroupLeaves(query);
|
|
904
|
-
if (optimizedIncludesGroupQueries != null) return buildIncludesGroupQueryExpression(context, optimizedIncludesGroupQueries);
|
|
905
|
-
const
|
|
906
|
-
return
|
|
987
|
+
if (optimizedIncludesGroupQueries != null) return buildCtsItemsPlan(buildIncludesGroupQueryExpression(context, optimizedIncludesGroupQueries));
|
|
988
|
+
const childPlans = Array.from(getQueryGroupChildren(query), (childQuery) => buildItemsPlan(context, childQuery));
|
|
989
|
+
return getQueryGroupOperator(query) === "and" ? buildAndItemsPlan(childPlans) : buildOrItemsPlan(childPlans);
|
|
990
|
+
}
|
|
991
|
+
function collectItemsSearchPlans(plan, searchPlans) {
|
|
992
|
+
if (plan.kind === "search") {
|
|
993
|
+
searchPlans.push(plan);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
for (const childPlan of plan.children) collectItemsSearchPlans(childPlan, searchPlans);
|
|
997
|
+
}
|
|
998
|
+
function buildItemsPlanExpression(parameters) {
|
|
999
|
+
const { plan, baseItemsExpression, queryNamesByPlan } = parameters;
|
|
1000
|
+
if (plan.kind === "search") {
|
|
1001
|
+
const itemsExpression = `${baseItemsExpression}${plan.itemPredicates.join("")}`;
|
|
1002
|
+
const queryName = queryNamesByPlan.get(plan);
|
|
1003
|
+
return queryName == null ? itemsExpression : `cts:search(${itemsExpression}, ${queryName})`;
|
|
1004
|
+
}
|
|
1005
|
+
return `(${Array.from(plan.children, (childPlan) => buildItemsPlanExpression({
|
|
1006
|
+
plan: childPlan,
|
|
1007
|
+
baseItemsExpression,
|
|
1008
|
+
queryNamesByPlan
|
|
1009
|
+
})).join(plan.kind === "union" ? " | " : " intersect ")})`;
|
|
907
1010
|
}
|
|
908
1011
|
function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, belongsToCollectionPropertyVariableUuid) {
|
|
909
1012
|
if (belongsToCollectionScopeUuids.length === 0) return null;
|
|
@@ -917,46 +1020,58 @@ function buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids,
|
|
|
917
1020
|
});
|
|
918
1021
|
}
|
|
919
1022
|
/**
|
|
920
|
-
* Compile a query tree into the
|
|
1023
|
+
* Compile a query tree into the XQuery `let` clauses that bind `$items` to the
|
|
1024
|
+
* matching Set items
|
|
1025
|
+
*
|
|
1026
|
+
* Most queries compile to a single `cts:search` over the Set item projections.
|
|
1027
|
+
* An `ocr` leaf cannot: the projections drop the `<ocr>` layer, so it resolves
|
|
1028
|
+
* to a search over the Resource documents whose matching UUIDs are joined back
|
|
1029
|
+
* in as an item path predicate. Path predicates only ever AND, so an `ocr` leaf
|
|
1030
|
+
* that sits under an `or` becomes its own arm of a node union instead, and one
|
|
1031
|
+
* that sits under an `and` alongside a union becomes an intersection.
|
|
921
1032
|
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
*
|
|
929
|
-
* OCR text leaves are all conjunctive still compiles to a single search.
|
|
1033
|
+
* The searchable path has to stay inline in `cts:search`: binding it to a
|
|
1034
|
+
* variable first makes every query XDMP-UNSEARCHABLE.
|
|
1035
|
+
* @param parameters - The parameters for the compilation
|
|
1036
|
+
* @param parameters.queries - Recursive query tree to compile, if any
|
|
1037
|
+
* @param parameters.baseItemsExpression - The inline XQuery path selecting the items to search
|
|
1038
|
+
* @param parameters.scopeQueryExpression - An optional CTS query ANDed into every compiled search
|
|
1039
|
+
* @returns The prolog declaring the query helpers, and the `let` clauses binding `$items`
|
|
930
1040
|
*/
|
|
931
1041
|
function buildQueryPlan(parameters) {
|
|
932
|
-
const { queries } = parameters;
|
|
933
|
-
if (queries == null) return {
|
|
934
|
-
prolog: "",
|
|
935
|
-
ocrTextBindings: [],
|
|
936
|
-
branches: [{
|
|
937
|
-
itemPredicates: "",
|
|
938
|
-
queryExpression: null
|
|
939
|
-
}]
|
|
940
|
-
};
|
|
1042
|
+
const { queries, baseItemsExpression, scopeQueryExpression } = parameters;
|
|
941
1043
|
const context = createQueryCompilerContext();
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1044
|
+
const plan = queries == null ? {
|
|
1045
|
+
kind: "search",
|
|
1046
|
+
itemPredicates: [],
|
|
1047
|
+
queryExpressions: []
|
|
1048
|
+
} : buildItemsPlan(context, queries);
|
|
1049
|
+
const searchPlans = [];
|
|
1050
|
+
collectItemsSearchPlans(plan, searchPlans);
|
|
1051
|
+
const boundSearchPlans = [];
|
|
1052
|
+
for (const searchPlan of searchPlans) {
|
|
1053
|
+
const queryExpression = buildAndCtsQueryExpression([...searchPlan.queryExpressions, ...scopeQueryExpression == null ? [] : [scopeQueryExpression]]);
|
|
1054
|
+
if (queryExpression != null) boundSearchPlans.push({
|
|
1055
|
+
plan: searchPlan,
|
|
1056
|
+
queryExpression
|
|
950
1057
|
});
|
|
951
1058
|
}
|
|
1059
|
+
const queryNamesByPlan = /* @__PURE__ */ new Map();
|
|
1060
|
+
const letClauses = Array.from(context.ocrBindings, (binding) => `let ${binding.name} := ${binding.expression}`);
|
|
1061
|
+
for (const [index, boundSearchPlan] of boundSearchPlans.entries()) {
|
|
1062
|
+
const queryName = boundSearchPlans.length === 1 ? "$query" : `$query${index + 1}`;
|
|
1063
|
+
queryNamesByPlan.set(boundSearchPlan.plan, queryName);
|
|
1064
|
+
letClauses.push(`let ${queryName} := ${boundSearchPlan.queryExpression}`);
|
|
1065
|
+
}
|
|
1066
|
+
letClauses.push(`let $items := ${buildItemsPlanExpression({
|
|
1067
|
+
plan,
|
|
1068
|
+
baseItemsExpression,
|
|
1069
|
+
queryNamesByPlan
|
|
1070
|
+
})}`);
|
|
952
1071
|
return {
|
|
953
1072
|
prolog: context.helperDeclarations.join("\n\n"),
|
|
954
|
-
|
|
955
|
-
name: condition.variableName,
|
|
956
|
-
expression: condition.bindingExpression
|
|
957
|
-
})),
|
|
958
|
-
branches
|
|
1073
|
+
itemsClause: letClauses.join("\n ")
|
|
959
1074
|
};
|
|
960
1075
|
}
|
|
961
1076
|
//#endregion
|
|
962
|
-
export {
|
|
1077
|
+
export { buildBelongsToCollectionQueryExpression, buildQueryPlan };
|
package/dist/schemas.mjs
CHANGED
|
@@ -142,7 +142,7 @@ const setQueryLeafSchema = v.union([
|
|
|
142
142
|
...standardQueryFields
|
|
143
143
|
}),
|
|
144
144
|
v.strictObject({
|
|
145
|
-
target: v.literal("
|
|
145
|
+
target: v.literal("ocr"),
|
|
146
146
|
value: v.string(),
|
|
147
147
|
matchMode: standardQueryFields.matchMode,
|
|
148
148
|
isCaseSensitive: standardQueryFields.isCaseSensitive,
|
package/dist/types/index.d.mts
CHANGED
|
@@ -815,12 +815,13 @@ type SetItemsSort = {
|
|
|
815
815
|
/**
|
|
816
816
|
* Represents a leaf query for Set items
|
|
817
817
|
*
|
|
818
|
-
* The `
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
* `
|
|
818
|
+
* The `ocr` target matches the OCR text layer of Resource items. Set item
|
|
819
|
+
* projections do not carry `<ocr>`, so it is resolved by a document join rather
|
|
820
|
+
* than by a CTS term, and it composes with `and`, `or`, and `isNegated` at the
|
|
821
|
+
* cost of one extra search per distinct OCR value. Each `<string>` in that
|
|
822
|
+
* layer holds a single OCR word, so `includes` matches every search term as its
|
|
823
|
+
* own word and `exact` matches the terms as an adjacent run of whole words. OCR
|
|
824
|
+
* text carries no language, so `ocr` leaves take no `language`.
|
|
824
825
|
*/
|
|
825
826
|
type QueryLeaf = {
|
|
826
827
|
target: "property";
|
|
@@ -888,7 +889,7 @@ type QueryLeaf = {
|
|
|
888
889
|
language: string;
|
|
889
890
|
isNegated?: boolean;
|
|
890
891
|
} | {
|
|
891
|
-
target: "
|
|
892
|
+
target: "ocr";
|
|
892
893
|
value: string;
|
|
893
894
|
matchMode: "includes" | "exact";
|
|
894
895
|
isCaseSensitive: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ochre-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.74",
|
|
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",
|
|
@@ -51,11 +51,12 @@
|
|
|
51
51
|
"valibot": "^1.4.2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@antfu/eslint-config": "^9.
|
|
54
|
+
"@antfu/eslint-config": "^9.3.0",
|
|
55
55
|
"@types/node": "^24.13.3",
|
|
56
56
|
"bumpp": "^12.2.0",
|
|
57
57
|
"eslint": "^10.8.0",
|
|
58
|
-
"
|
|
58
|
+
"eslint-plugin-erasable-syntax-only": "^0.4.2",
|
|
59
|
+
"knip": "^6.32.0",
|
|
59
60
|
"oxfmt": "^0.62.0",
|
|
60
61
|
"tsdown": "^0.22.14",
|
|
61
62
|
"typescript": "^6.0.3",
|