@janga/norna 0.7.17 → 0.7.22

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 (533) hide show
  1. package/README.md +32 -13
  2. package/astro.config.mjs +22 -0
  3. package/bin/norna-cli.mjs +7 -1
  4. package/package.json +46 -12
  5. package/schemas/category.schema.json +20 -0
  6. package/schemas/config.schema.json +55 -4
  7. package/schemas/content-frontmatter.schema.json +5 -5
  8. package/schemas/manifest.json +4 -2
  9. package/schemas/page-theme.schema.json +192 -0
  10. package/schemas/sitewide-content.schema.json +18 -18
  11. package/schemas/theme.schema.json +364 -226
  12. package/scripts/add-site-node.mjs +251 -0
  13. package/scripts/check-config.mjs +12 -3
  14. package/scripts/deploy-site.mjs +6 -9
  15. package/scripts/generate-images.mjs +18 -29
  16. package/scripts/generate-schemas.mjs +7 -1
  17. package/scripts/init-site.mjs +35 -0
  18. package/scripts/lib/astro-command.mjs +14 -9
  19. package/scripts/lib/editor-language-service.mjs +28 -191
  20. package/scripts/lib/heading-ids.mjs +3 -1
  21. package/scripts/lib/navigation-model.mjs +23 -17
  22. package/scripts/lib/norna-markdown-blocks.mjs +64 -145
  23. package/scripts/lib/norna-markdown-render-plugin.mjs +37 -0
  24. package/scripts/lib/object.mjs +28 -0
  25. package/scripts/lib/page-markdown.mjs +282 -0
  26. package/scripts/lib/page-model.mjs +26 -7
  27. package/scripts/lib/presentation-contract.mjs +266 -0
  28. package/scripts/lib/presentation-palette-metadata.mjs +51 -0
  29. package/scripts/lib/presentation.mjs +548 -93
  30. package/scripts/lib/project-config.mjs +73 -16
  31. package/scripts/lib/schema-definitions.mjs +89 -21
  32. package/scripts/lib/schema-editor-metadata.mjs +166 -33
  33. package/scripts/lib/schema-value-definitions.mjs +32 -4
  34. package/scripts/lib/site-content.mjs +33 -223
  35. package/scripts/lib/site-conventions.mjs +1 -0
  36. package/scripts/lib/site-paths.mjs +11 -6
  37. package/scripts/lib/site-structure.mjs +189 -0
  38. package/scripts/lib/theme-config.mjs +3 -2
  39. package/scripts/lib/theme-presets.mjs +105 -101
  40. package/scripts/lib/theme-profiles.mjs +221 -0
  41. package/scripts/lib/typography.mjs +220 -35
  42. package/scripts/lib/yaml-config.mjs +46 -6
  43. package/scripts/lib/yaml-indentation.mjs +86 -0
  44. package/scripts/show-typography.mjs +3 -36
  45. package/scripts/sync-content-sections.mjs +88 -79
  46. package/src/components/DisplaySettings.astro +107 -0
  47. package/src/components/ImageCarousel.astro +34 -13
  48. package/src/components/NavigationPageTree.astro +154 -34
  49. package/src/components/PageContentsLinks.astro +40 -0
  50. package/src/components/SectionNavigationScript.astro +68 -7
  51. package/src/components/SiteBreadcrumbs.astro +24 -18
  52. package/src/components/SiteNavigation.astro +130 -46
  53. package/src/components/SitePage.astro +65 -55
  54. package/src/components/SiteSection.astro +37 -198
  55. package/src/components/SiteTreeNavigation.astro +36 -0
  56. package/src/components/TreeNavigationScript.astro +83 -0
  57. package/src/content.config.ts +2 -8
  58. package/src/layouts/BaseLayout.astro +52 -2
  59. package/src/lib/generatedImages.ts +0 -7
  60. package/src/lib/pageThemes.ts +11 -9
  61. package/src/lib/readerPreferencesScript.mjs +141 -0
  62. package/src/lib/sectionContent.ts +120 -206
  63. package/src/lib/siteNavigation.ts +38 -19
  64. package/src/lib/sitePages.ts +115 -33
  65. package/src/pages/[...slug].astro +2 -2
  66. package/src/pages/index.astro +2 -1
  67. package/src/styles/global.css +1110 -279
  68. package/starters/basic/README.md +8 -8
  69. package/starters/basic/site/theme.yaml +1 -105
  70. package/docs/README.md +0 -135
  71. package/docs/assets/norna-dog-shelter.jpg +0 -0
  72. package/docs/commands.md +0 -189
  73. package/docs/configuration.md +0 -119
  74. package/docs/content.md +0 -342
  75. package/docs/design/command-organization.md +0 -414
  76. package/docs/design/navigation-and-theme-plan.md +0 -115
  77. package/docs/design/norna-diagram-design.md +0 -159
  78. package/docs/design/site-examples-structure.md +0 -115
  79. package/docs/engine-development.md +0 -218
  80. package/docs/getting-started.md +0 -206
  81. package/docs/how-to/embedded-site.md +0 -35
  82. package/docs/images-and-metadata.md +0 -186
  83. package/docs/local-development.md +0 -82
  84. package/docs/pages.md +0 -134
  85. package/docs/public-files.md +0 -118
  86. package/docs/publishing.md +0 -117
  87. package/docs/requirements.md +0 -54
  88. package/docs/site-files.md +0 -145
  89. package/docs/sitewide-content.md +0 -73
  90. package/docs/theme.md +0 -271
  91. package/docs/typography.md +0 -180
  92. package/examples/README.md +0 -43
  93. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +0 -35
  94. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +0 -97
  95. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +0 -852
  96. package/examples/complete-sites/dog-shelter-multi-page/.astro/content-assets.mjs +0 -4
  97. package/examples/complete-sites/dog-shelter-multi-page/.astro/content-modules.mjs +0 -1
  98. package/examples/complete-sites/dog-shelter-multi-page/.astro/content.d.ts +0 -183
  99. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +0 -1
  100. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev-local.json +0 -8
  101. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +0 -13
  102. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +0 -36
  103. package/examples/complete-sites/dog-shelter-multi-page/.astro/settings.json +0 -5
  104. package/examples/complete-sites/dog-shelter-multi-page/.astro/types.d.ts +0 -2
  105. package/examples/complete-sites/dog-shelter-multi-page/README.md +0 -24
  106. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +0 -74
  107. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro-B3JHJyiv.js +0 -434
  108. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro-B3JHJyiv.js.map +0 -1
  109. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_aria-query.js +0 -6390
  110. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -1
  111. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_axobject-query.js +0 -2702
  112. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -1
  113. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_html-escaper.js +0 -65
  114. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -1
  115. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -455
  116. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -1
  117. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/audit-C3SVr7dT.js +0 -1534
  118. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/audit-C3SVr7dT.js.map +0 -1
  119. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/highlight-BLtvqtzO.js +0 -48
  120. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +0 -1
  121. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/icons-C92i61-2.js +0 -42
  122. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/icons-C92i61-2.js.map +0 -1
  123. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/package.json +0 -3
  124. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -4
  125. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/settings-v5GgKks_.js +0 -190
  126. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/settings-v5GgKks_.js.map +0 -1
  127. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/toolbar-BFvJVp26.js +0 -458
  128. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +0 -1
  129. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/ui-library-Cd35oAvo.js +0 -1016
  130. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +0 -1
  131. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/window-vo3A6TMn.js +0 -32
  132. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/window-vo3A6TMn.js.map +0 -1
  133. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/xray-DwhzGckb.js +0 -111
  134. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/xray-DwhzGckb.js.map +0 -1
  135. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +0 -98
  136. package/examples/complete-sites/dog-shelter-multi-page/site/config.yaml +0 -1
  137. package/examples/complete-sites/dog-shelter-multi-page/site/content.md +0 -23
  138. package/examples/complete-sites/dog-shelter-multi-page/site/images/shelter-dog.jpg +0 -0
  139. package/examples/complete-sites/dog-shelter-multi-page/site/pages/010-dogs/content.md +0 -21
  140. package/examples/complete-sites/dog-shelter-multi-page/site/pages/010-dogs/images/rex.jpg +0 -0
  141. package/examples/complete-sites/dog-shelter-multi-page/site/pages/010-dogs/images/rover.jpg +0 -0
  142. package/examples/complete-sites/dog-shelter-multi-page/site/pages/020-adopt/content.md +0 -22
  143. package/examples/complete-sites/dog-shelter-multi-page/site/pages/020-adopt/images/adoption-dog.jpg +0 -0
  144. package/examples/complete-sites/dog-shelter-multi-page/site/public/favicon.svg +0 -10
  145. package/examples/complete-sites/dog-shelter-multi-page/site/public/logo.svg +0 -9
  146. package/examples/complete-sites/dog-shelter-multi-page/site/public/robots.txt +0 -2
  147. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +0 -4
  148. package/examples/complete-sites/dog-shelter-multi-page/site/theme.yaml +0 -1
  149. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +0 -35
  150. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +0 -97
  151. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +0 -852
  152. package/examples/complete-sites/dog-shelter-single-page/.astro/content-assets.mjs +0 -4
  153. package/examples/complete-sites/dog-shelter-single-page/.astro/content-modules.mjs +0 -1
  154. package/examples/complete-sites/dog-shelter-single-page/.astro/content.d.ts +0 -183
  155. package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +0 -1
  156. package/examples/complete-sites/dog-shelter-single-page/.astro/dev-local.json +0 -8
  157. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +0 -13
  158. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +0 -18
  159. package/examples/complete-sites/dog-shelter-single-page/.astro/settings.json +0 -5
  160. package/examples/complete-sites/dog-shelter-single-page/.astro/types.d.ts +0 -2
  161. package/examples/complete-sites/dog-shelter-single-page/README.md +0 -24
  162. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +0 -74
  163. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro-B3JHJyiv.js +0 -434
  164. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro-B3JHJyiv.js.map +0 -1
  165. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_aria-query.js +0 -6390
  166. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -1
  167. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_axobject-query.js +0 -2702
  168. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -1
  169. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_html-escaper.js +0 -65
  170. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -1
  171. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -455
  172. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -1
  173. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/audit-C3SVr7dT.js +0 -1534
  174. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/audit-C3SVr7dT.js.map +0 -1
  175. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/highlight-BLtvqtzO.js +0 -48
  176. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +0 -1
  177. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/icons-C92i61-2.js +0 -42
  178. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/icons-C92i61-2.js.map +0 -1
  179. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/package.json +0 -3
  180. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -4
  181. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/settings-v5GgKks_.js +0 -190
  182. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/settings-v5GgKks_.js.map +0 -1
  183. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/toolbar-BFvJVp26.js +0 -458
  184. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +0 -1
  185. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/ui-library-Cd35oAvo.js +0 -1016
  186. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +0 -1
  187. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/window-vo3A6TMn.js +0 -32
  188. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/window-vo3A6TMn.js.map +0 -1
  189. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/xray-DwhzGckb.js +0 -111
  190. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/xray-DwhzGckb.js.map +0 -1
  191. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +0 -50
  192. package/examples/complete-sites/dog-shelter-single-page/site/config.yaml +0 -1
  193. package/examples/complete-sites/dog-shelter-single-page/site/content.md +0 -37
  194. package/examples/complete-sites/dog-shelter-single-page/site/images/foster-dog.jpg +0 -0
  195. package/examples/complete-sites/dog-shelter-single-page/site/images/rescue-dog.jpg +0 -0
  196. package/examples/complete-sites/dog-shelter-single-page/site/public/favicon.svg +0 -10
  197. package/examples/complete-sites/dog-shelter-single-page/site/public/logo.svg +0 -9
  198. package/examples/complete-sites/dog-shelter-single-page/site/public/robots.txt +0 -2
  199. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +0 -4
  200. package/examples/complete-sites/dog-shelter-single-page/site/theme.yaml +0 -1
  201. package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +0 -35
  202. package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +0 -97
  203. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +0 -852
  204. package/examples/feature-demos/media-and-surfaces/.astro/content-assets.mjs +0 -4
  205. package/examples/feature-demos/media-and-surfaces/.astro/content-modules.mjs +0 -1
  206. package/examples/feature-demos/media-and-surfaces/.astro/content.d.ts +0 -183
  207. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +0 -1
  208. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +0 -14
  209. package/examples/feature-demos/media-and-surfaces/.astro/settings.json +0 -5
  210. package/examples/feature-demos/media-and-surfaces/.astro/types.d.ts +0 -2
  211. package/examples/feature-demos/media-and-surfaces/README.md +0 -21
  212. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +0 -80
  213. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro-B3JHJyiv.js +0 -434
  214. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro-B3JHJyiv.js.map +0 -1
  215. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_aria-query.js +0 -6390
  216. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -1
  217. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_axobject-query.js +0 -2702
  218. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -1
  219. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_html-escaper.js +0 -65
  220. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -1
  221. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -455
  222. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -1
  223. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/audit-C3SVr7dT.js +0 -1534
  224. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/audit-C3SVr7dT.js.map +0 -1
  225. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/embla-carousel.js +0 -1389
  226. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/embla-carousel.js.map +0 -1
  227. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/highlight-BLtvqtzO.js +0 -48
  228. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +0 -1
  229. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/icons-C92i61-2.js +0 -42
  230. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/icons-C92i61-2.js.map +0 -1
  231. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/package.json +0 -3
  232. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -4
  233. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/settings-v5GgKks_.js +0 -190
  234. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/settings-v5GgKks_.js.map +0 -1
  235. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/toolbar-BFvJVp26.js +0 -458
  236. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +0 -1
  237. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/ui-library-Cd35oAvo.js +0 -1016
  238. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +0 -1
  239. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/window-vo3A6TMn.js +0 -32
  240. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/window-vo3A6TMn.js.map +0 -1
  241. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/xray-DwhzGckb.js +0 -111
  242. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/xray-DwhzGckb.js.map +0 -1
  243. package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +0 -66
  244. package/examples/feature-demos/media-and-surfaces/site/config.yaml +0 -1
  245. package/examples/feature-demos/media-and-surfaces/site/content.md +0 -15
  246. package/examples/feature-demos/media-and-surfaces/site/pages/010-media/content.md +0 -71
  247. package/examples/feature-demos/media-and-surfaces/site/pages/010-media/images/card-carousel.svg +0 -11
  248. package/examples/feature-demos/media-and-surfaces/site/pages/010-media/images/card-stack.svg +0 -9
  249. package/examples/feature-demos/media-and-surfaces/site/pages/010-media/images/card-surfaces.svg +0 -8
  250. package/examples/feature-demos/media-and-surfaces/site/pages/010-media/images/carousel-one.svg +0 -8
  251. package/examples/feature-demos/media-and-surfaces/site/pages/010-media/images/carousel-three.svg +0 -13
  252. package/examples/feature-demos/media-and-surfaces/site/pages/010-media/images/carousel-two.svg +0 -8
  253. package/examples/feature-demos/media-and-surfaces/site/pages/010-media/images/stack-one.svg +0 -8
  254. package/examples/feature-demos/media-and-surfaces/site/pages/010-media/images/stack-two.svg +0 -9
  255. package/examples/feature-demos/media-and-surfaces/site/pages/020-surfaces/content.md +0 -41
  256. package/examples/feature-demos/media-and-surfaces/site/pages/020-surfaces/theme.yaml +0 -3
  257. package/examples/feature-demos/media-and-surfaces/site/public/robots.txt +0 -2
  258. package/examples/feature-demos/media-and-surfaces/site/sitewide-content.yaml +0 -2
  259. package/examples/feature-demos/media-and-surfaces/site/theme.yaml +0 -1
  260. package/examples/feature-demos/sitewide-content/.astro/collections/site.schema.json +0 -59
  261. package/examples/feature-demos/sitewide-content/.astro/collections/sitewide.schema.json +0 -138
  262. package/examples/feature-demos/sitewide-content/.astro/collections/theme.schema.json +0 -755
  263. package/examples/feature-demos/sitewide-content/.astro/content-assets.mjs +0 -1
  264. package/examples/feature-demos/sitewide-content/.astro/content-modules.mjs +0 -1
  265. package/examples/feature-demos/sitewide-content/.astro/content.d.ts +0 -183
  266. package/examples/feature-demos/sitewide-content/.astro/data-store.json +0 -1
  267. package/examples/feature-demos/sitewide-content/.astro/dev.json +0 -13
  268. package/examples/feature-demos/sitewide-content/.astro/dev.log +0 -18
  269. package/examples/feature-demos/sitewide-content/.astro/settings.json +0 -5
  270. package/examples/feature-demos/sitewide-content/.astro/types.d.ts +0 -2
  271. package/examples/feature-demos/sitewide-content/README.md +0 -26
  272. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/_metadata.json +0 -74
  273. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro-B3JHJyiv.js +0 -434
  274. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro-B3JHJyiv.js.map +0 -1
  275. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_aria-query.js +0 -6390
  276. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -1
  277. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_axobject-query.js +0 -2702
  278. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -1
  279. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_html-escaper.js +0 -65
  280. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -1
  281. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -455
  282. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -1
  283. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/audit-C3SVr7dT.js +0 -1534
  284. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/audit-C3SVr7dT.js.map +0 -1
  285. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/highlight-BLtvqtzO.js +0 -48
  286. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +0 -1
  287. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/icons-C92i61-2.js +0 -42
  288. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/icons-C92i61-2.js.map +0 -1
  289. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/package.json +0 -3
  290. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -4
  291. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/settings-v5GgKks_.js +0 -190
  292. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/settings-v5GgKks_.js.map +0 -1
  293. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/toolbar-BFvJVp26.js +0 -458
  294. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/toolbar-BFvJVp26.js.map +0 -1
  295. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/ui-library-Cd35oAvo.js +0 -1016
  296. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/ui-library-Cd35oAvo.js.map +0 -1
  297. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/window-vo3A6TMn.js +0 -32
  298. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/window-vo3A6TMn.js.map +0 -1
  299. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/xray-DwhzGckb.js +0 -111
  300. package/examples/feature-demos/sitewide-content/node_modules/.vite/deps/xray-DwhzGckb.js.map +0 -1
  301. package/examples/feature-demos/sitewide-content/site/.norna/generated-images.json +0 -1
  302. package/examples/feature-demos/sitewide-content/site/config.yaml +0 -1
  303. package/examples/feature-demos/sitewide-content/site/content.md +0 -17
  304. package/examples/feature-demos/sitewide-content/site/pages/010-logo-and-navigation/content.md +0 -17
  305. package/examples/feature-demos/sitewide-content/site/pages/020-notices/content.md +0 -23
  306. package/examples/feature-demos/sitewide-content/site/pages/030-footer/content.md +0 -18
  307. package/examples/feature-demos/sitewide-content/site/public/logo.svg +0 -6
  308. package/examples/feature-demos/sitewide-content/site/public/robots.txt +0 -2
  309. package/examples/feature-demos/sitewide-content/site/sitewide-content.yaml +0 -14
  310. package/examples/feature-demos/sitewide-content/site/theme.yaml +0 -1
  311. package/examples/feature-demos/theme-presets/.astro/collections/routeTheme.schema.json +0 -746
  312. package/examples/feature-demos/theme-presets/.astro/collections/site.schema.json +0 -59
  313. package/examples/feature-demos/theme-presets/.astro/collections/sitewide.schema.json +0 -138
  314. package/examples/feature-demos/theme-presets/.astro/collections/theme.schema.json +0 -755
  315. package/examples/feature-demos/theme-presets/.astro/content-assets.mjs +0 -4
  316. package/examples/feature-demos/theme-presets/.astro/content-modules.mjs +0 -1
  317. package/examples/feature-demos/theme-presets/.astro/content.d.ts +0 -183
  318. package/examples/feature-demos/theme-presets/.astro/data-store.json +0 -1
  319. package/examples/feature-demos/theme-presets/.astro/dev-local.json +0 -8
  320. package/examples/feature-demos/theme-presets/.astro/dev.json +0 -15
  321. package/examples/feature-demos/theme-presets/.astro/dev.log +0 -164
  322. package/examples/feature-demos/theme-presets/.astro/settings.json +0 -5
  323. package/examples/feature-demos/theme-presets/.astro/types.d.ts +0 -2
  324. package/examples/feature-demos/theme-presets/README.md +0 -18
  325. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/_metadata.json +0 -80
  326. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/astro-D_GIpJIE.js +0 -434
  327. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -1
  328. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/astro_n_aria-query.js +0 -6390
  329. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -1
  330. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/astro_n_axobject-query.js +0 -2702
  331. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -1
  332. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/astro_n_html-escaper.js +0 -65
  333. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -1
  334. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -455
  335. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -1
  336. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js +0 -1534
  337. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -1
  338. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/embla-carousel.js +0 -1389
  339. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/embla-carousel.js.map +0 -1
  340. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js +0 -48
  341. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -1
  342. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/icons-RyebKi45.js +0 -42
  343. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -1
  344. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/package.json +0 -3
  345. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -4
  346. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/settings-DaU-gNnK.js +0 -190
  347. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -1
  348. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/toolbar-63PA619H.js +0 -458
  349. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -1
  350. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js +0 -1016
  351. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -1
  352. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/window-jPkpKVcy.js +0 -32
  353. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -1
  354. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/xray-BXCHpEJP.js +0 -111
  355. package/examples/feature-demos/theme-presets/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -1
  356. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +0 -290
  357. package/examples/feature-demos/theme-presets/site/config.yaml +0 -1
  358. package/examples/feature-demos/theme-presets/site/content.md +0 -14
  359. package/examples/feature-demos/theme-presets/site/pages/010-portfolio/content.md +0 -31
  360. package/examples/feature-demos/theme-presets/site/pages/010-portfolio/images/puppy-portrait.jpg +0 -0
  361. package/examples/feature-demos/theme-presets/site/pages/010-portfolio/images/shepherd-portrait.jpg +0 -0
  362. package/examples/feature-demos/theme-presets/site/pages/010-portfolio/images/white-puppy.jpg +0 -0
  363. package/examples/feature-demos/theme-presets/site/pages/010-portfolio/theme.yaml +0 -1
  364. package/examples/feature-demos/theme-presets/site/pages/020-documentation/content.md +0 -30
  365. package/examples/feature-demos/theme-presets/site/pages/020-documentation/images/black-schnauzer.jpg +0 -0
  366. package/examples/feature-demos/theme-presets/site/pages/020-documentation/images/retriever-portrait.jpg +0 -0
  367. package/examples/feature-demos/theme-presets/site/pages/020-documentation/images/schnauzer-portrait.jpg +0 -0
  368. package/examples/feature-demos/theme-presets/site/pages/020-documentation/theme.yaml +0 -1
  369. package/examples/feature-demos/theme-presets/site/pages/030-project/content.md +0 -31
  370. package/examples/feature-demos/theme-presets/site/pages/030-project/images/boxer-portrait.jpg +0 -0
  371. package/examples/feature-demos/theme-presets/site/pages/030-project/images/grass-puppy.jpg +0 -0
  372. package/examples/feature-demos/theme-presets/site/pages/030-project/images/grey-street-dog.jpg +0 -0
  373. package/examples/feature-demos/theme-presets/site/pages/030-project/theme.yaml +0 -1
  374. package/examples/feature-demos/theme-presets/site/pages/040-statement/content.md +0 -31
  375. package/examples/feature-demos/theme-presets/site/pages/040-statement/images/black-pedigree.jpg +0 -0
  376. package/examples/feature-demos/theme-presets/site/pages/040-statement/images/sheepdog-grass.jpg +0 -0
  377. package/examples/feature-demos/theme-presets/site/pages/040-statement/images/sheepdog-wide.jpg +0 -0
  378. package/examples/feature-demos/theme-presets/site/pages/040-statement/theme.yaml +0 -1
  379. package/examples/feature-demos/theme-presets/site/public/robots.txt +0 -2
  380. package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +0 -3
  381. package/examples/feature-demos/theme-presets/site/theme.yaml +0 -1
  382. package/fixtures/basic/site/.norna/generated-images.json +0 -1
  383. package/fixtures/basic/site/config.yaml +0 -1
  384. package/fixtures/basic/site/content.md +0 -14
  385. package/fixtures/basic/site/public/robots.txt +0 -2
  386. package/fixtures/basic/site/sitewide-content.yaml +0 -3
  387. package/fixtures/basic/site/theme.yaml +0 -8
  388. package/fixtures/content-model-v2/.astro/collections/site.schema.json +0 -776
  389. package/fixtures/content-model-v2/.astro/collections/theme.schema.json +0 -668
  390. package/fixtures/content-model-v2/.astro/content-assets.mjs +0 -4
  391. package/fixtures/content-model-v2/.astro/content-modules.mjs +0 -1
  392. package/fixtures/content-model-v2/.astro/content.d.ts +0 -175
  393. package/fixtures/content-model-v2/.astro/data-store.json +0 -1
  394. package/fixtures/content-model-v2/.astro/dev-local.json +0 -8
  395. package/fixtures/content-model-v2/.astro/dev.json +0 -13
  396. package/fixtures/content-model-v2/.astro/dev.log +0 -231
  397. package/fixtures/content-model-v2/.astro/settings.json +0 -5
  398. package/fixtures/content-model-v2/.astro/types.d.ts +0 -2
  399. package/fixtures/content-model-v2/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +0 -1
  400. package/fixtures/content-model-v2/dist/_astro/SitePage.BbfZub4g.css +0 -1
  401. package/fixtures/content-model-v2/dist/guide/index.html +0 -3
  402. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-480.webp +0 -0
  403. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-768.webp +0 -0
  404. package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-971.webp +0 -0
  405. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-480.webp +0 -0
  406. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-768.webp +0 -0
  407. package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-971.webp +0 -0
  408. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-480.webp +0 -0
  409. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-768.webp +0 -0
  410. package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-971.webp +0 -0
  411. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1080.webp +0 -0
  412. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1440.webp +0 -0
  413. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1600.webp +0 -0
  414. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-480.webp +0 -0
  415. package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-768.webp +0 -0
  416. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-480.webp +0 -0
  417. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-768.webp +0 -0
  418. package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-960.webp +0 -0
  419. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1080.webp +0 -0
  420. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1440.webp +0 -0
  421. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1600.webp +0 -0
  422. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-480.webp +0 -0
  423. package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-768.webp +0 -0
  424. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1080.webp +0 -0
  425. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1440.webp +0 -0
  426. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1600.webp +0 -0
  427. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-480.webp +0 -0
  428. package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-768.webp +0 -0
  429. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1080.webp +0 -0
  430. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1440.webp +0 -0
  431. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1600.webp +0 -0
  432. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-480.webp +0 -0
  433. package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-768.webp +0 -0
  434. package/fixtures/content-model-v2/dist/index.html +0 -8
  435. package/fixtures/content-model-v2/node_modules/.vite/deps/_metadata.json +0 -80
  436. package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js +0 -434
  437. package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js.map +0 -1
  438. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js +0 -6390
  439. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js.map +0 -1
  440. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js +0 -2702
  441. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js.map +0 -1
  442. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js +0 -65
  443. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js.map +0 -1
  444. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -455
  445. package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -1
  446. package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js +0 -1534
  447. package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +0 -1
  448. package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js +0 -1389
  449. package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js.map +0 -1
  450. package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js +0 -48
  451. package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +0 -1
  452. package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js +0 -42
  453. package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js.map +0 -1
  454. package/fixtures/content-model-v2/node_modules/.vite/deps/package.json +0 -3
  455. package/fixtures/content-model-v2/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +0 -4
  456. package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js +0 -190
  457. package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js.map +0 -1
  458. package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js +0 -458
  459. package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js.map +0 -1
  460. package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js +0 -1016
  461. package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +0 -1
  462. package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js +0 -32
  463. package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js.map +0 -1
  464. package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js +0 -111
  465. package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js.map +0 -1
  466. package/fixtures/content-model-v2/site/.norna/generated-images.json +0 -210
  467. package/fixtures/content-model-v2/site/config.yaml +0 -1
  468. package/fixtures/content-model-v2/site/content.md +0 -79
  469. package/fixtures/content-model-v2/site/images/adopt.svg +0 -9
  470. package/fixtures/content-model-v2/site/images/detail.jpg +0 -0
  471. package/fixtures/content-model-v2/site/images/duplicate.jpg +0 -0
  472. package/fixtures/content-model-v2/site/images/foster.svg +0 -12
  473. package/fixtures/content-model-v2/site/images/hero.jpg +0 -0
  474. package/fixtures/content-model-v2/site/images/image.jpg +0 -0
  475. package/fixtures/content-model-v2/site/images/slide-one.jpg +0 -0
  476. package/fixtures/content-model-v2/site/images/slide-three.jpg +0 -0
  477. package/fixtures/content-model-v2/site/images/slide-two.jpg +0 -0
  478. package/fixtures/content-model-v2/site/pages/010-guide/content.md +0 -18
  479. package/fixtures/content-model-v2/site/pages/010-guide/images/duplicate.jpg +0 -0
  480. package/fixtures/content-model-v2/site/theme.yaml +0 -4
  481. package/fixtures/nested-pages/site/.norna/generated-images.json +0 -10
  482. package/fixtures/nested-pages/site/config.yaml +0 -1
  483. package/fixtures/nested-pages/site/content.md +0 -15
  484. package/fixtures/nested-pages/site/pages/010-guides/content.md +0 -15
  485. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/content.md +0 -21
  486. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/images/diagram.svg +0 -21
  487. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +0 -14
  488. package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +0 -11
  489. package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +0 -3
  490. package/fixtures/nested-pages/site/pages/020-reference/content.md +0 -10
  491. package/fixtures/nested-pages/site/pages/020-reference/pages/010-installation/content.md +0 -11
  492. package/fixtures/nested-pages/site/theme.yaml +0 -3
  493. package/scripts/build-pages.mjs +0 -69
  494. package/scripts/lib/example-sites.mjs +0 -37
  495. package/scripts/lib/frontmatter-yaml.mjs +0 -113
  496. package/scripts/lib/git-status.mjs +0 -7
  497. package/scripts/release.mjs +0 -153
  498. package/scripts/test-banner-visibility.mjs +0 -80
  499. package/scripts/test-ci-lockfile.mjs +0 -65
  500. package/scripts/test-cli-discovery.mjs +0 -124
  501. package/scripts/test-client-javascript.mjs +0 -218
  502. package/scripts/test-content-check.mjs +0 -621
  503. package/scripts/test-content-model-v2.mjs +0 -1629
  504. package/scripts/test-documentation.mjs +0 -187
  505. package/scripts/test-editor-language-service.mjs +0 -302
  506. package/scripts/test-engine-commands.mjs +0 -204
  507. package/scripts/test-examples.mjs +0 -24
  508. package/scripts/test-heading-ids.mjs +0 -68
  509. package/scripts/test-navigation-model.mjs +0 -63
  510. package/scripts/test-navigation-preview.mjs +0 -108
  511. package/scripts/test-navigation.mjs +0 -148
  512. package/scripts/test-nested-pages.mjs +0 -110
  513. package/scripts/test-package-check.mjs +0 -531
  514. package/scripts/test-page-model.mjs +0 -80
  515. package/scripts/test-project-config.mjs +0 -144
  516. package/scripts/test-schemas.mjs +0 -154
  517. package/scripts/test-site-public.mjs +0 -85
  518. package/scripts/test-theme-presets.mjs +0 -182
  519. package/src/components/SiteNotesScript.astro +0 -43
  520. package/starters/basic/site/images/.gitkeep +0 -1
  521. package/starters/project/.github/workflows/deploy.yml +0 -64
  522. package/starters/project/README.md +0 -84
  523. package/starters/project/package-lock.json +0 -4296
  524. package/starters/project/package.json +0 -37
  525. package/starters/project/site/config.yaml +0 -1
  526. package/starters/project/site/content.md +0 -119
  527. package/starters/project/site/pages/010-guide/content.md +0 -80
  528. package/starters/project/site/public/robots.txt +0 -2
  529. package/starters/project/site/sitewide-content.yaml +0 -3
  530. package/starters/project/site/theme.yaml +0 -31
  531. package/tsconfig.json +0 -5
  532. /package/starters/basic/site/{content.md → pages/000-home/content.md} +0 -0
  533. /package/{fixtures/basic/site → starters/basic/site/pages/000-home}/images/.gitkeep +0 -0
