@janga/norna 0.7.8 → 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 +18 -11
- package/docs/configuration.md +9 -54
- package/docs/content.md +6 -7
- package/docs/design/norna-diagram-design.md +158 -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 +59 -0
- package/examples/typography-presets/.astro/collections/sitewide.schema.json +114 -0
- package/examples/typography-presets/.astro/collections/theme.schema.json +770 -0
- package/examples/typography-presets/.astro/content-assets.mjs +4 -0
- package/examples/typography-presets/.astro/content-modules.mjs +1 -0
- package/examples/typography-presets/.astro/content.d.ts +183 -0
- package/examples/typography-presets/.astro/data-store.json +1 -0
- package/examples/typography-presets/.astro/dev-local.json +8 -0
- package/examples/typography-presets/.astro/dev.json +15 -0
- package/examples/typography-presets/.astro/dev.log +45 -0
- package/examples/typography-presets/.astro/settings.json +5 -0
- package/examples/typography-presets/.astro/types.d.ts +2 -0
- package/examples/typography-presets/node_modules/.vite/deps/_metadata.json +80 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
- package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js +1389 -0
- package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
- package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
- package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/package.json +3 -0
- package/examples/typography-presets/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
- package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
- package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
- package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
- package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
- package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
- package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
- package/examples/typography-presets/site/.norna/generated-images.json +290 -0
- package/examples/typography-presets/site/config.mjs +29 -0
- package/examples/typography-presets/site/content.md +16 -0
- package/examples/typography-presets/site/public/robots.txt +2 -0
- package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/puppy-portrait.jpg +0 -0
- package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/shepherd-portrait.jpg +0 -0
- package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/white-puppy.jpg +0 -0
- 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/images/compact-gallery/black-schnauzer.jpg +0 -0
- package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/retriever-portrait.jpg +0 -0
- package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/schnauzer-portrait.jpg +0 -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/images/text-forward/boxer-portrait.jpg +0 -0
- package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/grass-puppy.jpg +0 -0
- package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/grey-street-dog.jpg +0 -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/images/statement/black-pedigree.jpg +0 -0
- package/examples/typography-presets/site/routes/040-statement/images/statement/sheepdog-grass.jpg +0 -0
- package/examples/typography-presets/site/routes/040-statement/images/statement/sheepdog-wide.jpg +0 -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 +43 -0
- 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 -42
- package/scripts/lib/site-paths.mjs +2 -0
- package/scripts/lib/sitewide-config.mjs +34 -0
- package/scripts/lib/theme-config.mjs +72 -6
- package/scripts/lib/typography.mjs +20 -26
- package/scripts/show-typography.mjs +40 -147
- package/scripts/sync-content-sections.mjs +19 -14
- package/scripts/test-content-check.mjs +76 -14
- package/scripts/test-content-model-v2.mjs +154 -0
- package/scripts/test-engine-commands.mjs +3 -1
- package/scripts/test-package-check.mjs +78 -19
- 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 +194 -36
- 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 +236 -44
- 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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseYamlMapping } from '../../scripts/lib/frontmatter-yaml.mjs';
|
|
4
|
+
import {
|
|
5
|
+
splitSiteFile,
|
|
6
|
+
validateFrontmatterIndentation,
|
|
7
|
+
validateRouteThemeFrontmatterStructure,
|
|
8
|
+
} from '../../scripts/lib/site-content.mjs';
|
|
9
|
+
import { siteRoutesDir, siteRoutesLabel, sitewideContentLabel } from '../../scripts/lib/site-paths.mjs';
|
|
10
|
+
import { themeVisualSchema } from '../content.config';
|
|
11
|
+
|
|
12
|
+
type RouteTheme = {
|
|
13
|
+
id: string;
|
|
14
|
+
data: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const getRouteTheme = async (routeDirectory: string | null): Promise<RouteTheme | null> => {
|
|
18
|
+
if (!routeDirectory) return null;
|
|
19
|
+
|
|
20
|
+
const segments = routeDirectory.split(/[\\/]/).filter(Boolean);
|
|
21
|
+
for (let length = segments.length; length > 0; length -= 1) {
|
|
22
|
+
const themeSegments = segments.slice(0, length);
|
|
23
|
+
const themePath = path.join(siteRoutesDir, ...themeSegments, 'theme.md');
|
|
24
|
+
const themeLabel = `${siteRoutesLabel}/${themeSegments.join('/')}/theme.md`;
|
|
25
|
+
const source = await readFile(themePath, 'utf8').catch((error) => {
|
|
26
|
+
if (error?.code === 'ENOENT') return null;
|
|
27
|
+
throw error;
|
|
28
|
+
});
|
|
29
|
+
if (!source) continue;
|
|
30
|
+
|
|
31
|
+
const { frontmatter, frontmatterBody } = splitSiteFile(source, themeLabel);
|
|
32
|
+
const issues: Array<{ message: string }> = [];
|
|
33
|
+
validateFrontmatterIndentation(frontmatter, (issue) => issues.push(issue));
|
|
34
|
+
validateRouteThemeFrontmatterStructure(frontmatter, (issue) => issues.push(issue));
|
|
35
|
+
if (issues.length > 0) {
|
|
36
|
+
throw new Error([
|
|
37
|
+
`${themeLabel} has invalid frontmatter.`,
|
|
38
|
+
...issues.map((issue) => `- ${issue.message}`),
|
|
39
|
+
].join('\n'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = parseYamlMapping(frontmatterBody);
|
|
43
|
+
if (Object.hasOwn(data, 'navigation')) {
|
|
44
|
+
throw new Error(`${themeLabel} may not define navigation. Brand and logo belong in ${sitewideContentLabel}.`);
|
|
45
|
+
}
|
|
46
|
+
const parsed = themeVisualSchema.safeParse(data);
|
|
47
|
+
if (!parsed.success) {
|
|
48
|
+
throw new Error([
|
|
49
|
+
`${themeLabel} has invalid values.`,
|
|
50
|
+
...parsed.error.issues.map((issue) => `- ${issue.path.join('.') || '(root)'}: ${issue.message}`),
|
|
51
|
+
].join('\n'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
id: `routes/${themeSegments.join('/')}/theme`,
|
|
56
|
+
data: parsed.data,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
@@ -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,20 +30,32 @@ 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;
|
|
48
|
+
titleHtml: string;
|
|
41
49
|
contentBlocks: SectionContentBlock[];
|
|
42
50
|
};
|
|
43
51
|
|
|
52
|
+
export type SectionNavigation = {
|
|
53
|
+
id: string;
|
|
54
|
+
title: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
44
57
|
const headingRegex = /<h2\b([^>]*)>([\s\S]*?)<\/h2>/gi;
|
|
45
58
|
const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
|
|
46
|
-
const inlineStyleReferenceRegex = /\[([^\]<]+)\]\{\.([a-z][a-z0-9-]*)\}/g;
|
|
47
59
|
const markdownH2Regex = /^##\s+.*$/gm;
|
|
48
60
|
const imageProvenanceCommentRegex = /<!--\s*norna-image-provenance:[\s\S]*?-->/gi;
|
|
49
61
|
|
|
@@ -84,25 +96,141 @@ const getExplicitHeadingId = (headingHtml: string) =>
|
|
|
84
96
|
const getHeadingTitle = (headingHtml: string) =>
|
|
85
97
|
decodeHtmlEntities(stripTags(headingHtml)).replace(explicitHeadingIdRegex, '').trim();
|
|
86
98
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const style = inlineStyles?.[styleName];
|
|
99
|
+
const getHeadingTitleHtml = (headingHtml: string) =>
|
|
100
|
+
prepareContentHtml(headingHtml.replace(explicitHeadingIdRegex, '').trim());
|
|
90
101
|
|
|
91
|
-
|
|
92
|
-
|
|
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}}`);
|
|
93
114
|
}
|
|
94
115
|
|
|
95
|
-
|
|
116
|
+
if (sectionIds.has(id)) {
|
|
117
|
+
throw new Error(`Duplicate Markdown section heading id: ${id}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
sectionIds.add(id);
|
|
121
|
+
return { id, title };
|
|
96
122
|
});
|
|
97
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
|
+
|
|
98
135
|
const stripImageProvenanceComments = (html: string) => html.replace(imageProvenanceCommentRegex, '');
|
|
99
136
|
|
|
100
|
-
const prepareContentHtml = (html: string
|
|
137
|
+
const prepareContentHtml = (html: string) =>
|
|
101
138
|
applyBasePathToHtml(
|
|
102
139
|
projectConfig.site.basePath,
|
|
103
|
-
stripImageProvenanceComments(
|
|
140
|
+
stripImageProvenanceComments(html),
|
|
104
141
|
);
|
|
105
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
|
+
|
|
106
234
|
const getRawMarkdownSections = (markdown: string) => {
|
|
107
235
|
const matches = Array.from(markdown.matchAll(markdownH2Regex));
|
|
108
236
|
const sections = new Map<string, string>();
|
|
@@ -128,61 +256,103 @@ const getImageSourceKey = (page: SitePage, sectionId: string, image: string) =>
|
|
|
128
256
|
: `images/${sectionId}/${image}`
|
|
129
257
|
);
|
|
130
258
|
|
|
131
|
-
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 (
|
|
132
275
|
html: string,
|
|
133
276
|
rawMarkdown: string,
|
|
134
277
|
page: SitePage,
|
|
135
278
|
sectionId: string,
|
|
136
|
-
|
|
279
|
+
inlineNotes: InlineNote[] = [],
|
|
137
280
|
) => {
|
|
138
281
|
const rawBlocks = extractNornaMarkdownBlocks(rawMarkdown);
|
|
282
|
+
const renderedHtml = applyInlineNoteMarkup(html, inlineNotes);
|
|
139
283
|
const splitBlocks = rawBlocks.length > 0
|
|
140
|
-
? splitNornaRenderedCodeBlocks(
|
|
141
|
-
: 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[] = [];
|
|
142
293
|
|
|
143
|
-
|
|
294
|
+
for (const block of contentBlocks) {
|
|
144
295
|
if (block.type === 'html') {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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;
|
|
149
330
|
}
|
|
150
331
|
|
|
151
|
-
|
|
332
|
+
resolvedBlocks.push({
|
|
152
333
|
type: block.type,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
...(card.image ? { src: getImageSourceKey(page, sectionId, card.image) } : {}),
|
|
162
|
-
})),
|
|
163
|
-
}
|
|
164
|
-
: {
|
|
165
|
-
images: block.images.map((image: { image: string; alt?: string; caption?: string }) => ({
|
|
166
|
-
...image,
|
|
167
|
-
src: getImageSourceKey(page, sectionId, image.image),
|
|
168
|
-
})),
|
|
169
|
-
}),
|
|
170
|
-
};
|
|
171
|
-
});
|
|
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;
|
|
172
342
|
};
|
|
173
343
|
|
|
174
|
-
export const getSectionsContent = (
|
|
344
|
+
export const getSectionsContent = async (
|
|
175
345
|
html: string,
|
|
176
346
|
rawMarkdown: string,
|
|
177
347
|
sectionMetadata: SiteSectionMetadataMap,
|
|
178
348
|
page: SitePage,
|
|
179
|
-
inlineStyles?: InlineStyles,
|
|
180
349
|
) => {
|
|
181
350
|
const matches = Array.from(html.matchAll(headingRegex));
|
|
182
351
|
const rawSections = getRawMarkdownSections(rawMarkdown);
|
|
183
352
|
const metadataIds = new Set(Object.keys(sectionMetadata));
|
|
184
353
|
const sections: ResolvedSection[] = [];
|
|
185
354
|
const sectionIds = new Set<string>();
|
|
355
|
+
let nextNoteNumber = 1;
|
|
186
356
|
|
|
187
357
|
for (let index = 0; index < matches.length; index += 1) {
|
|
188
358
|
const match = matches[index];
|
|
@@ -205,11 +375,33 @@ export const getSectionsContent = (
|
|
|
205
375
|
}
|
|
206
376
|
|
|
207
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
|
+
|
|
208
399
|
sections.push({
|
|
209
400
|
...(sectionMetadata[id] ?? {}),
|
|
210
401
|
id,
|
|
211
402
|
title,
|
|
212
|
-
|
|
403
|
+
titleHtml: getHeadingTitleHtml(headingHtml),
|
|
404
|
+
contentBlocks: await resolveContentBlocks(content, rawSection, page, id, inlineNotes),
|
|
213
405
|
});
|
|
214
406
|
}
|
|
215
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 = {
|