@janga/norna 0.7.16 → 0.7.18
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 +18 -9
- package/astro.config.mjs +35 -3
- package/bin/norna-cli.mjs +1 -1
- package/docs/README.md +16 -14
- package/docs/commands.md +2 -2
- package/docs/configuration.md +25 -2
- package/docs/content.md +92 -103
- package/docs/design/navigation-and-theme-plan.md +186 -0
- package/docs/design/norna-diagram-design.md +9 -9
- package/docs/design/site-examples-structure.md +15 -11
- package/docs/engine-development.md +19 -22
- package/docs/getting-started.md +21 -16
- package/docs/images-and-metadata.md +12 -9
- package/docs/pages.md +190 -0
- package/docs/public-files.md +49 -36
- package/docs/publishing.md +19 -3
- package/docs/site-files.md +46 -38
- package/docs/sitewide-content.md +20 -36
- package/docs/theme.md +109 -143
- package/docs/typography.md +19 -31
- package/examples/README.md +10 -4
- 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 → pages/000-home/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 → pages/000-home/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 +180 -91
- 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 → pages/000-home/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 +9 -7
- package/examples/feature-demos/media-and-surfaces/site/pages/020-surfaces/theme.yaml +2 -0
- 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/pages/000-home/content.md +17 -0
- 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-preset-documentation/README.md +16 -0
- package/examples/feature-demos/theme-preset-documentation/site/.norna/generated-images.json +74 -0
- package/examples/feature-demos/{theme-presets/site/routes/020-documentation → theme-preset-documentation/site/pages/000-home}/content.md +4 -7
- package/examples/feature-demos/theme-preset-documentation/site/sitewide-content.yaml +3 -0
- package/examples/feature-demos/theme-preset-portfolio/README.md +16 -0
- package/examples/feature-demos/theme-preset-portfolio/site/.norna/generated-images.json +74 -0
- package/examples/feature-demos/theme-preset-portfolio/site/config.yaml +1 -0
- package/examples/feature-demos/{theme-presets/site/routes/010-portfolio → theme-preset-portfolio/site/pages/000-home}/content.md +4 -7
- package/examples/feature-demos/theme-preset-portfolio/site/public/robots.txt +2 -0
- package/examples/feature-demos/theme-preset-portfolio/site/sitewide-content.yaml +3 -0
- package/examples/feature-demos/theme-preset-project/README.md +16 -0
- package/examples/feature-demos/theme-preset-project/site/.norna/generated-images.json +74 -0
- package/examples/feature-demos/theme-preset-project/site/config.yaml +1 -0
- package/examples/feature-demos/{theme-presets/site/routes/030-project → theme-preset-project/site/pages/000-home}/content.md +4 -7
- package/examples/feature-demos/theme-preset-project/site/public/robots.txt +2 -0
- package/examples/feature-demos/theme-preset-project/site/sitewide-content.yaml +3 -0
- package/examples/feature-demos/theme-preset-statement/README.md +16 -0
- package/examples/feature-demos/theme-preset-statement/site/.norna/generated-images.json +74 -0
- package/examples/feature-demos/theme-preset-statement/site/config.yaml +1 -0
- package/examples/feature-demos/{theme-presets/site/routes/040-statement → theme-preset-statement/site/pages/000-home}/content.md +4 -7
- package/examples/feature-demos/theme-preset-statement/site/public/robots.txt +2 -0
- package/examples/feature-demos/theme-preset-statement/site/sitewide-content.yaml +3 -0
- package/fixtures/basic/site/{content.md → pages/000-home/content.md} +5 -6
- package/fixtures/basic/site/sitewide-content.yaml +1 -8
- package/fixtures/basic/site/theme.yaml +1 -8
- package/fixtures/content-model-v2/site/{content.md → pages/000-home/content.md} +11 -13
- package/fixtures/content-model-v2/site/pages/010-guide/content.md +18 -0
- package/fixtures/nested-pages/.astro/collections/site.schema.json +35 -0
- package/fixtures/nested-pages/.astro/collections/sitewide.schema.json +97 -0
- package/fixtures/nested-pages/.astro/collections/theme.schema.json +844 -0
- package/fixtures/nested-pages/.astro/content-assets.mjs +4 -0
- package/fixtures/nested-pages/.astro/content-modules.mjs +1 -0
- package/fixtures/nested-pages/.astro/content.d.ts +183 -0
- package/fixtures/nested-pages/.astro/data-store.json +1 -0
- package/fixtures/nested-pages/.astro/dev-local.json +8 -0
- package/fixtures/nested-pages/.astro/dev.log +1128 -0
- package/fixtures/nested-pages/.astro/settings.json +5 -0
- package/fixtures/nested-pages/.astro/types.d.ts +2 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/_metadata.json +74 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/package.json +3 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
- package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
- package/fixtures/nested-pages/site/.norna/generated-images.json +10 -0
- package/fixtures/nested-pages/site/config.yaml +3 -0
- package/fixtures/nested-pages/site/pages/000-home/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 +18 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/theme.yaml +2 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +11 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/030-release-notes/content.md +9 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/040-configuration/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/050-content/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/060-themes/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/070-images/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/080-cards/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/090-notes/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/100-validation/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/110-synchronization/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/120-building/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/130-publishing/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/140-troubleshooting/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/150-migration/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/160-accessibility/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/170-performance/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/180-security/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/190-localization/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/200-upgrading/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/pages/210-faq/content.md +3 -0
- package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +7 -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 +1 -0
- package/package.json +8 -4
- package/schemas/config.schema.json +46 -5
- package/schemas/content-frontmatter.schema.json +28 -63
- package/schemas/manifest.json +4 -2
- package/schemas/page-theme.schema.json +192 -0
- package/schemas/sitewide-content.schema.json +58 -132
- package/schemas/theme.schema.json +434 -152
- package/scripts/check-config.mjs +8 -6
- package/scripts/deploy-site.mjs +6 -8
- package/scripts/doctor.mjs +2 -2
- package/scripts/generate-images.mjs +15 -29
- package/scripts/generate-schemas.mjs +7 -218
- package/scripts/lib/base-path-redirect.mjs +9 -0
- package/scripts/lib/editor-language-service.mjs +125 -79
- package/scripts/lib/example-sites.mjs +15 -3
- 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 +102 -0
- package/scripts/lib/presentation.mjs +25 -17
- package/scripts/lib/project-config.mjs +46 -8
- package/scripts/lib/schema-definitions.mjs +78 -55
- package/scripts/lib/schema-editor-metadata.mjs +519 -0
- package/scripts/lib/schema-value-definitions.mjs +20 -6
- package/scripts/lib/site-content.mjs +159 -97
- package/scripts/lib/site-conventions.mjs +1 -0
- package/scripts/lib/site-paths.mjs +11 -5
- package/scripts/lib/theme-config.mjs +12 -18
- package/scripts/lib/theme-presets.mjs +55 -14
- package/scripts/release.mjs +1 -1
- package/scripts/show-typography.mjs +4 -30
- package/scripts/sync-content-sections.mjs +107 -68
- package/scripts/{test-temporary-visibility.mjs → test-banner-visibility.mjs} +8 -25
- package/scripts/test-client-javascript.mjs +50 -46
- package/scripts/test-content-check.mjs +153 -69
- package/scripts/test-content-model-v2.mjs +566 -285
- package/scripts/test-documentation.mjs +10 -3
- package/scripts/test-editor-language-service.mjs +172 -25
- package/scripts/test-engine-commands.mjs +6 -5
- package/scripts/test-examples.mjs +30 -1
- package/scripts/test-heading-ids.mjs +68 -0
- package/scripts/test-navigation-model.mjs +63 -0
- package/scripts/test-navigation.mjs +11 -2
- package/scripts/test-nested-pages.mjs +125 -0
- package/scripts/test-package-check.mjs +55 -95
- package/scripts/test-page-model.mjs +108 -0
- package/scripts/test-project-config.mjs +22 -3
- package/scripts/test-schemas.mjs +82 -15
- package/scripts/test-theme-presets.mjs +57 -42
- package/src/components/NavigationPageTree.astro +157 -0
- package/src/components/PageContentsLinks.astro +40 -0
- package/src/components/SectionNavigationScript.astro +117 -5
- package/src/components/SiteBreadcrumbs.astro +32 -0
- package/src/components/SiteNavigation.astro +97 -68
- package/src/components/SitePage.astro +93 -55
- package/src/components/SiteSection.astro +29 -27
- package/src/components/SiteTreeNavigation.astro +35 -0
- package/src/components/TreeNavigationScript.astro +83 -0
- package/src/content.config.ts +8 -10
- package/src/layouts/BaseLayout.astro +21 -20
- package/src/lib/pageThemes.ts +44 -0
- package/src/lib/sectionContent.ts +106 -107
- package/src/lib/siteNavigation.ts +49 -0
- package/src/lib/sitePages.ts +96 -38
- package/src/pages/{[slug].astro → [...slug].astro} +2 -2
- package/src/pages/index.astro +2 -1
- package/src/styles/global.css +743 -57
- package/starters/basic/README.md +9 -11
- package/starters/basic/site/{content.md → pages/000-home/content.md} +7 -4
- package/starters/basic/site/sitewide-content.yaml +1 -10
- package/starters/basic/site/theme.yaml +1 -105
- package/starters/project/README.md +14 -14
- package/starters/project/site/{content.md → pages/000-home/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/starters/project/site/theme.yaml +1 -31
- package/docs/routes.md +0 -121
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.yaml +0 -3
- package/examples/feature-demos/sitewide-content/site/content.md +0 -16
- package/examples/feature-demos/sitewide-content/site/routes/010-identity/content.md +0 -20
- package/examples/feature-demos/theme-presets/README.md +0 -18
- package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +0 -290
- package/examples/feature-demos/theme-presets/site/content.md +0 -17
- package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +0 -12
- package/examples/feature-demos/theme-presets/site/theme.yaml +0 -1
- 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 → pages/000-home/images}/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 → pages/000-home/images}/foster-dog.jpg +0 -0
- /package/examples/complete-sites/dog-shelter-single-page/site/{images/what-we-do → pages/000-home/images}/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/{theme-presets → theme-preset-documentation}/site/config.yaml +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/020-documentation/images/documentation → theme-preset-documentation/site/pages/000-home/images}/black-schnauzer.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/020-documentation/images/documentation → theme-preset-documentation/site/pages/000-home/images}/retriever-portrait.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/020-documentation/images/documentation → theme-preset-documentation/site/pages/000-home/images}/schnauzer-portrait.jpg +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/public/robots.txt +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/020-documentation → theme-preset-documentation/site}/theme.yaml +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio/images/portfolio → theme-preset-portfolio/site/pages/000-home/images}/puppy-portrait.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio/images/portfolio → theme-preset-portfolio/site/pages/000-home/images}/shepherd-portrait.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio/images/portfolio → theme-preset-portfolio/site/pages/000-home/images}/white-puppy.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio → theme-preset-portfolio/site}/theme.yaml +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/routeTheme.schema.json +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/site.schema.json +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/sitewide.schema.json +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/theme.schema.json +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-assets.mjs +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-modules.mjs +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content.d.ts +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/data-store.json +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev-local.json +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.json +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.log +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/settings.json +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/types.d.ts +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/_metadata.json +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/package.json +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js +0 -0
- /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/030-project/images/project → theme-preset-project/site/pages/000-home/images}/boxer-portrait.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/030-project/images/project → theme-preset-project/site/pages/000-home/images}/grass-puppy.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/030-project/images/project → theme-preset-project/site/pages/000-home/images}/grey-street-dog.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/030-project → theme-preset-project/site}/theme.yaml +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/040-statement/images/statement → theme-preset-statement/site/pages/000-home/images}/black-pedigree.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/040-statement/images/statement → theme-preset-statement/site/pages/000-home/images}/sheepdog-grass.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/040-statement/images/statement → theme-preset-statement/site/pages/000-home/images}/sheepdog-wide.jpg +0 -0
- /package/examples/feature-demos/{theme-presets/site/routes/040-statement → theme-preset-statement/site}/theme.yaml +0 -0
- /package/fixtures/basic/site/{images/work → pages/000-home/images}/.gitkeep +0 -0
- /package/fixtures/content-model-v2/site/{images/card-section → pages/000-home/images}/adopt.svg +0 -0
- /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/detail.jpg +0 -0
- /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/duplicate.jpg +0 -0
- /package/fixtures/content-model-v2/site/{images/card-section → pages/000-home/images}/foster.svg +0 -0
- /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/hero.jpg +0 -0
- /package/fixtures/content-model-v2/site/{images/plain → pages/000-home/images}/image.jpg +0 -0
- /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-one.jpg +0 -0
- /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-three.jpg +0 -0
- /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/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 → pages/000-home/images}/.gitkeep +0 -0
|
@@ -9,12 +9,11 @@ import {
|
|
|
9
9
|
nornaMarkdownBlockDefinitions,
|
|
10
10
|
} from './norna-markdown-blocks.mjs';
|
|
11
11
|
import { inspectPublicAssetFilenames, logoAssetFilenames } from './public-asset-conventions.mjs';
|
|
12
|
+
import { getHeadingIdentifierIssues, getMarkdownHeadings } from './heading-ids.mjs';
|
|
12
13
|
import { siteSchema } from './schema-definitions.mjs';
|
|
13
14
|
|
|
14
15
|
const supportedImageExtensions = new Set(['.jpg', '.jpeg', '.png', '.svg']);
|
|
15
16
|
const siteConfigNames = ['config.yaml'];
|
|
16
|
-
const headingIdPattern = /^##\s+.+?\s*\{#([a-z0-9-]+)\}\s*$/;
|
|
17
|
-
|
|
18
17
|
const fileExists = (filePath) => access(filePath).then(() => true, () => false);
|
|
19
18
|
const toPosixPath = (filePath) => filePath.split(path.sep).join('/');
|
|
20
19
|
|
|
@@ -37,7 +36,7 @@ export const findNornaSiteRoot = async (documentPath) => {
|
|
|
37
36
|
}
|
|
38
37
|
};
|
|
39
38
|
|
|
40
|
-
const
|
|
39
|
+
const readSitewideLogoForEditor = async (siteRoot) => {
|
|
41
40
|
const filename = await findFile(siteRoot, ['sitewide-content.yaml']);
|
|
42
41
|
if (!filename) return null;
|
|
43
42
|
const absolutePath = path.join(siteRoot, filename);
|
|
@@ -45,15 +44,16 @@ const readSitewideNavigationForEditor = async (siteRoot) => {
|
|
|
45
44
|
|
|
46
45
|
try {
|
|
47
46
|
const data = load(source) ?? {};
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const logoConfigured = Boolean(
|
|
48
|
+
data
|
|
49
|
+
&& typeof data === 'object'
|
|
50
|
+
&& Object.hasOwn(data, 'logo'),
|
|
51
|
+
);
|
|
51
52
|
const logoLine = source.replace(/\r\n?/g, '\n').split('\n')
|
|
52
|
-
.findIndex((line) =>
|
|
53
|
+
.findIndex((line) => /^logo:\s*(?:#.*)?$/.test(line)) + 1;
|
|
53
54
|
return {
|
|
54
55
|
absolutePath,
|
|
55
|
-
|
|
56
|
-
logoConfigured: Object.hasOwn(navigation, 'logo'),
|
|
56
|
+
logoConfigured,
|
|
57
57
|
logoLine: logoLine > 0 ? logoLine : 1,
|
|
58
58
|
};
|
|
59
59
|
} catch {
|
|
@@ -73,7 +73,7 @@ export const getSitePublicAssetStatus = async (documentPath) => {
|
|
|
73
73
|
const inspection = inspectPublicAssetFilenames(
|
|
74
74
|
entries.filter((entry) => entry.isFile()).map((entry) => entry.name),
|
|
75
75
|
);
|
|
76
|
-
const
|
|
76
|
+
const sitewideLogo = await readSitewideLogoForEditor(siteRoot);
|
|
77
77
|
const issues = inspection.suspicious.map((issue) => ({
|
|
78
78
|
...issue,
|
|
79
79
|
absolutePath: path.join(publicDirectory, issue.filename),
|
|
@@ -92,14 +92,14 @@ export const getSitePublicAssetStatus = async (documentPath) => {
|
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
if (inspection.logos.length === 0 &&
|
|
95
|
+
if (inspection.logos.length === 0 && sitewideLogo?.logoConfigured) {
|
|
96
96
|
const publicLabel = toPosixPath(path.relative(path.dirname(siteRoot), publicDirectory));
|
|
97
97
|
issues.push({
|
|
98
|
-
absolutePath:
|
|
98
|
+
absolutePath: sitewideLogo.absolutePath,
|
|
99
99
|
code: 'missing-logo-file',
|
|
100
|
-
filename: path.basename(
|
|
101
|
-
line:
|
|
102
|
-
message: `
|
|
100
|
+
filename: path.basename(sitewideLogo.absolutePath),
|
|
101
|
+
line: sitewideLogo.logoLine,
|
|
102
|
+
message: `Logo display settings are configured, but no logo file was found. Add exactly one of ${logoAssetFilenames.map((filename) => `${publicLabel}/${filename}`).join(', ')}, or remove logo.`,
|
|
103
103
|
severity: 'error',
|
|
104
104
|
});
|
|
105
105
|
}
|
|
@@ -107,7 +107,7 @@ export const getSitePublicAssetStatus = async (documentPath) => {
|
|
|
107
107
|
return {
|
|
108
108
|
...inspection,
|
|
109
109
|
issues,
|
|
110
|
-
|
|
110
|
+
sitewideLogo,
|
|
111
111
|
publicDirectory,
|
|
112
112
|
siteRoot,
|
|
113
113
|
};
|
|
@@ -116,39 +116,18 @@ export const getSitePublicAssetStatus = async (documentPath) => {
|
|
|
116
116
|
const getPageContext = (siteRoot, documentPath) => {
|
|
117
117
|
const absoluteDocumentPath = path.resolve(documentPath);
|
|
118
118
|
const relativePath = toPosixPath(path.relative(siteRoot, absoluteDocumentPath));
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
contentPath: absoluteDocumentPath,
|
|
122
|
-
imagesRoot: path.join(siteRoot, 'images'),
|
|
123
|
-
pageLabel: 'Home',
|
|
124
|
-
routeDirectory: null,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const match = relativePath.match(/^routes\/(.+)\/content\.md$/);
|
|
119
|
+
const match = relativePath.match(/^pages\/(.+)\/content\.md$/);
|
|
129
120
|
if (!match) return null;
|
|
130
121
|
|
|
131
|
-
const
|
|
122
|
+
const pageDirectory = match[1];
|
|
132
123
|
return {
|
|
133
124
|
contentPath: absoluteDocumentPath,
|
|
134
|
-
imagesRoot: path.join(siteRoot, '
|
|
135
|
-
pageLabel:
|
|
136
|
-
|
|
125
|
+
imagesRoot: path.join(siteRoot, 'pages', ...pageDirectory.split('/'), 'images'),
|
|
126
|
+
pageLabel: pageDirectory,
|
|
127
|
+
pageDirectory,
|
|
137
128
|
};
|
|
138
129
|
};
|
|
139
130
|
|
|
140
|
-
const getSectionAtLine = (source, lineIndex) => {
|
|
141
|
-
const lines = source.replace(/\r\n?/g, '\n').split('\n');
|
|
142
|
-
let section = null;
|
|
143
|
-
|
|
144
|
-
for (let index = 0; index <= Math.min(lineIndex, lines.length - 1); index += 1) {
|
|
145
|
-
const match = lines[index].match(headingIdPattern);
|
|
146
|
-
if (match) section = { id: match[1], line: index };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return section;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
131
|
const getOpenFenceAtLine = (source, lineIndex) => {
|
|
153
132
|
const lines = source.replace(/\r\n?/g, '\n').split('\n');
|
|
154
133
|
let open = null;
|
|
@@ -304,12 +283,11 @@ const collectImageFiles = async (directory, imageRoot, pageLabel, files) => {
|
|
|
304
283
|
filename: entry.name,
|
|
305
284
|
pageLabel,
|
|
306
285
|
relativeToImages,
|
|
307
|
-
sectionId: relativeToImages.split('/')[0] ?? '',
|
|
308
286
|
});
|
|
309
287
|
}
|
|
310
288
|
};
|
|
311
289
|
|
|
312
|
-
const
|
|
290
|
+
const collectPageImageRoots = async (directory, siteRoot, roots) => {
|
|
313
291
|
const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
314
292
|
if (error?.code === 'ENOENT') return [];
|
|
315
293
|
throw error;
|
|
@@ -319,18 +297,16 @@ const collectRouteImageRoots = async (directory, siteRoot, roots) => {
|
|
|
319
297
|
if (!entry.isDirectory()) continue;
|
|
320
298
|
const absolutePath = path.join(directory, entry.name);
|
|
321
299
|
if (entry.name === 'images') {
|
|
322
|
-
const
|
|
323
|
-
roots.push({ imageRoot: absolutePath, pageLabel:
|
|
300
|
+
const pageDirectory = toPosixPath(path.relative(path.join(siteRoot, 'pages'), path.dirname(absolutePath)));
|
|
301
|
+
roots.push({ imageRoot: absolutePath, pageLabel: pageDirectory });
|
|
324
302
|
continue;
|
|
325
303
|
}
|
|
326
|
-
await
|
|
304
|
+
await collectPageImageRoots(absolutePath, siteRoot, roots);
|
|
327
305
|
}
|
|
328
306
|
};
|
|
329
307
|
|
|
330
308
|
const getContentFiles = async (siteRoot) => {
|
|
331
309
|
const files = [];
|
|
332
|
-
const homePath = path.join(siteRoot, 'content.md');
|
|
333
|
-
if (await fileExists(homePath)) files.push(homePath);
|
|
334
310
|
|
|
335
311
|
const visit = async (directory) => {
|
|
336
312
|
const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
@@ -344,7 +320,7 @@ const getContentFiles = async (siteRoot) => {
|
|
|
344
320
|
}
|
|
345
321
|
};
|
|
346
322
|
|
|
347
|
-
await visit(path.join(siteRoot, '
|
|
323
|
+
await visit(path.join(siteRoot, 'pages'));
|
|
348
324
|
return files;
|
|
349
325
|
};
|
|
350
326
|
|
|
@@ -363,12 +339,9 @@ const getReferencesByFilename = async (siteRoot) => {
|
|
|
363
339
|
|
|
364
340
|
export const createSiteImageIndex = async (siteRoot) => {
|
|
365
341
|
const files = [];
|
|
366
|
-
const
|
|
367
|
-
await
|
|
368
|
-
|
|
369
|
-
const routeRoots = [];
|
|
370
|
-
await collectRouteImageRoots(path.join(siteRoot, 'routes'), siteRoot, routeRoots);
|
|
371
|
-
for (const root of routeRoots) {
|
|
342
|
+
const pageRoots = [];
|
|
343
|
+
await collectPageImageRoots(path.join(siteRoot, 'pages'), siteRoot, pageRoots);
|
|
344
|
+
for (const root of pageRoots) {
|
|
372
345
|
await collectImageFiles(root.imageRoot, root.imageRoot, root.pageLabel, files);
|
|
373
346
|
}
|
|
374
347
|
|
|
@@ -397,9 +370,7 @@ export const getImageCompletionContext = async ({ documentPath, source, line })
|
|
|
397
370
|
const currentLine = source.replace(/\r\n?/g, '\n').split('\n')[line] ?? '';
|
|
398
371
|
if (!/^\s*(?:-\s+)?image:\s*[^\s]*$/.test(currentLine)) return null;
|
|
399
372
|
|
|
400
|
-
const
|
|
401
|
-
if (!section) return null;
|
|
402
|
-
const expectedDirectory = path.join(page.imagesRoot, section.id);
|
|
373
|
+
const expectedDirectory = page.imagesRoot;
|
|
403
374
|
const index = await createSiteImageIndex(siteRoot);
|
|
404
375
|
const candidates = index.files.map((file) => {
|
|
405
376
|
const isExpected = path.dirname(file.absolutePath) === expectedDirectory;
|
|
@@ -421,7 +392,6 @@ export const getImageCompletionContext = async ({ documentPath, source, line })
|
|
|
421
392
|
candidates,
|
|
422
393
|
expectedDirectory,
|
|
423
394
|
page,
|
|
424
|
-
section,
|
|
425
395
|
siteRoot,
|
|
426
396
|
};
|
|
427
397
|
};
|
|
@@ -434,10 +404,9 @@ export const getImageDefinitionContext = async ({ documentPath, source, line })
|
|
|
434
404
|
const filename = match[1].replace(/^['"]|['"]$/g, '');
|
|
435
405
|
const siteRoot = await findNornaSiteRoot(documentPath);
|
|
436
406
|
const page = siteRoot ? getPageContext(siteRoot, documentPath) : null;
|
|
437
|
-
|
|
438
|
-
if (!siteRoot || !page || !section) return null;
|
|
407
|
+
if (!siteRoot || !page) return null;
|
|
439
408
|
|
|
440
|
-
const expectedPath = path.join(page.imagesRoot,
|
|
409
|
+
const expectedPath = path.join(page.imagesRoot, filename);
|
|
441
410
|
if (await fileExists(expectedPath)) return { files: [expectedPath], filename, siteRoot };
|
|
442
411
|
|
|
443
412
|
const index = await createSiteImageIndex(siteRoot);
|
|
@@ -480,7 +449,9 @@ const getImageReferenceLines = (source) => {
|
|
|
480
449
|
const getContentFrontmatterDiagnostics = (source) => {
|
|
481
450
|
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
482
451
|
if (!match) {
|
|
483
|
-
return
|
|
452
|
+
return /^---(?:\r?\n|$)/.test(source)
|
|
453
|
+
? [{ severity: 'error', line: 1, message: 'YAML frontmatter starts with ---, but no closing --- delimiter was found.' }]
|
|
454
|
+
: [];
|
|
484
455
|
}
|
|
485
456
|
|
|
486
457
|
let data;
|
|
@@ -504,21 +475,98 @@ const getContentFrontmatterDiagnostics = (source) => {
|
|
|
504
475
|
}));
|
|
505
476
|
};
|
|
506
477
|
|
|
507
|
-
const
|
|
508
|
-
.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
478
|
+
const getMarkdownStructure = (source) => {
|
|
479
|
+
const lines = source.replace(/\r\n?/g, '\n').split('\n');
|
|
480
|
+
let bodyStart = 0;
|
|
481
|
+
if (lines[0]?.trim() === '---') {
|
|
482
|
+
const closingIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
483
|
+
if (closingIndex < 0) return { bodyStart: lines.length, headings: [], lines };
|
|
484
|
+
bodyStart = closingIndex + 1;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const headings = [];
|
|
488
|
+
let fence = null;
|
|
489
|
+
for (let index = bodyStart; index < lines.length; index += 1) {
|
|
490
|
+
const line = lines[index];
|
|
491
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})([^\r\n]*)$/);
|
|
492
|
+
if (fenceMatch) {
|
|
493
|
+
if (!fence) {
|
|
494
|
+
fence = { character: fenceMatch[1][0], length: fenceMatch[1].length };
|
|
495
|
+
} else if (
|
|
496
|
+
fenceMatch[1][0] === fence.character
|
|
497
|
+
&& fenceMatch[1].length >= fence.length
|
|
498
|
+
&& !fenceMatch[2].trim()
|
|
499
|
+
) {
|
|
500
|
+
fence = null;
|
|
501
|
+
}
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (fence) continue;
|
|
505
|
+
|
|
506
|
+
const match = line.match(/^(#{1,2})\s+(.+?)\s*$/);
|
|
507
|
+
if (match) headings.push({ level: match[1].length, line: index + 1, source: line });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { bodyStart, headings, lines };
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const getPageTitleDiagnostics = (structure) => {
|
|
514
|
+
const { bodyStart, headings, lines } = structure;
|
|
515
|
+
const pageHeadings = headings.filter((heading) => heading.level === 1);
|
|
516
|
+
const diagnostics = [];
|
|
517
|
+
|
|
518
|
+
if (pageHeadings.length === 0) {
|
|
519
|
+
diagnostics.push({
|
|
520
|
+
code: 'missing-page-title',
|
|
512
521
|
severity: 'error',
|
|
513
|
-
line:
|
|
514
|
-
message: '
|
|
515
|
-
}
|
|
516
|
-
})
|
|
522
|
+
line: 1,
|
|
523
|
+
message: 'Norna pages require exactly one Markdown H1 page title, for example "# About".',
|
|
524
|
+
});
|
|
525
|
+
} else if (pageHeadings.length > 1) {
|
|
526
|
+
for (const heading of pageHeadings.slice(1)) {
|
|
527
|
+
diagnostics.push({
|
|
528
|
+
code: 'duplicate-page-title',
|
|
529
|
+
severity: 'error',
|
|
530
|
+
line: heading.line,
|
|
531
|
+
message: 'A Norna page can contain only one Markdown H1. Use a level 2 heading with an explicit id for a section.',
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (headings[0] && headings[0].level !== 1) {
|
|
537
|
+
diagnostics.push({
|
|
538
|
+
code: 'page-title-order',
|
|
539
|
+
severity: 'error',
|
|
540
|
+
line: headings[0].line,
|
|
541
|
+
message: 'The Markdown H1 page title must come before every level 2 section.',
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const firstHeadingLineIndex = (headings[0]?.line ?? lines.length + 1) - 1;
|
|
546
|
+
if (lines.slice(bodyStart, firstHeadingLineIndex).some((line) => line.trim())) {
|
|
547
|
+
diagnostics.push({
|
|
548
|
+
code: 'page-title-order',
|
|
549
|
+
severity: 'error',
|
|
550
|
+
line: bodyStart + 1,
|
|
551
|
+
message: 'The Markdown H1 page title must be the first content after optional frontmatter.',
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return diagnostics;
|
|
556
|
+
};
|
|
517
557
|
|
|
518
558
|
export const getMarkdownDiagnostics = async ({ documentPath, source }) => {
|
|
559
|
+
const markdownStructure = getMarkdownStructure(source);
|
|
560
|
+
const { headings } = await getMarkdownHeadings(source);
|
|
519
561
|
const diagnostics = [
|
|
520
562
|
...getContentFrontmatterDiagnostics(source),
|
|
521
|
-
...
|
|
563
|
+
...getPageTitleDiagnostics(markdownStructure),
|
|
564
|
+
...getHeadingIdentifierIssues(headings).map((issue) => ({
|
|
565
|
+
code: issue.code,
|
|
566
|
+
severity: 'error',
|
|
567
|
+
line: issue.heading.line,
|
|
568
|
+
message: `${issue.message} ${issue.fix}`,
|
|
569
|
+
})),
|
|
522
570
|
];
|
|
523
571
|
const blockResult = extractNornaMarkdownBlockDiagnostics(source, { label: path.basename(documentPath) });
|
|
524
572
|
for (const error of blockResult.errors) {
|
|
@@ -547,9 +595,7 @@ export const getMarkdownDiagnostics = async ({ documentPath, source }) => {
|
|
|
547
595
|
|
|
548
596
|
const index = await createSiteImageIndex(siteRoot);
|
|
549
597
|
for (const reference of getImageReferenceLines(source)) {
|
|
550
|
-
const
|
|
551
|
-
if (!section) continue;
|
|
552
|
-
const expectedPath = path.join(page.imagesRoot, section.id, reference.filename);
|
|
598
|
+
const expectedPath = path.join(page.imagesRoot, reference.filename);
|
|
553
599
|
if (await fileExists(expectedPath)) continue;
|
|
554
600
|
|
|
555
601
|
const candidates = index.filesByName.get(reference.filename) ?? [];
|
|
@@ -558,7 +604,7 @@ export const getMarkdownDiagnostics = async ({ documentPath, source }) => {
|
|
|
558
604
|
code: 'missing-image',
|
|
559
605
|
severity: 'error',
|
|
560
606
|
line: reference.line,
|
|
561
|
-
message: `Image "${reference.filename}" was not found in
|
|
607
|
+
message: `Image "${reference.filename}" was not found in this page's images folder or elsewhere in this Norna site.`,
|
|
562
608
|
});
|
|
563
609
|
continue;
|
|
564
610
|
}
|
|
@@ -586,7 +632,7 @@ export const getMarkdownDiagnostics = async ({ documentPath, source }) => {
|
|
|
586
632
|
line: reference.line,
|
|
587
633
|
message: otherReferences.length > 0
|
|
588
634
|
? `Image "${reference.filename}" is stored at ${toPosixPath(path.relative(siteRoot, candidate.absolutePath))} and is still referenced by ${otherReferences.join(', ')}. Norna cannot relocate it safely.`
|
|
589
|
-
: `Image "${reference.filename}" is stored at ${toPosixPath(path.relative(siteRoot, candidate.absolutePath))}. Run "norna content:sync" to relocate it to this
|
|
635
|
+
: `Image "${reference.filename}" is stored at ${toPosixPath(path.relative(siteRoot, candidate.absolutePath))}. Run "norna content:sync" to relocate it to this page's images folder.`,
|
|
590
636
|
});
|
|
591
637
|
}
|
|
592
638
|
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readdir } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { homePageDirectory } from './site-conventions.mjs';
|
|
4
5
|
|
|
5
6
|
export const exampleCategories = ['complete-sites', 'feature-demos'];
|
|
6
7
|
|
|
8
|
+
const isGeneratedOnlyCacheDirectory = async (exampleDirectory, siteDirectory) => {
|
|
9
|
+
if (!existsSync(path.join(siteDirectory, '.norna'))) return false;
|
|
10
|
+
|
|
11
|
+
const exampleEntries = await readdir(exampleDirectory);
|
|
12
|
+
const siteEntries = await readdir(siteDirectory);
|
|
13
|
+
return exampleEntries.every((name) => name === 'site' || name === '.DS_Store')
|
|
14
|
+
&& siteEntries.every((name) => name === '.norna' || name === '.DS_Store');
|
|
15
|
+
};
|
|
16
|
+
|
|
7
17
|
export const getExampleSites = async (root) => {
|
|
8
18
|
const examples = [];
|
|
9
19
|
|
|
@@ -12,12 +22,14 @@ export const getExampleSites = async (root) => {
|
|
|
12
22
|
const entries = await readdir(categoryDirectory, { withFileTypes: true });
|
|
13
23
|
|
|
14
24
|
for (const entry of entries.filter((candidate) => candidate.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
15
|
-
const
|
|
25
|
+
const exampleDirectory = path.join(categoryDirectory, entry.name);
|
|
26
|
+
const siteDirectory = path.join(exampleDirectory, 'site');
|
|
16
27
|
if (!existsSync(path.join(siteDirectory, 'config.yaml'))) {
|
|
28
|
+
if (await isGeneratedOnlyCacheDirectory(exampleDirectory, siteDirectory)) continue;
|
|
17
29
|
throw new Error(`Example ${path.relative(root, siteDirectory)} is missing config.yaml.`);
|
|
18
30
|
}
|
|
19
|
-
if (!existsSync(path.join(siteDirectory, 'content.md'))) {
|
|
20
|
-
throw new Error(`Example ${path.relative(root, siteDirectory)} is missing content.md.`);
|
|
31
|
+
if (!existsSync(path.join(siteDirectory, 'pages', homePageDirectory, 'content.md'))) {
|
|
32
|
+
throw new Error(`Example ${path.relative(root, siteDirectory)} is missing pages/${homePageDirectory}/content.md.`);
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
examples.push({
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { markdownToMdast } from 'satteri';
|
|
2
|
+
|
|
3
|
+
export const explicitHeadingIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4
|
+
export const reservedHeadingIds = new Set(['page-title']);
|
|
5
|
+
|
|
6
|
+
const explicitHeadingIdSuffixPattern = /\s*\{#([^{}]*)\}\s*$/;
|
|
7
|
+
const transliterations = new Map([
|
|
8
|
+
['æ', 'ae'],
|
|
9
|
+
['đ', 'd'],
|
|
10
|
+
['ð', 'd'],
|
|
11
|
+
['ħ', 'h'],
|
|
12
|
+
['ı', 'i'],
|
|
13
|
+
['ł', 'l'],
|
|
14
|
+
['ø', 'o'],
|
|
15
|
+
['œ', 'oe'],
|
|
16
|
+
['ß', 'ss'],
|
|
17
|
+
['þ', 'th'],
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const transliterateToAscii = (value) => Array.from(value)
|
|
21
|
+
.map((character) => transliterations.get(character) ?? character)
|
|
22
|
+
.join('');
|
|
23
|
+
|
|
24
|
+
export const slugifyHeadingText = (value) => transliterateToAscii(value
|
|
25
|
+
.normalize('NFKD')
|
|
26
|
+
.toLowerCase())
|
|
27
|
+
.replace(/\p{Mark}+/gu, '')
|
|
28
|
+
.replace(/['’]/g, '')
|
|
29
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
30
|
+
.replace(/^-+|-+$/g, '');
|
|
31
|
+
|
|
32
|
+
const getNodeText = (node) => {
|
|
33
|
+
if (!node || typeof node !== 'object') return '';
|
|
34
|
+
if (node.type === 'text' || node.type === 'inlineCode') return node.value ?? '';
|
|
35
|
+
if (node.type === 'image') return node.alt ?? '';
|
|
36
|
+
if (!Array.isArray(node.children)) return '';
|
|
37
|
+
return node.children.map(getNodeText).join('');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const resolveHeadingIdentifier = (headingText, depth) => {
|
|
41
|
+
const explicitMatch = headingText.match(explicitHeadingIdSuffixPattern);
|
|
42
|
+
const explicitSource = explicitMatch?.[1];
|
|
43
|
+
const title = explicitMatch
|
|
44
|
+
? headingText.slice(0, explicitMatch.index).trim()
|
|
45
|
+
: headingText.trim();
|
|
46
|
+
const explicitId = explicitSource && explicitHeadingIdPattern.test(explicitSource)
|
|
47
|
+
? explicitSource
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
depth,
|
|
52
|
+
explicitId,
|
|
53
|
+
explicitSource: explicitSource ?? null,
|
|
54
|
+
id: depth === 1 ? null : explicitId ?? slugifyHeadingText(title),
|
|
55
|
+
title,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const getMarkdownHeadings = async (source) => {
|
|
60
|
+
const normalizedSource = source.replace(/\r\n?/g, '\n');
|
|
61
|
+
const tree = await markdownToMdast(normalizedSource, {
|
|
62
|
+
features: {
|
|
63
|
+
frontmatter: true,
|
|
64
|
+
gfm: true,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const headings = tree.children
|
|
69
|
+
.filter((node) => node.type === 'heading' && node.depth >= 1 && node.depth <= 3)
|
|
70
|
+
.map((node) => ({
|
|
71
|
+
...resolveHeadingIdentifier(getNodeText(node), node.depth),
|
|
72
|
+
index: node.position?.start.offset ?? 0,
|
|
73
|
+
line: node.position?.start.line ?? 1,
|
|
74
|
+
source: normalizedSource.slice(
|
|
75
|
+
node.position?.start.offset ?? 0,
|
|
76
|
+
node.position?.end.offset ?? node.position?.start.offset ?? 0,
|
|
77
|
+
),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
return { headings, source: normalizedSource };
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const getHeadingIdentifierIssues = (headings) => {
|
|
84
|
+
const issues = [];
|
|
85
|
+
const headingsById = new Map();
|
|
86
|
+
|
|
87
|
+
for (const heading of headings) {
|
|
88
|
+
if (heading.depth === 1) {
|
|
89
|
+
if (heading.explicitSource !== null) {
|
|
90
|
+
issues.push({
|
|
91
|
+
code: 'page-title-id',
|
|
92
|
+
heading,
|
|
93
|
+
message: 'The Markdown H1 is the page title and must not have an id.',
|
|
94
|
+
fix: 'Remove the {#...} suffix. Heading ids apply to H2 and H3.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (heading.explicitSource !== null && !heading.explicitId) {
|
|
101
|
+
issues.push({
|
|
102
|
+
code: 'invalid-heading-id',
|
|
103
|
+
heading,
|
|
104
|
+
message: `Heading "${heading.title}" has an invalid explicit id "${heading.explicitSource}".`,
|
|
105
|
+
fix: 'Use lowercase ASCII letters, numbers, and single hyphens, for example {#getting-started}.',
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!heading.id) {
|
|
111
|
+
issues.push({
|
|
112
|
+
code: 'empty-heading-id',
|
|
113
|
+
heading,
|
|
114
|
+
message: `Heading "${heading.title}" cannot produce an automatic ASCII id.`,
|
|
115
|
+
fix: `Add an explicit id, for example ${'#'.repeat(heading.depth)} ${heading.title} {#section-id}.`,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (reservedHeadingIds.has(heading.id)) {
|
|
121
|
+
issues.push({
|
|
122
|
+
code: 'reserved-heading-id',
|
|
123
|
+
heading,
|
|
124
|
+
message: `Heading id "${heading.id}" is reserved by Norna.`,
|
|
125
|
+
fix: 'Choose another explicit id.',
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const existing = headingsById.get(heading.id);
|
|
131
|
+
if (existing) {
|
|
132
|
+
issues.push({
|
|
133
|
+
code: 'duplicate-heading-id',
|
|
134
|
+
heading,
|
|
135
|
+
otherHeading: existing,
|
|
136
|
+
message: `Two headings resolve to id "${heading.id}".`,
|
|
137
|
+
fix: `Add a unique explicit id to at least one heading, for example {#${heading.id}-details}.`,
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
headingsById.set(heading.id, heading);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return issues;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const formatHeadingIdentifierIssue = (issue, sourceLabel = 'Markdown') => {
|
|
149
|
+
const lines = [`${sourceLabel}: ${issue.message}`];
|
|
150
|
+
if (issue.otherHeading) {
|
|
151
|
+
lines.push(
|
|
152
|
+
`- line ${issue.otherHeading.line}: "${issue.otherHeading.title}"`,
|
|
153
|
+
`- line ${issue.heading.line}: "${issue.heading.title}"`,
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
lines.push(`- line ${issue.heading.line}: "${issue.heading.title}"`);
|
|
157
|
+
}
|
|
158
|
+
if (issue.fix) lines.push(issue.fix);
|
|
159
|
+
|
|
160
|
+
return lines.join('\n');
|
|
161
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const navigationModeNames = Object.freeze([
|
|
2
|
+
'automatic',
|
|
3
|
+
'sections',
|
|
4
|
+
'top',
|
|
5
|
+
'tree',
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
const assertNavigationMode = (mode) => {
|
|
9
|
+
if (!navigationModeNames.includes(mode)) {
|
|
10
|
+
throw new Error(`Unknown navigation mode "${mode}". Use one of: ${navigationModeNames.join(', ')}.`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return mode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const getListedPages = (pages) => pages.filter((page) => page.isHome || page.listed !== false);
|
|
17
|
+
|
|
18
|
+
const getPageNavigationDepth = (page) => {
|
|
19
|
+
const pageDepth = page.depth ?? 1;
|
|
20
|
+
return (page.headings ?? []).reduce((maximum, heading) => (
|
|
21
|
+
Math.max(maximum, pageDepth + heading.depth - 1)
|
|
22
|
+
), pageDepth);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getAutomaticNavigationMode = (pages) => {
|
|
26
|
+
const listedPages = getListedPages(pages);
|
|
27
|
+
if (listedPages.length <= 1) return 'sections';
|
|
28
|
+
|
|
29
|
+
const maximumDepth = listedPages.reduce((maximum, page) => (
|
|
30
|
+
Math.max(maximum, getPageNavigationDepth(page))
|
|
31
|
+
), 1);
|
|
32
|
+
|
|
33
|
+
return maximumDepth <= 2 ? 'top' : 'tree';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const resolveNavigationModel = ({ mode = 'automatic', pages }) => {
|
|
37
|
+
const requestedMode = assertNavigationMode(mode);
|
|
38
|
+
const listedPages = getListedPages(pages);
|
|
39
|
+
const maximumDepth = listedPages.reduce((maximum, page) => (
|
|
40
|
+
Math.max(maximum, getPageNavigationDepth(page))
|
|
41
|
+
), 1);
|
|
42
|
+
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
mode: requestedMode === 'automatic'
|
|
45
|
+
? getAutomaticNavigationMode(listedPages)
|
|
46
|
+
: requestedMode,
|
|
47
|
+
requestedMode,
|
|
48
|
+
listedPageCount: listedPages.length,
|
|
49
|
+
maximumDepth,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
@@ -3,12 +3,11 @@ import { documentationLink } from './documentation-links.mjs';
|
|
|
3
3
|
|
|
4
4
|
const field = (description, options = {}) => Object.freeze({ description, ...options });
|
|
5
5
|
const value = (title, description) => Object.freeze({ title, description });
|
|
6
|
-
const nornaBlocksDocumentation = documentationLink('Norna blocks reference', 'content.md', 'norna-blocks');
|
|
7
6
|
|
|
8
7
|
export const nornaMarkdownBlockDefinitions = Object.freeze({
|
|
9
8
|
'norna-image-stack': Object.freeze({
|
|
10
9
|
description: 'Display one or more managed images in a vertical stack.',
|
|
11
|
-
documentation:
|
|
10
|
+
documentation: documentationLink('Image stack reference', 'content.md', 'image-stack'),
|
|
12
11
|
item: Object.freeze({
|
|
13
12
|
start: field('Start another managed image.', { key: 'image', prefix: '- ' }),
|
|
14
13
|
fields: Object.freeze({
|
|
@@ -19,7 +18,7 @@ export const nornaMarkdownBlockDefinitions = Object.freeze({
|
|
|
19
18
|
}),
|
|
20
19
|
'norna-image-carousel': Object.freeze({
|
|
21
20
|
description: 'Display two or more managed images in an interactive carousel.',
|
|
22
|
-
documentation:
|
|
21
|
+
documentation: documentationLink('Image carousel reference', 'content.md', 'image-carousel'),
|
|
23
22
|
item: Object.freeze({
|
|
24
23
|
start: field('Start another managed carousel image.', { key: 'image', prefix: '- ' }),
|
|
25
24
|
fields: Object.freeze({
|
|
@@ -30,7 +29,7 @@ export const nornaMarkdownBlockDefinitions = Object.freeze({
|
|
|
30
29
|
}),
|
|
31
30
|
'norna-card-list': Object.freeze({
|
|
32
31
|
description: 'Display a structured list of cards.',
|
|
33
|
-
documentation:
|
|
32
|
+
documentation: documentationLink('Card list reference', 'content.md', 'card-list'),
|
|
34
33
|
options: Object.freeze({
|
|
35
34
|
layout: field('Place each card image above, left, or right of its text. Defaults to image-top.', {
|
|
36
35
|
default: 'image-top',
|