@canopy-iiif/app 0.8.4 → 0.8.6
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/lib/build/iiif.js +361 -84
- package/lib/build/mdx.js +16 -8
- package/lib/build/pages.js +2 -1
- package/lib/build/styles.js +53 -1
- package/lib/common.js +28 -6
- package/lib/search/search-app.jsx +177 -25
- package/lib/search/search-form-runtime.js +126 -19
- package/lib/search/search.js +130 -18
- package/package.json +4 -1
- package/ui/dist/index.mjs +239 -97
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +129 -72
- package/ui/dist/server.mjs.map +4 -4
- package/ui/styles/_variables.scss +1 -0
- package/ui/styles/base/_common.scss +27 -5
- package/ui/styles/base/_heading.scss +2 -4
- package/ui/styles/base/index.scss +1 -0
- package/ui/styles/components/_card.scss +47 -4
- package/ui/styles/components/_sub-navigation.scss +14 -14
- package/ui/styles/components/header/_header.scss +1 -4
- package/ui/styles/components/header/_logo.scss +33 -10
- package/ui/styles/components/search/_filters.scss +5 -7
- package/ui/styles/components/search/_form.scss +55 -17
- package/ui/styles/components/search/_results.scss +13 -15
- package/ui/styles/index.css +250 -72
- package/ui/styles/index.scss +2 -4
- package/ui/tailwind-canopy-iiif-plugin.js +10 -2
- package/ui/tailwind-canopy-iiif-preset.js +21 -19
- package/ui/theme.js +303 -0
- package/ui/styles/variables.emit.scss +0 -72
- package/ui/styles/variables.scss +0 -76
package/lib/build/iiif.js
CHANGED
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
rootRelativeHref,
|
|
15
15
|
} = require("../common");
|
|
16
16
|
const mdx = require("./mdx");
|
|
17
|
-
const {
|
|
17
|
+
const {log, logLine, logResponse} = require("./log");
|
|
18
18
|
|
|
19
19
|
const IIIF_CACHE_DIR = path.resolve(".cache/iiif");
|
|
20
20
|
const IIIF_CACHE_MANIFESTS_DIR = path.join(IIIF_CACHE_DIR, "manifests");
|
|
@@ -70,6 +70,189 @@ function firstLabelString(label) {
|
|
|
70
70
|
return "Untitled";
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function flattenMetadataValue(value, out, depth) {
|
|
74
|
+
if (!value || depth > 5) return;
|
|
75
|
+
if (typeof value === "string") {
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
if (trimmed) out.push(trimmed);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
for (const entry of value) flattenMetadataValue(entry, out, depth + 1);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === "object") {
|
|
85
|
+
for (const key of Object.keys(value))
|
|
86
|
+
flattenMetadataValue(value[key], out, depth + 1);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const str = String(value).trim();
|
|
91
|
+
if (str) out.push(str);
|
|
92
|
+
} catch (_) {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeMetadataLabel(label) {
|
|
96
|
+
if (typeof label !== "string") return "";
|
|
97
|
+
const trimmed = label.trim().replace(/[:\s]+$/g, "");
|
|
98
|
+
return trimmed.toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function extractSummaryValues(manifest) {
|
|
102
|
+
const values = [];
|
|
103
|
+
try {
|
|
104
|
+
flattenMetadataValue(manifest && manifest.summary, values, 0);
|
|
105
|
+
} catch (_) {}
|
|
106
|
+
const unique = Array.from(
|
|
107
|
+
new Set(values.map((val) => String(val || "").trim()).filter(Boolean))
|
|
108
|
+
);
|
|
109
|
+
if (!unique.length) return "";
|
|
110
|
+
return unique.join(" ");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stripHtml(value) {
|
|
114
|
+
try {
|
|
115
|
+
return String(value || "")
|
|
116
|
+
.replace(/<[^>]*>/g, " ")
|
|
117
|
+
.replace(/\s+/g, " ")
|
|
118
|
+
.trim();
|
|
119
|
+
} catch (_) {
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function collectTextualBody(body, out) {
|
|
125
|
+
if (!body) return;
|
|
126
|
+
if (Array.isArray(body)) {
|
|
127
|
+
for (const entry of body) collectTextualBody(entry, out);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (typeof body === "string") {
|
|
131
|
+
const text = stripHtml(body);
|
|
132
|
+
if (text) out.push(text);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (typeof body !== "object") return;
|
|
136
|
+
const type = String(body.type || "").toLowerCase();
|
|
137
|
+
const format = String(body.format || "").toLowerCase();
|
|
138
|
+
const isTextual =
|
|
139
|
+
type === "textualbody" ||
|
|
140
|
+
format.startsWith("text/") ||
|
|
141
|
+
typeof body.value === "string" ||
|
|
142
|
+
Array.isArray(body.value);
|
|
143
|
+
if (!isTextual) return;
|
|
144
|
+
if (body.value !== undefined) collectTextualBody(body.value, out);
|
|
145
|
+
if (body.label !== undefined) collectTextualBody(body.label, out);
|
|
146
|
+
if (body.body !== undefined) collectTextualBody(body.body, out);
|
|
147
|
+
if (body.items !== undefined) collectTextualBody(body.items, out);
|
|
148
|
+
if (body.text !== undefined) collectTextualBody(body.text, out);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractAnnotationText(manifest, options = {}) {
|
|
152
|
+
if (!manifest || typeof manifest !== "object") return "";
|
|
153
|
+
if (!options.enabled) return "";
|
|
154
|
+
const motivations =
|
|
155
|
+
options.motivations instanceof Set ? options.motivations : new Set();
|
|
156
|
+
const allowAll = motivations.size === 0;
|
|
157
|
+
const results = [];
|
|
158
|
+
const seenText = new Set();
|
|
159
|
+
const seenNodes = new Set();
|
|
160
|
+
|
|
161
|
+
function matchesMotivation(value) {
|
|
162
|
+
if (allowAll) return true;
|
|
163
|
+
if (!value) return false;
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
return value.some((entry) => matchesMotivation(entry));
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const norm = String(value || "")
|
|
169
|
+
.trim()
|
|
170
|
+
.toLowerCase();
|
|
171
|
+
return motivations.has(norm);
|
|
172
|
+
} catch (_) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function handleAnnotation(annotation) {
|
|
178
|
+
if (!annotation || typeof annotation !== "object") return;
|
|
179
|
+
if (!matchesMotivation(annotation.motivation)) return;
|
|
180
|
+
const body = annotation.body;
|
|
181
|
+
const texts = [];
|
|
182
|
+
collectTextualBody(body, texts);
|
|
183
|
+
for (const text of texts) {
|
|
184
|
+
if (!text) continue;
|
|
185
|
+
if (seenText.has(text)) continue;
|
|
186
|
+
seenText.add(text);
|
|
187
|
+
results.push(text);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function walk(value) {
|
|
192
|
+
if (!value) return;
|
|
193
|
+
if (Array.isArray(value)) {
|
|
194
|
+
for (const entry of value) walk(entry);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (typeof value !== "object") return;
|
|
198
|
+
if (seenNodes.has(value)) return;
|
|
199
|
+
seenNodes.add(value);
|
|
200
|
+
if (Array.isArray(value.annotations)) {
|
|
201
|
+
for (const page of value.annotations) {
|
|
202
|
+
if (page && Array.isArray(page.items)) {
|
|
203
|
+
for (const item of page.items) handleAnnotation(item);
|
|
204
|
+
}
|
|
205
|
+
walk(page);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (Array.isArray(value.items)) {
|
|
209
|
+
for (const item of value.items) walk(item);
|
|
210
|
+
}
|
|
211
|
+
for (const key of Object.keys(value)) {
|
|
212
|
+
if (key === "annotations" || key === "items") continue;
|
|
213
|
+
walk(value[key]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
walk(manifest);
|
|
218
|
+
if (!results.length) return "";
|
|
219
|
+
return results.join(" ");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function extractMetadataValues(manifest, options = {}) {
|
|
223
|
+
const meta = Array.isArray(manifest && manifest.metadata)
|
|
224
|
+
? manifest.metadata
|
|
225
|
+
: [];
|
|
226
|
+
if (!meta.length) return [];
|
|
227
|
+
const includeAll = !!options.includeAll;
|
|
228
|
+
const labelsSet = includeAll
|
|
229
|
+
? null
|
|
230
|
+
: options && options.labelsSet instanceof Set
|
|
231
|
+
? options.labelsSet
|
|
232
|
+
: new Set();
|
|
233
|
+
const seen = new Set();
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const entry of meta) {
|
|
236
|
+
if (!entry) continue;
|
|
237
|
+
const label = firstLabelString(entry.label);
|
|
238
|
+
if (!label) continue;
|
|
239
|
+
if (!includeAll && labelsSet && labelsSet.size) {
|
|
240
|
+
const normLabel = normalizeMetadataLabel(label);
|
|
241
|
+
if (!labelsSet.has(normLabel)) continue;
|
|
242
|
+
}
|
|
243
|
+
const values = [];
|
|
244
|
+
flattenMetadataValue(entry.value, values, 0);
|
|
245
|
+
for (const val of values) {
|
|
246
|
+
const normalized = String(val || "").trim();
|
|
247
|
+
if (!normalized) continue;
|
|
248
|
+
if (seen.has(normalized)) continue;
|
|
249
|
+
seen.add(normalized);
|
|
250
|
+
out.push(normalized);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
73
256
|
async function normalizeToV3(resource) {
|
|
74
257
|
try {
|
|
75
258
|
const helpers = await import("@iiif/helpers");
|
|
@@ -107,12 +290,12 @@ function normalizeIiifId(raw) {
|
|
|
107
290
|
}
|
|
108
291
|
}
|
|
109
292
|
|
|
110
|
-
async function readJsonFromUri(uri, options = {
|
|
293
|
+
async function readJsonFromUri(uri, options = {log: false}) {
|
|
111
294
|
try {
|
|
112
295
|
if (/^https?:\/\//i.test(uri)) {
|
|
113
296
|
if (typeof fetch !== "function") return null;
|
|
114
297
|
const res = await fetch(uri, {
|
|
115
|
-
headers: {
|
|
298
|
+
headers: {Accept: "application/json"},
|
|
116
299
|
}).catch(() => null);
|
|
117
300
|
if (options && options.log) {
|
|
118
301
|
try {
|
|
@@ -122,14 +305,14 @@ async function readJsonFromUri(uri, options = { log: false }) {
|
|
|
122
305
|
});
|
|
123
306
|
} else {
|
|
124
307
|
const code = res ? res.status : "ERR";
|
|
125
|
-
logLine(`⊘ ${String(uri)} → ${code}`, "red", {
|
|
308
|
+
logLine(`⊘ ${String(uri)} → ${code}`, "red", {bright: true});
|
|
126
309
|
}
|
|
127
310
|
} catch (_) {}
|
|
128
311
|
}
|
|
129
312
|
if (!res || !res.ok) return null;
|
|
130
313
|
return await res.json();
|
|
131
314
|
}
|
|
132
|
-
const p = uri.startsWith("file://") ? new URL(uri) : {
|
|
315
|
+
const p = uri.startsWith("file://") ? new URL(uri) : {pathname: uri};
|
|
133
316
|
const localPath = uri.startsWith("file://")
|
|
134
317
|
? p.pathname
|
|
135
318
|
: path.resolve(String(p.pathname));
|
|
@@ -168,14 +351,14 @@ async function loadManifestIndex() {
|
|
|
168
351
|
const byId = Array.isArray(idx.byId)
|
|
169
352
|
? idx.byId
|
|
170
353
|
: idx.byId && typeof idx.byId === "object"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return {
|
|
354
|
+
? Object.keys(idx.byId).map((k) => ({
|
|
355
|
+
id: k,
|
|
356
|
+
type: "Manifest",
|
|
357
|
+
slug: String(idx.byId[k] || ""),
|
|
358
|
+
parent: (idx.parents && idx.parents[k]) || "",
|
|
359
|
+
}))
|
|
360
|
+
: [];
|
|
361
|
+
return {byId, collection: idx.collection || null};
|
|
179
362
|
}
|
|
180
363
|
}
|
|
181
364
|
// Legacy index location retained for backward compatibility
|
|
@@ -185,14 +368,14 @@ async function loadManifestIndex() {
|
|
|
185
368
|
const byId = Array.isArray(idx.byId)
|
|
186
369
|
? idx.byId
|
|
187
370
|
: idx.byId && typeof idx.byId === "object"
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return {
|
|
371
|
+
? Object.keys(idx.byId).map((k) => ({
|
|
372
|
+
id: k,
|
|
373
|
+
type: "Manifest",
|
|
374
|
+
slug: String(idx.byId[k] || ""),
|
|
375
|
+
parent: (idx.parents && idx.parents[k]) || "",
|
|
376
|
+
}))
|
|
377
|
+
: [];
|
|
378
|
+
return {byId, collection: idx.collection || null};
|
|
196
379
|
}
|
|
197
380
|
}
|
|
198
381
|
// Legacy manifests index retained for backward compatibility
|
|
@@ -202,18 +385,18 @@ async function loadManifestIndex() {
|
|
|
202
385
|
const byId = Array.isArray(idx.byId)
|
|
203
386
|
? idx.byId
|
|
204
387
|
: idx.byId && typeof idx.byId === "object"
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return {
|
|
388
|
+
? Object.keys(idx.byId).map((k) => ({
|
|
389
|
+
id: k,
|
|
390
|
+
type: "Manifest",
|
|
391
|
+
slug: String(idx.byId[k] || ""),
|
|
392
|
+
parent: (idx.parents && idx.parents[k]) || "",
|
|
393
|
+
}))
|
|
394
|
+
: [];
|
|
395
|
+
return {byId, collection: idx.collection || null};
|
|
213
396
|
}
|
|
214
397
|
}
|
|
215
398
|
} catch (_) {}
|
|
216
|
-
return {
|
|
399
|
+
return {byId: [], collection: null};
|
|
217
400
|
}
|
|
218
401
|
|
|
219
402
|
async function saveManifestIndex(index) {
|
|
@@ -228,10 +411,10 @@ async function saveManifestIndex(index) {
|
|
|
228
411
|
await fsp.writeFile(IIIF_CACHE_INDEX, JSON.stringify(out, null, 2), "utf8");
|
|
229
412
|
// Remove legacy files to avoid confusion
|
|
230
413
|
try {
|
|
231
|
-
await fsp.rm(IIIF_CACHE_INDEX_LEGACY, {
|
|
414
|
+
await fsp.rm(IIIF_CACHE_INDEX_LEGACY, {force: true});
|
|
232
415
|
} catch (_) {}
|
|
233
416
|
try {
|
|
234
|
-
await fsp.rm(IIIF_CACHE_INDEX_MANIFESTS, {
|
|
417
|
+
await fsp.rm(IIIF_CACHE_INDEX_MANIFESTS, {force: true});
|
|
235
418
|
} catch (_) {}
|
|
236
419
|
} catch (_) {}
|
|
237
420
|
}
|
|
@@ -240,7 +423,7 @@ async function saveManifestIndex(index) {
|
|
|
240
423
|
const MEMO_ID_TO_SLUG = new Map();
|
|
241
424
|
// Track slugs chosen during this run to avoid collisions when multiple
|
|
242
425
|
// collections/manifests share the same base title but mappings aren't yet saved.
|
|
243
|
-
const RESERVED_SLUGS = {
|
|
426
|
+
const RESERVED_SLUGS = {Manifest: new Set(), Collection: new Set()};
|
|
244
427
|
|
|
245
428
|
function computeUniqueSlug(index, baseSlug, id, type) {
|
|
246
429
|
const byId = Array.isArray(index && index.byId) ? index.byId : [];
|
|
@@ -371,7 +554,7 @@ async function saveCachedManifest(manifest, id, parentId) {
|
|
|
371
554
|
const index = await loadManifestIndex();
|
|
372
555
|
const title = firstLabelString(manifest && manifest.label);
|
|
373
556
|
const baseSlug =
|
|
374
|
-
slugify(title || "untitled", {
|
|
557
|
+
slugify(title || "untitled", {lower: true, strict: true, trim: true}) ||
|
|
375
558
|
"untitled";
|
|
376
559
|
const slug = computeUniqueSlug(index, baseSlug, id, "Manifest");
|
|
377
560
|
ensureDirSync(IIIF_CACHE_MANIFESTS_DIR);
|
|
@@ -399,13 +582,16 @@ async function saveCachedManifest(manifest, id, parentId) {
|
|
|
399
582
|
async function ensureFeaturedInCache(cfg) {
|
|
400
583
|
try {
|
|
401
584
|
const CONFIG = cfg || (await loadConfig());
|
|
402
|
-
const featured = Array.isArray(CONFIG && CONFIG.featured)
|
|
585
|
+
const featured = Array.isArray(CONFIG && CONFIG.featured)
|
|
586
|
+
? CONFIG.featured
|
|
587
|
+
: [];
|
|
403
588
|
if (!featured.length) return;
|
|
404
|
-
const {
|
|
405
|
-
const {
|
|
589
|
+
const {getThumbnail, getRepresentativeImage} = require("../iiif/thumbnail");
|
|
590
|
+
const {size: thumbSize, unsafe: unsafeThumbs} =
|
|
591
|
+
resolveThumbnailPreferences();
|
|
406
592
|
const HERO_THUMBNAIL_SIZE = 1200;
|
|
407
593
|
for (const rawId of featured) {
|
|
408
|
-
const id = normalizeIiifId(String(rawId ||
|
|
594
|
+
const id = normalizeIiifId(String(rawId || ""));
|
|
409
595
|
if (!id) continue;
|
|
410
596
|
let manifest = await loadCachedManifestById(id);
|
|
411
597
|
if (!manifest) {
|
|
@@ -413,7 +599,7 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
413
599
|
if (!m) continue;
|
|
414
600
|
const v3 = await normalizeToV3(m);
|
|
415
601
|
if (!v3 || !v3.id) continue;
|
|
416
|
-
await saveCachedManifest(v3, id,
|
|
602
|
+
await saveCachedManifest(v3, id, "");
|
|
417
603
|
manifest = v3;
|
|
418
604
|
}
|
|
419
605
|
// Ensure thumbnail fields exist in index for this manifest (if computable)
|
|
@@ -424,8 +610,9 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
424
610
|
const entry = idx.byId.find(
|
|
425
611
|
(e) =>
|
|
426
612
|
e &&
|
|
427
|
-
e.type ===
|
|
428
|
-
normalizeIiifId(String(e.id)) ===
|
|
613
|
+
e.type === "Manifest" &&
|
|
614
|
+
normalizeIiifId(String(e.id)) ===
|
|
615
|
+
normalizeIiifId(String(manifest.id))
|
|
429
616
|
);
|
|
430
617
|
if (!entry) continue;
|
|
431
618
|
|
|
@@ -436,11 +623,11 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
436
623
|
entry.thumbnail = nextUrl;
|
|
437
624
|
touched = true;
|
|
438
625
|
}
|
|
439
|
-
if (typeof t.width ===
|
|
626
|
+
if (typeof t.width === "number") {
|
|
440
627
|
if (entry.thumbnailWidth !== t.width) touched = true;
|
|
441
628
|
entry.thumbnailWidth = t.width;
|
|
442
629
|
}
|
|
443
|
-
if (typeof t.height ===
|
|
630
|
+
if (typeof t.height === "number") {
|
|
444
631
|
if (entry.thumbnailHeight !== t.height) touched = true;
|
|
445
632
|
entry.thumbnailHeight = t.height;
|
|
446
633
|
}
|
|
@@ -449,7 +636,7 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
449
636
|
try {
|
|
450
637
|
const heroSource = (() => {
|
|
451
638
|
if (manifest && manifest.thumbnail) {
|
|
452
|
-
const clone = {
|
|
639
|
+
const clone = {...manifest};
|
|
453
640
|
try {
|
|
454
641
|
delete clone.thumbnail;
|
|
455
642
|
} catch (_) {
|
|
@@ -470,14 +657,14 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
470
657
|
entry.heroThumbnail = nextHero;
|
|
471
658
|
touched = true;
|
|
472
659
|
}
|
|
473
|
-
if (typeof heroRep.width ===
|
|
660
|
+
if (typeof heroRep.width === "number") {
|
|
474
661
|
if (entry.heroThumbnailWidth !== heroRep.width) touched = true;
|
|
475
662
|
entry.heroThumbnailWidth = heroRep.width;
|
|
476
663
|
} else if (entry.heroThumbnailWidth !== undefined) {
|
|
477
664
|
delete entry.heroThumbnailWidth;
|
|
478
665
|
touched = true;
|
|
479
666
|
}
|
|
480
|
-
if (typeof heroRep.height ===
|
|
667
|
+
if (typeof heroRep.height === "number") {
|
|
481
668
|
if (entry.heroThumbnailHeight !== heroRep.height) touched = true;
|
|
482
669
|
entry.heroThumbnailHeight = heroRep.height;
|
|
483
670
|
} else if (entry.heroThumbnailHeight !== undefined) {
|
|
@@ -511,12 +698,12 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
511
698
|
|
|
512
699
|
async function flushManifestCache() {
|
|
513
700
|
try {
|
|
514
|
-
await fsp.rm(IIIF_CACHE_MANIFESTS_DIR, {
|
|
701
|
+
await fsp.rm(IIIF_CACHE_MANIFESTS_DIR, {recursive: true, force: true});
|
|
515
702
|
} catch (_) {}
|
|
516
703
|
ensureDirSync(IIIF_CACHE_MANIFESTS_DIR);
|
|
517
704
|
ensureDirSync(IIIF_CACHE_COLLECTIONS_DIR);
|
|
518
705
|
try {
|
|
519
|
-
await fsp.rm(IIIF_CACHE_COLLECTIONS_DIR, {
|
|
706
|
+
await fsp.rm(IIIF_CACHE_COLLECTIONS_DIR, {recursive: true, force: true});
|
|
520
707
|
} catch (_) {}
|
|
521
708
|
ensureDirSync(IIIF_CACHE_COLLECTIONS_DIR);
|
|
522
709
|
}
|
|
@@ -602,8 +789,8 @@ async function saveCachedCollection(collection, id, parentId) {
|
|
|
602
789
|
await fsp.writeFile(dest, JSON.stringify(collection, null, 2), "utf8");
|
|
603
790
|
try {
|
|
604
791
|
if (process.env.CANOPY_IIIF_DEBUG === "1") {
|
|
605
|
-
const {
|
|
606
|
-
logLine(`IIIF: saved collection → ${slug}.json`, "cyan", {
|
|
792
|
+
const {logLine} = require("./log");
|
|
793
|
+
logLine(`IIIF: saved collection → ${slug}.json`, "cyan", {dim: true});
|
|
607
794
|
}
|
|
608
795
|
} catch (_) {}
|
|
609
796
|
index.byId = Array.isArray(index.byId) ? index.byId : [];
|
|
@@ -639,18 +826,69 @@ async function loadConfig() {
|
|
|
639
826
|
// Traverse IIIF collection, cache manifests/collections, and render pages
|
|
640
827
|
async function buildIiifCollectionPages(CONFIG) {
|
|
641
828
|
const cfg = CONFIG || (await loadConfig());
|
|
829
|
+
|
|
642
830
|
const collectionUri =
|
|
643
|
-
(cfg && cfg.collection
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
831
|
+
(cfg && cfg.collection) || process.env.CANOPY_COLLECTION_URI || "";
|
|
832
|
+
if (!collectionUri) return {searchRecords: []};
|
|
833
|
+
|
|
834
|
+
const searchIndexCfg = (cfg && cfg.search && cfg.search.index) || {};
|
|
835
|
+
const metadataCfg = (searchIndexCfg && searchIndexCfg.metadata) || {};
|
|
836
|
+
const summaryCfg = (searchIndexCfg && searchIndexCfg.summary) || {};
|
|
837
|
+
const annotationsCfg = (searchIndexCfg && searchIndexCfg.annotations) || {};
|
|
838
|
+
const metadataEnabled =
|
|
839
|
+
metadataCfg && Object.prototype.hasOwnProperty.call(metadataCfg, "enabled")
|
|
840
|
+
? resolveBoolean(metadataCfg.enabled)
|
|
841
|
+
: true;
|
|
842
|
+
const summaryEnabled =
|
|
843
|
+
summaryCfg && Object.prototype.hasOwnProperty.call(summaryCfg, "enabled")
|
|
844
|
+
? resolveBoolean(summaryCfg.enabled)
|
|
845
|
+
: true;
|
|
846
|
+
const annotationsEnabled =
|
|
847
|
+
annotationsCfg &&
|
|
848
|
+
Object.prototype.hasOwnProperty.call(annotationsCfg, "enabled")
|
|
849
|
+
? resolveBoolean(annotationsCfg.enabled)
|
|
850
|
+
: false;
|
|
851
|
+
const metadataIncludeAll = metadataEnabled && resolveBoolean(metadataCfg.all);
|
|
852
|
+
const metadataLabelsRaw = Array.isArray(cfg && cfg.metadata)
|
|
853
|
+
? cfg.metadata
|
|
854
|
+
: [];
|
|
855
|
+
const metadataLabelSet = new Set(
|
|
856
|
+
metadataLabelsRaw
|
|
857
|
+
.map((label) => normalizeMetadataLabel(String(label || "")))
|
|
858
|
+
.filter(Boolean)
|
|
859
|
+
);
|
|
860
|
+
const metadataOptions = {
|
|
861
|
+
enabled:
|
|
862
|
+
metadataEnabled &&
|
|
863
|
+
(metadataIncludeAll || (metadataLabelSet && metadataLabelSet.size > 0)),
|
|
864
|
+
includeAll: metadataIncludeAll,
|
|
865
|
+
labelsSet: metadataIncludeAll ? null : metadataLabelSet,
|
|
866
|
+
};
|
|
867
|
+
const summaryOptions = {
|
|
868
|
+
enabled: summaryEnabled,
|
|
869
|
+
};
|
|
870
|
+
const annotationMotivations = new Set(
|
|
871
|
+
Array.isArray(annotationsCfg && annotationsCfg.motivation)
|
|
872
|
+
? annotationsCfg.motivation
|
|
873
|
+
.map((m) =>
|
|
874
|
+
String(m || "")
|
|
875
|
+
.trim()
|
|
876
|
+
.toLowerCase()
|
|
877
|
+
)
|
|
878
|
+
.filter(Boolean)
|
|
879
|
+
: []
|
|
880
|
+
);
|
|
881
|
+
const annotationsOptions = {
|
|
882
|
+
enabled: annotationsEnabled,
|
|
883
|
+
motivations: annotationMotivations,
|
|
884
|
+
};
|
|
647
885
|
|
|
648
886
|
// Fetch top-level collection
|
|
649
|
-
logLine("• Traversing IIIF Collection(s)", "blue", {
|
|
650
|
-
const root = await readJsonFromUri(collectionUri, {
|
|
887
|
+
logLine("• Traversing IIIF Collection(s)", "blue", {dim: true});
|
|
888
|
+
const root = await readJsonFromUri(collectionUri, {log: true});
|
|
651
889
|
if (!root) {
|
|
652
890
|
logLine("IIIF: Failed to fetch collection", "red");
|
|
653
|
-
return {
|
|
891
|
+
return {searchRecords: []};
|
|
654
892
|
}
|
|
655
893
|
const normalizedRoot = await normalizeToV3(root);
|
|
656
894
|
// Save collection cache
|
|
@@ -682,7 +920,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
682
920
|
const col =
|
|
683
921
|
typeof colLike === "object" && colLike && colLike.items
|
|
684
922
|
? colLike
|
|
685
|
-
: await readJsonFromUri(uri, {
|
|
923
|
+
: await readJsonFromUri(uri, {log: true});
|
|
686
924
|
if (!col) return;
|
|
687
925
|
const ncol = await normalizeToV3(col);
|
|
688
926
|
const colId = String((ncol && (ncol.id || uri)) || uri);
|
|
@@ -698,7 +936,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
698
936
|
const t = String(entry.type || entry["@type"] || "").toLowerCase();
|
|
699
937
|
const entryId = entry.id || entry["@id"] || "";
|
|
700
938
|
if (t === "manifest") {
|
|
701
|
-
tasks.push({
|
|
939
|
+
tasks.push({id: entryId, parent: colId});
|
|
702
940
|
} else if (t === "collection") {
|
|
703
941
|
await gatherFromCollection(entryId, colId);
|
|
704
942
|
}
|
|
@@ -707,7 +945,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
707
945
|
} catch (_) {}
|
|
708
946
|
}
|
|
709
947
|
await gatherFromCollection(normalizedRoot, "");
|
|
710
|
-
if (!tasks.length) return {
|
|
948
|
+
if (!tasks.length) return {searchRecords: []};
|
|
711
949
|
|
|
712
950
|
// Split into chunks and process with limited concurrency
|
|
713
951
|
const chunkSize = resolvePositiveInteger(
|
|
@@ -721,12 +959,11 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
721
959
|
logLine(
|
|
722
960
|
`• Fetching ${tasks.length} Manifest(s) in ${chunks} chunk(s) across ${collectionsCount} Collection(s)`,
|
|
723
961
|
"blue",
|
|
724
|
-
{
|
|
962
|
+
{dim: true}
|
|
725
963
|
);
|
|
726
964
|
} catch (_) {}
|
|
727
965
|
const iiifRecords = [];
|
|
728
|
-
const {
|
|
729
|
-
resolveThumbnailPreferences();
|
|
966
|
+
const {size: thumbSize, unsafe: unsafeThumbs} = resolveThumbnailPreferences();
|
|
730
967
|
|
|
731
968
|
// Compile the works layout component once per run
|
|
732
969
|
const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
|
|
@@ -740,14 +977,12 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
740
977
|
WorksLayoutComp = await mdx.compileMdxToComponent(worksLayoutPath);
|
|
741
978
|
} catch (err) {
|
|
742
979
|
const message = err && err.message ? err.message : err;
|
|
743
|
-
throw new Error(
|
|
744
|
-
`Failed to compile content/works/_layout.mdx: ${message}`
|
|
745
|
-
);
|
|
980
|
+
throw new Error(`Failed to compile content/works/_layout.mdx: ${message}`);
|
|
746
981
|
}
|
|
747
982
|
|
|
748
983
|
for (let ci = 0; ci < chunks; ci++) {
|
|
749
984
|
const chunk = tasks.slice(ci * chunkSize, (ci + 1) * chunkSize);
|
|
750
|
-
logLine(`• Chunk ${ci + 1}/${chunks}`, "blue", {
|
|
985
|
+
logLine(`• Chunk ${ci + 1}/${chunks}`, "blue", {dim: true});
|
|
751
986
|
|
|
752
987
|
const concurrency = resolvePositiveInteger(
|
|
753
988
|
process.env.CANOPY_FETCH_CONCURRENCY,
|
|
@@ -783,7 +1018,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
783
1018
|
} else if (/^https?:\/\//i.test(String(id || ""))) {
|
|
784
1019
|
try {
|
|
785
1020
|
const res = await fetch(String(id), {
|
|
786
|
-
headers: {
|
|
1021
|
+
headers: {Accept: "application/json"},
|
|
787
1022
|
}).catch(() => null);
|
|
788
1023
|
if (res && res.ok) {
|
|
789
1024
|
lns.push([`↓ ${String(id)} → ${res.status}`, "yellow"]);
|
|
@@ -808,7 +1043,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
808
1043
|
}
|
|
809
1044
|
} else if (/^file:\/\//i.test(String(id || ""))) {
|
|
810
1045
|
try {
|
|
811
|
-
const local = await readJsonFromUri(String(id), {
|
|
1046
|
+
const local = await readJsonFromUri(String(id), {log: false});
|
|
812
1047
|
if (!local) {
|
|
813
1048
|
lns.push([`⊘ ${String(id)} → ERR`, "red"]);
|
|
814
1049
|
continue;
|
|
@@ -871,14 +1106,14 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
871
1106
|
} catch (_) {
|
|
872
1107
|
components = {};
|
|
873
1108
|
}
|
|
874
|
-
const {
|
|
1109
|
+
const {withBase} = require("../common");
|
|
875
1110
|
const Anchor = function A(props) {
|
|
876
|
-
let {
|
|
1111
|
+
let {href = "", ...rest} = props || {};
|
|
877
1112
|
href = withBase(href);
|
|
878
|
-
return React.createElement("a", {
|
|
1113
|
+
return React.createElement("a", {href, ...rest}, props.children);
|
|
879
1114
|
};
|
|
880
1115
|
// Map exported UI components into MDX and add anchor helper
|
|
881
|
-
const compMap = {
|
|
1116
|
+
const compMap = {...components, a: Anchor};
|
|
882
1117
|
let MDXProvider = null;
|
|
883
1118
|
try {
|
|
884
1119
|
const mod = await import("@mdx-js/react");
|
|
@@ -886,10 +1121,10 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
886
1121
|
} catch (_) {
|
|
887
1122
|
MDXProvider = null;
|
|
888
1123
|
}
|
|
889
|
-
const {
|
|
1124
|
+
const {loadAppWrapper} = require("./mdx");
|
|
890
1125
|
const app = await loadAppWrapper();
|
|
891
1126
|
|
|
892
|
-
const mdxContent = React.createElement(WorksLayoutComp, {
|
|
1127
|
+
const mdxContent = React.createElement(WorksLayoutComp, {manifest});
|
|
893
1128
|
const siteTree = app && app.App ? mdxContent : mdxContent;
|
|
894
1129
|
const wrappedApp =
|
|
895
1130
|
app && app.App
|
|
@@ -898,7 +1133,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
898
1133
|
const page = MDXProvider
|
|
899
1134
|
? React.createElement(
|
|
900
1135
|
MDXProvider,
|
|
901
|
-
{
|
|
1136
|
+
{components: compMap},
|
|
902
1137
|
wrappedApp
|
|
903
1138
|
)
|
|
904
1139
|
: wrappedApp;
|
|
@@ -909,7 +1144,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
909
1144
|
React.createElement(app.Head)
|
|
910
1145
|
)
|
|
911
1146
|
: "";
|
|
912
|
-
const needsHydrateViewer =
|
|
1147
|
+
const needsHydrateViewer =
|
|
1148
|
+
body.includes("data-canopy-viewer") || body.includes("data-canopy-scroll");
|
|
913
1149
|
const needsRelated = body.includes("data-canopy-related-items");
|
|
914
1150
|
const needsHero = body.includes("data-canopy-hero");
|
|
915
1151
|
const needsSearchForm = body.includes("data-canopy-search-form");
|
|
@@ -971,7 +1207,11 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
971
1207
|
else if (viewerRel) jsRel = viewerRel;
|
|
972
1208
|
|
|
973
1209
|
let headExtra = head;
|
|
974
|
-
const needsReact = !!(
|
|
1210
|
+
const needsReact = !!(
|
|
1211
|
+
needsHydrateViewer ||
|
|
1212
|
+
needsRelated ||
|
|
1213
|
+
needsHero
|
|
1214
|
+
);
|
|
975
1215
|
let vendorTag = "";
|
|
976
1216
|
if (needsReact) {
|
|
977
1217
|
try {
|
|
@@ -1005,7 +1245,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1005
1245
|
if (extraScripts.length)
|
|
1006
1246
|
headExtra = extraScripts.join("") + headExtra;
|
|
1007
1247
|
try {
|
|
1008
|
-
const {
|
|
1248
|
+
const {BASE_PATH} = require("../common");
|
|
1009
1249
|
if (BASE_PATH)
|
|
1010
1250
|
vendorTag =
|
|
1011
1251
|
`<script>window.CANOPY_BASE_PATH=${JSON.stringify(
|
|
@@ -1032,7 +1272,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1032
1272
|
let thumbWidth = undefined;
|
|
1033
1273
|
let thumbHeight = undefined;
|
|
1034
1274
|
try {
|
|
1035
|
-
const {
|
|
1275
|
+
const {getThumbnail} = require("../iiif/thumbnail");
|
|
1036
1276
|
const t = await getThumbnail(manifest, thumbSize, unsafeThumbs);
|
|
1037
1277
|
if (t && t.url) {
|
|
1038
1278
|
thumbUrl = String(t.url);
|
|
@@ -1057,6 +1297,33 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1057
1297
|
}
|
|
1058
1298
|
}
|
|
1059
1299
|
} catch (_) {}
|
|
1300
|
+
let metadataValues = [];
|
|
1301
|
+
let summaryValue = "";
|
|
1302
|
+
let annotationValue = "";
|
|
1303
|
+
if (metadataOptions && metadataOptions.enabled) {
|
|
1304
|
+
try {
|
|
1305
|
+
metadataValues = extractMetadataValues(manifest, metadataOptions);
|
|
1306
|
+
} catch (_) {
|
|
1307
|
+
metadataValues = [];
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (summaryOptions && summaryOptions.enabled) {
|
|
1311
|
+
try {
|
|
1312
|
+
summaryValue = extractSummaryValues(manifest);
|
|
1313
|
+
} catch (_) {
|
|
1314
|
+
summaryValue = "";
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
if (annotationsOptions && annotationsOptions.enabled) {
|
|
1318
|
+
try {
|
|
1319
|
+
annotationValue = extractAnnotationText(
|
|
1320
|
+
manifest,
|
|
1321
|
+
annotationsOptions
|
|
1322
|
+
);
|
|
1323
|
+
} catch (_) {
|
|
1324
|
+
annotationValue = "";
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1060
1327
|
iiifRecords.push({
|
|
1061
1328
|
id: String(manifest.id || id),
|
|
1062
1329
|
title,
|
|
@@ -1067,6 +1334,16 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1067
1334
|
typeof thumbWidth === "number" ? thumbWidth : undefined,
|
|
1068
1335
|
thumbnailHeight:
|
|
1069
1336
|
typeof thumbHeight === "number" ? thumbHeight : undefined,
|
|
1337
|
+
searchMetadataValues:
|
|
1338
|
+
metadataValues && metadataValues.length
|
|
1339
|
+
? metadataValues
|
|
1340
|
+
: undefined,
|
|
1341
|
+
searchSummary:
|
|
1342
|
+
summaryValue && summaryValue.length ? summaryValue : undefined,
|
|
1343
|
+
searchAnnotation:
|
|
1344
|
+
annotationValue && annotationValue.length
|
|
1345
|
+
? annotationValue
|
|
1346
|
+
: undefined,
|
|
1070
1347
|
});
|
|
1071
1348
|
} catch (e) {
|
|
1072
1349
|
lns.push([
|
|
@@ -1079,12 +1356,12 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1079
1356
|
}
|
|
1080
1357
|
}
|
|
1081
1358
|
const workers = Array.from(
|
|
1082
|
-
{
|
|
1359
|
+
{length: Math.min(concurrency, chunk.length)},
|
|
1083
1360
|
() => worker()
|
|
1084
1361
|
);
|
|
1085
1362
|
await Promise.all(workers);
|
|
1086
1363
|
}
|
|
1087
|
-
return {
|
|
1364
|
+
return {iiifRecords};
|
|
1088
1365
|
}
|
|
1089
1366
|
|
|
1090
1367
|
module.exports = {
|
|
@@ -1101,7 +1378,7 @@ module.exports = {
|
|
|
1101
1378
|
// Debug: list collections cache after traversal
|
|
1102
1379
|
try {
|
|
1103
1380
|
if (process.env.CANOPY_IIIF_DEBUG === "1") {
|
|
1104
|
-
const {
|
|
1381
|
+
const {logLine} = require("./log");
|
|
1105
1382
|
try {
|
|
1106
1383
|
const files = fs.existsSync(IIIF_CACHE_COLLECTIONS_DIR)
|
|
1107
1384
|
? fs
|
|
@@ -1113,7 +1390,7 @@ try {
|
|
|
1113
1390
|
`IIIF: cache/collections (end): ${files.length} file(s)` +
|
|
1114
1391
|
(head ? ` [${head}${files.length > 8 ? ", …" : ""}]` : ""),
|
|
1115
1392
|
"blue",
|
|
1116
|
-
{
|
|
1393
|
+
{dim: true}
|
|
1117
1394
|
);
|
|
1118
1395
|
} catch (_) {}
|
|
1119
1396
|
}
|