@janga/norna 0.7.1 → 0.7.3

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 (168) hide show
  1. package/README.md +32 -18
  2. package/astro.config.mjs +2 -0
  3. package/bin/norna-cli.mjs +175 -0
  4. package/bin/norna.mjs +149 -150
  5. package/docs/README.md +41 -22
  6. package/docs/commands.md +25 -12
  7. package/docs/configuration.md +38 -135
  8. package/docs/content.md +108 -265
  9. package/docs/{command-organization.md → design/command-organization.md} +64 -60
  10. package/docs/{site-examples-structure-note.md → design/site-examples-structure.md} +19 -24
  11. package/docs/engine-development.md +35 -7
  12. package/docs/getting-started.md +55 -26
  13. package/docs/images-and-metadata.md +1 -1
  14. package/docs/local-development.md +14 -1
  15. package/docs/publishing.md +24 -1
  16. package/docs/routes.md +90 -0
  17. package/docs/site-structure.md +18 -10
  18. package/docs/theme.md +292 -0
  19. package/docs/typography.md +144 -0
  20. package/examples/dog-gallery/.astro/collections/site.schema.json +802 -0
  21. package/examples/dog-gallery/.astro/collections/theme.schema.json +714 -0
  22. package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
  23. package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
  24. package/examples/dog-gallery/.astro/content.d.ts +175 -0
  25. package/examples/dog-gallery/.astro/data-store.json +1 -0
  26. package/examples/dog-gallery/.astro/dev-local.json +8 -0
  27. package/examples/dog-gallery/.astro/dev.json +13 -0
  28. package/examples/dog-gallery/.astro/dev.log +49 -0
  29. package/examples/dog-gallery/.astro/settings.json +5 -0
  30. package/examples/dog-gallery/.astro/types.d.ts +2 -0
  31. package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
  32. package/examples/dog-gallery/dist/_astro/SitePage.BFHZm5UG.css +1 -0
  33. package/examples/dog-gallery/dist/dog-care/index.html +15 -0
  34. package/examples/dog-gallery/dist/favicon.svg +7 -0
  35. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
  36. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
  37. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
  38. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
  39. package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
  40. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
  41. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
  42. package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
  43. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
  44. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
  45. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
  46. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
  47. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
  48. package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
  49. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
  50. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
  51. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
  52. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
  53. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
  54. package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
  55. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
  56. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
  57. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
  58. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
  59. package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
  60. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
  61. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
  62. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
  63. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
  64. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
  65. package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
  66. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
  67. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
  68. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
  69. package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
  70. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
  71. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
  72. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
  73. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
  74. package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
  75. package/examples/dog-gallery/dist/index.html +43 -0
  76. package/examples/dog-gallery/dist/robots.txt +2 -0
  77. package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +74 -0
  78. package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
  79. package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
  80. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  81. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  82. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
  83. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  84. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  85. package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  86. package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  87. package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  88. package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
  89. package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
  90. package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
  91. package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
  92. package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
  93. package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
  94. package/examples/dog-gallery/node_modules/.vite/deps/package.json +3 -0
  95. package/examples/dog-gallery/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
  96. package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
  97. package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
  98. package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
  99. package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
  100. package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
  101. package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
  102. package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
  103. package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
  104. package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
  105. package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
  106. package/examples/dog-gallery/site/.norna/generated-images.json +226 -0
  107. package/examples/dog-gallery/site/config.mjs +67 -0
  108. package/examples/dog-gallery/site/content.md +168 -0
  109. package/examples/dog-gallery/site/images/black-dogs/black-puppy-meadow.png +0 -0
  110. package/examples/dog-gallery/site/images/black-dogs/photo-of-a-black-dog.jpg +0 -0
  111. package/examples/dog-gallery/site/images/brown-dogs/brown-dog.jpg +0 -0
  112. package/examples/dog-gallery/site/images/brown-dogs/dog-accompanies-master.jpg +0 -0
  113. package/examples/dog-gallery/site/images/golden-dogs/golden-retriever.jpg +0 -0
  114. package/examples/dog-gallery/site/images/golden-dogs/toller-puppy.jpg +0 -0
  115. package/examples/dog-gallery/site/images/white-dogs/white-cute-dog.jpg +0 -0
  116. package/examples/dog-gallery/site/images/white-dogs/white-puppy-garden.png +0 -0
  117. package/examples/dog-gallery/site/public/favicon.svg +7 -0
  118. package/examples/dog-gallery/site/public/robots.txt +2 -0
  119. package/examples/dog-gallery/site/routes/dog-care/route-content.md +49 -0
  120. package/examples/dog-gallery/site/theme.md +114 -0
  121. package/fixtures/basic/site/config.mjs +3 -22
  122. package/fixtures/basic/site/content.md +1 -1
  123. package/fixtures/basic/site/theme.md +7 -3
  124. package/package.json +10 -9
  125. package/scripts/check-config.mjs +4 -3
  126. package/scripts/deploy-site.mjs +3 -3
  127. package/scripts/dev-local.mjs +64 -15
  128. package/scripts/init-site.mjs +11 -9
  129. package/scripts/lib/frontmatter-yaml.mjs +85 -0
  130. package/scripts/lib/presentation.mjs +10 -11
  131. package/scripts/lib/project-config.mjs +113 -45
  132. package/scripts/lib/site-content.mjs +21 -2
  133. package/scripts/lib/site-paths.mjs +18 -3
  134. package/scripts/lib/theme-config.mjs +34 -0
  135. package/scripts/lib/typography.mjs +103 -23
  136. package/scripts/show-typography.mjs +202 -75
  137. package/scripts/sync-content-sections.mjs +6 -6
  138. package/scripts/test-cli-discovery.mjs +124 -0
  139. package/scripts/test-content-check.mjs +3 -4
  140. package/scripts/test-engine-commands.mjs +20 -6
  141. package/scripts/test-navigation.mjs +10 -6
  142. package/scripts/test-package-check.mjs +40 -11
  143. package/scripts/test-temporary-visibility.mjs +2 -3
  144. package/src/components/SiteNavigation.astro +7 -5
  145. package/src/components/SitePage.astro +5 -3
  146. package/src/components/SiteSection.astro +63 -29
  147. package/src/content.config.ts +64 -2
  148. package/src/layouts/BaseLayout.astro +19 -1
  149. package/src/lib/basePath.ts +21 -0
  150. package/src/lib/generatedImages.ts +8 -4
  151. package/src/lib/sectionContent.ts +6 -1
  152. package/src/lib/sitePublicAssets.ts +8 -1
  153. package/src/styles/global.css +59 -55
  154. package/starters/basic/.github/workflows/deploy.yml +3 -3
  155. package/starters/basic/README.md +30 -14
  156. package/starters/basic/package.json +3 -3
  157. package/starters/basic/site/config.mjs +4 -24
  158. package/starters/basic/site/content.md +44 -9
  159. package/starters/basic/site/theme.md +113 -16
  160. package/starters/project/.github/workflows/deploy.yml +65 -0
  161. package/starters/project/README.md +72 -0
  162. package/starters/project/package-lock.json +4296 -0
  163. package/starters/project/package.json +35 -0
  164. package/starters/project/site/config.mjs +43 -0
  165. package/starters/project/site/content.md +88 -0
  166. package/starters/project/site/public/robots.txt +2 -0
  167. package/starters/project/site/routes/guide/route-content.md +58 -0
  168. package/starters/project/site/theme.md +15 -0
