@janga/norna 0.7.27 → 0.7.29
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/README.md +41 -29
- package/astro.config.mjs +33 -3
- package/bin/norna-cli.mjs +5 -3
- package/package.json +18 -12
- package/schemas/card-list.schema.json +1 -1
- package/schemas/config.schema.json +81 -66
- package/schemas/content-frontmatter.schema.json +23 -7
- package/schemas/manifest.json +49 -3
- package/schemas/page-theme.schema.json +1585 -41
- package/schemas/root-theme.schema.json +1767 -0
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +243 -243
- package/scripts/add-site-node.mjs +31 -250
- package/scripts/build-site.mjs +11 -6
- package/scripts/check-config.mjs +7 -23
- package/scripts/dev-local.mjs +51 -17
- package/scripts/export-theme-preset.mjs +2 -2
- package/scripts/generate-images.mjs +89 -70
- package/scripts/generate-schemas.mjs +9 -6
- package/scripts/generate-search-index.mjs +3 -2
- package/scripts/lib/attachment-render.mjs +23 -0
- package/scripts/lib/category-destinations.mjs +13 -12
- package/scripts/lib/dev-server-identity.mjs +30 -0
- package/scripts/lib/documentation-links.mjs +1 -1
- package/scripts/lib/documentation-routes.json +70 -62
- package/scripts/lib/docusaurus-migration-audit.mjs +15 -12
- package/scripts/lib/editor-attachments.mjs +69 -0
- package/scripts/lib/editor-file-policy.mjs +20 -0
- package/scripts/lib/editor-language-service.mjs +74 -73
- package/scripts/lib/editor-page-addresses.mjs +57 -0
- package/scripts/lib/editor-page-placement.mjs +102 -0
- package/scripts/lib/editor-resource-actions.mjs +216 -0
- package/scripts/lib/editor-site-editing-tree.mjs +181 -0
- package/scripts/lib/editor-site-files.mjs +154 -0
- package/scripts/lib/editor-site-links.mjs +81 -0
- package/scripts/lib/editor-site-preview.mjs +149 -0
- package/scripts/lib/editor-site-tree.mjs +158 -0
- package/scripts/lib/editor-source-files.mjs +87 -0
- package/scripts/lib/editor-theme-help.mjs +43 -0
- package/scripts/lib/locale-registry.mjs +1 -1
- package/scripts/lib/locales/en.mjs +1 -1
- package/scripts/lib/logo-assets.mjs +2 -1
- package/scripts/lib/markdown-images.mjs +59 -0
- package/scripts/lib/markdown-links.mjs +29 -9
- package/scripts/lib/navigation-model.mjs +2 -44
- package/scripts/lib/navigation-review.mjs +14 -50
- package/scripts/lib/norna-markdown-blocks.mjs +1 -58
- package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
- package/scripts/lib/page-aliases.mjs +8 -1
- package/scripts/lib/page-attachments.mjs +59 -0
- package/scripts/lib/page-markdown.mjs +11 -7
- package/scripts/lib/page-model.mjs +19 -36
- package/scripts/lib/page-move-apply.mjs +119 -0
- package/scripts/lib/page-move-plan.mjs +64 -78
- package/scripts/lib/page-order-source.mjs +22 -0
- package/scripts/lib/page-order.mjs +45 -0
- package/scripts/lib/page-placement-cli.mjs +50 -0
- package/scripts/lib/presentation.mjs +3 -3
- package/scripts/lib/project-config.mjs +10 -8
- package/scripts/lib/publication-paths.mjs +72 -0
- package/scripts/lib/schema-definitions.mjs +15 -38
- package/scripts/lib/schema-editor-metadata.mjs +16 -60
- package/scripts/lib/schema-value-definitions.mjs +1 -1
- package/scripts/lib/site-content.mjs +8 -25
- package/scripts/lib/site-conventions.mjs +19 -1
- package/scripts/lib/site-link-graph.mjs +88 -18
- package/scripts/lib/site-navigation-tree.mjs +2 -7
- package/scripts/lib/site-node-create.mjs +182 -0
- package/scripts/lib/site-paths.mjs +24 -12
- package/scripts/lib/site-source-format.mjs +12 -0
- package/scripts/lib/site-structure.mjs +86 -95
- package/scripts/lib/site-upgrade.mjs +32 -0
- package/scripts/lib/social-image-assets.mjs +2 -1
- package/scripts/lib/source-files.mjs +34 -0
- package/scripts/lib/structured-blocks.mjs +3 -1
- package/scripts/lib/theme-config.mjs +26 -68
- package/scripts/lib/theme-packages.mjs +47 -0
- package/scripts/lib/theme-presets.mjs +20 -53
- package/scripts/lib/typography.mjs +2 -2
- package/scripts/lib/yaml-config.mjs +1 -1
- package/scripts/lib/yaml-source-edit.mjs +94 -0
- package/scripts/move-site-page.mjs +46 -254
- package/scripts/show-typography.mjs +32 -17
- package/scripts/sync-content-sections.mjs +17 -17
- package/scripts/sync-site-public.mjs +44 -66
- package/scripts/upgrade-site.mjs +17 -0
- package/src/components/AreaNavigationMenu.astro +24 -0
- package/src/components/AreaNavigationPanel.astro +55 -0
- package/src/components/AreaNavigationSwitcher.astro +30 -0
- package/src/components/ContentBlocks.astro +2 -1
- package/src/components/ImageStack.astro +7 -5
- package/src/components/ImageStackEnhancement.astro +0 -7
- package/src/components/NavigationContinuity.astro +26 -0
- package/src/components/NavigationPageTree.astro +13 -4
- package/src/components/NavigationState.astro +11 -0
- package/src/components/NavigationTreeControls.astro +3 -2
- package/src/components/SearchPage.astro +11 -2
- package/src/components/SectionNavigationScript.astro +106 -26
- package/src/components/SiteNavigation.astro +45 -20
- package/src/components/SitePage.astro +38 -61
- package/src/components/SiteSection.astro +1 -0
- package/src/components/SiteTreeNavigation.astro +38 -18
- package/src/components/TopPageMenu.astro +4 -3
- package/src/components/TreeNavigationScript.astro +50 -57
- package/src/content.config.ts +11 -12
- package/src/layouts/BaseLayout.astro +15 -39
- package/src/lib/areaNavigation.ts +42 -0
- package/src/lib/imagePresentation.ts +3 -0
- package/src/lib/markdownImages.ts +101 -0
- package/src/lib/navigationContinuity.ts +212 -0
- package/src/lib/navigationFollowing.ts +28 -18
- package/src/lib/navigationState.mjs +88 -0
- package/src/lib/navigationStateScript.mjs +76 -0
- package/src/lib/navigationVisibility.ts +9 -0
- package/src/lib/pageThemes.ts +5 -42
- package/src/lib/sectionContent.ts +67 -21
- package/src/lib/siteNavigation.ts +6 -0
- package/src/lib/sitePages.ts +13 -36
- package/src/lib/sitePublicAssets.ts +2 -1
- package/src/pages/404.astro +1 -1
- package/src/pages/[...slug].astro +7 -16
- package/src/pages/index.astro +1 -3
- package/src/styles/area-navigation.css +408 -0
- package/src/styles/content.css +8 -29
- package/src/styles/media.css +24 -0
- package/src/styles/navigation-continuity.css +163 -0
- package/src/styles/page-layout.css +57 -68
- package/src/styles/responsive.css +0 -4
- package/starters/basic/README.md +9 -8
- package/starters/basic/site/{pages/000-home → root}/content.md +2 -2
- package/starters/basic/site/{config.yaml → site-config/settings.yaml} +1 -0
- package/schemas/category.schema.json +0 -20
- package/src/components/PageContentsNavigation.astro +0 -20
- /package/starters/basic/site/{pages/000-home → root}/images/.gitkeep +0 -0
- /package/starters/basic/site/{theme.yaml → root/tree-theme.yaml} +0 -0
- /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { createNavigationStateHelpers } from '../lib/navigationState.mjs';
|
|
3
|
+
|
|
2
4
|
type NavigationState = {
|
|
3
5
|
openPaths: string[];
|
|
4
6
|
sectionOpenByPath: Record<string, boolean>;
|
|
@@ -10,12 +12,12 @@
|
|
|
10
12
|
sectionOpen: boolean[];
|
|
11
13
|
scrollTop: number;
|
|
12
14
|
};
|
|
15
|
+
const { storageKeys, parseState, initialState, arrivalPolicy, readEntryState, saveEntryState, pageIsOpen, sectionsAreOpen } = createNavigationStateHelpers();
|
|
13
16
|
|
|
14
17
|
const setupNavigationState = (container: HTMLElement, scope: 'desktop' | 'mobile') => {
|
|
15
18
|
const root = container.dataset.navigationRoot ?? '/';
|
|
16
19
|
const sharedRoot = container.dataset.navigationStateRoot ?? root;
|
|
17
|
-
const stateKey =
|
|
18
|
-
const sharedStateKey = `norna:tree-navigation:shared:${sharedRoot}`;
|
|
20
|
+
const { stateKey, sharedStateKey } = storageKeys(scope, root, sharedRoot);
|
|
19
21
|
const branches = Array.from(container.querySelectorAll<HTMLDetailsElement>(
|
|
20
22
|
'.navigation-page-disclosure[data-page-path]',
|
|
21
23
|
));
|
|
@@ -38,22 +40,7 @@
|
|
|
38
40
|
const scrollContainer = getScrollContainer();
|
|
39
41
|
let filterSnapshot: NavigationSnapshot | null = null;
|
|
40
42
|
let lastSharedState = '';
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const parsedState = JSON.parse(value ?? '{}');
|
|
44
|
-
return {
|
|
45
|
-
openPaths: Array.isArray(parsedState.openPaths) ? parsedState.openPaths : [],
|
|
46
|
-
sectionOpenByPath: parsedState.sectionOpenByPath
|
|
47
|
-
&& typeof parsedState.sectionOpenByPath === 'object'
|
|
48
|
-
? Object.fromEntries(Object.entries(parsedState.sectionOpenByPath)
|
|
49
|
-
.filter((entry): entry is [string, boolean] => typeof entry[1] === 'boolean'))
|
|
50
|
-
: {},
|
|
51
|
-
scrollTop: Number.isFinite(Number(parsedState.scrollTop)) ? Number(parsedState.scrollTop) : 0,
|
|
52
|
-
};
|
|
53
|
-
} catch {
|
|
54
|
-
return { openPaths: [], sectionOpenByPath: {}, scrollTop: 0 };
|
|
55
|
-
}
|
|
56
|
-
};
|
|
43
|
+
let preserveOutgoingEntry = false;
|
|
57
44
|
const getDisclosureState = (): SharedNavigationState => ({
|
|
58
45
|
openPaths: branches.filter((branch) => branch.open)
|
|
59
46
|
.map((branch) => branch.dataset.pagePath ?? ''),
|
|
@@ -64,23 +51,13 @@
|
|
|
64
51
|
});
|
|
65
52
|
const applyDisclosureState = (
|
|
66
53
|
state: SharedNavigationState,
|
|
67
|
-
|
|
54
|
+
policy: { onArrival?: boolean; preserveCurrentPage?: boolean; preserveCurrentOutline?: boolean } = {},
|
|
68
55
|
) => {
|
|
69
56
|
branches.forEach((branch) => {
|
|
70
|
-
|
|
71
|
-
branch.open = (openCurrentPageOnArrival && branch.dataset.currentBranch === 'true')
|
|
72
|
-
|| state.openPaths.includes(pagePath);
|
|
57
|
+
branch.open = pageIsOpen(state, branch.dataset, policy);
|
|
73
58
|
});
|
|
74
59
|
sectionBranches.forEach((branch) => {
|
|
75
|
-
|
|
76
|
-
const storedOpen = state.sectionOpenByPath[pagePath];
|
|
77
|
-
if (openCurrentPageOnArrival && branch.dataset.currentPage === 'true') {
|
|
78
|
-
branch.open = true;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
branch.open = typeof storedOpen === 'boolean'
|
|
82
|
-
? storedOpen
|
|
83
|
-
: branch.dataset.currentPage === 'true';
|
|
60
|
+
branch.open = sectionsAreOpen(state, branch.dataset, policy);
|
|
84
61
|
});
|
|
85
62
|
};
|
|
86
63
|
const mergeSharedState = (state: SharedNavigationState): SharedNavigationState => {
|
|
@@ -99,18 +76,16 @@
|
|
|
99
76
|
};
|
|
100
77
|
|
|
101
78
|
try {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
applyDisclosureState(
|
|
105
|
-
? storedState
|
|
106
|
-
: parseState(storedSharedState), true);
|
|
79
|
+
const state = initialState(sessionStorage.getItem(stateKey), sessionStorage.getItem(sharedStateKey),
|
|
80
|
+
readEntryState(stateKey));
|
|
81
|
+
applyDisclosureState(state.disclosure, arrivalPolicy(state));
|
|
107
82
|
const initialSharedState = mergeSharedState(getDisclosureState());
|
|
108
83
|
lastSharedState = JSON.stringify(initialSharedState);
|
|
109
84
|
sessionStorage.setItem(sharedStateKey, lastSharedState);
|
|
110
85
|
|
|
111
|
-
if (
|
|
86
|
+
if (state.local.scrollTop > 0 || state.fromEntry) {
|
|
112
87
|
requestAnimationFrame(() => {
|
|
113
|
-
scrollContainer.scrollTop =
|
|
88
|
+
scrollContainer.scrollTop = Math.max(0, state.local.scrollTop);
|
|
114
89
|
});
|
|
115
90
|
}
|
|
116
91
|
} catch {
|
|
@@ -127,6 +102,7 @@
|
|
|
127
102
|
scrollTop: scrollContainer.scrollTop,
|
|
128
103
|
};
|
|
129
104
|
sessionStorage.setItem(stateKey, JSON.stringify(state));
|
|
105
|
+
if (!preserveOutgoingEntry) saveEntryState(stateKey, state);
|
|
130
106
|
const sharedState = mergeSharedState(disclosureState);
|
|
131
107
|
const serializedSharedState = JSON.stringify(sharedState);
|
|
132
108
|
sessionStorage.setItem(sharedStateKey, serializedSharedState);
|
|
@@ -278,23 +254,6 @@
|
|
|
278
254
|
- Math.max(0, (scrollContainer.clientHeight - linkBox.height) / 2);
|
|
279
255
|
setStatus(labels.located);
|
|
280
256
|
};
|
|
281
|
-
const openLinkedPage = (link: HTMLAnchorElement) => {
|
|
282
|
-
if (filterInput?.value) {
|
|
283
|
-
filterInput.value = '';
|
|
284
|
-
clearFilter();
|
|
285
|
-
}
|
|
286
|
-
const pageNode = link.closest<HTMLElement>('.navigation-page-node');
|
|
287
|
-
const pageDisclosure = pageNode?.querySelector<HTMLDetailsElement>(
|
|
288
|
-
':scope > .navigation-page-disclosure:not(.navigation-category-disclosure)',
|
|
289
|
-
);
|
|
290
|
-
const sectionDisclosure = pageNode?.querySelector<HTMLDetailsElement>(
|
|
291
|
-
':scope > .navigation-page-sections-disclosure[data-page-path]',
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
if (pageDisclosure) pageDisclosure.open = true;
|
|
295
|
-
if (sectionDisclosure) sectionDisclosure.open = true;
|
|
296
|
-
saveState();
|
|
297
|
-
};
|
|
298
257
|
|
|
299
258
|
requestAnimationFrame(() => {
|
|
300
259
|
branches.forEach((branch) => branch.addEventListener('toggle', saveState));
|
|
@@ -314,8 +273,42 @@
|
|
|
314
273
|
collapseButton?.addEventListener('click', () => setAllOpen(false));
|
|
315
274
|
locateButton?.addEventListener('click', locateCurrentPage);
|
|
316
275
|
scrollContainer.addEventListener('scroll', saveState, { passive: true });
|
|
317
|
-
|
|
318
|
-
|
|
276
|
+
// Cross-document arrivals open their current branch in the early
|
|
277
|
+
// restore. Do not expand the departing tree and paint an extra frame.
|
|
278
|
+
document.addEventListener('click', (event) => {
|
|
279
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
280
|
+
const link = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>('a[href]') : null;
|
|
281
|
+
if (!link || link.hasAttribute('download') || (link.target && link.target !== '_self')) return;
|
|
282
|
+
const url = new URL(link.href, location.href);
|
|
283
|
+
if (url.origin !== location.origin || url.pathname !== location.pathname || url.search !== location.search || !currentPageNode) return;
|
|
284
|
+
// A same-document fragment creates a new history entry after this
|
|
285
|
+
// click. Keep the old entry's disclosure snapshot until then.
|
|
286
|
+
preserveOutgoingEntry = url.href !== location.href;
|
|
287
|
+
if (filterInput?.value) { filterInput.value = ''; clearFilter(); }
|
|
288
|
+
const scrollTop = scrollContainer.scrollTop;
|
|
289
|
+
revealNodeAndAncestors(currentPageNode);
|
|
290
|
+
currentPageNode.querySelectorAll<HTMLDetailsElement>(
|
|
291
|
+
':scope > .navigation-page-disclosure, :scope > .navigation-page-sections-disclosure',
|
|
292
|
+
).forEach((branch) => { branch.open = true; });
|
|
293
|
+
scrollContainer.scrollTop = scrollTop;
|
|
294
|
+
saveState();
|
|
295
|
+
});
|
|
296
|
+
const restoreEntry = () => {
|
|
297
|
+
preserveOutgoingEntry = false;
|
|
298
|
+
try {
|
|
299
|
+
const source = readEntryState(stateKey);
|
|
300
|
+
if (source === null) return;
|
|
301
|
+
const state = parseState(source);
|
|
302
|
+
applyDisclosureState(state);
|
|
303
|
+
scrollContainer.scrollTop = Math.max(0, state.scrollTop);
|
|
304
|
+
saveState();
|
|
305
|
+
} catch { /* Native links work when storage is unavailable. */ }
|
|
306
|
+
};
|
|
307
|
+
window.addEventListener('pageshow', (event) => { if (event.persisted) restoreEntry(); });
|
|
308
|
+
window.addEventListener('popstate', restoreEntry);
|
|
309
|
+
window.addEventListener('hashchange', () => { preserveOutgoingEntry = false; saveState(); });
|
|
310
|
+
requestAnimationFrame(saveState);
|
|
311
|
+
|
|
319
312
|
window.addEventListener('pagehide', saveState);
|
|
320
313
|
};
|
|
321
314
|
|
package/src/content.config.ts
CHANGED
|
@@ -1,36 +1,35 @@
|
|
|
1
1
|
import { defineCollection } from 'astro:content';
|
|
2
2
|
import { glob } from 'astro/loaders';
|
|
3
3
|
import { z } from 'astro/zod';
|
|
4
|
-
import { basename } from 'node:path';
|
|
4
|
+
import { basename, dirname } from 'node:path';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
siteHomePageDir,
|
|
8
8
|
siteDirLabel,
|
|
9
9
|
siteThemePath,
|
|
10
10
|
sitewideContentPath,
|
|
11
11
|
} from '../scripts/lib/site-paths.mjs';
|
|
12
12
|
import { encodePageEntryId, getSiteEntryPrefix, parsePageDirectoryPath } from '../scripts/lib/page-model.mjs';
|
|
13
|
+
import { homePageDirectory } from '../scripts/lib/site-conventions.mjs';
|
|
13
14
|
import {
|
|
14
15
|
siteSchema,
|
|
15
16
|
sitewideSchema,
|
|
16
|
-
|
|
17
|
+
rootThemeSchema,
|
|
17
18
|
} from '../scripts/lib/schema-definitions.mjs';
|
|
18
19
|
|
|
19
20
|
const siteEntryPrefix = getSiteEntryPrefix(siteDirLabel);
|
|
20
21
|
const emptyYamlMapping = (value: unknown) => value ?? {};
|
|
21
|
-
const siteThemeSchema = z.preprocess(emptyYamlMapping,
|
|
22
|
+
const siteThemeSchema = z.preprocess(emptyYamlMapping, rootThemeSchema);
|
|
22
23
|
const sitewideContentSchema = z.preprocess(emptyYamlMapping, sitewideSchema);
|
|
23
24
|
|
|
24
25
|
const site = defineCollection({
|
|
25
26
|
loader: glob({
|
|
26
|
-
pattern: 'pages/**/content.md',
|
|
27
|
-
base: pathToFileURL(
|
|
27
|
+
pattern: ['content.md', 'pages/**/content.md'],
|
|
28
|
+
base: pathToFileURL(siteHomePageDir),
|
|
28
29
|
generateId: ({ entry }) => {
|
|
30
|
+
if (entry === 'content.md') return encodePageEntryId(siteDirLabel, homePageDirectory);
|
|
29
31
|
const pageEntryDirectory = entry.split('/').slice(1, -1).join('/');
|
|
30
|
-
const pageDirectory = pageEntryDirectory
|
|
31
|
-
.split('/')
|
|
32
|
-
.filter((segment) => segment !== 'pages')
|
|
33
|
-
.join('/pages/');
|
|
32
|
+
const pageDirectory = pageEntryDirectory;
|
|
34
33
|
return encodePageEntryId(siteDirLabel, parsePageDirectoryPath(pageDirectory, `page directory pages/${pageEntryDirectory}`).pageDirectory);
|
|
35
34
|
},
|
|
36
35
|
}),
|
|
@@ -40,7 +39,7 @@ const site = defineCollection({
|
|
|
40
39
|
const theme = defineCollection({
|
|
41
40
|
loader: glob({
|
|
42
41
|
pattern: basename(siteThemePath),
|
|
43
|
-
base: pathToFileURL(
|
|
42
|
+
base: pathToFileURL(dirname(siteThemePath)),
|
|
44
43
|
generateId: () => `${siteEntryPrefix}-theme`,
|
|
45
44
|
}),
|
|
46
45
|
schema: siteThemeSchema,
|
|
@@ -49,7 +48,7 @@ const theme = defineCollection({
|
|
|
49
48
|
const sitewide = defineCollection({
|
|
50
49
|
loader: glob({
|
|
51
50
|
pattern: basename(sitewideContentPath),
|
|
52
|
-
base: pathToFileURL(
|
|
51
|
+
base: pathToFileURL(dirname(sitewideContentPath)),
|
|
53
52
|
generateId: () => `${siteEntryPrefix}-sitewide`,
|
|
54
53
|
}),
|
|
55
54
|
schema: sitewideContentSchema,
|
|
@@ -5,6 +5,8 @@ import { getPresentationCssVariables } from '../../scripts/lib/presentation.mjs'
|
|
|
5
5
|
import { getIconLinks, getSocialImageAsset } from '../lib/sitePublicAssets';
|
|
6
6
|
import { withBasePath } from '../lib/basePath';
|
|
7
7
|
import { getReaderPreferencesScript } from '../lib/readerPreferencesScript.mjs';
|
|
8
|
+
import NavigationContinuity from '../components/NavigationContinuity.astro';
|
|
9
|
+
import areaNavigationStyles from '../styles/area-navigation.css?inline';
|
|
8
10
|
import '../styles/global.css';
|
|
9
11
|
|
|
10
12
|
type FrameColors = {
|
|
@@ -80,6 +82,8 @@ interface Props {
|
|
|
80
82
|
contentsNavigation?: boolean;
|
|
81
83
|
indexable?: boolean;
|
|
82
84
|
searchable?: boolean;
|
|
85
|
+
areaNavigation?: boolean;
|
|
86
|
+
areaReadingFrame?: boolean;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
const {
|
|
@@ -93,6 +97,8 @@ const {
|
|
|
93
97
|
contentsNavigation = false,
|
|
94
98
|
indexable = true,
|
|
95
99
|
searchable = false,
|
|
100
|
+
areaNavigation = false,
|
|
101
|
+
areaReadingFrame = false,
|
|
96
102
|
} = Astro.props;
|
|
97
103
|
|
|
98
104
|
const documentTitle = title;
|
|
@@ -150,49 +156,13 @@ const marginNoteOuterReserve = '0.5rem';
|
|
|
150
156
|
const marginNoteDeclarations = marginNoteLayoutDeclarations;
|
|
151
157
|
const marginNoteLayoutCss = [
|
|
152
158
|
'@media (min-width: 1101px) {',
|
|
153
|
-
...marginNoteReadingWidths.flatMap(([readingWidth, textWidth]) => [
|
|
154
|
-
[
|
|
155
|
-
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap})) {`,
|
|
156
|
-
`:root[data-reading-width="${readingWidth}"] .site-page-layout:not(.site-page-layout-tree) .section-note-paragraph {`,
|
|
157
|
-
marginNoteDeclarations,
|
|
158
|
-
'}',
|
|
159
|
-
'}',
|
|
160
|
-
].join('\n'),
|
|
161
|
-
[
|
|
162
|
-
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap} + ${marginNoteOuterReserve} - 11rem - ${visualTheme.layout.localNavigationGap})) {`,
|
|
163
|
-
`:root[data-reader-preferences-ready="true"][data-reading-width="${readingWidth}"][data-focus-reading="on"] .site-page-layout-tree .section-note-paragraph {`,
|
|
164
|
-
marginNoteDeclarations,
|
|
165
|
-
'}',
|
|
166
|
-
'}',
|
|
167
|
-
].join('\n'),
|
|
168
|
-
]),
|
|
169
|
-
'}',
|
|
170
|
-
'@media (min-width: 1101px) and (max-width: 1280px) {',
|
|
171
159
|
...marginNoteReadingWidths.map(([readingWidth, textWidth]) => [
|
|
172
160
|
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap} + ${marginNoteOuterReserve})) {`,
|
|
173
|
-
`:root[data-reading-width="${readingWidth}"] .
|
|
174
|
-
|
|
175
|
-
'}',
|
|
176
|
-
'}',
|
|
177
|
-
].join('\n')),
|
|
178
|
-
'}',
|
|
179
|
-
'@media (min-width: 1281px) {',
|
|
180
|
-
...marginNoteReadingWidths.flatMap(([readingWidth, textWidth]) => [
|
|
181
|
-
[
|
|
182
|
-
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap})) {`,
|
|
183
|
-
`:root[data-reading-width="${readingWidth}"] .site-page-layout-tree.site-page-layout-contents .section-note-paragraph {`,
|
|
184
|
-
marginNoteDeclarations,
|
|
185
|
-
'}',
|
|
161
|
+
`:root[data-reading-width="${readingWidth}"] .section-note-paragraph {`,
|
|
162
|
+
marginNoteDeclarations,
|
|
186
163
|
'}',
|
|
187
|
-
].join('\n'),
|
|
188
|
-
[
|
|
189
|
-
`@container (min-width: calc(${textWidth} + ${visualTheme.layout.noteWidth} + ${visualTheme.layout.noteGap} + ${marginNoteOuterReserve} - 11rem - ${visualTheme.layout.localNavigationGap})) {`,
|
|
190
|
-
`:root[data-reading-width="${readingWidth}"] .site-page-layout-tree:not(.site-page-layout-contents) .section-note-paragraph {`,
|
|
191
|
-
marginNoteDeclarations,
|
|
192
|
-
'}',
|
|
193
164
|
'}',
|
|
194
|
-
|
|
195
|
-
]),
|
|
165
|
+
].join('\n')),
|
|
196
166
|
'}',
|
|
197
167
|
].join('\n');
|
|
198
168
|
const { buildInfo, copyrightMessage } = footer ?? {};
|
|
@@ -227,6 +197,9 @@ const readerPreferencesScript = readerPreferencesEnabled
|
|
|
227
197
|
data-reading-width={presentation.readerPreferences.defaults.readingWidth}
|
|
228
198
|
data-focus-reading={presentation.readerPreferences.defaults.focusReading}
|
|
229
199
|
data-section-tracking={contentsNavigation ? 'enabled' : undefined}
|
|
200
|
+
data-navigation-continuity={projectConfig.site.basePath}
|
|
201
|
+
data-area-navigation={areaNavigation ? '' : undefined}
|
|
202
|
+
data-area-reading-frame={areaReadingFrame ? '' : undefined}
|
|
230
203
|
data-pagefind-ignore={searchable ? undefined : 'all'}
|
|
231
204
|
style={documentStyle}
|
|
232
205
|
>
|
|
@@ -248,6 +221,8 @@ const readerPreferencesScript = readerPreferencesEnabled
|
|
|
248
221
|
{indexable && socialImageUrl && <meta name="twitter:image" content={socialImageUrl} />}
|
|
249
222
|
<meta name="theme-color" content={presentation.palette.page.backgroundColor} />
|
|
250
223
|
{readerPreferencesScript && <script is:inline set:html={readerPreferencesScript} />}
|
|
224
|
+
<NavigationContinuity />
|
|
225
|
+
{areaNavigation && <style is:inline set:html={areaNavigationStyles} />}
|
|
251
226
|
<style is:inline set:html={marginNoteLayoutCss} />
|
|
252
227
|
{indexable && <link rel="canonical" href={canonicalUrl} />}
|
|
253
228
|
<meta name="generator" content={Astro.generator} />
|
|
@@ -272,5 +247,6 @@ const readerPreferencesScript = readerPreferencesEnabled
|
|
|
272
247
|
</p>
|
|
273
248
|
</footer>
|
|
274
249
|
)}
|
|
250
|
+
<span id="norna-navigation-ready" hidden></span>
|
|
275
251
|
</body>
|
|
276
252
|
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SiteNavigationNode } from './siteNavigation';
|
|
2
|
+
import type { SiteNode } from './sitePages';
|
|
3
|
+
|
|
4
|
+
const containsPage = (root: SiteNavigationNode, page: SiteNode) => (
|
|
5
|
+
root.node.pagePath === page.pagePath
|
|
6
|
+
|| (root.node.pagePath !== '' && page.pagePath.startsWith(`${root.node.pagePath}/`))
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
// Beyond twelve choices, use a destination with a local tree instead of a panel.
|
|
10
|
+
export const hasLargeAreaMenu = (root: SiteNavigationNode) => (
|
|
11
|
+
root.children.length + (root.node.kind === 'page' ? 1 : 0) > 12
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const getAreaMenuGroups = (root: SiteNavigationNode) => {
|
|
15
|
+
const choices = root.node.kind === 'page' ? [root, ...root.children] : root.children;
|
|
16
|
+
return {
|
|
17
|
+
branches: choices.filter(({ children }) => children.length > 0),
|
|
18
|
+
pages: choices.filter(({ children }) => children.length === 0),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Only the listed tree enters this resolver. Flat collections keep their pages
|
|
23
|
+
// together; collections with subgroups select child branches. Large collections
|
|
24
|
+
// and a parent's own overview retain the full tree. Scope never depends on the
|
|
25
|
+
// arrival route, stored state or the number of headings on a page.
|
|
26
|
+
export const resolveAreaNavigation = (roots: SiteNavigationNode[], page?: SiteNode) => {
|
|
27
|
+
const globalRoot = page ? roots.find((root) => containsPage(root, page)) : undefined;
|
|
28
|
+
if (globalRoot && globalRoot.children.length > 0 && (
|
|
29
|
+
globalRoot.children.every((child) => child.children.length === 0)
|
|
30
|
+
|| hasLargeAreaMenu(globalRoot) || page?.pagePath === globalRoot.node.pagePath
|
|
31
|
+
)) return { globalRoot, localRoot: globalRoot };
|
|
32
|
+
const area = page && globalRoot?.children.find((child) => (
|
|
33
|
+
child.children.length > 0 && containsPage(child, page)
|
|
34
|
+
));
|
|
35
|
+
return { globalRoot, localRoot: area };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const withH2Outlines = (roots: SiteNavigationNode[]): SiteNavigationNode[] => roots.map((root) => ({
|
|
39
|
+
...root,
|
|
40
|
+
headings: root.headings.filter(({ depth }) => depth === 2),
|
|
41
|
+
children: withH2Outlines(root.children),
|
|
42
|
+
}));
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { parseFragment, serializeOuter, type DefaultTreeAdapterMap } from 'parse5';
|
|
2
|
+
import { getPageImageSourceKey } from '../../scripts/lib/site-conventions.mjs';
|
|
3
|
+
import { getGeneratedImage, getImageAttributes, getImageInspectionAttributes } from './generatedImages';
|
|
4
|
+
import { getImageClass, getImageLoadingProps, getManagedImageSizes } from './imagePresentation';
|
|
5
|
+
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
6
|
+
|
|
7
|
+
type Node = DefaultTreeAdapterMap['node'];
|
|
8
|
+
type Element = DefaultTreeAdapterMap['element'];
|
|
9
|
+
const isElement = (node: Node): node is Element => 'tagName' in node;
|
|
10
|
+
const attribute = (node: Element, name: string) => node.attrs.find((attr) => attr.name === name)?.value;
|
|
11
|
+
const meaningfulChildren = (node: Element) => node.childNodes.filter((child) => child.nodeName !== '#text' || ('value' in child && child.value.trim()));
|
|
12
|
+
const soleImage = (node: Element): { image: Element; href?: string; linkTitle?: string } | null => {
|
|
13
|
+
const children = meaningfulChildren(node);
|
|
14
|
+
if (children.length !== 1 || !isElement(children[0])) return null;
|
|
15
|
+
const child = children[0];
|
|
16
|
+
if (child.tagName === 'img' && attribute(child, 'data-norna-managed-image')) return { image: child };
|
|
17
|
+
if (child.tagName === 'a') {
|
|
18
|
+
const nested = soleImage(child);
|
|
19
|
+
if (nested) return { image: nested.image, href: attribute(child, 'href'), linkTitle: attribute(child, 'title') };
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Promote only top-level, single-image paragraphs to the shared Astro stack.
|
|
25
|
+
// Nested images stay in their original list, quote, table, details or text.
|
|
26
|
+
export const splitMarkdownImages = (html: string, pageDirectory: string) => {
|
|
27
|
+
if (!html.includes('data-norna-managed-image=')) return [{ type: 'html' as const, html }];
|
|
28
|
+
const fragment = parseFragment(html);
|
|
29
|
+
const result: Array<{ type: 'html'; html: string } | { type: 'image-stack'; images: Array<{ image: string; src: string; alt: string; title?: string; href?: string; linkTitle?: string }> }> = [];
|
|
30
|
+
let pending = '';
|
|
31
|
+
const flush = () => { if (pending.trim()) result.push({ type: 'html', html: pending }); pending = ''; };
|
|
32
|
+
const markSource = (node: Node) => {
|
|
33
|
+
if (!isElement(node)) return;
|
|
34
|
+
const marker = node.attrs.find((attr) => attr.name === 'data-norna-managed-image');
|
|
35
|
+
if (marker) marker.value = getPageImageSourceKey(pageDirectory, marker.value);
|
|
36
|
+
for (const child of node.childNodes) markSource(child);
|
|
37
|
+
};
|
|
38
|
+
for (const node of fragment.childNodes) {
|
|
39
|
+
const standalone = isElement(node) && node.tagName === 'p' ? soleImage(node) : null;
|
|
40
|
+
if (standalone) {
|
|
41
|
+
flush();
|
|
42
|
+
const name = attribute(standalone.image, 'data-norna-managed-image')!;
|
|
43
|
+
result.push({ type: 'image-stack', images: [{ image: name, src: getPageImageSourceKey(pageDirectory, name), alt: attribute(standalone.image, 'alt') ?? '', title: attribute(standalone.image, 'title'), href: standalone.href, linkTitle: standalone.linkTitle }] });
|
|
44
|
+
} else {
|
|
45
|
+
markSource(node);
|
|
46
|
+
pending += serializeOuter(node);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
flush();
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const escapeAttribute = (value: unknown) => String(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
54
|
+
const attributes = (values: Record<string, unknown>) => Object.entries(values).filter(([, value]) => value !== undefined && value !== null).map(([key, value]) => ` ${key}="${escapeAttribute(value)}"`).join('');
|
|
55
|
+
|
|
56
|
+
export const renderNestedMarkdownImages = (html: string, title: string, width: { desktop: number; mobile: number }) => {
|
|
57
|
+
if (!html.includes('data-norna-managed-image=')) return html;
|
|
58
|
+
const fragment = parseFragment(html, { sourceCodeLocationInfo: true });
|
|
59
|
+
const edits: Array<{ start: number; end: number; html: string }> = [];
|
|
60
|
+
const renderImage = (node: Element, standalone = false, href?: string, linkTitle?: string) => {
|
|
61
|
+
const src = attribute(node, 'data-norna-managed-image')!;
|
|
62
|
+
if (!getGeneratedImage(src)) throw new Error(`Managed Markdown image is missing from the generated manifest: ${src}. Run Norna's image preparation after correcting the source file.`);
|
|
63
|
+
const alt = attribute(node, 'alt') ?? '';
|
|
64
|
+
const imageClass = standalone ? ['managed-image', getImageClass({ image: src })].filter(Boolean).join(' ') : 'managed-markdown-inline';
|
|
65
|
+
const image = `<img${attributes({ ...getImageAttributes(src, getManagedImageSizes(width)), class: imageClass, alt, title: attribute(node, 'title'), ...getImageLoadingProps(false) })}>`;
|
|
66
|
+
if (!standalone) return image;
|
|
67
|
+
const inspection = getImageInspectionAttributes(src);
|
|
68
|
+
const link = attributes({
|
|
69
|
+
class: 'managed-image-inspection-link', href: href ?? inspection.href, title: linkTitle,
|
|
70
|
+
'aria-label': href === undefined ? projectConfig.locale.labels.inspectImage.replace('{description}', alt || title) : undefined,
|
|
71
|
+
'data-image-inspection-trigger': href === undefined ? '' : undefined,
|
|
72
|
+
'data-image-intrinsic-width': inspection.width, 'data-image-intrinsic-height': inspection.height,
|
|
73
|
+
'data-image-inspection-scalable': inspection.scalable ? 'true' : undefined,
|
|
74
|
+
style: inspection.width && inspection.height ? `--managed-image-intrinsic-width: ${inspection.width}px; --managed-image-aspect-ratio: ${inspection.width / inspection.height}` : undefined,
|
|
75
|
+
});
|
|
76
|
+
// The same Lucide Maximize2 affordance used by ImageStack.astro; this
|
|
77
|
+
// fragment stays inside the authored Markdown container.
|
|
78
|
+
const affordance = href === undefined ? '<span class="managed-image-inspection-affordance" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="m21 3-7 7"/><path d="m3 21 7-7"/><path d="M9 21H3v-6"/></svg></span>' : '';
|
|
79
|
+
return `<div class="image-stack managed-markdown-standalone content-block-note-lane-boundary"><figure class="managed-image-item"><div class="managed-image-frame"><a${link}>${image}${affordance}</a></div></figure></div>`;
|
|
80
|
+
};
|
|
81
|
+
const visit = (node: Node) => {
|
|
82
|
+
if (!isElement(node)) return;
|
|
83
|
+
const location = node.sourceCodeLocation;
|
|
84
|
+
if (location && (node.tagName === 'p' || node.tagName === 'li')) {
|
|
85
|
+
const standalone = soleImage(node);
|
|
86
|
+
if (standalone) {
|
|
87
|
+
const start = node.tagName === 'li' ? location.startTag!.endOffset : location.startOffset;
|
|
88
|
+
const end = node.tagName === 'li' ? (location.endTag?.startOffset ?? location.endOffset) : location.endOffset;
|
|
89
|
+
edits.push({ start, end, html: renderImage(standalone.image, true, standalone.href, standalone.linkTitle) });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (location && node.tagName === 'img' && attribute(node, 'data-norna-managed-image')) {
|
|
94
|
+
edits.push({ start: location.startOffset, end: location.endOffset, html: renderImage(node) });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const child of node.childNodes) visit(child);
|
|
98
|
+
};
|
|
99
|
+
for (const node of fragment.childNodes) visit(node);
|
|
100
|
+
return edits.sort((a, b) => b.start - a.start).reduce((result, edit) => result.slice(0, edit.start) + edit.html + result.slice(edit.end), html);
|
|
101
|
+
};
|