@janga/norna 0.7.27 → 0.7.29

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 (136) hide show
  1. package/README.md +41 -29
  2. package/astro.config.mjs +33 -3
  3. package/bin/norna-cli.mjs +5 -3
  4. package/package.json +18 -12
  5. package/schemas/card-list.schema.json +1 -1
  6. package/schemas/config.schema.json +81 -66
  7. package/schemas/content-frontmatter.schema.json +23 -7
  8. package/schemas/manifest.json +49 -3
  9. package/schemas/page-theme.schema.json +1585 -41
  10. package/schemas/root-theme.schema.json +1767 -0
  11. package/schemas/sitewide-content.schema.json +18 -18
  12. package/schemas/theme.schema.json +243 -243
  13. package/scripts/add-site-node.mjs +31 -250
  14. package/scripts/build-site.mjs +11 -6
  15. package/scripts/check-config.mjs +7 -23
  16. package/scripts/dev-local.mjs +51 -17
  17. package/scripts/export-theme-preset.mjs +2 -2
  18. package/scripts/generate-images.mjs +89 -70
  19. package/scripts/generate-schemas.mjs +9 -6
  20. package/scripts/generate-search-index.mjs +3 -2
  21. package/scripts/lib/attachment-render.mjs +23 -0
  22. package/scripts/lib/category-destinations.mjs +13 -12
  23. package/scripts/lib/dev-server-identity.mjs +30 -0
  24. package/scripts/lib/documentation-links.mjs +1 -1
  25. package/scripts/lib/documentation-routes.json +70 -62
  26. package/scripts/lib/docusaurus-migration-audit.mjs +15 -12
  27. package/scripts/lib/editor-attachments.mjs +69 -0
  28. package/scripts/lib/editor-file-policy.mjs +20 -0
  29. package/scripts/lib/editor-language-service.mjs +74 -73
  30. package/scripts/lib/editor-page-addresses.mjs +57 -0
  31. package/scripts/lib/editor-page-placement.mjs +102 -0
  32. package/scripts/lib/editor-resource-actions.mjs +216 -0
  33. package/scripts/lib/editor-site-editing-tree.mjs +181 -0
  34. package/scripts/lib/editor-site-files.mjs +154 -0
  35. package/scripts/lib/editor-site-links.mjs +81 -0
  36. package/scripts/lib/editor-site-preview.mjs +149 -0
  37. package/scripts/lib/editor-site-tree.mjs +158 -0
  38. package/scripts/lib/editor-source-files.mjs +87 -0
  39. package/scripts/lib/editor-theme-help.mjs +43 -0
  40. package/scripts/lib/locale-registry.mjs +1 -1
  41. package/scripts/lib/locales/en.mjs +1 -1
  42. package/scripts/lib/logo-assets.mjs +2 -1
  43. package/scripts/lib/markdown-images.mjs +59 -0
  44. package/scripts/lib/markdown-links.mjs +29 -9
  45. package/scripts/lib/navigation-model.mjs +2 -44
  46. package/scripts/lib/navigation-review.mjs +14 -50
  47. package/scripts/lib/norna-markdown-blocks.mjs +1 -58
  48. package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
  49. package/scripts/lib/page-aliases.mjs +8 -1
  50. package/scripts/lib/page-attachments.mjs +59 -0
  51. package/scripts/lib/page-markdown.mjs +11 -7
  52. package/scripts/lib/page-model.mjs +19 -36
  53. package/scripts/lib/page-move-apply.mjs +119 -0
  54. package/scripts/lib/page-move-plan.mjs +64 -78
  55. package/scripts/lib/page-order-source.mjs +22 -0
  56. package/scripts/lib/page-order.mjs +45 -0
  57. package/scripts/lib/page-placement-cli.mjs +50 -0
  58. package/scripts/lib/presentation.mjs +3 -3
  59. package/scripts/lib/project-config.mjs +10 -8
  60. package/scripts/lib/publication-paths.mjs +72 -0
  61. package/scripts/lib/schema-definitions.mjs +15 -38
  62. package/scripts/lib/schema-editor-metadata.mjs +16 -60
  63. package/scripts/lib/schema-value-definitions.mjs +1 -1
  64. package/scripts/lib/site-content.mjs +8 -25
  65. package/scripts/lib/site-conventions.mjs +19 -1
  66. package/scripts/lib/site-link-graph.mjs +88 -18
  67. package/scripts/lib/site-navigation-tree.mjs +2 -7
  68. package/scripts/lib/site-node-create.mjs +182 -0
  69. package/scripts/lib/site-paths.mjs +24 -12
  70. package/scripts/lib/site-source-format.mjs +12 -0
  71. package/scripts/lib/site-structure.mjs +86 -95
  72. package/scripts/lib/site-upgrade.mjs +32 -0
  73. package/scripts/lib/social-image-assets.mjs +2 -1
  74. package/scripts/lib/source-files.mjs +34 -0
  75. package/scripts/lib/structured-blocks.mjs +3 -1
  76. package/scripts/lib/theme-config.mjs +26 -68
  77. package/scripts/lib/theme-packages.mjs +47 -0
  78. package/scripts/lib/theme-presets.mjs +20 -53
  79. package/scripts/lib/typography.mjs +2 -2
  80. package/scripts/lib/yaml-config.mjs +1 -1
  81. package/scripts/lib/yaml-source-edit.mjs +94 -0
  82. package/scripts/move-site-page.mjs +46 -254
  83. package/scripts/show-typography.mjs +32 -17
  84. package/scripts/sync-content-sections.mjs +17 -17
  85. package/scripts/sync-site-public.mjs +44 -66
  86. package/scripts/upgrade-site.mjs +17 -0
  87. package/src/components/AreaNavigationMenu.astro +24 -0
  88. package/src/components/AreaNavigationPanel.astro +55 -0
  89. package/src/components/AreaNavigationSwitcher.astro +30 -0
  90. package/src/components/ContentBlocks.astro +2 -1
  91. package/src/components/ImageStack.astro +7 -5
  92. package/src/components/ImageStackEnhancement.astro +0 -7
  93. package/src/components/NavigationContinuity.astro +26 -0
  94. package/src/components/NavigationPageTree.astro +13 -4
  95. package/src/components/NavigationState.astro +11 -0
  96. package/src/components/NavigationTreeControls.astro +3 -2
  97. package/src/components/SearchPage.astro +11 -2
  98. package/src/components/SectionNavigationScript.astro +106 -26
  99. package/src/components/SiteNavigation.astro +45 -20
  100. package/src/components/SitePage.astro +38 -61
  101. package/src/components/SiteSection.astro +1 -0
  102. package/src/components/SiteTreeNavigation.astro +38 -18
  103. package/src/components/TopPageMenu.astro +4 -3
  104. package/src/components/TreeNavigationScript.astro +50 -57
  105. package/src/content.config.ts +11 -12
  106. package/src/layouts/BaseLayout.astro +15 -39
  107. package/src/lib/areaNavigation.ts +42 -0
  108. package/src/lib/imagePresentation.ts +3 -0
  109. package/src/lib/markdownImages.ts +101 -0
  110. package/src/lib/navigationContinuity.ts +212 -0
  111. package/src/lib/navigationFollowing.ts +28 -18
  112. package/src/lib/navigationState.mjs +88 -0
  113. package/src/lib/navigationStateScript.mjs +76 -0
  114. package/src/lib/navigationVisibility.ts +9 -0
  115. package/src/lib/pageThemes.ts +5 -42
  116. package/src/lib/sectionContent.ts +67 -21
  117. package/src/lib/siteNavigation.ts +6 -0
  118. package/src/lib/sitePages.ts +13 -36
  119. package/src/lib/sitePublicAssets.ts +2 -1
  120. package/src/pages/404.astro +1 -1
  121. package/src/pages/[...slug].astro +7 -16
  122. package/src/pages/index.astro +1 -3
  123. package/src/styles/area-navigation.css +408 -0
  124. package/src/styles/content.css +8 -29
  125. package/src/styles/media.css +24 -0
  126. package/src/styles/navigation-continuity.css +163 -0
  127. package/src/styles/page-layout.css +57 -68
  128. package/src/styles/responsive.css +0 -4
  129. package/starters/basic/README.md +9 -8
  130. package/starters/basic/site/{pages/000-home → root}/content.md +2 -2
  131. package/starters/basic/site/{config.yaml → site-config/settings.yaml} +1 -0
  132. package/schemas/category.schema.json +0 -20
  133. package/src/components/PageContentsNavigation.astro +0 -20
  134. /package/starters/basic/site/{pages/000-home → root}/images/.gitkeep +0 -0
  135. /package/starters/basic/site/{theme.yaml → root/tree-theme.yaml} +0 -0
  136. /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