@@ -7,11 +7,31 @@ export const typographyPresetNames = [
7
7
 
8
8
  export const typographyPresets = {
9
9
  'quiet-gallery': {
10
- heading: {
11
- align: { desktop: 'left', mobile: 'left' },
12
- size: 'medium',
13
- lineHeight: 1.08,
14
- spacing: '0.65em',
10
+ headings: {
11
+ h1: {
12
+ align: { desktop: 'left', mobile: 'left' },
13
+ size: 'large',
14
+ lineHeight: 1.04,
15
+ spacing: '0.55em',
16
+ },
17
+ h2: {
18
+ align: { desktop: 'left', mobile: 'left' },
19
+ size: 'medium',
20
+ lineHeight: 1.08,
21
+ spacing: '0.65em',
22
+ },
23
+ h3: {
24
+ align: { desktop: 'left', mobile: 'left' },
25
+ size: 'small',
26
+ lineHeight: 1.35,
27
+ spacing: '0.9rem',
28
+ },
29
+ h4: {
30
+ align: { desktop: 'left', mobile: 'left' },
31
+ size: 'medium',
32
+ lineHeight: 1.5,
33
+ spacing: '0.85rem',
34
+ },
15
35
  },
16
36
  body: {
17
37
  align: { desktop: 'left', mobile: 'left' },
@@ -27,15 +47,35 @@ export const typographyPresets = {
27
47
  },
28
48
  },
29
49
  'compact-gallery': {
30
- heading: {
31
- align: { desktop: 'left', mobile: 'left' },
32
- size: 'small',
33
- lineHeight: 1.08,
34
- spacing: '0.45em',
50
+ headings: {
51
+ h1: {
52
+ align: { desktop: 'left', mobile: 'left' },
53
+ size: 'large',
54
+ lineHeight: 1.04,
55
+ spacing: '0.45em',
56
+ },
57
+ h2: {
58
+ align: { desktop: 'left', mobile: 'left' },
59
+ size: 'medium',
60
+ lineHeight: 1.08,
61
+ spacing: '0.45em',
62
+ },
63
+ h3: {
64
+ align: { desktop: 'left', mobile: 'left' },
65
+ size: 'small',
66
+ lineHeight: 1.3,
67
+ spacing: '0.75rem',
68
+ },
69
+ h4: {
70
+ align: { desktop: 'left', mobile: 'left' },
71
+ size: 'medium',
72
+ lineHeight: 1.42,
73
+ spacing: '0.6rem',
74
+ },
35
75
  },
36
76
  body: {
37
77
  align: { desktop: 'left', mobile: 'left' },
38
- size: 'small',
78
+ size: 'medium',
39
79
  lineHeight: 1.42,
40
80
  paragraphSpacing: '0.6em',
41
81
  },
@@ -47,15 +87,35 @@ export const typographyPresets = {
47
87
  },
48
88
  },
49
89
  'text-forward': {
50
- heading: {
51
- align: { desktop: 'left', mobile: 'left' },
52
- size: 'medium',
53
- lineHeight: 1.12,
54
- spacing: '0.8em',
90
+ headings: {
91
+ h1: {
92
+ align: { desktop: 'left', mobile: 'left' },
93
+ size: 'large',
94
+ lineHeight: 1.06,
95
+ spacing: '0.65em',
96
+ },
97
+ h2: {
98
+ align: { desktop: 'left', mobile: 'left' },
99
+ size: 'medium',
100
+ lineHeight: 1.12,
101
+ spacing: '0.8em',
102
+ },
103
+ h3: {
104
+ align: { desktop: 'left', mobile: 'left' },
105
+ size: 'small',
106
+ lineHeight: 1.35,
107
+ spacing: '1rem',
108
+ },
109
+ h4: {
110
+ align: { desktop: 'left', mobile: 'left' },
111
+ size: 'medium',
112
+ lineHeight: 1.55,
113
+ spacing: '0.85rem',
114
+ },
55
115
  },
56
116
  body: {
57
117
  align: { desktop: 'left', mobile: 'left' },
58
- size: 'large',
118
+ size: 'medium',
59
119
  lineHeight: 1.62,
60
120
  paragraphSpacing: '1em',
61
121
  },
@@ -67,15 +127,35 @@ export const typographyPresets = {
67
127
  },
68
128
  },
69
129
  statement: {
70
- heading: {
71
- align: { desktop: 'left', mobile: 'left' },
72
- size: 'large',
73
- lineHeight: 1.04,
74
- spacing: '0.5em',
130
+ headings: {
131
+ h1: {
132
+ align: { desktop: 'left', mobile: 'left' },
133
+ size: 'xlarge',
134
+ lineHeight: 1.02,
135
+ spacing: '0.45em',
136
+ },
137
+ h2: {
138
+ align: { desktop: 'left', mobile: 'left' },
139
+ size: 'large',
140
+ lineHeight: 1.04,
141
+ spacing: '0.5em',
142
+ },
143
+ h3: {
144
+ align: { desktop: 'left', mobile: 'left' },
145
+ size: 'medium',
146
+ lineHeight: 1.18,
147
+ spacing: '0.85rem',
148
+ },
149
+ h4: {
150
+ align: { desktop: 'left', mobile: 'left' },
151
+ size: 'medium',
152
+ lineHeight: 1.42,
153
+ spacing: '0.75rem',
154
+ },
75
155
  },
76
156
  body: {
77
157
  align: { desktop: 'left', mobile: 'left' },
78
- size: 'large',
158
+ size: 'medium',
79
159
  lineHeight: 1.42,
80
160
  paragraphSpacing: '0.75em',
81
161
  },
@@ -1,4 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { findMap } from './lib/frontmatter-yaml.mjs';
2
3
  import {
3
4
  defaultTypography,
4
5
  resolveTypographyOverride,
@@ -7,6 +8,8 @@ import {
7
8
  typographyPresets,
8
9
  } from './lib/typography.mjs';
9
10
  import {
11
+ getContentFiles,
12
+ readSiteFile,
10
13
  splitSiteFile,
11
14
  validateContentFrontmatterStructure,
12
15
  validateFrontmatterIndentation,
@@ -21,68 +24,189 @@ import {
21
24
 
22
25
  const mode = process.argv[2] ?? 'show';
23
26
 
27
+ const typographyValuePaths = [
28
+ ['headings', 'h1', 'align', 'desktop'],
29
+ ['headings', 'h1', 'align', 'mobile'],
30
+ ['headings', 'h1', 'size'],
31
+ ['headings', 'h1', 'lineHeight'],
32
+ ['headings', 'h1', 'spacing'],
33
+ ['headings', 'h2', 'align', 'desktop'],
34
+ ['headings', 'h2', 'align', 'mobile'],
35
+ ['headings', 'h2', 'size'],
36
+ ['headings', 'h2', 'lineHeight'],
37
+ ['headings', 'h2', 'spacing'],
38
+ ['headings', 'h3', 'align', 'desktop'],
39
+ ['headings', 'h3', 'align', 'mobile'],
40
+ ['headings', 'h3', 'size'],
41
+ ['headings', 'h3', 'lineHeight'],
42
+ ['headings', 'h3', 'spacing'],
43
+ ['headings', 'h4', 'align', 'desktop'],
44
+ ['headings', 'h4', 'align', 'mobile'],
45
+ ['headings', 'h4', 'size'],
46
+ ['headings', 'h4', 'lineHeight'],
47
+ ['headings', 'h4', 'spacing'],
48
+ ['body', 'align', 'desktop'],
49
+ ['body', 'align', 'mobile'],
50
+ ['body', 'size'],
51
+ ['body', 'lineHeight'],
52
+ ['body', 'paragraphSpacing'],
53
+ ['caption', 'align', 'desktop'],
54
+ ['caption', 'align', 'mobile'],
55
+ ['caption', 'size'],
56
+ ['caption', 'lineHeight'],
57
+ ['caption', 'spacing'],
58
+ ];
24
59
  const countIndent = (line) => line.match(/^\s*/)?.[0].length ?? 0;
25
60
 
26
- const parseScalar = (rawValue) => {
27
- const value = rawValue.trim();
61
+ const isPlainObject = (value) => (
62
+ value !== null &&
63
+ typeof value === 'object' &&
64
+ !Array.isArray(value)
65
+ );
28
66
 
29
- if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
30
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
31
- return value.slice(1, -1);
67
+ const hasPath = (value, path) => {
68
+ let current = value;
69
+
70
+ for (const segment of path) {
71
+ if (!isPlainObject(current) || !(segment in current)) {
72
+ return false;
73
+ }
74
+
75
+ current = current[segment];
32
76
  }
33
77
 
34
- return value;
78
+ return true;
35
79
  };
36
80
 
37
- const parseMapping = (lines, startIndex, baseIndent) => {
38
- const value = {};
39
- let index = startIndex;
81
+ const getPath = (value, path) => path.reduce((current, segment) => current?.[segment], value);
40
82
 
41
- while (index < lines.length) {
42
- const line = lines[index];
43
- if (!line.trim() || line.trim().startsWith('#')) {
44
- index += 1;
45
- continue;
46
- }
83
+ const setPath = (value, path, entry) => {
84
+ let current = value;
47
85
 
48
- const indent = countIndent(line);
49
- if (indent <= baseIndent) break;
50
- if (line.trim().startsWith('- ')) break;
51
-
52
- const match = line.trim().match(/^([a-zA-Z][a-zA-Z0-9-]*):(?:\s+(.*))?$/);
53
- if (!match) break;
54
-
55
- const [, key, rawValue] = match;
56
- if (rawValue === undefined) {
57
- const parsed = parseMapping(lines, index + 1, indent);
58
- value[key] = parsed.value;
59
- index = parsed.nextIndex;
60
- } else {
61
- value[key] = parseScalar(rawValue);
62
- index += 1;
86
+ for (const segment of path.slice(0, -1)) {
87
+ current[segment] ??= {};
88
+ current = current[segment];
89
+ }
90
+
91
+ current[path.at(-1)] = entry;
92
+ };
93
+
94
+ const annotateResolvedValues = (resolved, sources) => {
95
+ const annotated = {};
96
+
97
+ for (const path of typographyValuePaths) {
98
+ const source = getPath(sources, path);
99
+ setPath(annotated, path, {
100
+ value: getPath(resolved.values, path),
101
+ source: source.source,
102
+ ...(source.inherited ? { inherited: true } : {}),
103
+ });
104
+ }
105
+
106
+ return annotated;
107
+ };
108
+
109
+ const presetSources = (presetName) => {
110
+ const sources = {};
111
+
112
+ for (const path of typographyValuePaths) {
113
+ setPath(sources, path, {
114
+ source: `preset:${presetName}`,
115
+ inherited: false,
116
+ });
117
+ }
118
+
119
+ return sources;
120
+ };
121
+
122
+ const applyOverrideSources = (sources, typographyConfig, sourceLabel) => {
123
+ const overrides = typographyConfig?.overrides;
124
+ if (!overrides) return sources;
125
+
126
+ for (const path of typographyValuePaths) {
127
+ if (hasPath(overrides, path)) {
128
+ setPath(sources, path, {
129
+ source: `${sourceLabel} override`,
130
+ inherited: false,
131
+ });
63
132
  }
64
133
  }
65
134
 
66
- return { value, nextIndex: index };
135
+ return sources;
67
136
  };
68
137
 
69
- const findMap = (lines, label, parentStart = 0, parentEnd = lines.length, requiredIndent = null) => {
70
- for (let index = parentStart; index < parentEnd; index += 1) {
71
- const line = lines[index];
72
- const match = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9-]*):\s*$/);
73
- if (!match || match[2] !== label) continue;
74
- if (requiredIndent !== null && match[1].length !== requiredIndent) continue;
138
+ const inheritedSources = (sources) => {
139
+ const inherited = structuredClone(sources);
140
+
141
+ for (const path of typographyValuePaths) {
142
+ setPath(inherited, path, {
143
+ ...getPath(sources, path),
144
+ inherited: true,
145
+ });
146
+ }
147
+
148
+ return inherited;
149
+ };
150
+
151
+ const resolveAnnotatedTypographyConfig = (typographyConfig, sourceLabel) => {
152
+ const resolved = resolveTypographyConfig(typographyConfig ?? defaultTypography);
153
+ const sources = applyOverrideSources(
154
+ presetSources(resolved.preset),
155
+ typographyConfig,
156
+ sourceLabel,
157
+ );
158
+
159
+ return {
160
+ preset: {
161
+ value: resolved.preset,
162
+ source: typographyConfig?.preset ? sourceLabel : 'engine default',
163
+ ...(typographyConfig?.preset ? {} : { inherited: true }),
164
+ },
165
+ resolved,
166
+ sources,
167
+ };
168
+ };
169
+
170
+ const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sourceLabel) => {
171
+ if (typographyConfig?.preset) {
172
+ return resolveAnnotatedTypographyConfig(typographyConfig, sourceLabel);
173
+ }
174
+
175
+ if (typographyConfig?.overrides) {
176
+ const resolved = resolveTypographyOverride(baseAnnotated.resolved, typographyConfig);
177
+ const sources = applyOverrideSources(
178
+ inheritedSources(baseAnnotated.sources),
179
+ typographyConfig,
180
+ sourceLabel,
181
+ );
75
182
 
76
183
  return {
77
- index,
78
- indent: match[1].length,
79
- ...parseMapping(lines, index + 1, match[1].length),
184
+ preset: {
185
+ value: resolved.preset,
186
+ source: baseAnnotated.preset.source,
187
+ inherited: true,
188
+ },
189
+ resolved,
190
+ sources,
80
191
  };
81
192
  }
82
193
 
83
- return null;
194
+ return {
195
+ preset: {
196
+ value: baseAnnotated.resolved.preset,
197
+ source: baseAnnotated.preset.source,
198
+ inherited: true,
199
+ },
200
+ resolved: baseAnnotated.resolved,
201
+ sources: inheritedSources(baseAnnotated.sources),
202
+ };
84
203
  };
85
204
 
205
+ const formatAnnotatedTypography = (annotated) => ({
206
+ preset: annotated.preset,
207
+ resolved: annotateResolvedValues(annotated.resolved, annotated.sources),
208
+ });
209
+
86
210
  const getSectionBlocks = (lines) => {
87
211
  const sectionsMap = findMap(lines, 'sections');
88
212
  if (!sectionsMap) return [];
@@ -118,8 +242,7 @@ const getSectionBlocks = (lines) => {
118
242
  return sections;
119
243
  };
120
244
 
121
- const readSiteTypography = async () => {
122
- const { frontmatter, frontmatterBody } = splitSiteFile(await readFile(siteContentPath, 'utf8'));
245
+ const readThemeTypography = async () => {
123
246
  const themeFile = await readFile(siteThemePath, 'utf8').catch((error) => {
124
247
  if (error?.code === 'ENOENT') {
125
248
  return '---\n---\n';
@@ -129,29 +252,38 @@ const readSiteTypography = async () => {
129
252
  });
130
253
  const { frontmatter: themeFrontmatter, frontmatterBody: themeFrontmatterBody } = splitSiteFile(themeFile, siteThemeLabel);
131
254
  const indentationIssues = [];
132
- validateFrontmatterIndentation(frontmatter, (issue) => indentationIssues.push(issue));
133
- validateContentFrontmatterStructure(frontmatter, (issue) => indentationIssues.push(issue));
134
255
  validateFrontmatterIndentation(themeFrontmatter, (issue) => indentationIssues.push(issue));
135
256
  validateThemeFrontmatterStructure(themeFrontmatter, (issue) => indentationIssues.push(issue));
136
257
  if (indentationIssues.length > 0) {
137
258
  throw new Error([
138
- `Cannot inspect typography because ${siteContentLabel} or ${siteThemeLabel} has invalid frontmatter.`,
259
+ `Cannot inspect typography because ${siteThemeLabel} has invalid frontmatter.`,
139
260
  ...indentationIssues.map((issue) => `- ${issue.message}`),
140
261
  ].join('\n'));
141
262
  }
142
263
 
143
- const lines = frontmatterBody.split(/\r?\n/);
144
264
  const themeLines = themeFrontmatterBody.split(/\r?\n/);
145
- const themePresentation = findMap(themeLines, 'presentation', 0, themeLines.length, 0);
146
- const themeTypographyConfig = themePresentation
147
- ? findMap(themeLines, 'typography', themePresentation.index + 1, themePresentation.nextIndex)?.value
148
- : null;
265
+ const themeTypographyConfig = findMap(themeLines, 'typography', 0, themeLines.length, 0)?.value;
266
+ return resolveAnnotatedTypographyConfig(themeTypographyConfig ?? defaultTypography, siteThemeLabel);
267
+ };
268
+
269
+ const readPageTypography = async (contentFile, themeTypography) => {
270
+ const { frontmatter, frontmatterBody } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
271
+ const indentationIssues = [];
272
+ validateFrontmatterIndentation(frontmatter, (issue) => indentationIssues.push(issue));
273
+ validateContentFrontmatterStructure(frontmatter, (issue) => indentationIssues.push(issue));
274
+ if (indentationIssues.length > 0) {
275
+ throw new Error([
276
+ `Cannot inspect typography because ${contentFile.contentLabel} has invalid frontmatter.`,
277
+ ...indentationIssues.map((issue) => `- ${issue.message}`),
278
+ ].join('\n'));
279
+ }
280
+
281
+ const lines = frontmatterBody.split(/\r?\n/);
149
282
  const pagePresentation = findMap(lines, 'presentation', 0, lines.length, 0);
150
283
  const pageTypographyConfig = pagePresentation
151
284
  ? findMap(lines, 'typography', pagePresentation.index + 1, pagePresentation.nextIndex)?.value
152
285
  : null;
153
- const themeTypography = resolveTypographyConfig(themeTypographyConfig ?? defaultTypography);
154
- const pageTypography = resolveTypographyOverride(themeTypography, pageTypographyConfig ?? undefined);
286
+ const pageTypography = resolveAnnotatedTypographyOverride(themeTypography, pageTypographyConfig ?? undefined, contentFile.contentLabel);
155
287
  const sections = getSectionBlocks(lines).map((section) => {
156
288
  const presentation = findMap(lines, 'presentation', section.start + 1, section.end);
157
289
  const typography = presentation
@@ -160,13 +292,13 @@ const readSiteTypography = async () => {
160
292
 
161
293
  return {
162
294
  id: section.id,
163
- typography,
164
- resolved: resolveTypographyOverride(pageTypography, typography ?? undefined),
295
+ typography: resolveAnnotatedTypographyOverride(pageTypography, typography ?? undefined, `${contentFile.contentLabel} sections.${section.id}`),
165
296
  };
166
297
  });
167
298
 
168
299
  return {
169
- themeTypography,
300
+ source: contentFile.contentLabel,
301
+ route: contentFile.isHome ? '/' : `/${contentFile.routeFolder}/`,
170
302
  pageTypography,
171
303
  sections,
172
304
  };
@@ -175,36 +307,31 @@ const readSiteTypography = async () => {
175
307
  if (mode === 'presets') {
176
308
  console.log(toYamlLines(typographyPresets).join('\n'));
177
309
  } else if (mode === 'show') {
178
- const siteTypography = await readSiteTypography();
310
+ const themeTypography = await readThemeTypography();
311
+ const pages = await Promise.all((await getContentFiles()).map((contentFile) => readPageTypography(contentFile, themeTypography)));
179
312
  const output = {
180
- source: siteContentLabel,
181
313
  theme: {
182
314
  source: siteThemeLabel,
183
315
  presentation: {
184
- typography: {
185
- preset: siteTypography.themeTypography.preset,
186
- resolved: siteTypography.themeTypography.values,
187
- },
188
- },
189
- },
190
- page: {
191
- typography: {
192
- preset: siteTypography.pageTypography.preset,
193
- resolved: siteTypography.pageTypography.values,
316
+ typography: formatAnnotatedTypography(themeTypography),
194
317
  },
195
318
  },
196
- sections: Object.fromEntries(siteTypography.sections.map((section) => [
197
- section.id,
319
+ pages: Object.fromEntries(pages.map((page) => [
320
+ page.route,
198
321
  {
199
- typography: {
200
- preset: section.resolved.preset,
201
- resolved: section.resolved.values,
202
- },
322
+ source: page.source,
323
+ typography: formatAnnotatedTypography(page.pageTypography),
324
+ sections: Object.fromEntries(page.sections.map((section) => [
325
+ section.id,
326
+ {
327
+ typography: formatAnnotatedTypography(section.typography),
328
+ },
329
+ ])),
203
330
  },
204
331
  ])),
205
332
  };
206
333
 
207
334
  console.log(toYamlLines(output).join('\n'));
208
335
  } else {
209
- throw new Error('Usage: norna typography:presets|typography:show');
336
+ throw new Error('Usage: norna typography presets|show');
210
337
  }
@@ -59,7 +59,7 @@ const promptForWrite = async () => {
59
59
  if (!process.stdin.isTTY) return false;
60
60
 
61
61
  const rl = createInterface({ input, output });
62
- const answer = await rl.question(`This will rewrite Markdown sections in ${siteContentLabel} and move gallery image files if needed. Continue? [y/N] `);
62
+ const answer = await rl.question(`This will rewrite Markdown sections in ${siteContentLabel} and move image files if needed. Continue? [y/N] `);
63
63
  rl.close();
64
64
 
65
65
  return answer.trim().toLowerCase() === 'y';
@@ -269,7 +269,7 @@ const validateRouteContentFiles = async () => {
269
269
  if (referencedImages.has(imageName)) {
270
270
  const previousSectionId = referencedImages.get(imageName);
271
271
  const message = `Image "${imageName}" is referenced more than once, in "${previousSectionId}" and "${contentFile.contentLabel}:${section.id}".`;
272
- const fix = 'Each image filename can be referenced by only one gallery row.';
272
+ const fix = 'Each image filename can be referenced by only one image row.';
273
273
 
274
274
  addIssue({ severity: 'error', sectionId: previousSectionId, message, fix });
275
275
  addIssue({
@@ -291,7 +291,7 @@ const validateRouteContentFiles = async () => {
291
291
  sectionId: `${contentFile.contentLabel}:${section.id}`,
292
292
  sectionLabel: `${contentFile.contentLabel} [${section.id}]`,
293
293
  message: `Image "${imageName}" does not exist anywhere under ${contentFile.imagesLabel}/.`,
294
- fix: `Add the source image to ${contentFile.imagesLabel}/${section.id}/ or remove the gallery row.`,
294
+ fix: `Add the source image to ${contentFile.imagesLabel}/${section.id}/ or remove the image row.`,
295
295
  });
296
296
  continue;
297
297
  }
@@ -380,7 +380,7 @@ const getReportLines = (title) => {
380
380
  leftKey.localeCompare(rightKey, 'sv')
381
381
  ));
382
382
 
383
- lines.push('', 'Section and Gallery Issues');
383
+ lines.push('', 'Section And Image Issues');
384
384
 
385
385
  for (const [, group] of orderedSectionGroups) {
386
386
  lines.push('', `[${group.label}]`);
@@ -506,7 +506,7 @@ for (const section of frontmatterSections) {
506
506
  const message = previousSectionId === section.id
507
507
  ? `Image "${imageName}" is referenced more than once in this section.`
508
508
  : `Image "${imageName}" is referenced more than once, in sections "${previousSectionId}" and "${section.id}".`;
509
- const fix = 'Each image filename can be referenced by only one gallery row.';
509
+ const fix = 'Each image filename can be referenced by only one image row.';
510
510
 
511
511
  addSectionIssue(previousSectionId, { severity: 'error', message, fix });
512
512
  if (previousSectionId !== section.id) {
@@ -522,7 +522,7 @@ for (const section of frontmatterSections) {
522
522
  addSectionIssue(section.id, {
523
523
  severity: 'error',
524
524
  message: `Image "${imageName}" does not exist anywhere under ${siteImagesLabel}/.`,
525
- fix: `Add the source image to ${siteImagesLabel}/${section.id}/ or remove the gallery row.`,
525
+ fix: `Add the source image to ${siteImagesLabel}/${section.id}/ or remove the image row.`,
526
526
  });
527
527
  continue;
528
528
  }