@janga/norna 0.7.8 → 0.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +1 -1
  2. package/docs/README.md +18 -11
  3. package/docs/configuration.md +9 -54
  4. package/docs/content.md +6 -7
  5. package/docs/design/norna-diagram-design.md +158 -0
  6. package/docs/getting-started.md +18 -16
  7. package/docs/routes.md +7 -6
  8. package/docs/site-structure.md +4 -2
  9. package/docs/sitewide-content.md +74 -0
  10. package/docs/theme.md +61 -128
  11. package/docs/typography.md +45 -33
  12. package/examples/001-dog-shelter-single-page/README.md +9 -0
  13. package/examples/001-dog-shelter-single-page/site/config.mjs +27 -0
  14. package/examples/001-dog-shelter-single-page/site/content.md +38 -0
  15. package/examples/001-dog-shelter-single-page/site/images/what-we-do/dog-house.svg +25 -0
  16. package/examples/001-dog-shelter-single-page/site/images/you-can-help/heart.svg +13 -0
  17. package/examples/001-dog-shelter-single-page/site/public/favicon.svg +10 -0
  18. package/examples/001-dog-shelter-single-page/site/public/robots.txt +2 -0
  19. package/examples/001-dog-shelter-single-page/site/sitewide-content.md +6 -0
  20. package/examples/001-dog-shelter-single-page/site/theme.md +25 -0
  21. package/examples/002-dog-shelter-multi-page/README.md +9 -0
  22. package/examples/002-dog-shelter-multi-page/site/config.mjs +27 -0
  23. package/examples/002-dog-shelter-multi-page/site/content.md +12 -0
  24. package/examples/002-dog-shelter-multi-page/site/public/favicon.svg +10 -0
  25. package/examples/002-dog-shelter-multi-page/site/public/robots.txt +2 -0
  26. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.svg +19 -0
  27. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.svg +19 -0
  28. package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/route-content.md +30 -0
  29. package/examples/002-dog-shelter-multi-page/site/routes/020-adopt/route-content.md +12 -0
  30. package/examples/002-dog-shelter-multi-page/site/sitewide-content.md +6 -0
  31. package/examples/002-dog-shelter-multi-page/site/theme.md +25 -0
  32. package/examples/dog-gallery/.astro/collections/site.schema.json +776 -0
  33. package/examples/dog-gallery/.astro/collections/theme.schema.json +672 -0
  34. package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
  35. package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
  36. package/examples/dog-gallery/.astro/content.d.ts +175 -0
  37. package/examples/dog-gallery/.astro/data-store.json +1 -0
  38. package/examples/dog-gallery/.astro/dev-local.json +8 -0
  39. package/examples/dog-gallery/.astro/dev.json +13 -0
  40. package/examples/dog-gallery/.astro/dev.log +95 -0
  41. package/examples/dog-gallery/.astro/settings.json +5 -0
  42. package/examples/dog-gallery/.astro/types.d.ts +2 -0
  43. package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  44. package/examples/dog-gallery/dist/_astro/SitePage.DicGh_5N.css +1 -0
  45. package/examples/dog-gallery/dist/dog-care/index.html +15 -0
  46. package/examples/dog-gallery/dist/dog-shelter-logo.svg +14 -0
  47. package/examples/dog-gallery/dist/favicon.svg +7 -0
  48. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
  49. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
  50. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
  51. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
  52. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
  53. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
  54. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
  55. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
  56. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
  57. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
  58. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
  59. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
  60. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
  61. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
  62. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
  63. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
  64. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
  65. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
  66. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
  67. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
  68. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
  69. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
  70. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
  71. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
  72. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
  73. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
  74. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
  75. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
  76. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
  77. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
  78. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
  79. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
  80. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
  81. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
  82. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
  83. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
  84. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
  85. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
  86. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
  87. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
  88. package/examples/dog-gallery/dist/index.html +45 -0
  89. package/examples/dog-gallery/dist/robots.txt +2 -0
  90. package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +7 -7
  91. package/examples/dog-gallery/site/.norna/generated-images.json +8 -8
  92. package/examples/dog-gallery/site/config.mjs +0 -20
  93. package/examples/dog-gallery/site/content.md +4 -5
  94. package/examples/dog-gallery/site/public/logo.svg +14 -0
  95. package/examples/dog-gallery/site/sitewide-content.md +16 -0
  96. package/examples/dog-gallery/site/theme.md +13 -24
  97. package/examples/presentation/README.md +15 -0
  98. package/examples/presentation/site/.norna/generated-images.json +66 -0
  99. package/examples/presentation/site/config.mjs +29 -0
  100. package/examples/presentation/site/content.md +67 -0
  101. package/examples/presentation/site/public/robots.txt +2 -0
  102. package/examples/presentation/site/routes/010-presets/route-content.md +63 -0
  103. package/examples/presentation/site/routes/010-presets/theme.md +4 -0
  104. package/examples/presentation/site/routes/020-media/images/cards/card-carousel.svg +11 -0
  105. package/examples/presentation/site/routes/020-media/images/cards/card-stack.svg +9 -0
  106. package/examples/presentation/site/routes/020-media/images/cards/card-surfaces.svg +8 -0
  107. package/examples/presentation/site/routes/020-media/images/carousel/carousel-one.svg +8 -0
  108. package/examples/presentation/site/routes/020-media/images/carousel/carousel-three.svg +13 -0
  109. package/examples/presentation/site/routes/020-media/images/carousel/carousel-two.svg +8 -0
  110. package/examples/presentation/site/routes/020-media/images/stack/stack-one.svg +8 -0
  111. package/examples/presentation/site/routes/020-media/images/stack/stack-two.svg +9 -0
  112. package/examples/presentation/site/routes/020-media/route-content.md +77 -0
  113. package/examples/presentation/site/routes/020-media/theme.md +4 -0
  114. package/examples/presentation/site/routes/030-surfaces/route-content.md +44 -0
  115. package/examples/presentation/site/routes/030-surfaces/theme.md +7 -0
  116. package/examples/presentation/site/sitewide-content.md +6 -0
  117. package/examples/presentation/site/theme.md +43 -0
  118. package/examples/typography-presets/.astro/collections/routeTheme.schema.json +746 -0
  119. package/examples/typography-presets/.astro/collections/site.schema.json +59 -0
  120. package/examples/typography-presets/.astro/collections/sitewide.schema.json +114 -0
  121. package/examples/typography-presets/.astro/collections/theme.schema.json +770 -0
  122. package/examples/typography-presets/.astro/content-assets.mjs +4 -0
  123. package/examples/typography-presets/.astro/content-modules.mjs +1 -0
  124. package/examples/typography-presets/.astro/content.d.ts +183 -0
  125. package/examples/typography-presets/.astro/data-store.json +1 -0
  126. package/examples/typography-presets/.astro/dev-local.json +8 -0
  127. package/examples/typography-presets/.astro/dev.json +15 -0
  128. package/examples/typography-presets/.astro/dev.log +45 -0
  129. package/examples/typography-presets/.astro/settings.json +5 -0
  130. package/examples/typography-presets/.astro/types.d.ts +2 -0
  131. package/examples/typography-presets/node_modules/.vite/deps/_metadata.json +80 -0
  132. package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  133. package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  134. package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  135. package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  136. package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  137. package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  138. package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  139. package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  140. package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  141. package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  142. package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  143. package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  144. package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js +1389 -0
  145. package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js.map +1 -0
  146. package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  147. package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  148. package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  149. package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  150. package/examples/typography-presets/node_modules/.vite/deps/package.json +3 -0
  151. package/examples/typography-presets/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  152. package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  153. package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  154. package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  155. package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  156. package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  157. package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  158. package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  159. package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  160. package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  161. package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  162. package/examples/typography-presets/site/.norna/generated-images.json +290 -0
  163. package/examples/typography-presets/site/config.mjs +29 -0
  164. package/examples/typography-presets/site/content.md +16 -0
  165. package/examples/typography-presets/site/public/robots.txt +2 -0
  166. package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/puppy-portrait.jpg +0 -0
  167. package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/shepherd-portrait.jpg +0 -0
  168. package/examples/typography-presets/site/routes/010-quiet-gallery/images/quiet-gallery/white-puppy.jpg +0 -0
  169. package/examples/typography-presets/site/routes/010-quiet-gallery/route-content.md +34 -0
  170. package/examples/typography-presets/site/routes/010-quiet-gallery/theme.md +4 -0
  171. package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/black-schnauzer.jpg +0 -0
  172. package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/retriever-portrait.jpg +0 -0
  173. package/examples/typography-presets/site/routes/020-compact-gallery/images/compact-gallery/schnauzer-portrait.jpg +0 -0
  174. package/examples/typography-presets/site/routes/020-compact-gallery/route-content.md +33 -0
  175. package/examples/typography-presets/site/routes/020-compact-gallery/theme.md +4 -0
  176. package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/boxer-portrait.jpg +0 -0
  177. package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/grass-puppy.jpg +0 -0
  178. package/examples/typography-presets/site/routes/030-text-forward/images/text-forward/grey-street-dog.jpg +0 -0
  179. package/examples/typography-presets/site/routes/030-text-forward/route-content.md +34 -0
  180. package/examples/typography-presets/site/routes/030-text-forward/theme.md +4 -0
  181. package/examples/typography-presets/site/routes/040-statement/images/statement/black-pedigree.jpg +0 -0
  182. package/examples/typography-presets/site/routes/040-statement/images/statement/sheepdog-grass.jpg +0 -0
  183. package/examples/typography-presets/site/routes/040-statement/images/statement/sheepdog-wide.jpg +0 -0
  184. package/examples/typography-presets/site/routes/040-statement/route-content.md +34 -0
  185. package/examples/typography-presets/site/routes/040-statement/theme.md +4 -0
  186. package/examples/typography-presets/site/sitewide-content.md +14 -0
  187. package/examples/typography-presets/site/theme.md +43 -0
  188. package/fixtures/basic/site/config.mjs +0 -13
  189. package/fixtures/basic/site/sitewide-content.md +12 -0
  190. package/fixtures/basic/site/theme.md +2 -2
  191. package/fixtures/content-model-v2/site/content.md +2 -5
  192. package/fixtures/content-model-v2/site/theme.md +1 -3
  193. package/package.json +3 -2
  194. package/scripts/check-config.mjs +22 -1
  195. package/scripts/lib/frontmatter-yaml.mjs +28 -0
  196. package/scripts/lib/logo-assets.mjs +35 -0
  197. package/scripts/lib/norna-markdown-blocks.mjs +166 -0
  198. package/scripts/lib/presentation.mjs +104 -46
  199. package/scripts/lib/project-config.mjs +67 -105
  200. package/scripts/lib/site-content.mjs +26 -42
  201. package/scripts/lib/site-paths.mjs +2 -0
  202. package/scripts/lib/sitewide-config.mjs +34 -0
  203. package/scripts/lib/theme-config.mjs +72 -6
  204. package/scripts/lib/typography.mjs +20 -26
  205. package/scripts/show-typography.mjs +40 -147
  206. package/scripts/sync-content-sections.mjs +19 -14
  207. package/scripts/test-content-check.mjs +76 -14
  208. package/scripts/test-content-model-v2.mjs +154 -0
  209. package/scripts/test-engine-commands.mjs +3 -1
  210. package/scripts/test-package-check.mjs +78 -19
  211. package/scripts/test-temporary-visibility.mjs +19 -2
  212. package/src/components/CardList.astro +5 -2
  213. package/src/components/GalleryGrid.astro +37 -16
  214. package/src/components/SiteBanners.astro +100 -0
  215. package/src/components/SiteNavigation.astro +48 -31
  216. package/src/components/SitePage.astro +55 -13
  217. package/src/components/SiteSection.astro +194 -36
  218. package/src/content.config.ts +78 -48
  219. package/src/layouts/BaseLayout.astro +64 -14
  220. package/src/lib/routeThemes.ts +61 -0
  221. package/src/lib/sectionContent.ts +236 -44
  222. package/src/lib/sitePublicAssets.ts +1 -0
  223. package/src/styles/global.css +282 -93
  224. package/starters/basic/.github/workflows/deploy.yml +1 -1
  225. package/starters/basic/README.md +7 -6
  226. package/starters/basic/site/config.mjs +0 -13
  227. package/starters/basic/site/content.md +1 -1
  228. package/starters/basic/site/sitewide-content.md +14 -0
  229. package/starters/basic/site/theme.md +13 -24
  230. package/starters/project/.github/workflows/deploy.yml +1 -1
  231. package/starters/project/README.md +10 -8
  232. package/starters/project/site/config.mjs +0 -13
  233. package/starters/project/site/sitewide-content.md +14 -0
  234. package/starters/project/site/theme.md +3 -11
