ochre-sdk 1.0.71 → 1.0.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/constants.mjs +0 -5
- package/dist/fetchers/gallery.mjs +11 -5
- package/dist/fetchers/item-children.mjs +4 -2
- package/dist/fetchers/item-links.mjs +10 -5
- package/dist/fetchers/item-ocr-data.d.mts +37 -0
- package/dist/fetchers/item-ocr-data.mjs +166 -0
- package/dist/fetchers/item.mjs +43 -58
- package/dist/fetchers/set/items.mjs +26 -12
- package/dist/fetchers/set/property-values.mjs +24 -14
- package/dist/fetchers/website-metadata.mjs +7 -3
- package/dist/fetchers/website.mjs +20 -3
- package/dist/helpers.d.mts +1 -5
- package/dist/helpers.mjs +1 -5
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/parsers/index.d.mts +3 -10
- package/dist/parsers/index.mjs +1 -75
- package/dist/parsers/string.mjs +5 -1
- package/dist/query.d.mts +19 -28
- package/dist/query.mjs +104 -86
- package/dist/schemas.d.mts +7 -8
- package/dist/schemas.mjs +8 -10
- package/dist/types/index.d.mts +27 -57
- package/dist/utilities.d.mts +25 -1
- package/dist/utilities.mjs +41 -1
- package/dist/xml/schemas.d.mts +2 -7
- package/dist/xml/schemas.mjs +9 -43
- package/dist/xml/types.d.mts +13 -49
- package/package.json +3 -3
- package/dist/fetchers/ocr-matches.d.mts +0 -44
- package/dist/fetchers/ocr-matches.mjs +0 -134
package/README.md
CHANGED
|
@@ -57,6 +57,8 @@ present and `error` is `null`; on failure, the parsed value is `null` and
|
|
|
57
57
|
`category` lets the XQuery search only the matching OCHRE collection.
|
|
58
58
|
- `fetchItemLinks(uuid, options)` fetches items linked from a source item and
|
|
59
59
|
parses them as embedded OCHRE items.
|
|
60
|
+
- `fetchItemOcrData(uuid, value, options)` fetches the positioned OCR strings of
|
|
61
|
+
an item that match a search value, for drawing hit boxes over a scanned page.
|
|
60
62
|
- `fetchGallery(params, options)` fetches paginated resource galleries with an
|
|
61
63
|
optional label filter.
|
|
62
64
|
- `fetchWebsite(abbreviation, options)` fetches an OCHRE website presentation
|
|
@@ -119,6 +121,26 @@ const result = await fetchSetItems(
|
|
|
119
121
|
Use `fetchSetPropertyValues` with the same query shape when you need facet data
|
|
120
122
|
for a filtered result set.
|
|
121
123
|
|
|
124
|
+
## OCR Data
|
|
125
|
+
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { fetchItemOcrData } from "ochre-sdk";
|
|
130
|
+
|
|
131
|
+
const result = await fetchItemOcrData("<item-uuid>", "Artifact", {
|
|
132
|
+
matchMode: "exact",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
for (const ocrString of result.ocrStrings ?? []) {
|
|
136
|
+
console.log(ocrString.content, ocrString.x, ocrString.y, ocrString.vertices);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`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
|
+
|
|
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, which pairs with how the `ocrText` query target tokenizes. Requesting an item that does not exist is an error; an item with no OCR layer, or no matches, returns an empty array.
|
|
143
|
+
|
|
122
144
|
## Helpers And Types
|
|
123
145
|
|
|
124
146
|
The root export includes the SDK's public TypeScript model, website component
|
package/dist/constants.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_LANGUAGES, XML_PARSER_OPTIONS } from "../constants.mjs";
|
|
2
|
-
import { createSchemaValidationError, getErrorOutput, stringLiteral } from "../utilities.mjs";
|
|
2
|
+
import { SUPPLEMENTAL_XQUERY_PROLOG, createSchemaValidationError, getErrorOutput, omitSupplemental, stringLiteral } from "../utilities.mjs";
|
|
3
3
|
import { gallerySchema, iso639_3Schema } from "../schemas.mjs";
|
|
4
4
|
import { restoreXMLMetadata } from "../xml/metadata.mjs";
|
|
5
5
|
import { parseGallery } from "../parsers/index.mjs";
|
|
@@ -37,7 +37,11 @@ function buildXQuery(parameters) {
|
|
|
37
37
|
const { uuid, filter, page, perPage } = parameters;
|
|
38
38
|
const start = (page - 1) * perPage + 1;
|
|
39
39
|
const filterLiteral = stringLiteral(filter?.trim() ?? "");
|
|
40
|
-
return
|
|
40
|
+
return `xquery version "1.0-ml";
|
|
41
|
+
|
|
42
|
+
${SUPPLEMENTAL_XQUERY_PROLOG}
|
|
43
|
+
|
|
44
|
+
<ochre>{
|
|
41
45
|
for $q in doc()/ochre[@uuid=${stringLiteral(uuid)}]
|
|
42
46
|
let $filter := ${filterLiteral}
|
|
43
47
|
let $resources := $q//items/resource
|
|
@@ -47,9 +51,11 @@ function buildXQuery(parameters) {
|
|
|
47
51
|
else $resources[contains(lower-case(string-join(identification/label//text(), "")), lower-case($filter))]
|
|
48
52
|
let $maxLength := count($filtered)
|
|
49
53
|
return <gallery maxLength="{$maxLength}">{
|
|
50
|
-
$
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
${omitSupplemental(`(
|
|
55
|
+
$q/metadata/project,
|
|
56
|
+
$q/metadata/item,
|
|
57
|
+
subsequence($filtered, ${start}, ${perPage})
|
|
58
|
+
)`)}
|
|
53
59
|
}</gallery>
|
|
54
60
|
}</ochre>`;
|
|
55
61
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_LANGUAGES, XML_PARSER_OPTIONS } from "../constants.mjs";
|
|
2
|
-
import { createSchemaValidationError, getErrorOutput, stringLiteral } from "../utilities.mjs";
|
|
2
|
+
import { SUPPLEMENTAL_XQUERY_PROLOG, createSchemaValidationError, getErrorOutput, omitSupplemental, stringLiteral } from "../utilities.mjs";
|
|
3
3
|
import { iso639_3Schema, uuidSchema } from "../schemas.mjs";
|
|
4
4
|
import { restoreXMLMetadata } from "../xml/metadata.mjs";
|
|
5
5
|
import { parseLinkedItems } from "../parsers/index.mjs";
|
|
@@ -51,6 +51,8 @@ function buildXQuery(uuid, category) {
|
|
|
51
51
|
const collectionQueries = Array.from(categories, (possibleCategory) => `cts:search(fn:collection("ochre/${possibleCategory}")/ochre, $uuid-query)`);
|
|
52
52
|
return `xquery version "1.0-ml";
|
|
53
53
|
|
|
54
|
+
${SUPPLEMENTAL_XQUERY_PROLOG}
|
|
55
|
+
|
|
54
56
|
declare function local:item-children($nodes as node()*) as node()* {
|
|
55
57
|
for $node in $nodes
|
|
56
58
|
return
|
|
@@ -89,7 +91,7 @@ let $children :=
|
|
|
89
91
|
else ()
|
|
90
92
|
return
|
|
91
93
|
<ochre>
|
|
92
|
-
<items>{$children}</items>
|
|
94
|
+
<items>{${omitSupplemental("$children")}}</items>
|
|
93
95
|
</ochre>`;
|
|
94
96
|
}
|
|
95
97
|
async function fetchItemChildren(uuid, options) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_LANGUAGES, XML_PARSER_OPTIONS } from "../constants.mjs";
|
|
2
|
-
import { createSchemaValidationError, getErrorOutput } from "../utilities.mjs";
|
|
2
|
+
import { SUPPLEMENTAL_XQUERY_PROLOG, createSchemaValidationError, getErrorOutput, omitSupplemental, stringLiteral } from "../utilities.mjs";
|
|
3
3
|
import { iso639_3Schema, uuidSchema } from "../schemas.mjs";
|
|
4
4
|
import { restoreXMLMetadata } from "../xml/metadata.mjs";
|
|
5
5
|
import { parseLinkedItems } from "../parsers/index.mjs";
|
|
@@ -40,7 +40,7 @@ function resolveItemLinksLanguages(data, requestedLanguages) {
|
|
|
40
40
|
* @returns An XQuery string
|
|
41
41
|
*/
|
|
42
42
|
function buildXQuery(uuid) {
|
|
43
|
-
|
|
43
|
+
const xquery = `let $item-uuid := ${stringLiteral(uuid)}
|
|
44
44
|
|
|
45
45
|
let $source-items := (
|
|
46
46
|
fn:collection("ochre/resource")/ochre[@uuid = $item-uuid]/resource,
|
|
@@ -64,7 +64,7 @@ let $link-nodes := (
|
|
|
64
64
|
|
|
65
65
|
return
|
|
66
66
|
<items>{
|
|
67
|
-
for $link at $position in $link-nodes
|
|
67
|
+
${omitSupplemental(`for $link at $position in $link-nodes
|
|
68
68
|
let $uuid := $link/@uuid/string()
|
|
69
69
|
let $category := name($link)
|
|
70
70
|
where $uuid ne "" and not($uuid = $link-nodes[position() lt $position]/@uuid/string())
|
|
@@ -80,8 +80,13 @@ return
|
|
|
80
80
|
else if ($category = "set") then fn:collection("ochre/set")/ochre/set[@uuid = $uuid]
|
|
81
81
|
else if ($category = "spatialUnit") then fn:collection("ochre/spatialUnit")/ochre/spatialUnit[@uuid = $uuid]
|
|
82
82
|
else if ($category = "concept") then fn:collection("ochre/concept")/ochre/concept[@uuid = $uuid]
|
|
83
|
-
else ()
|
|
84
|
-
}</items
|
|
83
|
+
else ()`)}
|
|
84
|
+
}</items>`;
|
|
85
|
+
return `xquery version "1.0-ml";
|
|
86
|
+
|
|
87
|
+
${SUPPLEMENTAL_XQUERY_PROLOG}
|
|
88
|
+
|
|
89
|
+
<ochre>{${xquery}}</ochre>`;
|
|
85
90
|
}
|
|
86
91
|
async function fetchItemLinks(uuid, options) {
|
|
87
92
|
try {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { OcrString } from "../types/index.mjs";
|
|
2
|
+
//#region src/fetchers/item-ocr-data.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Fetches and parses the OCR strings of an OCHRE item that match a search value
|
|
5
|
+
*
|
|
6
|
+
* Resources may carry an `<ocr>` layer whose internal hierarchy is irregular
|
|
7
|
+
* and therefore not parsed. Only its `<string>` nodes are returned, wherever
|
|
8
|
+
* they occur in that subtree, in document order. Matching runs per string, and
|
|
9
|
+
* each `<string>` holds a single OCR word, so a multi-word search value is
|
|
10
|
+
* split on whitespace and a string is returned when it matches any one term.
|
|
11
|
+
* Nested child Resources are searched too, with `resourceUuid` naming the
|
|
12
|
+
* Resource each match belongs to.
|
|
13
|
+
*
|
|
14
|
+
* @param uuid - The UUID of the OCHRE item to read the OCR layer of
|
|
15
|
+
* @param value - The search value to match against each OCR string's content
|
|
16
|
+
* @param options - Options for the fetch
|
|
17
|
+
* @param options.matchMode - Whether a term has to be contained in a string's content ("includes", the default) or equal it ("exact")
|
|
18
|
+
* @param options.isCaseSensitive - Whether matching is case sensitive, defaulting to false
|
|
19
|
+
* @param options.fetch - The fetch function to use
|
|
20
|
+
* @returns The matching OCR strings, an empty array when the item has no OCR
|
|
21
|
+
* layer or nothing matches, and a null output on fetch/parse errors
|
|
22
|
+
*/
|
|
23
|
+
declare function fetchItemOcrData(uuid: string, value: string, options?: {
|
|
24
|
+
matchMode?: "includes" | "exact";
|
|
25
|
+
isCaseSensitive?: boolean;
|
|
26
|
+
fetch?: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response>;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
ocrStrings: Array<OcrString>;
|
|
29
|
+
error: null;
|
|
30
|
+
detailedError: null;
|
|
31
|
+
} | {
|
|
32
|
+
ocrStrings: null;
|
|
33
|
+
error: string;
|
|
34
|
+
detailedError: string;
|
|
35
|
+
}>;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { fetchItemOcrData };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { XML_PARSER_OPTIONS } from "../constants.mjs";
|
|
2
|
+
import { createSchemaValidationError, getErrorOutput, stringLiteral } from "../utilities.mjs";
|
|
3
|
+
import { itemOcrDataParametersSchema } from "../schemas.mjs";
|
|
4
|
+
import * as v from "valibot";
|
|
5
|
+
import { XMLParser } from "fast-xml-parser";
|
|
6
|
+
//#region src/fetchers/item-ocr-data.ts
|
|
7
|
+
const OCR_STRING_VERTEX_REGEX = /\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
8
|
+
/**
|
|
9
|
+
* Schema for a single matched OCR string in the OCHRE API response
|
|
10
|
+
*/
|
|
11
|
+
const ocrStringSchema = v.object({
|
|
12
|
+
resourceUuid: v.optional(v.string(), ""),
|
|
13
|
+
content: v.optional(v.string(), ""),
|
|
14
|
+
x: v.optional(v.string(), ""),
|
|
15
|
+
y: v.optional(v.string(), ""),
|
|
16
|
+
width: v.optional(v.string(), ""),
|
|
17
|
+
height: v.optional(v.string(), ""),
|
|
18
|
+
vertices: v.optional(v.string(), "")
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Schema for the item OCR data OCHRE API response
|
|
22
|
+
*/
|
|
23
|
+
const responseSchema = v.object({ result: v.object({ ochre: v.object({ ocrStrings: v.object({
|
|
24
|
+
found: v.optional(v.string(), "false"),
|
|
25
|
+
ocrString: v.optional(v.union([v.array(ocrStringSchema), ocrStringSchema]))
|
|
26
|
+
}) }) }) });
|
|
27
|
+
function getSearchTerms(parameters) {
|
|
28
|
+
const { value, isCaseSensitive } = parameters;
|
|
29
|
+
const terms = [];
|
|
30
|
+
for (const term of value.split(/\s+/u)) if (term !== "") terms.push(isCaseSensitive ? term : term.toLocaleLowerCase("en-US"));
|
|
31
|
+
return terms;
|
|
32
|
+
}
|
|
33
|
+
function parseOcrStringNumber(value) {
|
|
34
|
+
const trimmedValue = value.trim();
|
|
35
|
+
if (trimmedValue === "") return null;
|
|
36
|
+
const numericValue = Number(trimmedValue);
|
|
37
|
+
return Number.isFinite(numericValue) ? numericValue : null;
|
|
38
|
+
}
|
|
39
|
+
function parseOcrStringVertices(value) {
|
|
40
|
+
const vertices = [];
|
|
41
|
+
for (const match of value.matchAll(OCR_STRING_VERTEX_REGEX)) {
|
|
42
|
+
const x = Number(match[1]);
|
|
43
|
+
const y = Number(match[2]);
|
|
44
|
+
if (Number.isFinite(x) && Number.isFinite(y)) vertices.push({
|
|
45
|
+
x,
|
|
46
|
+
y
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return vertices;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build an XQuery string to fetch matching OCR strings from the OCHRE API
|
|
53
|
+
*
|
|
54
|
+
* The `<ocr>` layer is marked supplemental, so it is deliberately read without
|
|
55
|
+
* the supplemental stripping the other fetchers apply. Only the `<string>`
|
|
56
|
+
* nodes are projected, at any depth, because OCHRE does not guarantee the shape
|
|
57
|
+
* of the surrounding hierarchy.
|
|
58
|
+
*
|
|
59
|
+
* The matches are wrapped in an `<ocrStrings>` element rather than returned
|
|
60
|
+
* directly under `<ochre>`: the API collapses an `<ochre>` element that has no
|
|
61
|
+
* element children down to a bare `<ochre/>`, which would drop the `found`
|
|
62
|
+
* flag and make "no such item" indistinguishable from "no matches".
|
|
63
|
+
* @param parameters - The parameters for the fetch
|
|
64
|
+
* @param parameters.uuid - The UUID of the OCHRE item to read the OCR layer of
|
|
65
|
+
* @param parameters.terms - The whitespace-separated search terms to match against, already lowercased for case-insensitive matching
|
|
66
|
+
* @param parameters.matchMode - Whether a term has to be contained in a string's content or equal it
|
|
67
|
+
* @param parameters.isCaseSensitive - Whether matching is case sensitive
|
|
68
|
+
* @returns An XQuery string
|
|
69
|
+
*/
|
|
70
|
+
function buildXQuery(parameters) {
|
|
71
|
+
const { uuid, terms, matchMode, isCaseSensitive } = parameters;
|
|
72
|
+
const termValues = terms.map((term) => stringLiteral(term));
|
|
73
|
+
const contentExpression = isCaseSensitive ? "string($string/@CONTENT)" : "lower-case(string($string/@CONTENT))";
|
|
74
|
+
const matchExpression = matchMode === "exact" ? `normalize-space(${contentExpression}) = $term` : `contains(${contentExpression}, $term)`;
|
|
75
|
+
return `xquery version "1.0-ml";
|
|
76
|
+
|
|
77
|
+
declare variable $terms := (${termValues.join(", ")});
|
|
78
|
+
|
|
79
|
+
let $ochre := doc(${stringLiteral(uuid)})/ochre
|
|
80
|
+
let $ocrStrings :=
|
|
81
|
+
for $string in $ochre//ocr//string[@CONTENT]
|
|
82
|
+
where (some $term in $terms satisfies ${matchExpression})
|
|
83
|
+
return <ocrString
|
|
84
|
+
resourceUuid="{string($string/ancestor::resource[1]/@uuid)}"
|
|
85
|
+
content="{string($string/@CONTENT)}"
|
|
86
|
+
x="{string($string/@HPOS)}"
|
|
87
|
+
y="{string($string/@VPOS)}"
|
|
88
|
+
width="{string($string/@WIDTH)}"
|
|
89
|
+
height="{string($string/@HEIGHT)}"
|
|
90
|
+
vertices="{string($string/@VERTICES)}"/>
|
|
91
|
+
|
|
92
|
+
return <ochre><ocrStrings found="{exists($ochre)}">{$ocrStrings}</ocrStrings></ochre>`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fetches and parses the OCR strings of an OCHRE item that match a search value
|
|
96
|
+
*
|
|
97
|
+
* Resources may carry an `<ocr>` layer whose internal hierarchy is irregular
|
|
98
|
+
* and therefore not parsed. Only its `<string>` nodes are returned, wherever
|
|
99
|
+
* they occur in that subtree, in document order. Matching runs per string, and
|
|
100
|
+
* each `<string>` holds a single OCR word, so a multi-word search value is
|
|
101
|
+
* split on whitespace and a string is returned when it matches any one term.
|
|
102
|
+
* Nested child Resources are searched too, with `resourceUuid` naming the
|
|
103
|
+
* Resource each match belongs to.
|
|
104
|
+
*
|
|
105
|
+
* @param uuid - The UUID of the OCHRE item to read the OCR layer of
|
|
106
|
+
* @param value - The search value to match against each OCR string's content
|
|
107
|
+
* @param options - Options for the fetch
|
|
108
|
+
* @param options.matchMode - Whether a term has to be contained in a string's content ("includes", the default) or equal it ("exact")
|
|
109
|
+
* @param options.isCaseSensitive - Whether matching is case sensitive, defaulting to false
|
|
110
|
+
* @param options.fetch - The fetch function to use
|
|
111
|
+
* @returns The matching OCR strings, an empty array when the item has no OCR
|
|
112
|
+
* layer or nothing matches, and a null output on fetch/parse errors
|
|
113
|
+
*/
|
|
114
|
+
async function fetchItemOcrData(uuid, value, options) {
|
|
115
|
+
try {
|
|
116
|
+
const parameters = v.parse(itemOcrDataParametersSchema, {
|
|
117
|
+
uuid,
|
|
118
|
+
value,
|
|
119
|
+
matchMode: options?.matchMode,
|
|
120
|
+
isCaseSensitive: options?.isCaseSensitive
|
|
121
|
+
});
|
|
122
|
+
const terms = getSearchTerms({
|
|
123
|
+
value: parameters.value,
|
|
124
|
+
isCaseSensitive: parameters.isCaseSensitive
|
|
125
|
+
});
|
|
126
|
+
const xquery = buildXQuery({
|
|
127
|
+
uuid: parameters.uuid,
|
|
128
|
+
terms,
|
|
129
|
+
matchMode: parameters.matchMode,
|
|
130
|
+
isCaseSensitive: parameters.isCaseSensitive
|
|
131
|
+
});
|
|
132
|
+
const response = await (options?.fetch ?? fetch)("https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?xquery&xsl=none&lang=\"*\"", {
|
|
133
|
+
method: "POST",
|
|
134
|
+
body: xquery,
|
|
135
|
+
headers: { "Content-Type": "application/xquery" }
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) throw new Error(`OCHRE API responded with status: ${response.status}`, { cause: response.statusText });
|
|
138
|
+
const dataRaw = await response.text();
|
|
139
|
+
const data = new XMLParser(XML_PARSER_OPTIONS).parse(dataRaw);
|
|
140
|
+
const { success, issues, output } = v.safeParse(responseSchema, data);
|
|
141
|
+
if (!success) throw createSchemaValidationError("Failed to parse OCHRE item OCR data", issues);
|
|
142
|
+
const { found, ocrString } = output.result.ochre.ocrStrings;
|
|
143
|
+
if (found !== "true") throw new Error(`No OCHRE item found for UUID: ${parameters.uuid}`, { cause: parameters.uuid });
|
|
144
|
+
const parsedOcrStrings = ocrString == null ? [] : Array.isArray(ocrString) ? ocrString : [ocrString];
|
|
145
|
+
return {
|
|
146
|
+
ocrStrings: Array.from(parsedOcrStrings, (parsedOcrString) => ({
|
|
147
|
+
resourceUuid: parsedOcrString.resourceUuid !== "" ? parsedOcrString.resourceUuid : null,
|
|
148
|
+
content: parsedOcrString.content,
|
|
149
|
+
x: parseOcrStringNumber(parsedOcrString.x),
|
|
150
|
+
y: parseOcrStringNumber(parsedOcrString.y),
|
|
151
|
+
width: parseOcrStringNumber(parsedOcrString.width),
|
|
152
|
+
height: parseOcrStringNumber(parsedOcrString.height),
|
|
153
|
+
vertices: parseOcrStringVertices(parsedOcrString.vertices)
|
|
154
|
+
})),
|
|
155
|
+
error: null,
|
|
156
|
+
detailedError: null
|
|
157
|
+
};
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
ocrStrings: null,
|
|
161
|
+
...getErrorOutput(error, "Failed to fetch item OCR data")
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { fetchItemOcrData };
|
package/dist/fetchers/item.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { XML_PARSER_OPTIONS } from "../constants.mjs";
|
|
2
|
-
import { createSchemaValidationError, getErrorOutput, stringLiteral } from "../utilities.mjs";
|
|
2
|
+
import { SUPPLEMENTAL_XQUERY_PROLOG, createSchemaValidationError, getErrorOutput, omitSupplemental, stringLiteral } from "../utilities.mjs";
|
|
3
3
|
import { iso639_3Schema, uuidSchema } from "../schemas.mjs";
|
|
4
4
|
import { restoreXMLMetadata } from "../xml/metadata.mjs";
|
|
5
5
|
import { parseItem } from "../parsers/index.mjs";
|
|
@@ -31,27 +31,24 @@ function assertItemCategoryAllowed(category, containedItemCategory) {
|
|
|
31
31
|
for (const possibleCategory of categories) if (isItemContainerCategory(possibleCategory)) return;
|
|
32
32
|
throw new Error(`containedItemCategory can only be used when category is "tree" or "set"; received category "${categories.join(", ")}"`);
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
let $
|
|
52
|
-
${collectionQueries.join(",\n ")}
|
|
53
|
-
)[1]
|
|
54
|
-
let $item := (
|
|
34
|
+
/**
|
|
35
|
+
* Build an XQuery string to fetch a single OCHRE item document by UUID.
|
|
36
|
+
*
|
|
37
|
+
* Nodes marked `supplemental="true"` are always dropped. `$item` only ever
|
|
38
|
+
* binds the item categories that carry embedded items, so the omission branch
|
|
39
|
+
* is a no-op for every other category.
|
|
40
|
+
*
|
|
41
|
+
* @param parameters - The parameters for the fetch
|
|
42
|
+
* @param parameters.uuid - The UUID of the OCHRE item to fetch
|
|
43
|
+
* @param parameters.shouldOmitEmbeddedItems - Whether to drop the embedded item hierarchy
|
|
44
|
+
* @returns An XQuery string
|
|
45
|
+
*/
|
|
46
|
+
function buildXQuery(parameters) {
|
|
47
|
+
const { uuid, shouldOmitEmbeddedItems } = parameters;
|
|
48
|
+
const letClauses = [`let $ochre := doc(${stringLiteral(uuid)})/ochre`];
|
|
49
|
+
let itemNodesExpression = "$ochre/node()";
|
|
50
|
+
if (shouldOmitEmbeddedItems) {
|
|
51
|
+
letClauses.push(`let $item := (
|
|
55
52
|
$ochre/tree,
|
|
56
53
|
$ochre/bibliography,
|
|
57
54
|
$ochre/concept,
|
|
@@ -59,17 +56,25 @@ let $item := (
|
|
|
59
56
|
$ochre/period,
|
|
60
57
|
$ochre/resource,
|
|
61
58
|
$ochre/set
|
|
62
|
-
)[1]
|
|
63
|
-
|
|
59
|
+
)[1]`, `let $embedded-child-name := if (local-name($item) = ("tree", "set")) then "items" else local-name($item)`);
|
|
60
|
+
itemNodesExpression = `(
|
|
61
|
+
for $node in $ochre/node()
|
|
62
|
+
return
|
|
63
|
+
if ($node is $item)
|
|
64
|
+
then element { node-name($item) } { $item/@*, $item/node()[not(self::*[local-name() = $embedded-child-name])] }
|
|
65
|
+
else $node
|
|
66
|
+
)`;
|
|
67
|
+
}
|
|
68
|
+
return `xquery version "1.0-ml";
|
|
69
|
+
|
|
70
|
+
${SUPPLEMENTAL_XQUERY_PROLOG}
|
|
71
|
+
|
|
72
|
+
${letClauses.join("\n")}
|
|
64
73
|
return
|
|
65
|
-
if (empty($ochre)
|
|
74
|
+
if (empty($ochre)) then ()
|
|
66
75
|
else element ochre {
|
|
67
76
|
$ochre/@*,
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
if ($node is $item)
|
|
71
|
-
then element { node-name($item) } { $item/@*, $item/node()[not(self::*[local-name() = $embedded-child-name])] }
|
|
72
|
-
else $node
|
|
77
|
+
${omitSupplemental(itemNodesExpression)}
|
|
73
78
|
}`;
|
|
74
79
|
}
|
|
75
80
|
function omitEmbeddedItems(item) {
|
|
@@ -106,39 +111,19 @@ async function fetchItem(uuid, options) {
|
|
|
106
111
|
const parsedUuid = v.parse(uuidSchema, uuid);
|
|
107
112
|
assertItemCategoryAllowed(options?.category, options?.containedItemCategory);
|
|
108
113
|
const shouldOmitEmbeddedItems = options?.shouldOmitEmbeddedItems === true;
|
|
109
|
-
let shouldFetchOmittedEmbeddedItems = shouldOmitEmbeddedItems;
|
|
110
|
-
let omitEmbeddedItemsCategory;
|
|
111
|
-
if (options?.category != null) if (typeof options.category === "string") if (isItemCategoryWithEmbeddedItems(options.category)) omitEmbeddedItemsCategory = options.category;
|
|
112
|
-
else shouldFetchOmittedEmbeddedItems = false;
|
|
113
|
-
else {
|
|
114
|
-
const categories = [];
|
|
115
|
-
for (const possibleCategory of options.category) if (isItemCategoryWithEmbeddedItems(possibleCategory)) categories.push(possibleCategory);
|
|
116
|
-
omitEmbeddedItemsCategory = categories;
|
|
117
|
-
shouldFetchOmittedEmbeddedItems = shouldOmitEmbeddedItems && categories.length > 0;
|
|
118
|
-
}
|
|
119
114
|
const languages = options?.languages == null ? [] : parseLanguages(options.languages);
|
|
120
|
-
const
|
|
121
|
-
const regularItemUrl = `https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?uuid=${parsedUuid}&xsl=none&lang="*"`;
|
|
122
|
-
let response = shouldFetchOmittedEmbeddedItems ? await fetcher("https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?xquery&xsl=none&lang=\"*\"", {
|
|
115
|
+
const response = await (options?.fetch ?? fetch)("https://ochre.lib.uchicago.edu/ochre/v2/ochre.php?xquery&xsl=none&lang=\"*\"", {
|
|
123
116
|
method: "POST",
|
|
124
|
-
body:
|
|
117
|
+
body: buildXQuery({
|
|
118
|
+
uuid: parsedUuid,
|
|
119
|
+
shouldOmitEmbeddedItems
|
|
120
|
+
}),
|
|
125
121
|
headers: { "Content-Type": "application/xquery" }
|
|
126
|
-
})
|
|
122
|
+
});
|
|
127
123
|
if (!response.ok) throw new Error("Failed to fetch OCHRE data", { cause: response.statusText });
|
|
128
124
|
const dataRaw = await response.text();
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
if (shouldFetchOmittedEmbeddedItems && typeof data === "object" && data != null && "result" in data) {
|
|
132
|
-
const result = data.result;
|
|
133
|
-
if (typeof result === "object" && result != null && "ochre" in result) {
|
|
134
|
-
const ochre = result.ochre;
|
|
135
|
-
if (typeof ochre === "object" && ochre != null && (Object.keys(ochre).length === 0 || "payload" in ochre && ochre.payload === "" && Object.keys(ochre).length === 1)) {
|
|
136
|
-
response = await fetcher(regularItemUrl);
|
|
137
|
-
if (!response.ok) throw new Error("Failed to fetch OCHRE data", { cause: response.statusText });
|
|
138
|
-
data = parser.parse(await response.text());
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
125
|
+
const data = new XMLParser(XML_PARSER_OPTIONS).parse(dataRaw);
|
|
126
|
+
if (data.result?.ochre?.uuid == null) throw new Error(`No OCHRE item found for UUID "${parsedUuid}"`, { cause: dataRaw });
|
|
142
127
|
const { success, issues, output } = v.safeParse(XMLData, data);
|
|
143
128
|
if (!success) throw createSchemaValidationError("Failed to parse OCHRE data", issues);
|
|
144
129
|
restoreXMLMetadata(output, data);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BELONGS_TO_COLLECTION_UUID, DEFAULT_LANGUAGES, XML_PARSER_OPTIONS } from "../../constants.mjs";
|
|
2
|
-
import { createSchemaValidationError, getErrorOutput, stringLiteral } from "../../utilities.mjs";
|
|
3
|
-
import { buildAndCtsQueryExpression, buildBelongsToCollectionQueryExpression, buildQueryPlan } from "../../query.mjs";
|
|
2
|
+
import { SUPPLEMENTAL_XQUERY_PROLOG, createSchemaValidationError, getErrorOutput, omitSupplemental, stringLiteral } from "../../utilities.mjs";
|
|
4
3
|
import { iso639_3Schema, setItemsParametersSchema } from "../../schemas.mjs";
|
|
5
4
|
import { restoreXMLMetadata } from "../../xml/metadata.mjs";
|
|
6
5
|
import { parseSetItems } from "../../parsers/index.mjs";
|
|
7
6
|
import { XMLSetItemsData } from "../../xml/schemas.mjs";
|
|
7
|
+
import { buildAndCtsQueryExpression, 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
|
|
@@ -139,18 +139,32 @@ function buildXQuery(parameters) {
|
|
|
139
139
|
const startPosition = (page - 1) * pageSize + 1;
|
|
140
140
|
const setScopeDeclaration = `declare variable $setScopeUuids := (${setScopeUuids.map((uuid) => stringLiteral(uuid)).join(", ")});`;
|
|
141
141
|
const compiledQueryPlan = buildQueryPlan({ queries });
|
|
142
|
-
const baseItemsExpression =
|
|
143
|
-
const itemsQueryExpressions = [];
|
|
142
|
+
const baseItemsExpression = "doc()/ochre/set[@uuid = $setScopeUuids]/items/*";
|
|
144
143
|
const belongsToCollectionQueryExpression = buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, BELONGS_TO_COLLECTION_UUID);
|
|
145
|
-
if (compiledQueryPlan.queryExpression != null) itemsQueryExpressions.push(compiledQueryPlan.queryExpression);
|
|
146
|
-
if (belongsToCollectionQueryExpression != null) itemsQueryExpressions.push(belongsToCollectionQueryExpression);
|
|
147
|
-
const itemsQueryExpression = buildAndCtsQueryExpression(itemsQueryExpressions);
|
|
148
144
|
const orderedItemsClause = buildOrderedItemsClause(sort);
|
|
149
|
-
const xqueryDeclarations = [
|
|
145
|
+
const xqueryDeclarations = [
|
|
146
|
+
"xquery version \"1.0-ml\";",
|
|
147
|
+
setScopeDeclaration,
|
|
148
|
+
SUPPLEMENTAL_XQUERY_PROLOG
|
|
149
|
+
];
|
|
150
150
|
if (compiledQueryPlan.prolog !== "") xqueryDeclarations.push(compiledQueryPlan.prolog);
|
|
151
|
-
const letClauses = Array.from(compiledQueryPlan.
|
|
152
|
-
|
|
153
|
-
|
|
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(" | ")})`}`);
|
|
154
168
|
const itemsClause = letClauses.join("\n ");
|
|
155
169
|
return `${xqueryDeclarations.join("\n\n")}
|
|
156
170
|
|
|
@@ -161,7 +175,7 @@ ${itemsClause}
|
|
|
161
175
|
let $pagedItems := subsequence($orderedItems, ${startPosition}, ${pageSize})
|
|
162
176
|
|
|
163
177
|
return <items totalCount="{$totalCount}" page="${page}" pageSize="${pageSize}">{
|
|
164
|
-
$pagedItems
|
|
178
|
+
${omitSupplemental("$pagedItems")}
|
|
165
179
|
}</items>
|
|
166
180
|
}</ochre>`;
|
|
167
181
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BELONGS_TO_COLLECTION_UUID, DEFAULT_LANGUAGES, XML_PARSER_OPTIONS } from "../../constants.mjs";
|
|
2
|
-
import { createSchemaValidationError, getErrorOutput, stringLiteral } from "../../utilities.mjs";
|
|
2
|
+
import { NOT_SUPPLEMENTAL_PREDICATE, createSchemaValidationError, getErrorOutput, stringLiteral } from "../../utilities.mjs";
|
|
3
3
|
import { MultilingualString } from "../../parsers/multilingual.mjs";
|
|
4
|
-
import { buildAndCtsQueryExpression, buildBelongsToCollectionQueryExpression, buildQueryPlan } from "../../query.mjs";
|
|
5
4
|
import { setPropertyValuesParametersSchema } from "../../schemas.mjs";
|
|
6
5
|
import { parseXMLContent } from "../../parsers/string.mjs";
|
|
6
|
+
import { buildAndCtsQueryExpression, 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
|
|
@@ -191,12 +191,8 @@ 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
193
|
const compiledQueryPlan = buildQueryPlan({ queries: getItemFilterQueriesFromPropertyValueQueries(queries) });
|
|
194
|
-
const baseItemsExpression =
|
|
195
|
-
const itemsQueryExpressions = [];
|
|
194
|
+
const baseItemsExpression = "doc()/ochre/set[@uuid = $setScopeUuids]/items/*";
|
|
196
195
|
const belongsToCollectionQueryExpression = buildBelongsToCollectionQueryExpression(belongsToCollectionScopeUuids, BELONGS_TO_COLLECTION_UUID);
|
|
197
|
-
if (compiledQueryPlan.queryExpression != null) itemsQueryExpressions.push(compiledQueryPlan.queryExpression);
|
|
198
|
-
if (belongsToCollectionQueryExpression != null) itemsQueryExpressions.push(belongsToCollectionQueryExpression);
|
|
199
|
-
const itemsQueryExpression = buildAndCtsQueryExpression(itemsQueryExpressions);
|
|
200
196
|
const valueFilter = isLimitedToLeafPropertyValues ? "[not(@i)]" : "";
|
|
201
197
|
const queryBlocks = [];
|
|
202
198
|
const returnedSequences = [];
|
|
@@ -331,9 +327,9 @@ let $_property-aggregation := xdmp:eager(
|
|
|
331
327
|
let $global-seen := map:map()
|
|
332
328
|
let $variable-seen := map:map()
|
|
333
329
|
return
|
|
334
|
-
for $p in $item/properties/property[${facetPropertyPredicate}]
|
|
330
|
+
for $p in $item/properties/property[${facetPropertyPredicate}]${NOT_SUPPLEMENTAL_PREDICATE}
|
|
335
331
|
let $variable-uuid := string($p/label/@uuid)
|
|
336
|
-
for $v in $p/value${valueFilter}
|
|
332
|
+
for $v in $p/value${valueFilter}${NOT_SUPPLEMENTAL_PREDICATE}
|
|
337
333
|
let $value-uuid := string($v/@uuid)
|
|
338
334
|
let $raw-value := string($v/@rawValue)
|
|
339
335
|
let $data-type := string($v/@dataType)
|
|
@@ -370,7 +366,7 @@ let $_bibliography-aggregation := xdmp:eager(
|
|
|
370
366
|
for $item in $items
|
|
371
367
|
let $seen := map:map()
|
|
372
368
|
return
|
|
373
|
-
for $bibliography in $item/bibliographies/bibliography
|
|
369
|
+
for $bibliography in $item/bibliographies/bibliography${NOT_SUPPLEMENTAL_PREDICATE}
|
|
374
370
|
let $label := string-join($bibliography/identification/label/content[@xml:lang="eng"]//text(), "")
|
|
375
371
|
where string-length($label) gt 0
|
|
376
372
|
return local:add-attribute-facet($bibliography-counts, $seen, $label)
|
|
@@ -390,7 +386,7 @@ let $_period-aggregation := xdmp:eager(
|
|
|
390
386
|
for $item in $items
|
|
391
387
|
let $seen := map:map()
|
|
392
388
|
return
|
|
393
|
-
for $period in $item/periods/period
|
|
389
|
+
for $period in $item/periods/period${NOT_SUPPLEMENTAL_PREDICATE}
|
|
394
390
|
let $label := string-join($period/identification/label/content[@xml:lang="eng"]//text(), "")
|
|
395
391
|
where string-length($label) gt 0
|
|
396
392
|
return local:add-attribute-facet($period-counts, $seen, $label)
|
|
@@ -404,9 +400,23 @@ let $period-values :=
|
|
|
404
400
|
)`);
|
|
405
401
|
returnedSequences.push("$period-values");
|
|
406
402
|
}
|
|
407
|
-
const letClauses = Array.from(compiledQueryPlan.
|
|
408
|
-
|
|
409
|
-
|
|
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(" | ")})`}`);
|
|
410
420
|
const itemsClause = letClauses.join("\n ");
|
|
411
421
|
return `${xqueryDeclarations.join("\n\n")}
|
|
412
422
|
|