@canopy-iiif/app 0.8.3 → 0.8.5
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/dev.js +38 -22
- package/lib/build/iiif.js +359 -83
- package/lib/build/mdx.js +12 -2
- package/lib/build/pages.js +15 -1
- package/lib/build/styles.js +53 -1
- package/lib/common.js +28 -6
- package/lib/components/navigation.js +308 -0
- package/lib/page-context.js +14 -0
- package/lib/search/search-app.jsx +177 -25
- package/lib/search/search-form-runtime.js +126 -19
- package/lib/search/search.js +130 -18
- package/package.json +4 -1
- package/ui/dist/index.mjs +204 -101
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +167 -59
- package/ui/dist/server.mjs.map +4 -4
- package/ui/styles/_variables.scss +1 -0
- package/ui/styles/base/_common.scss +27 -5
- package/ui/styles/base/_heading.scss +2 -4
- package/ui/styles/base/index.scss +1 -0
- package/ui/styles/components/_card.scss +47 -4
- package/ui/styles/components/_sub-navigation.scss +76 -0
- package/ui/styles/components/header/_header.scss +1 -4
- package/ui/styles/components/header/_logo.scss +33 -10
- package/ui/styles/components/index.scss +1 -0
- package/ui/styles/components/search/_filters.scss +5 -7
- package/ui/styles/components/search/_form.scss +55 -17
- package/ui/styles/components/search/_results.scss +49 -14
- package/ui/styles/index.css +344 -56
- package/ui/styles/index.scss +2 -4
- package/ui/tailwind-canopy-iiif-plugin.js +10 -2
- package/ui/tailwind-canopy-iiif-preset.js +21 -19
- package/ui/theme.js +303 -0
- package/ui/styles/variables.emit.scss +0 -72
- package/ui/styles/variables.scss +0 -76
package/lib/build/mdx.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
withBase,
|
|
13
13
|
} = require("../common");
|
|
14
14
|
const yaml = require("js-yaml");
|
|
15
|
+
const { getPageContext } = require("../page-context");
|
|
15
16
|
|
|
16
17
|
function parseFrontmatter(src) {
|
|
17
18
|
let input = String(src || "");
|
|
@@ -280,10 +281,19 @@ async function compileMdxFile(filePath, outPath, Layout, extraProps = {}) {
|
|
|
280
281
|
const app = await loadAppWrapper();
|
|
281
282
|
const dirLayout = await getNearestDirLayout(filePath);
|
|
282
283
|
const contentNode = React.createElement(MDXContent, extraProps);
|
|
284
|
+
const layoutProps = dirLayout ? { ...extraProps } : null;
|
|
283
285
|
const withLayout = dirLayout
|
|
284
|
-
? React.createElement(dirLayout,
|
|
286
|
+
? React.createElement(dirLayout, layoutProps, contentNode)
|
|
285
287
|
: contentNode;
|
|
286
|
-
const
|
|
288
|
+
const PageContext = getPageContext();
|
|
289
|
+
const contextValue = {
|
|
290
|
+
navigation: extraProps && extraProps.navigation ? extraProps.navigation : null,
|
|
291
|
+
page: extraProps && extraProps.page ? extraProps.page : null,
|
|
292
|
+
};
|
|
293
|
+
const withContext = PageContext
|
|
294
|
+
? React.createElement(PageContext.Provider, { value: contextValue }, withLayout)
|
|
295
|
+
: withLayout;
|
|
296
|
+
const withApp = React.createElement(app.App, null, withContext);
|
|
287
297
|
const compMap = { ...components, a: Anchor };
|
|
288
298
|
const page = MDXProvider
|
|
289
299
|
? React.createElement(MDXProvider, { components: compMap }, withApp)
|
package/lib/build/pages.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { fs, fsp, path, CONTENT_DIR, OUT_DIR, ensureDirSync, htmlShell } = require('../common');
|
|
2
2
|
const { log } = require('./log');
|
|
3
3
|
const mdx = require('./mdx');
|
|
4
|
+
const navigation = require('../components/navigation');
|
|
4
5
|
|
|
5
6
|
// Cache: dir -> frontmatter data for _layout.mdx in that dir
|
|
6
7
|
const LAYOUT_META = new Map();
|
|
@@ -44,7 +45,20 @@ function mapContentPathToOutput(filePath) {
|
|
|
44
45
|
async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
45
46
|
const source = await fsp.readFile(filePath, 'utf8');
|
|
46
47
|
const title = mdx.extractTitle(source);
|
|
47
|
-
const
|
|
48
|
+
const relContentPath = path.relative(CONTENT_DIR, filePath);
|
|
49
|
+
const normalizedRel = navigation.normalizeRelativePath(relContentPath);
|
|
50
|
+
const pageInfo = navigation.getPageInfo(normalizedRel);
|
|
51
|
+
const navData = navigation.buildNavigationForFile(normalizedRel);
|
|
52
|
+
const mergedProps = { ...(extraProps || {}) };
|
|
53
|
+
if (pageInfo) {
|
|
54
|
+
mergedProps.page = mergedProps.page
|
|
55
|
+
? { ...pageInfo, ...mergedProps.page }
|
|
56
|
+
: pageInfo;
|
|
57
|
+
}
|
|
58
|
+
if (navData && !mergedProps.navigation) {
|
|
59
|
+
mergedProps.navigation = navData;
|
|
60
|
+
}
|
|
61
|
+
const { body, head } = await mdx.compileMdxFile(filePath, outPath, null, mergedProps);
|
|
48
62
|
const needsHydrateViewer = body.includes('data-canopy-viewer');
|
|
49
63
|
const needsHydrateSlider = body.includes('data-canopy-slider');
|
|
50
64
|
const needsSearchForm = true; // search form runtime is global
|
package/lib/build/styles.js
CHANGED
|
@@ -97,6 +97,52 @@ async function ensureStyles() {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function injectThemeTokens(targetPath) {
|
|
101
|
+
try {
|
|
102
|
+
const { loadCanopyTheme } = require("@canopy-iiif/app/ui/theme");
|
|
103
|
+
const theme = loadCanopyTheme();
|
|
104
|
+
const themeCss = theme && theme.css ? theme.css.trim() : "";
|
|
105
|
+
if (!themeCss) return;
|
|
106
|
+
|
|
107
|
+
let existing = "";
|
|
108
|
+
try {
|
|
109
|
+
existing = fs.readFileSync(targetPath, "utf8");
|
|
110
|
+
} catch (_) {}
|
|
111
|
+
|
|
112
|
+
const marker = "/* canopy-theme */";
|
|
113
|
+
const markerEnd = "/* canopy-theme:end */";
|
|
114
|
+
const markerRegex = new RegExp(`${marker}[\\s\\S]*?${markerEnd}\\n?`, "g");
|
|
115
|
+
const sanitized = existing.replace(markerRegex, "");
|
|
116
|
+
|
|
117
|
+
const layerRegex = /@layer properties\{([\s\S]*?)\}(?=@|$)/;
|
|
118
|
+
const match = layerRegex.exec(sanitized);
|
|
119
|
+
let before = sanitized;
|
|
120
|
+
let after = "";
|
|
121
|
+
let customRulesBlock = "";
|
|
122
|
+
|
|
123
|
+
if (match) {
|
|
124
|
+
before = sanitized.slice(0, match.index);
|
|
125
|
+
after = sanitized.slice(match.index + match[0].length);
|
|
126
|
+
const layerBody = match[1] || "";
|
|
127
|
+
const boundaryMatch = /}\s*:/.exec(layerBody);
|
|
128
|
+
if (boundaryMatch) {
|
|
129
|
+
const start = boundaryMatch.index + boundaryMatch[0].length - 1;
|
|
130
|
+
const customSegment = layerBody.slice(start).trim();
|
|
131
|
+
if (customSegment) {
|
|
132
|
+
const normalized = customSegment.endsWith("}")
|
|
133
|
+
? customSegment
|
|
134
|
+
: `${customSegment}}`;
|
|
135
|
+
customRulesBlock = `@layer properties {\n ${normalized}\n}\n`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const themeBlock = `${marker}\n${themeCss}\n${markerEnd}\n`;
|
|
141
|
+
const next = `${before}${themeBlock}${customRulesBlock}${after}`;
|
|
142
|
+
fs.writeFileSync(targetPath, next, "utf8");
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
|
|
100
146
|
if (configPath && (inputCss || generatedInput)) {
|
|
101
147
|
const ok = buildTailwindCli({
|
|
102
148
|
input: inputCss || generatedInput,
|
|
@@ -104,7 +150,10 @@ async function ensureStyles() {
|
|
|
104
150
|
config: configPath,
|
|
105
151
|
minify: true,
|
|
106
152
|
});
|
|
107
|
-
if (ok)
|
|
153
|
+
if (ok) {
|
|
154
|
+
injectThemeTokens(dest);
|
|
155
|
+
return; // Tailwind compiled CSS
|
|
156
|
+
}
|
|
108
157
|
}
|
|
109
158
|
|
|
110
159
|
function isTailwindSource(p) {
|
|
@@ -118,18 +167,21 @@ async function ensureStyles() {
|
|
|
118
167
|
if (fs.existsSync(customAppCss)) {
|
|
119
168
|
if (!isTailwindSource(customAppCss)) {
|
|
120
169
|
await fsp.copyFile(customAppCss, dest);
|
|
170
|
+
injectThemeTokens(dest);
|
|
121
171
|
return;
|
|
122
172
|
}
|
|
123
173
|
}
|
|
124
174
|
if (fs.existsSync(customContentCss)) {
|
|
125
175
|
if (!isTailwindSource(customContentCss)) {
|
|
126
176
|
await fsp.copyFile(customContentCss, dest);
|
|
177
|
+
injectThemeTokens(dest);
|
|
127
178
|
return;
|
|
128
179
|
}
|
|
129
180
|
}
|
|
130
181
|
|
|
131
182
|
const css = `:root{--max-w:760px;--muted:#6b7280}*{box-sizing:border-box}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Helvetica,Arial,sans-serif;max-width:var(--max-w);margin:2rem auto;padding:0 1rem;line-height:1.6}a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}.site-header,.site-footer{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:1rem 0;border-bottom:1px solid #e5e7eb}.site-footer{border-bottom:0;border-top:1px solid #e5e7eb;color:var(--muted)}.brand{font-weight:600}.content pre{background:#f6f8fa;padding:1rem;overflow:auto}.content code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;background:#f6f8fa;padding:.1rem .3rem;border-radius:4px}.tabs{display:flex;gap:.5rem;align-items:center;border-bottom:1px solid #e5e7eb;margin:.5rem 0}.tab{background:none;border:0;color:#374151;padding:.25rem .5rem;border-radius:.375rem;cursor:pointer}.tab:hover{color:#111827}.tab-active{color:#2563eb;border:1px solid #e5e7eb;border-bottom:0;background:#fff}.masonry{column-gap:1rem;column-count:1}@media(min-width:768px){.masonry{column-count:2}}@media(min-width:1024px){.masonry{column-count:3}}.masonry>*{break-inside:avoid;margin-bottom:1rem;display:block}[data-grid-variant=masonry]{column-gap:var(--grid-gap,1rem);column-count:var(--cols-base,1)}@media(min-width:768px){[data-grid-variant=masonry]{column-count:var(--cols-md,2)}}@media(min-width:1024px){[data-grid-variant=masonry]{column-count:var(--cols-lg,3)}}[data-grid-variant=masonry]>*{break-inside:avoid;margin-bottom:var(--grid-gap,1rem);display:block}[data-grid-variant=grid]{display:grid;grid-template-columns:repeat(var(--cols-base,1),minmax(0,1fr));gap:var(--grid-gap,1rem)}@media(min-width:768px){[data-grid-variant=grid]{grid-template-columns:repeat(var(--cols-md,2),minmax(0,1fr))}}@media(min-width:1024px){[data-grid-variant=grid]{grid-template-columns:repeat(var(--cols-lg,3),minmax(0,1fr))}}`;
|
|
132
183
|
await fsp.writeFile(dest, css, "utf8");
|
|
184
|
+
injectThemeTokens(dest);
|
|
133
185
|
}
|
|
134
186
|
|
|
135
187
|
module.exports = { ensureStyles };
|
package/lib/common.js
CHANGED
|
@@ -8,6 +8,23 @@ const CACHE_DIR = path.resolve('.cache/mdx');
|
|
|
8
8
|
const ASSETS_DIR = path.resolve('assets');
|
|
9
9
|
|
|
10
10
|
const BASE_PATH = String(process.env.CANOPY_BASE_PATH || '').replace(/\/$/, '');
|
|
11
|
+
let cachedAppearance = null;
|
|
12
|
+
|
|
13
|
+
function resolveThemeAppearance() {
|
|
14
|
+
if (cachedAppearance) return cachedAppearance;
|
|
15
|
+
cachedAppearance = 'light';
|
|
16
|
+
try {
|
|
17
|
+
const { loadCanopyTheme } = require('@canopy-iiif/app/ui/theme');
|
|
18
|
+
if (typeof loadCanopyTheme === 'function') {
|
|
19
|
+
const theme = loadCanopyTheme();
|
|
20
|
+
const appearance = theme && theme.appearance ? String(theme.appearance) : '';
|
|
21
|
+
if (appearance.toLowerCase() === 'dark') {
|
|
22
|
+
cachedAppearance = 'dark';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch (_) {}
|
|
26
|
+
return cachedAppearance;
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
function readYamlConfigBaseUrl() {
|
|
13
30
|
try {
|
|
@@ -51,7 +68,9 @@ function htmlShell({ title, body, cssHref, scriptHref, headExtra }) {
|
|
|
51
68
|
const scriptTag = scriptHref ? `<script defer src="${scriptHref}"></script>` : '';
|
|
52
69
|
const extra = headExtra ? String(headExtra) : '';
|
|
53
70
|
const cssTag = cssHref ? `<link rel="stylesheet" href="${cssHref}">` : '';
|
|
54
|
-
|
|
71
|
+
const appearance = resolveThemeAppearance();
|
|
72
|
+
const htmlClass = appearance === 'dark' ? ' class="dark"' : '';
|
|
73
|
+
return `<!doctype html><html lang="en"${htmlClass}><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>${title}</title>${extra}${cssTag}${scriptTag}</head><body>${body}</body></html>`;
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
function withBase(href) {
|
|
@@ -111,16 +130,19 @@ function absoluteUrl(p) {
|
|
|
111
130
|
}
|
|
112
131
|
}
|
|
113
132
|
|
|
114
|
-
// Apply BASE_PATH to
|
|
133
|
+
// Apply BASE_PATH to key URL-bearing attributes (href/src/action/formaction) in an HTML string.
|
|
115
134
|
function applyBaseToHtml(html) {
|
|
116
135
|
if (!BASE_PATH) return html;
|
|
117
136
|
try {
|
|
118
137
|
const out = String(html || '');
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
: `/${BASE_PATH.replace(/\/$/, '')}`;
|
|
138
|
+
const baseRaw = BASE_PATH.startsWith('/') ? BASE_PATH : `/${BASE_PATH}`;
|
|
139
|
+
const normalizedBase = baseRaw.replace(/\/$/, '');
|
|
122
140
|
if (!normalizedBase || normalizedBase === '/') return out;
|
|
123
|
-
|
|
141
|
+
|
|
142
|
+
const attrPattern = '(?:href|src|action|formaction)';
|
|
143
|
+
const pathPattern = "\\/(?!\\/)[^'\"\\s<]*";
|
|
144
|
+
const pattern = new RegExp(`(${attrPattern})=(["'])((${pathPattern}))\\2`, 'g');
|
|
145
|
+
|
|
124
146
|
return out.replace(pattern, (match, attr, quote, path) => {
|
|
125
147
|
if (path === normalizedBase || path.startsWith(`${normalizedBase}/`)) {
|
|
126
148
|
return match;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
const {fs, path, CONTENT_DIR, rootRelativeHref} = require("../common");
|
|
2
|
+
const mdx = require("../build/mdx.js");
|
|
3
|
+
const {getPageContext} = require("../page-context");
|
|
4
|
+
|
|
5
|
+
const EXCLUDED_ROOTS = new Set(["works", "search"]);
|
|
6
|
+
|
|
7
|
+
let NAV_CACHE = null;
|
|
8
|
+
|
|
9
|
+
function normalizeRelativePath(rel) {
|
|
10
|
+
if (!rel) return "";
|
|
11
|
+
let normalized = String(rel).replace(/\\+/g, "/");
|
|
12
|
+
while (normalized.startsWith("./")) normalized = normalized.slice(2);
|
|
13
|
+
while (normalized.startsWith("../")) normalized = normalized.slice(3);
|
|
14
|
+
if (normalized.startsWith("/")) {
|
|
15
|
+
normalized = normalized.replace(/^\/+/, "");
|
|
16
|
+
}
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function humanizeSegment(seg) {
|
|
21
|
+
if (!seg) return "";
|
|
22
|
+
const cleaned = String(seg).replace(/[-_]+/g, " ");
|
|
23
|
+
return cleaned.replace(/(^|\s)([a-z])/g, (match) => match.toUpperCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function slugFromRelative(relativePath) {
|
|
27
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
28
|
+
if (!normalized) {
|
|
29
|
+
return {slug: "", segments: [], isIndex: false};
|
|
30
|
+
}
|
|
31
|
+
const parts = normalized.split("/");
|
|
32
|
+
const fileName = parts.pop() || "";
|
|
33
|
+
const baseName = fileName.replace(/\.mdx$/i, "");
|
|
34
|
+
const isIndex = baseName.toLowerCase() === "index";
|
|
35
|
+
const dirSegments = parts.filter(Boolean);
|
|
36
|
+
const segments = isIndex ? dirSegments : dirSegments.concat(baseName);
|
|
37
|
+
const slug = segments.join("/");
|
|
38
|
+
return {slug, segments, isIndex};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pageSortKey(relativePath) {
|
|
42
|
+
const normalized = normalizeRelativePath(relativePath).toLowerCase();
|
|
43
|
+
if (!normalized) return "";
|
|
44
|
+
return normalized.replace(/(^|\/)index\.mdx$/i, "$1-index.mdx");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractTitleSafe(raw) {
|
|
48
|
+
try {
|
|
49
|
+
return mdx.extractTitle(raw);
|
|
50
|
+
} catch (_) {
|
|
51
|
+
return "Untitled";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function collectPagesSync() {
|
|
56
|
+
const pages = [];
|
|
57
|
+
|
|
58
|
+
function walk(dir) {
|
|
59
|
+
let entries = [];
|
|
60
|
+
try {
|
|
61
|
+
entries = fs.readdirSync(dir, {withFileTypes: true});
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (!entry) continue;
|
|
67
|
+
const name = entry.name || "";
|
|
68
|
+
if (!name) continue;
|
|
69
|
+
if (name.startsWith(".")) continue;
|
|
70
|
+
const absPath = path.join(dir, name);
|
|
71
|
+
const relPath = path.relative(CONTENT_DIR, absPath);
|
|
72
|
+
const normalizedRel = normalizeRelativePath(relPath);
|
|
73
|
+
if (!normalizedRel) continue;
|
|
74
|
+
const segments = normalizedRel.split("/");
|
|
75
|
+
const firstRaw = segments[0] || "";
|
|
76
|
+
const firstSegment = firstRaw.replace(/\.mdx$/i, "");
|
|
77
|
+
if (EXCLUDED_ROOTS.has(firstSegment)) continue;
|
|
78
|
+
if (segments.some((segment) => segment.startsWith("_"))) continue;
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
walk(absPath);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!entry.isFile()) continue;
|
|
84
|
+
if (!/\.mdx$/i.test(name)) continue;
|
|
85
|
+
let raw = "";
|
|
86
|
+
try {
|
|
87
|
+
raw = fs.readFileSync(absPath, "utf8");
|
|
88
|
+
} catch (_) {
|
|
89
|
+
raw = "";
|
|
90
|
+
}
|
|
91
|
+
const {
|
|
92
|
+
slug,
|
|
93
|
+
segments: slugSegments,
|
|
94
|
+
isIndex,
|
|
95
|
+
} = slugFromRelative(normalizedRel);
|
|
96
|
+
const titleRaw = extractTitleSafe(raw);
|
|
97
|
+
const fallbackTitle = humanizeSegment(
|
|
98
|
+
slugSegments.slice(-1)[0] || firstSegment || ""
|
|
99
|
+
);
|
|
100
|
+
const title =
|
|
101
|
+
titleRaw && titleRaw !== "Untitled"
|
|
102
|
+
? titleRaw
|
|
103
|
+
: fallbackTitle || titleRaw;
|
|
104
|
+
const htmlRel = normalizedRel.replace(/\.mdx$/i, ".html");
|
|
105
|
+
const href = rootRelativeHref(htmlRel);
|
|
106
|
+
const page = {
|
|
107
|
+
filePath: absPath,
|
|
108
|
+
relativePath: normalizedRel,
|
|
109
|
+
slug,
|
|
110
|
+
segments: slugSegments,
|
|
111
|
+
isIndex,
|
|
112
|
+
href,
|
|
113
|
+
title,
|
|
114
|
+
fallbackTitle,
|
|
115
|
+
sortKey: pageSortKey(normalizedRel),
|
|
116
|
+
topSegment: slugSegments[0] || firstSegment || "",
|
|
117
|
+
};
|
|
118
|
+
pages.push(page);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
walk(CONTENT_DIR);
|
|
123
|
+
pages.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
124
|
+
return pages;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createNode(slug) {
|
|
128
|
+
const segments = slug ? slug.split("/") : [];
|
|
129
|
+
const name = segments.slice(-1)[0] || "";
|
|
130
|
+
return {
|
|
131
|
+
slug,
|
|
132
|
+
segments,
|
|
133
|
+
name,
|
|
134
|
+
title: humanizeSegment(name),
|
|
135
|
+
href: null,
|
|
136
|
+
hasContent: false,
|
|
137
|
+
relativePath: null,
|
|
138
|
+
sortKey: slug || name,
|
|
139
|
+
sourcePage: null,
|
|
140
|
+
children: [],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getNavigationCache() {
|
|
145
|
+
if (NAV_CACHE) return NAV_CACHE;
|
|
146
|
+
const pages = collectPagesSync();
|
|
147
|
+
const pagesByRelative = new Map();
|
|
148
|
+
const nodes = new Map();
|
|
149
|
+
|
|
150
|
+
for (const page of pages) {
|
|
151
|
+
const {slug, segments} = page;
|
|
152
|
+
pagesByRelative.set(page.relativePath, page);
|
|
153
|
+
if (!segments.length) continue;
|
|
154
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
155
|
+
const key = segments.slice(0, i + 1).join("/");
|
|
156
|
+
if (key && !nodes.has(key)) {
|
|
157
|
+
nodes.set(key, createNode(key));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const page of pages) {
|
|
163
|
+
if (!page.slug) continue;
|
|
164
|
+
const node = nodes.get(page.slug);
|
|
165
|
+
if (!node) continue;
|
|
166
|
+
if (
|
|
167
|
+
!node.sourcePage ||
|
|
168
|
+
(node.sourcePage && node.sourcePage.isIndex && !page.isIndex)
|
|
169
|
+
) {
|
|
170
|
+
node.sourcePage = page;
|
|
171
|
+
node.title = page.title || node.title;
|
|
172
|
+
node.href = page.href || node.href;
|
|
173
|
+
node.relativePath = page.relativePath;
|
|
174
|
+
node.sortKey = page.sortKey || node.sortKey;
|
|
175
|
+
node.hasContent = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const node of nodes.values()) {
|
|
180
|
+
const {segments} = node;
|
|
181
|
+
if (!segments.length) continue;
|
|
182
|
+
const parentSlug = segments.slice(0, -1).join("/");
|
|
183
|
+
if (!parentSlug) continue;
|
|
184
|
+
const parent = nodes.get(parentSlug);
|
|
185
|
+
if (!parent) continue;
|
|
186
|
+
if (!parent.children.some((child) => child.slug === node.slug)) {
|
|
187
|
+
parent.children.push(node);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const sortChildren = (node) => {
|
|
192
|
+
node.children.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
|
|
193
|
+
for (const child of node.children) sortChildren(child);
|
|
194
|
+
};
|
|
195
|
+
for (const node of nodes.values()) {
|
|
196
|
+
sortChildren(node);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const roots = new Map();
|
|
200
|
+
for (const node of nodes.values()) {
|
|
201
|
+
if (node.segments.length === 1) {
|
|
202
|
+
roots.set(node.slug, node);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
NAV_CACHE = {
|
|
207
|
+
pages,
|
|
208
|
+
pagesByRelative,
|
|
209
|
+
nodes,
|
|
210
|
+
roots,
|
|
211
|
+
};
|
|
212
|
+
return NAV_CACHE;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function cloneNode(node, currentSlug) {
|
|
216
|
+
if (!node) return null;
|
|
217
|
+
const slug = node.slug;
|
|
218
|
+
const isActive = currentSlug && slug === currentSlug;
|
|
219
|
+
const isAncestor = !!(
|
|
220
|
+
currentSlug &&
|
|
221
|
+
slug &&
|
|
222
|
+
slug.length < currentSlug.length &&
|
|
223
|
+
currentSlug.startsWith(slug + "/")
|
|
224
|
+
);
|
|
225
|
+
const children = node.children
|
|
226
|
+
.map((child) => cloneNode(child, currentSlug))
|
|
227
|
+
.filter(Boolean);
|
|
228
|
+
if (!node.hasContent && !children.length) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
slug,
|
|
233
|
+
title: node.title,
|
|
234
|
+
href: node.href,
|
|
235
|
+
segments: node.segments.slice(),
|
|
236
|
+
depth: Math.max(0, node.segments.length - 1),
|
|
237
|
+
isActive: !!isActive,
|
|
238
|
+
isAncestor,
|
|
239
|
+
isExpanded: !!(isActive || isAncestor),
|
|
240
|
+
hasContent: node.hasContent,
|
|
241
|
+
relativePath: node.relativePath,
|
|
242
|
+
children,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function getPageInfo(relativePath) {
|
|
247
|
+
const cache = getNavigationCache();
|
|
248
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
249
|
+
const page = cache.pagesByRelative.get(normalized);
|
|
250
|
+
if (page) {
|
|
251
|
+
return {
|
|
252
|
+
title: page.title,
|
|
253
|
+
href: page.href,
|
|
254
|
+
slug: page.slug,
|
|
255
|
+
segments: page.segments.slice(),
|
|
256
|
+
relativePath: page.relativePath,
|
|
257
|
+
rootSegment: page.segments[0] || "",
|
|
258
|
+
isIndex: page.isIndex,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const {slug, segments} = slugFromRelative(normalized);
|
|
262
|
+
const htmlRel = normalized.replace(/\.mdx$/i, ".html");
|
|
263
|
+
return {
|
|
264
|
+
title: humanizeSegment(segments.slice(-1)[0] || slug || ""),
|
|
265
|
+
href: rootRelativeHref(htmlRel),
|
|
266
|
+
slug,
|
|
267
|
+
segments,
|
|
268
|
+
relativePath: normalized,
|
|
269
|
+
rootSegment: segments[0] || "",
|
|
270
|
+
isIndex: false,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function buildNavigationForFile(relativePath) {
|
|
275
|
+
const cache = getNavigationCache();
|
|
276
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
277
|
+
const page = cache.pagesByRelative.get(normalized);
|
|
278
|
+
const fallback = slugFromRelative(normalized);
|
|
279
|
+
const slug = page ? page.slug : fallback.slug;
|
|
280
|
+
const rootSegment = page
|
|
281
|
+
? page.segments[0] || ""
|
|
282
|
+
: fallback.segments[0] || "";
|
|
283
|
+
if (!slug || !rootSegment || EXCLUDED_ROOTS.has(rootSegment)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const rootNode = cache.roots.get(rootSegment);
|
|
287
|
+
if (!rootNode) return null;
|
|
288
|
+
const cloned = cloneNode(rootNode, slug);
|
|
289
|
+
if (!cloned) return null;
|
|
290
|
+
return {
|
|
291
|
+
rootSegment,
|
|
292
|
+
currentSlug: slug,
|
|
293
|
+
root: cloned,
|
|
294
|
+
title: cloned.title,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resetNavigationCache() {
|
|
299
|
+
NAV_CACHE = null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = {
|
|
303
|
+
normalizeRelativePath,
|
|
304
|
+
getPageInfo,
|
|
305
|
+
buildNavigationForFile,
|
|
306
|
+
resetNavigationCache,
|
|
307
|
+
getPageContext,
|
|
308
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
|
|
3
|
+
let PageContext = null;
|
|
4
|
+
|
|
5
|
+
function getPageContext() {
|
|
6
|
+
if (!PageContext) {
|
|
7
|
+
PageContext = React.createContext({ navigation: null, page: null });
|
|
8
|
+
}
|
|
9
|
+
return PageContext;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
getPageContext,
|
|
14
|
+
};
|