@janga/norna 0.7.16 → 0.7.17

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 (200) hide show
  1. package/README.md +6 -6
  2. package/astro.config.mjs +35 -3
  3. package/docs/README.md +10 -10
  4. package/docs/commands.md +2 -2
  5. package/docs/content.md +82 -102
  6. package/docs/design/navigation-and-theme-plan.md +115 -0
  7. package/docs/design/norna-diagram-design.md +9 -9
  8. package/docs/design/site-examples-structure.md +9 -9
  9. package/docs/engine-development.md +9 -9
  10. package/docs/getting-started.md +20 -15
  11. package/docs/images-and-metadata.md +10 -9
  12. package/docs/pages.md +134 -0
  13. package/docs/public-files.md +49 -36
  14. package/docs/publishing.md +19 -3
  15. package/docs/site-files.md +29 -28
  16. package/docs/sitewide-content.md +20 -36
  17. package/docs/theme.md +20 -20
  18. package/docs/typography.md +10 -10
  19. package/examples/README.md +2 -2
  20. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/site.schema.json +17 -41
  21. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/sitewide.schema.json +26 -67
  22. package/examples/complete-sites/dog-shelter-multi-page/.astro/collections/theme.schema.json +185 -88
  23. package/examples/complete-sites/dog-shelter-multi-page/.astro/data-store.json +1 -1
  24. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev-local.json +4 -4
  25. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.json +5 -5
  26. package/examples/complete-sites/dog-shelter-multi-page/.astro/dev.log +33 -23
  27. package/examples/complete-sites/dog-shelter-multi-page/README.md +1 -1
  28. package/examples/complete-sites/dog-shelter-multi-page/node_modules/.vite/deps/_metadata.json +8 -8
  29. package/examples/complete-sites/dog-shelter-multi-page/site/.norna/generated-images.json +20 -20
  30. package/examples/complete-sites/dog-shelter-multi-page/site/content.md +3 -4
  31. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/010-dogs/content.md +3 -4
  32. package/examples/complete-sites/dog-shelter-multi-page/site/{routes → pages}/020-adopt/content.md +3 -4
  33. package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +2 -4
  34. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/site.schema.json +17 -41
  35. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/sitewide.schema.json +26 -67
  36. package/examples/complete-sites/dog-shelter-single-page/.astro/collections/theme.schema.json +185 -88
  37. package/examples/complete-sites/dog-shelter-single-page/.astro/data-store.json +1 -1
  38. package/examples/complete-sites/dog-shelter-single-page/.astro/dev-local.json +8 -0
  39. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.json +5 -5
  40. package/examples/complete-sites/dog-shelter-single-page/.astro/dev.log +16 -12
  41. package/examples/complete-sites/dog-shelter-single-page/README.md +1 -1
  42. package/examples/complete-sites/dog-shelter-single-page/node_modules/.vite/deps/_metadata.json +8 -8
  43. package/examples/complete-sites/dog-shelter-single-page/site/.norna/generated-images.json +12 -12
  44. package/examples/complete-sites/dog-shelter-single-page/site/content.md +5 -2
  45. package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +2 -4
  46. package/examples/feature-demos/media-and-surfaces/.astro/collections/site.schema.json +17 -41
  47. package/examples/feature-demos/media-and-surfaces/.astro/collections/sitewide.schema.json +26 -67
  48. package/examples/feature-demos/media-and-surfaces/.astro/collections/theme.schema.json +185 -88
  49. package/examples/feature-demos/media-and-surfaces/.astro/data-store.json +1 -1
  50. package/examples/feature-demos/media-and-surfaces/.astro/dev.log +4 -14
  51. package/examples/feature-demos/media-and-surfaces/README.md +2 -2
  52. package/examples/feature-demos/media-and-surfaces/node_modules/.vite/deps/_metadata.json +9 -9
  53. package/examples/feature-demos/media-and-surfaces/site/.norna/generated-images.json +16 -16
  54. package/examples/feature-demos/media-and-surfaces/site/content.md +4 -7
  55. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/010-media/content.md +6 -5
  56. package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/content.md +7 -6
  57. package/examples/feature-demos/media-and-surfaces/site/sitewide-content.yaml +0 -2
  58. package/examples/feature-demos/sitewide-content/README.md +3 -3
  59. package/examples/feature-demos/sitewide-content/site/content.md +8 -7
  60. package/examples/feature-demos/sitewide-content/site/pages/010-logo-and-navigation/content.md +17 -0
  61. package/examples/feature-demos/sitewide-content/site/{routes → pages}/020-notices/content.md +7 -6
  62. package/examples/feature-demos/sitewide-content/site/{routes → pages}/030-footer/content.md +6 -5
  63. package/examples/feature-demos/sitewide-content/site/sitewide-content.yaml +4 -13
  64. package/examples/feature-demos/theme-presets/README.md +1 -1
  65. package/examples/feature-demos/theme-presets/site/.norna/generated-images.json +60 -60
  66. package/examples/feature-demos/theme-presets/site/content.md +4 -7
  67. package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/content.md +4 -7
  68. package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/content.md +4 -7
  69. package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/content.md +4 -7
  70. package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/content.md +4 -7
  71. package/examples/feature-demos/theme-presets/site/sitewide-content.yaml +1 -10
  72. package/fixtures/basic/site/content.md +5 -6
  73. package/fixtures/basic/site/sitewide-content.yaml +1 -8
  74. package/fixtures/content-model-v2/site/content.md +11 -13
  75. package/fixtures/content-model-v2/site/pages/010-guide/content.md +18 -0
  76. package/fixtures/nested-pages/site/.norna/generated-images.json +10 -0
  77. package/fixtures/nested-pages/site/config.yaml +1 -0
  78. package/fixtures/nested-pages/site/content.md +15 -0
  79. package/fixtures/nested-pages/site/pages/010-guides/content.md +15 -0
  80. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/content.md +21 -0
  81. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/images/diagram.svg +21 -0
  82. package/fixtures/nested-pages/site/pages/010-guides/pages/010-installation/pages/010-macos/content.md +14 -0
  83. package/fixtures/nested-pages/site/pages/010-guides/pages/020-workflows/content.md +11 -0
  84. package/fixtures/nested-pages/site/pages/010-guides/theme.yaml +3 -0
  85. package/fixtures/nested-pages/site/pages/020-reference/content.md +10 -0
  86. package/fixtures/nested-pages/site/pages/020-reference/pages/010-installation/content.md +11 -0
  87. package/fixtures/nested-pages/site/theme.yaml +3 -0
  88. package/package.json +7 -4
  89. package/schemas/config.schema.json +5 -5
  90. package/schemas/content-frontmatter.schema.json +28 -63
  91. package/schemas/manifest.json +1 -0
  92. package/schemas/sitewide-content.schema.json +58 -132
  93. package/schemas/theme.schema.json +396 -86
  94. package/scripts/check-config.mjs +6 -5
  95. package/scripts/deploy-site.mjs +6 -8
  96. package/scripts/doctor.mjs +2 -2
  97. package/scripts/generate-images.mjs +11 -18
  98. package/scripts/generate-schemas.mjs +3 -217
  99. package/scripts/lib/base-path-redirect.mjs +9 -0
  100. package/scripts/lib/editor-language-service.mjs +126 -66
  101. package/scripts/lib/heading-ids.mjs +161 -0
  102. package/scripts/lib/navigation-model.mjs +51 -0
  103. package/scripts/lib/norna-markdown-blocks.mjs +3 -4
  104. package/scripts/lib/page-model.mjs +83 -0
  105. package/scripts/lib/project-config.mjs +28 -0
  106. package/scripts/lib/schema-definitions.mjs +41 -46
  107. package/scripts/lib/schema-editor-metadata.mjs +475 -0
  108. package/scripts/lib/schema-value-definitions.mjs +6 -6
  109. package/scripts/lib/site-content.mjs +124 -87
  110. package/scripts/lib/site-paths.mjs +2 -2
  111. package/scripts/lib/theme-config.mjs +9 -16
  112. package/scripts/lib/theme-presets.mjs +9 -1
  113. package/scripts/release.mjs +1 -1
  114. package/scripts/show-typography.mjs +26 -20
  115. package/scripts/sync-content-sections.mjs +106 -65
  116. package/scripts/{test-temporary-visibility.mjs → test-banner-visibility.mjs} +7 -24
  117. package/scripts/test-client-javascript.mjs +39 -42
  118. package/scripts/test-content-check.mjs +138 -54
  119. package/scripts/test-content-model-v2.mjs +445 -228
  120. package/scripts/test-documentation.mjs +6 -1
  121. package/scripts/test-editor-language-service.mjs +168 -24
  122. package/scripts/test-engine-commands.mjs +2 -1
  123. package/scripts/test-heading-ids.mjs +68 -0
  124. package/scripts/test-navigation-model.mjs +63 -0
  125. package/scripts/test-nested-pages.mjs +110 -0
  126. package/scripts/test-package-check.mjs +31 -40
  127. package/scripts/test-page-model.mjs +80 -0
  128. package/scripts/test-project-config.mjs +25 -1
  129. package/scripts/test-schemas.mjs +66 -14
  130. package/scripts/test-theme-presets.mjs +33 -24
  131. package/src/components/NavigationPageTree.astro +68 -0
  132. package/src/components/SectionNavigationScript.astro +110 -5
  133. package/src/components/SiteBreadcrumbs.astro +29 -0
  134. package/src/components/SiteNavigation.astro +65 -69
  135. package/src/components/SitePage.astro +70 -46
  136. package/src/components/SiteSection.astro +27 -20
  137. package/src/content.config.ts +9 -5
  138. package/src/layouts/BaseLayout.astro +11 -20
  139. package/src/lib/pageThemes.ts +42 -0
  140. package/src/lib/sectionContent.ts +109 -106
  141. package/src/lib/siteNavigation.ts +48 -0
  142. package/src/lib/sitePages.ts +85 -38
  143. package/src/pages/{[slug].astro → [...slug].astro} +2 -2
  144. package/src/styles/global.css +373 -55
  145. package/starters/basic/README.md +5 -7
  146. package/starters/basic/site/content.md +7 -4
  147. package/starters/basic/site/sitewide-content.yaml +1 -10
  148. package/starters/project/README.md +8 -8
  149. package/starters/project/site/content.md +5 -4
  150. package/starters/project/site/{routes → pages}/010-guide/content.md +5 -4
  151. package/starters/project/site/sitewide-content.yaml +1 -10
  152. package/docs/routes.md +0 -121
  153. package/examples/feature-demos/sitewide-content/site/routes/010-identity/content.md +0 -20
  154. package/fixtures/content-model-v2/site/routes/010-guide/content.md +0 -17
  155. package/scripts/lib/route-model.mjs +0 -17
  156. package/scripts/test-route-model.mjs +0 -32
  157. package/src/lib/routeThemes.ts +0 -41
  158. /package/examples/complete-sites/dog-shelter-multi-page/site/images/{intro/shelter-dog.jpg → shelter-dog.jpg} +0 -0
  159. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rex.jpg +0 -0
  160. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/010-dogs/images/dogs → pages/010-dogs/images}/rover.jpg +0 -0
  161. /package/examples/complete-sites/dog-shelter-multi-page/site/{routes/020-adopt/images/adopt → pages/020-adopt/images}/adoption-dog.jpg +0 -0
  162. /package/examples/complete-sites/dog-shelter-single-page/site/images/{you-can-help/foster-dog.jpg → foster-dog.jpg} +0 -0
  163. /package/examples/complete-sites/dog-shelter-single-page/site/images/{what-we-do/rescue-dog.jpg → rescue-dog.jpg} +0 -0
  164. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-carousel.svg +0 -0
  165. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-stack.svg +0 -0
  166. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/cards → pages/010-media/images}/card-surfaces.svg +0 -0
  167. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-one.svg +0 -0
  168. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-three.svg +0 -0
  169. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/carousel → pages/010-media/images}/carousel-two.svg +0 -0
  170. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-one.svg +0 -0
  171. /package/examples/feature-demos/media-and-surfaces/site/{routes/010-media/images/stack → pages/010-media/images}/stack-two.svg +0 -0
  172. /package/examples/feature-demos/media-and-surfaces/site/{routes → pages}/020-surfaces/theme.yaml +0 -0
  173. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/puppy-portrait.jpg +0 -0
  174. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/shepherd-portrait.jpg +0 -0
  175. /package/examples/feature-demos/theme-presets/site/{routes/010-portfolio/images/portfolio → pages/010-portfolio/images}/white-puppy.jpg +0 -0
  176. /package/examples/feature-demos/theme-presets/site/{routes → pages}/010-portfolio/theme.yaml +0 -0
  177. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/black-schnauzer.jpg +0 -0
  178. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/retriever-portrait.jpg +0 -0
  179. /package/examples/feature-demos/theme-presets/site/{routes/020-documentation/images/documentation → pages/020-documentation/images}/schnauzer-portrait.jpg +0 -0
  180. /package/examples/feature-demos/theme-presets/site/{routes → pages}/020-documentation/theme.yaml +0 -0
  181. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/boxer-portrait.jpg +0 -0
  182. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grass-puppy.jpg +0 -0
  183. /package/examples/feature-demos/theme-presets/site/{routes/030-project/images/project → pages/030-project/images}/grey-street-dog.jpg +0 -0
  184. /package/examples/feature-demos/theme-presets/site/{routes → pages}/030-project/theme.yaml +0 -0
  185. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/black-pedigree.jpg +0 -0
  186. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-grass.jpg +0 -0
  187. /package/examples/feature-demos/theme-presets/site/{routes/040-statement/images/statement → pages/040-statement/images}/sheepdog-wide.jpg +0 -0
  188. /package/examples/feature-demos/theme-presets/site/{routes → pages}/040-statement/theme.yaml +0 -0
  189. /package/fixtures/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
  190. /package/fixtures/content-model-v2/site/images/{card-section/adopt.svg → adopt.svg} +0 -0
  191. /package/fixtures/content-model-v2/site/images/{intro/detail.jpg → detail.jpg} +0 -0
  192. /package/fixtures/content-model-v2/site/images/{intro/duplicate.jpg → duplicate.jpg} +0 -0
  193. /package/fixtures/content-model-v2/site/images/{card-section/foster.svg → foster.svg} +0 -0
  194. /package/fixtures/content-model-v2/site/images/{intro/hero.jpg → hero.jpg} +0 -0
  195. /package/fixtures/content-model-v2/site/images/{plain/image.jpg → image.jpg} +0 -0
  196. /package/fixtures/content-model-v2/site/images/{timed/slide-one.jpg → slide-one.jpg} +0 -0
  197. /package/fixtures/content-model-v2/site/images/{timed/slide-three.jpg → slide-three.jpg} +0 -0
  198. /package/fixtures/content-model-v2/site/images/{timed/slide-two.jpg → slide-two.jpg} +0 -0
  199. /package/fixtures/content-model-v2/site/{routes/010-guide/images/intro → pages/010-guide/images}/duplicate.jpg +0 -0
  200. /package/starters/basic/site/images/{work/.gitkeep → .gitkeep} +0 -0