@@ -1,18 +1,24 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
2
3
  import { parseYamlMapping } from './frontmatter-yaml.mjs';
3
4
  import {
4
5
  splitSiteFile,
5
6
  validateFrontmatterIndentation,
7
+ validateRouteThemeFrontmatterStructure,
6
8
  validateThemeFrontmatterStructure,
7
9
  } from './site-content.mjs';
8
- import { siteThemeLabel, siteThemePath } from './site-paths.mjs';
9
-
10
- const emptyThemeFile = '---\n---\n';
10
+ import {
11
+ siteRoutesDir,
12
+ siteRoutesLabel,
13
+ siteThemeLabel,
14
+ siteThemePath,
15
+ sitewideContentLabel,
16
+ } from './site-paths.mjs';
11
17
 
12
18
  export const readThemeConfig = async () => {
13
19
  const themeFile = await readFile(siteThemePath, 'utf8').catch((error) => {
14
20
  if (error?.code === 'ENOENT') {
15
- return emptyThemeFile;
21
+ throw new Error(`${siteThemeLabel} is required. Create it before running Norna.`);
16
22
  }
17
23
 
18
24
  throw error;
@@ -30,5 +36,65 @@ export const readThemeConfig = async () => {
30
36
  ].join('\n'));
31
37
  }
32
38
 
33
- return parseYamlMapping(frontmatterBody);
39
+ const config = parseYamlMapping(frontmatterBody);
40
+ if (Object.hasOwn(config, 'navigation')) {
41
+ throw new Error(`${siteThemeLabel} may not define navigation. Brand and logo belong in ${sitewideContentLabel}.`);
42
+ }
43
+
44
+ return config;
45
+ };
46
+
47
+ const getRouteThemeFiles = async (directory, relativeDirectory = '') => {
48
+ const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
49
+ if (error?.code === 'ENOENT') return [];
50
+ throw error;
51
+ });
52
+ const files = [];
53
+
54
+ for (const entry of entries) {
55
+ const relativePath = path.join(relativeDirectory, entry.name);
56
+ const absolutePath = path.join(directory, entry.name);
57
+ if (entry.isDirectory()) {
58
+ files.push(...await getRouteThemeFiles(absolutePath, relativePath));
59
+ continue;
60
+ }
61
+
62
+ if (entry.name === 'theme.md') {
63
+ files.push({
64
+ path: absolutePath,
65
+ label: `${siteRoutesLabel}/${relativePath.split(path.sep).join('/')}`,
66
+ });
67
+ }
68
+ }
69
+
70
+ return files;
71
+ };
72
+
73
+ export const validateRouteThemeFiles = async () => {
74
+ const files = await getRouteThemeFiles(siteRoutesDir);
75
+ const configs = [];
76
+
77
+ for (const file of files) {
78
+ const source = await readFile(file.path, 'utf8');
79
+ const { frontmatter, frontmatterBody } = splitSiteFile(source, file.label);
80
+ const issues = [];
81
+ validateFrontmatterIndentation(frontmatter, (issue) => issues.push(issue));
82
+ validateRouteThemeFrontmatterStructure(frontmatter, (issue) => issues.push(issue));
83
+
84
+ if (issues.length > 0) {
85
+ throw new Error([
86
+ `${file.label} has invalid frontmatter.`,
87
+ ...issues.map((issue) => `- ${issue.message}`),
88
+ ].join('\n'));
89
+ }
90
+
91
+ const config = parseYamlMapping(frontmatterBody);
92
+ if (Object.hasOwn(config, 'navigation')) {
93
+ throw new Error(`${file.label} may not define navigation. Brand and logo belong in ${sitewideContentLabel}.`);
94
+ }
95
+
96
+ configs.push({ ...file, config });
97
+ }
98
+
99
+ return configs;
34
100
  };
