@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.
- package/README.md +41 -29
- package/astro.config.mjs +33 -3
- package/bin/norna-cli.mjs +5 -3
- package/package.json +18 -12
- package/schemas/card-list.schema.json +1 -1
- package/schemas/config.schema.json +81 -66
- package/schemas/content-frontmatter.schema.json +23 -7
- package/schemas/manifest.json +49 -3
- package/schemas/page-theme.schema.json +1585 -41
- package/schemas/root-theme.schema.json +1767 -0
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +243 -243
- package/scripts/add-site-node.mjs +31 -250
- package/scripts/build-site.mjs +11 -6
- package/scripts/check-config.mjs +7 -23
- package/scripts/dev-local.mjs +51 -17
- package/scripts/export-theme-preset.mjs +2 -2
- package/scripts/generate-images.mjs +89 -70
- package/scripts/generate-schemas.mjs +9 -6
- package/scripts/generate-search-index.mjs +3 -2
- package/scripts/lib/attachment-render.mjs +23 -0
- package/scripts/lib/category-destinations.mjs +13 -12
- package/scripts/lib/dev-server-identity.mjs +30 -0
- package/scripts/lib/documentation-links.mjs +1 -1
- package/scripts/lib/documentation-routes.json +70 -62
- package/scripts/lib/docusaurus-migration-audit.mjs +15 -12
- package/scripts/lib/editor-attachments.mjs +69 -0
- package/scripts/lib/editor-file-policy.mjs +20 -0
- package/scripts/lib/editor-language-service.mjs +74 -73
- package/scripts/lib/editor-page-addresses.mjs +57 -0
- package/scripts/lib/editor-page-placement.mjs +102 -0
- package/scripts/lib/editor-resource-actions.mjs +216 -0
- package/scripts/lib/editor-site-editing-tree.mjs +181 -0
- package/scripts/lib/editor-site-files.mjs +154 -0
- package/scripts/lib/editor-site-links.mjs +81 -0
- package/scripts/lib/editor-site-preview.mjs +149 -0
- package/scripts/lib/editor-site-tree.mjs +158 -0
- package/scripts/lib/editor-source-files.mjs +87 -0
- package/scripts/lib/editor-theme-help.mjs +43 -0
- package/scripts/lib/locale-registry.mjs +1 -1
- package/scripts/lib/locales/en.mjs +1 -1
- package/scripts/lib/logo-assets.mjs +2 -1
- package/scripts/lib/markdown-images.mjs +59 -0
- package/scripts/lib/markdown-links.mjs +29 -9
- package/scripts/lib/navigation-model.mjs +2 -44
- package/scripts/lib/navigation-review.mjs +14 -50
- package/scripts/lib/norna-markdown-blocks.mjs +1 -58
- package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
- package/scripts/lib/page-aliases.mjs +8 -1
- package/scripts/lib/page-attachments.mjs +59 -0
- package/scripts/lib/page-markdown.mjs +11 -7
- package/scripts/lib/page-model.mjs +19 -36
- package/scripts/lib/page-move-apply.mjs +119 -0
- package/scripts/lib/page-move-plan.mjs +64 -78
- package/scripts/lib/page-order-source.mjs +22 -0
- package/scripts/lib/page-order.mjs +45 -0
- package/scripts/lib/page-placement-cli.mjs +50 -0
- package/scripts/lib/presentation.mjs +3 -3
- package/scripts/lib/project-config.mjs +10 -8
- package/scripts/lib/publication-paths.mjs +72 -0
- package/scripts/lib/schema-definitions.mjs +15 -38
- package/scripts/lib/schema-editor-metadata.mjs +16 -60
- package/scripts/lib/schema-value-definitions.mjs +1 -1
- package/scripts/lib/site-content.mjs +8 -25
- package/scripts/lib/site-conventions.mjs +19 -1
- package/scripts/lib/site-link-graph.mjs +88 -18
- package/scripts/lib/site-navigation-tree.mjs +2 -7
- package/scripts/lib/site-node-create.mjs +182 -0
- package/scripts/lib/site-paths.mjs +24 -12
- package/scripts/lib/site-source-format.mjs +12 -0
- package/scripts/lib/site-structure.mjs +86 -95
- package/scripts/lib/site-upgrade.mjs +32 -0
- package/scripts/lib/social-image-assets.mjs +2 -1
- package/scripts/lib/source-files.mjs +34 -0
- package/scripts/lib/structured-blocks.mjs +3 -1
- package/scripts/lib/theme-config.mjs +26 -68
- package/scripts/lib/theme-packages.mjs +47 -0
- package/scripts/lib/theme-presets.mjs +20 -53
- package/scripts/lib/typography.mjs +2 -2
- package/scripts/lib/yaml-config.mjs +1 -1
- package/scripts/lib/yaml-source-edit.mjs +94 -0
- package/scripts/move-site-page.mjs +46 -254
- package/scripts/show-typography.mjs +32 -17
- package/scripts/sync-content-sections.mjs +17 -17
- package/scripts/sync-site-public.mjs +44 -66
- package/scripts/upgrade-site.mjs +17 -0
- package/src/components/AreaNavigationMenu.astro +24 -0
- package/src/components/AreaNavigationPanel.astro +55 -0
- package/src/components/AreaNavigationSwitcher.astro +30 -0
- package/src/components/ContentBlocks.astro +2 -1
- package/src/components/ImageStack.astro +7 -5
- package/src/components/ImageStackEnhancement.astro +0 -7
- package/src/components/NavigationContinuity.astro +26 -0
- package/src/components/NavigationPageTree.astro +13 -4
- package/src/components/NavigationState.astro +11 -0
- package/src/components/NavigationTreeControls.astro +3 -2
- package/src/components/SearchPage.astro +11 -2
- package/src/components/SectionNavigationScript.astro +106 -26
- package/src/components/SiteNavigation.astro +45 -20
- package/src/components/SitePage.astro +38 -61
- package/src/components/SiteSection.astro +1 -0
- package/src/components/SiteTreeNavigation.astro +38 -18
- package/src/components/TopPageMenu.astro +4 -3
- package/src/components/TreeNavigationScript.astro +50 -57
- package/src/content.config.ts +11 -12
- package/src/layouts/BaseLayout.astro +15 -39
- package/src/lib/areaNavigation.ts +42 -0
- package/src/lib/imagePresentation.ts +3 -0
- package/src/lib/markdownImages.ts +101 -0
- package/src/lib/navigationContinuity.ts +212 -0
- package/src/lib/navigationFollowing.ts +28 -18
- package/src/lib/navigationState.mjs +88 -0
- package/src/lib/navigationStateScript.mjs +76 -0
- package/src/lib/navigationVisibility.ts +9 -0
- package/src/lib/pageThemes.ts +5 -42
- package/src/lib/sectionContent.ts +67 -21
- package/src/lib/siteNavigation.ts +6 -0
- package/src/lib/sitePages.ts +13 -36
- package/src/lib/sitePublicAssets.ts +2 -1
- package/src/pages/404.astro +1 -1
- package/src/pages/[...slug].astro +7 -16
- package/src/pages/index.astro +1 -3
- package/src/styles/area-navigation.css +408 -0
- package/src/styles/content.css +8 -29
- package/src/styles/media.css +24 -0
- package/src/styles/navigation-continuity.css +163 -0
- package/src/styles/page-layout.css +57 -68
- package/src/styles/responsive.css +0 -4
- package/starters/basic/README.md +9 -8
- package/starters/basic/site/{pages/000-home → root}/content.md +2 -2
- package/starters/basic/site/{config.yaml → site-config/settings.yaml} +1 -0
- package/schemas/category.schema.json +0 -20
- package/src/components/PageContentsNavigation.astro +0 -20
- /package/starters/basic/site/{pages/000-home → root}/images/.gitkeep +0 -0
- /package/starters/basic/site/{theme.yaml → root/tree-theme.yaml} +0 -0
- /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
|
@@ -1,58 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sources": {
|
|
3
|
-
"
|
|
4
|
-
"site/
|
|
5
|
-
"site/
|
|
6
|
-
"site/
|
|
7
|
-
"site/
|
|
8
|
-
"site/
|
|
9
|
-
"site/
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"configuration/
|
|
14
|
-
"configuration/
|
|
15
|
-
"configuration/
|
|
16
|
-
"configuration/
|
|
17
|
-
"configuration/
|
|
18
|
-
"configuration/
|
|
19
|
-
"configuration/
|
|
20
|
-
"configuration/
|
|
21
|
-
"configuration/
|
|
22
|
-
"configuration/
|
|
23
|
-
"configuration/
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"content/
|
|
28
|
-
"content/
|
|
29
|
-
"content/
|
|
30
|
-
"content/
|
|
31
|
-
"content/
|
|
32
|
-
"content/
|
|
33
|
-
"content/
|
|
34
|
-
"content/
|
|
35
|
-
"content/
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"commands/
|
|
42
|
-
"commands/
|
|
43
|
-
"commands/
|
|
44
|
-
"commands/
|
|
45
|
-
"commands/
|
|
46
|
-
"commands/
|
|
47
|
-
"commands/
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
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/#
|
|
82
|
-
"pages.md#choose-a-section-page-or-category": "site/pages/#
|
|
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/#
|
|
85
|
-
"pages.md#opening-a-category-url": "site/pages/#
|
|
86
|
-
"pages.md#reachable-content-and-stable-links": "site/pages/#
|
|
87
|
-
"pages.md#nested-pages": "site/pages/#
|
|
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/#
|
|
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/#
|
|
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/#
|
|
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:
|
|
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: '
|
|
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
|
-
|
|
588
|
-
await
|
|
589
|
-
await
|
|
590
|
-
await writeFile(path.join(resolvedReportDir, '
|
|
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(
|
|
595
|
+
const categoryDir = path.join(source.pages, 'problems');
|
|
593
596
|
await mkdir(path.join(categoryDir, 'pages'), { recursive: true });
|
|
594
|
-
await writeFile(path.join(categoryDir, '
|
|
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',
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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, ['
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
624
|
-
const
|
|
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
|
|
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: '
|
|
716
|
+
severity: 'error',
|
|
716
717
|
line: reference.line,
|
|
717
|
-
message: `Image "${filename}" is
|
|
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: '
|
|
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
|
|