ochre-sdk 1.0.78 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -3
- package/dist/_virtual/_rolldown/runtime.mjs +13 -0
- package/dist/categories.d.mts +249 -0
- package/dist/categories.mjs +259 -0
- package/dist/constants.d.mts +14 -0
- package/dist/constants.mjs +18 -1
- package/dist/errors.d.mts +23 -0
- package/dist/{utilities.mjs → errors.mjs} +33 -84
- package/dist/fetchers/gallery.mjs +15 -51
- package/dist/fetchers/item-children.mjs +20 -66
- package/dist/fetchers/item-links.mjs +26 -79
- package/dist/fetchers/item-ocr-data.d.mts +2 -2
- package/dist/fetchers/item-ocr-data.mjs +11 -15
- package/dist/fetchers/item.d.mts +0 -15
- package/dist/fetchers/item.mjs +20 -72
- package/dist/fetchers/request.d.mts +70 -0
- package/dist/fetchers/request.mjs +100 -0
- package/dist/fetchers/set/items.mjs +27 -73
- package/dist/fetchers/set/property-values.d.mts +2 -3
- package/dist/fetchers/set/property-values.mjs +96 -130
- package/dist/fetchers/website-metadata.mjs +35 -57
- package/dist/fetchers/website.d.mts +2 -3
- package/dist/fetchers/website.mjs +22 -31
- package/dist/getters.d.mts +78 -148
- package/dist/getters.mjs +127 -208
- package/dist/helpers.d.mts +0 -4
- package/dist/helpers.mjs +19 -6
- package/dist/index.d.mts +8 -6
- package/dist/index.mjs +6 -4
- package/dist/ocr.d.mts +37 -0
- package/dist/ocr.mjs +52 -0
- package/dist/parsers/helpers.d.mts +21 -1
- package/dist/parsers/helpers.mjs +26 -6
- package/dist/parsers/index.d.mts +0 -8
- package/dist/parsers/index.mjs +118 -259
- package/dist/parsers/languages.d.mts +72 -0
- package/dist/parsers/languages.mjs +132 -0
- package/dist/parsers/multilingual.d.mts +49 -74
- package/dist/parsers/multilingual.mjs +88 -189
- package/dist/parsers/property-token.d.mts +34 -0
- package/dist/parsers/property-token.mjs +29 -0
- package/dist/parsers/string.d.mts +19 -0
- package/dist/parsers/string.mjs +45 -25
- package/dist/parsers/website/bounds.d.mts +10 -0
- package/dist/parsers/website/bounds.mjs +28 -0
- package/dist/parsers/website/components.d.mts +91 -0
- package/dist/parsers/website/components.mjs +681 -0
- package/dist/parsers/website/index.d.mts +0 -7
- package/dist/parsers/website/index.mjs +92 -1153
- package/dist/parsers/website/links.d.mts +36 -0
- package/dist/parsers/website/links.mjs +58 -0
- package/dist/parsers/website/messages.d.mts +24 -0
- package/dist/parsers/website/messages.mjs +31 -0
- package/dist/parsers/website/options.d.mts +6 -0
- package/dist/parsers/website/options.mjs +114 -0
- package/dist/parsers/website/properties.d.mts +12 -0
- package/dist/parsers/website/properties.mjs +158 -0
- package/dist/parsers/website/reader.d.mts +54 -4
- package/dist/parsers/website/reader.mjs +65 -20
- package/dist/parsers/website/slug.d.mts +64 -0
- package/dist/parsers/website/slug.mjs +82 -0
- package/dist/parsers/website/styles.d.mts +28 -0
- package/dist/parsers/website/styles.mjs +103 -0
- package/dist/parsers/website/walk.d.mts +68 -0
- package/dist/parsers/website/walk.mjs +116 -0
- package/dist/query.d.mts +66 -18
- package/dist/query.mjs +202 -48
- package/dist/reflection.d.mts +64 -0
- package/dist/reflection.mjs +79 -0
- package/dist/schemas.d.mts +7 -0
- package/dist/schemas.mjs +12 -3
- package/dist/types/index.d.mts +1 -31
- package/dist/types/utilities.d.mts +9 -0
- package/dist/types/utilities.mjs +1 -0
- package/dist/types/website.d.mts +49 -58
- package/dist/xml/metadata.d.mts +16 -0
- package/dist/xml/metadata.mjs +32 -11
- package/dist/xml/schemas.d.mts +5970 -3
- package/dist/xml/schemas.mjs +43 -45
- package/dist/xml/types.d.mts +13 -30
- package/dist/xquery.d.mts +46 -0
- package/dist/xquery.mjs +66 -0
- package/package.json +3 -3
- package/dist/utilities.d.mts +0 -54
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { componentSchema } from "../../schemas.mjs";
|
|
2
|
+
import { parseXMLContent, transformPermanentIdentificationUrlToItemLink } from "../string.mjs";
|
|
3
|
+
import { parseStringContent } from "../helpers.mjs";
|
|
4
|
+
import { parseBibliographyList, parseIdentification, parseLinks, parseSimplifiedProperties } from "../index.mjs";
|
|
5
|
+
import { parseBounds } from "./bounds.mjs";
|
|
6
|
+
import { findWebsiteLink, findWebsiteLinkByCategories, getWebsiteLinks, parseWebsiteLinkTarget, webImageFromLink } from "./links.mjs";
|
|
7
|
+
import { formatComponentError, formatXMLWebsiteResourceMetadata } from "./messages.mjs";
|
|
8
|
+
import { parseWebsiteOptions } from "./options.mjs";
|
|
9
|
+
import { websitePresentationReader } from "./reader.mjs";
|
|
10
|
+
import { parseResponsiveCssStyles } from "./styles.mjs";
|
|
11
|
+
import { normalizeWebsiteResources } from "./walk.mjs";
|
|
12
|
+
import * as v from "valibot";
|
|
13
|
+
//#region src/parsers/website/components.ts
|
|
14
|
+
/**
|
|
15
|
+
* Default values for a collection's display properties, shared between the
|
|
16
|
+
* "collection" component and the "query" component's collection overrides.
|
|
17
|
+
*/
|
|
18
|
+
const COLLECTION_PROPERTY_DEFAULTS = {
|
|
19
|
+
variant: "slide",
|
|
20
|
+
paginationVariant: "default",
|
|
21
|
+
loadingVariant: "skeleton",
|
|
22
|
+
minimumColumnCount: null,
|
|
23
|
+
maximumColumnCount: null,
|
|
24
|
+
expectedItemCount: null,
|
|
25
|
+
isSortDisplayed: false,
|
|
26
|
+
isUsingQueryParams: false,
|
|
27
|
+
isInteractive: true
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Default values for a collection's image display properties, merged separately
|
|
31
|
+
* from {@link COLLECTION_PROPERTY_DEFAULTS} because overrides of the nested
|
|
32
|
+
* "image" object are themselves partial.
|
|
33
|
+
*/
|
|
34
|
+
const COLLECTION_IMAGE_DEFAULTS = {
|
|
35
|
+
layout: "start",
|
|
36
|
+
fit: "fit",
|
|
37
|
+
alignment: null,
|
|
38
|
+
isPlaceholderDisplayed: true
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Reads the collection display properties explicitly set on a reader, omitting
|
|
42
|
+
* any that are unset. The "collection" component merges these over
|
|
43
|
+
* {@link COLLECTION_PROPERTY_DEFAULTS} and {@link COLLECTION_IMAGE_DEFAULTS},
|
|
44
|
+
* while the "query" component uses them as partial overrides for its embedded
|
|
45
|
+
* collection.
|
|
46
|
+
*/
|
|
47
|
+
function parseCollectionPropertyOverrides(reader) {
|
|
48
|
+
const overrides = {};
|
|
49
|
+
const imageOverrides = {};
|
|
50
|
+
function read(key, label) {
|
|
51
|
+
const value = reader.value(label);
|
|
52
|
+
if (value != null) overrides[key] = value;
|
|
53
|
+
}
|
|
54
|
+
function readImage(key, label) {
|
|
55
|
+
const value = reader.value(label);
|
|
56
|
+
if (value != null) imageOverrides[key] = value;
|
|
57
|
+
}
|
|
58
|
+
read("variant", "variant");
|
|
59
|
+
read("paginationVariant", "pagination-variant");
|
|
60
|
+
read("loadingVariant", "loading-variant");
|
|
61
|
+
read("minimumColumnCount", "minimum-column-count");
|
|
62
|
+
read("maximumColumnCount", "maximum-column-count");
|
|
63
|
+
read("expectedItemCount", "item-count");
|
|
64
|
+
read("isSortDisplayed", "sort-displayed");
|
|
65
|
+
read("isUsingQueryParams", "is-using-query-params");
|
|
66
|
+
read("isInteractive", "is-interactive");
|
|
67
|
+
readImage("layout", "image-layout");
|
|
68
|
+
readImage("fit", "image-fit");
|
|
69
|
+
readImage("alignment", "image-alignment");
|
|
70
|
+
readImage("isPlaceholderDisplayed", "image-placeholder-displayed");
|
|
71
|
+
return Object.keys(imageOverrides).length > 0 ? {
|
|
72
|
+
...overrides,
|
|
73
|
+
image: imageOverrides
|
|
74
|
+
} : overrides;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parses the "use-property" values defining which item properties a collection
|
|
78
|
+
* displays, returning `null` when none are set.
|
|
79
|
+
*/
|
|
80
|
+
function parseCollectionDisplayedProperties(reader) {
|
|
81
|
+
const property = reader.property("use-property");
|
|
82
|
+
if (property == null) return null;
|
|
83
|
+
return property.values.flatMap((value) => value.uuid === null ? [] : [{
|
|
84
|
+
uuid: value.uuid,
|
|
85
|
+
label: value.label
|
|
86
|
+
}]);
|
|
87
|
+
}
|
|
88
|
+
const THREE_D_VIEWER_COMPONENT_DEFAULTS = {
|
|
89
|
+
isInteractive: true,
|
|
90
|
+
isControlsDisplayed: true
|
|
91
|
+
};
|
|
92
|
+
function parse3dViewerComponent(parameters) {
|
|
93
|
+
const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
|
|
94
|
+
const resourceLink = findWebsiteLink(websiteLinks, "resource", (link) => link.fileFormat === "model/obj");
|
|
95
|
+
if (resourceLink == null) throw new Error(formatComponentError("Resource link not found", "3d-viewer", elementResource), { cause: componentProperty });
|
|
96
|
+
const properties = { ...THREE_D_VIEWER_COMPONENT_DEFAULTS };
|
|
97
|
+
componentReader.readAll(properties, {
|
|
98
|
+
isInteractive: "is-interactive",
|
|
99
|
+
isControlsDisplayed: "controls-displayed"
|
|
100
|
+
});
|
|
101
|
+
const { isInteractive, isControlsDisplayed } = properties;
|
|
102
|
+
return {
|
|
103
|
+
component: "3d-viewer",
|
|
104
|
+
linkUuid: resourceLink.uuid,
|
|
105
|
+
fileSize: resourceLink.fileSize,
|
|
106
|
+
isInteractive,
|
|
107
|
+
isControlsDisplayed
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function parseAdvancedSearchComponent(parameters) {
|
|
111
|
+
const { componentProperty, componentReader, elementResource, options, pageSlugs } = parameters;
|
|
112
|
+
const boundElementPropertyUuid = componentReader.uuid("bound-element");
|
|
113
|
+
const href = parseWebsiteLinkTarget(componentReader.valueNode("link-to"), pageSlugs);
|
|
114
|
+
if (boundElementPropertyUuid == null && href == null) throw new Error(formatComponentError("Bound element or href not found", "advanced-search", elementResource), { cause: componentProperty });
|
|
115
|
+
return {
|
|
116
|
+
component: "advanced-search",
|
|
117
|
+
boundElementUuid: boundElementPropertyUuid,
|
|
118
|
+
href,
|
|
119
|
+
options: parseWebsiteOptions(elementResource.options, options)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function parseAnnotatedDocumentComponent(parameters) {
|
|
123
|
+
const { componentProperty, elementResource, websiteLinks } = parameters;
|
|
124
|
+
const documentLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "internalDocument");
|
|
125
|
+
if (documentLink == null) throw new Error(formatComponentError("Document link not found", "annotated-document", elementResource), { cause: componentProperty });
|
|
126
|
+
return {
|
|
127
|
+
component: "annotated-document",
|
|
128
|
+
linkUuid: documentLink.uuid
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const ANNOTATED_IMAGE_COMPONENT_DEFAULTS = {
|
|
132
|
+
isFilterInputDisplayed: true,
|
|
133
|
+
isOptionsDisplayed: true,
|
|
134
|
+
isAnnotationHighlightsDisplayed: true,
|
|
135
|
+
isAnnotationTooltipsDisplayed: true
|
|
136
|
+
};
|
|
137
|
+
function parseAnnotatedImageComponent(parameters) {
|
|
138
|
+
const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
|
|
139
|
+
const imageLinks = getWebsiteLinks(websiteLinks, "resource").filter((link) => link.type === "image" || link.type === "IIIF");
|
|
140
|
+
if (imageLinks.length === 0) throw new Error(formatComponentError("Image link not found", "annotated-image", elementResource), { cause: componentProperty });
|
|
141
|
+
const properties = { ...ANNOTATED_IMAGE_COMPONENT_DEFAULTS };
|
|
142
|
+
componentReader.readAll(properties, {
|
|
143
|
+
isFilterInputDisplayed: "filter-input-displayed",
|
|
144
|
+
isOptionsDisplayed: "options-displayed",
|
|
145
|
+
isAnnotationHighlightsDisplayed: "annotation-highlights-displayed",
|
|
146
|
+
isAnnotationTooltipsDisplayed: "annotation-tooltips-displayed"
|
|
147
|
+
});
|
|
148
|
+
const { isFilterInputDisplayed, isOptionsDisplayed, isAnnotationHighlightsDisplayed, isAnnotationTooltipsDisplayed } = properties;
|
|
149
|
+
return {
|
|
150
|
+
component: "annotated-image",
|
|
151
|
+
linkUuid: imageLinks[0].uuid,
|
|
152
|
+
isFilterInputDisplayed,
|
|
153
|
+
isOptionsDisplayed,
|
|
154
|
+
isAnnotationHighlightsDisplayed,
|
|
155
|
+
isAnnotationTooltipsDisplayed
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const AUDIO_PLAYER_COMPONENT_DEFAULTS = {
|
|
159
|
+
isSpeedControlsDisplayed: true,
|
|
160
|
+
isVolumeControlsDisplayed: true,
|
|
161
|
+
isSeekBarDisplayed: true
|
|
162
|
+
};
|
|
163
|
+
function parseAudioPlayerComponent(parameters) {
|
|
164
|
+
const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
|
|
165
|
+
const audioLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "audio");
|
|
166
|
+
if (audioLink == null) throw new Error(formatComponentError("Audio link not found", "audio-player", elementResource), { cause: componentProperty });
|
|
167
|
+
const properties = { ...AUDIO_PLAYER_COMPONENT_DEFAULTS };
|
|
168
|
+
componentReader.readAll(properties, {
|
|
169
|
+
isSpeedControlsDisplayed: "speed-controls-displayed",
|
|
170
|
+
isVolumeControlsDisplayed: "volume-controls-displayed",
|
|
171
|
+
isSeekBarDisplayed: "seek-bar-displayed"
|
|
172
|
+
});
|
|
173
|
+
const { isSpeedControlsDisplayed, isVolumeControlsDisplayed, isSeekBarDisplayed } = properties;
|
|
174
|
+
return {
|
|
175
|
+
component: "audio-player",
|
|
176
|
+
linkUuid: audioLink.uuid,
|
|
177
|
+
isSpeedControlsDisplayed,
|
|
178
|
+
isVolumeControlsDisplayed,
|
|
179
|
+
isSeekBarDisplayed
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function parseBibliographyComponent(parameters) {
|
|
183
|
+
const { componentProperty, componentReader, elementResource, websiteLinks, options } = parameters;
|
|
184
|
+
const bibliographies = parseBibliographyList(elementResource.bibliographies, options);
|
|
185
|
+
if (websiteLinks.length === 0 && bibliographies.length === 0) throw new Error(formatComponentError("No links found", "bibliography", elementResource), { cause: componentProperty });
|
|
186
|
+
const layout = componentReader.valueOr("layout", "long");
|
|
187
|
+
const isSourceDocumentDisplayed = componentReader.valueOr("source-document-displayed", true);
|
|
188
|
+
return {
|
|
189
|
+
component: "bibliography",
|
|
190
|
+
linkUuids: websiteLinks.map((link) => link.uuid),
|
|
191
|
+
bibliographies,
|
|
192
|
+
layout,
|
|
193
|
+
isSourceDocumentDisplayed
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function parseButtonComponent(parameters) {
|
|
197
|
+
const { componentProperty, componentReader, elementResource, websiteLinks, options, pageSlugs } = parameters;
|
|
198
|
+
const variant = componentReader.valueOr("variant", "default");
|
|
199
|
+
let isExternal = false;
|
|
200
|
+
let isRelative = false;
|
|
201
|
+
let href = parseWebsiteLinkTarget(componentReader.valueNode("navigate-to"), pageSlugs);
|
|
202
|
+
if (href === null) {
|
|
203
|
+
href = parseWebsiteLinkTarget(componentReader.valueNode("link-to"), pageSlugs);
|
|
204
|
+
if (href === null) throw new Error(formatComponentError("Properties “navigate-to” or “link-to” not found", "button", elementResource), { cause: componentProperty });
|
|
205
|
+
isExternal = href.startsWith("http");
|
|
206
|
+
isRelative = !href.startsWith("/");
|
|
207
|
+
}
|
|
208
|
+
const startIcon = componentReader.value("start-icon");
|
|
209
|
+
const endIcon = componentReader.value("end-icon");
|
|
210
|
+
const imageLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "image" || link.type === "IIIF");
|
|
211
|
+
const image = imageLink == null ? null : webImageFromLink(imageLink, "high");
|
|
212
|
+
const childResources = normalizeWebsiteResources(elementResource.resource);
|
|
213
|
+
const elements = [];
|
|
214
|
+
for (const childResource of childResources) {
|
|
215
|
+
const childReader = websitePresentationReader(parseSimplifiedProperties(childResource.properties, options));
|
|
216
|
+
if (childReader.value("presentation") !== "element") continue;
|
|
217
|
+
if (childReader.nestedByValue("presentation", "element").value("component") === "button") continue;
|
|
218
|
+
elements.push(parseWebElement(childResource, options, pageSlugs));
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
component: "button",
|
|
222
|
+
variant,
|
|
223
|
+
href,
|
|
224
|
+
isExternal,
|
|
225
|
+
isRelative,
|
|
226
|
+
label: elementResource.document && "content" in elementResource.document ? parseXMLContent(elementResource.document, options) : null,
|
|
227
|
+
startIcon,
|
|
228
|
+
endIcon,
|
|
229
|
+
image,
|
|
230
|
+
elements
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const COLLECTION_FILTER_DEFAULTS = {
|
|
234
|
+
isResultsBarDisplayed: false,
|
|
235
|
+
isInputDisplayed: false,
|
|
236
|
+
isLimitedToInputFilter: false,
|
|
237
|
+
isLimitedToLeafPropertyValues: false,
|
|
238
|
+
isSidebarDisplayed: false,
|
|
239
|
+
sidebarSort: "default",
|
|
240
|
+
isSidebarHelpTooltipsDisplayed: false
|
|
241
|
+
};
|
|
242
|
+
function parseCollectionComponent(parameters) {
|
|
243
|
+
const { componentProperty, componentReader, elementResource, websiteLinks, options } = parameters;
|
|
244
|
+
const setLinks = getWebsiteLinks(websiteLinks, "set");
|
|
245
|
+
if (setLinks.length === 0) throw new Error(formatComponentError("Set links not found", "collection", elementResource), { cause: componentProperty });
|
|
246
|
+
const filter = { ...COLLECTION_FILTER_DEFAULTS };
|
|
247
|
+
componentReader.readAll(filter, {
|
|
248
|
+
isResultsBarDisplayed: "filter-results-bar-displayed",
|
|
249
|
+
isInputDisplayed: "filter-input-displayed",
|
|
250
|
+
isLimitedToInputFilter: "filter-limit-to-input-filter",
|
|
251
|
+
isLimitedToLeafPropertyValues: "filter-limit-to-leaf-property-values",
|
|
252
|
+
isSidebarDisplayed: "filter-sidebar-displayed",
|
|
253
|
+
sidebarSort: "filter-sidebar-sort",
|
|
254
|
+
isSidebarHelpTooltipsDisplayed: "filter-sidebar-help-tooltips-displayed"
|
|
255
|
+
});
|
|
256
|
+
const componentOptions = parseWebsiteOptions(elementResource.options, options);
|
|
257
|
+
const propertyOverrides = parseCollectionPropertyOverrides(componentReader);
|
|
258
|
+
return {
|
|
259
|
+
component: "collection",
|
|
260
|
+
linkUuids: setLinks.map((link) => link.uuid),
|
|
261
|
+
displayedProperties: parseCollectionDisplayedProperties(componentReader),
|
|
262
|
+
...COLLECTION_PROPERTY_DEFAULTS,
|
|
263
|
+
...propertyOverrides,
|
|
264
|
+
image: {
|
|
265
|
+
...COLLECTION_IMAGE_DEFAULTS,
|
|
266
|
+
...propertyOverrides.image
|
|
267
|
+
},
|
|
268
|
+
filter,
|
|
269
|
+
options: componentOptions
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function parseEmptySpaceComponent(parameters) {
|
|
273
|
+
const { componentReader } = parameters;
|
|
274
|
+
return {
|
|
275
|
+
component: "empty-space",
|
|
276
|
+
height: componentReader.stringValue("height"),
|
|
277
|
+
width: componentReader.stringValue("width")
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function parseEntriesComponent(parameters) {
|
|
281
|
+
const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
|
|
282
|
+
const entriesLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
|
|
283
|
+
if (entriesLink == null) throw new Error(formatComponentError("Entries link not found", "entries", elementResource), { cause: componentProperty });
|
|
284
|
+
const variant = componentReader.valueOr("variant", "entry");
|
|
285
|
+
const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", false);
|
|
286
|
+
return {
|
|
287
|
+
component: "entries",
|
|
288
|
+
linkUuid: entriesLink.uuid,
|
|
289
|
+
variant,
|
|
290
|
+
isFilterInputDisplayed
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function parseIframeComponent(parameters) {
|
|
294
|
+
const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
|
|
295
|
+
const webpageLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "webpage");
|
|
296
|
+
if (webpageLink?.href == null) throw new Error(formatComponentError("URL not found", "iframe", elementResource), { cause: componentProperty });
|
|
297
|
+
return {
|
|
298
|
+
component: "iframe",
|
|
299
|
+
href: transformPermanentIdentificationUrlToItemLink(webpageLink.href),
|
|
300
|
+
height: componentReader.stringValue("height"),
|
|
301
|
+
width: componentReader.stringValue("width")
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function parseIiifViewerComponent(parameters) {
|
|
305
|
+
const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
|
|
306
|
+
const manifestLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "IIIF");
|
|
307
|
+
if (manifestLink == null) throw new Error(formatComponentError("Manifest link not found", "iiif-viewer", elementResource), { cause: componentProperty });
|
|
308
|
+
const variant = componentReader.valueOr("variant", "universal-viewer");
|
|
309
|
+
return {
|
|
310
|
+
component: "iiif-viewer",
|
|
311
|
+
linkUuid: manifestLink.uuid,
|
|
312
|
+
variant
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The defaults an image component falls back to
|
|
317
|
+
*
|
|
318
|
+
* Annotated rather than `as const`, so each field keeps the full union its
|
|
319
|
+
* type allows and the parser can still compare against the other members.
|
|
320
|
+
*/
|
|
321
|
+
const IMAGE_COMPONENT_DEFAULTS = {
|
|
322
|
+
imageQuality: "high",
|
|
323
|
+
variant: "default",
|
|
324
|
+
captionLayout: "bottom",
|
|
325
|
+
isFullWidth: true,
|
|
326
|
+
isFullHeight: true,
|
|
327
|
+
captionSource: "name",
|
|
328
|
+
altTextSource: "name",
|
|
329
|
+
isTransparentBackground: false,
|
|
330
|
+
isCover: false
|
|
331
|
+
};
|
|
332
|
+
function parseImageComponent(parameters) {
|
|
333
|
+
const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
|
|
334
|
+
if (websiteLinks.length === 0) throw new Error(formatComponentError("No links found", "image", elementResource), { cause: componentProperty });
|
|
335
|
+
const properties = { ...IMAGE_COMPONENT_DEFAULTS };
|
|
336
|
+
componentReader.readAll(properties, {
|
|
337
|
+
imageQuality: "image-quality",
|
|
338
|
+
variant: "variant",
|
|
339
|
+
captionLayout: "layout-caption",
|
|
340
|
+
isFullWidth: "is-full-width",
|
|
341
|
+
isFullHeight: "is-full-height",
|
|
342
|
+
captionSource: "source-caption",
|
|
343
|
+
altTextSource: "alt-text-source",
|
|
344
|
+
isTransparentBackground: "is-transparent",
|
|
345
|
+
isCover: "is-cover"
|
|
346
|
+
});
|
|
347
|
+
const { imageQuality, variant, captionLayout, isFullWidth, isFullHeight, captionSource, altTextSource, isTransparentBackground, isCover } = properties;
|
|
348
|
+
const images = Array.from(websiteLinks, (link) => webImageFromLink(link, imageQuality));
|
|
349
|
+
const width = componentReader.stringValue("width");
|
|
350
|
+
const height = componentReader.stringValue("height");
|
|
351
|
+
const variantReader = componentReader.nested("variant");
|
|
352
|
+
let carouselOptions = null;
|
|
353
|
+
if (images.length > 1) {
|
|
354
|
+
const secondsPerImage = variant === "carousel" ? variantReader.stringValue("seconds-per-image") : null;
|
|
355
|
+
carouselOptions = { secondsPerImage: Number(secondsPerImage ?? 5) };
|
|
356
|
+
}
|
|
357
|
+
let heroOptions = null;
|
|
358
|
+
if (variant === "hero") heroOptions = {
|
|
359
|
+
isBackgroundImageDisplayed: variantReader.valueOr("background-image-displayed", true),
|
|
360
|
+
isDocumentDisplayed: variantReader.valueOr("document-displayed", true)
|
|
361
|
+
};
|
|
362
|
+
return {
|
|
363
|
+
component: "image",
|
|
364
|
+
images,
|
|
365
|
+
variant,
|
|
366
|
+
width,
|
|
367
|
+
height,
|
|
368
|
+
isFullWidth,
|
|
369
|
+
isFullHeight,
|
|
370
|
+
imageQuality,
|
|
371
|
+
captionLayout,
|
|
372
|
+
captionSource,
|
|
373
|
+
altTextSource,
|
|
374
|
+
isTransparentBackground,
|
|
375
|
+
isCover,
|
|
376
|
+
carouselOptions,
|
|
377
|
+
heroOptions
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function parseImageGalleryComponent(parameters) {
|
|
381
|
+
const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
|
|
382
|
+
const galleryLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
|
|
383
|
+
if (galleryLink == null) throw new Error(formatComponentError("Image gallery link not found", "image-gallery", elementResource), { cause: componentProperty });
|
|
384
|
+
const isFilterInputDisplayed = componentReader.valueOr("filter-input-displayed", true);
|
|
385
|
+
return {
|
|
386
|
+
component: "image-gallery",
|
|
387
|
+
linkUuid: galleryLink.uuid,
|
|
388
|
+
isFilterInputDisplayed
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const MAP_COMPONENT_DEFAULTS = {
|
|
392
|
+
isInteractive: true,
|
|
393
|
+
isClustered: false,
|
|
394
|
+
isUsingPins: false,
|
|
395
|
+
isControlsDisplayed: false,
|
|
396
|
+
isFullHeight: false
|
|
397
|
+
};
|
|
398
|
+
function parseMapComponent(parameters) {
|
|
399
|
+
const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
|
|
400
|
+
const mapLink = findWebsiteLinkByCategories(websiteLinks, ["set", "tree"]);
|
|
401
|
+
if (mapLink == null) throw new Error(formatComponentError("Map link not found", "map", elementResource), { cause: componentProperty });
|
|
402
|
+
const properties = { ...MAP_COMPONENT_DEFAULTS };
|
|
403
|
+
componentReader.readAll(properties, {
|
|
404
|
+
isInteractive: "is-interactive",
|
|
405
|
+
isClustered: "is-clustered",
|
|
406
|
+
isUsingPins: "is-using-pins",
|
|
407
|
+
isControlsDisplayed: "controls-displayed",
|
|
408
|
+
isFullHeight: "is-full-height"
|
|
409
|
+
});
|
|
410
|
+
const { isInteractive, isClustered, isUsingPins, isControlsDisplayed, isFullHeight } = properties;
|
|
411
|
+
const customBasemap = componentReader.value("custom-basemap");
|
|
412
|
+
let initialBounds = null;
|
|
413
|
+
const initialBoundsProperty = componentReader.value("initial-bounds");
|
|
414
|
+
if (initialBoundsProperty !== null) initialBounds = parseBounds(String(initialBoundsProperty));
|
|
415
|
+
let maximumBounds = null;
|
|
416
|
+
const maximumBoundsProperty = componentReader.value("maximum-bounds");
|
|
417
|
+
if (maximumBoundsProperty !== null) maximumBounds = parseBounds(String(maximumBoundsProperty));
|
|
418
|
+
return {
|
|
419
|
+
component: "map",
|
|
420
|
+
linkUuid: mapLink.uuid,
|
|
421
|
+
customBasemap,
|
|
422
|
+
initialBounds,
|
|
423
|
+
maximumBounds,
|
|
424
|
+
isInteractive,
|
|
425
|
+
isClustered,
|
|
426
|
+
isUsingPins,
|
|
427
|
+
isControlsDisplayed,
|
|
428
|
+
isFullHeight
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function parseQueryItemQueries(propertyVariables, queryLanguage, elementResource) {
|
|
432
|
+
const queries = [];
|
|
433
|
+
for (const propertyVariable of propertyVariables) {
|
|
434
|
+
if (propertyVariable.uuid === null) throw new Error(formatComponentError("Property variable UUID not found", "query", elementResource), { cause: propertyVariable });
|
|
435
|
+
const dataType = propertyVariable.dataType;
|
|
436
|
+
if (dataType === "coordinate") throw new Error(formatComponentError("Query prompts with data type \"coordinate\" are not supported", "query", elementResource), { cause: propertyVariable });
|
|
437
|
+
queries.push({
|
|
438
|
+
target: "property",
|
|
439
|
+
propertyVariable: propertyVariable.uuid,
|
|
440
|
+
dataType,
|
|
441
|
+
matchMode: "exact",
|
|
442
|
+
isCaseSensitive: true,
|
|
443
|
+
language: queryLanguage
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return queries;
|
|
447
|
+
}
|
|
448
|
+
function parseQueryComponent(parameters) {
|
|
449
|
+
const { componentProperty, componentReader, elementResource, websiteLinks, options } = parameters;
|
|
450
|
+
const setLinks = getWebsiteLinks(websiteLinks, "set");
|
|
451
|
+
if (setLinks.length === 0) throw new Error(formatComponentError("Set links not found", "query", elementResource), { cause: componentProperty });
|
|
452
|
+
if (componentProperty.properties.length === 0) throw new Error(formatComponentError("Query properties not found", "query", elementResource), { cause: componentProperty });
|
|
453
|
+
const items = [];
|
|
454
|
+
for (const queryItem of componentProperty.properties) {
|
|
455
|
+
const queryReader = websitePresentationReader(queryItem.properties);
|
|
456
|
+
const label = queryReader.multilingualValue("query-prompt", options);
|
|
457
|
+
if (label === null) continue;
|
|
458
|
+
const propertyVariables = queryReader.property("use-property")?.values.filter((value) => value.uuid !== null) ?? [];
|
|
459
|
+
const queryLanguage = options.languages[0];
|
|
460
|
+
if (queryLanguage == null) throw new Error(formatComponentError("Query language not found", "query", elementResource));
|
|
461
|
+
const queries = parseQueryItemQueries(propertyVariables, queryLanguage, elementResource);
|
|
462
|
+
const startIcon = queryReader.value("start-icon");
|
|
463
|
+
const endIcon = queryReader.value("end-icon");
|
|
464
|
+
items.push({
|
|
465
|
+
label,
|
|
466
|
+
queries,
|
|
467
|
+
startIcon,
|
|
468
|
+
endIcon
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (items.length === 0) throw new Error(formatComponentError("No queries found", "query", elementResource), { cause: componentProperty });
|
|
472
|
+
const componentOptions = parseWebsiteOptions(elementResource.options, options);
|
|
473
|
+
const collectionProperties = parseCollectionPropertyOverrides(componentReader.nestedByValue("sub-component-override", "collection"));
|
|
474
|
+
const displayedProperties = parseCollectionDisplayedProperties(componentReader);
|
|
475
|
+
if (displayedProperties != null) collectionProperties.displayedProperties = displayedProperties;
|
|
476
|
+
return {
|
|
477
|
+
component: "query",
|
|
478
|
+
linkUuids: setLinks.map((link) => link.uuid),
|
|
479
|
+
items,
|
|
480
|
+
options: componentOptions,
|
|
481
|
+
collectionProperties
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function parseTableComponent(parameters) {
|
|
485
|
+
const { componentProperty, elementResource, websiteLinks } = parameters;
|
|
486
|
+
const tableLink = findWebsiteLink(websiteLinks, "set");
|
|
487
|
+
if (tableLink == null) throw new Error(formatComponentError("Table link not found", "table", elementResource), { cause: componentProperty });
|
|
488
|
+
return {
|
|
489
|
+
component: "table",
|
|
490
|
+
linkUuid: tableLink.uuid
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function parseSearchBarComponent(parameters) {
|
|
494
|
+
const { componentProperty, componentReader, elementResource, options, pageSlugs } = parameters;
|
|
495
|
+
const queryVariant = componentReader.valueOr("query-variant", "submit");
|
|
496
|
+
const boundElementUuid = componentReader.uuid("bound-element");
|
|
497
|
+
const href = parseWebsiteLinkTarget(componentReader.valueNode("link-to"), pageSlugs);
|
|
498
|
+
if (boundElementUuid === null && href === null) throw new Error(formatComponentError("Bound element or href not found", "search-bar", elementResource), { cause: componentProperty });
|
|
499
|
+
return {
|
|
500
|
+
component: "search-bar",
|
|
501
|
+
queryVariant,
|
|
502
|
+
placeholder: componentReader.multilingualValue("placeholder-text", options),
|
|
503
|
+
baseFilterQueries: componentReader.value("base-filter-queries")?.replaceAll(String.raw`\{`, "{").replaceAll(String.raw`\}`, "}") ?? null,
|
|
504
|
+
boundElementUuid,
|
|
505
|
+
href
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function parseTextComponent(parameters) {
|
|
509
|
+
const { componentProperty, componentReader, elementResource, options } = parameters;
|
|
510
|
+
const content = elementResource.document && "content" in elementResource.document ? parseXMLContent(elementResource.document, options) : null;
|
|
511
|
+
if (content == null) throw new Error(formatComponentError("Content not found", "text", elementResource), { cause: componentProperty });
|
|
512
|
+
let variantName = "block";
|
|
513
|
+
let variant;
|
|
514
|
+
const variantProperty = componentReader.property("variant");
|
|
515
|
+
if (variantProperty !== null) {
|
|
516
|
+
const variantReader = websitePresentationReader(variantProperty.properties);
|
|
517
|
+
variantName = variantProperty.values[0].content;
|
|
518
|
+
switch (variantName) {
|
|
519
|
+
case "paragraph":
|
|
520
|
+
variant = {
|
|
521
|
+
name: variantName,
|
|
522
|
+
size: variantReader.valueOr("size", "md")
|
|
523
|
+
};
|
|
524
|
+
break;
|
|
525
|
+
case "label":
|
|
526
|
+
variant = {
|
|
527
|
+
name: variantName,
|
|
528
|
+
size: variantReader.valueOr("size", "md")
|
|
529
|
+
};
|
|
530
|
+
break;
|
|
531
|
+
case "heading":
|
|
532
|
+
variant = {
|
|
533
|
+
name: variantName,
|
|
534
|
+
size: variantReader.valueOr("size", "md")
|
|
535
|
+
};
|
|
536
|
+
break;
|
|
537
|
+
case "display":
|
|
538
|
+
variant = {
|
|
539
|
+
name: variantName,
|
|
540
|
+
size: variantReader.valueOr("size", "md")
|
|
541
|
+
};
|
|
542
|
+
break;
|
|
543
|
+
default: variant = { name: variantName };
|
|
544
|
+
}
|
|
545
|
+
} else variant = { name: variantName };
|
|
546
|
+
const headingLevel = componentReader.value("heading-level");
|
|
547
|
+
return {
|
|
548
|
+
component: "text",
|
|
549
|
+
variant,
|
|
550
|
+
headingLevel,
|
|
551
|
+
content
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function parseTimelineComponent(parameters) {
|
|
555
|
+
const { componentProperty, elementResource, websiteLinks } = parameters;
|
|
556
|
+
const timelineLink = findWebsiteLink(websiteLinks, "tree");
|
|
557
|
+
if (timelineLink == null) throw new Error(formatComponentError("Timeline link not found", "timeline", elementResource), { cause: componentProperty });
|
|
558
|
+
return {
|
|
559
|
+
component: "timeline",
|
|
560
|
+
linkUuid: timelineLink.uuid
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function parseVideoComponent(parameters) {
|
|
564
|
+
const { componentProperty, componentReader, elementResource, websiteLinks } = parameters;
|
|
565
|
+
const videoLink = findWebsiteLink(websiteLinks, "resource", (link) => link.type === "video");
|
|
566
|
+
if (videoLink == null) throw new Error(formatComponentError("Video link not found", "video", elementResource), { cause: componentProperty });
|
|
567
|
+
const isChaptersDisplayed = componentReader.valueOr("chapters-displayed", true);
|
|
568
|
+
return {
|
|
569
|
+
component: "video",
|
|
570
|
+
linkUuid: videoLink.uuid,
|
|
571
|
+
isChaptersDisplayed
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Parses raw web element properties into a standardized WebElementComponent structure
|
|
576
|
+
*
|
|
577
|
+
* @param componentProperty - Raw component property data in OCHRE format
|
|
578
|
+
* @param elementResource - Raw element resource data in OCHRE format
|
|
579
|
+
* @returns Parsed WebElementComponent object
|
|
580
|
+
*/
|
|
581
|
+
/**
|
|
582
|
+
* The parser for each web element component, keyed by its OCHRE component name
|
|
583
|
+
*
|
|
584
|
+
* Every component parser takes the same {@link WebElementComponentParameters}
|
|
585
|
+
* bag, so this is a table rather than a `switch`: a missing component is a type
|
|
586
|
+
* error here instead of a runtime throw, and each parser can be exercised on
|
|
587
|
+
* its own.
|
|
588
|
+
*/
|
|
589
|
+
const WEB_ELEMENT_COMPONENT_PARSERS = {
|
|
590
|
+
"3d-viewer": parse3dViewerComponent,
|
|
591
|
+
"advanced-search": parseAdvancedSearchComponent,
|
|
592
|
+
"annotated-document": parseAnnotatedDocumentComponent,
|
|
593
|
+
"annotated-image": parseAnnotatedImageComponent,
|
|
594
|
+
"audio-player": parseAudioPlayerComponent,
|
|
595
|
+
bibliography: parseBibliographyComponent,
|
|
596
|
+
button: parseButtonComponent,
|
|
597
|
+
collection: parseCollectionComponent,
|
|
598
|
+
"empty-space": parseEmptySpaceComponent,
|
|
599
|
+
entries: parseEntriesComponent,
|
|
600
|
+
iframe: parseIframeComponent,
|
|
601
|
+
"iiif-viewer": parseIiifViewerComponent,
|
|
602
|
+
image: parseImageComponent,
|
|
603
|
+
"image-gallery": parseImageGalleryComponent,
|
|
604
|
+
map: parseMapComponent,
|
|
605
|
+
query: parseQueryComponent,
|
|
606
|
+
table: parseTableComponent,
|
|
607
|
+
"search-bar": parseSearchBarComponent,
|
|
608
|
+
text: parseTextComponent,
|
|
609
|
+
timeline: parseTimelineComponent,
|
|
610
|
+
video: parseVideoComponent
|
|
611
|
+
};
|
|
612
|
+
function parseWebElementProperties(componentProperty, elementResource, options, pageSlugs) {
|
|
613
|
+
const unparsedComponentName = componentProperty.values[0].content;
|
|
614
|
+
const componentNameResult = v.safeParse(componentSchema, unparsedComponentName);
|
|
615
|
+
const parse = componentNameResult.success ? WEB_ELEMENT_COMPONENT_PARSERS[componentNameResult.output] : void 0;
|
|
616
|
+
if (parse == null) throw new Error(`Invalid or non-implemented component name \u{201C}${unparsedComponentName.toString()}\u{201D} for the following element: \u{201C}${parseStringContent(elementResource.identification.label, options)}\u{201D}`);
|
|
617
|
+
return parse({
|
|
618
|
+
componentProperty,
|
|
619
|
+
componentReader: websitePresentationReader(componentProperty.properties),
|
|
620
|
+
elementResource,
|
|
621
|
+
websiteLinks: parseLinks(elementResource.links, options),
|
|
622
|
+
options,
|
|
623
|
+
pageSlugs
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function parseWebTitle(properties, identification, overrides) {
|
|
627
|
+
const title = {
|
|
628
|
+
label: identification.label,
|
|
629
|
+
variant: "default",
|
|
630
|
+
properties: {
|
|
631
|
+
isNameDisplayed: overrides?.isNameDisplayed ?? false,
|
|
632
|
+
isDescriptionDisplayed: overrides?.isDescriptionDisplayed ?? false,
|
|
633
|
+
isDateDisplayed: overrides?.isDateDisplayed ?? false,
|
|
634
|
+
isCreatorsDisplayed: overrides?.isCreatorsDisplayed ?? false,
|
|
635
|
+
isCountDisplayed: overrides?.isCountDisplayed ?? false
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const titleReader = websitePresentationReader(properties).nestedByValue("presentation", "title");
|
|
639
|
+
if (titleReader.size > 0) {
|
|
640
|
+
titleReader.readAll(title, { variant: "variant" });
|
|
641
|
+
titleReader.readAll(title.properties, {
|
|
642
|
+
isNameDisplayed: "name-displayed",
|
|
643
|
+
isDescriptionDisplayed: "description-displayed",
|
|
644
|
+
isDateDisplayed: "date-displayed",
|
|
645
|
+
isCreatorsDisplayed: "creators-displayed",
|
|
646
|
+
isCountDisplayed: "count-displayed"
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return title;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Parses raw web element data into a standardized WebElement structure
|
|
653
|
+
*
|
|
654
|
+
* @param elementResource - Raw element resource data in OCHRE format
|
|
655
|
+
* @returns Parsed WebElement object
|
|
656
|
+
*/
|
|
657
|
+
function parseWebElement(elementResource, options, pageSlugs) {
|
|
658
|
+
const identification = parseIdentification(elementResource.identification, options);
|
|
659
|
+
const elementProperties = parseSimplifiedProperties(elementResource.properties, options);
|
|
660
|
+
const presentationProperty = websitePresentationReader(elementProperties).requiredProperty("presentation", `Presentation property not found for element (${formatXMLWebsiteResourceMetadata(elementResource)})`);
|
|
661
|
+
const properties = parseWebElementProperties(websitePresentationReader(presentationProperty.properties).requiredProperty("component", `Component property not found for element (${formatXMLWebsiteResourceMetadata(elementResource)})`), elementResource, options, pageSlugs);
|
|
662
|
+
const cssStyles = parseResponsiveCssStyles(elementProperties);
|
|
663
|
+
const title = parseWebTitle(elementProperties, identification, {
|
|
664
|
+
isNameDisplayed: [
|
|
665
|
+
"annotated-image",
|
|
666
|
+
"annotated-document",
|
|
667
|
+
"collection"
|
|
668
|
+
].includes(properties.component),
|
|
669
|
+
isCountDisplayed: properties.component === "collection"
|
|
670
|
+
});
|
|
671
|
+
return {
|
|
672
|
+
uuid: elementResource.uuid,
|
|
673
|
+
language: elementResource.lang ?? null,
|
|
674
|
+
type: "element",
|
|
675
|
+
title,
|
|
676
|
+
cssStyles,
|
|
677
|
+
...properties
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
export { WEB_ELEMENT_COMPONENT_PARSERS, parseWebElement, parseWebTitle };
|