@@ -1,58 +1,66 @@
1
1
  {
2
2
  "sources": {
3
- "site/files/": "site/pages/032-reference/pages/010-site/pages/010-files/content.md",
4
- "site/pages/": "site/pages/032-reference/pages/010-site/pages/020-pages/content.md",
5
- "site/urls/": "site/pages/032-reference/pages/010-site/pages/030-urls/content.md",
6
- "site/metadata/": "site/pages/032-reference/pages/010-site/pages/040-metadata/content.md",
7
- "site/images/": "site/pages/032-reference/pages/010-site/pages/050-images/content.md",
8
- "site/public-files/": "site/pages/032-reference/pages/010-site/pages/060-public-files/content.md",
9
- "site/requirements/": "site/pages/032-reference/pages/010-site/pages/070-requirements/content.md",
10
- "configuration/site/": "site/pages/032-reference/pages/020-configuration/pages/010-site/content.md",
11
- "configuration/language/": "site/pages/032-reference/pages/020-configuration/pages/020-language/content.md",
12
- "configuration/navigation/": "site/pages/032-reference/pages/020-configuration/pages/030-navigation/content.md",
13
- "configuration/search/": "site/pages/032-reference/pages/020-configuration/pages/040-search/content.md",
14
- "configuration/source-links/": "site/pages/032-reference/pages/020-configuration/pages/050-source-links/content.md",
15
- "configuration/theme/": "site/pages/032-reference/pages/020-configuration/pages/060-theme/content.md",
16
- "configuration/presets/": "site/pages/032-reference/pages/020-configuration/pages/065-presets/content.md",
17
- "configuration/palettes/": "site/pages/032-reference/pages/020-configuration/pages/070-palettes/content.md",
18
- "configuration/appearance/": "site/pages/032-reference/pages/020-configuration/pages/080-appearance/content.md",
19
- "configuration/layout/": "site/pages/032-reference/pages/020-configuration/pages/090-layout/content.md",
20
- "configuration/images/": "site/pages/032-reference/pages/020-configuration/pages/100-images/content.md",
21
- "configuration/typography/": "site/pages/032-reference/pages/020-configuration/pages/110-typography/content.md",
22
- "configuration/sections/": "site/pages/032-reference/pages/020-configuration/pages/120-sections/content.md",
23
- "configuration/shared-content/": "site/pages/032-reference/pages/020-configuration/pages/130-shared-content/content.md",
24
- "content/markdown/": "site/pages/032-reference/pages/030-content/pages/010-markdown/content.md",
25
- "content/structured-blocks/": "site/pages/032-reference/pages/030-content/pages/015-structured-blocks/content.md",
26
- "content/images/": "site/pages/032-reference/pages/030-content/pages/020-images/content.md",
27
- "content/cards/": "site/pages/032-reference/pages/030-content/pages/030-cards/content.md",
28
- "content/child-lists/": "site/pages/032-reference/pages/030-content/pages/040-child-lists/content.md",
29
- "content/callouts/": "site/pages/032-reference/pages/030-content/pages/050-callouts/content.md",
30
- "content/tabs/": "site/pages/032-reference/pages/030-content/pages/060-tabs/content.md",
31
- "content/details/": "site/pages/032-reference/pages/030-content/pages/070-details/content.md",
32
- "content/tables/": "site/pages/032-reference/pages/030-content/pages/080-tables/content.md",
33
- "content/code/": "site/pages/032-reference/pages/030-content/pages/090-code/content.md",
34
- "content/sidenotes/": "site/pages/032-reference/pages/030-content/pages/100-sidenotes/content.md",
35
- "content/footnotes/": "site/pages/032-reference/pages/030-content/pages/110-footnotes/content.md",
36
- "commands/invocation/": "site/pages/032-reference/pages/040-commands/pages/010-invocation/content.md",
37
- "commands/init/": "site/pages/032-reference/pages/040-commands/pages/020-init/content.md",
38
- "commands/create/": "site/pages/032-reference/pages/040-commands/pages/030-create/content.md",
39
- "commands/move/": "site/pages/032-reference/pages/040-commands/pages/040-move/content.md",
40
- "commands/validate/": "site/pages/032-reference/pages/040-commands/pages/050-validate/content.md",
41
- "commands/navigation/": "site/pages/032-reference/pages/040-commands/pages/055-navigation/content.md",
42
- "commands/development/": "site/pages/032-reference/pages/040-commands/pages/060-development/content.md",
43
- "commands/build/": "site/pages/032-reference/pages/040-commands/pages/070-build/content.md",
44
- "commands/publishing/": "site/pages/032-reference/pages/040-commands/pages/080-publishing/content.md",
45
- "commands/theme/": "site/pages/032-reference/pages/040-commands/pages/090-theme/content.md",
46
- "commands/engine/": "site/pages/032-reference/pages/040-commands/pages/100-engine/content.md",
47
- "commands/migration-audit/": "site/pages/032-reference/pages/040-commands/pages/110-migration-audit/content.md",
48
- "reader/display/": "site/pages/032-reference/pages/050-reader/pages/010-display/content.md",
49
- "reader/accessibility/": "site/pages/032-reference/pages/050-reader/pages/020-accessibility/content.md",
50
- "workflows/editor/": "site/pages/032-reference/pages/060-workflows/pages/010-editor/content.md",
51
- "workflows/editor-ai-suggestions/": "site/pages/032-reference/pages/060-workflows/pages/015-editor-ai-suggestions/content.md",
52
- "workflows/publishing/": "site/pages/032-reference/pages/060-workflows/pages/020-publishing/content.md",
53
- "workflows/embedded-publishing/": "site/pages/032-reference/pages/060-workflows/pages/030-embedded-publishing/content.md",
54
- "workflows/upgrading/": "site/pages/032-reference/pages/060-workflows/pages/040-upgrading/content.md",
55
- "workflows/legacy-source/": "site/pages/032-reference/pages/060-workflows/pages/050-legacy-source/content.md"
3
+ "": "site/root/pages/reference/content.md",
4
+ "site/": "site/root/pages/reference/pages/site/content.md",
5
+ "site/files/": "site/root/pages/reference/pages/site/pages/files/content.md",
6
+ "site/pages/": "site/root/pages/reference/pages/site/pages/pages/content.md",
7
+ "site/urls/": "site/root/pages/reference/pages/site/pages/urls/content.md",
8
+ "site/metadata/": "site/root/pages/reference/pages/site/pages/metadata/content.md",
9
+ "site/images/": "site/root/pages/reference/pages/site/pages/images/content.md",
10
+ "site/attachments/": "site/root/pages/reference/pages/site/pages/attachments/content.md",
11
+ "site/public-files/": "site/root/pages/reference/pages/site/pages/public-files/content.md",
12
+ "site/requirements/": "site/root/pages/reference/pages/site/pages/requirements/content.md",
13
+ "configuration/": "site/root/pages/reference/pages/configuration/content.md",
14
+ "configuration/site/": "site/root/pages/reference/pages/configuration/pages/site/content.md",
15
+ "configuration/language/": "site/root/pages/reference/pages/configuration/pages/language/content.md",
16
+ "configuration/navigation/": "site/root/pages/reference/pages/configuration/pages/navigation/content.md",
17
+ "configuration/search/": "site/root/pages/reference/pages/configuration/pages/search/content.md",
18
+ "configuration/source-links/": "site/root/pages/reference/pages/configuration/pages/source-links/content.md",
19
+ "configuration/theme/": "site/root/pages/reference/pages/configuration/pages/theme/content.md",
20
+ "configuration/presets/": "site/root/pages/reference/pages/configuration/pages/presets/content.md",
21
+ "configuration/palettes/": "site/root/pages/reference/pages/configuration/pages/palettes/content.md",
22
+ "configuration/appearance/": "site/root/pages/reference/pages/configuration/pages/appearance/content.md",
23
+ "configuration/layout/": "site/root/pages/reference/pages/configuration/pages/layout/content.md",
24
+ "configuration/images/": "site/root/pages/reference/pages/configuration/pages/images/content.md",
25
+ "configuration/typography/": "site/root/pages/reference/pages/configuration/pages/typography/content.md",
26
+ "configuration/sections/": "site/root/pages/reference/pages/configuration/pages/sections/content.md",
27
+ "configuration/shared-content/": "site/root/pages/reference/pages/configuration/pages/shared-content/content.md",
28
+ "content/": "site/root/pages/reference/pages/content/content.md",
29
+ "content/markdown/": "site/root/pages/reference/pages/content/pages/markdown/content.md",
30
+ "content/structured-blocks/": "site/root/pages/reference/pages/content/pages/structured-blocks/content.md",
31
+ "content/images/": "site/root/pages/reference/pages/content/pages/images/content.md",
32
+ "content/cards/": "site/root/pages/reference/pages/content/pages/cards/content.md",
33
+ "content/child-lists/": "site/root/pages/reference/pages/content/pages/child-lists/content.md",
34
+ "content/callouts/": "site/root/pages/reference/pages/content/pages/callouts/content.md",
35
+ "content/tabs/": "site/root/pages/reference/pages/content/pages/tabs/content.md",
36
+ "content/details/": "site/root/pages/reference/pages/content/pages/details/content.md",
37
+ "content/tables/": "site/root/pages/reference/pages/content/pages/tables/content.md",
38
+ "content/code/": "site/root/pages/reference/pages/content/pages/code/content.md",
39
+ "content/sidenotes/": "site/root/pages/reference/pages/content/pages/sidenotes/content.md",
40
+ "content/footnotes/": "site/root/pages/reference/pages/content/pages/footnotes/content.md",
41
+ "commands/": "site/root/pages/reference/pages/commands/content.md",
42
+ "commands/invocation/": "site/root/pages/reference/pages/commands/pages/invocation/content.md",
43
+ "commands/init/": "site/root/pages/reference/pages/commands/pages/init/content.md",
44
+ "commands/create/": "site/root/pages/reference/pages/commands/pages/create/content.md",
45
+ "commands/move/": "site/root/pages/reference/pages/commands/pages/move/content.md",
46
+ "commands/validate/": "site/root/pages/reference/pages/commands/pages/validate/content.md",
47
+ "commands/navigation/": "site/root/pages/reference/pages/commands/pages/navigation/content.md",
48
+ "commands/development/": "site/root/pages/reference/pages/commands/pages/development/content.md",
49
+ "commands/build/": "site/root/pages/reference/pages/commands/pages/build/content.md",
50
+ "commands/publishing/": "site/root/pages/reference/pages/commands/pages/publishing/content.md",
51
+ "commands/theme/": "site/root/pages/reference/pages/commands/pages/theme/content.md",
52
+ "commands/engine/": "site/root/pages/reference/pages/commands/pages/engine/content.md",
53
+ "commands/migration-audit/": "site/root/pages/reference/pages/commands/pages/migration-audit/content.md",
54
+ "reader/": "site/root/pages/reference/pages/reader/content.md",
55
+ "reader/display/": "site/root/pages/reference/pages/reader/pages/display/content.md",
56
+ "reader/accessibility/": "site/root/pages/reference/pages/reader/pages/accessibility/content.md",
57
+ "workflows/": "site/root/pages/reference/pages/workflows/content.md",
58
+ "workflows/editor/": "site/root/pages/reference/pages/workflows/pages/editor/content.md",
59
+ "workflows/editor-ai-suggestions/": "site/root/pages/reference/pages/workflows/pages/editor-ai-suggestions/content.md",
60
+ "workflows/publishing/": "site/root/pages/reference/pages/workflows/pages/publishing/content.md",
61
+ "workflows/embedded-publishing/": "site/root/pages/reference/pages/workflows/pages/embedded-publishing/content.md",
62
+ "workflows/upgrading/": "site/root/pages/reference/pages/workflows/pages/upgrading/content.md",
63
+ "workflows/legacy-source/": "site/root/pages/reference/pages/workflows/pages/legacy-source/content.md"
56
64
  },
57
65
  "legacy": {
58
66
  "site-files.md": "site/files/",
@@ -78,13 +86,13 @@
78
86
  "site-files.md#selecting-the-site-directory": "commands/invocation/#site-selection",
79
87
  "site-files.md#versioned-files": "site/files/#generated-files",
80
88
  "site-files.md#generated-files": "site/files/#generated-files",
81
- "pages.md#homepage-and-top-level-entries": "site/pages/#page-or-category",
82
- "pages.md#choose-a-section-page-or-category": "site/pages/#page-or-category",
89
+ "pages.md#homepage-and-top-level-entries": "site/pages/#choose-pages-and-sections",
90
+ "pages.md#choose-a-section-page-or-category": "site/pages/#choose-pages-and-sections",
83
91
  "pages.md#review-an-existing-hierarchy": "commands/navigation/",
84
- "pages.md#navigation-categories": "site/pages/#page-or-category",
85
- "pages.md#opening-a-category-url": "site/pages/#opening-a-category-url",
86
- "pages.md#reachable-content-and-stable-links": "site/pages/#opening-a-category-url",
87
- "pages.md#nested-pages": "site/pages/#page-or-category",
92
+ "pages.md#navigation-categories": "site/pages/#choose-pages-and-sections",
93
+ "pages.md#opening-a-category-url": "site/pages/#list-child-pages",
94
+ "pages.md#reachable-content-and-stable-links": "site/pages/#list-child-pages",
95
+ "pages.md#nested-pages": "site/pages/#choose-pages-and-sections",
88
96
  "pages.md#directory-names-order-and-urls": "site/pages/#names-and-order",
89
97
  "pages.md#preserve-old-page-urls": "site/urls/#keep-an-old-url",
90
98
  "pages.md#move-or-reconcile-a-page": "commands/move/",
@@ -148,10 +156,10 @@
148
156
  "public-files.md#navigation-logo": "site/public-files/#recognized-files",
149
157
  "public-files.md#browser-icons": "site/public-files/#recognized-files",
150
158
  "public-files.md#social-sharing-image": "site/public-files/#social-previews",
151
- "public-files.md#github-pages-custom-domain": "site/public-files/#downloads-and-hosting-files",
159
+ "public-files.md#github-pages-custom-domain": "site/public-files/#allowed-files",
152
160
  "public-files.md#generated-sitemap": "site/public-files/#generated-sitemap",
153
161
  "public-files.md#generated-404-page": "site/public-files/#generated-404-page",
154
- "public-files.md#other-static-files": "site/public-files/#downloads-and-hosting-files",
162
+ "public-files.md#other-static-files": "site/public-files/#allowed-files",
155
163
  "public-files.md#generated-copy": "site/public-files/#preparation-and-reserved-paths",
156
164
  "theme.md#theme-presets": "configuration/presets/",
157
165
  "theme.md#choose-a-preset": "configuration/theme/#presets-and-root-overrides",
@@ -169,7 +177,7 @@
169
177
  "theme.md#palette": "configuration/palettes/",
170
178
  "theme.md#appearance": "configuration/appearance/",
171
179
  "theme.md#reader-display-controls": "reader/display/",
172
- "theme.md#corners": "configuration/theme/#site-wide-settings",
180
+ "theme.md#corners": "configuration/theme/#visual-settings",
173
181
  "theme.md#section-backgrounds": "configuration/sections/",
174
182
  "theme.md#page-themes": "configuration/theme/#page-themes",
175
183
  "typography.md#typography-profiles": "configuration/typography/#font-profile-and-rhythm",
@@ -1,3 +1,4 @@
1
+ import { getSiteSourcePaths } from './site-conventions.mjs';
1
2
  import { execFile } from 'node:child_process';
2
3
  import { createHash } from 'node:crypto';
3
4
  import { access, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
@@ -312,11 +313,11 @@ const inspectPage = async ({ absolutePath, relativePath, root }) => {
312
313
  if (node.type === 'code') codeRanges.push(nodeRange(node));
313
314
  if (node.type === 'image') {
314
315
  collector.add({
315
- action: 'Move the asset beside the Norna page and convert the reference to image-stack or image-carousel syntax.',
316
+ action: "Place the local asset in the Norna page's images/ folder and update its Markdown path if needed; review external images separately.",
316
317
  classification: 'rewrite',
317
318
  code: 'managed-image-rewrite',
318
319
  line: nodeRange(node).line,
319
- message: 'A Markdown image needs conversion to a Norna-managed image block.',
320
+ message: 'Review the image location and supported format; ordinary Markdown images can use Norna-managed files.',
320
321
  });
321
322
  }
322
323
  if (node.type === 'html') inspectHtmlNode(node, collector);
@@ -559,12 +560,12 @@ const renderReportHome = (report) => {
559
560
  + (problemRows || '| None | No findings | 0 | copy |') + '\n';
560
561
  };
561
562
 
562
- const renderProblemPage = (report, problem) => {
563
+ const renderProblemPage = (report, problem, order) => {
563
564
  const rows = problem.sample.map((occurrence) => {
564
565
  const link = sourceLink(report, occurrence);
565
566
  return '| ' + occurrence.pageTitle.replaceAll('|', '\\|') + ' | ' + codeMark + occurrence.sourceFile + codeMark + ' | ' + occurrence.line + ' | ' + (link ? '[Open source](' + link + ')' : 'Unavailable') + ' |';
566
567
  }).join('\n');
567
- return '---\npage:\n description: ' + problemDescription(problem) + '.\n---\n\n'
568
+ return '---\nnavigation:\n order: ' + order + '\npage:\n description: ' + problemDescription(problem) + '.\n---\n\n'
568
569
  + '# ' + problem.id + '\n\n'
569
570
  + problem.message + '\n\n'
570
571
  + 'Classification: **' + problem.classification + '**. Occurrences: **' + problem.occurrences + '**.\n\n'
@@ -584,18 +585,20 @@ export const writeMigrationReport = async (report, reportDir, { sourceRoot = nul
584
585
  const entries = await readdir(resolvedReportDir);
585
586
  if (entries.length > 0) throw new Error('Migration report directory must be empty: ' + resolvedReportDir);
586
587
  } else await mkdir(resolvedReportDir, { recursive: true });
587
- await mkdir(path.join(resolvedReportDir, 'pages', '000-home'), { recursive: true });
588
- await writeFile(path.join(resolvedReportDir, 'config.yaml'), 'url: https://example.invalid/\n');
589
- await writeFile(path.join(resolvedReportDir, 'theme.yaml'), 'preset: documentation\n');
590
- await writeFile(path.join(resolvedReportDir, 'pages', '000-home', 'content.md'), renderReportHome(report));
588
+ const source = getSiteSourcePaths(resolvedReportDir);
589
+ await mkdir(source.pages, { recursive: true });
590
+ await mkdir(path.join(resolvedReportDir, 'site-config'), { recursive: true });
591
+ await writeFile(path.join(resolvedReportDir, 'site-config/settings.yaml'), 'sourceFormat: 2\nurl: https://example.invalid/\n');
592
+ await writeFile(path.join(resolvedReportDir, 'root', 'tree-theme.yaml'), 'preset: documentation\n');
593
+ await writeFile(source.content, renderReportHome(report));
591
594
  if (report.problemTypes.length > 0) {
592
- const categoryDir = path.join(resolvedReportDir, 'pages', '010-problems');
595
+ const categoryDir = path.join(source.pages, 'problems');
593
596
  await mkdir(path.join(categoryDir, 'pages'), { recursive: true });
594
- await writeFile(path.join(categoryDir, 'category.yaml'), 'label: Problems\n');
597
+ await writeFile(path.join(categoryDir, 'content.md'), '---\nnavigation:\n order: 1000\npage:\n listChildren: true\n---\n\n# Problems\n');
595
598
  for (const [index, problem] of report.problemTypes.entries()) {
596
- const pageDir = path.join(categoryDir, 'pages', String((index + 1) * 10).padStart(3, '0') + '-' + problemPageId(problem));
599
+ const pageDir = path.join(categoryDir, 'pages', problemPageId(problem));
597
600
  await mkdir(pageDir, { recursive: true });
598
- await writeFile(path.join(pageDir, 'content.md'), renderProblemPage(report, problem));
601
+ await writeFile(path.join(pageDir, 'content.md'), renderProblemPage(report, problem, (index + 1) * 1000));
599
602
  }
600
603
  }
601
604
  await writeFile(path.join(resolvedReportDir, 'migration-report.json'), JSON.stringify(report, null, 2) + '\n');
@@ -0,0 +1,69 @@
1
+ import { lstat, readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { markdownToMdast } from 'satteri';
4
+ import { checkedPath, snapshot } from './editor-site-files.mjs';
5
+ import { readEditorLinkState } from './editor-site-links.mjs';
6
+ import { attachmentPathname, attachmentOutputIssues, validateAttachmentName, encodeAttachmentPath } from './page-attachments.mjs';
7
+
8
+ export const siteAttachmentsApiVersion = 1;
9
+ const info = filename => lstat(filename).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
10
+ export const planEditorAttachmentCopy = async ({ siteRoot, sourcePath, filePath, filename, replace = false, pending = [], sources = new Map() }) => {
11
+ validateAttachmentName(filename);
12
+ await checkedPath(siteRoot, sourcePath);
13
+ const state = await readEditorLinkState({ siteRoot, sources });
14
+ if (state.incomplete.length) throw new Error(`Repair the site before importing attachments. ${state.incomplete.join('\n')}`);
15
+ const page = state.graph.pages.find(page => page.contentFile.contentPath === sourcePath);
16
+ if (!page) throw new Error('Choose a content page for these attachments.');
17
+ const source = path.resolve(filePath), sourceInfo = await info(source);
18
+ if (!sourceInfo?.isFile() || sourceInfo.isSymbolicLink()) throw new Error('Choose a regular file to import.');
19
+ const directory = path.join(path.dirname(sourcePath), 'downloads');
20
+ const destination = await checkedPath(siteRoot, path.join(directory, filename));
21
+ if (source === destination) throw new Error('This attachment is already on the page. Use Insert Link in Page.');
22
+ const key = value => value.normalize('NFC').toLowerCase();
23
+ const entries = await readdir(directory).catch(error => { if (error.code === 'ENOENT') return []; throw error; });
24
+ const occupied = entries.find(name => key(name) === key(filename));
25
+ const existing = await info(destination);
26
+ if (replace ? !existing?.isFile() || existing.isSymbolicLink() : Boolean(occupied)) throw new Error(replace ? 'The file to replace no longer exists.' : `“${occupied}” already exists. Choose Replace, another filename or Ignore.`);
27
+ if (occupied && occupied !== filename) throw new Error(`Use the exact existing filename “${occupied}” to replace it, or choose a different name.`);
28
+ if (pending.some(name => key(name) === key(filename))) throw new Error('Two selected files use this name. Rename or ignore one.');
29
+ const pendingPaths = new Set(pending.map(name => path.join(directory, name)));
30
+ const files = state.graph.attachments.files.filter(file => file.filePath !== destination && !pendingPaths.has(file.filePath));
31
+ for (const name of [...pending, filename]) files.push({ filePath: path.join(directory, name), pathname: attachmentPathname(page.pathname, name) });
32
+ const problems = attachmentOutputIssues({ files, pages: state.graph.pages, publicFiles: state.publicFiles, generatedRoutes: state.generatedRoutes });
33
+ if (problems.length) throw new Error(problems.map(issue => issue.message).join('\n'));
34
+ return { source, sourcePath, destination, filename, replace,
35
+ sourceInfo: { size: sourceInfo.size, modified: sourceInfo.mtimeMs },
36
+ existingInfo: existing ? { size: existing.size, modified: existing.mtimeMs } : null,
37
+ fingerprint: JSON.stringify([await snapshot(source), existing ? await snapshot(destination) : null, state.graph.pages.map(page => [page.pathname, page.document.fullSource]), files.map(file => file.pathname).sort()]) };
38
+ };
39
+
40
+ export const createEditorAttachmentInsertion = ({ source, items, offset = source.length }) => {
41
+ if (!items.length) return { start: offset, text: '' };
42
+ if (!Number.isInteger(offset) || offset < 0 || offset > source.length) throw new Error('The insertion position is no longer valid.');
43
+ const ast = markdownToMdast(source, { features: { gfm: true, frontmatter: true } });
44
+ const visit = node => {
45
+ if (node.position && offset >= node.position.start.offset && offset < node.position.end.offset && ['yaml', 'toml', 'code', 'inlineCode', 'html', 'link', 'image', 'definition', 'linkReference', 'imageReference', 'table'].includes(node.type)) throw new Error('Place the cursor in ordinary page text, outside code, links, HTML, tables and page settings, then insert again.');
46
+ for (const child of node.children ?? []) visit(child);
47
+ };
48
+ visit(ast);
49
+ const newline = source.includes('\r\n') ? '\r\n' : '\n';
50
+ const links = items.map(item => {
51
+ validateAttachmentName(item.filename);
52
+ if (typeof item.text !== 'string' || !item.text.trim() || /[\r\n]/.test(item.text)) throw new Error('Enter link text on one line for each inserted attachment.');
53
+ const text = item.text.replace(/[\\`*_[\]<>]/g, '\\$&');
54
+ const href = `downloads/${encodeAttachmentPath(item.filename)}`;
55
+ return `[${text}](${href})`;
56
+ });
57
+ const multiple = links.length > 1;
58
+ const text = multiple ? links.map(link => '- ' + link).join(newline) : links[0];
59
+ const insertion = multiple ? newline + newline + text + newline + newline : (offset === source.length ? newline + newline : '') + text + (offset === source.length ? newline : '');
60
+ const result = markdownToMdast(source.slice(0, offset) + insertion + source.slice(offset), { features: { gfm: true, frontmatter: true } });
61
+ const inserted = [];
62
+ const collect = node => {
63
+ if (node.type === 'link' && node.position.start.offset >= offset && node.position.end.offset <= offset + insertion.length) inserted.push(node);
64
+ for (const child of node.children ?? []) collect(child);
65
+ };
66
+ collect(result);
67
+ if (inserted.length !== items.length) throw new Error('Place the cursor in ordinary page text outside code, HTML and page settings, then insert again.');
68
+ return { start: offset, text: insertion };
69
+ };
@@ -0,0 +1,20 @@
1
+ import { getSiteSourcePaths } from './site-conventions.mjs';
2
+ import path from 'node:path';
3
+ import { getSourcePageLocation } from './source-files.mjs';
4
+ import { getSourceFileDefinition } from './source-files.mjs';
5
+ import { isInside } from './editor-site-links.mjs';
6
+
7
+ export const editorFileRemovalPolicy = ({ siteRoot, sourcePath, filePath }) => {
8
+ const directory = path.dirname(sourcePath);
9
+ const siteOwner = sourcePath === siteRoot || directory === getSiteSourcePaths(siteRoot).root;
10
+ const definition = getSourceFileDefinition(siteRoot, filePath);
11
+ if (definition && !definition.required && definition.removalEffect
12
+ && (path.dirname(filePath) === directory || definition.location === 'site-config' && siteOwner)) {
13
+ return { kind: definition.schemaKind === 'sitewideContent' ? 'shared-content' : 'theme', effect: definition.removalEffect };
14
+ }
15
+ if (siteOwner && isInside(path.join(siteRoot, 'public'), filePath)) return {
16
+ kind: 'public', effect: 'This file will no longer be published. Links to it may break; an icon or logo may disappear. References outside page content are not checked.',
17
+ };
18
+ if (getSourcePageLocation(siteRoot, directory) && path.dirname(filePath) === path.join(directory, 'downloads')) return { kind: 'attachment', effect: 'Links to this attachment will stop working. Restore its file from Trash if needed.' };
19
+ return null;
20
+ };
@@ -1,3 +1,4 @@
1
+ import { extractMarkdownImageReferences } from './markdown-images.mjs';
1
2
  import { access, readdir, readFile } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { isDeepStrictEqual } from 'node:util';
@@ -17,11 +18,14 @@ import {
17
18
  socialImageAssetFilenames,
18
19
  } from './public-asset-conventions.mjs';
19
20
  import { parsePageMarkdownSource } from './page-markdown.mjs';
21
+ import { selectPageTheme } from './theme-packages.mjs';
22
+ import { getSourcePageLocation } from './source-files.mjs';
23
+ import { assertSectionBackgroundPatternCompatibility } from './presentation.mjs';
20
24
  import { siteSchema } from './schema-definitions.mjs';
21
- import { homePageDirectory } from './site-conventions.mjs';
25
+ import { getSiteSourcePaths, homePageDirectory } from './site-conventions.mjs';
22
26
 
23
27
  const supportedImageExtensions = new Set(['.jpg', '.jpeg', '.png', '.svg']);
24
- const siteConfigNames = ['config.yaml'];
28
+ const siteConfigNames = ['site-config/settings.yaml'];
25
29
  const fileExists = (filePath) => access(filePath).then(() => true, () => false);
26
30
  const toPosixPath = (filePath) => filePath.split(path.sep).join('/');
27
31
 
@@ -79,45 +83,35 @@ const getNavigationNodesForEditor = async (siteRoot) => {
79
83
  for (const entry of entries) {
80
84
  const nodeDirectory = path.join(pagesDirectory, entry.name);
81
85
  const contentPath = path.join(nodeDirectory, 'content.md');
82
- const categoryPath = path.join(nodeDirectory, 'category.yaml');
83
- const [hasContent, hasCategory] = await Promise.all([
84
- fileExists(contentPath),
85
- fileExists(categoryPath),
86
- ]);
87
-
88
- if (hasContent !== hasCategory) {
89
- if (hasCategory) {
90
- nodes.push({
91
- depth,
92
- headings: [],
93
- isHome: false,
94
- kind: 'category',
95
- listed: true,
96
- });
97
- } else {
98
- const source = await readFile(contentPath, 'utf8');
99
- const data = readContentFrontmatterForEditor(source);
100
- const document = await parsePageMarkdownSource(source, { label: contentPath });
101
- nodes.push({
102
- depth,
103
- headings: document.navigationHeadings,
104
- isHome: depth === 1 && entry.name === homePageDirectory,
105
- kind: 'page',
106
- listed: data.navigation?.listed !== false,
107
- });
108
- }
86
+ if (await fileExists(contentPath)) {
87
+ const source = await readFile(contentPath, 'utf8');
88
+ const data = readContentFrontmatterForEditor(source);
89
+ const document = await parsePageMarkdownSource(source, { label: contentPath });
90
+ nodes.push({
91
+ depth,
92
+ headings: document.navigationHeadings,
93
+ isHome: false,
94
+ kind: 'page',
95
+ listed: data.navigation?.listed !== false,
96
+ });
109
97
  }
110
98
 
111
99
  await visit(path.join(nodeDirectory, 'pages'), depth + 1);
112
100
  }
113
101
  };
114
102
 
115
- await visit(path.join(siteRoot, 'pages'));
103
+ const homeContent = getSiteSourcePaths(siteRoot).content;
104
+ if (await fileExists(homeContent)) {
105
+ const source = await readFile(homeContent, 'utf8');
106
+ const document = await parsePageMarkdownSource(source, { label: homeContent });
107
+ nodes.unshift({ depth: 0, headings: document.navigationHeadings, isHome: true, kind: 'page', listed: true });
108
+ }
109
+ await visit(getSiteSourcePaths(siteRoot).pages);
116
110
  return nodes;
117
111
  };
118
112
 
119
113
  const getNavigationModeForEditor = async (siteRoot) => {
120
- const config = await readEditorYaml(path.join(siteRoot, 'config.yaml'));
114
+ const config = await readEditorYaml(path.join(siteRoot, 'site-config', 'settings.yaml'));
121
115
  if (!config) return null;
122
116
  const requestedMode = config.navigation?.mode ?? 'automatic';
123
117
  if (requestedMode !== 'automatic') return requestedMode;
@@ -148,30 +142,33 @@ const findNestedYamlPropertyLine = (source, parentKey, propertyKey) => {
148
142
  return 1;
149
143
  };
150
144
 
151
- export const getThemeDiagnostics = async ({ documentPath, source }) => {
152
- let theme;
145
+ export const getThemeDiagnostics = async ({ siteRoot, documentPath, source, sources = new Map() }) => {
146
+ // Parse errors are reported by the YAML provider/schema validator.
147
+ try { const value = load(source); if (value && (typeof value !== 'object' || Array.isArray(value))) return []; }
148
+ catch { return []; }
149
+ siteRoot ??= await findNornaSiteRoot(documentPath);
150
+ if (!siteRoot) return [];
151
+ const location = getSourcePageLocation(siteRoot, path.dirname(documentPath));
152
+ if (!location) return [];
153
153
  try {
154
- theme = load(source) ?? {};
155
- } catch {
154
+ const overlays = new Map(sources); overlays.set(documentPath, source);
155
+ const { treeTheme, selected } = await selectPageTheme({
156
+ siteRoot, pageDirectory: location.pageDirectory, sources: overlays,
157
+ includePageTheme: path.basename(documentPath) === 'page-theme.yaml',
158
+ });
159
+ const theme = path.basename(documentPath) === 'tree-theme.yaml' ? treeTheme : selected;
160
+ assertSectionBackgroundPatternCompatibility(theme.config, documentPath, await getNavigationModeForEditor(siteRoot));
156
161
  return [];
162
+ } catch (error) {
163
+ const surface = error.message.includes('backgroundPattern');
164
+ return [{ code: surface ? 'tree-section-background-pattern' : 'invalid-resolved-theme',
165
+ line: surface ? findNestedYamlPropertyLine(source, 'sections', 'backgroundPattern') : 1,
166
+ message: error.message, severity: 'error' }];
157
167
  }
158
- if (!theme || typeof theme !== 'object' || Array.isArray(theme)) return [];
159
-
160
- const backgroundPattern = theme.sections?.backgroundPattern;
161
- if (backgroundPattern === undefined || backgroundPattern === 'uniform') return [];
162
- const siteRoot = await findNornaSiteRoot(documentPath);
163
- if (!siteRoot || await getNavigationModeForEditor(siteRoot) !== 'tree') return [];
164
-
165
- return [{
166
- code: 'tree-section-background-pattern',
167
- line: findNestedYamlPropertyLine(source, 'sections', 'backgroundPattern'),
168
- message: `sections.backgroundPattern "${backgroundPattern}" cannot be used because this site resolves to tree navigation. Tree navigation uses one uniform reading surface so the navigation rail and page content remain distinct. Remove sections.backgroundPattern or set it to uniform.`,
169
- severity: 'error',
170
- }];
171
168
  };
172
169
 
173
170
  const readSitewideLogoForEditor = async (siteRoot) => {
174
- const filename = await findFile(siteRoot, ['sitewide-content.yaml']);
171
+ const filename = await findFile(siteRoot, ['site-config/shared-content.yaml']);
175
172
  if (!filename) return null;
176
173
  const absolutePath = path.join(siteRoot, filename);
177
174
  const source = await readFile(absolutePath, 'utf8');
@@ -262,13 +259,14 @@ export const getSitePublicAssetStatus = async (documentPath) => {
262
259
  const getPageContext = (siteRoot, documentPath) => {
263
260
  const absoluteDocumentPath = path.resolve(documentPath);
264
261
  const relativePath = toPosixPath(path.relative(siteRoot, absoluteDocumentPath));
265
- const match = relativePath.match(/^pages\/(.+)\/content\.md$/);
262
+ if (relativePath === 'root/content.md') return { contentPath: absoluteDocumentPath, imagesRoot: getSiteSourcePaths(siteRoot).images, pageLabel: 'Home', pageDirectory: homePageDirectory };
263
+ const match = relativePath.match(/^root\/pages\/(.+)\/content\.md$/);
266
264
  if (!match) return null;
267
265
 
268
266
  const pageDirectory = match[1];
269
267
  return {
270
268
  contentPath: absoluteDocumentPath,
271
- imagesRoot: path.join(siteRoot, 'pages', ...pageDirectory.split('/'), 'images'),
269
+ imagesRoot: path.join(getSiteSourcePaths(siteRoot).pages, ...pageDirectory.split('/'), 'images'),
272
270
  pageLabel: pageDirectory,
273
271
  pageDirectory,
274
272
  };
@@ -492,7 +490,7 @@ const collectPageImageRoots = async (directory, siteRoot, roots) => {
492
490
  if (!entry.isDirectory()) continue;
493
491
  const absolutePath = path.join(directory, entry.name);
494
492
  if (entry.name === 'images') {
495
- const pageDirectory = toPosixPath(path.relative(path.join(siteRoot, 'pages'), path.dirname(absolutePath)));
493
+ const pageDirectory = toPosixPath(path.relative(getSiteSourcePaths(siteRoot).pages, path.dirname(absolutePath)));
496
494
  roots.push({ imageRoot: absolutePath, pageLabel: pageDirectory });
497
495
  continue;
498
496
  }
@@ -515,7 +513,8 @@ const getContentFiles = async (siteRoot) => {
515
513
  }
516
514
  };
517
515
 
518
- await visit(path.join(siteRoot, 'pages'));
516
+ if (await fileExists(getSiteSourcePaths(siteRoot).content)) files.push(getSiteSourcePaths(siteRoot).content);
517
+ await visit(getSiteSourcePaths(siteRoot).pages);
519
518
  return files;
520
519
  };
521
520
 
@@ -534,8 +533,8 @@ const getReferencesByFilename = async (siteRoot) => {
534
533
 
535
534
  export const createSiteImageIndex = async (siteRoot) => {
536
535
  const files = [];
537
- const pageRoots = [];
538
- await collectPageImageRoots(path.join(siteRoot, 'pages'), siteRoot, pageRoots);
536
+ const pageRoots = [{ imageRoot: getSiteSourcePaths(siteRoot).images, pageLabel: 'Home' }];
537
+ await collectPageImageRoots(getSiteSourcePaths(siteRoot).pages, siteRoot, pageRoots);
539
538
  for (const root of pageRoots) {
540
539
  await collectImageFiles(root.imageRoot, root.imageRoot, root.pageLabel, files);
541
540
  }
@@ -566,7 +565,9 @@ const getEditorImageReferences = (source) => {
566
565
  }
567
566
  for (const child of node.children ?? []) visit(child);
568
567
  };
569
- visit(markdownToMdast(source, { features: { gfm: true, frontmatter: true } }));
568
+ const tree = markdownToMdast(source, { features: { gfm: true, frontmatter: true } });
569
+ visit(tree);
570
+ for (const reference of extractMarkdownImageReferences({ source, tree })) if (reference.image) names.add(reference.image);
570
571
  return names;
571
572
  };
572
573
 
@@ -618,10 +619,17 @@ export const getImageCompletionContext = async ({ documentPath, source, line })
618
619
  };
619
620
  };
620
621
 
621
- export const getImageDefinitionContext = async ({ documentPath, source, line }) => {
622
+ export const getImageDefinitionContext = async ({ documentPath, source, line, character = 0 }) => {
622
623
  const context = getNornaBlockFieldContext({ source, line });
623
- if (context?.key !== 'image' || typeof context.value !== 'string') return null;
624
- const filename = context.value;
624
+ let filename = context?.key === 'image' && typeof context.value === 'string' ? context.value : null;
625
+ const markdown = !filename;
626
+ if (markdown) {
627
+ const document = await parsePageMarkdownSource(source);
628
+ const offset = source.split('\n').slice(0, line).reduce((count, text) => count + text.length + 1, 0) + character;
629
+ const reference = document.markdownImages.find((ref) => [ref.range, ref.targetRange].some(range => range && offset >= range.start && offset < range.end));
630
+ filename = reference?.image;
631
+ }
632
+ if (!filename) return null;
625
633
  if (!/^[a-z0-9][a-z0-9.-]*\.(jpe?g|png|svg)$/i.test(filename)) return null;
626
634
  const siteRoot = await findNornaSiteRoot(documentPath);
627
635
  const page = siteRoot ? getPageContext(siteRoot, documentPath) : null;
@@ -629,6 +637,7 @@ export const getImageDefinitionContext = async ({ documentPath, source, line })
629
637
 
630
638
  const expectedPath = path.join(page.imagesRoot, filename);
631
639
  if (await fileExists(expectedPath)) return { files: [expectedPath], filename, siteRoot };
640
+ if (markdown) return { files: [], filename, siteRoot };
632
641
 
633
642
  const index = await createSiteImageIndex(siteRoot);
634
643
  return {
@@ -680,14 +689,6 @@ export const getMarkdownDiagnostics = async ({ documentPath, source }) => {
680
689
  : diagnostic.message,
681
690
  })),
682
691
  ];
683
- for (const reference of document.markdownImages) {
684
- diagnostics.push({
685
- code: 'local-markdown-image',
686
- severity: 'warning',
687
- line: reference.line,
688
- message: `Local Markdown image "${reference.target}" is not managed by Norna. Use image-stack, image-carousel, or card-list for validated and synchronized site images.`,
689
- });
690
- }
691
692
 
692
693
  const siteRoot = await findNornaSiteRoot(documentPath);
693
694
  const pageContext = siteRoot ? getPageContext(siteRoot, documentPath) : null;
@@ -705,16 +706,16 @@ export const getMarkdownDiagnostics = async ({ documentPath, source }) => {
705
706
  code: 'missing-image',
706
707
  severity: 'error',
707
708
  line: reference.line,
708
- message: `Image "${filename}" was not found in this page's images folder or elsewhere in this Norna site.`,
709
+ message: `Image "${filename}" was not found at ${toPosixPath(expectedPath)}.`,
709
710
  });
710
711
  continue;
711
712
  }
712
713
  if (candidates.length > 1) {
713
714
  diagnostics.push({
714
715
  code: 'ambiguous-image',
715
- severity: 'warning',
716
+ severity: 'error',
716
717
  line: reference.line,
717
- message: `Image "${filename}" is ambiguous. Found: ${candidates.map((candidate) => toPosixPath(path.relative(siteRoot, candidate.absolutePath))).join(', ')}.`,
718
+ message: `Image "${filename}" is missing at ${toPosixPath(expectedPath)}. Repair candidates: ${candidates.map((candidate) => toPosixPath(path.relative(siteRoot, candidate.absolutePath))).join(', ')}.`,
718
719
  });
719
720
  continue;
720
721
  }
@@ -729,11 +730,11 @@ export const getMarkdownDiagnostics = async ({ documentPath, source }) => {
729
730
  imagePath: candidate.absolutePath,
730
731
  siteRoot,
731
732
  },
732
- severity: 'warning',
733
+ severity: 'error',
733
734
  line: reference.line,
734
- message: otherReferences.length > 0
735
+ message: `Image "${filename}" is missing at ${toPosixPath(expectedPath)}. ` + (otherReferences.length > 0
735
736
  ? `Image "${filename}" is stored at ${toPosixPath(path.relative(siteRoot, candidate.absolutePath))} and is still referenced by ${otherReferences.join(', ')}. Norna cannot relocate it safely.`
736
- : `Image "${filename}" is stored at ${toPosixPath(path.relative(siteRoot, candidate.absolutePath))}. Run "norna content:sync" to relocate it to this page's images folder.`,
737
+ : `Image "${filename}" is stored at ${toPosixPath(path.relative(siteRoot, candidate.absolutePath))}. Run "norna content:sync" to relocate it to this page's images folder.`),
737
738
  });
738
739
  }
739
740