@canopy-iiif/app 1.8.14 → 1.9.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/iiif.js +176 -102
- package/lib/build/mdx.js +39 -3
- package/lib/build/pages.js +15 -1
- package/lib/build/search-index.js +3 -2
- package/lib/build/search-workflow.js +16 -5
- package/lib/common.js +272 -15
- package/lib/components/referenced.js +7 -2
- package/lib/locales.js +172 -0
- package/lib/search/search-app.jsx +68 -7
- package/lib/search/search-form-runtime.js +46 -1
- package/lib/search/search.js +82 -16
- package/package.json +1 -1
- package/ui/dist/index.mjs +674 -285
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +668 -279
- package/ui/dist/server.mjs.map +4 -4
- package/ui/styles/components/_gallery.scss +57 -86
- package/ui/styles/components/header/_header.scss +113 -1
- package/ui/styles/index.css +141 -64
|
@@ -60,6 +60,45 @@ function rootBase() {
|
|
|
60
60
|
return getBasePath();
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function documentLocale() {
|
|
64
|
+
try {
|
|
65
|
+
if (document && document.documentElement && document.documentElement.lang) {
|
|
66
|
+
const lang = String(document.documentElement.lang).trim();
|
|
67
|
+
if (lang) return lang;
|
|
68
|
+
}
|
|
69
|
+
} catch (_) {}
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveLocaleHref(record, fallbackHref) {
|
|
74
|
+
const routes =
|
|
75
|
+
record && record.routes && typeof record.routes === 'object'
|
|
76
|
+
? record.routes
|
|
77
|
+
: null;
|
|
78
|
+
if (!routes) return fallbackHref;
|
|
79
|
+
const current = documentLocale();
|
|
80
|
+
const candidates = [];
|
|
81
|
+
if (current) {
|
|
82
|
+
candidates.push(current);
|
|
83
|
+
if (current.includes('-')) {
|
|
84
|
+
const base = current.split('-')[0];
|
|
85
|
+
if (base && base !== current) candidates.push(base);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (record && typeof record.locale === 'string') {
|
|
89
|
+
const recLocale = record.locale.trim();
|
|
90
|
+
if (recLocale && !candidates.includes(recLocale)) candidates.push(recLocale);
|
|
91
|
+
}
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
if (!candidate) continue;
|
|
94
|
+
if (routes[candidate]) return routes[candidate];
|
|
95
|
+
const lower = candidate.toLowerCase();
|
|
96
|
+
if (lower !== candidate && routes[lower]) return routes[lower];
|
|
97
|
+
}
|
|
98
|
+
const first = Object.values(routes).find((value) => value);
|
|
99
|
+
return first || fallbackHref;
|
|
100
|
+
}
|
|
101
|
+
|
|
63
102
|
function isOnSearchPage() {
|
|
64
103
|
try {
|
|
65
104
|
const base = rootBase();
|
|
@@ -163,7 +202,13 @@ async function loadRecords() {
|
|
|
163
202
|
const display = key ? displayMap.get(key) : null;
|
|
164
203
|
const merged = { ...(display || {}), ...(rec || {}) };
|
|
165
204
|
if (!merged.id && key) merged.id = key;
|
|
166
|
-
|
|
205
|
+
const fallbackHref = merged.href || (display && display.href) || '';
|
|
206
|
+
const localeHref = resolveLocaleHref(display || merged, fallbackHref);
|
|
207
|
+
if (localeHref) {
|
|
208
|
+
merged.href = withBase(localeHref);
|
|
209
|
+
} else if (fallbackHref) {
|
|
210
|
+
merged.href = withBase(fallbackHref);
|
|
211
|
+
}
|
|
167
212
|
if (!Array.isArray(merged.metadata)) {
|
|
168
213
|
const meta = Array.isArray(rec && rec.metadata) ? rec.metadata : [];
|
|
169
214
|
merged.metadata = meta;
|
package/lib/search/search.js
CHANGED
|
@@ -12,6 +12,10 @@ const {
|
|
|
12
12
|
htmlShell,
|
|
13
13
|
canopyBodyClassForType,
|
|
14
14
|
readSearchPageMetadata,
|
|
15
|
+
resolveLocaleFromHref,
|
|
16
|
+
getLocaleRouteEntries,
|
|
17
|
+
getDefaultRoute,
|
|
18
|
+
getDefaultLocaleCode,
|
|
15
19
|
} = require('../common');
|
|
16
20
|
const { resolveCanopyConfigPath } = require('../config-path');
|
|
17
21
|
|
|
@@ -91,6 +95,29 @@ function createSearchMdxPlugin() {
|
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
function getSearchRouteEntries() {
|
|
99
|
+
let entries = getLocaleRouteEntries('search');
|
|
100
|
+
if (!entries.length) {
|
|
101
|
+
entries = [
|
|
102
|
+
{
|
|
103
|
+
locale: getDefaultLocaleCode(),
|
|
104
|
+
route: getDefaultRoute('search'),
|
|
105
|
+
isDefault: true,
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
return entries;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveSearchOutputRelative(routeValue) {
|
|
113
|
+
const defaultRoute = getDefaultRoute('search') || 'search';
|
|
114
|
+
const trimmed = typeof routeValue === 'string' ? routeValue.trim().replace(/^\/+|\/+$/g, '') : '';
|
|
115
|
+
if (!trimmed || trimmed === defaultRoute) {
|
|
116
|
+
return `${trimmed || defaultRoute}.html`;
|
|
117
|
+
}
|
|
118
|
+
return path.join(trimmed, 'index.html');
|
|
119
|
+
}
|
|
120
|
+
|
|
94
121
|
async function ensureSearchRuntime() {
|
|
95
122
|
ensureDirSync(OUT_DIR);
|
|
96
123
|
let esbuild = null;
|
|
@@ -248,18 +275,32 @@ async function ensureSearchRuntime() {
|
|
|
248
275
|
}
|
|
249
276
|
|
|
250
277
|
async function buildSearchPage() {
|
|
278
|
+
for (const entry of getSearchRouteEntries()) {
|
|
279
|
+
await buildSearchPageForEntry(entry);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function buildSearchPageForEntry(routeEntry) {
|
|
251
284
|
try {
|
|
252
|
-
const
|
|
285
|
+
const defaultRoute = getDefaultRoute('search') || 'search';
|
|
286
|
+
const routeBase =
|
|
287
|
+
routeEntry && typeof routeEntry.route === 'string'
|
|
288
|
+
? routeEntry.route
|
|
289
|
+
: defaultRoute;
|
|
290
|
+
const relativeOutput = resolveSearchOutputRelative(routeBase);
|
|
291
|
+
const outPath = path.join(OUT_DIR, relativeOutput);
|
|
253
292
|
ensureDirSync(path.dirname(outPath));
|
|
254
|
-
// Require author-provided content/search/_layout.mdx; do not fall back to a generated page.
|
|
255
293
|
const searchLayoutPath = path.join(path.resolve('content'), 'search', '_layout.mdx');
|
|
256
|
-
let body = '';
|
|
257
|
-
let head = '';
|
|
258
294
|
if (!require('../common').fs.existsSync(searchLayoutPath)) {
|
|
259
295
|
throw new Error('Missing required file: content/search/_layout.mdx');
|
|
260
296
|
}
|
|
261
297
|
const mdx = require('../build/mdx');
|
|
262
|
-
const
|
|
298
|
+
const normalizedRoute = routeBase ? routeBase.replace(/^\/+|\/+$/g, '') : '';
|
|
299
|
+
const fileHref = rootRelativeHref(relativeOutput.split(path.sep).join('/'));
|
|
300
|
+
const prettyHref =
|
|
301
|
+
normalizedRoute && normalizedRoute !== defaultRoute
|
|
302
|
+
? rootRelativeHref(`${normalizedRoute}/`)
|
|
303
|
+
: fileHref;
|
|
263
304
|
const searchPageMeta = readSearchPageMetadata() || {};
|
|
264
305
|
const pageTitle =
|
|
265
306
|
typeof searchPageMeta.title === 'string' && searchPageMeta.title.trim()
|
|
@@ -272,23 +313,30 @@ async function buildSearchPage() {
|
|
|
272
313
|
const pageDetails = {
|
|
273
314
|
title: pageTitle,
|
|
274
315
|
description: pageDescription,
|
|
275
|
-
href:
|
|
276
|
-
url:
|
|
316
|
+
href: prettyHref,
|
|
317
|
+
url: prettyHref,
|
|
277
318
|
type: 'search',
|
|
278
|
-
canonical:
|
|
319
|
+
canonical: prettyHref,
|
|
279
320
|
meta: {
|
|
280
321
|
title: pageTitle,
|
|
281
322
|
description: pageDescription,
|
|
282
323
|
type: 'search',
|
|
283
|
-
url:
|
|
284
|
-
canonical:
|
|
324
|
+
url: prettyHref,
|
|
325
|
+
canonical: prettyHref,
|
|
285
326
|
},
|
|
286
327
|
};
|
|
328
|
+
const fallbackLocale = resolveLocaleFromHref(prettyHref);
|
|
329
|
+
const pageLocale =
|
|
330
|
+
(routeEntry && routeEntry.locale) ||
|
|
331
|
+
fallbackLocale ||
|
|
332
|
+
getDefaultLocaleCode();
|
|
333
|
+
pageDetails.locale = pageLocale;
|
|
334
|
+
if (pageDetails.meta) pageDetails.meta.locale = pageLocale;
|
|
287
335
|
const rendered = await mdx.compileMdxFile(searchLayoutPath, outPath, null, {
|
|
288
336
|
page: pageDetails,
|
|
289
337
|
});
|
|
290
|
-
body = rendered && rendered.body ? rendered.body : '';
|
|
291
|
-
head = rendered && rendered.head ? rendered.head : '';
|
|
338
|
+
const body = rendered && rendered.body ? rendered.body : '';
|
|
339
|
+
const head = rendered && rendered.head ? rendered.head : '';
|
|
292
340
|
if (!body) throw new Error('Search: content/search/_layout.mdx produced empty output');
|
|
293
341
|
const importMap = '';
|
|
294
342
|
const jsAbs = path.join(OUT_DIR, 'scripts', 'search.js');
|
|
@@ -296,7 +344,6 @@ async function buildSearchPage() {
|
|
|
296
344
|
let v = '';
|
|
297
345
|
try { const st = require('fs').statSync(jsAbs); v = `?v=${Math.floor(st.mtimeMs || Date.now())}`; } catch (_) {}
|
|
298
346
|
jsRel = jsRel + v;
|
|
299
|
-
// Include react-globals vendor shim before search.js to provide window.React globals
|
|
300
347
|
const vendorReactAbs = path.join(OUT_DIR, 'scripts', 'react-globals.js');
|
|
301
348
|
const vendorFlexAbs = path.join(OUT_DIR, 'scripts', 'flexsearch-globals.js');
|
|
302
349
|
const vendorSearchFormAbs = path.join(OUT_DIR, 'scripts', 'canopy-search-form.js');
|
|
@@ -326,7 +373,7 @@ async function buildSearchPage() {
|
|
|
326
373
|
}
|
|
327
374
|
} catch (_) {}
|
|
328
375
|
const bodyClass = canopyBodyClassForType('search');
|
|
329
|
-
let html = htmlShell({ title: pageTitle, body, cssHref: null, scriptHref: jsRel, headExtra, bodyClass });
|
|
376
|
+
let html = htmlShell({ title: pageTitle, body, cssHref: null, scriptHref: jsRel, headExtra, bodyClass, lang: pageLocale });
|
|
330
377
|
try { html = require('../common').applyBaseToHtml(html); } catch (_) {}
|
|
331
378
|
await fsp.writeFile(outPath, html, 'utf8');
|
|
332
379
|
console.log('Search: Built', path.relative(process.cwd(), outPath));
|
|
@@ -395,6 +442,17 @@ function sanitizeRecordForDisplay(r) {
|
|
|
395
442
|
const out = { ...base };
|
|
396
443
|
if (out.metadata) delete out.metadata;
|
|
397
444
|
if (out.summary) out.summary = toSafeString(out.summary, '');
|
|
445
|
+
const locale = toSafeString(r && r.locale, '').trim();
|
|
446
|
+
if (locale) out.locale = locale;
|
|
447
|
+
if (r && r.routes && typeof r.routes === 'object') {
|
|
448
|
+
const normalizedRoutes = {};
|
|
449
|
+
Object.keys(r.routes).forEach((key) => {
|
|
450
|
+
const routeHref = toSafeString(r.routes[key], '');
|
|
451
|
+
if (!routeHref) return;
|
|
452
|
+
normalizedRoutes[key] = rootRelativeHref(routeHref);
|
|
453
|
+
});
|
|
454
|
+
if (Object.keys(normalizedRoutes).length) out.routes = normalizedRoutes;
|
|
455
|
+
}
|
|
398
456
|
const summaryMarkdown = toSafeString(
|
|
399
457
|
(r && r.summaryMarkdown) ||
|
|
400
458
|
(r && r.searchSummaryMarkdown) ||
|
|
@@ -405,7 +463,9 @@ function sanitizeRecordForDisplay(r) {
|
|
|
405
463
|
out.summaryMarkdown = summaryMarkdown;
|
|
406
464
|
}
|
|
407
465
|
const hrefRaw = toSafeString(r && r.href, '');
|
|
408
|
-
|
|
466
|
+
if (hrefRaw) {
|
|
467
|
+
out.href = rootRelativeHref(hrefRaw);
|
|
468
|
+
}
|
|
409
469
|
const thumbnail = toSafeString(r && r.thumbnail, '');
|
|
410
470
|
if (thumbnail) out.thumbnail = thumbnail;
|
|
411
471
|
// Preserve optional thumbnail dimensions for aspect ratio calculations in the UI
|
|
@@ -603,4 +663,10 @@ async function ensureResultTemplate() {
|
|
|
603
663
|
} catch (_) {}
|
|
604
664
|
}
|
|
605
665
|
|
|
606
|
-
module.exports = {
|
|
666
|
+
module.exports = {
|
|
667
|
+
ensureSearchRuntime,
|
|
668
|
+
ensureResultTemplate,
|
|
669
|
+
buildSearchPage,
|
|
670
|
+
writeSearchIndex,
|
|
671
|
+
resolveSearchOutputRelative,
|
|
672
|
+
};
|