@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 CHANGED
@@ -14,7 +14,7 @@ const {
14
14
  rootRelativeHref,
15
15
  } = require("../common");
16
16
  const mdx = require("./mdx");
17
- const { log, logLine, logResponse } = require("./log");
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 = { log: false }) {
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: { Accept: "application/json" },
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", { bright: true });
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) : { pathname: 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
- ? Object.keys(idx.byId).map((k) => ({
172
- id: k,
173
- type: "Manifest",
174
- slug: String(idx.byId[k] || ""),
175
- parent: (idx.parents && idx.parents[k]) || "",
176
- }))
177
- : [];
178
- return { byId, collection: idx.collection || null };
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
- ? Object.keys(idx.byId).map((k) => ({
189
- id: k,
190
- type: "Manifest",
191
- slug: String(idx.byId[k] || ""),
192
- parent: (idx.parents && idx.parents[k]) || "",
193
- }))
194
- : [];
195
- return { byId, collection: idx.collection || null };
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
- ? Object.keys(idx.byId).map((k) => ({
206
- id: k,
207
- type: "Manifest",
208
- slug: String(idx.byId[k] || ""),
209
- parent: (idx.parents && idx.parents[k]) || "",
210
- }))
211
- : [];
212
- return { byId, collection: idx.collection || null };
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 { byId: [], collection: null };
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, { force: true });
414
+ await fsp.rm(IIIF_CACHE_INDEX_LEGACY, {force: true});
232
415
  } catch (_) {}
233
416
  try {
234
- await fsp.rm(IIIF_CACHE_INDEX_MANIFESTS, { force: true });
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 = { Manifest: new Set(), Collection: new Set() };
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", { lower: true, strict: true, trim: true }) ||
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) ? CONFIG.featured : [];
585
+ const featured = Array.isArray(CONFIG && CONFIG.featured)
586
+ ? CONFIG.featured
587
+ : [];
403
588
  if (!featured.length) return;
404
- const { getThumbnail, getRepresentativeImage } = require("../iiif/thumbnail");
405
- const { size: thumbSize, unsafe: unsafeThumbs } = resolveThumbnailPreferences();
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 === 'Manifest' &&
428
- normalizeIiifId(String(e.id)) === normalizeIiifId(String(manifest.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 === 'number') {
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 === 'number') {
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 = { ...manifest };
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 === 'number') {
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 === 'number') {
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, { recursive: true, force: true });
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, { recursive: true, force: true });
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 { logLine } = require("./log");
606
- logLine(`IIIF: saved collection → ${slug}.json`, "cyan", { dim: true });
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 && cfg.collection.uri) ||
644
- process.env.CANOPY_COLLECTION_URI ||
645
- "";
646
- if (!collectionUri) return { searchRecords: [] };
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", { dim: true });
650
- const root = await readJsonFromUri(collectionUri, { log: true });
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 { searchRecords: [] };
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, { log: true });
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({ id: entryId, parent: colId });
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 { searchRecords: [] };
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
- { dim: true }
962
+ {dim: true}
725
963
  );
726
964
  } catch (_) {}
727
965
  const iiifRecords = [];
728
- const { size: thumbSize, unsafe: unsafeThumbs } =
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", { dim: true });
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: { Accept: "application/json" },
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), { log: false });
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 { withBase } = require("../common");
1109
+ const {withBase} = require("../common");
875
1110
  const Anchor = function A(props) {
876
- let { href = "", ...rest } = props || {};
1111
+ let {href = "", ...rest} = props || {};
877
1112
  href = withBase(href);
878
- return React.createElement("a", { href, ...rest }, props.children);
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 = { ...components, a: Anchor };
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 { loadAppWrapper } = require("./mdx");
1124
+ const {loadAppWrapper} = require("./mdx");
890
1125
  const app = await loadAppWrapper();
891
1126
 
892
- const mdxContent = React.createElement(WorksLayoutComp, { manifest });
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
- { components: compMap },
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 = body.includes("data-canopy-viewer");
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 = !!(needsHydrateViewer || needsRelated || needsHero);
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 { BASE_PATH } = require("../common");
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 { getThumbnail } = require("../iiif/thumbnail");
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
- { length: Math.min(concurrency, chunk.length) },
1359
+ {length: Math.min(concurrency, chunk.length)},
1083
1360
  () => worker()
1084
1361
  );
1085
1362
  await Promise.all(workers);
1086
1363
  }
1087
- return { iiifRecords };
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 { logLine } = require("./log");
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
- { dim: true }
1393
+ {dim: true}
1117
1394
  );
1118
1395
  } catch (_) {}
1119
1396
  }