@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.
Files changed (36) hide show
  1. package/lib/build/build.js +2 -0
  2. package/lib/build/dev.js +38 -22
  3. package/lib/build/iiif.js +359 -83
  4. package/lib/build/mdx.js +12 -2
  5. package/lib/build/pages.js +15 -1
  6. package/lib/build/styles.js +53 -1
  7. package/lib/common.js +28 -6
  8. package/lib/components/navigation.js +308 -0
  9. package/lib/page-context.js +14 -0
  10. package/lib/search/search-app.jsx +177 -25
  11. package/lib/search/search-form-runtime.js +126 -19
  12. package/lib/search/search.js +130 -18
  13. package/package.json +4 -1
  14. package/ui/dist/index.mjs +204 -101
  15. package/ui/dist/index.mjs.map +4 -4
  16. package/ui/dist/server.mjs +167 -59
  17. package/ui/dist/server.mjs.map +4 -4
  18. package/ui/styles/_variables.scss +1 -0
  19. package/ui/styles/base/_common.scss +27 -5
  20. package/ui/styles/base/_heading.scss +2 -4
  21. package/ui/styles/base/index.scss +1 -0
  22. package/ui/styles/components/_card.scss +47 -4
  23. package/ui/styles/components/_sub-navigation.scss +76 -0
  24. package/ui/styles/components/header/_header.scss +1 -4
  25. package/ui/styles/components/header/_logo.scss +33 -10
  26. package/ui/styles/components/index.scss +1 -0
  27. package/ui/styles/components/search/_filters.scss +5 -7
  28. package/ui/styles/components/search/_form.scss +55 -17
  29. package/ui/styles/components/search/_results.scss +49 -14
  30. package/ui/styles/index.css +344 -56
  31. package/ui/styles/index.scss +2 -4
  32. package/ui/tailwind-canopy-iiif-plugin.js +10 -2
  33. package/ui/tailwind-canopy-iiif-preset.js +21 -19
  34. package/ui/theme.js +303 -0
  35. package/ui/styles/variables.emit.scss +0 -72
  36. 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, null, contentNode)
286
+ ? React.createElement(dirLayout, layoutProps, contentNode)
285
287
  : contentNode;
286
- const withApp = React.createElement(app.App, null, withLayout);
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)
@@ -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 { body, head } = await mdx.compileMdxFile(filePath, outPath, null, extraProps);
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
@@ -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) return; // Tailwind compiled CSS
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
- 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}${cssTag}${scriptTag}</head><body>${body}</body></html>`;
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 any absolute href/src attributes found in an HTML string.
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 normalizedBase = BASE_PATH.startsWith('/')
120
- ? BASE_PATH.replace(/\/$/, '')
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
- const pattern = /(href|src)=(['"])(\/(?!\/)[^'"\s]*)\2/g;
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
+ };