@canopy-iiif/app 1.4.6 → 1.4.8

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.
@@ -23,6 +23,7 @@ const { copyAssets } = require("./assets");
23
23
  const { logLine } = require("./log");
24
24
  const navigation = require("../components/navigation");
25
25
  const referenced = require("../components/referenced");
26
+ const bibliography = require("../components/bibliography");
26
27
 
27
28
  // hold records between builds if skipping IIIF
28
29
  let iiifRecordsCache = [];
@@ -54,6 +55,7 @@ async function build(options = {}) {
54
55
  mdx?.resetMdxCaches();
55
56
  navigation?.resetNavigationCache?.();
56
57
  referenced?.resetReferenceIndex?.();
58
+ bibliography?.resetBibliographyIndex?.();
57
59
  if (!skipIiif) {
58
60
  await cleanDir(OUT_DIR);
59
61
  await ensureNoJekyllMarker();
@@ -104,6 +106,15 @@ async function build(options = {}) {
104
106
  underscore: true,
105
107
  });
106
108
  // Interstitials read directly from the local IIIF cache; no API file needed
109
+ try {
110
+ bibliography?.buildBibliographyIndexSync?.();
111
+ } catch (err) {
112
+ logLine(
113
+ "• Failed to build bibliography index: " + String(err && err.message ? err.message : err),
114
+ "red",
115
+ { dim: true }
116
+ );
117
+ }
107
118
  pageRecords = await searchBuild.collectMdxPageRecords();
108
119
  await pages.buildContentTree(CONTENT_DIR, pageRecords);
109
120
  logLine("✓ MDX pages built", "green");
@@ -0,0 +1,197 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const {
4
+ CONTENT_DIR,
5
+ rootRelativeHref,
6
+ ensureDirSync,
7
+ } = require('../common');
8
+ const mdx = require('../build/mdx.js');
9
+ const {fromMarkdown} = require('mdast-util-from-markdown');
10
+ const {gfmFootnote} = require('micromark-extension-gfm-footnote');
11
+ const {gfmFootnoteFromMarkdown} = require('mdast-util-gfm-footnote');
12
+ const {toString} = require('mdast-util-to-string');
13
+ const {toHast} = require('mdast-util-to-hast');
14
+ const {toHtml} = require('hast-util-to-html');
15
+
16
+ const CACHE_FILE = path.resolve('.cache', 'bibliography.json');
17
+
18
+ let bibliographyEntries = null;
19
+ let bibliographyBuilt = false;
20
+
21
+ function isReservedContentFile(filePath) {
22
+ if (mdx && typeof mdx.isReservedFile === 'function') {
23
+ return mdx.isReservedFile(filePath);
24
+ }
25
+ const base = path.basename(filePath);
26
+ return base.startsWith('_');
27
+ }
28
+
29
+ function normalizeHtmlFromChildren(children) {
30
+ if (!Array.isArray(children) || !children.length) return '';
31
+ const hast = toHast(
32
+ {
33
+ type: 'root',
34
+ children,
35
+ },
36
+ {allowDangerousHtml: true}
37
+ );
38
+ try {
39
+ return toHtml(hast, {allowDangerousHtml: true});
40
+ } catch (_) {
41
+ return '';
42
+ }
43
+ }
44
+
45
+ function collectFootnotesFromSource(source) {
46
+ if (!source) return [];
47
+ const parsed = mdx.parseFrontmatter ? mdx.parseFrontmatter(source) : {content: source};
48
+ const content = parsed && typeof parsed.content === 'string' ? parsed.content : '';
49
+ if (!content.trim()) return [];
50
+ const tree = fromMarkdown(content, {
51
+ extensions: [gfmFootnote()],
52
+ mdastExtensions: [gfmFootnoteFromMarkdown()],
53
+ });
54
+ const results = [];
55
+ const visit = (node) => {
56
+ if (!node || typeof node !== 'object') return;
57
+ if (node.type === 'footnoteDefinition') {
58
+ const identifier = node.identifier || node.label || String(results.length + 1);
59
+ const text = toString(node).trim();
60
+ const html = normalizeHtmlFromChildren(node.children || []);
61
+ if (text || html) {
62
+ results.push({
63
+ identifier: String(identifier || results.length + 1),
64
+ text,
65
+ html,
66
+ });
67
+ }
68
+ return;
69
+ }
70
+ if (Array.isArray(node.children)) {
71
+ node.children.forEach(visit);
72
+ }
73
+ };
74
+ visit(tree);
75
+ return results;
76
+ }
77
+
78
+ function readPageFootnotes(filePath) {
79
+ let raw = '';
80
+ try {
81
+ raw = fs.readFileSync(filePath, 'utf8');
82
+ } catch (_) {
83
+ return null;
84
+ }
85
+ const footnotes = collectFootnotesFromSource(raw);
86
+ if (!footnotes.length) return null;
87
+ const frontmatter = mdx.parseFrontmatter ? mdx.parseFrontmatter(raw) : {data: null};
88
+ const data = frontmatter && frontmatter.data ? frontmatter.data : null;
89
+ let title = '';
90
+ if (data && typeof data.title === 'string' && data.title.trim()) {
91
+ title = data.title.trim();
92
+ } else if (typeof mdx.extractTitle === 'function') {
93
+ title = mdx.extractTitle(raw);
94
+ }
95
+ const rel = path.relative(CONTENT_DIR, filePath).replace(/\\/g, '/');
96
+ const href = rootRelativeHref(rel.replace(/\.mdx$/i, '.html'));
97
+ return {
98
+ title,
99
+ href,
100
+ relativePath: rel,
101
+ footnotes,
102
+ };
103
+ }
104
+
105
+ function sortBibliography(entries) {
106
+ return entries.sort((a, b) => {
107
+ const titleA = (a && a.title) || '';
108
+ const titleB = (b && b.title) || '';
109
+ if (titleA && titleB) {
110
+ const cmp = titleA.localeCompare(titleB);
111
+ if (cmp !== 0) return cmp;
112
+ }
113
+ const hrefA = (a && a.href) || '';
114
+ const hrefB = (b && b.href) || '';
115
+ return hrefA.localeCompare(hrefB);
116
+ });
117
+ }
118
+
119
+ function walkContentDir(dir, entries) {
120
+ let dirents = [];
121
+ try {
122
+ dirents = fs.readdirSync(dir, {withFileTypes: true});
123
+ } catch (_) {
124
+ return;
125
+ }
126
+ for (const dirent of dirents) {
127
+ if (!dirent) continue;
128
+ const name = dirent.name || '';
129
+ if (!name || name.startsWith('.')) continue;
130
+ const absPath = path.join(dir, name);
131
+ if (dirent.isDirectory()) {
132
+ if (name.startsWith('_')) continue;
133
+ walkContentDir(absPath, entries);
134
+ continue;
135
+ }
136
+ if (!dirent.isFile() || !/\.mdx$/i.test(name)) continue;
137
+ if (isReservedContentFile(absPath)) continue;
138
+ const record = readPageFootnotes(absPath);
139
+ if (record) entries.push(record);
140
+ }
141
+ }
142
+
143
+ function writeCache(entries) {
144
+ try {
145
+ ensureDirSync(path.dirname(CACHE_FILE));
146
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(entries, null, 2), 'utf8');
147
+ } catch (_) {}
148
+ }
149
+
150
+ function readCache() {
151
+ try {
152
+ if (!fs.existsSync(CACHE_FILE)) return null;
153
+ const raw = fs.readFileSync(CACHE_FILE, 'utf8');
154
+ const parsed = JSON.parse(raw);
155
+ if (Array.isArray(parsed)) return parsed;
156
+ } catch (_) {}
157
+ return null;
158
+ }
159
+
160
+ function buildBibliographyIndexSync() {
161
+ if (!fs.existsSync(CONTENT_DIR)) {
162
+ bibliographyEntries = [];
163
+ bibliographyBuilt = true;
164
+ return bibliographyEntries;
165
+ }
166
+ const entries = [];
167
+ walkContentDir(CONTENT_DIR, entries);
168
+ const sorted = sortBibliography(entries);
169
+ bibliographyEntries = sorted;
170
+ bibliographyBuilt = true;
171
+ writeCache(sorted);
172
+ return bibliographyEntries;
173
+ }
174
+
175
+ function getBibliographyEntries() {
176
+ if (bibliographyBuilt && Array.isArray(bibliographyEntries)) {
177
+ return bibliographyEntries;
178
+ }
179
+ const cached = readCache();
180
+ if (cached) {
181
+ bibliographyEntries = cached;
182
+ bibliographyBuilt = true;
183
+ return bibliographyEntries;
184
+ }
185
+ return buildBibliographyIndexSync();
186
+ }
187
+
188
+ function resetBibliographyIndex() {
189
+ bibliographyEntries = null;
190
+ bibliographyBuilt = false;
191
+ }
192
+
193
+ module.exports = {
194
+ getBibliographyEntries,
195
+ buildBibliographyIndexSync,
196
+ resetBibliographyIndex,
197
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
@@ -65,6 +65,12 @@
65
65
  "js-yaml": "^4.1.0",
66
66
  "leaflet": "^1.9.4",
67
67
  "leaflet.markercluster": "^1.5.3",
68
+ "mdast-util-from-markdown": "^2.0.2",
69
+ "mdast-util-gfm-footnote": "^2.1.0",
70
+ "mdast-util-to-hast": "^13.2.0",
71
+ "mdast-util-to-string": "^4.0.0",
72
+ "micromark-extension-gfm-footnote": "^2.1.0",
73
+ "hast-util-to-html": "^9.0.5",
68
74
  "react-masonry-css": "^1.0.16",
69
75
  "remark-gfm": "^4.0.0",
70
76
  "sass": "^1.77.0",