@janga/norna 0.7.16 → 0.7.17
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 +6 -6
- package/astro.config.mjs +35 -3
- package/docs/README.md +10 -10
- package/docs/commands.md +2 -2
- package/docs/content.md +82 -102
- package/docs/design/navigation-and-theme-plan.md +115 -0
- package/docs/design/norna-diagram-design.md +9 -9
- package/docs/design/site-examples-structure.md +9 -9
- package/docs/engine-development.md +9 -9
- package/docs/getting-started.md +20 -15
- package/docs/images-and-metadata.md +10 -9
- package/docs/pages.md +134 -0
- package/docs/public-files.md +49 -36
- package/docs/publishing.md +19 -3
- package/docs/site-files.md +29 -28
- package/docs/sitewide-content.md +20 -36
- package/docs/theme.md +20 -20
- package/docs/typography.md +10 -10
- package/examples/README.md +2 -2
- package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +17 -41
- package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +26 -67
- package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +185 -88
- package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
- package/examples/complete-sites/dog-shelter-multi-page/.astro/dev-local.json +4 -4
- package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +5 -5
- package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +33 -23
- package/examples/complete-sites/dog-shelter-multi-page/README.md +1 -1
- package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +8 -8
- package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +20 -20
- package/examples/complete-sites/dog-shelter-multi-page/site/content.md +3 -4
- package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/010-dogs/content.md +3 -4
- package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/020-adopt/content.md +3 -4
- package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +2 -4
- package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +17 -41
- package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +26 -67
- package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +185 -88
- package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -1
- package/examples/complete-sites/dog-shelter-single-page/.astro/dev-local.json +8 -0
- package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +5 -5
- package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +16 -12
- package/examples/complete-sites/dog-shelter-single-page/README.md +1 -1
- package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +8 -8
- package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +12 -12
- package/examples/complete-sites/dog-shelter-single-page/site/content.md +5 -2
- package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +2 -4
- package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +17 -41
- package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +26 -67
- package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +185 -88
- package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
- package/examples/feature-demos/media-and-surfaces/.astro/dev.log +4 -14
- package/examples/feature-demos/media-and-surfaces/README.md +2 -2
- package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +9 -9
- package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +16 -16
- package/examples/feature-demos/media-and-surfaces/site/content.md +4 -7
- package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/010-media/content.md +6 -5
- package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/content.md +7 -6
- package/examples/feature-demos/media-and-surfaces/site/sitewide-content.yaml +0 -2
- package/examples/feature-demos/sitewide-content/README.md +3 -3
- package/examples/feature-demos/sitewide-content/site/content.md +8 -7
- package/examples/feature-demos/sitewide-content/site/pages/010-logo-and-navigation/content.md +17 -0
- package/examples/feature-demos/sitewide-content/site/{routes → pages}/020-notices/content.md +7 -6
- package/examples/feature-demos/sitewide-content/site/{routes → pages}/030-footer/content.md +6 -5
- package/examples/feature-demos/sitewide-content/site/sitewide-content.yaml +4 -13
- package/examples/feature-demos/theme-presets/README.md +1 -1
- package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +60 -60
- package/examples/feature-demos/theme-presets/site/content.md +4 -7
- package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/content.md +4 -7
- package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/content.md +4 -7
- package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/content.md +4 -7
- package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/content.md +4 -7
- package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +1 -10
- package/fixtures/basic/site/content.md +5 -6
- package/fixtures/basic/site/sitewide-content.yaml +1 -8
- package/fixtures/content-model-v2/site/content.md +11 -13
- package/fixtures/content-model-v2/site/pages/010-guide/content.md +18 -0
- package/fixtures/nested-pages/site/.norna/generated-images.json +10 -0
- package/fixtures/nested-pages/site/config.yaml +1 -0
- package/fixtures/nested-pages/site/content.md +15 -0
- package/fixtures/nested-pages/site/pages/010-guides/content.md +15 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/content.md +21 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/images/diagram.svg +21 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +14 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +11 -0
- package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +3 -0
- package/fixtures/nested-pages/site/pages/020-reference/content.md +10 -0
- package/fixtures/nested-pages/site/pages/020-reference/pages/010-installation/content.md +11 -0
- package/fixtures/nested-pages/site/theme.yaml +3 -0
- package/package.json +7 -4
- package/schemas/config.schema.json +5 -5
- package/schemas/content-frontmatter.schema.json +28 -63
- package/schemas/manifest.json +1 -0
- package/schemas/sitewide-content.schema.json +58 -132
- package/schemas/theme.schema.json +396 -86
- package/scripts/check-config.mjs +6 -5
- package/scripts/deploy-site.mjs +6 -8
- package/scripts/doctor.mjs +2 -2
- package/scripts/generate-images.mjs +11 -18
- package/scripts/generate-schemas.mjs +3 -217
- package/scripts/lib/base-path-redirect.mjs +9 -0
- package/scripts/lib/editor-language-service.mjs +126 -66
- package/scripts/lib/heading-ids.mjs +161 -0
- package/scripts/lib/navigation-model.mjs +51 -0
- package/scripts/lib/norna-markdown-blocks.mjs +3 -4
- package/scripts/lib/page-model.mjs +83 -0
- package/scripts/lib/project-config.mjs +28 -0
- package/scripts/lib/schema-definitions.mjs +41 -46
- package/scripts/lib/schema-editor-metadata.mjs +475 -0
- package/scripts/lib/schema-value-definitions.mjs +6 -6
- package/scripts/lib/site-content.mjs +124 -87
- package/scripts/lib/site-paths.mjs +2 -2
- package/scripts/lib/theme-config.mjs +9 -16
- package/scripts/lib/theme-presets.mjs +9 -1
- package/scripts/release.mjs +1 -1
- package/scripts/show-typography.mjs +26 -20
- package/scripts/sync-content-sections.mjs +106 -65
- package/scripts/{test-temporary-visibility.mjs → test-banner-visibility.mjs} +7 -24
- package/scripts/test-client-javascript.mjs +39 -42
- package/scripts/test-content-check.mjs +138 -54
- package/scripts/test-content-model-v2.mjs +445 -228
- package/scripts/test-documentation.mjs +6 -1
- package/scripts/test-editor-language-service.mjs +168 -24
- package/scripts/test-engine-commands.mjs +2 -1
- package/scripts/test-heading-ids.mjs +68 -0
- package/scripts/test-navigation-model.mjs +63 -0
- package/scripts/test-nested-pages.mjs +110 -0
- package/scripts/test-package-check.mjs +31 -40
- package/scripts/test-page-model.mjs +80 -0
- package/scripts/test-project-config.mjs +25 -1
- package/scripts/test-schemas.mjs +66 -14
- package/scripts/test-theme-presets.mjs +33 -24
- package/src/components/NavigationPageTree.astro +68 -0
- package/src/components/SectionNavigationScript.astro +110 -5
- package/src/components/SiteBreadcrumbs.astro +29 -0
- package/src/components/SiteNavigation.astro +65 -69
- package/src/components/SitePage.astro +70 -46
- package/src/components/SiteSection.astro +27 -20
- package/src/content.config.ts +9 -5
- package/src/layouts/BaseLayout.astro +11 -20
- package/src/lib/pageThemes.ts +42 -0
- package/src/lib/sectionContent.ts +109 -106
- package/src/lib/siteNavigation.ts +48 -0
- package/src/lib/sitePages.ts +85 -38
- package/src/pages/{[slug].astro → [...slug].astro} +2 -2
- package/src/styles/global.css +373 -55
- package/starters/basic/README.md +5 -7
- package/starters/basic/site/content.md +7 -4
- package/starters/basic/site/sitewide-content.yaml +1 -10
- package/starters/project/README.md +8 -8
- package/starters/project/site/content.md +5 -4
- package/starters/project/site/{routes → pages}/010-guide/content.md +5 -4
- package/starters/project/site/sitewide-content.yaml +1 -10
- package/docs/routes.md +0 -121
- package/examples/feature-demos/sitewide-content/site/routes/010-identity/content.md +0 -20
- package/fixtures/content-model-v2/site/routes/010-guide/content.md +0 -17
- package/scripts/lib/route-model.mjs +0 -17
- package/scripts/test-route-model.mjs +0 -32
- package/src/lib/routeThemes.ts +0 -41
- /package/examples/complete-sites/dog-shelter-multi-page/site/images/{intro/shelter-dog.jpg → shelter-dog.jpg} +0 -0
- /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rex.jpg +0 -0
- /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rover.jpg +0 -0
- /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/020-adopt/images/adopt → pages/020-adopt/images}/adoption-dog.jpg +0 -0
- /package/examples/complete-sites/dog-shelter-single-page/site/images/{you-can-help/foster-dog.jpg → foster-dog.jpg} +0 -0
- /package/examples/complete-sites/dog-shelter-single-page/site/images/{what-we-do/rescue-dog.jpg → rescue-dog.jpg} +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-carousel.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-stack.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-surfaces.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-one.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-three.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-two.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-one.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-two.svg +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/theme.yaml +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/puppy-portrait.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/shepherd-portrait.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/white-puppy.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/theme.yaml +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/black-schnauzer.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/retriever-portrait.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/schnauzer-portrait.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/theme.yaml +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/boxer-portrait.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grass-puppy.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grey-street-dog.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/theme.yaml +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/black-pedigree.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-grass.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-wide.jpg +0 -0
- /package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/theme.yaml +0 -0
- /package/fixtures/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
- /package/fixtures/content-model-v2/site/images/{card-section/adopt.svg → adopt.svg} +0 -0
- /package/fixtures/content-model-v2/site/images/{intro/detail.jpg → detail.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{intro/duplicate.jpg → duplicate.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{card-section/foster.svg → foster.svg} +0 -0
- /package/fixtures/content-model-v2/site/images/{intro/hero.jpg → hero.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{plain/image.jpg → image.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{timed/slide-one.jpg → slide-one.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{timed/slide-three.jpg → slide-three.jpg} +0 -0
- /package/fixtures/content-model-v2/site/images/{timed/slide-two.jpg → slide-two.jpg} +0 -0
- /package/fixtures/content-model-v2/site/{routes/010-guide/images/intro → pages/010-guide/images}/duplicate.jpg +0 -0
- /package/starters/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
|
@@ -35,34 +35,36 @@ interface Props {
|
|
|
35
35
|
pagePresentation: ResolvedPagePresentation;
|
|
36
36
|
visualTheme: VisualTheme;
|
|
37
37
|
sectionIndex: number;
|
|
38
|
-
headingLevel: 1 | 2;
|
|
39
38
|
prioritizeFirstImage?: boolean;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
const { section, pagePresentation, visualTheme, sectionIndex,
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const sectionHeadingVisualLevel = 'h2';
|
|
46
|
-
const
|
|
47
|
-
|
|
41
|
+
const { section, pagePresentation, visualTheme, sectionIndex, prioritizeFirstImage = false } = Astro.props;
|
|
42
|
+
const isPageTitle = section.headingLevel === 1;
|
|
43
|
+
const HeadingTag = section.headingLevel === 1 ? 'h1' : 'h2';
|
|
44
|
+
const sectionHeadingVisualLevel = section.headingLevel === 1 ? 'h1' : 'h2';
|
|
45
|
+
const headingId = section.id ?? 'page-title';
|
|
46
|
+
const firstContentBlock = section.contentBlocks[0];
|
|
47
|
+
const firstContentBlockIsVisual = firstContentBlock !== undefined
|
|
48
|
+
&& firstContentBlock.type !== 'html'
|
|
49
|
+
&& firstContentBlock.type !== 'note';
|
|
48
50
|
type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4';
|
|
49
51
|
const headingSizeValues: Record<HeadingLevel, Record<TextSize, ResponsiveValue<string>>> = {
|
|
50
52
|
h1: {
|
|
51
53
|
small: {
|
|
52
|
-
desktop: 'clamp(1.
|
|
53
|
-
mobile: 'clamp(1.
|
|
54
|
+
desktop: 'clamp(1.35rem, 1.2rem + 0.55vw, 1.7rem)',
|
|
55
|
+
mobile: 'clamp(1.32rem, 1.16rem + 0.8vw, 1.58rem)',
|
|
54
56
|
},
|
|
55
57
|
medium: {
|
|
56
|
-
desktop: 'clamp(1.
|
|
57
|
-
mobile: 'clamp(1.
|
|
58
|
+
desktop: 'clamp(1.65rem, 1.35rem + 1.1vw, 2.4rem)',
|
|
59
|
+
mobile: 'clamp(1.5rem, 1.22rem + 1.4vw, 2rem)',
|
|
58
60
|
},
|
|
59
61
|
large: {
|
|
60
|
-
desktop: 'clamp(
|
|
61
|
-
mobile: 'clamp(1.
|
|
62
|
+
desktop: 'clamp(1.95rem, 1.48rem + 1.7vw, 3.2rem)',
|
|
63
|
+
mobile: 'clamp(1.75rem, 1.32rem + 2.15vw, 2.5rem)',
|
|
62
64
|
},
|
|
63
65
|
xlarge: {
|
|
64
|
-
desktop: 'clamp(2.
|
|
65
|
-
mobile: 'clamp(
|
|
66
|
+
desktop: 'clamp(2.35rem, 1.58rem + 2.75vw, 4.4rem)',
|
|
67
|
+
mobile: 'clamp(2rem, 1.4rem + 3vw, 3.1rem)',
|
|
66
68
|
},
|
|
67
69
|
},
|
|
68
70
|
h2: {
|
|
@@ -168,7 +170,9 @@ const textWidthValues: Record<TextWidth, string> = {
|
|
|
168
170
|
normal: 'min(72ch, var(--text-width))',
|
|
169
171
|
wide: 'min(72ch, var(--image-layout-width))',
|
|
170
172
|
};
|
|
171
|
-
const surface =
|
|
173
|
+
const surface = isPageTitle
|
|
174
|
+
? pagePresentation.palette.surfaces.base
|
|
175
|
+
: resolveSectionSurface(pagePresentation, sectionIndex);
|
|
172
176
|
const typography = pagePresentation.typography.values;
|
|
173
177
|
const heading = typography.headings[sectionHeadingVisualLevel] as {
|
|
174
178
|
align: ResponsiveValue<TextAlign>;
|
|
@@ -249,6 +253,7 @@ for (const block of section.contentBlocks) {
|
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
flushReadingBlocks();
|
|
256
|
+
const firstMediaGroupIndex = contentGroups.findIndex((group) => group.type === 'media');
|
|
252
257
|
const markdownHeadingStyle = (level: 'h3' | 'h4') => {
|
|
253
258
|
const headingConfig = markdownHeadings[level];
|
|
254
259
|
const fontSize = getHeadingSizeValue(level, headingConfig.size);
|
|
@@ -298,10 +303,12 @@ const sectionStyle = [
|
|
|
298
303
|
---
|
|
299
304
|
|
|
300
305
|
<section
|
|
301
|
-
aria-labelledby={
|
|
306
|
+
aria-labelledby={headingId}
|
|
302
307
|
class:list={[
|
|
303
308
|
'site-section',
|
|
304
309
|
{
|
|
310
|
+
'site-section-page-title': isPageTitle,
|
|
311
|
+
'site-section-page-title-empty': isPageTitle && section.contentBlocks.length === 0,
|
|
305
312
|
'site-section-first-block-is-visual': firstContentBlockIsVisual,
|
|
306
313
|
'site-section-has-margin-notes': hasMarginNotes,
|
|
307
314
|
},
|
|
@@ -311,7 +318,7 @@ const sectionStyle = [
|
|
|
311
318
|
>
|
|
312
319
|
<div class="section-card">
|
|
313
320
|
<header class="section-header">
|
|
314
|
-
<HeadingTag id={
|
|
321
|
+
<HeadingTag id={headingId} set:html={section.titleHtml} />
|
|
315
322
|
</header>
|
|
316
323
|
|
|
317
324
|
<div class:list={['section-body', { 'section-body-has-media': hasMedia, 'section-body-text-only': !hasMedia }]}>
|
|
@@ -381,7 +388,7 @@ const sectionStyle = [
|
|
|
381
388
|
title={section.title}
|
|
382
389
|
images={block.images}
|
|
383
390
|
maxAvailableWidthPercent={visualTheme.images.maxAvailableWidthPercent}
|
|
384
|
-
priorityFirstImage={prioritizeFirstImage && groupIndex ===
|
|
391
|
+
priorityFirstImage={prioritizeFirstImage && groupIndex === firstMediaGroupIndex}
|
|
385
392
|
/>
|
|
386
393
|
);
|
|
387
394
|
}
|
|
@@ -395,7 +402,7 @@ const sectionStyle = [
|
|
|
395
402
|
title={section.title}
|
|
396
403
|
images={block.images}
|
|
397
404
|
maxAvailableWidthPercent={visualTheme.images.maxAvailableWidthPercent}
|
|
398
|
-
priorityFirstImage={prioritizeFirstImage && groupIndex ===
|
|
405
|
+
priorityFirstImage={prioritizeFirstImage && groupIndex === firstMediaGroupIndex}
|
|
399
406
|
/>
|
|
400
407
|
);
|
|
401
408
|
})}
|
package/src/content.config.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
siteThemePath,
|
|
9
9
|
sitewideContentPath,
|
|
10
10
|
} from '../scripts/lib/site-paths.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { encodePageDirectoryPath, parsePageDirectoryPath } from '../scripts/lib/page-model.mjs';
|
|
12
12
|
import {
|
|
13
13
|
siteSchema,
|
|
14
14
|
sitewideSchema,
|
|
@@ -23,16 +23,20 @@ const siteThemeSchema = themeVisualSchema;
|
|
|
23
23
|
|
|
24
24
|
const site = defineCollection({
|
|
25
25
|
loader: glob({
|
|
26
|
-
pattern: ['content.md', '
|
|
26
|
+
pattern: ['content.md', 'pages/**/content.md'],
|
|
27
27
|
base: pathToFileURL(siteDir),
|
|
28
28
|
generateId: ({ entry }) => {
|
|
29
29
|
if (entry === 'content.md') {
|
|
30
30
|
return siteEntryId;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const pageEntryDirectory = entry.split('/').slice(1, -1).join('/');
|
|
34
|
+
const pageDirectory = pageEntryDirectory
|
|
35
|
+
.split('/')
|
|
36
|
+
.filter((segment) => segment !== 'pages')
|
|
37
|
+
.join('/pages/');
|
|
38
|
+
return pageDirectory
|
|
39
|
+
? `${siteEntryId.replace(/-content$/, '')}-page-${encodePageDirectoryPath(parsePageDirectoryPath(pageDirectory, `page directory pages/${pageEntryDirectory}`).pageDirectory)}`
|
|
36
40
|
: entry.replace(/[^a-zA-Z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
37
41
|
},
|
|
38
42
|
}),
|
|
@@ -39,7 +39,7 @@ type VisualTheme = {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
interface Props {
|
|
42
|
-
title
|
|
42
|
+
title: string;
|
|
43
43
|
description?: string;
|
|
44
44
|
presentation: Presentation;
|
|
45
45
|
visualTheme: VisualTheme;
|
|
@@ -47,22 +47,13 @@ interface Props {
|
|
|
47
47
|
pathname?: string;
|
|
48
48
|
footer?: {
|
|
49
49
|
copyrightMessage?: string;
|
|
50
|
-
buildInfo?:
|
|
51
|
-
enabled: boolean;
|
|
52
|
-
text: string;
|
|
53
|
-
dateTimeFormat: {
|
|
54
|
-
locale: string;
|
|
55
|
-
timeZone: string;
|
|
56
|
-
dateStyle: string;
|
|
57
|
-
timeStyle: string;
|
|
58
|
-
};
|
|
59
|
-
};
|
|
50
|
+
buildInfo?: boolean;
|
|
60
51
|
};
|
|
61
52
|
}
|
|
62
53
|
|
|
63
54
|
const {
|
|
64
|
-
title
|
|
65
|
-
description
|
|
55
|
+
title,
|
|
56
|
+
description,
|
|
66
57
|
presentation,
|
|
67
58
|
visualTheme,
|
|
68
59
|
frameColors,
|
|
@@ -103,12 +94,12 @@ const documentStyle = [
|
|
|
103
94
|
`--image-max-height-mobile: ${formatViewportHeightPercent(visualTheme.images.maxAvailableHeightPercent.mobile)}`,
|
|
104
95
|
].join('; ');
|
|
105
96
|
const { buildInfo, copyrightMessage } = footer ?? {};
|
|
106
|
-
const showBuildInfo =
|
|
97
|
+
const showBuildInfo = buildInfo === true;
|
|
107
98
|
const buildTime = showBuildInfo
|
|
108
|
-
? new Intl.DateTimeFormat(
|
|
109
|
-
dateStyle:
|
|
110
|
-
timeStyle:
|
|
111
|
-
timeZone:
|
|
99
|
+
? new Intl.DateTimeFormat(projectConfig.locale.lang, {
|
|
100
|
+
dateStyle: 'short',
|
|
101
|
+
timeStyle: 'short',
|
|
102
|
+
timeZone: 'UTC',
|
|
112
103
|
}).format(new Date())
|
|
113
104
|
: null;
|
|
114
105
|
const showFooter = Boolean(copyrightMessage || showBuildInfo);
|
|
@@ -124,7 +115,7 @@ const footerStyle = [
|
|
|
124
115
|
<meta charset="utf-8" />
|
|
125
116
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
126
117
|
<meta name="format-detection" content="telephone=no" />
|
|
127
|
-
<meta name="description" content={description} />
|
|
118
|
+
{description && <meta name="description" content={description} />}
|
|
128
119
|
<meta name="theme-color" content={presentation.palette.page.backgroundColor} />
|
|
129
120
|
<link rel="canonical" href={canonicalUrl} />
|
|
130
121
|
<meta name="generator" content={Astro.generator} />
|
|
@@ -144,7 +135,7 @@ const footerStyle = [
|
|
|
144
135
|
<footer class="site-footer" style={footerStyle}>
|
|
145
136
|
<p>
|
|
146
137
|
{copyrightMessage}
|
|
147
|
-
{showBuildInfo && <span>{
|
|
138
|
+
{showBuildInfo && <span>{projectConfig.locale.labels.built} {buildTime}</span>}
|
|
148
139
|
</p>
|
|
149
140
|
</footer>
|
|
150
141
|
)}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
validatePageThemeYamlStructure,
|
|
5
|
+
} from '../../scripts/lib/site-content.mjs';
|
|
6
|
+
import { sitePagesDir, sitePagesLabel } from '../../scripts/lib/site-paths.mjs';
|
|
7
|
+
import { parseYamlConfig } from '../../scripts/lib/yaml-config.mjs';
|
|
8
|
+
import { getPageDirectoryAncestors } from '../../scripts/lib/page-model.mjs';
|
|
9
|
+
|
|
10
|
+
type PageTheme = {
|
|
11
|
+
id: string;
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getPageTheme = async (pageDirectory: string | null): Promise<PageTheme | null> => {
|
|
16
|
+
if (!pageDirectory) return null;
|
|
17
|
+
|
|
18
|
+
const pageAncestors = getPageDirectoryAncestors(pageDirectory).reverse();
|
|
19
|
+
for (const pageAncestor of pageAncestors) {
|
|
20
|
+
const themeSegments = pageAncestor.split('/');
|
|
21
|
+
const themePath = path.join(sitePagesDir, ...themeSegments, 'theme.yaml');
|
|
22
|
+
const source = await readFile(themePath, 'utf8').catch((error) => {
|
|
23
|
+
if (error?.code === 'ENOENT') return null;
|
|
24
|
+
throw error;
|
|
25
|
+
});
|
|
26
|
+
if (!source) continue;
|
|
27
|
+
const themeLabel = `${sitePagesLabel}/${themeSegments.join('/')}/theme.yaml`;
|
|
28
|
+
|
|
29
|
+
const data = parseYamlConfig(source, themeLabel, {
|
|
30
|
+
validateStructure: validatePageThemeYamlStructure,
|
|
31
|
+
});
|
|
32
|
+
if (Object.hasOwn(data, 'navigation')) {
|
|
33
|
+
throw new Error(`${themeLabel} may not define navigation. Set the site-wide navigation mode in the root theme.yaml.`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
id: `pages/${themeSegments.join('/')}/theme`,
|
|
37
|
+
data,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { CollectionEntry } from 'astro:content';
|
|
2
1
|
import { markdownToHtml } from 'satteri';
|
|
3
2
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
|
+
import {
|
|
4
|
+
formatHeadingIdentifierIssue,
|
|
5
|
+
getHeadingIdentifierIssues,
|
|
6
|
+
} from '../../scripts/lib/heading-ids.mjs';
|
|
7
|
+
import { getBodySections } from '../../scripts/lib/site-content.mjs';
|
|
4
8
|
import {
|
|
5
9
|
extractInlineNoteDiagnostics,
|
|
6
10
|
extractNornaMarkdownBlocks,
|
|
@@ -10,8 +14,6 @@ import {
|
|
|
10
14
|
import { applyBasePathToHtml } from './basePath';
|
|
11
15
|
import type { SitePage } from './sitePages';
|
|
12
16
|
|
|
13
|
-
type SiteSectionMetadataMap = CollectionEntry<'site'>['data']['sections'];
|
|
14
|
-
type SiteSectionMetadata = SiteSectionMetadataMap[string];
|
|
15
17
|
type ManagedImage = {
|
|
16
18
|
image: string;
|
|
17
19
|
src: string;
|
|
@@ -42,10 +44,11 @@ type SectionContentBlock =
|
|
|
42
44
|
| { type: 'image-carousel'; images: ManagedImage[] }
|
|
43
45
|
| { type: 'card-list'; layout: CardListLayout; flow: CardListFlow; size: CardListSize; width: CardListWidth; cards: CardListItem[] }
|
|
44
46
|
| { type: 'note'; html: string; number?: number; id?: string; referenceId?: string };
|
|
45
|
-
export type ResolvedSection =
|
|
46
|
-
id: string;
|
|
47
|
+
export type ResolvedSection = {
|
|
48
|
+
id: string | null;
|
|
47
49
|
title: string;
|
|
48
50
|
titleHtml: string;
|
|
51
|
+
headingLevel: 1 | 2;
|
|
49
52
|
contentBlocks: SectionContentBlock[];
|
|
50
53
|
};
|
|
51
54
|
|
|
@@ -54,9 +57,13 @@ export type SectionNavigation = {
|
|
|
54
57
|
title: string;
|
|
55
58
|
};
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
export type HeadingNavigation = SectionNavigation & {
|
|
61
|
+
depth: 2 | 3;
|
|
62
|
+
parentId: string | null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const contentHeadingRegex = /<h([12])\b([^>]*)>([\s\S]*?)<\/h\1>/gi;
|
|
58
66
|
const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
|
|
59
|
-
const markdownH2Regex = /^##\s+.*$/gm;
|
|
60
67
|
const imageProvenanceCommentRegex = /<!--\s*norna-image-provenance:[\s\S]*?-->/gi;
|
|
61
68
|
|
|
62
69
|
const stripTags = (html: string) => html.replace(/<[^>]*>/g, '');
|
|
@@ -69,69 +76,43 @@ const decodeHtmlEntities = (value: string) =>
|
|
|
69
76
|
.replace(/"/g, '"')
|
|
70
77
|
.replace(/'/g, "'");
|
|
71
78
|
|
|
72
|
-
const slugify = (value: string) =>
|
|
73
|
-
decodeHtmlEntities(stripTags(value))
|
|
74
|
-
.trim()
|
|
75
|
-
.toLowerCase()
|
|
76
|
-
.normalize('NFD')
|
|
77
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
78
|
-
.replace(/å/g, 'a')
|
|
79
|
-
.replace(/ä/g, 'a')
|
|
80
|
-
.replace(/ö/g, 'o')
|
|
81
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
82
|
-
.replace(/^-+|-+$/g, '');
|
|
83
|
-
|
|
84
|
-
const getHeadingId = (attributes: string, headingHtml: string) => {
|
|
85
|
-
const headingText = decodeHtmlEntities(stripTags(headingHtml)).trim();
|
|
86
|
-
const explicitId = headingText.match(explicitHeadingIdRegex)?.[1];
|
|
87
|
-
if (explicitId) return explicitId;
|
|
88
|
-
|
|
89
|
-
const id = attributes.match(/\sid=(["'])(.*?)\1/i)?.[2];
|
|
90
|
-
return id ? decodeHtmlEntities(id) : slugify(headingHtml);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const getExplicitHeadingId = (headingHtml: string) =>
|
|
94
|
-
decodeHtmlEntities(stripTags(headingHtml)).trim().match(explicitHeadingIdRegex)?.[1];
|
|
95
|
-
|
|
96
79
|
const getHeadingTitle = (headingHtml: string) =>
|
|
97
80
|
decodeHtmlEntities(stripTags(headingHtml)).replace(explicitHeadingIdRegex, '').trim();
|
|
98
81
|
|
|
99
82
|
const getHeadingTitleHtml = (headingHtml: string) =>
|
|
100
83
|
prepareContentHtml(headingHtml.replace(explicitHeadingIdRegex, '').trim());
|
|
101
84
|
|
|
102
|
-
export const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return { id, title };
|
|
85
|
+
export const getHeadingNavigation = async (
|
|
86
|
+
markdown: string,
|
|
87
|
+
sourceLabel = 'Markdown',
|
|
88
|
+
): Promise<HeadingNavigation[]> => {
|
|
89
|
+
const bodyStructure = await getBodySections(markdown);
|
|
90
|
+
const issues = getHeadingIdentifierIssues(bodyStructure.headings);
|
|
91
|
+
if (issues.length > 0) throw new Error(formatHeadingIdentifierIssue(issues[0], sourceLabel));
|
|
92
|
+
|
|
93
|
+
let parentId: string | null = null;
|
|
94
|
+
return bodyStructure.headings.flatMap((heading) => {
|
|
95
|
+
if (heading.depth === 1 || !heading.id) return [];
|
|
96
|
+
if (heading.depth === 2) parentId = heading.id;
|
|
97
|
+
|
|
98
|
+
return [{
|
|
99
|
+
depth: heading.depth,
|
|
100
|
+
id: heading.id,
|
|
101
|
+
parentId: heading.depth === 3 ? parentId : null,
|
|
102
|
+
title: heading.title,
|
|
103
|
+
}];
|
|
122
104
|
});
|
|
123
|
-
|
|
124
|
-
for (const id of Object.keys(sectionMetadata)) {
|
|
125
|
-
if (!sectionIds.has(id)) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`Section metadata "${id}" does not match any Markdown section. Add "## Heading {#${id}}" or remove sections.${id}.`,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return sections;
|
|
133
105
|
};
|
|
134
106
|
|
|
107
|
+
export const getSectionNavigation = async (
|
|
108
|
+
markdown: string,
|
|
109
|
+
sourceLabel = 'Markdown',
|
|
110
|
+
): Promise<SectionNavigation[]> => (
|
|
111
|
+
(await getHeadingNavigation(markdown, sourceLabel))
|
|
112
|
+
.filter((heading) => heading.depth === 2)
|
|
113
|
+
.map(({ id, title }) => ({ id, title }))
|
|
114
|
+
);
|
|
115
|
+
|
|
135
116
|
const stripImageProvenanceComments = (html: string) => html.replace(imageProvenanceCommentRegex, '');
|
|
136
117
|
|
|
137
118
|
const prepareContentHtml = (html: string) =>
|
|
@@ -231,29 +212,44 @@ const applyInlineNoteMarkup = (html: string, notes: InlineNote[]) => {
|
|
|
231
212
|
});
|
|
232
213
|
};
|
|
233
214
|
|
|
234
|
-
const getRawMarkdownSections = (
|
|
235
|
-
const matches = Array.from(markdown.matchAll(markdownH2Regex));
|
|
215
|
+
const getRawMarkdownSections = (bodySections: Awaited<ReturnType<typeof getBodySections>>['sections']) => {
|
|
236
216
|
const sections = new Map<string, string>();
|
|
237
217
|
|
|
238
|
-
for (
|
|
239
|
-
const
|
|
240
|
-
const heading = match[0] ?? '';
|
|
241
|
-
const id = heading.match(explicitHeadingIdRegex)?.[1];
|
|
218
|
+
for (const section of bodySections) {
|
|
219
|
+
const id = section.isPageTitle ? '__page-title' : section.id;
|
|
242
220
|
if (!id) continue;
|
|
243
|
-
|
|
244
|
-
const start = match.index ?? 0;
|
|
245
|
-
const next = matches[index + 1];
|
|
246
|
-
const end = next?.index ?? markdown.length;
|
|
247
|
-
sections.set(id, markdown.slice(start, end));
|
|
221
|
+
sections.set(id, section.text);
|
|
248
222
|
}
|
|
249
223
|
|
|
250
224
|
return sections;
|
|
251
225
|
};
|
|
252
226
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
227
|
+
const applyH3HeadingIds = (
|
|
228
|
+
html: string,
|
|
229
|
+
headings: Awaited<ReturnType<typeof getBodySections>>['headings'],
|
|
230
|
+
) => {
|
|
231
|
+
let headingIndex = 0;
|
|
232
|
+
const result = html.replace(/<h3\b([^>]*)>([\s\S]*?)<\/h3>/gi, (_match, attributes, headingHtml) => {
|
|
233
|
+
const heading = headings[headingIndex];
|
|
234
|
+
headingIndex += 1;
|
|
235
|
+
if (!heading?.id) throw new Error('Rendered Markdown H3 could not be matched to its heading id.');
|
|
236
|
+
|
|
237
|
+
const cleanAttributes = String(attributes).replace(/\s+id=(["']).*?\1/gi, '');
|
|
238
|
+
const cleanHeadingHtml = String(headingHtml).replace(explicitHeadingIdRegex, '').trim();
|
|
239
|
+
return `<h3${cleanAttributes} id="${heading.id}">${cleanHeadingHtml}</h3>`;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (headingIndex !== headings.length) {
|
|
243
|
+
throw new Error('Markdown H3 headings could not be matched to the rendered page content.');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return result;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const getImageSourceKey = (page: SitePage, image: string) => (
|
|
250
|
+
page.pageDirectory
|
|
251
|
+
? `pages/${page.pageDirectory}/images/${image}`
|
|
252
|
+
: `images/${image}`
|
|
257
253
|
);
|
|
258
254
|
|
|
259
255
|
const renderInlineNoteMarkdown = async (markdown: string) => {
|
|
@@ -275,7 +271,6 @@ const resolveContentBlocks = async (
|
|
|
275
271
|
html: string,
|
|
276
272
|
rawMarkdown: string,
|
|
277
273
|
page: SitePage,
|
|
278
|
-
sectionId: string,
|
|
279
274
|
inlineNotes: InlineNote[] = [],
|
|
280
275
|
) => {
|
|
281
276
|
const rawBlocks = extractNornaMarkdownBlocks(rawMarkdown);
|
|
@@ -323,7 +318,7 @@ const resolveContentBlocks = async (
|
|
|
323
318
|
width: block.width,
|
|
324
319
|
cards: block.cards.map((card: CardListItem) => ({
|
|
325
320
|
...card,
|
|
326
|
-
...(card.image ? { src: getImageSourceKey(page,
|
|
321
|
+
...(card.image ? { src: getImageSourceKey(page, card.image) } : {}),
|
|
327
322
|
})),
|
|
328
323
|
});
|
|
329
324
|
continue;
|
|
@@ -333,7 +328,7 @@ const resolveContentBlocks = async (
|
|
|
333
328
|
type: block.type,
|
|
334
329
|
images: block.images.map((image: { image: string; alt?: string; caption?: string }) => ({
|
|
335
330
|
...image,
|
|
336
|
-
src: getImageSourceKey(page,
|
|
331
|
+
src: getImageSourceKey(page, image.image),
|
|
337
332
|
})),
|
|
338
333
|
});
|
|
339
334
|
}
|
|
@@ -344,38 +339,54 @@ const resolveContentBlocks = async (
|
|
|
344
339
|
export const getSectionsContent = async (
|
|
345
340
|
html: string,
|
|
346
341
|
rawMarkdown: string,
|
|
347
|
-
sectionMetadata: SiteSectionMetadataMap,
|
|
348
342
|
page: SitePage,
|
|
343
|
+
sourceLabel = 'Markdown',
|
|
349
344
|
) => {
|
|
350
|
-
const matches = Array.from(html.matchAll(
|
|
351
|
-
const
|
|
352
|
-
const
|
|
345
|
+
const matches = Array.from(html.matchAll(contentHeadingRegex));
|
|
346
|
+
const bodyStructure = await getBodySections(rawMarkdown);
|
|
347
|
+
const headingIdentifierIssues = getHeadingIdentifierIssues(bodyStructure.headings);
|
|
348
|
+
if (headingIdentifierIssues.length > 0) {
|
|
349
|
+
throw new Error(formatHeadingIdentifierIssue(headingIdentifierIssues[0], sourceLabel));
|
|
350
|
+
}
|
|
351
|
+
const rawSections = getRawMarkdownSections(bodyStructure.sections);
|
|
353
352
|
const sections: ResolvedSection[] = [];
|
|
354
353
|
const sectionIds = new Set<string>();
|
|
355
354
|
let nextNoteNumber = 1;
|
|
355
|
+
const pageHeadingCount = matches.filter((match) => match[1] === '1').length;
|
|
356
|
+
|
|
357
|
+
if (pageHeadingCount !== 1 || matches[0]?.[1] !== '1') {
|
|
358
|
+
throw new Error('Each Norna page must start with exactly one Markdown H1 page title, for example "# About".');
|
|
359
|
+
}
|
|
356
360
|
|
|
357
361
|
for (let index = 0; index < matches.length; index += 1) {
|
|
358
362
|
const match = matches[index];
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
+
const bodySection = bodyStructure.sections[index];
|
|
364
|
+
const headingLevel = Number.parseInt(match[1] ?? '', 10) as 1 | 2;
|
|
365
|
+
const headingHtml = match[3] ?? '';
|
|
366
|
+
if (!bodySection || bodySection.level !== headingLevel) {
|
|
367
|
+
throw new Error('Markdown H1/H2 headings could not be matched to the rendered page content.');
|
|
368
|
+
}
|
|
369
|
+
const id = bodySection.id;
|
|
363
370
|
const contentStart = (match.index ?? 0) + match[0].length;
|
|
364
371
|
const nextMatch = matches[index + 1];
|
|
365
372
|
const contentEnd = nextMatch?.index ?? html.length;
|
|
366
|
-
const
|
|
373
|
+
const nextBodySection = bodyStructure.sections[index + 1];
|
|
374
|
+
const h3Headings = bodyStructure.headings.filter((heading) => (
|
|
375
|
+
heading.depth === 3
|
|
376
|
+
&& heading.index > bodySection.index
|
|
377
|
+
&& heading.index < (nextBodySection?.index ?? Number.POSITIVE_INFINITY)
|
|
378
|
+
));
|
|
379
|
+
const content = applyH3HeadingIds(html.slice(contentStart, contentEnd).trim(), h3Headings);
|
|
367
380
|
const title = getHeadingTitle(headingHtml);
|
|
368
381
|
|
|
369
|
-
if (!
|
|
370
|
-
throw new Error(`Section heading "${title}" is missing an explicit id. Write it as: ## ${title} {#${id}}`);
|
|
371
|
-
}
|
|
382
|
+
if (headingLevel === 2 && !id) throw new Error(`Section heading "${title}" cannot produce an automatic ASCII id.`);
|
|
372
383
|
|
|
373
|
-
if (sectionIds.has(id)) {
|
|
384
|
+
if (id && sectionIds.has(id)) {
|
|
374
385
|
throw new Error(`Duplicate Markdown section heading id: ${id}`);
|
|
375
386
|
}
|
|
376
387
|
|
|
377
|
-
sectionIds.add(id);
|
|
378
|
-
const rawSection = rawSections.get(id) ?? '';
|
|
388
|
+
if (id) sectionIds.add(id);
|
|
389
|
+
const rawSection = rawSections.get(id ?? '__page-title') ?? '';
|
|
379
390
|
const inlineNoteDiagnostics = extractInlineNoteDiagnostics(rawSection, {
|
|
380
391
|
label: 'Markdown section',
|
|
381
392
|
});
|
|
@@ -386,8 +397,8 @@ export const getSectionsContent = async (
|
|
|
386
397
|
const inlineNotes = inlineNoteDiagnostics.notes.map((note) => {
|
|
387
398
|
const number = nextNoteNumber;
|
|
388
399
|
nextNoteNumber += 1;
|
|
389
|
-
const pageKey = page.
|
|
390
|
-
const noteKey = `${pageKey}-${id}-${number}`;
|
|
400
|
+
const pageKey = page.pageId || 'home';
|
|
401
|
+
const noteKey = `${pageKey}-${id ?? 'page-title'}-${number}`;
|
|
391
402
|
return {
|
|
392
403
|
...note,
|
|
393
404
|
number,
|
|
@@ -397,21 +408,13 @@ export const getSectionsContent = async (
|
|
|
397
408
|
});
|
|
398
409
|
|
|
399
410
|
sections.push({
|
|
400
|
-
...(sectionMetadata[id] ?? {}),
|
|
401
411
|
id,
|
|
402
412
|
title,
|
|
403
413
|
titleHtml: getHeadingTitleHtml(headingHtml),
|
|
404
|
-
|
|
414
|
+
headingLevel,
|
|
415
|
+
contentBlocks: await resolveContentBlocks(content, rawSection, page, inlineNotes),
|
|
405
416
|
});
|
|
406
417
|
}
|
|
407
418
|
|
|
408
|
-
for (const id of metadataIds) {
|
|
409
|
-
if (!sectionIds.has(id)) {
|
|
410
|
-
throw new Error(
|
|
411
|
-
`Section metadata "${id}" does not match any Markdown section. Add "## Heading {#${id}}" or remove sections.${id}.`,
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
419
|
return sections;
|
|
417
420
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SectionNavigation } from './sectionContent';
|
|
2
|
+
import type { SitePage } from './sitePages';
|
|
3
|
+
|
|
4
|
+
export type PageNavigationEntry = {
|
|
5
|
+
page: SitePage;
|
|
6
|
+
sections: SectionNavigation[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type PageNavigationNode = PageNavigationEntry & {
|
|
10
|
+
children: PageNavigationNode[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const getPageNavigationTree = (entries: PageNavigationEntry[]) => {
|
|
14
|
+
const nodes = new Map<string, PageNavigationNode>();
|
|
15
|
+
const roots: PageNavigationNode[] = [];
|
|
16
|
+
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
nodes.set(entry.page.pagePath, { ...entry, children: [] });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const node = nodes.get(entry.page.pagePath);
|
|
23
|
+
if (!node) continue;
|
|
24
|
+
|
|
25
|
+
if (entry.page.isHome || entry.page.parentPagePath === null) {
|
|
26
|
+
roots.push(node);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const parent = nodes.get(entry.page.parentPagePath);
|
|
31
|
+
if (!parent) {
|
|
32
|
+
throw new Error(`Page "${entry.page.pathname}" has no parent page "${entry.page.parentPagePath}".`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
parent.children.push(node);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return roots;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const getListedPageNavigationTree = (entries: PageNavigationEntry[]) => {
|
|
42
|
+
const filterListed = (nodes: PageNavigationNode[]): PageNavigationNode[] => nodes.flatMap((node) => {
|
|
43
|
+
if (!node.page.isHome && !node.page.navigation.listed) return [];
|
|
44
|
+
return [{ ...node, children: filterListed(node.children) }];
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return filterListed(getPageNavigationTree(entries));
|
|
48
|
+
};
|