@@ -12,7 +12,9 @@ const obsoleteSourceReferences = [
12
12
  'site/config.md',
13
13
  'site/theme.md',
14
14
  'site/sitewide-content.md',
15
+ 'site/routes/',
15
16
  'route-content.md',
17
+ 'docs/routes.md',
16
18
  'docs/site-structure.md',
17
19
  ];
18
20
  const obsoleteSiteFilenames = new Set([
@@ -129,7 +131,10 @@ const checkObsoleteSiteFiles = async () => {
129
131
  ];
130
132
  const files = (await Promise.all(siteRoots.map(collectFiles))).flat();
131
133
  const obsolete = files
132
- .filter((filePath) => obsoleteSiteFilenames.has(path.basename(filePath)))
134
+ .filter((filePath) => (
135
+ obsoleteSiteFilenames.has(path.basename(filePath))
136
+ || filePath.split(path.sep).includes('routes')
137
+ ))
133
138
  .map((filePath) => path.relative(repoRoot, filePath));
134
139
 
135
140
  assert.deepEqual(obsolete, [], `Obsolete Norna site files:\n${obsolete.join('\n')}`);
@@ -1,7 +1,9 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
+ import projectContext from '../editors/vscode/norna-project.cjs';
6
+ import yamlSchemaCompletions from '../editors/vscode/yaml-schema-completions.cjs';
5
7
  import { documentationRef } from './lib/documentation-links.mjs';
6
8
  import {
7
9
  findNornaSiteRoot,
@@ -17,13 +19,27 @@ import {
17
19
  const root = await mkdtemp(path.join(os.tmpdir(), 'norna-editor-language-'));
18
20
  const siteRoot = path.join(root, 'site');
19
21
  const homeContentPath = path.join(siteRoot, 'content.md');
20
- const routeContentPath = path.join(siteRoot, 'routes', '010-about', 'content.md');
22
+ const pageContentPath = path.join(siteRoot, 'pages', '010-about', 'content.md');
23
+ const pageThemePath = path.join(siteRoot, 'pages', '010-about', 'theme.yaml');
24
+ const nestedPageContentPath = path.join(siteRoot, 'pages', '010-about', 'pages', '020-team', 'content.md');
25
+ const nestedPageThemePath = path.join(siteRoot, 'pages', '010-about', 'pages', '020-team', 'theme.yaml');
26
+ const installedNornaRoot = path.join(root, 'node_modules', '@janga', 'norna');
27
+ const packageManifestPath = path.join(installedNornaRoot, 'schemas', 'manifest.json');
28
+ const {
29
+ getNornaDocumentContext,
30
+ getNornaProjectContext,
31
+ supportedEditorApiVersion,
32
+ supportedSchemaVersion,
33
+ } = projectContext;
34
+ const { getYamlSchemaSnippetCompletions } = yamlSchemaCompletions;
21
35
 
22
36
  const homeSource = `---
23
- title: Editor fixture
24
- description: Exercises project-local image discovery.
37
+ page:
38
+ description: Exercises project-local image discovery.
25
39
  ---
26
40
 
41
+ # Editor fixture
42
+
27
43
  ## Intro {#intro}
28
44
 
29
45
  Text with a missing note {note-ref}.
@@ -33,28 +49,43 @@ Text with a missing note {note-ref}.
33
49
  - image: portrait.jpg
34
50
  \`\`\`
35
51
  `;
36
- const routeSource = `---
37
- title: About
38
- description: About this fixture.
52
+ const pageSource = `---
53
+ page:
54
+ description: About this fixture.
39
55
  ---
40
56
 
57
+ # About
58
+
41
59
  ## Team {#team}
42
60
 
43
- Route content.
61
+ Page content.
44
62
  `;
45
63
 
46
64
  try {
47
- await mkdir(path.join(siteRoot, 'images', 'intro'), { recursive: true });
48
- await mkdir(path.join(siteRoot, 'routes', '010-about', 'images', 'team'), { recursive: true });
65
+ await mkdir(path.join(installedNornaRoot, 'schemas'), { recursive: true });
66
+ await mkdir(path.join(siteRoot, 'images'), { recursive: true });
67
+ await mkdir(path.join(siteRoot, 'pages', '010-about', 'images'), { recursive: true });
68
+ await mkdir(path.dirname(nestedPageContentPath), { recursive: true });
49
69
  await mkdir(path.join(siteRoot, 'public'), { recursive: true });
70
+ await writeFile(path.join(root, 'package.json'), JSON.stringify({ name: 'editor-fixture', version: '1.0.0' }));
71
+ await writeFile(path.join(installedNornaRoot, 'package.json'), JSON.stringify({ name: '@janga/norna', version: '9.8.7' }));
72
+ await writeFile(packageManifestPath, JSON.stringify({
73
+ editorApiVersion: supportedEditorApiVersion,
74
+ files: {
75
+ config: 'config.schema.json',
76
+ contentFrontmatter: 'content-frontmatter.schema.json',
77
+ sitewideContent: 'sitewide-content.schema.json',
78
+ theme: 'theme.schema.json',
79
+ },
80
+ schemaVersion: supportedSchemaVersion,
81
+ }));
50
82
  await writeFile(path.join(siteRoot, 'config.yaml'), 'url: https://example.com/\n');
51
- await writeFile(path.join(siteRoot, 'sitewide-content.yaml'), `navigation:
52
- label: Editor fixture
53
- logo:
54
- height: 2rem
83
+ await writeFile(path.join(siteRoot, 'theme.yaml'), 'preset: project\n');
84
+ await writeFile(path.join(siteRoot, 'sitewide-content.yaml'), `logo:
85
+ height: 2rem
55
86
  `);
56
- await writeFile(path.join(siteRoot, 'images', 'intro', 'local.jpg'), 'local');
57
- await writeFile(path.join(siteRoot, 'routes', '010-about', 'images', 'team', 'portrait.jpg'), 'portrait');
87
+ await writeFile(path.join(siteRoot, 'images', 'local.jpg'), 'local');
88
+ await writeFile(path.join(siteRoot, 'pages', '010-about', 'images', 'portrait.jpg'), 'portrait');
58
89
  await writeFile(path.join(siteRoot, 'public', 'logo.svg'), '<svg/>');
59
90
  await writeFile(path.join(siteRoot, 'public', 'logo.png'), 'logo');
60
91
  await writeFile(path.join(siteRoot, 'public', 'favicon.svg'), '<svg/>');
@@ -62,9 +93,104 @@ try {
62
93
  await writeFile(path.join(siteRoot, 'public', 'favion.ico'), 'typo');
63
94
  await writeFile(path.join(siteRoot, 'public', 'favicon-32x32.png'), 'unrecognized');
64
95
  await writeFile(homeContentPath, homeSource);
65
- await writeFile(routeContentPath, routeSource);
96
+ await writeFile(pageContentPath, pageSource);
97
+ await writeFile(pageThemePath, 'preset: portfolio\n');
98
+ await writeFile(nestedPageContentPath, pageSource.replace('# About', '# Team'));
99
+ await writeFile(nestedPageThemePath, 'preset: documentation\n');
66
100
 
67
101
  assert.equal(await findNornaSiteRoot(homeContentPath), siteRoot);
102
+ assert.equal(getNornaDocumentContext(homeContentPath).schemaKind, 'contentFrontmatter');
103
+ assert.equal(getNornaDocumentContext(homeContentPath).nornaPackage.root, installedNornaRoot);
104
+ assert.equal(getNornaDocumentContext(path.join(siteRoot, 'config.yaml')).schemaKind, 'config');
105
+ assert.equal(getNornaDocumentContext(path.join(siteRoot, 'theme.yaml')).schemaKind, 'theme');
106
+ assert.equal(getNornaDocumentContext(path.join(siteRoot, 'sitewide-content.yaml')).schemaKind, 'sitewideContent');
107
+ assert.equal(getNornaDocumentContext(pageContentPath).pageDirectory, '010-about');
108
+ assert.equal(getNornaDocumentContext(pageThemePath).schemaKind, 'theme');
109
+ assert.equal(getNornaDocumentContext(nestedPageContentPath).pageDirectory, '010-about/pages/020-team');
110
+ assert.equal(getNornaDocumentContext(nestedPageContentPath).schemaKind, 'contentFrontmatter');
111
+ assert.equal(getNornaDocumentContext(nestedPageThemePath).schemaKind, 'theme');
112
+ assert.equal(getNornaProjectContext(path.join(siteRoot, 'public', 'logo.svg')).siteRoot, siteRoot);
113
+ assert.equal(getNornaDocumentContext(path.join(siteRoot, 'public', 'logo.svg')), null);
114
+ assert.equal(getNornaDocumentContext(path.join(root, 'README.md')), null);
115
+ assert.equal(getNornaDocumentContext(path.join(root, 'docs', 'content.md')), null);
116
+ await mkdir(path.join(siteRoot, 'pages', 'about'), { recursive: true });
117
+ await writeFile(path.join(siteRoot, 'pages', 'about', 'content.md'), pageSource);
118
+ assert.equal(getNornaDocumentContext(path.join(siteRoot, 'pages', 'about', 'content.md')), null);
119
+ await mkdir(path.join(siteRoot, 'pages', '010-about', 'nested'), { recursive: true });
120
+ await writeFile(path.join(siteRoot, 'pages', '010-about', 'nested', 'content.md'), pageSource);
121
+ assert.equal(getNornaDocumentContext(path.join(siteRoot, 'pages', '010-about', 'nested', 'content.md')), null);
122
+
123
+ const compatibleManifest = await readFile(packageManifestPath, 'utf8');
124
+ await writeFile(packageManifestPath, JSON.stringify({
125
+ ...JSON.parse(compatibleManifest),
126
+ editorApiVersion: supportedEditorApiVersion + 1,
127
+ }));
128
+ assert.equal(getNornaDocumentContext(homeContentPath).schemaCompatible, true);
129
+ assert.equal(getNornaDocumentContext(homeContentPath).editorCompatible, false);
130
+ await writeFile(packageManifestPath, JSON.stringify({
131
+ ...JSON.parse(compatibleManifest),
132
+ schemaVersion: supportedSchemaVersion + 1,
133
+ }));
134
+ assert.equal(getNornaDocumentContext(homeContentPath).schemaCompatible, false);
135
+ await writeFile(packageManifestPath, compatibleManifest);
136
+
137
+ const sitewideSchema = JSON.parse(await readFile(path.join(process.cwd(), 'schemas', 'sitewide-content.schema.json'), 'utf8'));
138
+ const bannerSource = 'banners:\n - ';
139
+ const bannerSnippets = getYamlSchemaSnippetCompletions({
140
+ lineText: ' - ',
141
+ offset: bannerSource.length,
142
+ schema: sitewideSchema,
143
+ source: bannerSource,
144
+ });
145
+ assert.equal(bannerSnippets.length, 1);
146
+ assert.equal(bannerSnippets[0].label, 'Norna: Warning banner');
147
+ assert.equal(bannerSnippets[0].text, ` - id: \${1:project-status}
148
+ tone: warning
149
+ title: \${2:Important notice}
150
+ text: \${3:Brief explanation.}`);
151
+ assert.deepEqual(getYamlSchemaSnippetCompletions({
152
+ lineText: ' - id: existing',
153
+ offset: bannerSource.length,
154
+ schema: sitewideSchema,
155
+ source: bannerSource,
156
+ }), []);
157
+ const unrelatedSequence = 'other:\n - ';
158
+ assert.deepEqual(getYamlSchemaSnippetCompletions({
159
+ lineText: ' - ',
160
+ offset: unrelatedSequence.length,
161
+ schema: sitewideSchema,
162
+ source: unrelatedSequence,
163
+ }), []);
164
+ const themeSchema = JSON.parse(await readFile(path.join(process.cwd(), 'schemas', 'theme.schema.json'), 'utf8'));
165
+ const gutterSource = 'layout:\n gutter: ';
166
+ const gutterSnippets = getYamlSchemaSnippetCompletions({
167
+ lineText: ' gutter: ',
168
+ offset: gutterSource.length,
169
+ schema: themeSchema,
170
+ source: gutterSource,
171
+ });
172
+ assert.equal(gutterSnippets[0].label, 'Norna: Responsive page gutter');
173
+ assert.equal(gutterSnippets[0].text, ` gutter:
174
+ desktop: \${1:clamp(1.25rem, 4vw, 3rem)}
175
+ mobile: \${2:1rem}`);
176
+ const typographySource = 'typography:\n overrides: ';
177
+ const typographySnippets = getYamlSchemaSnippetCompletions({
178
+ lineText: ' overrides: ',
179
+ offset: typographySource.length,
180
+ schema: themeSchema,
181
+ source: typographySource,
182
+ });
183
+ assert.equal(typographySnippets[0].label, 'Norna: Typography overrides');
184
+ assert.match(typographySnippets[0].text, /headings:\n h2:/);
185
+ const buildInfoSource = 'footer:\n buildInfo: ';
186
+ const buildInfoSnippets = getYamlSchemaSnippetCompletions({
187
+ lineText: ' buildInfo: ',
188
+ offset: buildInfoSource.length,
189
+ schema: sitewideSchema,
190
+ source: buildInfoSource,
191
+ });
192
+ assert.deepEqual(buildInfoSnippets, []);
193
+
68
194
  const publicAssetStatus = await getSitePublicAssetStatus(homeContentPath);
69
195
  assert.deepEqual(publicAssetStatus.logos, ['logo.png', 'logo.svg']);
70
196
  assert.deepEqual(publicAssetStatus.browserIcons, ['favicon.svg']);
@@ -75,12 +201,14 @@ try {
75
201
  await rm(path.join(siteRoot, 'public', 'logo.svg'), { force: true });
76
202
  await rm(path.join(siteRoot, 'public', 'logo.png'), { force: true });
77
203
  const missingLogoStatus = await getSitePublicAssetStatus(homeContentPath);
78
- assert.ok(missingLogoStatus.issues.some(({ code, line }) => code === 'missing-logo-file' && line === 3));
204
+ assert.ok(missingLogoStatus.issues.some(({ code, line }) => code === 'missing-logo-file' && line === 1));
79
205
  assert.equal(nornaBlockDefinitions['norna-image-stack'].description.includes('vertical stack'), true);
80
206
  assert.match(
81
207
  nornaBlockDefinitions['norna-image-stack'].documentation,
82
- new RegExp(`/blob/${documentationRef.replaceAll('.', '\\.')}\/docs/content\\.md#norna-blocks`),
208
+ new RegExp(`/blob/${documentationRef.replaceAll('.', '\\.')}\/docs/content\\.md#image-stack`),
83
209
  );
210
+ assert.match(nornaBlockDefinitions['norna-image-carousel'].documentation, /docs\/content\.md#image-carousel/);
211
+ assert.match(nornaBlockDefinitions['norna-card-list'].documentation, /docs\/content\.md#card-list/);
84
212
  assert.equal(nornaBlockDefinitions['norna-card-list'].options.layout.default, 'image-top');
85
213
 
86
214
  const stackFieldSource = homeSource.replace('- image: portrait.jpg', '- image: portrait.jpg\n ');
@@ -116,7 +244,7 @@ try {
116
244
  line: localDefinitionLine,
117
245
  source: homeSource,
118
246
  });
119
- assert.deepEqual(localDefinition.files, [path.join(siteRoot, 'images', 'intro', 'local.jpg')]);
247
+ assert.deepEqual(localDefinition.files, [path.join(siteRoot, 'images', 'local.jpg')]);
120
248
 
121
249
  const diagnostics = await getMarkdownDiagnostics({ documentPath: homeContentPath, source: homeSource });
122
250
  assert.ok(diagnostics.some(({ code, message }) => code === 'image-needs-sync' && message.includes('Run "norna content:sync"')));
@@ -125,7 +253,23 @@ try {
125
253
  documentPath: homeContentPath,
126
254
  source: homeSource.replace('## Intro {#intro}', '## Intro'),
127
255
  });
128
- assert.ok(missingIdDiagnostics.some(({ code }) => code === 'missing-section-id'));
256
+ assert.equal(missingIdDiagnostics.some(({ code }) => code === 'missing-section-id'), false);
257
+ const duplicateIdDiagnostics = await getMarkdownDiagnostics({
258
+ documentPath: homeContentPath,
259
+ source: homeSource.replace('## Intro {#intro}', '## Intro\n\n### Intro'),
260
+ });
261
+ assert.ok(duplicateIdDiagnostics.some(({ code }) => code === 'duplicate-heading-id'));
262
+ const missingTitleDiagnostics = await getMarkdownDiagnostics({
263
+ documentPath: homeContentPath,
264
+ source: homeSource.replace('# Editor fixture', '## Editor fixture {#editor-fixture}'),
265
+ });
266
+ assert.ok(missingTitleDiagnostics.some(({ code }) => code === 'missing-page-title'));
267
+ const codeHeadingDiagnostics = await getMarkdownDiagnostics({
268
+ documentPath: homeContentPath,
269
+ source: `${homeSource}\n\`\`\`md\n# Example title\n## Example section\n\`\`\`\n`,
270
+ });
271
+ assert.equal(codeHeadingDiagnostics.some(({ code }) => code === 'duplicate-page-title'), false);
272
+ assert.equal(codeHeadingDiagnostics.some(({ code }) => code === 'duplicate-heading-id'), false);
129
273
  const unclosedBlockDiagnostics = await getMarkdownDiagnostics({
130
274
  documentPath: homeContentPath,
131
275
  source: homeSource.replace(/\n```\n$/, '\n'),
@@ -137,14 +281,14 @@ try {
137
281
  });
138
282
  assert.ok(markdownImageDiagnostics.some(({ code }) => code === 'local-markdown-image'));
139
283
 
140
- await writeFile(routeContentPath, routeSource.replace('Route content.', `\`\`\`norna-image-stack
284
+ await writeFile(pageContentPath, pageSource.replace('Page content.', `\`\`\`norna-image-stack
141
285
  - image: portrait.jpg
142
286
  \`\`\``));
143
287
  const sharedDiagnostics = await getMarkdownDiagnostics({ documentPath: homeContentPath, source: homeSource });
144
288
  assert.ok(sharedDiagnostics.some(({ message }) => message.includes('is still referenced by')));
145
289
 
146
- await mkdir(path.join(siteRoot, 'routes', '020-contact', 'images', 'people'), { recursive: true });
147
- await writeFile(path.join(siteRoot, 'routes', '020-contact', 'images', 'people', 'portrait.jpg'), 'duplicate');
290
+ await mkdir(path.join(siteRoot, 'pages', '020-contact', 'images'), { recursive: true });
291
+ await writeFile(path.join(siteRoot, 'pages', '020-contact', 'images', 'portrait.jpg'), 'duplicate');
148
292
  const ambiguousDiagnostics = await getMarkdownDiagnostics({ documentPath: homeContentPath, source: homeSource });
149
293
  assert.ok(ambiguousDiagnostics.some(({ message }) => message.includes('is ambiguous')));
150
294
 
@@ -41,7 +41,7 @@ try {
41
41
  const directSiteRoot = path.join(tempRoot, 'current-directory-site');
42
42
  await mkdir(directSiteRoot, { recursive: true });
43
43
  await writeFile(path.join(directSiteRoot, 'config.yaml'), 'url: https://example.com/\n');
44
- await writeFile(path.join(directSiteRoot, 'content.md'), '---\ntitle: Direct Site\ndescription: Direct site fixture.\n---\n\n## Intro {#intro}\n\nText.\n');
44
+ await writeFile(path.join(directSiteRoot, 'content.md'), '# Direct Site\n\n## Intro {#intro}\n\nText.\n');
45
45
  const directSiteProjectRoot = await realpath(tempRoot);
46
46
  const directSiteDoctorResult = runCli(['doctor'], {
47
47
  cwd: directSiteRoot,
@@ -185,6 +185,7 @@ try {
185
185
  "['run', 'schemas:generate']",
186
186
  "['test']",
187
187
  "['commit', '-m', `Release ${releaseTag}`]",
188
+ "['tag', '-a', releaseTag, '-m', `Release ${releaseTag}`]",
188
189
  "['run', 'release:publish']",
189
190
  "['push', '--follow-tags']",
190
191
  ];
@@ -0,0 +1,68 @@
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ formatHeadingIdentifierIssue,
4
+ getHeadingIdentifierIssues,
5
+ getMarkdownHeadings,
6
+ slugifyHeadingText,
7
+ } from './lib/heading-ids.mjs';
8
+
9
+ assert.equal(slugifyHeadingText('Förstå Norna'), 'forsta-norna');
10
+ assert.equal(slugifyHeadingText('Café Über Straße'), 'cafe-uber-strasse');
11
+ assert.equal(slugifyHeadingText('Smørrebrød & œuvre'), 'smorrebrod-oeuvre');
12
+ assert.equal(slugifyHeadingText("What's new?"), 'whats-new');
13
+
14
+ const { headings } = await getMarkdownHeadings(`# Page title
15
+
16
+ ## A **bold** [heading](https://example.com)
17
+
18
+ ### Café details
19
+
20
+ ## Renamed heading {#stable-id}
21
+
22
+ \`\`\`md
23
+ ## Not a heading
24
+ \`\`\`
25
+ `);
26
+
27
+ assert.deepEqual(
28
+ headings.map(({ depth, explicitId, id, title }) => ({ depth, explicitId, id, title })),
29
+ [
30
+ { depth: 1, explicitId: null, id: null, title: 'Page title' },
31
+ { depth: 2, explicitId: null, id: 'a-bold-heading', title: 'A bold heading' },
32
+ { depth: 3, explicitId: null, id: 'cafe-details', title: 'Café details' },
33
+ { depth: 2, explicitId: 'stable-id', id: 'stable-id', title: 'Renamed heading' },
34
+ ],
35
+ );
36
+ assert.deepEqual(getHeadingIdentifierIssues(headings), []);
37
+
38
+ const collision = await getMarkdownHeadings(`# Page
39
+
40
+ ## Förstå
41
+
42
+ ### Forsta
43
+ `);
44
+ assert.equal(getHeadingIdentifierIssues(collision.headings)[0]?.code, 'duplicate-heading-id');
45
+ assert.equal(
46
+ formatHeadingIdentifierIssue(getHeadingIdentifierIssues(collision.headings)[0], 'site/content.md'),
47
+ [
48
+ 'site/content.md: Two headings resolve to id "forsta".',
49
+ '- line 3: "Förstå"',
50
+ '- line 5: "Forsta"',
51
+ 'Add a unique explicit id to at least one heading, for example {#forsta-details}.',
52
+ ].join('\n'),
53
+ );
54
+
55
+ const invalid = await getMarkdownHeadings(`# Page
56
+
57
+ ## Symbols 🎉
58
+
59
+ ### 日本語
60
+
61
+ ## Explicit {#Not Valid}
62
+ `);
63
+ assert.deepEqual(
64
+ getHeadingIdentifierIssues(invalid.headings).map(({ code }) => code),
65
+ ['empty-heading-id', 'invalid-heading-id'],
66
+ );
67
+
68
+ console.log('Heading id tests passed.');
@@ -0,0 +1,63 @@
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ getAutomaticNavigationMode,
4
+ navigationModeNames,
5
+ resolveNavigationModel,
6
+ } from './lib/navigation-model.mjs';
7
+
8
+ const home = (headings = []) => ({
9
+ isHome: true,
10
+ listed: true,
11
+ depth: 1,
12
+ headings,
13
+ });
14
+ const page = (headings = [], overrides = {}) => ({
15
+ isHome: false,
16
+ listed: true,
17
+ depth: 1,
18
+ headings,
19
+ ...overrides,
20
+ });
21
+ const h2 = { depth: 2 };
22
+ const h3 = { depth: 3 };
23
+
24
+ assert.deepEqual(navigationModeNames, ['automatic', 'sections', 'top', 'tree']);
25
+ assert.equal(getAutomaticNavigationMode([home([h2, h3])]), 'sections');
26
+ assert.equal(getAutomaticNavigationMode([home([h2]), page([h2])]), 'top');
27
+ assert.equal(getAutomaticNavigationMode([home([h2]), page([h2, h3])]), 'tree');
28
+ assert.equal(
29
+ getAutomaticNavigationMode([home([h2]), page([h2, h3], { listed: false })]),
30
+ 'sections',
31
+ );
32
+ assert.equal(
33
+ getAutomaticNavigationMode([home([h2]), page([], { depth: 2 })]),
34
+ 'top',
35
+ );
36
+ assert.equal(
37
+ getAutomaticNavigationMode([home([h2]), page([h2], { depth: 2 })]),
38
+ 'tree',
39
+ );
40
+
41
+ for (const mode of ['sections', 'top', 'tree']) {
42
+ const resolved = resolveNavigationModel({
43
+ mode,
44
+ pages: [home([h2]), page([h2, h3])],
45
+ });
46
+ assert.equal(resolved.requestedMode, mode);
47
+ assert.equal(resolved.mode, mode);
48
+ }
49
+
50
+ const automatic = resolveNavigationModel({
51
+ pages: [home([h2]), page([h2, h3])],
52
+ });
53
+ assert.equal(automatic.requestedMode, 'automatic');
54
+ assert.equal(automatic.mode, 'tree');
55
+ assert.equal(automatic.listedPageCount, 2);
56
+ assert.equal(automatic.maximumDepth, 3);
57
+
58
+ assert.throws(
59
+ () => resolveNavigationModel({ mode: 'sidebar', pages: [home()] }),
60
+ /Unknown navigation mode "sidebar".*automatic, sections, top, tree/,
61
+ );
62
+
63
+ console.log('Navigation model test passed.');
@@ -0,0 +1,110 @@
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 path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+ const repoRoot = path.resolve(import.meta.dirname, '..');
9
+ const fixtureRoot = path.join(repoRoot, 'fixtures', 'nested-pages');
10
+ const tempParent = path.join(repoRoot, 'node_modules', '.cache');
11
+ await mkdir(tempParent, { recursive: true });
12
+ const tempRoot = await mkdtemp(path.join(tempParent, 'norna-nested-pages-'));
13
+ const fixtureCopyRoot = path.join(tempRoot, 'nested-pages');
14
+ const nornaBin = path.join(repoRoot, 'bin', 'norna.mjs');
15
+ const runNorna = (siteDir, ...args) => execFileAsync(process.execPath, [
16
+ nornaBin,
17
+ '--site-dir',
18
+ siteDir,
19
+ ...args,
20
+ ], {
21
+ cwd: repoRoot,
22
+ maxBuffer: 1024 * 1024 * 10,
23
+ });
24
+ const runGit = (args) => execFileAsync('git', args, {
25
+ cwd: fixtureCopyRoot,
26
+ env: {
27
+ ...process.env,
28
+ GIT_AUTHOR_NAME: 'Norna Test',
29
+ GIT_AUTHOR_EMAIL: 'norna@example.test',
30
+ GIT_COMMITTER_NAME: 'Norna Test',
31
+ GIT_COMMITTER_EMAIL: 'norna@example.test',
32
+ },
33
+ });
34
+ const fileExists = (filePath) => access(filePath).then(() => true, () => false);
35
+
36
+ try {
37
+ await cp(fixtureRoot, fixtureCopyRoot, { recursive: true });
38
+ const siteDir = path.join(fixtureCopyRoot, 'site');
39
+ await runNorna(siteDir, 'content:check');
40
+ await runNorna(siteDir, 'build');
41
+
42
+ const distDir = path.join(fixtureCopyRoot, 'dist');
43
+ const expectedPages = [
44
+ 'index.html',
45
+ 'guides/index.html',
46
+ 'guides/installation/index.html',
47
+ 'guides/installation/macos/index.html',
48
+ 'guides/workflows/index.html',
49
+ 'reference/index.html',
50
+ 'reference/installation/index.html',
51
+ ];
52
+ for (const pagePath of expectedPages) {
53
+ assert.match(await readFile(path.join(distDir, pagePath), 'utf8'), /<main\b[^>]*id="main-content"/);
54
+ }
55
+
56
+ const rootHtml = await readFile(path.join(distDir, 'index.html'), 'utf8');
57
+ const guidesHtml = await readFile(path.join(distDir, 'guides', 'index.html'), 'utf8');
58
+ const installationHtml = await readFile(path.join(distDir, 'guides', 'installation', 'index.html'), 'utf8');
59
+ const macosHtml = await readFile(path.join(distDir, 'guides', 'installation', 'macos', 'index.html'), 'utf8');
60
+ assert.match(rootHtml, /--color-page: #000000/);
61
+ assert.match(guidesHtml, /--color-page: #ffffff/);
62
+ assert.match(installationHtml, /--color-page: #ffffff/);
63
+ assert.match(macosHtml, /--color-page: #ffffff/);
64
+ assert.match(installationHtml, /Three nested page levels connected in sequence/);
65
+ assert.match(installationHtml, /\/original\/pages\/010-guides\/pages\/010-installation\/images\/diagram-[a-f0-9]+\.svg/);
66
+ assert.match(macosHtml, /class="site-nav-item site-nav-item-has-submenu site-nav-item-current-branch"/);
67
+ assert.match(macosHtml, /href="\/guides\/installation\/macos\/" aria-current="page"/);
68
+ assert.match(macosHtml, /<details class="mobile-navigation-branch" open>/);
69
+ assert.match(macosHtml, /<nav class="site-breadcrumbs" aria-label="Breadcrumb"><ol><li><a href="\/guides\/">Guides<\/a><\/li><li><a href="\/guides\/installation\/">Installation<\/a><\/li><li><span aria-current="page">macOS<\/span>/);
70
+ for (const submenuMatch of macosHtml.matchAll(/<div class="site-nav-submenu">([\s\S]*?)<\/div>/g)) {
71
+ assert.doesNotMatch(submenuMatch[1], /href="[^"#]*#[^"]+"/);
72
+ }
73
+ assert.match(macosHtml, /<nav class="mobile-nav-view" aria-label="On this page">/);
74
+ assert.ok(
75
+ macosHtml.indexOf('href="/guides/installation/"') < macosHtml.indexOf('href="/guides/workflows/"'),
76
+ 'Nested sibling navigation should follow page directory order.',
77
+ );
78
+
79
+ await runGit(['init']);
80
+ await runGit(['add', '.']);
81
+ await runGit(['commit', '-m', 'Initial nested fixture']);
82
+ const installationContentPath = path.join(
83
+ siteDir,
84
+ 'pages',
85
+ '010-guides',
86
+ 'pages',
87
+ '010-installation',
88
+ 'content.md',
89
+ );
90
+ const workflowsContentPath = path.join(siteDir, 'pages', '010-guides', 'pages', '020-workflows', 'content.md');
91
+ const imageBlockPattern = /\n```norna-image-stack\n[\s\S]*?\n```\n/;
92
+ const installationContent = await readFile(installationContentPath, 'utf8');
93
+ const imageBlock = installationContent.match(imageBlockPattern)?.[0];
94
+ assert.ok(imageBlock, 'The nested fixture needs a managed image block to exercise content:sync.');
95
+ await writeFile(installationContentPath, installationContent.replace(imageBlockPattern, '\n'));
96
+ await writeFile(workflowsContentPath, `${await readFile(workflowsContentPath, 'utf8')}${imageBlock}`);
97
+ await runGit(['add', '.']);
98
+ await runGit(['commit', '-m', 'Move nested image reference']);
99
+ await runNorna(siteDir, 'content:sync', '--yes');
100
+
101
+ const previousImagePath = path.join(siteDir, 'pages', '010-guides', 'pages', '010-installation', 'images', 'diagram.svg');
102
+ const nextImagePath = path.join(siteDir, 'pages', '010-guides', 'pages', '020-workflows', 'images', 'diagram.svg');
103
+ assert.equal(await fileExists(previousImagePath), false);
104
+ assert.equal(await fileExists(nextImagePath), true);
105
+ await runNorna(siteDir, 'content:check');
106
+
107
+ console.log('Nested pages fixture check passed.');
108
+ } finally {
109
+ await rm(tempRoot, { recursive: true, force: true });
110
+ }