@@ -17,27 +17,32 @@ export const typographyPresets = {
17
17
  h1: {
18
18
  align: { desktop: 'left', mobile: 'left' },
19
19
  size: 'medium',
20
+ weight: 400,
20
21
  lineHeight: 1.04,
21
22
  },
22
23
  h2: {
23
24
  align: { desktop: 'left', mobile: 'left' },
24
25
  size: 'medium',
26
+ weight: 400,
25
27
  lineHeight: 1.08,
26
28
  },
27
29
  h3: {
28
30
  align: { desktop: 'left', mobile: 'left' },
29
31
  size: 'medium',
32
+ weight: 500,
30
33
  lineHeight: 1.35,
31
34
  },
32
35
  h4: {
33
36
  align: { desktop: 'left', mobile: 'left' },
34
37
  size: 'medium',
38
+ weight: 400,
35
39
  lineHeight: 1.5,
36
40
  },
37
41
  },
38
42
  body: {
39
43
  align: { desktop: 'left', mobile: 'left' },
40
44
  size: 'medium',
45
+ width: 'normal',
41
46
  lineHeight: 1.5,
42
47
  },
43
48
  caption: {
@@ -51,27 +56,32 @@ export const typographyPresets = {
51
56
  h1: {
52
57
  align: { desktop: 'left', mobile: 'left' },
53
58
  size: 'medium',
59
+ weight: 500,
54
60
  lineHeight: 1.04,
55
61
  },
56
62
  h2: {
57
63
  align: { desktop: 'left', mobile: 'left' },
58
64
  size: 'medium',
65
+ weight: 500,
59
66
  lineHeight: 1.08,
60
67
  },
61
68
  h3: {
62
69
  align: { desktop: 'left', mobile: 'left' },
63
70
  size: 'medium',
71
+ weight: 600,
64
72
  lineHeight: 1.3,
65
73
  },
66
74
  h4: {
67
75
  align: { desktop: 'left', mobile: 'left' },
68
76
  size: 'medium',
77
+ weight: 500,
69
78
  lineHeight: 1.42,
70
79
  },
71
80
  },
72
81
  body: {
73
82
  align: { desktop: 'left', mobile: 'left' },
74
83
  size: 'medium',
84
+ width: 'wide',
75
85
  lineHeight: 1.42,
76
86
  },
77
87
  caption: {
@@ -85,27 +95,32 @@ export const typographyPresets = {
85
95
  h1: {
86
96
  align: { desktop: 'left', mobile: 'left' },
87
97
  size: 'medium',
98
+ weight: 500,
88
99
  lineHeight: 1.06,
89
100
  },
90
101
  h2: {
91
102
  align: { desktop: 'left', mobile: 'left' },
92
103
  size: 'medium',
104
+ weight: 500,
93
105
  lineHeight: 1.12,
94
106
  },
95
107
  h3: {
96
108
  align: { desktop: 'left', mobile: 'left' },
97
109
  size: 'medium',
110
+ weight: 500,
98
111
  lineHeight: 1.35,
99
112
  },
100
113
  h4: {
101
114
  align: { desktop: 'left', mobile: 'left' },
102
115
  size: 'medium',
116
+ weight: 400,
103
117
  lineHeight: 1.55,
104
118
  },
105
119
  },
106
120
  body: {
107
121
  align: { desktop: 'left', mobile: 'left' },
108
122
  size: 'medium',
123
+ width: 'narrow',
109
124
  lineHeight: 1.62,
110
125
  },
111
126
  caption: {
@@ -119,27 +134,32 @@ export const typographyPresets = {
119
134
  h1: {
120
135
  align: { desktop: 'left', mobile: 'left' },
121
136
  size: 'medium',
137
+ weight: 600,
122
138
  lineHeight: 1.02,
123
139
  },
124
140
  h2: {
125
141
  align: { desktop: 'left', mobile: 'left' },
126
142
  size: 'medium',
143
+ weight: 600,
127
144
  lineHeight: 1.04,
128
145
  },
129
146
  h3: {
130
147
  align: { desktop: 'left', mobile: 'left' },
131
148
  size: 'medium',
149
+ weight: 600,
132
150
  lineHeight: 1.18,
133
151
  },
134
152
  h4: {
135
153
  align: { desktop: 'left', mobile: 'left' },
136
154
  size: 'medium',
155
+ weight: 500,
137
156
  lineHeight: 1.42,
138
157
  },
139
158
  },
140
159
  body: {
141
160
  align: { desktop: 'left', mobile: 'left' },
142
161
  size: 'medium',
162
+ width: 'narrow',
143
163
  lineHeight: 1.42,
144
164
  },
145
165
  caption: {
@@ -243,32 +263,6 @@ export const resolveTypographyConfig = (typography = defaultTypography) => {
243
263
  };
244
264
  };
245
265
 
246
- export const resolveTypographyOverride = (baseResolved, typographyConfig) => {
247
- if (typographyConfig?.preset || typographyConfig?.rhythm) {
248
- return resolveTypographyConfig({
249
- preset: typographyConfig.preset ?? baseResolved.preset,
250
- rhythm: typographyConfig.rhythm ?? baseResolved.rhythm,
251
- overrides: typographyConfig.overrides,
252
- });
253
- }
254
-
255
- if (typographyConfig?.overrides) {
256
- return {
257
- preset: baseResolved.preset,
258
- rhythm: baseResolved.rhythm,
259
- values: mergeDeep(baseResolved.values, typographyConfig.overrides),
260
- };
261
- }
262
-
263
- return baseResolved;
264
- };
265
-
266
- export const resolveSectionTypography = (defaultTypographyConfig, sectionTypographyConfig) => {
267
- const defaultResolved = resolveTypographyConfig(defaultTypographyConfig);
268
-
269
- return resolveTypographyOverride(defaultResolved, sectionTypographyConfig);
270
- };
271
-
272
266
  const quoteString = (value) => typeof value === 'string' && !/^[a-z0-9.-]+$/i.test(value)
273
267
  ? JSON.stringify(value)
274
268
  : value;
@@ -1,8 +1,8 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
2
3
  import { findMap } from './lib/frontmatter-yaml.mjs';
3
4
  import {
4
5
  defaultTypography,
5
- resolveTypographyOverride,
6
6
  resolveTypographyConfig,
7
7
  toYamlLines,
8
8
  typographyPresets,
@@ -15,11 +15,13 @@ import {
15
15
  splitSiteFile,
16
16
  validateContentFrontmatterStructure,
17
17
  validateFrontmatterIndentation,
18
+ validateRouteThemeFrontmatterStructure,
18
19
  validateThemeFrontmatterStructure,
19
20
  } from './lib/site-content.mjs';
20
21
  import {
21
22
  siteContentLabel,
22
23
  siteContentPath,
24
+ siteRoutesDir,
23
25
  siteThemeLabel,
24
26
  siteThemePath,
25
27
  } from './lib/site-paths.mjs';
@@ -30,30 +32,35 @@ const typographyValuePaths = [
30
32
  ['headings', 'h1', 'align', 'desktop'],
31
33
  ['headings', 'h1', 'align', 'mobile'],
32
34
  ['headings', 'h1', 'size'],
35
+ ['headings', 'h1', 'weight'],
33
36
  ['headings', 'h1', 'lineHeight'],
34
37
  ['headings', 'h1', 'spacingBefore'],
35
38
  ['headings', 'h1', 'spacingAfter'],
36
39
  ['headings', 'h2', 'align', 'desktop'],
37
40
  ['headings', 'h2', 'align', 'mobile'],
38
41
  ['headings', 'h2', 'size'],
42
+ ['headings', 'h2', 'weight'],
39
43
  ['headings', 'h2', 'lineHeight'],
40
44
  ['headings', 'h2', 'spacingBefore'],
41
45
  ['headings', 'h2', 'spacingAfter'],
42
46
  ['headings', 'h3', 'align', 'desktop'],
43
47
  ['headings', 'h3', 'align', 'mobile'],
44
48
  ['headings', 'h3', 'size'],
49
+ ['headings', 'h3', 'weight'],
45
50
  ['headings', 'h3', 'lineHeight'],
46
51
  ['headings', 'h3', 'spacingBefore'],
47
52
  ['headings', 'h3', 'spacingAfter'],
48
53
  ['headings', 'h4', 'align', 'desktop'],
49
54
  ['headings', 'h4', 'align', 'mobile'],
50
55
  ['headings', 'h4', 'size'],
56
+ ['headings', 'h4', 'weight'],
51
57
  ['headings', 'h4', 'lineHeight'],
52
58
  ['headings', 'h4', 'spacingBefore'],
53
59
  ['headings', 'h4', 'spacingAfter'],
54
60
  ['body', 'align', 'desktop'],
55
61
  ['body', 'align', 'mobile'],
56
62
  ['body', 'size'],
63
+ ['body', 'width'],
57
64
  ['body', 'lineHeight'],
58
65
  ['body', 'paragraphSpacing'],
59
66
  ['caption', 'align', 'desktop'],
@@ -74,8 +81,6 @@ const rhythmValuePaths = new Set([
74
81
  'body.paragraphSpacing',
75
82
  'caption.spacingBefore',
76
83
  ]);
77
- const countIndent = (line) => line.match(/^\s*/)?.[0].length ?? 0;
78
-
79
84
  const isPlainObject = (value) => (
80
85
  value !== null &&
81
86
  typeof value === 'object' &&
@@ -156,19 +161,6 @@ const applyOverrideSources = (sources, typographyConfig, sourceLabel) => {
156
161
  return sources;
157
162
  };
158
163
 
159
- const inheritedSources = (sources) => {
160
- const inherited = structuredClone(sources);
161
-
162
- for (const path of typographyValuePaths) {
163
- setPath(inherited, path, {
164
- ...getPath(sources, path),
165
- inherited: true,
166
- });
167
- }
168
-
169
- return inherited;
170
- };
171
-
172
164
  const resolveAnnotatedTypographyConfig = (typographyConfig, sourceLabel) => {
173
165
  const resolved = resolveTypographyConfig(typographyConfig ?? defaultTypography);
174
166
  const sources = applyOverrideSources(
@@ -193,126 +185,14 @@ const resolveAnnotatedTypographyConfig = (typographyConfig, sourceLabel) => {
193
185
  };
194
186
  };
195
187
 
196
- const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sourceLabel) => {
197
- if (typographyConfig?.preset || typographyConfig?.rhythm) {
198
- const resolvedConfig = {
199
- preset: typographyConfig.preset ?? baseAnnotated.resolved.preset,
200
- rhythm: typographyConfig.rhythm ?? baseAnnotated.resolved.rhythm,
201
- overrides: typographyConfig.overrides,
202
- };
203
- const resolved = resolveTypographyConfig(resolvedConfig);
204
- const sources = applyOverrideSources(
205
- defaultSources(resolved.preset, resolved.rhythm),
206
- typographyConfig,
207
- sourceLabel,
208
- );
209
-
210
- return {
211
- preset: {
212
- value: resolved.preset,
213
- source: typographyConfig.preset ? sourceLabel : baseAnnotated.preset.source,
214
- ...(typographyConfig.preset ? {} : { inherited: true }),
215
- },
216
- rhythm: {
217
- value: resolved.rhythm,
218
- source: typographyConfig.rhythm ? sourceLabel : baseAnnotated.rhythm.source,
219
- ...(typographyConfig.rhythm ? {} : { inherited: true }),
220
- },
221
- resolved,
222
- sources,
223
- };
224
- }
225
-
226
- if (typographyConfig?.overrides) {
227
- const resolved = resolveTypographyOverride(baseAnnotated.resolved, typographyConfig);
228
- const sources = applyOverrideSources(
229
- inheritedSources(baseAnnotated.sources),
230
- typographyConfig,
231
- sourceLabel,
232
- );
233
-
234
- return {
235
- preset: {
236
- value: resolved.preset,
237
- source: baseAnnotated.preset.source,
238
- inherited: true,
239
- },
240
- rhythm: {
241
- value: resolved.rhythm,
242
- source: baseAnnotated.rhythm.source,
243
- inherited: true,
244
- },
245
- resolved,
246
- sources,
247
- };
248
- }
249
-
250
- return {
251
- preset: {
252
- value: baseAnnotated.resolved.preset,
253
- source: baseAnnotated.preset.source,
254
- inherited: true,
255
- },
256
- rhythm: {
257
- value: baseAnnotated.resolved.rhythm,
258
- source: baseAnnotated.rhythm.source,
259
- inherited: true,
260
- },
261
- resolved: baseAnnotated.resolved,
262
- sources: inheritedSources(baseAnnotated.sources),
263
- };
264
- };
265
-
266
188
  const formatAnnotatedTypography = (annotated) => ({
267
189
  preset: annotated.preset,
268
190
  rhythm: annotated.rhythm,
269
191
  resolved: annotateResolvedValues(annotated.resolved, annotated.sources),
270
192
  });
271
193
 
272
- const getSectionMetadataBlocks = (lines) => {
273
- const sectionsMap = findMap(lines, 'sections');
274
- if (!sectionsMap) return new Map();
275
-
276
- const sections = new Map();
277
- let current = null;
278
-
279
- for (let index = sectionsMap.index + 1; index < lines.length; index += 1) {
280
- const line = lines[index];
281
- if (!line.trim()) continue;
282
-
283
- const indent = countIndent(line);
284
- if (indent <= sectionsMap.indent) break;
285
-
286
- const sectionMatch = line.match(/^\s{2}([a-z0-9-]+):(?:\s+.*)?\s*$/);
287
- if (sectionMatch) {
288
- if (current) current.end = index;
289
- current = { id: sectionMatch[1], start: index, end: lines.length };
290
- sections.set(current.id, current);
291
- }
292
- }
293
-
294
- const sectionList = Array.from(sections.values());
295
- const finalSection = sectionList.at(-1);
296
- if (finalSection) {
297
- const afterSections = lines.findIndex((line, index) => (
298
- index > finalSection.start &&
299
- line.trim() &&
300
- countIndent(line) <= sectionsMap.indent
301
- ));
302
- finalSection.end = afterSections === -1 ? lines.length : afterSections;
303
- }
304
-
305
- return sections;
306
- };
307
-
308
194
  const readThemeTypography = async () => {
309
- const themeFile = await readFile(siteThemePath, 'utf8').catch((error) => {
310
- if (error?.code === 'ENOENT') {
311
- return '---\n---\n';
312
- }
313
-
314
- throw error;
315
- });
195
+ const themeFile = await readFile(siteThemePath, 'utf8');
316
196
  const { frontmatter: themeFrontmatter, frontmatterBody: themeFrontmatterBody } = splitSiteFile(themeFile, siteThemeLabel);
317
197
  const indentationIssues = [];
318
198
  validateFrontmatterIndentation(themeFrontmatter, (issue) => indentationIssues.push(issue));
@@ -329,8 +209,35 @@ const readThemeTypography = async () => {
329
209
  return resolveAnnotatedTypographyConfig(themeTypographyConfig ?? defaultTypography, siteThemeLabel);
330
210
  };
331
211
 
332
- const readPageTypography = async (contentFile, themeTypography) => {
333
- const { frontmatter, frontmatterBody, body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
212
+ const readRouteThemeTypography = async (contentFile, themeTypography) => {
213
+ if (contentFile.isHome) return themeTypography;
214
+
215
+ const routeThemePath = path.join(siteRoutesDir, contentFile.routeDirectory, 'theme.md');
216
+ const routeThemeLabel = `${contentFile.contentLabel.replace(/\/route-content\.md$/, '')}/theme.md`;
217
+ const routeThemeFile = await readFile(routeThemePath, 'utf8').catch((error) => {
218
+ if (error?.code === 'ENOENT') return null;
219
+ throw error;
220
+ });
221
+ if (!routeThemeFile) return themeTypography;
222
+
223
+ const { frontmatter, frontmatterBody } = splitSiteFile(routeThemeFile, routeThemeLabel);
224
+ const indentationIssues = [];
225
+ validateFrontmatterIndentation(frontmatter, (issue) => indentationIssues.push(issue));
226
+ validateRouteThemeFrontmatterStructure(frontmatter, (issue) => indentationIssues.push(issue));
227
+ if (indentationIssues.length > 0) {
228
+ throw new Error([
229
+ `Cannot inspect typography because ${routeThemeLabel} has invalid frontmatter.`,
230
+ ...indentationIssues.map((issue) => `- ${issue.message}`),
231
+ ].join('\n'));
232
+ }
233
+
234
+ const lines = frontmatterBody.split(/\r?\n/);
235
+ const typographyConfig = findMap(lines, 'typography', 0, lines.length, 0)?.value;
236
+ return resolveAnnotatedTypographyConfig(typographyConfig ?? defaultTypography, routeThemeLabel);
237
+ };
238
+
239
+ const readPageTypography = async (contentFile, siteThemeTypography) => {
240
+ const { frontmatter, body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
334
241
  const indentationIssues = [];
335
242
  validateFrontmatterIndentation(frontmatter, (issue) => indentationIssues.push(issue));
336
243
  validateContentFrontmatterStructure(frontmatter, (issue) => indentationIssues.push(issue));
@@ -341,27 +248,13 @@ const readPageTypography = async (contentFile, themeTypography) => {
341
248
  ].join('\n'));
342
249
  }
343
250
 
344
- const lines = frontmatterBody.split(/\r?\n/);
345
- const pagePresentation = findMap(lines, 'presentation', 0, lines.length, 0);
346
- const pageTypographyConfig = pagePresentation
347
- ? findMap(lines, 'typography', pagePresentation.index + 1, pagePresentation.nextIndex)?.value
348
- : null;
349
- const pageTypography = resolveAnnotatedTypographyOverride(themeTypography, pageTypographyConfig ?? undefined, contentFile.contentLabel);
350
- const sectionMetadataBlocks = getSectionMetadataBlocks(lines);
251
+ const pageTypography = await readRouteThemeTypography(contentFile, siteThemeTypography);
351
252
  const sections = getBodySections(body).sections
352
253
  .filter((section) => section.id)
353
254
  .map((section) => {
354
- const metadata = sectionMetadataBlocks.get(section.id);
355
- const presentation = metadata
356
- ? findMap(lines, 'presentation', metadata.start + 1, metadata.end)
357
- : null;
358
- const typography = presentation
359
- ? findMap(lines, 'typography', presentation.index + 1, presentation.nextIndex)?.value
360
- : null;
361
-
362
255
  return {
363
256
  id: section.id,
364
- typography: resolveAnnotatedTypographyOverride(pageTypography, typography ?? undefined, `${contentFile.contentLabel} sections.${section.id}`),
257
+ typography: pageTypography,
365
258
  };
366
259
  });
367
260
 
@@ -5,6 +5,7 @@ import { stdin as input, stdout as output } from 'node:process';
5
5
  import path from 'node:path';
6
6
  import { promisify } from 'node:util';
7
7
  import {
8
+ extractInlineNoteDiagnostics,
8
9
  extractMarkdownImageReferences,
9
10
  extractNornaMarkdownBlockDiagnostics,
10
11
  getNornaBlockImageReferences,
@@ -12,10 +13,9 @@ import {
12
13
  import {
13
14
  getBodySections,
14
15
  getContentFiles,
15
- getFrontmatterInlineStyleNames,
16
16
  getFrontmatterSections,
17
17
  getImageCandidatesByName,
18
- getInlineStyleReferences,
18
+ getDeprecatedInlineStyleReferences,
19
19
  readSiteFile,
20
20
  readThemeFile,
21
21
  toPosixPath,
@@ -400,10 +400,6 @@ try {
400
400
  }
401
401
  }
402
402
 
403
- const themeInlineStyleNames = themeFrontmatter
404
- ? getFrontmatterInlineStyleNames(themeFrontmatter)
405
- : new Set();
406
-
407
403
  const contentFiles = await getContentFiles();
408
404
  const allImageFiles = [];
409
405
  const contentFileContexts = [];
@@ -462,6 +458,17 @@ for (const contentFile of contentFiles) {
462
458
  });
463
459
  blockResultsBySectionId.set(section.id, blockResults);
464
460
 
461
+ const inlineNoteResults = extractInlineNoteDiagnostics(section.text, {
462
+ label: contentFile.contentLabel,
463
+ lineOffset: bodyLineOffset + section.line - 1,
464
+ });
465
+ for (const error of inlineNoteResults.errors) {
466
+ addSectionIssue(contentFile, section, {
467
+ severity: 'error',
468
+ message: error.message,
469
+ });
470
+ }
471
+
465
472
  for (const reference of getNornaBlockImageReferences(blockResults.blocks)) {
466
473
  const expectedPath = getExpectedImagePath(contentFile, section.id, reference.image);
467
474
  addExpectedReference(globalExpectedReferencesByPath, expectedPath, { contentFile, section, reference });
@@ -498,14 +505,12 @@ for (const context of contentFileContexts) {
498
505
  }
499
506
  }
500
507
 
501
- for (const styleName of new Set(getInlineStyleReferences(body))) {
502
- if (!themeInlineStyleNames.has(styleName)) {
503
- addContentIssue(contentFile, {
504
- severity: 'error',
505
- message: `Inline style ".${styleName}" is used in ${contentFile.contentLabel} but is not defined in ${siteThemeLabel} presentation.inlineStyles.`,
506
- fix: `Add presentation.inlineStyles.${styleName} to ${siteThemeLabel} or remove "{.${styleName}}" from Markdown.`,
507
- });
508
- }
508
+ for (const styleName of new Set(getDeprecatedInlineStyleReferences(body))) {
509
+ addContentIssue(contentFile, {
510
+ severity: 'error',
511
+ message: `Inline color style ".${styleName}" is no longer supported in ${contentFile.contentLabel}.`,
512
+ fix: 'Use standard Markdown emphasis, a blockquote, or a semantic Norna block instead of color syntax.',
513
+ });
509
514
  }
510
515
 
511
516
  for (const section of sections) {