@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.
Files changed (342) hide show
  1. package/README.md +18 -9
  2. package/astro.config.mjs +35 -3
  3. package/bin/norna-cli.mjs +1 -1
  4. package/docs/README.md +16 -14
  5. package/docs/commands.md +2 -2
  6. package/docs/configuration.md +25 -2
  7. package/docs/content.md +92 -103
  8. package/docs/design/navigation-and-theme-plan.md +186 -0
  9. package/docs/design/norna-diagram-design.md +9 -9
  10. package/docs/design/site-examples-structure.md +15 -11
  11. package/docs/engine-development.md +19 -22
  12. package/docs/getting-started.md +21 -16
  13. package/docs/images-and-metadata.md +12 -9
  14. package/docs/pages.md +190 -0
  15. package/docs/public-files.md +49 -36
  16. package/docs/publishing.md +19 -3
  17. package/docs/site-files.md +46 -38
  18. package/docs/sitewide-content.md +20 -36
  19. package/docs/theme.md +109 -143
  20. package/docs/typography.md +19 -31
  21. package/examples/README.md +10 -4
  22. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +17 -41
  23. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +26 -67
  24. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +185 -88
  25. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
  26. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev-local.json +4 -4
  27. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +5 -5
  28. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +33 -23
  29. package/examples/complete-sites/dog-shelter-multi-page/README.md +1 -1
  30. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +8 -8
  31. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +20 -20
  32. package/examples/complete-sites/dog-shelter-multi-page/site/{content.md → pages/000-home/content.md} +3 -4
  33. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/010-dogs/content.md +3 -4
  34. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/020-adopt/content.md +3 -4
  35. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +2 -4
  36. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +17 -41
  37. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +26 -67
  38. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +185 -88
  39. package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -1
  40. package/examples/complete-sites/dog-shelter-single-page/.astro/dev-local.json +8 -0
  41. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +5 -5
  42. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +16 -12
  43. package/examples/complete-sites/dog-shelter-single-page/README.md +1 -1
  44. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +8 -8
  45. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +12 -12
  46. package/examples/complete-sites/dog-shelter-single-page/site/{content.md → pages/000-home/content.md} +5 -2
  47. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +2 -4
  48. package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +17 -41
  49. package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +26 -67
  50. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +180 -91
  51. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  52. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +4 -14
  53. package/examples/feature-demos/media-and-surfaces/README.md +2 -2
  54. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +9 -9
  55. package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +16 -16
  56. package/examples/feature-demos/media-and-surfaces/site/{content.md → pages/000-home/content.md} +4 -7
  57. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/010-media/content.md +6 -5
  58. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/content.md +9 -7
  59. package/examples/feature-demos/media-and-surfaces/site/pages/020-surfaces/theme.yaml +2 -0
  60. package/examples/feature-demos/media-and-surfaces/site/sitewide-content.yaml +0 -2
  61. package/examples/feature-demos/sitewide-content/README.md +3 -3
  62. package/examples/feature-demos/sitewide-content/site/pages/000-home/content.md +17 -0
  63. package/examples/feature-demos/sitewide-content/site/pages/010-logo-and-navigation/content.md +17 -0
  64. package/examples/feature-demos/sitewide-content/site/{routes → pages}/020-notices/content.md +7 -6
  65. package/examples/feature-demos/sitewide-content/site/{routes → pages}/030-footer/content.md +6 -5
  66. package/examples/feature-demos/sitewide-content/site/sitewide-content.yaml +4 -13
  67. package/examples/feature-demos/theme-preset-documentation/README.md +16 -0
  68. package/examples/feature-demos/theme-preset-documentation/site/.norna/generated-images.json +74 -0
  69. package/examples/feature-demos/{theme-presets/site/routes/020-documentation → theme-preset-documentation/site/pages/000-home}/content.md +4 -7
  70. package/examples/feature-demos/theme-preset-documentation/site/sitewide-content.yaml +3 -0
  71. package/examples/feature-demos/theme-preset-portfolio/README.md +16 -0
  72. package/examples/feature-demos/theme-preset-portfolio/site/.norna/generated-images.json +74 -0
  73. package/examples/feature-demos/theme-preset-portfolio/site/config.yaml +1 -0
  74. package/examples/feature-demos/{theme-presets/site/routes/010-portfolio → theme-preset-portfolio/site/pages/000-home}/content.md +4 -7
  75. package/examples/feature-demos/theme-preset-portfolio/site/public/robots.txt +2 -0
  76. package/examples/feature-demos/theme-preset-portfolio/site/sitewide-content.yaml +3 -0
  77. package/examples/feature-demos/theme-preset-project/README.md +16 -0
  78. package/examples/feature-demos/theme-preset-project/site/.norna/generated-images.json +74 -0
  79. package/examples/feature-demos/theme-preset-project/site/config.yaml +1 -0
  80. package/examples/feature-demos/{theme-presets/site/routes/030-project → theme-preset-project/site/pages/000-home}/content.md +4 -7
  81. package/examples/feature-demos/theme-preset-project/site/public/robots.txt +2 -0
  82. package/examples/feature-demos/theme-preset-project/site/sitewide-content.yaml +3 -0
  83. package/examples/feature-demos/theme-preset-statement/README.md +16 -0
  84. package/examples/feature-demos/theme-preset-statement/site/.norna/generated-images.json +74 -0
  85. package/examples/feature-demos/theme-preset-statement/site/config.yaml +1 -0
  86. package/examples/feature-demos/{theme-presets/site/routes/040-statement → theme-preset-statement/site/pages/000-home}/content.md +4 -7
  87. package/examples/feature-demos/theme-preset-statement/site/public/robots.txt +2 -0
  88. package/examples/feature-demos/theme-preset-statement/site/sitewide-content.yaml +3 -0
  89. package/fixtures/basic/site/{content.md → pages/000-home/content.md} +5 -6
  90. package/fixtures/basic/site/sitewide-content.yaml +1 -8
  91. package/fixtures/basic/site/theme.yaml +1 -8
  92. package/fixtures/content-model-v2/site/{content.md → pages/000-home/content.md} +11 -13
  93. package/fixtures/content-model-v2/site/pages/010-guide/content.md +18 -0
  94. package/fixtures/nested-pages/.astro/collections/site.schema.json +35 -0
  95. package/fixtures/nested-pages/.astro/collections/sitewide.schema.json +97 -0
  96. package/fixtures/nested-pages/.astro/collections/theme.schema.json +844 -0
  97. package/fixtures/nested-pages/.astro/content-assets.mjs +4 -0
  98. package/fixtures/nested-pages/.astro/content-modules.mjs +1 -0
  99. package/fixtures/nested-pages/.astro/content.d.ts +183 -0
  100. package/fixtures/nested-pages/.astro/data-store.json +1 -0
  101. package/fixtures/nested-pages/.astro/dev-local.json +8 -0
  102. package/fixtures/nested-pages/.astro/dev.log +1128 -0
  103. package/fixtures/nested-pages/.astro/settings.json +5 -0
  104. package/fixtures/nested-pages/.astro/types.d.ts +2 -0
  105. package/fixtures/nested-pages/node_modules/.vite/deps/_metadata.json +74 -0
  106. package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  107. package/fixtures/nested-pages/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  108. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  109. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  110. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  111. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  112. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  113. package/fixtures/nested-pages/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  114. package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  115. package/fixtures/nested-pages/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  116. package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  117. package/fixtures/nested-pages/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  118. package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  119. package/fixtures/nested-pages/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  120. package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  121. package/fixtures/nested-pages/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  122. package/fixtures/nested-pages/node_modules/.vite/deps/package.json +3 -0
  123. package/fixtures/nested-pages/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  124. package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  125. package/fixtures/nested-pages/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  126. package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  127. package/fixtures/nested-pages/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  128. package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  129. package/fixtures/nested-pages/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  130. package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  131. package/fixtures/nested-pages/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  132. package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  133. package/fixtures/nested-pages/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  134. package/fixtures/nested-pages/site/.norna/generated-images.json +10 -0
  135. package/fixtures/nested-pages/site/config.yaml +3 -0
  136. package/fixtures/nested-pages/site/pages/000-home/content.md +15 -0
  137. package/fixtures/nested-pages/site/pages/010-guides/content.md +15 -0
  138. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/content.md +21 -0
  139. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/images/diagram.svg +21 -0
  140. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +18 -0
  141. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/theme.yaml +2 -0
  142. package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +11 -0
  143. package/fixtures/nested-pages/site/pages/010-guides/pages/030-release-notes/content.md +9 -0
  144. package/fixtures/nested-pages/site/pages/010-guides/pages/040-configuration/content.md +3 -0
  145. package/fixtures/nested-pages/site/pages/010-guides/pages/050-content/content.md +3 -0
  146. package/fixtures/nested-pages/site/pages/010-guides/pages/060-themes/content.md +3 -0
  147. package/fixtures/nested-pages/site/pages/010-guides/pages/070-images/content.md +3 -0
  148. package/fixtures/nested-pages/site/pages/010-guides/pages/080-cards/content.md +3 -0
  149. package/fixtures/nested-pages/site/pages/010-guides/pages/090-notes/content.md +3 -0
  150. package/fixtures/nested-pages/site/pages/010-guides/pages/100-validation/content.md +3 -0
  151. package/fixtures/nested-pages/site/pages/010-guides/pages/110-synchronization/content.md +3 -0
  152. package/fixtures/nested-pages/site/pages/010-guides/pages/120-building/content.md +3 -0
  153. package/fixtures/nested-pages/site/pages/010-guides/pages/130-publishing/content.md +3 -0
  154. package/fixtures/nested-pages/site/pages/010-guides/pages/140-troubleshooting/content.md +3 -0
  155. package/fixtures/nested-pages/site/pages/010-guides/pages/150-migration/content.md +3 -0
  156. package/fixtures/nested-pages/site/pages/010-guides/pages/160-accessibility/content.md +3 -0
  157. package/fixtures/nested-pages/site/pages/010-guides/pages/170-performance/content.md +3 -0
  158. package/fixtures/nested-pages/site/pages/010-guides/pages/180-security/content.md +3 -0
  159. package/fixtures/nested-pages/site/pages/010-guides/pages/190-localization/content.md +3 -0
  160. package/fixtures/nested-pages/site/pages/010-guides/pages/200-upgrading/content.md +3 -0
  161. package/fixtures/nested-pages/site/pages/010-guides/pages/210-faq/content.md +3 -0
  162. package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +7 -0
  163. package/fixtures/nested-pages/site/pages/020-reference/content.md +10 -0
  164. package/fixtures/nested-pages/site/pages/020-reference/pages/010-installation/content.md +11 -0
  165. package/fixtures/nested-pages/site/theme.yaml +1 -0
  166. package/package.json +8 -4
  167. package/schemas/config.schema.json +46 -5
  168. package/schemas/content-frontmatter.schema.json +28 -63
  169. package/schemas/manifest.json +4 -2
  170. package/schemas/page-theme.schema.json +192 -0
  171. package/schemas/sitewide-content.schema.json +58 -132
  172. package/schemas/theme.schema.json +434 -152
  173. package/scripts/check-config.mjs +8 -6
  174. package/scripts/deploy-site.mjs +6 -8
  175. package/scripts/doctor.mjs +2 -2
  176. package/scripts/generate-images.mjs +15 -29
  177. package/scripts/generate-schemas.mjs +7 -218
  178. package/scripts/lib/base-path-redirect.mjs +9 -0
  179. package/scripts/lib/editor-language-service.mjs +125 -79
  180. package/scripts/lib/example-sites.mjs +15 -3
  181. package/scripts/lib/heading-ids.mjs +161 -0
  182. package/scripts/lib/navigation-model.mjs +51 -0
  183. package/scripts/lib/norna-markdown-blocks.mjs +3 -4
  184. package/scripts/lib/page-model.mjs +102 -0
  185. package/scripts/lib/presentation.mjs +25 -17
  186. package/scripts/lib/project-config.mjs +46 -8
  187. package/scripts/lib/schema-definitions.mjs +78 -55
  188. package/scripts/lib/schema-editor-metadata.mjs +519 -0
  189. package/scripts/lib/schema-value-definitions.mjs +20 -6
  190. package/scripts/lib/site-content.mjs +159 -97
  191. package/scripts/lib/site-conventions.mjs +1 -0
  192. package/scripts/lib/site-paths.mjs +11 -5
  193. package/scripts/lib/theme-config.mjs +12 -18
  194. package/scripts/lib/theme-presets.mjs +55 -14
  195. package/scripts/release.mjs +1 -1
  196. package/scripts/show-typography.mjs +4 -30
  197. package/scripts/sync-content-sections.mjs +107 -68
  198. package/scripts/{test-temporary-visibility.mjs → test-banner-visibility.mjs} +8 -25
  199. package/scripts/test-client-javascript.mjs +50 -46
  200. package/scripts/test-content-check.mjs +153 -69
  201. package/scripts/test-content-model-v2.mjs +566 -285
  202. package/scripts/test-documentation.mjs +10 -3
  203. package/scripts/test-editor-language-service.mjs +172 -25
  204. package/scripts/test-engine-commands.mjs +6 -5
  205. package/scripts/test-examples.mjs +30 -1
  206. package/scripts/test-heading-ids.mjs +68 -0
  207. package/scripts/test-navigation-model.mjs +63 -0
  208. package/scripts/test-navigation.mjs +11 -2
  209. package/scripts/test-nested-pages.mjs +125 -0
  210. package/scripts/test-package-check.mjs +55 -95
  211. package/scripts/test-page-model.mjs +108 -0
  212. package/scripts/test-project-config.mjs +22 -3
  213. package/scripts/test-schemas.mjs +82 -15
  214. package/scripts/test-theme-presets.mjs +57 -42
  215. package/src/components/NavigationPageTree.astro +157 -0
  216. package/src/components/PageContentsLinks.astro +40 -0
  217. package/src/components/SectionNavigationScript.astro +117 -5
  218. package/src/components/SiteBreadcrumbs.astro +32 -0
  219. package/src/components/SiteNavigation.astro +97 -68
  220. package/src/components/SitePage.astro +93 -55
  221. package/src/components/SiteSection.astro +29 -27
  222. package/src/components/SiteTreeNavigation.astro +35 -0
  223. package/src/components/TreeNavigationScript.astro +83 -0
  224. package/src/content.config.ts +8 -10
  225. package/src/layouts/BaseLayout.astro +21 -20
  226. package/src/lib/pageThemes.ts +44 -0
  227. package/src/lib/sectionContent.ts +106 -107
  228. package/src/lib/siteNavigation.ts +49 -0
  229. package/src/lib/sitePages.ts +96 -38
  230. package/src/pages/{[slug].astro → [...slug].astro} +2 -2
  231. package/src/pages/index.astro +2 -1
  232. package/src/styles/global.css +743 -57
  233. package/starters/basic/README.md +9 -11
  234. package/starters/basic/site/{content.md → pages/000-home/content.md} +7 -4
  235. package/starters/basic/site/sitewide-content.yaml +1 -10
  236. package/starters/basic/site/theme.yaml +1 -105
  237. package/starters/project/README.md +14 -14
  238. package/starters/project/site/{content.md → pages/000-home/content.md} +5 -4
  239. package/starters/project/site/{routes → pages}/010-guide/content.md +5 -4
  240. package/starters/project/site/sitewide-content.yaml +1 -10
  241. package/starters/project/site/theme.yaml +1 -31
  242. package/docs/routes.md +0 -121
  243. package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.yaml +0 -3
  244. package/examples/feature-demos/sitewide-content/site/content.md +0 -16
  245. package/examples/feature-demos/sitewide-content/site/routes/010-identity/content.md +0 -20
  246. package/examples/feature-demos/theme-presets/README.md +0 -18
  247. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +0 -290
  248. package/examples/feature-demos/theme-presets/site/content.md +0 -17
  249. package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +0 -12
  250. package/examples/feature-demos/theme-presets/site/theme.yaml +0 -1
  251. package/fixtures/content-model-v2/site/routes/010-guide/content.md +0 -17
  252. package/scripts/lib/route-model.mjs +0 -17
  253. package/scripts/test-route-model.mjs +0 -32
  254. package/src/lib/routeThemes.ts +0 -41
  255. /package/examples/complete-sites/dog-shelter-multi-page/site/{images/intro → pages/000-home/images}/shelter-dog.jpg +0 -0
  256. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rex.jpg +0 -0
  257. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rover.jpg +0 -0
  258. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/020-adopt/images/adopt → pages/020-adopt/images}/adoption-dog.jpg +0 -0
  259. /package/examples/complete-sites/dog-shelter-single-page/site/{images/you-can-help → pages/000-home/images}/foster-dog.jpg +0 -0
  260. /package/examples/complete-sites/dog-shelter-single-page/site/{images/what-we-do → pages/000-home/images}/rescue-dog.jpg +0 -0
  261. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-carousel.svg +0 -0
  262. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-stack.svg +0 -0
  263. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-surfaces.svg +0 -0
  264. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-one.svg +0 -0
  265. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-three.svg +0 -0
  266. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-two.svg +0 -0
  267. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-one.svg +0 -0
  268. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-two.svg +0 -0
  269. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/config.yaml +0 -0
  270. /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
  271. /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
  272. /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
  273. /package/examples/feature-demos/{theme-presets → theme-preset-documentation}/site/public/robots.txt +0 -0
  274. /package/examples/feature-demos/{theme-presets/site/routes/020-documentation → theme-preset-documentation/site}/theme.yaml +0 -0
  275. /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
  276. /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
  277. /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
  278. /package/examples/feature-demos/{theme-presets/site/routes/010-portfolio → theme-preset-portfolio/site}/theme.yaml +0 -0
  279. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/routeTheme.schema.json +0 -0
  280. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/site.schema.json +0 -0
  281. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/sitewide.schema.json +0 -0
  282. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/collections/theme.schema.json +0 -0
  283. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-assets.mjs +0 -0
  284. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content-modules.mjs +0 -0
  285. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/content.d.ts +0 -0
  286. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/data-store.json +0 -0
  287. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev-local.json +0 -0
  288. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.json +0 -0
  289. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/dev.log +0 -0
  290. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/settings.json +0 -0
  291. /package/examples/feature-demos/{theme-presets → theme-preset-project}/.astro/types.d.ts +0 -0
  292. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/_metadata.json +0 -0
  293. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js +0 -0
  294. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -0
  295. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js +0 -0
  296. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -0
  297. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js +0 -0
  298. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -0
  299. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js +0 -0
  300. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -0
  301. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -0
  302. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -0
  303. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js +0 -0
  304. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -0
  305. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js +0 -0
  306. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/embla-carousel.js.map +0 -0
  307. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js +0 -0
  308. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -0
  309. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js +0 -0
  310. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -0
  311. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/package.json +0 -0
  312. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -0
  313. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js +0 -0
  314. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -0
  315. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js +0 -0
  316. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -0
  317. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js +0 -0
  318. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -0
  319. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js +0 -0
  320. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -0
  321. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js +0 -0
  322. /package/examples/feature-demos/{theme-presets → theme-preset-project}/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -0
  323. /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
  324. /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
  325. /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
  326. /package/examples/feature-demos/{theme-presets/site/routes/030-project → theme-preset-project/site}/theme.yaml +0 -0
  327. /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
  328. /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
  329. /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
  330. /package/examples/feature-demos/{theme-presets/site/routes/040-statement → theme-preset-statement/site}/theme.yaml +0 -0
  331. /package/fixtures/basic/site/{images/work → pages/000-home/images}/.gitkeep +0 -0
  332. /package/fixtures/content-model-v2/site/{images/card-section → pages/000-home/images}/adopt.svg +0 -0
  333. /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/detail.jpg +0 -0
  334. /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/duplicate.jpg +0 -0
  335. /package/fixtures/content-model-v2/site/{images/card-section → pages/000-home/images}/foster.svg +0 -0
  336. /package/fixtures/content-model-v2/site/{images/intro → pages/000-home/images}/hero.jpg +0 -0
  337. /package/fixtures/content-model-v2/site/{images/plain → pages/000-home/images}/image.jpg +0 -0
  338. /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-one.jpg +0 -0
  339. /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-three.jpg +0 -0
  340. /package/fixtures/content-model-v2/site/{images/timed → pages/000-home/images}/slide-two.jpg +0 -0
  341. /package/fixtures/content-model-v2/site/{routes/010-guide/images/intro → pages/010-guide/images}/duplicate.jpg +0 -0
  342. /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 readSitewideNavigationForEditor = async (siteRoot) => {
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 navigation = data && typeof data === 'object' && data.navigation && typeof data.navigation === 'object'
49
- ? data.navigation
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) => /^\s+logo:\s*(?:#.*)?$/.test(line)) + 1;
53
+ .findIndex((line) => /^logo:\s*(?:#.*)?$/.test(line)) + 1;
53
54
  return {
54
55
  absolutePath,
55
- label: typeof navigation.label === 'string' ? navigation.label : null,
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 navigation = await readSitewideNavigationForEditor(siteRoot);
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 && navigation?.logoConfigured) {
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: navigation.absolutePath,
98
+ absolutePath: sitewideLogo.absolutePath,
99
99
  code: 'missing-logo-file',
100
- filename: path.basename(navigation.absolutePath),
101
- line: navigation.logoLine,
102
- message: `Navigation logo settings are configured, but no logo file was found. Add exactly one of ${logoAssetFilenames.map((filename) => `${publicLabel}/${filename}`).join(', ')}, or remove navigation.logo.`,
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
- navigation,
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
- if (relativePath === 'content.md') {
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 routeDirectory = match[1];
122
+ const pageDirectory = match[1];
132
123
  return {
133
124
  contentPath: absoluteDocumentPath,
134
- imagesRoot: path.join(siteRoot, 'routes', ...routeDirectory.split('/'), 'images'),
135
- pageLabel: routeDirectory,
136
- routeDirectory,
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 collectRouteImageRoots = async (directory, siteRoot, roots) => {
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 routeDirectory = toPosixPath(path.relative(path.join(siteRoot, 'routes'), path.dirname(absolutePath)));
323
- roots.push({ imageRoot: absolutePath, pageLabel: routeDirectory });
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 collectRouteImageRoots(absolutePath, siteRoot, roots);
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, 'routes'));
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 homeImageRoot = path.join(siteRoot, 'images');
367
- await collectImageFiles(homeImageRoot, homeImageRoot, 'Home', files);
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 section = getSectionAtLine(source, line);
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
- const section = getSectionAtLine(source, line);
438
- if (!siteRoot || !page || !section) return null;
407
+ if (!siteRoot || !page) return null;
439
408
 
440
- const expectedPath = path.join(page.imagesRoot, section.id, filename);
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 [{ severity: 'error', line: 1, message: 'Norna content files must start with YAML frontmatter delimited by ---.' }];
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 getSectionHeadingDiagnostics = (source) => source.replace(/\r\n?/g, '\n').split('\n')
508
- .flatMap((line, index) => {
509
- if (!/^##(?:\s|$)/.test(line) || headingIdPattern.test(line)) return [];
510
- return [{
511
- code: 'missing-section-id',
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: index + 1,
514
- message: 'Level 2 Norna section headings require an explicit id, for example "## Introduction {#introduction}".',
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
- ...getSectionHeadingDiagnostics(source),
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 section = getSectionAtLine(source, reference.line - 1);
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 the expected section folder or elsewhere in this Norna site.`,
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 section.`,
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 siteDirectory = path.join(categoryDirectory, entry.name, 'site');
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: nornaBlocksDocumentation,
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: nornaBlocksDocumentation,
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: nornaBlocksDocumentation,
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',