@janga/norna 0.7.9 → 0.7.10
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 +1 -1
- package/docs/README.md +14 -11
- package/docs/configuration.md +9 -54
- package/docs/content.md +6 -7
- package/docs/design/norna-diagram-design.md +2 -0
- package/docs/getting-started.md +18 -16
- package/docs/routes.md +7 -6
- package/docs/site-structure.md +4 -2
- package/docs/sitewide-content.md +74 -0
- package/docs/theme.md +61 -128
- package/docs/typography.md +45 -33
- package/examples/001-dog-shelter-single-page/README.md +9 -0
- package/examples/001-dog-shelter-single-page/site/config.mjs +27 -0
- package/examples/001-dog-shelter-single-page/site/content.md +38 -0
- package/examples/001-dog-shelter-single-page/site/images/what-we-do/dog-house.svg +25 -0
- package/examples/001-dog-shelter-single-page/site/images/you-can-help/heart.svg +13 -0
- package/examples/001-dog-shelter-single-page/site/public/favicon.svg +10 -0
- package/examples/001-dog-shelter-single-page/site/public/robots.txt +2 -0
- package/examples/001-dog-shelter-single-page/site/sitewide-content.md +6 -0
- package/examples/001-dog-shelter-single-page/site/theme.md +25 -0
- package/examples/002-dog-shelter-multi-page/README.md +9 -0
- package/examples/002-dog-shelter-multi-page/site/config.mjs +27 -0
- package/examples/002-dog-shelter-multi-page/site/content.md +12 -0
- package/examples/002-dog-shelter-multi-page/site/public/favicon.svg +10 -0
- package/examples/002-dog-shelter-multi-page/site/public/robots.txt +2 -0
- package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.svg +19 -0
- package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.svg +19 -0
- package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/route-content.md +30 -0
- package/examples/002-dog-shelter-multi-page/site/routes/020-adopt/route-content.md +12 -0
- package/examples/002-dog-shelter-multi-page/site/sitewide-content.md +6 -0
- package/examples/002-dog-shelter-multi-page/site/theme.md +25 -0
- package/examples/dog-gallery/.astro/collections/site.schema.json +776 -0
- package/examples/dog-gallery/.astro/collections/theme.schema.json +672 -0
- package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
- package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
- package/examples/dog-gallery/.astro/content.d.ts +175 -0
- package/examples/dog-gallery/.astro/data-store.json +1 -0
- package/examples/dog-gallery/.astro/dev-local.json +8 -0
- package/examples/dog-gallery/.astro/dev.json +13 -0
- package/examples/dog-gallery/.astro/dev.log +95 -0
- package/examples/dog-gallery/.astro/settings.json +5 -0
- package/examples/dog-gallery/.astro/types.d.ts +2 -0
- package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
- package/examples/dog-gallery/dist/_astro/SitePage.DicGh_5N.css +1 -0
- package/examples/dog-gallery/dist/dog-care/index.html +15 -0
- package/examples/dog-gallery/dist/dog-shelter-logo.svg +14 -0
- package/examples/dog-gallery/dist/favicon.svg +7 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
- package/examples/dog-gallery/dist/index.html +45 -0
- package/examples/dog-gallery/dist/robots.txt +2 -0
- package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +7 -7
- package/examples/dog-gallery/site/.norna/generated-images.json +8 -8
- package/examples/dog-gallery/site/config.mjs +0 -20
- package/examples/dog-gallery/site/content.md +4 -5
- package/examples/dog-gallery/site/public/logo.svg +14 -0
- package/examples/dog-gallery/site/sitewide-content.md +16 -0
- package/examples/dog-gallery/site/theme.md +13 -24
- package/examples/presentation/README.md +15 -0
- package/examples/presentation/site/.norna/generated-images.json +66 -0
- package/examples/presentation/site/config.mjs +29 -0
- package/examples/presentation/site/content.md +67 -0
- package/examples/presentation/site/public/robots.txt +2 -0
- package/examples/presentation/site/routes/010-presets/route-content.md +63 -0
- package/examples/presentation/site/routes/010-presets/theme.md +4 -0
- package/examples/presentation/site/routes/020-media/images/cards/card-carousel.svg +11 -0
- package/examples/presentation/site/routes/020-media/images/cards/card-stack.svg +9 -0
- package/examples/presentation/site/routes/020-media/images/cards/card-surfaces.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/carousel/carousel-one.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/carousel/carousel-three.svg +13 -0
- package/examples/presentation/site/routes/020-media/images/carousel/carousel-two.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/stack/stack-one.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/stack/stack-two.svg +9 -0
- package/examples/presentation/site/routes/020-media/route-content.md +77 -0
- package/examples/presentation/site/routes/020-media/theme.md +4 -0
- package/examples/presentation/site/routes/030-surfaces/route-content.md +44 -0
- package/examples/presentation/site/routes/030-surfaces/theme.md +7 -0
- package/examples/presentation/site/sitewide-content.md +6 -0
- package/examples/presentation/site/theme.md +43 -0
- package/examples/typography-presets/.astro/collections/routeTheme.schema.json +746 -0
- package/examples/typography-presets/.astro/collections/site.schema.json +0 -717
- package/examples/typography-presets/.astro/collections/sitewide.schema.json +114 -0
- package/examples/typography-presets/.astro/collections/theme.schema.json +161 -59
- package/examples/typography-presets/.astro/content.d.ts +8 -0
- package/examples/typography-presets/.astro/data-store.json +1 -1
- package/examples/typography-presets/.astro/dev-local.json +2 -2
- package/examples/typography-presets/.astro/dev.json +2 -2
- package/examples/typography-presets/.astro/dev.log +37 -16
- package/examples/typography-presets/site/.norna/generated-images.json +76 -76
- package/examples/typography-presets/site/config.mjs +0 -13
- package/examples/typography-presets/site/content.md +8 -126
- package/examples/typography-presets/site/routes/010-quiet-gallery/route-content.md +34 -0
- package/examples/typography-presets/site/routes/010-quiet-gallery/theme.md +4 -0
- package/examples/typography-presets/site/routes/020-compact-gallery/route-content.md +33 -0
- package/examples/typography-presets/site/routes/020-compact-gallery/theme.md +4 -0
- package/examples/typography-presets/site/routes/030-text-forward/route-content.md +34 -0
- package/examples/typography-presets/site/routes/030-text-forward/theme.md +4 -0
- package/examples/typography-presets/site/routes/040-statement/route-content.md +34 -0
- package/examples/typography-presets/site/routes/040-statement/theme.md +4 -0
- package/examples/typography-presets/site/sitewide-content.md +14 -0
- package/examples/typography-presets/site/theme.md +9 -9
- package/fixtures/basic/site/config.mjs +0 -13
- package/fixtures/basic/site/sitewide-content.md +12 -0
- package/fixtures/basic/site/theme.md +2 -2
- package/fixtures/content-model-v2/site/content.md +2 -5
- package/fixtures/content-model-v2/site/theme.md +1 -3
- package/package.json +3 -2
- package/scripts/check-config.mjs +22 -1
- package/scripts/lib/frontmatter-yaml.mjs +28 -0
- package/scripts/lib/logo-assets.mjs +35 -0
- package/scripts/lib/norna-markdown-blocks.mjs +166 -0
- package/scripts/lib/presentation.mjs +104 -46
- package/scripts/lib/project-config.mjs +67 -105
- package/scripts/lib/site-content.mjs +26 -118
- package/scripts/lib/site-paths.mjs +2 -0
- package/scripts/lib/sitewide-config.mjs +34 -0
- package/scripts/lib/theme-config.mjs +72 -8
- package/scripts/lib/typography.mjs +20 -26
- package/scripts/show-typography.mjs +40 -147
- package/scripts/sync-content-sections.mjs +19 -17
- package/scripts/test-content-check.mjs +59 -27
- package/scripts/test-content-model-v2.mjs +154 -0
- package/scripts/test-engine-commands.mjs +3 -1
- package/scripts/test-package-check.mjs +66 -24
- package/scripts/test-temporary-visibility.mjs +19 -2
- package/src/components/CardList.astro +5 -2
- package/src/components/GalleryGrid.astro +37 -16
- package/src/components/SiteBanners.astro +100 -0
- package/src/components/SiteNavigation.astro +48 -31
- package/src/components/SitePage.astro +55 -13
- package/src/components/SiteSection.astro +193 -35
- package/src/content.config.ts +78 -48
- package/src/layouts/BaseLayout.astro +64 -14
- package/src/lib/routeThemes.ts +61 -0
- package/src/lib/sectionContent.ts +235 -51
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/styles/global.css +282 -93
- package/starters/basic/.github/workflows/deploy.yml +1 -1
- package/starters/basic/README.md +7 -6
- package/starters/basic/site/config.mjs +0 -13
- package/starters/basic/site/content.md +1 -1
- package/starters/basic/site/sitewide-content.md +14 -0
- package/starters/basic/site/theme.md +13 -24
- package/starters/project/.github/workflows/deploy.yml +1 -1
- package/starters/project/README.md +10 -8
- package/starters/project/site/config.mjs +0 -13
- package/starters/project/site/sitewide-content.md +14 -0
- package/starters/project/site/theme.md +3 -11
- /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/puppy-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/shepherd-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/white-puppy.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/black-schnauzer.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/retriever-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/schnauzer-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/boxer-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/grass-puppy.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/grey-street-dog.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/black-pedigree.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/sheepdog-grass.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/sheepdog-wide.jpg +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CollectionEntry } from 'astro:content';
|
|
2
|
+
import { markdownToHtml } from 'satteri';
|
|
2
3
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
3
4
|
import {
|
|
5
|
+
extractInlineNoteDiagnostics,
|
|
4
6
|
extractNornaMarkdownBlocks,
|
|
5
7
|
splitNornaMarkdownBlockMarkers,
|
|
6
8
|
splitNornaRenderedCodeBlocks,
|
|
@@ -10,8 +12,6 @@ import type { SitePage } from './sitePages';
|
|
|
10
12
|
|
|
11
13
|
type SiteSectionMetadataMap = CollectionEntry<'site'>['data']['sections'];
|
|
12
14
|
type SiteSectionMetadata = SiteSectionMetadataMap[string];
|
|
13
|
-
type ThemePresentation = CollectionEntry<'theme'>['data']['presentation'];
|
|
14
|
-
type InlineStyles = NonNullable<NonNullable<ThemePresentation>['inlineStyles']>;
|
|
15
15
|
type GalleryImage = {
|
|
16
16
|
image: string;
|
|
17
17
|
src: string;
|
|
@@ -30,11 +30,18 @@ type CardListLayout = 'image-top' | 'image-left' | 'image-right';
|
|
|
30
30
|
type CardListFlow = 'grid' | 'stack';
|
|
31
31
|
type CardListSize = 's' | 'm' | 'l' | 'xl';
|
|
32
32
|
type CardListWidth = 'text' | 'narrow' | 'normal' | 'wide';
|
|
33
|
+
type InlineNote = {
|
|
34
|
+
markdown: string;
|
|
35
|
+
number: number;
|
|
36
|
+
id: string;
|
|
37
|
+
referenceId: string;
|
|
38
|
+
};
|
|
33
39
|
type SectionContentBlock =
|
|
34
40
|
| { type: 'html'; html: string }
|
|
35
41
|
| { type: 'image-stack'; images: GalleryImage[] }
|
|
36
42
|
| { type: 'image-carousel'; images: GalleryImage[] }
|
|
37
|
-
| { type: 'card-list'; layout: CardListLayout; flow: CardListFlow; size: CardListSize; width: CardListWidth; cards: CardListItem[] }
|
|
43
|
+
| { type: 'card-list'; layout: CardListLayout; flow: CardListFlow; size: CardListSize; width: CardListWidth; cards: CardListItem[] }
|
|
44
|
+
| { type: 'note'; html: string; number?: number; id?: string; referenceId?: string };
|
|
38
45
|
export type ResolvedSection = SiteSectionMetadata & {
|
|
39
46
|
id: string;
|
|
40
47
|
title: string;
|
|
@@ -42,9 +49,13 @@ export type ResolvedSection = SiteSectionMetadata & {
|
|
|
42
49
|
contentBlocks: SectionContentBlock[];
|
|
43
50
|
};
|
|
44
51
|
|
|
52
|
+
export type SectionNavigation = {
|
|
53
|
+
id: string;
|
|
54
|
+
title: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
45
57
|
const headingRegex = /<h2\b([^>]*)>([\s\S]*?)<\/h2>/gi;
|
|
46
58
|
const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
|
|
47
|
-
const inlineStyleReferenceRegex = /\[([\s\S]*?)\]\{\.([a-z][a-z0-9-]*)\}/g;
|
|
48
59
|
const markdownH2Regex = /^##\s+.*$/gm;
|
|
49
60
|
const imageProvenanceCommentRegex = /<!--\s*norna-image-provenance:[\s\S]*?-->/gi;
|
|
50
61
|
|
|
@@ -82,34 +93,144 @@ const getHeadingId = (attributes: string, headingHtml: string) => {
|
|
|
82
93
|
const getExplicitHeadingId = (headingHtml: string) =>
|
|
83
94
|
decodeHtmlEntities(stripTags(headingHtml)).trim().match(explicitHeadingIdRegex)?.[1];
|
|
84
95
|
|
|
85
|
-
const stripInlineStyleMarkers = (value: string) =>
|
|
86
|
-
value.replace(inlineStyleReferenceRegex, '$1');
|
|
87
|
-
|
|
88
96
|
const getHeadingTitle = (headingHtml: string) =>
|
|
89
|
-
|
|
97
|
+
decodeHtmlEntities(stripTags(headingHtml)).replace(explicitHeadingIdRegex, '').trim();
|
|
90
98
|
|
|
91
|
-
const getHeadingTitleHtml = (headingHtml: string
|
|
92
|
-
prepareContentHtml(headingHtml.replace(explicitHeadingIdRegex, '').trim()
|
|
99
|
+
const getHeadingTitleHtml = (headingHtml: string) =>
|
|
100
|
+
prepareContentHtml(headingHtml.replace(explicitHeadingIdRegex, '').trim());
|
|
93
101
|
|
|
94
|
-
const
|
|
95
|
-
html.
|
|
96
|
-
|
|
102
|
+
export const getSectionNavigation = (html: string, sectionMetadata: SiteSectionMetadataMap): SectionNavigation[] => {
|
|
103
|
+
const matches = Array.from(html.matchAll(headingRegex));
|
|
104
|
+
const sectionIds = new Set<string>();
|
|
105
|
+
const sections = matches.map((match) => {
|
|
106
|
+
const attributes = match[1] ?? '';
|
|
107
|
+
const headingHtml = match[2] ?? '';
|
|
108
|
+
const explicitId = getExplicitHeadingId(headingHtml);
|
|
109
|
+
const id = getHeadingId(attributes, headingHtml);
|
|
110
|
+
const title = getHeadingTitle(headingHtml);
|
|
111
|
+
|
|
112
|
+
if (!explicitId) {
|
|
113
|
+
throw new Error(`Section heading "${title}" is missing an explicit id. Write it as: ## ${title} {#${id}}`);
|
|
114
|
+
}
|
|
97
115
|
|
|
98
|
-
if (
|
|
99
|
-
throw new Error(`Markdown
|
|
116
|
+
if (sectionIds.has(id)) {
|
|
117
|
+
throw new Error(`Duplicate Markdown section heading id: ${id}`);
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
|
|
120
|
+
sectionIds.add(id);
|
|
121
|
+
return { id, 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
|
+
};
|
|
134
|
+
|
|
105
135
|
const stripImageProvenanceComments = (html: string) => html.replace(imageProvenanceCommentRegex, '');
|
|
106
136
|
|
|
107
|
-
const prepareContentHtml = (html: string
|
|
137
|
+
const prepareContentHtml = (html: string) =>
|
|
108
138
|
applyBasePathToHtml(
|
|
109
139
|
projectConfig.site.basePath,
|
|
110
|
-
stripImageProvenanceComments(
|
|
140
|
+
stripImageProvenanceComments(html),
|
|
111
141
|
);
|
|
112
142
|
|
|
143
|
+
const htmlVoidElements = new Set([
|
|
144
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link',
|
|
145
|
+
'meta', 'param', 'source', 'track', 'wbr',
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
// Keep notes aligned with the Markdown block immediately before them instead of
|
|
149
|
+
// aligning them with a whole run of paragraphs rendered as one HTML fragment.
|
|
150
|
+
const splitHtmlIntoBlocks = (html: string) => {
|
|
151
|
+
const tagRegex = /<!--[\s\S]*?-->|<\/?([a-z][\w:-]*)(?:\s[^<>]*?)?\/?>/gi;
|
|
152
|
+
const blocks: string[] = [];
|
|
153
|
+
let blockStart = -1;
|
|
154
|
+
let depth = 0;
|
|
155
|
+
let cursor = 0;
|
|
156
|
+
|
|
157
|
+
for (const match of html.matchAll(tagRegex)) {
|
|
158
|
+
const start = match.index ?? 0;
|
|
159
|
+
const token = match[0] ?? '';
|
|
160
|
+
const tagName = match[1]?.toLowerCase();
|
|
161
|
+
if (!tagName) continue;
|
|
162
|
+
|
|
163
|
+
const isClosing = /^<\//.test(token);
|
|
164
|
+
const isSelfClosing = /\/\s*>$/.test(token) || htmlVoidElements.has(tagName);
|
|
165
|
+
|
|
166
|
+
if (isClosing) {
|
|
167
|
+
if (depth > 0) depth -= 1;
|
|
168
|
+
if (depth === 0 && blockStart >= 0) {
|
|
169
|
+
blocks.push(html.slice(blockStart, start + token.length));
|
|
170
|
+
cursor = start + token.length;
|
|
171
|
+
blockStart = -1;
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (depth === 0) {
|
|
177
|
+
if (blockStart < 0) blockStart = start;
|
|
178
|
+
if (isSelfClosing) {
|
|
179
|
+
blocks.push(html.slice(blockStart, start + token.length));
|
|
180
|
+
cursor = start + token.length;
|
|
181
|
+
blockStart = -1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!isSelfClosing) depth += 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (blockStart >= 0) {
|
|
189
|
+
blocks.push(html.slice(blockStart).trim());
|
|
190
|
+
cursor = html.length;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const prefix = html.slice(0, blocks.length > 0 ? html.indexOf(blocks[0]!) : html.length).trim();
|
|
194
|
+
const suffix = html.slice(cursor).trim();
|
|
195
|
+
return [prefix, ...blocks, suffix].filter(Boolean);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const inlineNoteMarkerRegex = /^\s*<norna-inline-note\s+data-note-index="(\d+)"\s*><\/norna-inline-note>\s*$/i;
|
|
199
|
+
const noteDeclarationParagraphRegex = /<p>\s*\{note:\s*[\s\S]*?\}\s*<\/p>/gi;
|
|
200
|
+
const htmlCodeRegionRegex = /<pre\b[\s\S]*?<\/pre>|<code\b[\s\S]*?<\/code>/gi;
|
|
201
|
+
|
|
202
|
+
const replaceHtmlOutsideCode = (html: string, transform: (value: string) => string) => {
|
|
203
|
+
let result = '';
|
|
204
|
+
let cursor = 0;
|
|
205
|
+
|
|
206
|
+
for (const match of html.matchAll(htmlCodeRegionRegex)) {
|
|
207
|
+
const start = match.index ?? 0;
|
|
208
|
+
result += transform(html.slice(cursor, start));
|
|
209
|
+
result += match[0];
|
|
210
|
+
cursor = start + match[0].length;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result + transform(html.slice(cursor));
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const applyInlineNoteMarkup = (html: string, notes: InlineNote[]) => {
|
|
217
|
+
let referenceIndex = 0;
|
|
218
|
+
const withReferences = replaceHtmlOutsideCode(html, (value) => value.replace(/\{note-ref\}/g, () => {
|
|
219
|
+
const note = notes[referenceIndex];
|
|
220
|
+
if (!note) return '{note-ref}';
|
|
221
|
+
|
|
222
|
+
referenceIndex += 1;
|
|
223
|
+
return `\u2060<sup class="section-note-ref"><a id="${note.referenceId}" href="#${note.id}" aria-label="Note ${note.number}" aria-describedby="${note.id}" data-note-id="${note.id}">${note.number}</a></sup>`;
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
let noteIndex = 0;
|
|
227
|
+
return withReferences.replace(noteDeclarationParagraphRegex, () => {
|
|
228
|
+
const marker = `<norna-inline-note data-note-index="${noteIndex}"></norna-inline-note>`;
|
|
229
|
+
noteIndex += 1;
|
|
230
|
+
return marker;
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
|
|
113
234
|
const getRawMarkdownSections = (markdown: string) => {
|
|
114
235
|
const matches = Array.from(markdown.matchAll(markdownH2Regex));
|
|
115
236
|
const sections = new Map<string, string>();
|
|
@@ -135,61 +256,103 @@ const getImageSourceKey = (page: SitePage, sectionId: string, image: string) =>
|
|
|
135
256
|
: `images/${sectionId}/${image}`
|
|
136
257
|
);
|
|
137
258
|
|
|
138
|
-
const
|
|
259
|
+
const renderInlineNoteMarkdown = async (markdown: string) => {
|
|
260
|
+
const result = await markdownToHtml(markdown, {
|
|
261
|
+
features: {
|
|
262
|
+
gfm: true,
|
|
263
|
+
smartPunctuation: true,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (/<img\b/i.test(result.html)) {
|
|
268
|
+
throw new Error('Inline notes cannot contain images. Use a Norna image block with a caption instead.');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return prepareContentHtml(result.html);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const resolveContentBlocks = async (
|
|
139
275
|
html: string,
|
|
140
276
|
rawMarkdown: string,
|
|
141
277
|
page: SitePage,
|
|
142
278
|
sectionId: string,
|
|
143
|
-
|
|
279
|
+
inlineNotes: InlineNote[] = [],
|
|
144
280
|
) => {
|
|
145
281
|
const rawBlocks = extractNornaMarkdownBlocks(rawMarkdown);
|
|
282
|
+
const renderedHtml = applyInlineNoteMarkup(html, inlineNotes);
|
|
146
283
|
const splitBlocks = rawBlocks.length > 0
|
|
147
|
-
? splitNornaRenderedCodeBlocks(
|
|
148
|
-
: splitNornaMarkdownBlockMarkers(
|
|
284
|
+
? splitNornaRenderedCodeBlocks(renderedHtml, rawBlocks)
|
|
285
|
+
: splitNornaMarkdownBlockMarkers(renderedHtml);
|
|
286
|
+
const hasNotes = inlineNotes.length > 0;
|
|
287
|
+
const contentBlocks = hasNotes
|
|
288
|
+
? splitBlocks.flatMap((block) => block.type === 'html'
|
|
289
|
+
? splitHtmlIntoBlocks(block.html).map((htmlBlock) => ({ type: 'html' as const, html: htmlBlock }))
|
|
290
|
+
: [block])
|
|
291
|
+
: splitBlocks;
|
|
292
|
+
const resolvedBlocks: SectionContentBlock[] = [];
|
|
149
293
|
|
|
150
|
-
|
|
294
|
+
for (const block of contentBlocks) {
|
|
151
295
|
if (block.type === 'html') {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
296
|
+
const inlineNoteMarker = block.html.match(inlineNoteMarkerRegex);
|
|
297
|
+
if (inlineNoteMarker) {
|
|
298
|
+
const note = inlineNotes[Number(inlineNoteMarker[1])];
|
|
299
|
+
if (!note) {
|
|
300
|
+
throw new Error('Norna inline note markup could not be matched to its note text.');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
resolvedBlocks.push({
|
|
304
|
+
type: 'note',
|
|
305
|
+
html: await renderInlineNoteMarkdown(note.markdown),
|
|
306
|
+
number: note.number,
|
|
307
|
+
id: note.id,
|
|
308
|
+
referenceId: note.referenceId,
|
|
309
|
+
});
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
resolvedBlocks.push({ type: 'html', html: prepareContentHtml(block.html) });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (block.type === 'card-list') {
|
|
318
|
+
resolvedBlocks.push({
|
|
319
|
+
type: 'card-list',
|
|
320
|
+
layout: block.layout,
|
|
321
|
+
flow: block.flow,
|
|
322
|
+
size: block.size,
|
|
323
|
+
width: block.width,
|
|
324
|
+
cards: block.cards.map((card: CardListItem) => ({
|
|
325
|
+
...card,
|
|
326
|
+
...(card.image ? { src: getImageSourceKey(page, sectionId, card.image) } : {}),
|
|
327
|
+
})),
|
|
328
|
+
});
|
|
329
|
+
continue;
|
|
156
330
|
}
|
|
157
331
|
|
|
158
|
-
|
|
332
|
+
resolvedBlocks.push({
|
|
159
333
|
type: block.type,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
...(card.image ? { src: getImageSourceKey(page, sectionId, card.image) } : {}),
|
|
169
|
-
})),
|
|
170
|
-
}
|
|
171
|
-
: {
|
|
172
|
-
images: block.images.map((image: { image: string; alt?: string; caption?: string }) => ({
|
|
173
|
-
...image,
|
|
174
|
-
src: getImageSourceKey(page, sectionId, image.image),
|
|
175
|
-
})),
|
|
176
|
-
}),
|
|
177
|
-
};
|
|
178
|
-
});
|
|
334
|
+
images: block.images.map((image: { image: string; alt?: string; caption?: string }) => ({
|
|
335
|
+
...image,
|
|
336
|
+
src: getImageSourceKey(page, sectionId, image.image),
|
|
337
|
+
})),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return resolvedBlocks;
|
|
179
342
|
};
|
|
180
343
|
|
|
181
|
-
export const getSectionsContent = (
|
|
344
|
+
export const getSectionsContent = async (
|
|
182
345
|
html: string,
|
|
183
346
|
rawMarkdown: string,
|
|
184
347
|
sectionMetadata: SiteSectionMetadataMap,
|
|
185
348
|
page: SitePage,
|
|
186
|
-
inlineStyles?: InlineStyles,
|
|
187
349
|
) => {
|
|
188
350
|
const matches = Array.from(html.matchAll(headingRegex));
|
|
189
351
|
const rawSections = getRawMarkdownSections(rawMarkdown);
|
|
190
352
|
const metadataIds = new Set(Object.keys(sectionMetadata));
|
|
191
353
|
const sections: ResolvedSection[] = [];
|
|
192
354
|
const sectionIds = new Set<string>();
|
|
355
|
+
let nextNoteNumber = 1;
|
|
193
356
|
|
|
194
357
|
for (let index = 0; index < matches.length; index += 1) {
|
|
195
358
|
const match = matches[index];
|
|
@@ -212,12 +375,33 @@ export const getSectionsContent = (
|
|
|
212
375
|
}
|
|
213
376
|
|
|
214
377
|
sectionIds.add(id);
|
|
378
|
+
const rawSection = rawSections.get(id) ?? '';
|
|
379
|
+
const inlineNoteDiagnostics = extractInlineNoteDiagnostics(rawSection, {
|
|
380
|
+
label: 'Markdown section',
|
|
381
|
+
});
|
|
382
|
+
if (inlineNoteDiagnostics.errors.length > 0) {
|
|
383
|
+
throw new Error(inlineNoteDiagnostics.errors[0].message);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const inlineNotes = inlineNoteDiagnostics.notes.map((note) => {
|
|
387
|
+
const number = nextNoteNumber;
|
|
388
|
+
nextNoteNumber += 1;
|
|
389
|
+
const pageKey = page.routeId || 'home';
|
|
390
|
+
const noteKey = `${pageKey}-${id}-${number}`;
|
|
391
|
+
return {
|
|
392
|
+
...note,
|
|
393
|
+
number,
|
|
394
|
+
id: `note-${noteKey}`,
|
|
395
|
+
referenceId: `note-ref-${noteKey}`,
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
|
|
215
399
|
sections.push({
|
|
216
400
|
...(sectionMetadata[id] ?? {}),
|
|
217
401
|
id,
|
|
218
402
|
title,
|
|
219
|
-
titleHtml: getHeadingTitleHtml(headingHtml
|
|
220
|
-
contentBlocks: resolveContentBlocks(content,
|
|
403
|
+
titleHtml: getHeadingTitleHtml(headingHtml),
|
|
404
|
+
contentBlocks: await resolveContentBlocks(content, rawSection, page, id, inlineNotes),
|
|
221
405
|
});
|
|
222
406
|
}
|
|
223
407
|
|
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { sitePublicDir } from '../../scripts/lib/site-paths.mjs';
|
|
4
4
|
import projectConfig from '../../scripts/lib/project-config.mjs';
|
|
5
|
+
export { getLogoAsset } from '../../scripts/lib/logo-assets.mjs';
|
|
5
6
|
import { withBasePath } from './basePath';
|
|
6
7
|
|
|
7
8
|
type IconLink = {
|