@@ -1,1629 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { execFile } from 'node:child_process';
3
- import { access, cp, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { promisify } from 'node:util';
7
- import { test } from 'node:test';
8
-
9
- const execFileAsync = promisify(execFile);
10
- const repoRoot = path.resolve(import.meta.dirname, '..');
11
- const nodeBin = process.execPath;
12
- const nornaBin = path.join(repoRoot, 'bin', 'norna.mjs');
13
- const contentScript = path.join(repoRoot, 'scripts', 'sync-content-sections.mjs');
14
- const fixtureSiteDir = path.join(repoRoot, 'fixtures', 'content-model-v2', 'site');
15
- const fixtureRoot = path.dirname(fixtureSiteDir);
16
-
17
- const runNorna = async (args, options = {}) => {
18
- try {
19
- return await execFileAsync(nodeBin, [nornaBin, ...args], {
20
- cwd: repoRoot,
21
- maxBuffer: 1024 * 1024 * 10,
22
- ...options,
23
- });
24
- } catch (error) {
25
- error.output = `${error.stdout ?? ''}${error.stderr ?? ''}`;
26
- throw error;
27
- }
28
- };
29
-
30
- const runContentScript = async (siteDir, args) => {
31
- try {
32
- return await execFileAsync(nodeBin, [contentScript, ...args], {
33
- cwd: repoRoot,
34
- env: {
35
- ...process.env,
36
- NORNA_SITE_DIR: siteDir,
37
- },
38
- maxBuffer: 1024 * 1024 * 10,
39
- });
40
- } catch (error) {
41
- error.output = `${error.stdout ?? ''}${error.stderr ?? ''}`;
42
- throw error;
43
- }
44
- };
45
-
46
- const createTempSite = async ({ underRepoCache = false } = {}) => {
47
- const tempParent = underRepoCache
48
- ? path.join(repoRoot, 'node_modules', '.cache')
49
- : os.tmpdir();
50
- await mkdir(tempParent, { recursive: true });
51
- const root = await mkdtemp(path.join(tempParent, 'norna-content-model-v2-'));
52
- const siteDir = path.join(root, 'site');
53
- await mkdir(siteDir, { recursive: true });
54
- await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
55
- await writeFile(path.join(siteDir, 'theme.yaml'), `typography:
56
- profile: reading
57
- `);
58
- return { root, siteDir };
59
- };
60
-
61
- const fileExists = async (filePath) => access(filePath).then(() => true, () => false);
62
-
63
- const runGit = async (cwd, args) => execFileAsync('git', args, {
64
- cwd,
65
- env: {
66
- ...process.env,
67
- GIT_AUTHOR_NAME: 'Norna Test',
68
- GIT_AUTHOR_EMAIL: 'norna@example.test',
69
- GIT_COMMITTER_NAME: 'Norna Test',
70
- GIT_COMMITTER_EMAIL: 'norna@example.test',
71
- },
72
- maxBuffer: 1024 * 1024,
73
- });
74
-
75
- const initCleanGitWorktree = async (root) => {
76
- await runGit(root, ['init']);
77
- await runGit(root, ['config', 'user.email', 'norna@example.test']);
78
- await runGit(root, ['config', 'user.name', 'Norna Test']);
79
- await runGit(root, ['add', '.']);
80
- await runGit(root, ['commit', '-m', 'initial']);
81
- };
82
-
83
- const createTempFixtureCopy = async () => {
84
- const tempParent = path.join(repoRoot, 'node_modules', '.cache');
85
- await mkdir(tempParent, { recursive: true });
86
- const root = await mkdtemp(path.join(tempParent, 'norna-content-model-v2-fixture-'));
87
- const fixtureCopyRoot = path.join(root, 'content-model-v2');
88
-
89
- await cp(fixtureRoot, fixtureCopyRoot, {
90
- recursive: true,
91
- filter: (source) => ![
92
- path.join(fixtureRoot, '.astro'),
93
- path.join(fixtureRoot, 'dist'),
94
- path.join(fixtureRoot, 'node_modules'),
95
- path.join(fixtureSiteDir, '.norna'),
96
- ].some((ignoredPath) => source === ignoredPath || source.startsWith(`${ignoredPath}${path.sep}`)),
97
- });
98
-
99
- return {
100
- root,
101
- siteDir: path.join(fixtureCopyRoot, 'site'),
102
- };
103
- };
104
-
105
- test('content model v2 fixture checks and builds', async () => {
106
- const { root, siteDir } = await createTempFixtureCopy();
107
- try {
108
- await runNorna(['--site-dir', siteDir, 'content:check']);
109
- await runNorna(['--site-dir', siteDir, 'build']);
110
-
111
- const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
112
- assert.ok(manifest['images/duplicate.jpg']);
113
- assert.ok(manifest['pages/010-guide/images/duplicate.jpg']);
114
-
115
- const homepageHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'index.html'), 'utf8');
116
- assert.match(homepageHtml, /site-section-page-title/);
117
- assert.match(homepageHtml, /site-section-page-title-empty/);
118
- assert.match(homepageHtml, /fetchpriority="high"/);
119
- } finally {
120
- await rm(root, { recursive: true, force: true });
121
- }
122
- });
123
-
124
- test('the removed routes directory is rejected with a page migration hint', async () => {
125
- const { root, siteDir } = await createTempSite();
126
- try {
127
- await writeFile(path.join(siteDir, 'content.md'), `# Home
128
- Home content.
129
- `);
130
- await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
131
- await writeFile(path.join(siteDir, 'routes', '010-guide', 'content.md'), 'Old page content.\n');
132
-
133
- await assert.rejects(
134
- runNorna(['--site-dir', siteDir, 'content:check']),
135
- (error) => {
136
- assert.match(error.output, /site\/routes is no longer supported\. Rename it to site\/pages and use NNN-page-id directory names\./);
137
- return true;
138
- },
139
- );
140
- } finally {
141
- await rm(root, { recursive: true, force: true });
142
- }
143
- });
144
-
145
- test('page theme replaces visual theme without changing site navigation', async () => {
146
- const { root, siteDir } = await createTempSite({ underRepoCache: true });
147
- try {
148
- await writeFile(path.join(siteDir, 'theme.yaml'), `typography:
149
- profile: reading
150
- `);
151
- await mkdir(path.join(siteDir, 'pages', '010-guide'), { recursive: true });
152
- await writeFile(path.join(siteDir, 'content.md'), `---
153
- page:
154
- description: Root page
155
- ---
156
-
157
- # Root
158
-
159
- ## Home {#home}
160
-
161
- Root content.
162
- `);
163
- await writeFile(path.join(siteDir, 'pages', '010-guide', 'content.md'), `---
164
- page:
165
- description: Guide page
166
- ---
167
-
168
- # Guide
169
- Page content.
170
- `);
171
- await writeFile(path.join(siteDir, 'pages', '010-guide', 'theme.yaml'), `typography:
172
- profile: statement
173
- palette: light
174
- `);
175
-
176
- await runNorna(['--site-dir', siteDir, 'build']);
177
- const rootHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'index.html'), 'utf8');
178
- const pageHtml = await readFile(path.join(path.dirname(siteDir), 'dist', 'guide', 'index.html'), 'utf8');
179
- assert.match(rootHtml, /--color-page: #000000/);
180
- assert.match(pageHtml, /--color-page: #ffffff/);
181
- assert.match(pageHtml, /<a href="\/">\s*Root\s*<\/a>/);
182
- } finally {
183
- await rm(root, { recursive: true, force: true });
184
- }
185
- });
186
-
187
- test('page metadata, navigation logo, and page listing have separate roles', async () => {
188
- const { root, siteDir } = await createTempSite({ underRepoCache: true });
189
- try {
190
- await mkdir(path.join(siteDir, 'public'), { recursive: true });
191
- await writeFile(path.join(siteDir, 'public', 'logo.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"/>');
192
- await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `logo:
193
- height: 2rem
194
- `);
195
- await writeFile(path.join(siteDir, 'content.md'), `# Welcome
196
-
197
- ## Intro {#intro}
198
-
199
- Homepage content.
200
- `);
201
- await mkdir(path.join(siteDir, 'pages', '010-guide'), { recursive: true });
202
- await writeFile(path.join(siteDir, 'pages', '010-guide', 'content.md'), `---
203
- page:
204
- description: The visible guide page.
205
- ---
206
-
207
- # Guide
208
- Guide content.
209
- `);
210
- await mkdir(path.join(siteDir, 'pages', '020-private'), { recursive: true });
211
- await writeFile(path.join(siteDir, 'pages', '020-private', 'content.md'), `---
212
- navigation:
213
- listed: false
214
- ---
215
-
216
- # Unlisted
217
- Public but unlisted content.
218
- `);
219
-
220
- await runNorna(['--site-dir', siteDir, 'build']);
221
- const distDir = path.join(path.dirname(siteDir), 'dist');
222
- const homeHtml = await readFile(path.join(distDir, 'index.html'), 'utf8');
223
- const guideHtml = await readFile(path.join(distDir, 'guide', 'index.html'), 'utf8');
224
- const unlistedHtml = await readFile(path.join(distDir, 'private', 'index.html'), 'utf8');
225
-
226
- assert.match(homeHtml, /<title>Welcome<\/title>/);
227
- assert.doesNotMatch(homeHtml, /<meta name="description"/);
228
- assert.match(homeHtml, /<img class="site-brand-logo"[^>]+alt="Welcome"/);
229
- assert.match(homeHtml, /<a href="\/" aria-current="page">\s*Welcome\s*<\/a>/);
230
- assert.match(homeHtml, /<a href="\/guide\/">\s*Guide\s*<\/a>/);
231
- assert.doesNotMatch(homeHtml, />\s*Unlisted\s*<\/a>/);
232
- assert.match(guideHtml, /<meta name="description" content="The visible guide page\."/);
233
- assert.match(unlistedHtml, /Public but unlisted content/);
234
- } finally {
235
- await rm(root, { recursive: true, force: true });
236
- }
237
- });
238
-
239
- test('footer build information uses the site language and fixed formatting', async () => {
240
- const { root, siteDir } = await createTempSite({ underRepoCache: true });
241
- try {
242
- await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\nlanguage: sv-SE\n');
243
- await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `footer:
244
- buildInfo: true
245
- `);
246
- await writeFile(path.join(siteDir, 'content.md'), `---
247
- page:
248
- description: Footer build information.
249
- ---
250
-
251
- # Footer
252
-
253
- ## Home {#home}
254
-
255
- Content.
256
- `);
257
-
258
- await runNorna(['--site-dir', siteDir, 'build']);
259
- const html = await readFile(path.join(path.dirname(siteDir), 'dist', 'index.html'), 'utf8');
260
- assert.match(html, /<span>Byggd [^<]+<\/span>/);
261
- } finally {
262
- await rm(root, { recursive: true, force: true });
263
- }
264
- });
265
-
266
- test('page theme cannot define navigation logo settings', async () => {
267
- const { root, siteDir } = await createTempSite({ underRepoCache: true });
268
- try {
269
- await mkdir(path.join(siteDir, 'pages', '010-guide'), { recursive: true });
270
- await writeFile(path.join(siteDir, 'content.md'), `---
271
- page:
272
- description: Root page
273
- ---
274
-
275
- # Root
276
-
277
- ## Home {#home}
278
-
279
- Root content.
280
- `);
281
- await writeFile(path.join(siteDir, 'pages', '010-guide', 'content.md'), `---
282
- page:
283
- description: Guide page
284
- ---
285
-
286
- # Guide
287
- Page content.
288
- `);
289
- await writeFile(path.join(siteDir, 'pages', '010-guide', 'theme.yaml'), `logo:
290
- height: 3rem
291
- `);
292
-
293
- await assert.rejects(
294
- runNorna(['--site-dir', siteDir, 'config:check']),
295
- (error) => {
296
- assert.match(error.output, /may not define navigation logo settings\. Optional logo display settings belong under "logo:" in site\/sitewide-content\.yaml/);
297
- return true;
298
- },
299
- );
300
- } finally {
301
- await rm(root, { recursive: true, force: true });
302
- }
303
- });
304
-
305
- test('navigation logo settings require one conventional logo file', async () => {
306
- const { root, siteDir } = await createTempSite();
307
- try {
308
- await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `logo:
309
- height: 2rem
310
- `);
311
-
312
- await assert.rejects(
313
- runNorna(['--site-dir', siteDir, 'config:check']),
314
- (error) => {
315
- assert.match(error.output, /Site-wide logo is configured, but no logo file was found/);
316
- assert.match(error.output, /site\/public\/logo\.svg/);
317
- return true;
318
- },
319
- );
320
- } finally {
321
- await rm(root, { recursive: true, force: true });
322
- }
323
- });
324
-
325
- test('multiple conventional navigation logo files stop config validation', async () => {
326
- const { root, siteDir } = await createTempSite();
327
- try {
328
- await mkdir(path.join(siteDir, 'public'), { recursive: true });
329
- await writeFile(path.join(siteDir, 'public', 'logo.svg'), '<svg/>');
330
- await writeFile(path.join(siteDir, 'public', 'logo.png'), 'png');
331
-
332
- await assert.rejects(
333
- runNorna(['--site-dir', siteDir, 'config:check']),
334
- (error) => {
335
- assert.match(error.output, /Found multiple logo files/);
336
- assert.match(error.output, /site\/public\/logo\.svg/);
337
- assert.match(error.output, /site\/public\/logo\.png/);
338
- return true;
339
- },
340
- );
341
- } finally {
342
- await rm(root, { recursive: true, force: true });
343
- }
344
- });
345
-
346
- test('content:check warns for local Markdown images but not external Markdown images', async () => {
347
- const { root, siteDir } = await createTempSite();
348
- try {
349
- await writeFile(path.join(siteDir, 'content.md'), `---
350
- page:
351
- description: Fixture
352
- ---
353
-
354
- # Markdown Image Warning
355
-
356
- ## Intro {#intro}
357
-
358
- ![Local](portrait.jpg)
359
-
360
- ![External](https://example.com/portrait.jpg)
361
-
362
- ![Public](/favicon.svg)
363
- `);
364
-
365
- const { stdout } = await runContentScript(siteDir, ['--check']);
366
- assert.match(stdout, /Markdown image "portrait\.jpg" references a local image that is not managed by Norna\./);
367
- assert.doesNotMatch(stdout, /https:\/\/example\.com\/portrait\.jpg/);
368
- assert.doesNotMatch(stdout, /\/favicon\.svg/);
369
- } finally {
370
- await rm(root, { recursive: true, force: true });
371
- }
372
- });
373
-
374
- test('managed SVG images are copied as static image output', async () => {
375
- const { root, siteDir } = await createTempSite();
376
- try {
377
- await writeFile(path.join(siteDir, 'content.md'), `---
378
- page:
379
- description: Fixture
380
- ---
381
-
382
- # SVG Fixture
383
-
384
- ## Intro {#intro}
385
-
386
- \`\`\`norna-image-stack
387
- - image: diagram.svg
388
- alt: Diagram
389
- \`\`\`
390
- `);
391
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
392
- await writeFile(path.join(siteDir, 'images', 'diagram.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620"/></svg>\n');
393
-
394
- await runContentScript(siteDir, ['--check']);
395
- await runNorna(['--site-dir', siteDir, 'images']);
396
-
397
- const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
398
- const entry = manifest['images/diagram.svg'];
399
- assert.equal(entry.kind, 'static');
400
- assert.equal(entry.width, 900);
401
- assert.equal(entry.height, 620);
402
- assert.match(entry.src, /^\/images\/original\/images\/diagram-[a-f0-9]{8}\.svg$/);
403
- assert.equal(entry.variants, undefined);
404
- assert.equal(await fileExists(path.join(siteDir, '.norna', 'public', entry.src.replace(/^\//, ''))), true);
405
- } finally {
406
- await rm(root, { recursive: true, force: true });
407
- }
408
- });
409
-
410
- test('inline notes render as linked numbered margin notes', async () => {
411
- const { root, siteDir } = await createTempSite({ underRepoCache: true });
412
- try {
413
- await writeFile(path.join(siteDir, 'content.md'), `---
414
- page:
415
- description: Fixture
416
- ---
417
-
418
- # Inline notes
419
-
420
- ## Intro {#intro}
421
-
422
- The first paragraph points to an explanation.{note-ref}
423
-
424
- {note: This explanation names \`sitewide-content.yaml\` and stays beside the
425
- paragraph on wide screens.}
426
-
427
- The second paragraph has its own explanation.{note-ref}
428
-
429
- {note:
430
- This is the second
431
- explanation.
432
- }
433
- `);
434
-
435
- await runNorna(['--site-dir', siteDir, 'build']);
436
- const html = await readFile(path.join(path.dirname(siteDir), 'dist', 'index.html'), 'utf8');
437
- assert.match(html, /<sup class="section-note-ref"><a id="note-ref-home-intro-1" href="#note-home-intro-1" aria-label="Note 1" aria-describedby="note-home-intro-1" data-note-id="note-home-intro-1">1<\/a><\/sup>/);
438
- assert.match(html, /<aside class="section-note section-note-margin" id="note-home-intro-1" aria-label="Note 1" role="note" data-note-id="note-home-intro-1" style="grid-row: 1;">/);
439
- assert.match(html, /<a class="section-note-number" href="#note-ref-home-intro-1" aria-label="Note 1">\s*1\s*<\/a>/);
440
- assert.match(html, /This explanation names <code>sitewide-content\.yaml<\/code> and stays beside the paragraph on wide screens\./);
441
- assert.match(html, /<sup class="section-note-ref"><a id="note-ref-home-intro-2" href="#note-home-intro-2" aria-label="Note 2" aria-describedby="note-home-intro-2" data-note-id="note-home-intro-2">2<\/a><\/sup>/);
442
- assert.match(html, /<aside class="section-note section-note-margin" id="note-home-intro-2" aria-label="Note 2" role="note" data-note-id="note-home-intro-2" style="grid-row: 2;">/);
443
- assert.match(html, /This is the second explanation\./);
444
- assert.doesNotMatch(html, /\{note:/);
445
- } finally {
446
- await rm(root, { recursive: true, force: true });
447
- }
448
- });
449
-
450
- test('updated managed SVG images get updated static output', async () => {
451
- const { root, siteDir } = await createTempSite();
452
- try {
453
- await writeFile(path.join(siteDir, 'content.md'), `---
454
- page:
455
- description: Fixture
456
- ---
457
-
458
- # SVG Update Fixture
459
-
460
- ## Intro {#intro}
461
-
462
- \`\`\`norna-image-stack
463
- - image: diagram.svg
464
- alt: Diagram
465
- \`\`\`
466
- `);
467
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
468
- const sourcePath = path.join(siteDir, 'images', 'diagram.svg');
469
- await writeFile(sourcePath, '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620" fill="red"/></svg>\n');
470
-
471
- await runContentScript(siteDir, ['--check']);
472
- await runNorna(['--site-dir', siteDir, 'images']);
473
-
474
- const firstManifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
475
- const firstEntry = firstManifest['images/diagram.svg'];
476
- const firstOutputPath = path.join(siteDir, '.norna', 'public', firstEntry.src.replace(/^\//, ''));
477
-
478
- await writeFile(sourcePath, '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620" fill="blue"/></svg>\n');
479
- await runNorna(['--site-dir', siteDir, 'images']);
480
-
481
- const secondManifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
482
- const secondEntry = secondManifest['images/diagram.svg'];
483
- const secondOutputPath = path.join(siteDir, '.norna', 'public', secondEntry.src.replace(/^\//, ''));
484
-
485
- assert.notEqual(secondEntry.sourceHash, firstEntry.sourceHash);
486
- assert.notEqual(secondEntry.src, firstEntry.src);
487
- assert.equal(await fileExists(firstOutputPath), false);
488
- assert.match(await readFile(secondOutputPath, 'utf8'), /fill="blue"/);
489
- } finally {
490
- await rm(root, { recursive: true, force: true });
491
- }
492
- });
493
-
494
- test('content:check warns when carousel SVG images have no intrinsic aspect ratio', async () => {
495
- const { root, siteDir } = await createTempSite();
496
- try {
497
- await writeFile(path.join(siteDir, 'content.md'), `---
498
- page:
499
- description: Fixture
500
- ---
501
-
502
- # SVG Carousel Fixture
503
-
504
- ## Intro {#intro}
505
-
506
- \`\`\`norna-image-carousel
507
- - image: first.svg
508
- alt: First
509
- - image: second.svg
510
- alt: Second
511
- \`\`\`
512
- `);
513
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
514
- await writeFile(path.join(siteDir, 'images', 'first.svg'), '<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100"/></svg>\n');
515
- await writeFile(path.join(siteDir, 'images', 'second.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100"/></svg>\n');
516
-
517
- const { stdout } = await runContentScript(siteDir, ['--check']);
518
- assert.match(stdout, /Content check completed with warnings\./);
519
- assert.match(stdout, /Carousel on line \d+ uses images without an intrinsic aspect ratio: first\.svg\./);
520
- } finally {
521
- await rm(root, { recursive: true, force: true });
522
- }
523
- });
524
-
525
- test('norna-card-list images are managed image references', async () => {
526
- const { root, siteDir } = await createTempSite({ underRepoCache: true });
527
- try {
528
- await writeFile(path.join(siteDir, 'content.md'), `---
529
- page:
530
- description: Fixture
531
- ---
532
-
533
- # Card List Fixture
534
-
535
- ## Help {#help}
536
-
537
- \`\`\`norna-card-list
538
- layout: image-left
539
- flow: stack
540
- size: l
541
- width: narrow
542
-
543
- - title: Adopt
544
- text: Give a dog a new home.
545
- image: adopt.svg
546
- link: /adopt/
547
- badge-text: Recommended
548
- - title: Donate
549
- text: Support the shelter.
550
- \`\`\`
551
- `);
552
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
553
- await writeFile(path.join(siteDir, 'images', 'adopt.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 100"><rect width="160" height="100"/></svg>\n');
554
-
555
- const { stdout } = await runContentScript(siteDir, ['--check']);
556
- assert.match(stdout, /Content check passed\./);
557
-
558
- await runNorna(['--site-dir', siteDir, 'images']);
559
- const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
560
- assert.equal(manifest['images/adopt.svg'].kind, 'static');
561
-
562
- await runNorna(['--site-dir', siteDir, 'build']);
563
- const html = await readFile(path.join(root, 'dist', 'index.html'), 'utf8');
564
- assert.match(html, /card-list-layout-image-left/);
565
- assert.match(html, /card-list-flow-stack/);
566
- assert.match(html, /card-list-size-l/);
567
- assert.match(html, /card-list-width-narrow/);
568
- assert.match(html, /card-list-item-has-image/);
569
- assert.match(html, /class="card-list-badge">Recommended<\/p>/);
570
- assert.match(html, /Give a dog a new home\./);
571
- assert.match(html, /href="\/adopt\/"/);
572
- assert.match(html, /src="\/images\/original\/images\/adopt-[a-f0-9]{8}\.svg"/);
573
- } finally {
574
- await rm(root, { recursive: true, force: true });
575
- }
576
- });
577
-
578
- test('content:check fails when a norna-card-list image file is missing', async () => {
579
- const { root, siteDir } = await createTempSite();
580
- try {
581
- await writeFile(path.join(siteDir, 'content.md'), `---
582
- page:
583
- description: Fixture
584
- ---
585
-
586
- # Missing Card Image
587
-
588
- ## Help {#help}
589
-
590
- \`\`\`norna-card-list
591
- - title: Adopt
592
- image: missing.svg
593
- \`\`\`
594
- `);
595
-
596
- await assert.rejects(
597
- () => runContentScript(siteDir, ['--check']),
598
- (error) => {
599
- assert.match(error.output, /Image "missing\.svg" does not exist at .*site\/images\/missing\.svg or anywhere under any page image root\./);
600
- return true;
601
- },
602
- );
603
- } finally {
604
- await rm(root, { recursive: true, force: true });
605
- }
606
- });
607
-
608
- test('content:check fails for malformed norna-card-list blocks', async () => {
609
- const { root, siteDir } = await createTempSite();
610
- try {
611
- await writeFile(path.join(siteDir, 'content.md'), `---
612
- page:
613
- description: Fixture
614
- ---
615
-
616
- # Malformed Card List
617
-
618
- ## Help {#help}
619
-
620
- \`\`\`norna-card-list
621
- - text: Missing title
622
- \`\`\`
623
- `);
624
-
625
- await assert.rejects(
626
- () => runContentScript(siteDir, ['--check']),
627
- (error) => {
628
- assert.match(error.output, /Invalid norna-card-list entry "- text: Missing title"\. Start each card with "- title: Card title"\./);
629
- return true;
630
- },
631
- );
632
- } finally {
633
- await rm(root, { recursive: true, force: true });
634
- }
635
- });
636
-
637
- test('content:check fails for invalid norna-card-list options', async () => {
638
- const { root, siteDir } = await createTempSite();
639
- try {
640
- await writeFile(path.join(siteDir, 'content.md'), `---
641
- page:
642
- description: Fixture
643
- ---
644
-
645
- # Invalid Card List Options
646
-
647
- ## Help {#help}
648
-
649
- \`\`\`norna-card-list
650
- layout: floating
651
- flow: list
652
- size: huge
653
- width: full
654
-
655
- - title: Adopt
656
- text: Give a dog a new home.
657
- \`\`\`
658
- `);
659
-
660
- await assert.rejects(
661
- () => runContentScript(siteDir, ['--check']),
662
- (error) => {
663
- assert.match(error.output, /Invalid norna-card-list layout "floating"\. Use one of: image-top, image-left, image-right\./);
664
- return true;
665
- },
666
- );
667
- } finally {
668
- await rm(root, { recursive: true, force: true });
669
- }
670
- });
671
-
672
- test('content:check fails for invalid norna-card-list width', async () => {
673
- const { root, siteDir } = await createTempSite();
674
- try {
675
- await writeFile(path.join(siteDir, 'content.md'), `---
676
- page:
677
- description: Fixture
678
- ---
679
-
680
- # Invalid Card List Width
681
-
682
- ## Help {#help}
683
-
684
- \`\`\`norna-card-list
685
- width: full
686
-
687
- - title: Adopt
688
- text: Give a dog a new home.
689
- \`\`\`
690
- `);
691
-
692
- await assert.rejects(
693
- () => runContentScript(siteDir, ['--check']),
694
- (error) => {
695
- assert.match(error.output, /Invalid norna-card-list width "full"\. Use one of: text, narrow, normal, wide\./);
696
- return true;
697
- },
698
- );
699
- } finally {
700
- await rm(root, { recursive: true, force: true });
701
- }
702
- });
703
-
704
- test('content:check fails for invalid norna-card-list size', async () => {
705
- const { root, siteDir } = await createTempSite();
706
- try {
707
- await writeFile(path.join(siteDir, 'content.md'), `---
708
- page:
709
- description: Fixture
710
- ---
711
-
712
- # Invalid Card List Size
713
-
714
- ## Help {#help}
715
-
716
- \`\`\`norna-card-list
717
- size: huge
718
-
719
- - title: Adopt
720
- text: Give a dog a new home.
721
- \`\`\`
722
- `);
723
-
724
- await assert.rejects(
725
- () => runContentScript(siteDir, ['--check']),
726
- (error) => {
727
- assert.match(error.output, /Invalid norna-card-list size "huge"\. Use one of: s, m, l, xl\./);
728
- return true;
729
- },
730
- );
731
- } finally {
732
- await rm(root, { recursive: true, force: true });
733
- }
734
- });
735
-
736
- test('content:sync moves Norna-managed images inside the same page image root', async () => {
737
- const { root, siteDir } = await createTempSite();
738
- try {
739
- await writeFile(path.join(siteDir, 'content.md'), `---
740
- page:
741
- description: Fixture
742
- ---
743
-
744
- # Sync Fixture
745
-
746
- ## Work {#work}
747
-
748
- \`\`\`norna-image-stack
749
- - image: moved.jpg
750
- alt: Moved image
751
- \`\`\`
752
- `);
753
- await mkdir(path.join(siteDir, 'images', 'old'), { recursive: true });
754
- await writeFile(path.join(siteDir, 'images', 'old', 'moved.jpg'), 'fixture image');
755
-
756
- await runContentScript(siteDir, ['--write', '--yes']);
757
-
758
- const moved = await readFile(path.join(siteDir, 'images', 'moved.jpg'), 'utf8');
759
- assert.equal(moved, 'fixture image');
760
- } finally {
761
- await rm(root, { recursive: true, force: true });
762
- }
763
- });
764
-
765
- test('content:sync moves managed SVG images inside the same page image root', async () => {
766
- const { root, siteDir } = await createTempSite();
767
- try {
768
- await writeFile(path.join(siteDir, 'content.md'), `---
769
- page:
770
- description: Fixture
771
- ---
772
-
773
- # SVG Sync Fixture
774
-
775
- ## Work {#work}
776
-
777
- \`\`\`norna-image-stack
778
- - image: moved.svg
779
- alt: Moved diagram
780
- \`\`\`
781
- `);
782
- await mkdir(path.join(siteDir, 'images', 'old'), { recursive: true });
783
- await writeFile(path.join(siteDir, 'images', 'old', 'moved.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"></svg>\n');
784
-
785
- await runContentScript(siteDir, ['--write', '--yes']);
786
-
787
- const moved = await readFile(path.join(siteDir, 'images', 'moved.svg'), 'utf8');
788
- assert.match(moved, /viewBox="0 0 10 10"/);
789
- } finally {
790
- await rm(root, { recursive: true, force: true });
791
- }
792
- });
793
-
794
- test('content:sync moves Norna-managed images across page image roots when git is clean', async () => {
795
- const { root, siteDir } = await createTempSite();
796
- try {
797
- const sourcePageDir = path.join(siteDir, 'pages', '010-source');
798
- const targetPageDir = path.join(siteDir, 'pages', '020-target');
799
- await mkdir(path.join(sourcePageDir, 'images'), { recursive: true });
800
- await mkdir(targetPageDir, { recursive: true });
801
- await writeFile(path.join(siteDir, 'content.md'), `---
802
- page:
803
- description: Fixture
804
- ---
805
-
806
- # Cross Page Sync
807
-
808
- ## Home {#home}
809
-
810
- Text.
811
- `);
812
- await writeFile(path.join(sourcePageDir, 'content.md'), `---
813
- page:
814
- description: Fixture
815
- ---
816
-
817
- # Source
818
-
819
- ## Old {#old}
820
-
821
- Text.
822
- `);
823
- await writeFile(path.join(targetPageDir, 'content.md'), `---
824
- page:
825
- description: Fixture
826
- ---
827
-
828
- # Target
829
-
830
- ## Work {#work}
831
-
832
- \`\`\`norna-image-stack
833
- - image: moved.jpg
834
- \`\`\`
835
- `);
836
- await writeFile(path.join(sourcePageDir, 'images', 'moved.jpg'), 'page image');
837
- await initCleanGitWorktree(root);
838
-
839
- await runContentScript(siteDir, ['--write', '--yes']);
840
-
841
- assert.equal(await fileExists(path.join(targetPageDir, 'images', 'moved.jpg')), true);
842
- assert.equal(await fileExists(path.join(sourcePageDir, 'images', 'moved.jpg')), false);
843
- } finally {
844
- await rm(root, { recursive: true, force: true });
845
- }
846
- });
847
-
848
- test('content:sync refuses cross-page image moves when git status is dirty', async () => {
849
- const { root, siteDir } = await createTempSite();
850
- try {
851
- const sourcePageDir = path.join(siteDir, 'pages', '010-source');
852
- const targetPageDir = path.join(siteDir, 'pages', '020-target');
853
- await mkdir(path.join(sourcePageDir, 'images'), { recursive: true });
854
- await mkdir(targetPageDir, { recursive: true });
855
- await writeFile(path.join(siteDir, 'content.md'), `---
856
- page:
857
- description: Fixture
858
- ---
859
-
860
- # Dirty Cross Page Sync
861
-
862
- ## Home {#home}
863
-
864
- Text.
865
- `);
866
- await writeFile(path.join(sourcePageDir, 'content.md'), `---
867
- page:
868
- description: Fixture
869
- ---
870
-
871
- # Source
872
-
873
- ## Old {#old}
874
-
875
- Text.
876
- `);
877
- await writeFile(path.join(targetPageDir, 'content.md'), `---
878
- page:
879
- description: Fixture
880
- ---
881
-
882
- # Target
883
-
884
- ## Work {#work}
885
-
886
- \`\`\`norna-image-stack
887
- - image: moved.jpg
888
- \`\`\`
889
- `);
890
- await writeFile(path.join(sourcePageDir, 'images', 'moved.jpg'), 'page image');
891
- await initCleanGitWorktree(root);
892
- await writeFile(path.join(root, 'dirty.txt'), 'dirty');
893
-
894
- await assert.rejects(
895
- () => runContentScript(siteDir, ['--write', '--yes']),
896
- (error) => {
897
- assert.match(error.output, /Cross-page content sync requires a clean git working tree before moving files between page image roots\./);
898
- assert.match(error.output, /dirty\.txt/);
899
- return true;
900
- },
901
- );
902
-
903
- assert.equal(await fileExists(path.join(targetPageDir, 'images', 'moved.jpg')), false);
904
- assert.equal(await fileExists(path.join(sourcePageDir, 'images', 'moved.jpg')), true);
905
- } finally {
906
- await rm(root, { recursive: true, force: true });
907
- }
908
- });
909
-
910
- test('content:check reports cross-page image moves without requiring clean git status', async () => {
911
- const { root, siteDir } = await createTempSite();
912
- try {
913
- const sourcePageDir = path.join(siteDir, 'pages', '010-source');
914
- const targetPageDir = path.join(siteDir, 'pages', '020-target');
915
- await mkdir(path.join(sourcePageDir, 'images'), { recursive: true });
916
- await mkdir(targetPageDir, { recursive: true });
917
- await writeFile(path.join(siteDir, 'content.md'), `---
918
- page:
919
- description: Fixture
920
- ---
921
-
922
- # Cross Page Check
923
-
924
- ## Home {#home}
925
-
926
- Text.
927
- `);
928
- await writeFile(path.join(sourcePageDir, 'content.md'), `---
929
- page:
930
- description: Fixture
931
- ---
932
-
933
- # Source
934
-
935
- ## Old {#old}
936
-
937
- Text.
938
- `);
939
- await writeFile(path.join(targetPageDir, 'content.md'), `---
940
- page:
941
- description: Fixture
942
- ---
943
-
944
- # Target
945
-
946
- ## Work {#work}
947
-
948
- \`\`\`norna-image-stack
949
- - image: moved.jpg
950
- \`\`\`
951
- `);
952
- await writeFile(path.join(sourcePageDir, 'images', 'moved.jpg'), 'page image');
953
- await writeFile(path.join(root, 'dirty.txt'), 'dirty');
954
-
955
- await assert.rejects(
956
- () => runContentScript(siteDir, ['--check']),
957
- (error) => {
958
- assert.match(error.output, /Image "moved\.jpg" is used here but is located in .*site\/pages\/010-source\/images\/moved\.jpg\./);
959
- assert.match(error.output, /Run norna content:sync to move it from .*site\/pages\/010-source to .*site\/pages\/020-target\./);
960
- assert.doesNotMatch(error.output, /requires a clean git working tree before moving files/);
961
- return true;
962
- },
963
- );
964
- } finally {
965
- await rm(root, { recursive: true, force: true });
966
- }
967
- });
968
-
969
- test('content:sync refuses to move a cross-page image still referenced by its current section', async () => {
970
- const { root, siteDir } = await createTempSite();
971
- try {
972
- const sourcePageDir = path.join(siteDir, 'pages', '010-source');
973
- const targetPageDir = path.join(siteDir, 'pages', '020-target');
974
- await mkdir(path.join(sourcePageDir, 'images'), { recursive: true });
975
- await mkdir(targetPageDir, { recursive: true });
976
- await writeFile(path.join(siteDir, 'content.md'), `---
977
- page:
978
- description: Fixture
979
- ---
980
-
981
- # Cross Page Copy
982
-
983
- ## Home {#home}
984
-
985
- Text.
986
- `);
987
- await writeFile(path.join(sourcePageDir, 'content.md'), `---
988
- page:
989
- description: Fixture
990
- ---
991
-
992
- # Source
993
-
994
- ## Old {#old}
995
-
996
- \`\`\`norna-image-stack
997
- - image: shared.jpg
998
- \`\`\`
999
- `);
1000
- await writeFile(path.join(targetPageDir, 'content.md'), `---
1001
- page:
1002
- description: Fixture
1003
- ---
1004
-
1005
- # Target
1006
-
1007
- ## Work {#work}
1008
-
1009
- \`\`\`norna-image-stack
1010
- - image: shared.jpg
1011
- \`\`\`
1012
- `);
1013
- await writeFile(path.join(sourcePageDir, 'images', 'shared.jpg'), 'page image');
1014
- await initCleanGitWorktree(root);
1015
-
1016
- await assert.rejects(
1017
- () => runContentScript(siteDir, ['--write', '--yes']),
1018
- (error) => {
1019
- assert.match(error.output, /Cannot relocate "shared\.jpg" from .*site\/pages\/010-source\/images\/shared\.jpg because it is still referenced from .*site\/pages\/010-source\/content\.md \[old\]\./);
1020
- return true;
1021
- },
1022
- );
1023
-
1024
- assert.equal(await fileExists(path.join(targetPageDir, 'images', 'shared.jpg')), false);
1025
- assert.equal(await fileExists(path.join(sourcePageDir, 'images', 'shared.jpg')), true);
1026
- } finally {
1027
- await rm(root, { recursive: true, force: true });
1028
- }
1029
- });
1030
-
1031
- test('CLI content:sync refreshes generated images after moving a carousel image', async () => {
1032
- const { root, siteDir } = await createTempSite();
1033
- try {
1034
- await writeFile(path.join(siteDir, 'content.md'), `---
1035
- page:
1036
- description: Fixture
1037
- ---
1038
-
1039
- # Sync Fixture
1040
-
1041
- ## Work {#work}
1042
-
1043
- \`\`\`norna-image-carousel
1044
- - image: slide-one.jpg
1045
- alt: Slide one
1046
- - image: slide-two.jpg
1047
- alt: Slide two
1048
- \`\`\`
1049
- `);
1050
- await mkdir(path.join(siteDir, 'images', 'old'), { recursive: true });
1051
- await cp(path.join(fixtureSiteDir, 'images', 'hero.jpg'), path.join(siteDir, 'images', 'old', 'slide-one.jpg'));
1052
- await cp(path.join(fixtureSiteDir, 'images', 'detail.jpg'), path.join(siteDir, 'images', 'slide-two.jpg'), { force: true });
1053
-
1054
- await runNorna(['--site-dir', siteDir, 'content:sync', '--yes']);
1055
-
1056
- const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
1057
- assert.equal(await fileExists(path.join(siteDir, 'images', 'slide-one.jpg')), true);
1058
- assert.ok(manifest['images/slide-one.jpg']);
1059
- assert.ok(manifest['images/slide-two.jpg']);
1060
- } finally {
1061
- await rm(root, { recursive: true, force: true });
1062
- }
1063
- });
1064
-
1065
- test('automatic H2 and H3 ids render while explicit ids remain stable', async () => {
1066
- const { root, siteDir } = await createTempSite({ underRepoCache: true });
1067
- try {
1068
- await writeFile(path.join(siteDir, 'content.md'), `---
1069
- page:
1070
- description: Fixture
1071
- ---
1072
-
1073
- # Heading IDs
1074
-
1075
- ## Café and **code**
1076
-
1077
- ### More details
1078
-
1079
- Automatic identifiers.
1080
-
1081
- ## Renamed heading {#stable-id}
1082
-
1083
- Explicit identifier.
1084
- `);
1085
-
1086
- await runContentScript(siteDir, ['--check']);
1087
- await runNorna(['--site-dir', siteDir, 'build']);
1088
-
1089
- const html = await readFile(path.join(root, 'dist', 'index.html'), 'utf8');
1090
- assert.match(html, /<h2 id="cafe-and-code">Café and <strong>code<\/strong><\/h2>/);
1091
- assert.match(html, /<h3 id="more-details">More details<\/h3>/);
1092
- assert.match(html, /<h2 id="stable-id">Renamed heading<\/h2>/);
1093
- } finally {
1094
- await rm(root, { recursive: true, force: true });
1095
- }
1096
- });
1097
-
1098
- test('content:check fails for malformed Norna image blocks', async () => {
1099
- const { root, siteDir } = await createTempSite();
1100
- try {
1101
- await writeFile(path.join(siteDir, 'content.md'), `---
1102
- page:
1103
- description: Fixture
1104
- ---
1105
-
1106
- # Malformed Block
1107
-
1108
- ## Intro {#intro}
1109
-
1110
- \`\`\`norna-image-stack
1111
- image hero.jpg
1112
- alt: Hero
1113
- \`\`\`
1114
- `);
1115
-
1116
- await assert.rejects(
1117
- () => runContentScript(siteDir, ['--check']),
1118
- (error) => {
1119
- assert.match(error.output, /Invalid norna-image-stack entry "image hero\.jpg"\. Start each image with "- image: filename\.jpg"\./);
1120
- return true;
1121
- },
1122
- );
1123
- } finally {
1124
- await rm(root, { recursive: true, force: true });
1125
- }
1126
- });
1127
-
1128
- test('content:check fails for unknown Norna block names', async () => {
1129
- const { root, siteDir } = await createTempSite();
1130
- try {
1131
- await mkdir(path.join(siteDir, 'images', 'plain'), { recursive: true });
1132
- await writeFile(path.join(siteDir, 'images', 'plain', 'image.jpg'), 'fixture image');
1133
- await writeFile(path.join(siteDir, 'content.md'), `---
1134
- page:
1135
- description: Fixture
1136
- ---
1137
-
1138
- # Unknown Block
1139
-
1140
- ## Plain {#plain}
1141
-
1142
- \`\`\`norna-gallery-stack
1143
- - image: image.jpg
1144
- \`\`\`
1145
- `);
1146
-
1147
- await assert.rejects(
1148
- () => runContentScript(siteDir, ['--check']),
1149
- (error) => {
1150
- assert.match(error.output, /Unknown Norna block "norna-gallery-stack"\. Use one of: norna-image-stack, norna-image-carousel, norna-card-list\./);
1151
- assert.match(error.output, /Use norna-image-stack for one or more stacked images\./);
1152
- assert.match(error.output, /Example: ```norna-image-stack\n- image: filename\.jpg\n```/);
1153
- return true;
1154
- },
1155
- );
1156
- } finally {
1157
- await rm(root, { recursive: true, force: true });
1158
- }
1159
- });
1160
-
1161
- test('content:check rejects the removed norna-note block', async () => {
1162
- const { root, siteDir } = await createTempSite();
1163
- try {
1164
- await writeFile(path.join(siteDir, 'content.md'), `---
1165
- page:
1166
- description: Fixture
1167
- ---
1168
-
1169
- # Removed note block
1170
-
1171
- ## Intro {#intro}
1172
-
1173
- \`\`\`norna-note
1174
- This block is no longer supported.
1175
- \`\`\`
1176
- `);
1177
-
1178
- await assert.rejects(
1179
- () => runContentScript(siteDir, ['--check']),
1180
- (error) => {
1181
- assert.match(error.output, /Unknown Norna block "norna-note"\. Use one of: norna-image-stack, norna-image-carousel, norna-card-list\./);
1182
- return true;
1183
- },
1184
- );
1185
- } finally {
1186
- await rm(root, { recursive: true, force: true });
1187
- }
1188
- });
1189
-
1190
- test('content:check supports tilde fenced Norna blocks', async () => {
1191
- const { root, siteDir } = await createTempSite();
1192
- try {
1193
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
1194
- await writeFile(path.join(siteDir, 'images', 'hero.jpg'), 'fixture image');
1195
- await writeFile(path.join(siteDir, 'content.md'), `---
1196
- page:
1197
- description: Fixture
1198
- ---
1199
-
1200
- # Tilde Fence
1201
-
1202
- ## Intro {#intro}
1203
-
1204
- ~~~norna-image-stack
1205
- - image: hero.jpg
1206
- ~~~
1207
- `);
1208
-
1209
- const result = await runContentScript(siteDir, ['--check']);
1210
- assert.match(result.stdout, /Content check passed\./);
1211
- } finally {
1212
- await rm(root, { recursive: true, force: true });
1213
- }
1214
- });
1215
-
1216
- test('content:check ignores Norna blocks shown inside longer code fences', async () => {
1217
- const { root, siteDir } = await createTempSite();
1218
- try {
1219
- await writeFile(path.join(siteDir, 'content.md'), `---
1220
- page:
1221
- description: Fixture
1222
- ---
1223
-
1224
- # Code Example
1225
-
1226
- ## Intro {#intro}
1227
-
1228
- \`\`\`\`md
1229
- \`\`\`norna-image-stack
1230
- - image: missing-example.jpg
1231
- \`\`\`
1232
- \`\`\`\`
1233
- `);
1234
-
1235
- const result = await runContentScript(siteDir, ['--check']);
1236
- assert.match(result.stdout, /Content check passed\./);
1237
- } finally {
1238
- await rm(root, { recursive: true, force: true });
1239
- }
1240
- });
1241
-
1242
- test('content:check fails for likely Norna block names outside fences', async () => {
1243
- const { root, siteDir } = await createTempSite();
1244
- try {
1245
- await writeFile(path.join(siteDir, 'content.md'), `---
1246
- page:
1247
- description: Fixture
1248
- ---
1249
-
1250
- # Missing Fence
1251
-
1252
- ## Intro {#intro}
1253
-
1254
- norna-image-stack
1255
- - image: hero.jpg
1256
- `);
1257
-
1258
- await assert.rejects(
1259
- () => runContentScript(siteDir, ['--check']),
1260
- (error) => {
1261
- assert.match(error.output, /Found "norna-image-stack" outside a code block\./);
1262
- assert.match(error.output, /Start the block like this:\n```norna-image-stack\n- image: filename\.jpg\n```/);
1263
- return true;
1264
- },
1265
- );
1266
- } finally {
1267
- await rm(root, { recursive: true, force: true });
1268
- }
1269
- });
1270
-
1271
- test('content:check fails for short Norna block fences', async () => {
1272
- const { root, siteDir } = await createTempSite();
1273
- try {
1274
- await writeFile(path.join(siteDir, 'content.md'), `---
1275
- page:
1276
- description: Fixture
1277
- ---
1278
-
1279
- # Short Fence
1280
-
1281
- ## Intro {#intro}
1282
-
1283
- \`\`norna-image-stack
1284
- - image: hero.jpg
1285
- \`\`
1286
- `);
1287
-
1288
- await assert.rejects(
1289
- () => runContentScript(siteDir, ['--check']),
1290
- (error) => {
1291
- assert.match(error.output, /Invalid Norna block start for "norna-image-stack"\./);
1292
- assert.match(error.output, /Use three backticks or three tildes/);
1293
- return true;
1294
- },
1295
- );
1296
- } finally {
1297
- await rm(root, { recursive: true, force: true });
1298
- }
1299
- });
1300
-
1301
- test('content:check fails for unclosed Norna blocks', async () => {
1302
- const { root, siteDir } = await createTempSite();
1303
- try {
1304
- await writeFile(path.join(siteDir, 'content.md'), `---
1305
- page:
1306
- description: Fixture
1307
- ---
1308
-
1309
- # Unclosed Fence
1310
-
1311
- ## Intro {#intro}
1312
-
1313
- \`\`\`norna-image-stack
1314
- - image: hero.jpg
1315
- ~~~
1316
- `);
1317
-
1318
- await assert.rejects(
1319
- () => runContentScript(siteDir, ['--check']),
1320
- (error) => {
1321
- assert.match(error.output, /This Norna block was started on line \d+ but not closed\./);
1322
- assert.match(error.output, /Add a closing ``` line after the last entry\./);
1323
- return true;
1324
- },
1325
- );
1326
- } finally {
1327
- await rm(root, { recursive: true, force: true });
1328
- }
1329
- });
1330
-
1331
- test('content:check continues after malformed Norna blocks', async () => {
1332
- const { root, siteDir } = await createTempSite();
1333
- try {
1334
- await writeFile(path.join(siteDir, 'content.md'), `---
1335
- page:
1336
- description: Fixture
1337
- ---
1338
-
1339
- # Multiple Issues
1340
-
1341
- ## Intro {#intro}
1342
-
1343
- \`\`\`norna-image-stack
1344
- image broken.jpg
1345
- alt: Broken
1346
- \`\`\`
1347
-
1348
- \`\`\`norna-image-stack
1349
- - image: missing-intro.jpg
1350
- alt: Missing intro
1351
- \`\`\`
1352
-
1353
- ## More {#more}
1354
-
1355
- \`\`\`norna-image-stack
1356
- - image: missing-more.jpg
1357
- alt: Missing more
1358
- \`\`\`
1359
- `);
1360
-
1361
- await assert.rejects(
1362
- () => runContentScript(siteDir, ['--check']),
1363
- (error) => {
1364
- assert.match(error.output, /Invalid norna-image-stack entry "image broken\.jpg"\. Start each image with "- image: filename\.jpg"\./);
1365
- assert.match(error.output, /Image "missing-intro\.jpg" does not exist at .*site\/images\/missing-intro\.jpg or anywhere under any page image root\./);
1366
- assert.match(error.output, /Image "missing-more\.jpg" does not exist at .*site\/images\/missing-more\.jpg or anywhere under any page image root\./);
1367
- return true;
1368
- },
1369
- );
1370
- } finally {
1371
- await rm(root, { recursive: true, force: true });
1372
- }
1373
- });
1374
-
1375
- test('content:check allows Norna images without alt text', async () => {
1376
- const { root, siteDir } = await createTempSite();
1377
- try {
1378
- await mkdir(path.join(siteDir, 'images'), { recursive: true });
1379
- await writeFile(path.join(siteDir, 'images', 'hero.jpg'), 'fixture image');
1380
- await writeFile(path.join(siteDir, 'content.md'), `---
1381
- page:
1382
- description: Fixture
1383
- ---
1384
-
1385
- # Missing Alt
1386
-
1387
- ## Intro {#intro}
1388
-
1389
- \`\`\`norna-image-stack
1390
- - image: hero.jpg
1391
- \`\`\`
1392
- `);
1393
-
1394
- const result = await runContentScript(siteDir, ['--check']);
1395
- assert.match(result.stdout, /Content check passed\./);
1396
- } finally {
1397
- await rm(root, { recursive: true, force: true });
1398
- }
1399
- });
1400
-
1401
- test('content:check reports single-image carousel and missing file together', async () => {
1402
- const { root, siteDir } = await createTempSite();
1403
- try {
1404
- await writeFile(path.join(siteDir, 'content.md'), `---
1405
- page:
1406
- description: Fixture
1407
- ---
1408
-
1409
- # Beginner Errors
1410
-
1411
- ## Plain {#plain}
1412
-
1413
- \`\`\`norna-image-carousel
1414
- - image: foo.jpg
1415
- \`\`\`
1416
- `);
1417
-
1418
- await assert.rejects(
1419
- () => runContentScript(siteDir, ['--check']),
1420
- (error) => {
1421
- assert.match(error.output, /norna-image-carousel on line \d+ contains 1 image\. A carousel needs at least two images\./);
1422
- assert.match(error.output, /Image "foo\.jpg" does not exist at .*site\/images\/foo\.jpg or anywhere under any page image root\./);
1423
- return true;
1424
- },
1425
- );
1426
- } finally {
1427
- await rm(root, { recursive: true, force: true });
1428
- }
1429
- });
1430
-
1431
- test('content:check fails when a Norna-managed image file is missing', async () => {
1432
- const { root, siteDir } = await createTempSite();
1433
- try {
1434
- await writeFile(path.join(siteDir, 'content.md'), `---
1435
- page:
1436
- description: Fixture
1437
- ---
1438
-
1439
- # Missing Image
1440
-
1441
- ## Intro {#intro}
1442
-
1443
- \`\`\`norna-image-stack
1444
- - image: missing.jpg
1445
- alt: Missing
1446
- \`\`\`
1447
- `);
1448
-
1449
- await assert.rejects(
1450
- () => runContentScript(siteDir, ['--check']),
1451
- (error) => {
1452
- assert.match(error.output, /Image "missing\.jpg" does not exist at .*site\/images\/missing\.jpg or anywhere under any page image root\./);
1453
- return true;
1454
- },
1455
- );
1456
- } finally {
1457
- await rm(root, { recursive: true, force: true });
1458
- }
1459
- });
1460
-
1461
- test('content:check fails when a page repeats a section id', async () => {
1462
- const { root, siteDir } = await createTempSite();
1463
- try {
1464
- await writeFile(path.join(siteDir, 'content.md'), `---
1465
- page:
1466
- description: Fixture
1467
- ---
1468
-
1469
- # Duplicate Section
1470
-
1471
- ## Intro {#intro}
1472
-
1473
- First.
1474
-
1475
- ## Again {#intro}
1476
-
1477
- Second.
1478
- `);
1479
-
1480
- await assert.rejects(
1481
- () => runContentScript(siteDir, ['--check']),
1482
- (error) => {
1483
- assert.match(error.output, /Two headings resolve to id "intro"\./);
1484
- return true;
1485
- },
1486
- );
1487
- } finally {
1488
- await rm(root, { recursive: true, force: true });
1489
- }
1490
- });
1491
-
1492
- test('content:check reports collisions between automatic H2 and H3 ids', async () => {
1493
- const { root, siteDir } = await createTempSite();
1494
- try {
1495
- await writeFile(path.join(siteDir, 'content.md'), `---
1496
- page:
1497
- description: Fixture
1498
- ---
1499
-
1500
- # Automatic collision
1501
-
1502
- ## Förstå
1503
-
1504
- Section text.
1505
-
1506
- ### Forsta
1507
-
1508
- Topic text.
1509
- `);
1510
-
1511
- await assert.rejects(
1512
- () => runContentScript(siteDir, ['--check']),
1513
- (error) => {
1514
- assert.match(error.output, /Two headings resolve to id "forsta"\./);
1515
- assert.match(error.output, /The other heading is on line \d+\./);
1516
- assert.match(error.output, /Add a unique explicit id to at least one heading/);
1517
- return true;
1518
- },
1519
- );
1520
- } finally {
1521
- await rm(root, { recursive: true, force: true });
1522
- }
1523
- });
1524
-
1525
- test('content:check rejects removed sections frontmatter', async () => {
1526
- const { root, siteDir } = await createTempSite();
1527
- try {
1528
- await writeFile(path.join(siteDir, 'content.md'), `---
1529
- page:
1530
- description: Fixture
1531
- sections:
1532
- intro: {}
1533
- ---
1534
-
1535
- # Removed Section Metadata
1536
-
1537
- ## Intro {#intro}
1538
-
1539
- Text.
1540
- `);
1541
-
1542
- await assert.rejects(
1543
- () => runContentScript(siteDir, ['--check']),
1544
- (error) => {
1545
- assert.match(error.output, /defines "sections" at the top level, but it is not a valid top-level content field\./);
1546
- return true;
1547
- },
1548
- );
1549
- } finally {
1550
- await rm(root, { recursive: true, force: true });
1551
- }
1552
- });
1553
-
1554
- test('content:sync refuses ambiguous image relocation', async () => {
1555
- const { root, siteDir } = await createTempSite();
1556
- try {
1557
- await writeFile(path.join(siteDir, 'content.md'), `---
1558
- page:
1559
- description: Fixture
1560
- ---
1561
-
1562
- # Ambiguous Move
1563
-
1564
- ## Work {#work}
1565
-
1566
- \`\`\`norna-image-stack
1567
- - image: shared.jpg
1568
- alt: Shared
1569
- \`\`\`
1570
- `);
1571
- await mkdir(path.join(siteDir, 'images', 'old-a'), { recursive: true });
1572
- await mkdir(path.join(siteDir, 'images', 'old-b'), { recursive: true });
1573
- await writeFile(path.join(siteDir, 'images', 'old-a', 'shared.jpg'), 'a');
1574
- await writeFile(path.join(siteDir, 'images', 'old-b', 'shared.jpg'), 'b');
1575
-
1576
- await assert.rejects(
1577
- () => runContentScript(siteDir, ['--write', '--yes']),
1578
- (error) => {
1579
- assert.match(error.output, /Cannot relocate "shared\.jpg"\. Multiple files with this filename were found:/);
1580
- assert.match(error.output, /site\/images\/old-a\/shared\.jpg/);
1581
- assert.match(error.output, /site\/images\/old-b\/shared\.jpg/);
1582
- return true;
1583
- },
1584
- );
1585
-
1586
- assert.equal(await fileExists(path.join(siteDir, 'images', 'shared.jpg')), false);
1587
- assert.equal(await fileExists(path.join(siteDir, 'images', 'old-a', 'shared.jpg')), true);
1588
- assert.equal(await fileExists(path.join(siteDir, 'images', 'old-b', 'shared.jpg')), true);
1589
- } finally {
1590
- await rm(root, { recursive: true, force: true });
1591
- }
1592
- });
1593
-
1594
- test('content:sync moves one page-local image once when multiple sections reference it', async () => {
1595
- const { root, siteDir } = await createTempSite();
1596
- try {
1597
- await writeFile(path.join(siteDir, 'content.md'), `---
1598
- page:
1599
- description: Fixture
1600
- ---
1601
-
1602
- # Copied Image Reference
1603
-
1604
- ## Intro {#intro}
1605
-
1606
- \`\`\`norna-image-stack
1607
- - image: shared.jpg
1608
- alt: Shared original
1609
- \`\`\`
1610
-
1611
- ## Plain {#plain}
1612
-
1613
- \`\`\`norna-image-stack
1614
- - image: shared.jpg
1615
- alt: Shared copy
1616
- \`\`\`
1617
- `);
1618
- await mkdir(path.join(siteDir, 'images', 'intro'), { recursive: true });
1619
- await writeFile(path.join(siteDir, 'images', 'intro', 'shared.jpg'), 'shared image');
1620
-
1621
- await runContentScript(siteDir, ['--write', '--yes']);
1622
-
1623
- assert.equal(await fileExists(path.join(siteDir, 'images', 'intro', 'shared.jpg')), false);
1624
- assert.equal(await fileExists(path.join(siteDir, 'images', 'shared.jpg')), true);
1625
- await runContentScript(siteDir, ['--check']);
1626
- } finally {
1627
- await rm(root, { recursive: true, force: true });
1628
- }
1629
- });