@canopy-iiif/app 0.6.28

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/common.js ADDED
@@ -0,0 +1,124 @@
1
+ const fs = require('fs');
2
+ const fsp = fs.promises;
3
+ const path = require('path');
4
+
5
+ const CONTENT_DIR = path.resolve('content');
6
+ const OUT_DIR = path.resolve('site');
7
+ const CACHE_DIR = path.resolve('.cache/mdx');
8
+ const ASSETS_DIR = path.resolve('assets');
9
+
10
+ const BASE_PATH = String(process.env.CANOPY_BASE_PATH || '').replace(/\/$/, '');
11
+
12
+ function readYamlConfigBaseUrl() {
13
+ try {
14
+ const y = require('js-yaml');
15
+ const p = path.resolve(process.env.CANOPY_CONFIG || 'canopy.yml');
16
+ if (!fs.existsSync(p)) return '';
17
+ const raw = fs.readFileSync(p, 'utf8');
18
+ const data = y.load(raw) || {};
19
+ const site = data && data.site;
20
+ const url = site && site.baseUrl ? String(site.baseUrl) : '';
21
+ return url;
22
+ } catch (_) { return ''; }
23
+ }
24
+
25
+ // Determine the absolute site origin (scheme + host[:port])
26
+ // Priority:
27
+ // 1) CANOPY_BASE_URL env
28
+ // 2) canopy.yml → site.baseUrl
29
+ // 3) dev server default http://localhost:PORT (PORT env or 3000)
30
+ const BASE_ORIGIN = (() => {
31
+ const env = String(process.env.CANOPY_BASE_URL || '').trim();
32
+ if (env) return env.replace(/\/$/, '');
33
+ const cfg = readYamlConfigBaseUrl();
34
+ if (cfg) return cfg.replace(/\/$/, '');
35
+ const port = Number(process.env.PORT || 3000);
36
+ return `http://localhost:${port}`;
37
+ })();
38
+
39
+ function ensureDirSync(dir) {
40
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
41
+ }
42
+
43
+ async function cleanDir(dir) {
44
+ if (fs.existsSync(dir)) {
45
+ await fsp.rm(dir, { recursive: true, force: true });
46
+ }
47
+ await fsp.mkdir(dir, { recursive: true });
48
+ }
49
+
50
+ function htmlShell({ title, body, cssHref, scriptHref, headExtra }) {
51
+ const scriptTag = scriptHref ? `<script defer src="${scriptHref}"></script>` : '';
52
+ const extra = headExtra ? String(headExtra) : '';
53
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>${title}</title>${extra}<link rel="stylesheet" href="${cssHref}">${scriptTag}</head><body>${body}</body></html>`;
54
+ }
55
+
56
+ function withBase(href) {
57
+ if (!href) return href;
58
+ if (!BASE_PATH) return href;
59
+ if (typeof href === 'string' && href.startsWith('/')) return `${BASE_PATH}${href}`;
60
+ return href;
61
+ }
62
+
63
+ // Convert a site-relative path (e.g., "/api/foo.json") to an absolute URL.
64
+ // Handles either:
65
+ // - BASE_ORIGIN that may already include a path prefix (e.g., https://host/org/repo)
66
+ // - BASE_PATH (path prefix) when BASE_ORIGIN has no path
67
+ // If input is already absolute (http/https), returns as-is.
68
+ function absoluteUrl(p) {
69
+ try {
70
+ const raw = String(p || '');
71
+ if (/^https?:\/\//i.test(raw)) return raw;
72
+ const rel = raw.startsWith('/') ? raw : '/' + raw.replace(/^\/?/, '');
73
+ // Parse BASE_ORIGIN; it may include a path (e.g., GH Pages repo path)
74
+ let originBase = '';
75
+ let originPath = '';
76
+ try {
77
+ const u = new URL(BASE_ORIGIN);
78
+ originBase = u.origin.replace(/\/$/, '');
79
+ originPath = (u.pathname || '').replace(/\/$/, '');
80
+ } catch (_) {
81
+ originBase = String(BASE_ORIGIN || '').replace(/\/$/, '');
82
+ originPath = '';
83
+ }
84
+ // Prefer path from BASE_ORIGIN; if absent, fall back to BASE_PATH
85
+ let prefixPath = originPath || String(BASE_PATH || '');
86
+ prefixPath = prefixPath.replace(/\/$/, '');
87
+ const fullPath = (prefixPath ? prefixPath : '') + rel; // rel already has leading '/'
88
+ return originBase + fullPath;
89
+ } catch (_) {
90
+ return p;
91
+ }
92
+ }
93
+
94
+ // Apply BASE_PATH to any absolute href/src attributes found in an HTML string.
95
+ function applyBaseToHtml(html) {
96
+ if (!BASE_PATH) return html;
97
+ try {
98
+ let out = String(html || '');
99
+ // Avoid protocol-relative (//example.com) by using a negative lookahead
100
+ out = out.replace(/(href|src)=(\")\/(?!\/)/g, `$1=$2${BASE_PATH}/`);
101
+ out = out.replace(/(href|src)=(\')\/(?!\/)/g, `$1=$2${BASE_PATH}/`);
102
+ return out;
103
+ } catch (_) {
104
+ return html;
105
+ }
106
+ }
107
+
108
+ module.exports = {
109
+ fs,
110
+ fsp,
111
+ path,
112
+ CONTENT_DIR,
113
+ OUT_DIR,
114
+ CACHE_DIR,
115
+ ASSETS_DIR,
116
+ BASE_PATH,
117
+ ensureDirSync,
118
+ cleanDir,
119
+ htmlShell,
120
+ withBase,
121
+ BASE_ORIGIN,
122
+ absoluteUrl,
123
+ applyBaseToHtml,
124
+ };
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const React = require('react');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ // Cache of index/manifests to avoid repeated disk reads during a build
8
+ let CACHE = { loaded: false, byId: new Map(), bySlug: new Map() };
9
+
10
+ function safeReadJson(p) {
11
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
12
+ }
13
+
14
+ function loadIndexOnce() {
15
+ if (CACHE.loaded) return CACHE;
16
+ const idxPath = path.resolve('.cache/iiif/index.json');
17
+ const idx = safeReadJson(idxPath);
18
+ if (idx && Array.isArray(idx.byId)) {
19
+ for (const e of idx.byId) {
20
+ if (!e || e.type !== 'Manifest') continue;
21
+ const id = String(e.id || '');
22
+ const slug = String(e.slug || '');
23
+ const entry = { id, slug, parent: e.parent || '', thumbnail: e.thumbnail || '' };
24
+ if (id) CACHE.byId.set(id, entry);
25
+ if (slug) CACHE.bySlug.set(slug, entry);
26
+ }
27
+ }
28
+ CACHE.loaded = true;
29
+ return CACHE;
30
+ }
31
+
32
+ function readManifestTitle(slug) {
33
+ try {
34
+ const p = path.resolve('.cache/iiif/manifests', `${slug}.json`);
35
+ const m = safeReadJson(p);
36
+ const label = m && m.label;
37
+ if (!label) return null;
38
+ if (typeof label === 'string') return label;
39
+ const keys = Object.keys(label || {});
40
+ if (!keys.length) return null;
41
+ const arr = label[keys[0]];
42
+ if (Array.isArray(arr) && arr.length) return String(arr[0]);
43
+ return null;
44
+ } catch (_) { return null; }
45
+ }
46
+
47
+ function deriveTitleFromSlug(slug) {
48
+ try {
49
+ const s = decodeURIComponent(String(slug || ''));
50
+ return s.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim() || s;
51
+ } catch (_) { return String(slug || ''); }
52
+ }
53
+
54
+ function IIIFCard(props) {
55
+ const { id, slug, href, src, title, subtitle, alt, className, style, ...rest } = props || {};
56
+ const { byId, bySlug } = loadIndexOnce();
57
+ let entry = null;
58
+ if (id && byId.has(String(id))) entry = byId.get(String(id));
59
+ else if (slug && bySlug.has(String(slug))) entry = bySlug.get(String(slug));
60
+
61
+ const resolvedSlug = (entry && entry.slug) || (slug ? String(slug) : '');
62
+ const resolvedHref = href || (resolvedSlug ? `/works/${resolvedSlug}.html` : '#');
63
+ const resolvedSrc = src || (entry && entry.thumbnail) || '';
64
+ const resolvedTitle = title || readManifestTitle(resolvedSlug) || deriveTitleFromSlug(resolvedSlug);
65
+ const resolvedAlt = alt || resolvedTitle || '';
66
+
67
+ let Card = null;
68
+ try {
69
+ // Load the UI Card component for consistent markup/styles
70
+ const ui = require('@canopy-iiif/app/ui');
71
+ Card = ui && (ui.Card || ui.default && ui.default.Card) ? (ui.Card || ui.default.Card) : null;
72
+ } catch (_) { Card = null; }
73
+
74
+ if (Card) {
75
+ return React.createElement(Card, {
76
+ href: resolvedHref,
77
+ src: resolvedSrc || undefined,
78
+ alt: resolvedAlt,
79
+ title: resolvedTitle,
80
+ subtitle,
81
+ className,
82
+ style,
83
+ ...rest,
84
+ });
85
+ }
86
+ // Fallback minimal markup if UI is unavailable
87
+ return React.createElement(
88
+ 'a',
89
+ { href: resolvedHref, className, style, ...rest },
90
+ React.createElement(
91
+ 'figure',
92
+ { style: { margin: 0 } },
93
+ resolvedSrc ? React.createElement('img', { src: resolvedSrc, alt: resolvedAlt, loading: 'lazy', style: { display: 'block', width: '100%', height: 'auto', borderRadius: 4 } }) : null,
94
+ React.createElement('figcaption', { style: { marginTop: 8 } },
95
+ resolvedTitle ? React.createElement('strong', { style: { display: 'block' } }, resolvedTitle) : null,
96
+ subtitle ? React.createElement('span', { style: { display: 'block', color: '#6b7280' } }, subtitle) : null,
97
+ )
98
+ )
99
+ );
100
+ }
101
+
102
+ module.exports = IIIFCard;