@canopy-iiif/app 1.6.16 → 1.6.18

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
@@ -38,6 +38,10 @@ const IIIF_CACHE_DIR = path.resolve(".cache/iiif");
38
38
  const IIIF_CACHE_MANIFESTS_DIR = path.join(IIIF_CACHE_DIR, "manifests");
39
39
  const IIIF_CACHE_COLLECTIONS_DIR = path.join(IIIF_CACHE_DIR, "collections");
40
40
  const IIIF_CACHE_COLLECTION = path.join(IIIF_CACHE_DIR, "collection.json");
41
+ const IIIF_METADATA_INDEX_CACHE = path.join(
42
+ IIIF_CACHE_DIR,
43
+ "metadata-index.json",
44
+ );
41
45
 
42
46
  function relativeRuntimeScript(outPath, filename, versioned = false) {
43
47
  if (!outPath || !filename) return null;
@@ -623,6 +627,117 @@ function extractMetadataValues(manifest, options = {}) {
623
627
  return out;
624
628
  }
625
629
 
630
+ function extractMetadataEntries(manifest, options = {}) {
631
+ const meta = Array.isArray(manifest && manifest.metadata)
632
+ ? manifest.metadata
633
+ : [];
634
+ if (!meta.length) return [];
635
+ const includeAll = !!options.includeAll;
636
+ const labelsSet = includeAll
637
+ ? null
638
+ : options && options.labelsSet instanceof Set
639
+ ? options.labelsSet
640
+ : new Set();
641
+ const map = new Map();
642
+ const order = [];
643
+ for (const entry of meta) {
644
+ if (!entry) continue;
645
+ const label = firstLabelString(entry.label);
646
+ if (!label) continue;
647
+ const normalized = normalizeMetadataLabel(label);
648
+ if (!normalized) continue;
649
+ if (!includeAll && labelsSet && labelsSet.size && !labelsSet.has(normalized)) continue;
650
+ const values = [];
651
+ flattenMetadataValue(entry.value, values, 0);
652
+ const cleaned = [];
653
+ for (const val of values) {
654
+ const text = String(val || "").trim();
655
+ if (text) cleaned.push(text);
656
+ }
657
+ if (!cleaned.length) continue;
658
+ let record = map.get(normalized);
659
+ if (!record) {
660
+ record = {label, normalized, values: [], seen: new Set()};
661
+ map.set(normalized, record);
662
+ order.push(normalized);
663
+ } else if (!record.label && label) {
664
+ record.label = label;
665
+ }
666
+ for (const valueText of cleaned) {
667
+ if (record.seen.has(valueText)) continue;
668
+ record.seen.add(valueText);
669
+ record.values.push(valueText);
670
+ }
671
+ }
672
+ return order
673
+ .map((normalized) => map.get(normalized))
674
+ .filter(Boolean)
675
+ .map((record) => ({
676
+ label: record.label,
677
+ normalized: record.normalized,
678
+ values: record.values.slice(),
679
+ }));
680
+ }
681
+
682
+ function buildMetadataIndexPayload(map, explicitOrder, fallbackOrder) {
683
+ if (!map || !map.size) return [];
684
+ const ordered = [];
685
+ const seen = new Set();
686
+ const primaryOrder = Array.isArray(explicitOrder) ? explicitOrder : [];
687
+ const secondaryOrder = Array.isArray(fallbackOrder) ? fallbackOrder : [];
688
+ for (const normalized of primaryOrder) {
689
+ if (!normalized) continue;
690
+ const record = map.get(normalized);
691
+ if (!record) continue;
692
+ ordered.push(record);
693
+ seen.add(normalized);
694
+ }
695
+ for (const normalized of secondaryOrder) {
696
+ if (!normalized || seen.has(normalized)) continue;
697
+ const record = map.get(normalized);
698
+ if (!record) continue;
699
+ ordered.push(record);
700
+ seen.add(normalized);
701
+ }
702
+ map.forEach((record, normalized) => {
703
+ if (seen.has(normalized)) return;
704
+ ordered.push(record);
705
+ });
706
+ const payload = [];
707
+ for (const record of ordered) {
708
+ if (!record || !record.values) continue;
709
+ const values = Array.from(record.values.values()).sort((a, b) =>
710
+ String(a && a.value ? a.value : "").localeCompare(
711
+ String(b && b.value ? b.value : ""),
712
+ ),
713
+ );
714
+ if (!values.length) continue;
715
+ payload.push({
716
+ label: record.label || record.slug || record.normalized,
717
+ slug: record.slug || record.normalized,
718
+ values,
719
+ });
720
+ }
721
+ return payload;
722
+ }
723
+
724
+ async function writeMetadataIndexFile(payload) {
725
+ if (!payload || !payload.length) {
726
+ try {
727
+ await fsp.rm(IIIF_METADATA_INDEX_CACHE, {force: true});
728
+ } catch (_) {}
729
+ return;
730
+ }
731
+ try {
732
+ ensureDirSync(path.dirname(IIIF_METADATA_INDEX_CACHE));
733
+ await fsp.writeFile(
734
+ IIIF_METADATA_INDEX_CACHE,
735
+ JSON.stringify(payload, null, 2),
736
+ "utf8",
737
+ );
738
+ } catch (_) {}
739
+ }
740
+
626
741
  async function normalizeToV3(resource) {
627
742
  try {
628
743
  const helpers = await import("@iiif/helpers");
@@ -1521,11 +1636,10 @@ async function buildIiifCollectionPages(CONFIG) {
1521
1636
  const metadataLabelsRaw = Array.isArray(cfg && cfg.metadata)
1522
1637
  ? cfg.metadata
1523
1638
  : [];
1524
- const metadataLabelSet = new Set(
1525
- metadataLabelsRaw
1526
- .map((label) => normalizeMetadataLabel(String(label || "")))
1527
- .filter(Boolean),
1528
- );
1639
+ const metadataLabelsNormalized = metadataLabelsRaw
1640
+ .map((label) => normalizeMetadataLabel(String(label || "")))
1641
+ .filter(Boolean);
1642
+ const metadataLabelSet = new Set(metadataLabelsNormalized);
1529
1643
  const metadataFacetLabels = (() => {
1530
1644
  if (!Array.isArray(metadataLabelsRaw) || !metadataLabelsRaw.length)
1531
1645
  return [];
@@ -1544,6 +1658,11 @@ async function buildIiifCollectionPages(CONFIG) {
1544
1658
  }
1545
1659
  return entries;
1546
1660
  })();
1661
+ const metadataLabelSlugMap = new Map();
1662
+ for (const entry of metadataFacetLabels) {
1663
+ if (!entry || !entry.normalized) continue;
1664
+ metadataLabelSlugMap.set(entry.normalized, entry.slug || "");
1665
+ }
1547
1666
  const metadataOptions = {
1548
1667
  enabled:
1549
1668
  metadataEnabled &&
@@ -1551,6 +1670,51 @@ async function buildIiifCollectionPages(CONFIG) {
1551
1670
  includeAll: metadataIncludeAll,
1552
1671
  labelsSet: metadataIncludeAll ? null : metadataLabelSet,
1553
1672
  };
1673
+ const metadataIndexMap = new Map();
1674
+ const metadataDynamicOrder = [];
1675
+ const metadataDynamicOrderSet = new Set();
1676
+ const metadataCollectAllLabels = metadataLabelsNormalized.length === 0;
1677
+ function recordMetadataIndexEntry(entry) {
1678
+ if (!entry || !entry.normalized || !Array.isArray(entry.values)) return;
1679
+ if (!entry.values.length) return;
1680
+ const normalized = entry.normalized;
1681
+ if (!metadataCollectAllLabels && !metadataLabelSet.has(normalized)) return;
1682
+ let record = metadataIndexMap.get(normalized);
1683
+ const labelText = entry.label || normalized;
1684
+ if (!record) {
1685
+ const labelSlug =
1686
+ metadataLabelSlugMap.get(normalized) ||
1687
+ slugify(labelText, {lower: true, strict: true, trim: true}) ||
1688
+ normalized;
1689
+ record = {
1690
+ normalized,
1691
+ label: labelText,
1692
+ slug: labelSlug || normalized,
1693
+ values: new Map(),
1694
+ };
1695
+ metadataIndexMap.set(normalized, record);
1696
+ if (metadataCollectAllLabels && !metadataDynamicOrderSet.has(normalized)) {
1697
+ metadataDynamicOrderSet.add(normalized);
1698
+ metadataDynamicOrder.push(normalized);
1699
+ }
1700
+ } else if (!record.label && labelText) {
1701
+ record.label = labelText;
1702
+ }
1703
+ const valueMap = record.values;
1704
+ for (const text of entry.values) {
1705
+ const trimmed = String(text || "").trim();
1706
+ if (!trimmed) continue;
1707
+ const valueSlug =
1708
+ slugify(trimmed, {lower: true, strict: true, trim: true}) ||
1709
+ trimmed.toLowerCase().replace(/[^a-z0-9]+/g, "-") ||
1710
+ trimmed;
1711
+ if (!valueSlug) continue;
1712
+ if (!valueMap.has(valueSlug)) {
1713
+ valueMap.set(valueSlug, {value: trimmed, slug: valueSlug});
1714
+ }
1715
+ }
1716
+ }
1717
+
1554
1718
  const summaryOptions = {
1555
1719
  enabled: summaryEnabled,
1556
1720
  };
@@ -2308,6 +2472,15 @@ async function buildIiifCollectionPages(CONFIG) {
2308
2472
  let metadataValues = [];
2309
2473
  let summaryValue = "";
2310
2474
  let annotationValue = "";
2475
+ try {
2476
+ const metadataEntries = extractMetadataEntries(manifest, {
2477
+ includeAll: metadataCollectAllLabels,
2478
+ labelsSet: metadataLabelSet,
2479
+ });
2480
+ if (metadataEntries && metadataEntries.length) {
2481
+ for (const entry of metadataEntries) recordMetadataIndexEntry(entry);
2482
+ }
2483
+ } catch (_) {}
2311
2484
  if (metadataOptions && metadataOptions.enabled) {
2312
2485
  try {
2313
2486
  metadataValues = extractMetadataValues(manifest, metadataOptions);
@@ -2436,10 +2609,18 @@ async function buildIiifCollectionPages(CONFIG) {
2436
2609
  logLine(
2437
2610
  `IIIF chunk summary: ${totalItems} Manifest(s) in ${formatDurationMs(totalDuration)} (avg chunk ${formatDurationMs(avgDuration)}, ${rateLabel})`,
2438
2611
  "cyan",
2439
- {dim: true},
2612
+ {dim: true},
2440
2613
  );
2441
2614
  } catch (_) {}
2442
2615
  }
2616
+ try {
2617
+ const metadataIndexPayload = buildMetadataIndexPayload(
2618
+ metadataIndexMap,
2619
+ metadataLabelsNormalized,
2620
+ metadataDynamicOrder,
2621
+ );
2622
+ await writeMetadataIndexFile(metadataIndexPayload);
2623
+ } catch (_) {}
2443
2624
  try {
2444
2625
  await navPlace.writeNavPlaceDataset(navPlaceRecords);
2445
2626
  try {
@@ -2492,6 +2673,7 @@ module.exports.__TESTING__ = {
2492
2673
  extractSummaryValues,
2493
2674
  truncateSummary,
2494
2675
  extractMetadataValues,
2676
+ extractMetadataEntries,
2495
2677
  extractAnnotationText,
2496
2678
  normalizeIiifId,
2497
2679
  normalizeIiifType,
@@ -0,0 +1,32 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const METADATA_INDEX_PATH = path.resolve('.cache/iiif/metadata-index.json');
5
+
6
+ let cachedIndex = null;
7
+
8
+ function readMetadataIndexFromDisk() {
9
+ try {
10
+ if (!fs.existsSync(METADATA_INDEX_PATH)) return [];
11
+ const raw = fs.readFileSync(METADATA_INDEX_PATH, 'utf8');
12
+ const data = JSON.parse(raw);
13
+ return Array.isArray(data) ? data : [];
14
+ } catch (_) {
15
+ return [];
16
+ }
17
+ }
18
+
19
+ function getMetadataIndex() {
20
+ if (cachedIndex) return cachedIndex;
21
+ cachedIndex = readMetadataIndexFromDisk();
22
+ return cachedIndex;
23
+ }
24
+
25
+ function resetMetadataIndex() {
26
+ cachedIndex = null;
27
+ }
28
+
29
+ module.exports = {
30
+ getMetadataIndex,
31
+ resetMetadataIndex,
32
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.6.16",
3
+ "version": "1.6.18",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",