@canopy-iiif/app 0.10.17 → 0.10.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/build.js +2 -0
- package/lib/build/iiif.js +12 -1
- package/lib/build/pages.js +20 -0
- package/lib/components/referenced.js +212 -0
- package/package.json +1 -1
- package/ui/dist/index.mjs +5 -3
- package/ui/dist/index.mjs.map +2 -2
- package/ui/dist/server.mjs +227 -29
- package/ui/dist/server.mjs.map +4 -4
- package/ui/styles/components/_referenced-items.scss +74 -0
- package/ui/styles/components/index.scss +1 -0
- package/ui/styles/index.css +75 -0
package/lib/build/build.js
CHANGED
|
@@ -20,6 +20,7 @@ const { ensureStyles } = require("./styles");
|
|
|
20
20
|
const { copyAssets } = require("./assets");
|
|
21
21
|
const { logLine } = require("./log");
|
|
22
22
|
const navigation = require("../components/navigation");
|
|
23
|
+
const referenced = require("../components/referenced");
|
|
23
24
|
|
|
24
25
|
// hold records between builds if skipping IIIF
|
|
25
26
|
let iiifRecordsCache = [];
|
|
@@ -42,6 +43,7 @@ async function build(options = {}) {
|
|
|
42
43
|
logLine("• Reset MDX cache", "blue", { dim: true });
|
|
43
44
|
mdx?.resetMdxCaches();
|
|
44
45
|
navigation?.resetNavigationCache?.();
|
|
46
|
+
referenced?.resetReferenceIndex?.();
|
|
45
47
|
if (!skipIiif) {
|
|
46
48
|
await cleanDir(OUT_DIR);
|
|
47
49
|
logLine(`• Cleaned output directory`, "blue", { dim: true });
|
package/lib/build/iiif.js
CHANGED
|
@@ -18,6 +18,7 @@ const mdx = require("./mdx");
|
|
|
18
18
|
const {log, logLine, logResponse} = require("./log");
|
|
19
19
|
const { getPageContext } = require("../page-context");
|
|
20
20
|
const PageContext = getPageContext();
|
|
21
|
+
const referenced = require("../components/referenced");
|
|
21
22
|
const {
|
|
22
23
|
getThumbnail,
|
|
23
24
|
getRepresentativeImage,
|
|
@@ -1288,6 +1289,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1288
1289
|
throw new Error(`Failed to compile content/works/_layout.mdx: ${message}`);
|
|
1289
1290
|
}
|
|
1290
1291
|
|
|
1292
|
+
referenced.ensureReferenceIndex();
|
|
1293
|
+
|
|
1291
1294
|
for (let ci = 0; ci < chunks; ci++) {
|
|
1292
1295
|
const chunk = tasks.slice(ci * chunkSize, (ci + 1) * chunkSize);
|
|
1293
1296
|
logLine(`• Chunk ${ci + 1}/${chunks}`, "blue", {dim: true});
|
|
@@ -1412,6 +1415,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1412
1415
|
else idxMap.byId.push(newEntry);
|
|
1413
1416
|
await saveManifestIndex(idxMap);
|
|
1414
1417
|
}
|
|
1418
|
+
const manifestId = manifest && manifest.id ? manifest.id : id;
|
|
1419
|
+
const references = referenced.getReferencesForManifest(manifestId);
|
|
1415
1420
|
const href = path.join("works", slug + ".html");
|
|
1416
1421
|
const outPath = path.join(OUT_DIR, href);
|
|
1417
1422
|
ensureDirSync(path.dirname(outPath));
|
|
@@ -1456,6 +1461,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1456
1461
|
slug,
|
|
1457
1462
|
type: "work",
|
|
1458
1463
|
description: pageDescription,
|
|
1464
|
+
manifestId,
|
|
1465
|
+
referencedBy: references,
|
|
1459
1466
|
meta: {
|
|
1460
1467
|
title,
|
|
1461
1468
|
description: pageDescription,
|
|
@@ -1471,7 +1478,11 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
1471
1478
|
pageDetails.meta.ogImage = ogImageForPage;
|
|
1472
1479
|
}
|
|
1473
1480
|
const pageContextValue = { navigation: null, page: pageDetails };
|
|
1474
|
-
const mdxContent = React.createElement(WorksLayoutComp, {
|
|
1481
|
+
const mdxContent = React.createElement(WorksLayoutComp, {
|
|
1482
|
+
manifest,
|
|
1483
|
+
references,
|
|
1484
|
+
manifestId,
|
|
1485
|
+
});
|
|
1475
1486
|
const siteTree = mdxContent;
|
|
1476
1487
|
const wrappedApp =
|
|
1477
1488
|
app && app.App
|
package/lib/build/pages.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
const { log } = require('./log');
|
|
12
12
|
const mdx = require('./mdx');
|
|
13
13
|
const navigation = require('../components/navigation');
|
|
14
|
+
const referenced = require('../components/referenced');
|
|
14
15
|
|
|
15
16
|
function normalizeWhitespace(value) {
|
|
16
17
|
if (!value) return '';
|
|
@@ -89,6 +90,13 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
89
90
|
? mdx.parseFrontmatter(source)
|
|
90
91
|
: { data: null, content: source };
|
|
91
92
|
const frontmatterData = frontmatter && isPlainObject(frontmatter.data) ? frontmatter.data : null;
|
|
93
|
+
const referencedManifestIdsRaw = frontmatterData ? frontmatterData.referencedManifests : null;
|
|
94
|
+
const referencedManifestIds = referenced.normalizeReferencedManifestList(
|
|
95
|
+
referencedManifestIdsRaw
|
|
96
|
+
);
|
|
97
|
+
const referencedItems = referencedManifestIds.length
|
|
98
|
+
? referenced.buildReferencedItems(referencedManifestIds)
|
|
99
|
+
: [];
|
|
92
100
|
let layoutMeta = null;
|
|
93
101
|
try {
|
|
94
102
|
layoutMeta = await getNearestDirLayoutMeta(filePath);
|
|
@@ -134,11 +142,23 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
134
142
|
}
|
|
135
143
|
if (frontmatterMeta) Object.assign(pageMeta, frontmatterMeta);
|
|
136
144
|
if (Object.keys(pageMeta).length) basePage.meta = pageMeta;
|
|
145
|
+
if (referencedManifestIds.length) {
|
|
146
|
+
basePage.referencedManifests = referencedManifestIds;
|
|
147
|
+
}
|
|
148
|
+
if (referencedItems.length) {
|
|
149
|
+
basePage.referencedItems = referencedItems;
|
|
150
|
+
}
|
|
137
151
|
if (Object.keys(basePage).length) {
|
|
138
152
|
mergedProps.page = mergedProps.page
|
|
139
153
|
? { ...basePage, ...mergedProps.page }
|
|
140
154
|
: basePage;
|
|
141
155
|
}
|
|
156
|
+
if (referencedManifestIds.length) {
|
|
157
|
+
mergedProps.referencedManifests = referencedManifestIds;
|
|
158
|
+
}
|
|
159
|
+
if (referencedItems.length) {
|
|
160
|
+
mergedProps.referencedItems = referencedItems;
|
|
161
|
+
}
|
|
142
162
|
if (navData && !mergedProps.navigation) {
|
|
143
163
|
mergedProps.navigation = navData;
|
|
144
164
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const {
|
|
4
|
+
CONTENT_DIR,
|
|
5
|
+
rootRelativeHref,
|
|
6
|
+
} = require('../common');
|
|
7
|
+
const mdx = require('../build/mdx.js');
|
|
8
|
+
const {
|
|
9
|
+
firstLabelString,
|
|
10
|
+
normalizeIiifId,
|
|
11
|
+
equalIiifId,
|
|
12
|
+
readJson,
|
|
13
|
+
findSlugByIdFromDiskSync,
|
|
14
|
+
} = require('./featured');
|
|
15
|
+
|
|
16
|
+
const IIIF_INDEX_PATH = path.resolve('.cache/iiif/index.json');
|
|
17
|
+
const IIIF_MANIFESTS_DIR = path.resolve('.cache/iiif/manifests');
|
|
18
|
+
let manifestReferenceIndex = null;
|
|
19
|
+
let referenceIndexBuilt = false;
|
|
20
|
+
|
|
21
|
+
function firstTextValue(value) {
|
|
22
|
+
if (value == null) return '';
|
|
23
|
+
if (typeof value === 'string') return value.trim();
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
for (const entry of value) {
|
|
26
|
+
const text = firstTextValue(entry);
|
|
27
|
+
if (text) return text;
|
|
28
|
+
}
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'object') {
|
|
32
|
+
const keys = Object.keys(value);
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const entry = value[key];
|
|
35
|
+
if (Array.isArray(entry)) {
|
|
36
|
+
for (const child of entry) {
|
|
37
|
+
const text = firstTextValue(child);
|
|
38
|
+
if (text) return text;
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
const text = firstTextValue(entry);
|
|
42
|
+
if (text) return text;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeReferencedManifestList(raw) {
|
|
50
|
+
const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const entry of values) {
|
|
54
|
+
if (entry === undefined || entry === null) continue;
|
|
55
|
+
const normalized = normalizeIiifId(entry);
|
|
56
|
+
const key = normalized || String(entry).trim();
|
|
57
|
+
if (!key || seen.has(key)) continue;
|
|
58
|
+
seen.add(key);
|
|
59
|
+
out.push(key);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readManifestBySlug(slug) {
|
|
65
|
+
if (!slug) return null;
|
|
66
|
+
const filename = `${slug}.json`;
|
|
67
|
+
const filePath = path.join(IIIF_MANIFESTS_DIR, filename);
|
|
68
|
+
return readJson(filePath);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assignThumbnailFields(target, entry, manifest) {
|
|
72
|
+
if (!target) return target;
|
|
73
|
+
const heroThumb = entry && entry.heroThumbnail ? String(entry.heroThumbnail) : '';
|
|
74
|
+
const fallbackThumb = entry && entry.thumbnail ? String(entry.thumbnail) : '';
|
|
75
|
+
if (heroThumb) {
|
|
76
|
+
target.thumbnail = heroThumb;
|
|
77
|
+
if (typeof entry.heroThumbnailWidth === 'number') target.thumbnailWidth = entry.heroThumbnailWidth;
|
|
78
|
+
if (typeof entry.heroThumbnailHeight === 'number') target.thumbnailHeight = entry.heroThumbnailHeight;
|
|
79
|
+
} else if (fallbackThumb) {
|
|
80
|
+
target.thumbnail = fallbackThumb;
|
|
81
|
+
if (typeof entry.thumbnailWidth === 'number') target.thumbnailWidth = entry.thumbnailWidth;
|
|
82
|
+
if (typeof entry.thumbnailHeight === 'number') target.thumbnailHeight = entry.thumbnailHeight;
|
|
83
|
+
}
|
|
84
|
+
if (!target.thumbnail && manifest && manifest.thumbnail) {
|
|
85
|
+
const thumb = manifest.thumbnail;
|
|
86
|
+
if (Array.isArray(thumb) && thumb.length) {
|
|
87
|
+
const first = thumb[0] || {};
|
|
88
|
+
const id = first.id || first['@id'] || first.url || '';
|
|
89
|
+
if (id) target.thumbnail = String(id);
|
|
90
|
+
if (typeof first.width === 'number') target.thumbnailWidth = first.width;
|
|
91
|
+
if (typeof first.height === 'number') target.thumbnailHeight = first.height;
|
|
92
|
+
} else if (thumb && typeof thumb === 'object') {
|
|
93
|
+
const id = thumb.id || thumb['@id'] || thumb.url || '';
|
|
94
|
+
if (id) target.thumbnail = String(id);
|
|
95
|
+
if (typeof thumb.width === 'number') target.thumbnailWidth = thumb.width;
|
|
96
|
+
if (typeof thumb.height === 'number') target.thumbnailHeight = thumb.height;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return target;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildReferencedItems(referencedIds) {
|
|
103
|
+
const ids = normalizeReferencedManifestList(referencedIds);
|
|
104
|
+
if (!ids.length) return [];
|
|
105
|
+
const index = readJson(IIIF_INDEX_PATH) || {};
|
|
106
|
+
const byId = Array.isArray(index.byId) ? index.byId : [];
|
|
107
|
+
const items = [];
|
|
108
|
+
for (const id of ids) {
|
|
109
|
+
const entry = byId.find(
|
|
110
|
+
(candidate) => candidate && candidate.type === 'Manifest' && equalIiifId(candidate.id, id)
|
|
111
|
+
);
|
|
112
|
+
const slug = entry && entry.slug ? entry.slug : findSlugByIdFromDiskSync(id);
|
|
113
|
+
if (!slug) continue;
|
|
114
|
+
const manifest = readManifestBySlug(slug);
|
|
115
|
+
if (!manifest) continue;
|
|
116
|
+
const href = rootRelativeHref(path.join('works', `${slug}.html`).split(path.sep).join('/'));
|
|
117
|
+
const title = firstLabelString(manifest.label);
|
|
118
|
+
const summary = firstTextValue(manifest.summary);
|
|
119
|
+
const item = { id, slug, href, title };
|
|
120
|
+
if (summary) item.summary = summary;
|
|
121
|
+
assignThumbnailFields(item, entry, manifest);
|
|
122
|
+
items.push(item);
|
|
123
|
+
}
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isReservedContentFile(p) {
|
|
128
|
+
return mdx.isReservedFile ? mdx.isReservedFile(p) : path.basename(p).startsWith('_');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveHrefFromContentPath(filePath) {
|
|
132
|
+
const rel = path.relative(CONTENT_DIR, filePath).replace(/\\/g, '/');
|
|
133
|
+
const htmlRel = rel.replace(/\.mdx$/i, '.html');
|
|
134
|
+
return rootRelativeHref(htmlRel);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildReferenceIndexSync() {
|
|
138
|
+
const map = new Map();
|
|
139
|
+
function record(filePath) {
|
|
140
|
+
let raw = '';
|
|
141
|
+
try {
|
|
142
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
143
|
+
} catch (_) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const frontmatter = mdx.parseFrontmatter ? mdx.parseFrontmatter(raw) : { data: null };
|
|
147
|
+
const data = frontmatter && frontmatter.data && typeof frontmatter.data === 'object' ? frontmatter.data : null;
|
|
148
|
+
const ids = normalizeReferencedManifestList(data && data.referencedManifests);
|
|
149
|
+
if (!ids.length) return;
|
|
150
|
+
const title = (data && typeof data.title === 'string' && data.title.trim())
|
|
151
|
+
? data.title.trim()
|
|
152
|
+
: mdx.extractTitle ? mdx.extractTitle(raw) : path.basename(filePath, path.extname(filePath));
|
|
153
|
+
const href = resolveHrefFromContentPath(filePath);
|
|
154
|
+
const entry = { title, href };
|
|
155
|
+
for (const id of ids) {
|
|
156
|
+
const normalized = normalizeIiifId(id);
|
|
157
|
+
if (!normalized) continue;
|
|
158
|
+
const list = map.get(normalized) || [];
|
|
159
|
+
if (!list.some((item) => item.href === entry.href)) list.push(entry);
|
|
160
|
+
map.set(normalized, list);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function walk(dir) {
|
|
164
|
+
let entries = [];
|
|
165
|
+
try {
|
|
166
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
167
|
+
} catch (_) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const filePath = path.join(dir, entry.name);
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
walk(filePath);
|
|
174
|
+
} else if (entry.isFile() && /\.mdx$/i.test(entry.name) && !isReservedContentFile(filePath)) {
|
|
175
|
+
record(filePath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
walk(CONTENT_DIR);
|
|
180
|
+
for (const [key, list] of map.entries()) {
|
|
181
|
+
list.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
|
182
|
+
}
|
|
183
|
+
manifestReferenceIndex = map;
|
|
184
|
+
referenceIndexBuilt = true;
|
|
185
|
+
return manifestReferenceIndex;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function ensureReferenceIndex() {
|
|
189
|
+
if (referenceIndexBuilt && manifestReferenceIndex) return manifestReferenceIndex;
|
|
190
|
+
return buildReferenceIndexSync();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function resetReferenceIndex() {
|
|
194
|
+
referenceIndexBuilt = false;
|
|
195
|
+
manifestReferenceIndex = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getReferencesForManifest(manifestId) {
|
|
199
|
+
const normalized = normalizeIiifId(manifestId);
|
|
200
|
+
if (!normalized) return [];
|
|
201
|
+
const index = ensureReferenceIndex();
|
|
202
|
+
const list = index.get(normalized) || [];
|
|
203
|
+
return list.map((entry) => ({ ...entry }));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
normalizeReferencedManifestList,
|
|
208
|
+
buildReferencedItems,
|
|
209
|
+
ensureReferenceIndex,
|
|
210
|
+
resetReferenceIndex,
|
|
211
|
+
getReferencesForManifest,
|
|
212
|
+
};
|
package/package.json
CHANGED
package/ui/dist/index.mjs
CHANGED
|
@@ -19,12 +19,14 @@ function Card({
|
|
|
19
19
|
className,
|
|
20
20
|
style,
|
|
21
21
|
children,
|
|
22
|
+
lazy = true,
|
|
22
23
|
...rest
|
|
23
24
|
}) {
|
|
24
25
|
const containerRef = useRef(null);
|
|
25
|
-
const [inView, setInView] = useState(
|
|
26
|
-
const [imageLoaded, setImageLoaded] = useState(
|
|
26
|
+
const [inView, setInView] = useState(!lazy);
|
|
27
|
+
const [imageLoaded, setImageLoaded] = useState(!lazy);
|
|
27
28
|
useEffect(() => {
|
|
29
|
+
if (!lazy) return;
|
|
28
30
|
if (!containerRef.current) return;
|
|
29
31
|
if (typeof IntersectionObserver !== "function") {
|
|
30
32
|
setInView(true);
|
|
@@ -56,7 +58,7 @@ function Card({
|
|
|
56
58
|
} catch (_) {
|
|
57
59
|
}
|
|
58
60
|
};
|
|
59
|
-
}, []);
|
|
61
|
+
}, [lazy]);
|
|
60
62
|
const w = Number(imgWidth);
|
|
61
63
|
const h = Number(imgHeight);
|
|
62
64
|
const ratio = Number.isFinite(Number(aspectRatio)) && Number(aspectRatio) > 0 ? Number(aspectRatio) : Number.isFinite(w) && w > 0 && Number.isFinite(h) && h > 0 ? w / h : void 0;
|