@canopy-iiif/app 0.6.28 → 0.7.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/lib/build/assets.js +30 -0
- package/lib/build/build.js +184 -0
- package/lib/{dev.js → build/dev.js} +5 -5
- package/lib/build/facets.js +19 -0
- package/lib/{iiif.js → build/iiif.js} +222 -373
- package/lib/{mdx.js → build/mdx.js} +13 -10
- package/lib/build/pages.js +141 -0
- package/lib/build/runtimes.js +58 -0
- package/lib/build/search-index.js +42 -0
- package/lib/build/search.js +219 -0
- package/lib/build/styles.js +99 -0
- package/lib/{thumbnail.js → iiif/thumbnail.js} +0 -1
- package/lib/index.js +2 -3
- package/lib/{search.js → search/search.js} +8 -8
- package/package.json +1 -1
- package/lib/build.js +0 -762
- package/lib/components/IIIFCard.js +0 -102
- package/lib/runtime/command-entry.jsx +0 -44
- /package/lib/{devtoast.config.json → build/devtoast.config.json} +0 -0
- /package/lib/{devtoast.css → build/devtoast.css} +0 -0
- /package/lib/{log.js → build/log.js} +0 -0
- /package/lib/{search-app.jsx → search/search-app.jsx} +0 -0
|
@@ -11,7 +11,7 @@ const {
|
|
|
11
11
|
CONTENT_DIR,
|
|
12
12
|
ensureDirSync,
|
|
13
13
|
htmlShell,
|
|
14
|
-
} = require("
|
|
14
|
+
} = require("../common");
|
|
15
15
|
const mdx = require("./mdx");
|
|
16
16
|
const { log, logLine, logResponse } = require("./log");
|
|
17
17
|
|
|
@@ -88,7 +88,6 @@ async function readJsonFromUri(uri, options = { log: false }) {
|
|
|
88
88
|
if (options && options.log) {
|
|
89
89
|
try {
|
|
90
90
|
if (res && res.ok) {
|
|
91
|
-
// Bold success line for Collection fetches
|
|
92
91
|
logLine(`✓ ${String(uri)} ➜ ${res.status}`, "yellow", {
|
|
93
92
|
bright: true,
|
|
94
93
|
});
|
|
@@ -247,13 +246,18 @@ function computeUniqueSlug(index, baseSlug, id, type) {
|
|
|
247
246
|
function ensureBaseSlugFor(index, baseSlug, id, type) {
|
|
248
247
|
try {
|
|
249
248
|
const byId = Array.isArray(index && index.byId) ? index.byId : [];
|
|
250
|
-
const normId = normalizeIiifId(String(id ||
|
|
249
|
+
const normId = normalizeIiifId(String(id || ""));
|
|
251
250
|
const existingWithBase = byId.find(
|
|
252
251
|
(e) => e && e.type === type && String(e.slug) === String(baseSlug)
|
|
253
252
|
);
|
|
254
253
|
if (existingWithBase && normalizeIiifId(existingWithBase.id) !== normId) {
|
|
255
254
|
// Reassign the existing entry to the next available suffix to free the base
|
|
256
|
-
const newSlug = computeUniqueSlug(
|
|
255
|
+
const newSlug = computeUniqueSlug(
|
|
256
|
+
index,
|
|
257
|
+
baseSlug,
|
|
258
|
+
existingWithBase.id,
|
|
259
|
+
type
|
|
260
|
+
);
|
|
257
261
|
if (newSlug && newSlug !== baseSlug) existingWithBase.slug = newSlug;
|
|
258
262
|
}
|
|
259
263
|
} catch (_) {}
|
|
@@ -476,351 +480,152 @@ async function saveCachedCollection(collection, id, parentId) {
|
|
|
476
480
|
}
|
|
477
481
|
|
|
478
482
|
async function loadConfig() {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const configPath = path.resolve(overrideConfigPath || "canopy.yml");
|
|
488
|
-
if (fs.existsSync(configPath)) {
|
|
489
|
-
try {
|
|
490
|
-
const raw = await fsp.readFile(configPath, "utf8");
|
|
491
|
-
const data = yaml.load(raw) || {};
|
|
492
|
-
const d = data || {};
|
|
493
|
-
const di = d.iiif || {};
|
|
494
|
-
CONFIG = {
|
|
495
|
-
collection: {
|
|
496
|
-
uri: (d.collection && d.collection.uri) || CONFIG.collection.uri,
|
|
497
|
-
},
|
|
498
|
-
iiif: {
|
|
499
|
-
chunkSize:
|
|
500
|
-
Number(di.chunkSize || CONFIG.iiif.chunkSize) ||
|
|
501
|
-
CONFIG.iiif.chunkSize,
|
|
502
|
-
concurrency:
|
|
503
|
-
Number(di.concurrency || CONFIG.iiif.concurrency) ||
|
|
504
|
-
CONFIG.iiif.concurrency,
|
|
505
|
-
thumbnails: {
|
|
506
|
-
unsafe: !!(di.thumbnails && di.thumbnails.unsafe === true),
|
|
507
|
-
preferredSize:
|
|
508
|
-
Number(di.thumbnails && di.thumbnails.preferredSize) || 1200,
|
|
509
|
-
},
|
|
510
|
-
},
|
|
511
|
-
metadata: Array.isArray(d.metadata)
|
|
512
|
-
? d.metadata.map((s) => String(s)).filter(Boolean)
|
|
513
|
-
: [],
|
|
514
|
-
};
|
|
515
|
-
const src = overrideConfigPath ? overrideConfigPath : "canopy.yml";
|
|
516
|
-
logLine(`[canopy] Loaded config from ${src}...`, "white");
|
|
517
|
-
} catch (e) {
|
|
518
|
-
console.warn(
|
|
519
|
-
"[canopy] Failed to read",
|
|
520
|
-
overrideConfigPath ? overrideConfigPath : "canopy.yml",
|
|
521
|
-
e.message
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (process.env.CANOPY_COLLECTION_URI) {
|
|
526
|
-
CONFIG.collection.uri = String(process.env.CANOPY_COLLECTION_URI);
|
|
527
|
-
console.log("Using collection URI from CANOPY_COLLECTION_URI");
|
|
483
|
+
const cfgPath = path.resolve("canopy.yml");
|
|
484
|
+
if (!fs.existsSync(cfgPath)) return {};
|
|
485
|
+
const raw = await fsp.readFile(cfgPath, "utf8");
|
|
486
|
+
let cfg = {};
|
|
487
|
+
try {
|
|
488
|
+
cfg = yaml.load(raw) || {};
|
|
489
|
+
} catch (_) {
|
|
490
|
+
cfg = {};
|
|
528
491
|
}
|
|
529
|
-
return
|
|
492
|
+
return cfg || {};
|
|
530
493
|
}
|
|
531
494
|
|
|
495
|
+
// Traverse IIIF collection, cache manifests/collections, and render pages
|
|
532
496
|
async function buildIiifCollectionPages(CONFIG) {
|
|
533
|
-
const
|
|
534
|
-
const worksLayoutPath = path.join(worksDir, "_layout.mdx");
|
|
535
|
-
if (!fs.existsSync(worksLayoutPath)) {
|
|
536
|
-
console.log(
|
|
537
|
-
"IIIF: No content/works/_layout.mdx found; skipping IIIF page build."
|
|
538
|
-
);
|
|
539
|
-
return { searchRecords: [] };
|
|
540
|
-
}
|
|
541
|
-
ensureDirSync(IIIF_CACHE_MANIFESTS_DIR);
|
|
542
|
-
ensureDirSync(IIIF_CACHE_COLLECTIONS_DIR);
|
|
543
|
-
// Debug: list current collections cache contents
|
|
544
|
-
try {
|
|
545
|
-
if (process.env.CANOPY_IIIF_DEBUG === "1") {
|
|
546
|
-
const { logLine } = require("./log");
|
|
547
|
-
try {
|
|
548
|
-
const files = fs.existsSync(IIIF_CACHE_COLLECTIONS_DIR)
|
|
549
|
-
? fs
|
|
550
|
-
.readdirSync(IIIF_CACHE_COLLECTIONS_DIR)
|
|
551
|
-
.filter((n) => /\.json$/i.test(n))
|
|
552
|
-
: [];
|
|
553
|
-
const head = files.slice(0, 8).join(", ");
|
|
554
|
-
logLine(
|
|
555
|
-
`IIIF: cache/collections (start): ${files.length} file(s)` +
|
|
556
|
-
(head ? ` [${head}${files.length > 8 ? ", …" : ""}]` : ""),
|
|
557
|
-
"blue",
|
|
558
|
-
{ dim: true }
|
|
559
|
-
);
|
|
560
|
-
} catch (_) {}
|
|
561
|
-
}
|
|
562
|
-
} catch (_) {}
|
|
497
|
+
const cfg = CONFIG || (await loadConfig());
|
|
563
498
|
const collectionUri =
|
|
564
|
-
(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
} catch (_) {}
|
|
575
|
-
// Decide cache policy/log before any fetch
|
|
576
|
-
let index = await loadManifestIndex();
|
|
577
|
-
const prevUri = (index && index.collection && index.collection.uri) || "";
|
|
578
|
-
const uriChanged = !!(collectionUri && prevUri && prevUri !== collectionUri);
|
|
579
|
-
if (uriChanged) {
|
|
580
|
-
try {
|
|
581
|
-
const { logLine } = require("./log");
|
|
582
|
-
logLine("IIIF Collection changed. Resetting cache.\n", "cyan");
|
|
583
|
-
} catch (_) {}
|
|
584
|
-
await flushManifestCache();
|
|
585
|
-
index.byId = [];
|
|
586
|
-
} else {
|
|
587
|
-
try {
|
|
588
|
-
const { logLine } = require("./log");
|
|
589
|
-
logLine(
|
|
590
|
-
"IIIF Collection unchanged. Preserving cache. Detecting cached resources...\n",
|
|
591
|
-
"cyan"
|
|
592
|
-
);
|
|
593
|
-
} catch (_) {}
|
|
594
|
-
}
|
|
595
|
-
let collection = null;
|
|
596
|
-
if (collectionUri) {
|
|
597
|
-
// Try cached root collection first
|
|
598
|
-
collection = await loadCachedCollectionById(collectionUri);
|
|
599
|
-
if (collection) {
|
|
600
|
-
try {
|
|
601
|
-
const { logLine } = require("./log");
|
|
602
|
-
logLine(`✓ ${String(collectionUri)} ➜ Cached`, "yellow");
|
|
603
|
-
} catch (_) {}
|
|
604
|
-
} else {
|
|
605
|
-
collection = await readJsonFromUri(collectionUri, { log: true });
|
|
606
|
-
if (collection) {
|
|
607
|
-
try {
|
|
608
|
-
await saveCachedCollection(
|
|
609
|
-
collection,
|
|
610
|
-
String(collection.id || collection["@id"] || collectionUri),
|
|
611
|
-
""
|
|
612
|
-
);
|
|
613
|
-
} catch (_) {}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (!collection) {
|
|
618
|
-
console.warn("IIIF: No collection available; skipping.");
|
|
619
|
-
// Still write/update global index with configured URI, so other steps can rely on it
|
|
620
|
-
try {
|
|
621
|
-
const index = await loadManifestIndex();
|
|
622
|
-
index.collection = {
|
|
623
|
-
uri: String(collectionUri || ""),
|
|
624
|
-
hash: "",
|
|
625
|
-
updatedAt: new Date().toISOString(),
|
|
626
|
-
};
|
|
627
|
-
await saveManifestIndex(index);
|
|
628
|
-
} catch (_) {}
|
|
499
|
+
(cfg && cfg.collection && cfg.collection.uri) ||
|
|
500
|
+
process.env.CANOPY_COLLECTION_URI ||
|
|
501
|
+
"";
|
|
502
|
+
if (!collectionUri) return { searchRecords: [] };
|
|
503
|
+
|
|
504
|
+
// Fetch top-level collection
|
|
505
|
+
logLine("IIIF: Fetching collection...", "blue", { bright: true });
|
|
506
|
+
const root = await readJsonFromUri(collectionUri, { log: true });
|
|
507
|
+
if (!root) {
|
|
508
|
+
logLine("IIIF: Failed to fetch collection", "red");
|
|
629
509
|
return { searchRecords: [] };
|
|
630
510
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const currentSig = {
|
|
634
|
-
uri: String(collectionUri || ""),
|
|
635
|
-
hash: computeHash(collectionForHash),
|
|
636
|
-
};
|
|
637
|
-
index.collection = { ...currentSig, updatedAt: new Date().toISOString() };
|
|
638
|
-
// Upsert root collection entry in index.byId
|
|
511
|
+
const normalizedRoot = await normalizeToV3(root);
|
|
512
|
+
// Save collection cache
|
|
639
513
|
try {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const baseSlug = slugify(title || "collection", { lower: true, strict: true, trim: true }) || "collection";
|
|
645
|
-
index.byId = Array.isArray(index.byId) ? index.byId : [];
|
|
646
|
-
const slug = ensureBaseSlugFor(index, baseSlug, rootId, 'Collection');
|
|
647
|
-
const existingIdx = index.byId.findIndex(
|
|
648
|
-
(e) => e && normalizeIiifId(e.id) === rootId && e.type === "Collection"
|
|
514
|
+
await saveCachedCollection(
|
|
515
|
+
normalizedRoot,
|
|
516
|
+
normalizedRoot.id || collectionUri,
|
|
517
|
+
""
|
|
649
518
|
);
|
|
650
|
-
const entry = { id: rootId, type: "Collection", slug, parent: "" };
|
|
651
|
-
if (existingIdx >= 0) index.byId[existingIdx] = entry;
|
|
652
|
-
else index.byId.push(entry);
|
|
653
|
-
try { (RESERVED_SLUGS && RESERVED_SLUGS.Collection || new Set()).add(slug); } catch (_) {}
|
|
654
519
|
} catch (_) {}
|
|
655
|
-
await saveManifestIndex(index);
|
|
656
520
|
|
|
657
|
-
|
|
658
|
-
// Recursively collect manifests across subcollections
|
|
521
|
+
// Recursively traverse Collections and gather all Manifest tasks
|
|
659
522
|
const tasks = [];
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (c) {
|
|
663
|
-
try {
|
|
664
|
-
// Emit a standard Cached line so it mirrors network logs
|
|
665
|
-
const { logLine } = require("./log");
|
|
666
|
-
logLine(`✓ ${String(id)} ➜ Cached`, "yellow");
|
|
667
|
-
} catch (_) {}
|
|
668
|
-
if (lns) lns.push([`✓ ${String(id)} ➜ Cached`, "yellow"]);
|
|
669
|
-
// Always normalize to Presentation 3 for consistent traversal
|
|
670
|
-
return await normalizeToV3(c);
|
|
671
|
-
}
|
|
672
|
-
if (!fetchAllowed) return null;
|
|
673
|
-
const fetched = await readJsonFromUri(String(id), { log: true });
|
|
674
|
-
const normalized = await normalizeToV3(fetched);
|
|
523
|
+
const visitedCollections = new Set(); // normalized ids
|
|
524
|
+
const norm = (x) => {
|
|
675
525
|
try {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
""
|
|
680
|
-
);
|
|
681
|
-
} catch (_) {}
|
|
682
|
-
return normalized || fetched;
|
|
683
|
-
}
|
|
684
|
-
async function collectTasksFromCollection(colObj, parentUri, visited) {
|
|
685
|
-
if (!colObj) return;
|
|
686
|
-
// Prefer the URI we traversed from (parentUri) to work around sources
|
|
687
|
-
// that report the same id for multiple pages (e.g., Internet Archive).
|
|
688
|
-
const colId = String(parentUri || colObj.id || colObj["@id"] || "");
|
|
689
|
-
visited = visited || new Set();
|
|
690
|
-
if (colId) {
|
|
691
|
-
if (visited.has(colId)) return;
|
|
692
|
-
visited.add(colId);
|
|
526
|
+
return normalizeIiifId(String(x || ""));
|
|
527
|
+
} catch (_) {
|
|
528
|
+
return String(x || "");
|
|
693
529
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
slug,
|
|
725
|
-
parent: normalizeIiifId(String(colId || parentUri || "")),
|
|
726
|
-
};
|
|
727
|
-
const existing = idx.byId.findIndex(
|
|
728
|
-
(e) =>
|
|
729
|
-
e &&
|
|
730
|
-
normalizeIiifId(e.id) === subIdNorm &&
|
|
731
|
-
e.type === "Collection"
|
|
732
|
-
);
|
|
733
|
-
if (existing >= 0) idx.byId[existing] = entry;
|
|
734
|
-
else idx.byId.push(entry);
|
|
735
|
-
await saveManifestIndex(idx);
|
|
736
|
-
} catch (_) {}
|
|
737
|
-
await collectTasksFromCollection(subRaw, String(id), visited);
|
|
738
|
-
} else if (/^https?:\/\//i.test(String(id || ""))) {
|
|
739
|
-
let norm = await loadOrFetchCollectionById(String(id));
|
|
740
|
-
const nt = String((norm && (norm.type || norm["@type"])) || "");
|
|
741
|
-
if (nt.includes("Collection")) {
|
|
742
|
-
try {
|
|
743
|
-
const title = firstLabelString(norm && norm.label);
|
|
744
|
-
const baseSlug = slugify(title || "collection", { lower: true, strict: true, trim: true }) || "collection";
|
|
745
|
-
const idx = await loadManifestIndex();
|
|
746
|
-
idx.byId = Array.isArray(idx.byId) ? idx.byId : [];
|
|
747
|
-
const normId = normalizeIiifId(String(id));
|
|
748
|
-
const slug = computeUniqueSlug(idx, baseSlug, normId, 'Collection');
|
|
749
|
-
const entry = {
|
|
750
|
-
id: normId,
|
|
751
|
-
type: "Collection",
|
|
752
|
-
slug,
|
|
753
|
-
parent: normalizeIiifId(String(colId || parentUri || "")),
|
|
754
|
-
};
|
|
755
|
-
const existing = idx.byId.findIndex(
|
|
756
|
-
(e) => e && normalizeIiifId(e.id) === normId && e.type === "Collection"
|
|
757
|
-
);
|
|
758
|
-
if (existing >= 0) idx.byId[existing] = entry;
|
|
759
|
-
else idx.byId.push(entry);
|
|
760
|
-
await saveManifestIndex(idx);
|
|
761
|
-
} catch (_) {}
|
|
762
|
-
try {
|
|
763
|
-
await saveCachedCollection(
|
|
764
|
-
norm,
|
|
765
|
-
String(id),
|
|
766
|
-
String(colId || parentUri || "")
|
|
767
|
-
);
|
|
768
|
-
} catch (_) {}
|
|
769
|
-
await collectTasksFromCollection(norm, String(id), visited);
|
|
770
|
-
} else if (nt.includes("Manifest")) {
|
|
771
|
-
tasks.push({
|
|
772
|
-
id: String(id),
|
|
773
|
-
parent: String(colId || parentUri || ""),
|
|
774
|
-
});
|
|
530
|
+
};
|
|
531
|
+
async function gatherFromCollection(colLike, parentId) {
|
|
532
|
+
try {
|
|
533
|
+
// Resolve uri/id
|
|
534
|
+
const uri =
|
|
535
|
+
typeof colLike === "string"
|
|
536
|
+
? colLike
|
|
537
|
+
: (colLike && (colLike.id || colLike["@id"])) || "";
|
|
538
|
+
const col =
|
|
539
|
+
typeof colLike === "object" && colLike && colLike.items
|
|
540
|
+
? colLike
|
|
541
|
+
: await readJsonFromUri(uri, { log: true });
|
|
542
|
+
if (!col) return;
|
|
543
|
+
const ncol = await normalizeToV3(col);
|
|
544
|
+
const colId = String((ncol && (ncol.id || uri)) || uri);
|
|
545
|
+
const nid = norm(colId);
|
|
546
|
+
if (visitedCollections.has(nid)) return; // avoid cycles
|
|
547
|
+
visitedCollections.add(nid);
|
|
548
|
+
try {
|
|
549
|
+
await saveCachedCollection(ncol, colId, parentId || "");
|
|
550
|
+
} catch (_) {}
|
|
551
|
+
const itemsArr = Array.isArray(ncol.items) ? ncol.items : [];
|
|
552
|
+
for (const entry of itemsArr) {
|
|
553
|
+
if (!entry) continue;
|
|
554
|
+
const t = String(entry.type || entry["@type"] || "").toLowerCase();
|
|
555
|
+
const entryId = entry.id || entry["@id"] || "";
|
|
556
|
+
if (t === "manifest") {
|
|
557
|
+
tasks.push({ id: entryId, parent: colId });
|
|
558
|
+
} else if (t === "collection") {
|
|
559
|
+
await gatherFromCollection(entryId, colId);
|
|
775
560
|
}
|
|
776
561
|
}
|
|
777
|
-
|
|
562
|
+
// Traverse strictly by parent/child hierarchy (Presentation 3): items → Manifest or Collection
|
|
563
|
+
} catch (_) {}
|
|
778
564
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
String((collectionV3 && (collectionV3.id || collectionV3["@id"])) || collectionUri || ""),
|
|
784
|
-
new Set()
|
|
785
|
-
);
|
|
565
|
+
await gatherFromCollection(normalizedRoot, "");
|
|
566
|
+
if (!tasks.length) return { searchRecords: [] };
|
|
567
|
+
|
|
568
|
+
// Split into chunks and process with limited concurrency
|
|
786
569
|
const chunkSize = Math.max(
|
|
787
570
|
1,
|
|
788
571
|
Number(
|
|
789
|
-
process.env.CANOPY_CHUNK_SIZE ||
|
|
790
|
-
(CONFIG.iiif && CONFIG.iiif.chunkSize) ||
|
|
791
|
-
10
|
|
572
|
+
process.env.CANOPY_CHUNK_SIZE || (cfg.iiif && cfg.iiif.chunkSize) || 20
|
|
792
573
|
)
|
|
793
574
|
);
|
|
794
|
-
const chunks = Math.
|
|
795
|
-
try {
|
|
796
|
-
logLine(
|
|
797
|
-
`\nAggregating ${tasks.length} Manifest(s) in ${chunks} chunk(s)...`,
|
|
798
|
-
"cyan"
|
|
799
|
-
);
|
|
800
|
-
} catch (_) {}
|
|
575
|
+
const chunks = Math.ceil(tasks.length / chunkSize);
|
|
801
576
|
const searchRecords = [];
|
|
802
577
|
const unsafeThumbs = !!(
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
578
|
+
cfg &&
|
|
579
|
+
cfg.iiif &&
|
|
580
|
+
cfg.iiif.thumbnails &&
|
|
581
|
+
cfg.iiif.thumbnails.unsafe === true
|
|
807
582
|
);
|
|
808
583
|
const thumbSize =
|
|
809
|
-
(
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
584
|
+
(cfg &&
|
|
585
|
+
cfg.iiif &&
|
|
586
|
+
cfg.iiif.thumbnails &&
|
|
587
|
+
cfg.iiif.thumbnails.preferredSize) ||
|
|
813
588
|
1200;
|
|
589
|
+
|
|
590
|
+
// Compile the works layout component once per run
|
|
591
|
+
let WorksLayoutComp = null;
|
|
592
|
+
try {
|
|
593
|
+
const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
|
|
594
|
+
WorksLayoutComp = await mdx.compileMdxToComponent(worksLayoutPath);
|
|
595
|
+
} catch (_) {
|
|
596
|
+
// Minimal fallback layout if missing or fails to compile
|
|
597
|
+
WorksLayoutComp = function FallbackWorksLayout({ manifest }) {
|
|
598
|
+
const title = firstLabelString(manifest && manifest.label);
|
|
599
|
+
return React.createElement(
|
|
600
|
+
"div",
|
|
601
|
+
{ className: "content" },
|
|
602
|
+
React.createElement("h1", null, title || "Untitled"),
|
|
603
|
+
// Render viewer placeholder for hydration
|
|
604
|
+
React.createElement(
|
|
605
|
+
"div",
|
|
606
|
+
{ "data-canopy-viewer": "1" },
|
|
607
|
+
React.createElement("script", {
|
|
608
|
+
type: "application/json",
|
|
609
|
+
dangerouslySetInnerHTML: {
|
|
610
|
+
__html: JSON.stringify({
|
|
611
|
+
iiifContent: manifest && (manifest.id || ""),
|
|
612
|
+
}),
|
|
613
|
+
},
|
|
614
|
+
})
|
|
615
|
+
)
|
|
616
|
+
);
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
814
620
|
for (let ci = 0; ci < chunks; ci++) {
|
|
815
621
|
const chunk = tasks.slice(ci * chunkSize, (ci + 1) * chunkSize);
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
} catch (_) {}
|
|
622
|
+
logLine(`\nChunk (${ci + 1}/${chunks})`, "white", { dim: true });
|
|
623
|
+
|
|
819
624
|
const concurrency = Math.max(
|
|
820
625
|
1,
|
|
821
626
|
Number(
|
|
822
627
|
process.env.CANOPY_FETCH_CONCURRENCY ||
|
|
823
|
-
(
|
|
628
|
+
(cfg.iiif && cfg.iiif.concurrency) ||
|
|
824
629
|
6
|
|
825
630
|
)
|
|
826
631
|
);
|
|
@@ -848,9 +653,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
848
653
|
const idx = next - 1;
|
|
849
654
|
const id = it.id || it["@id"] || "";
|
|
850
655
|
let manifest = await loadCachedManifestById(id);
|
|
851
|
-
// Buffer logs for ordered output
|
|
852
656
|
const lns = [];
|
|
853
|
-
// Logging: cached or fetched
|
|
854
657
|
if (manifest) {
|
|
855
658
|
lns.push([`✓ ${String(id)} ➜ Cached`, "yellow"]);
|
|
856
659
|
} else if (/^https?:\/\//i.test(String(id || ""))) {
|
|
@@ -880,7 +683,6 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
880
683
|
continue;
|
|
881
684
|
}
|
|
882
685
|
} else if (/^file:\/\//i.test(String(id || ""))) {
|
|
883
|
-
// Support local manifests via file:// in dev
|
|
884
686
|
try {
|
|
885
687
|
const local = await readJsonFromUri(String(id), { log: false });
|
|
886
688
|
if (!local) {
|
|
@@ -900,48 +702,58 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
900
702
|
continue;
|
|
901
703
|
}
|
|
902
704
|
} else {
|
|
903
|
-
// Unsupported scheme; skip
|
|
904
705
|
lns.push([`✗ ${String(id)} ➜ SKIP`, "red"]);
|
|
905
706
|
continue;
|
|
906
707
|
}
|
|
907
708
|
if (!manifest) continue;
|
|
908
709
|
manifest = await normalizeToV3(manifest);
|
|
909
710
|
const title = firstLabelString(manifest.label);
|
|
910
|
-
const baseSlug =
|
|
711
|
+
const baseSlug =
|
|
712
|
+
slugify(title || "untitled", {
|
|
713
|
+
lower: true,
|
|
714
|
+
strict: true,
|
|
715
|
+
trim: true,
|
|
716
|
+
}) || "untitled";
|
|
911
717
|
const nid = normalizeIiifId(String(manifest.id || id));
|
|
912
|
-
// Use existing mapping if present; otherwise allocate and persist
|
|
913
718
|
let idxMap = await loadManifestIndex();
|
|
914
719
|
idxMap.byId = Array.isArray(idxMap.byId) ? idxMap.byId : [];
|
|
915
|
-
let mEntry = idxMap.byId.find(
|
|
720
|
+
let mEntry = idxMap.byId.find(
|
|
721
|
+
(e) => e && e.type === "Manifest" && normalizeIiifId(e.id) === nid
|
|
722
|
+
);
|
|
916
723
|
let slug = mEntry && mEntry.slug;
|
|
917
724
|
if (!slug) {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
725
|
+
slug = computeUniqueSlug(idxMap, baseSlug, nid, "Manifest");
|
|
726
|
+
const parentNorm = normalizeIiifId(String(it.parent || ""));
|
|
727
|
+
const newEntry = {
|
|
728
|
+
id: nid,
|
|
729
|
+
type: "Manifest",
|
|
730
|
+
slug,
|
|
731
|
+
parent: parentNorm,
|
|
732
|
+
};
|
|
733
|
+
const existingIdx = idxMap.byId.findIndex(
|
|
734
|
+
(e) => e && e.type === "Manifest" && normalizeIiifId(e.id) === nid
|
|
735
|
+
);
|
|
736
|
+
if (existingIdx >= 0) idxMap.byId[existingIdx] = newEntry;
|
|
737
|
+
else idxMap.byId.push(newEntry);
|
|
924
738
|
await saveManifestIndex(idxMap);
|
|
925
739
|
}
|
|
926
740
|
const href = path.join("works", slug + ".html");
|
|
927
741
|
const outPath = path.join(OUT_DIR, href);
|
|
928
742
|
ensureDirSync(path.dirname(outPath));
|
|
929
743
|
try {
|
|
930
|
-
// Provide MDX components mapping so tags like <Viewer/> and <HelloWorld/> resolve
|
|
931
744
|
let components = {};
|
|
932
745
|
try {
|
|
933
746
|
components = await import("@canopy-iiif/app/ui");
|
|
934
747
|
} catch (_) {
|
|
935
748
|
components = {};
|
|
936
749
|
}
|
|
937
|
-
const { withBase } = require("
|
|
750
|
+
const { withBase } = require("../common");
|
|
938
751
|
const Anchor = function A(props) {
|
|
939
752
|
let { href = "", ...rest } = props || {};
|
|
940
753
|
href = withBase(href);
|
|
941
754
|
return React.createElement("a", { href, ...rest }, props.children);
|
|
942
755
|
};
|
|
943
756
|
const compMap = { ...components, a: Anchor };
|
|
944
|
-
// Gracefully handle HelloWorld if not provided anywhere
|
|
945
757
|
if (!components.HelloWorld) {
|
|
946
758
|
components.HelloWorld = components.Fallback
|
|
947
759
|
? (props) =>
|
|
@@ -961,7 +773,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
961
773
|
const { loadAppWrapper } = require("./mdx");
|
|
962
774
|
const app = await loadAppWrapper();
|
|
963
775
|
|
|
964
|
-
const mdxContent = React.createElement(
|
|
776
|
+
const mdxContent = React.createElement(WorksLayoutComp, { manifest });
|
|
965
777
|
const siteTree = app && app.App ? mdxContent : mdxContent;
|
|
966
778
|
const wrappedApp =
|
|
967
779
|
app && app.App
|
|
@@ -988,59 +800,95 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
988
800
|
)
|
|
989
801
|
.split(path.sep)
|
|
990
802
|
.join("/");
|
|
991
|
-
// Detect placeholders to decide which runtimes to inject
|
|
992
803
|
const needsHydrateViewer = body.includes("data-canopy-viewer");
|
|
993
804
|
const needsRelated = body.includes("data-canopy-related-items");
|
|
994
|
-
const needsCommand = body.includes(
|
|
995
|
-
const needsHydrate =
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
805
|
+
const needsCommand = body.includes("data-canopy-command");
|
|
806
|
+
const needsHydrate =
|
|
807
|
+
body.includes("data-canopy-hydrate") ||
|
|
808
|
+
needsHydrateViewer ||
|
|
809
|
+
needsRelated ||
|
|
810
|
+
needsCommand;
|
|
999
811
|
|
|
1000
|
-
// Compute script paths relative to the output HTML
|
|
1001
812
|
const viewerRel = needsHydrateViewer
|
|
1002
|
-
? path
|
|
813
|
+
? path
|
|
814
|
+
.relative(
|
|
815
|
+
path.dirname(outPath),
|
|
816
|
+
path.join(OUT_DIR, "scripts", "canopy-viewer.js")
|
|
817
|
+
)
|
|
818
|
+
.split(path.sep)
|
|
819
|
+
.join("/")
|
|
1003
820
|
: null;
|
|
1004
821
|
const sliderRel = needsRelated
|
|
1005
|
-
? path
|
|
822
|
+
? path
|
|
823
|
+
.relative(
|
|
824
|
+
path.dirname(outPath),
|
|
825
|
+
path.join(OUT_DIR, "scripts", "canopy-slider.js")
|
|
826
|
+
)
|
|
827
|
+
.split(path.sep)
|
|
828
|
+
.join("/")
|
|
1006
829
|
: null;
|
|
1007
830
|
const relatedRel = needsRelated
|
|
1008
|
-
? path
|
|
831
|
+
? path
|
|
832
|
+
.relative(
|
|
833
|
+
path.dirname(outPath),
|
|
834
|
+
path.join(OUT_DIR, "scripts", "canopy-related-items.js")
|
|
835
|
+
)
|
|
836
|
+
.split(path.sep)
|
|
837
|
+
.join("/")
|
|
1009
838
|
: null;
|
|
1010
839
|
const commandRel = needsCommand
|
|
1011
|
-
? path
|
|
840
|
+
? path
|
|
841
|
+
.relative(
|
|
842
|
+
path.dirname(outPath),
|
|
843
|
+
path.join(OUT_DIR, "scripts", "canopy-command.js")
|
|
844
|
+
)
|
|
845
|
+
.split(path.sep)
|
|
846
|
+
.join("/")
|
|
1012
847
|
: null;
|
|
1013
848
|
|
|
1014
|
-
// Choose a main script so execution order is preserved (builder before slider)
|
|
1015
849
|
let jsRel = null;
|
|
1016
850
|
if (needsRelated && sliderRel) jsRel = sliderRel;
|
|
1017
851
|
else if (viewerRel) jsRel = viewerRel;
|
|
1018
852
|
|
|
1019
|
-
// Include hydration scripts via htmlShell
|
|
1020
853
|
let headExtra = head;
|
|
1021
|
-
// Ensure React globals are present only when client React is needed
|
|
1022
854
|
const needsReact = !!(needsHydrateViewer || needsRelated);
|
|
1023
|
-
let vendorTag =
|
|
855
|
+
let vendorTag = "";
|
|
1024
856
|
if (needsReact) {
|
|
1025
857
|
try {
|
|
1026
|
-
const vendorAbs = path.join(
|
|
1027
|
-
|
|
1028
|
-
|
|
858
|
+
const vendorAbs = path.join(
|
|
859
|
+
OUT_DIR,
|
|
860
|
+
"scripts",
|
|
861
|
+
"react-globals.js"
|
|
862
|
+
);
|
|
863
|
+
let vendorRel = path
|
|
864
|
+
.relative(path.dirname(outPath), vendorAbs)
|
|
865
|
+
.split(path.sep)
|
|
866
|
+
.join("/");
|
|
867
|
+
try {
|
|
868
|
+
const stv = fs.statSync(vendorAbs);
|
|
869
|
+
vendorRel += `?v=${Math.floor(stv.mtimeMs || Date.now())}`;
|
|
870
|
+
} catch (_) {}
|
|
1029
871
|
vendorTag = `<script src="${vendorRel}"></script>`;
|
|
1030
872
|
} catch (_) {}
|
|
1031
873
|
}
|
|
1032
|
-
// Prepend additional scripts so the selected jsRel runs last
|
|
1033
874
|
const extraScripts = [];
|
|
1034
|
-
if (relatedRel && jsRel !== relatedRel)
|
|
1035
|
-
|
|
1036
|
-
if (
|
|
1037
|
-
|
|
1038
|
-
if (
|
|
1039
|
-
|
|
1040
|
-
|
|
875
|
+
if (relatedRel && jsRel !== relatedRel)
|
|
876
|
+
extraScripts.push(`<script defer src="${relatedRel}"></script>`);
|
|
877
|
+
if (viewerRel && jsRel !== viewerRel)
|
|
878
|
+
extraScripts.push(`<script defer src="${viewerRel}"></script>`);
|
|
879
|
+
if (sliderRel && jsRel !== sliderRel)
|
|
880
|
+
extraScripts.push(`<script defer src="${sliderRel}"></script>`);
|
|
881
|
+
if (commandRel && jsRel !== commandRel)
|
|
882
|
+
extraScripts.push(`<script defer src="${commandRel}"></script>`);
|
|
883
|
+
if (extraScripts.length)
|
|
884
|
+
headExtra = extraScripts.join("") + headExtra;
|
|
1041
885
|
try {
|
|
1042
|
-
const { BASE_PATH } = require(
|
|
1043
|
-
if (BASE_PATH)
|
|
886
|
+
const { BASE_PATH } = require("../common");
|
|
887
|
+
if (BASE_PATH)
|
|
888
|
+
vendorTag =
|
|
889
|
+
`<script>window.CANOPY_BASE_PATH=${JSON.stringify(
|
|
890
|
+
BASE_PATH
|
|
891
|
+
)}</script>` + vendorTag;
|
|
1044
892
|
} catch (_) {}
|
|
1045
893
|
let pageBody = body;
|
|
1046
894
|
let html = htmlShell({
|
|
@@ -1051,24 +899,23 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1051
899
|
headExtra: vendorTag + headExtra,
|
|
1052
900
|
});
|
|
1053
901
|
try {
|
|
1054
|
-
html = require("
|
|
902
|
+
html = require("../common").applyBaseToHtml(html);
|
|
1055
903
|
} catch (_) {}
|
|
1056
904
|
await fsp.writeFile(outPath, html, "utf8");
|
|
1057
905
|
lns.push([
|
|
1058
906
|
`✓ Created ${path.relative(process.cwd(), outPath)}`,
|
|
1059
907
|
"green",
|
|
1060
908
|
]);
|
|
1061
|
-
// Resolve thumbnail URL and dimensions for this manifest (safe by default; expanded "unsafe" if configured)
|
|
1062
909
|
let thumbUrl = "";
|
|
1063
910
|
let thumbWidth = undefined;
|
|
1064
911
|
let thumbHeight = undefined;
|
|
1065
912
|
try {
|
|
1066
|
-
const { getThumbnail } = require("
|
|
913
|
+
const { getThumbnail } = require("../iiif/thumbnail");
|
|
1067
914
|
const t = await getThumbnail(manifest, thumbSize, unsafeThumbs);
|
|
1068
915
|
if (t && t.url) {
|
|
1069
916
|
thumbUrl = String(t.url);
|
|
1070
|
-
thumbWidth = typeof t.width ===
|
|
1071
|
-
thumbHeight = typeof t.height ===
|
|
917
|
+
thumbWidth = typeof t.width === "number" ? t.width : undefined;
|
|
918
|
+
thumbHeight = typeof t.height === "number" ? t.height : undefined;
|
|
1072
919
|
const idx = await loadManifestIndex();
|
|
1073
920
|
if (Array.isArray(idx.byId)) {
|
|
1074
921
|
const entry = idx.byId.find(
|
|
@@ -1079,23 +926,25 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1079
926
|
);
|
|
1080
927
|
if (entry) {
|
|
1081
928
|
entry.thumbnail = String(thumbUrl);
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
if (typeof thumbHeight ===
|
|
929
|
+
if (typeof thumbWidth === "number")
|
|
930
|
+
entry.thumbnailWidth = thumbWidth;
|
|
931
|
+
if (typeof thumbHeight === "number")
|
|
932
|
+
entry.thumbnailHeight = thumbHeight;
|
|
1085
933
|
await saveManifestIndex(idx);
|
|
1086
934
|
}
|
|
1087
935
|
}
|
|
1088
936
|
}
|
|
1089
937
|
} catch (_) {}
|
|
1090
|
-
// Push search record including thumbnail (if available)
|
|
1091
938
|
searchRecords.push({
|
|
1092
939
|
id: String(manifest.id || id),
|
|
1093
940
|
title,
|
|
1094
941
|
href: href.split(path.sep).join("/"),
|
|
1095
942
|
type: "work",
|
|
1096
943
|
thumbnail: thumbUrl || undefined,
|
|
1097
|
-
thumbnailWidth:
|
|
1098
|
-
|
|
944
|
+
thumbnailWidth:
|
|
945
|
+
typeof thumbWidth === "number" ? thumbWidth : undefined,
|
|
946
|
+
thumbnailHeight:
|
|
947
|
+
typeof thumbHeight === "number" ? thumbHeight : undefined,
|
|
1099
948
|
});
|
|
1100
949
|
} catch (e) {
|
|
1101
950
|
lns.push([
|
|
@@ -1119,10 +968,10 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1119
968
|
module.exports = {
|
|
1120
969
|
buildIiifCollectionPages,
|
|
1121
970
|
loadConfig,
|
|
1122
|
-
// Expose for other build steps that need to annotate cache metadata
|
|
1123
971
|
loadManifestIndex,
|
|
1124
972
|
saveManifestIndex,
|
|
1125
973
|
};
|
|
974
|
+
|
|
1126
975
|
// Debug: list collections cache after traversal
|
|
1127
976
|
try {
|
|
1128
977
|
if (process.env.CANOPY_IIIF_DEBUG === "1") {
|