@janga/norna 0.7.28 → 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 (77) hide show
  1. package/README.md +8 -5
  2. package/astro.config.mjs +14 -2
  3. package/package.json +10 -8
  4. package/schemas/config.schema.json +80 -65
  5. package/schemas/content-frontmatter.schema.json +15 -8
  6. package/schemas/manifest.json +1 -1
  7. package/schemas/page-theme.schema.json +237 -237
  8. package/schemas/root-theme.schema.json +237 -237
  9. package/schemas/sitewide-content.schema.json +18 -18
  10. package/schemas/theme.schema.json +237 -237
  11. package/scripts/add-site-node.mjs +29 -100
  12. package/scripts/dev-local.mjs +34 -7
  13. package/scripts/generate-images.mjs +83 -65
  14. package/scripts/generate-search-index.mjs +3 -2
  15. package/scripts/lib/attachment-render.mjs +23 -0
  16. package/scripts/lib/dev-server-identity.mjs +30 -0
  17. package/scripts/lib/documentation-routes.json +63 -62
  18. package/scripts/lib/docusaurus-migration-audit.mjs +9 -9
  19. package/scripts/lib/editor-attachments.mjs +69 -0
  20. package/scripts/lib/editor-file-policy.mjs +2 -0
  21. package/scripts/lib/editor-language-service.mjs +21 -18
  22. package/scripts/lib/editor-page-addresses.mjs +5 -3
  23. package/scripts/lib/editor-page-placement.mjs +28 -98
  24. package/scripts/lib/editor-resource-actions.mjs +216 -0
  25. package/scripts/lib/editor-site-editing-tree.mjs +44 -9
  26. package/scripts/lib/editor-site-files.mjs +3 -2
  27. package/scripts/lib/editor-site-links.mjs +6 -4
  28. package/scripts/lib/editor-site-preview.mjs +149 -0
  29. package/scripts/lib/editor-site-tree.mjs +10 -3
  30. package/scripts/lib/editor-source-files.mjs +1 -1
  31. package/scripts/lib/logo-assets.mjs +2 -1
  32. package/scripts/lib/markdown-images.mjs +59 -0
  33. package/scripts/lib/markdown-links.mjs +29 -9
  34. package/scripts/lib/norna-markdown-blocks.mjs +0 -57
  35. package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
  36. package/scripts/lib/page-aliases.mjs +5 -0
  37. package/scripts/lib/page-attachments.mjs +59 -0
  38. package/scripts/lib/page-markdown.mjs +11 -7
  39. package/scripts/lib/page-model.mjs +8 -22
  40. package/scripts/lib/page-move-apply.mjs +6 -4
  41. package/scripts/lib/page-move-plan.mjs +37 -19
  42. package/scripts/lib/page-order-source.mjs +22 -0
  43. package/scripts/lib/page-order.mjs +45 -0
  44. package/scripts/lib/page-placement-cli.mjs +50 -0
  45. package/scripts/lib/project-config.mjs +3 -0
  46. package/scripts/lib/publication-paths.mjs +72 -0
  47. package/scripts/lib/schema-definitions.mjs +4 -0
  48. package/scripts/lib/schema-editor-metadata.mjs +4 -1
  49. package/scripts/lib/site-content.mjs +1 -1
  50. package/scripts/lib/site-link-graph.mjs +88 -18
  51. package/scripts/lib/site-node-create.mjs +47 -34
  52. package/scripts/lib/site-paths.mjs +2 -2
  53. package/scripts/lib/site-source-format.mjs +12 -0
  54. package/scripts/lib/site-structure.mjs +14 -13
  55. package/scripts/lib/site-upgrade.mjs +2 -0
  56. package/scripts/lib/social-image-assets.mjs +2 -1
  57. package/scripts/lib/source-files.mjs +1 -1
  58. package/scripts/lib/structured-blocks.mjs +3 -1
  59. package/scripts/move-site-page.mjs +46 -133
  60. package/scripts/sync-content-sections.mjs +8 -10
  61. package/scripts/sync-site-public.mjs +44 -66
  62. package/src/components/ContentBlocks.astro +2 -1
  63. package/src/components/ImageStack.astro +7 -5
  64. package/src/components/SearchPage.astro +2 -1
  65. package/src/components/SitePage.astro +1 -0
  66. package/src/components/SiteSection.astro +1 -0
  67. package/src/content.config.ts +1 -4
  68. package/src/lib/imagePresentation.ts +3 -0
  69. package/src/lib/markdownImages.ts +101 -0
  70. package/src/lib/sectionContent.ts +64 -19
  71. package/src/lib/sitePages.ts +4 -30
  72. package/src/lib/sitePublicAssets.ts +2 -1
  73. package/src/pages/[...slug].astro +2 -1
  74. package/src/styles/area-navigation.css +14 -5
  75. package/src/styles/content.css +6 -0
  76. package/src/styles/media.css +24 -0
  77. package/starters/basic/site/site-config/settings.yaml +1 -0
@@ -1,65 +1,66 @@
1
1
  {
2
2
  "sources": {
3
- "": "site/root/pages/032-reference/content.md",
4
- "site/": "site/root/pages/032-reference/pages/010-site/content.md",
5
- "site/files/": "site/root/pages/032-reference/pages/010-site/pages/010-files/content.md",
6
- "site/pages/": "site/root/pages/032-reference/pages/010-site/pages/020-pages/content.md",
7
- "site/urls/": "site/root/pages/032-reference/pages/010-site/pages/030-urls/content.md",
8
- "site/metadata/": "site/root/pages/032-reference/pages/010-site/pages/040-metadata/content.md",
9
- "site/images/": "site/root/pages/032-reference/pages/010-site/pages/050-images/content.md",
10
- "site/public-files/": "site/root/pages/032-reference/pages/010-site/pages/060-public-files/content.md",
11
- "site/requirements/": "site/root/pages/032-reference/pages/010-site/pages/070-requirements/content.md",
12
- "configuration/": "site/root/pages/032-reference/pages/020-configuration/content.md",
13
- "configuration/site/": "site/root/pages/032-reference/pages/020-configuration/pages/010-site/content.md",
14
- "configuration/language/": "site/root/pages/032-reference/pages/020-configuration/pages/020-language/content.md",
15
- "configuration/navigation/": "site/root/pages/032-reference/pages/020-configuration/pages/030-navigation/content.md",
16
- "configuration/search/": "site/root/pages/032-reference/pages/020-configuration/pages/040-search/content.md",
17
- "configuration/source-links/": "site/root/pages/032-reference/pages/020-configuration/pages/050-source-links/content.md",
18
- "configuration/theme/": "site/root/pages/032-reference/pages/020-configuration/pages/060-theme/content.md",
19
- "configuration/presets/": "site/root/pages/032-reference/pages/020-configuration/pages/065-presets/content.md",
20
- "configuration/palettes/": "site/root/pages/032-reference/pages/020-configuration/pages/070-palettes/content.md",
21
- "configuration/appearance/": "site/root/pages/032-reference/pages/020-configuration/pages/080-appearance/content.md",
22
- "configuration/layout/": "site/root/pages/032-reference/pages/020-configuration/pages/090-layout/content.md",
23
- "configuration/images/": "site/root/pages/032-reference/pages/020-configuration/pages/100-images/content.md",
24
- "configuration/typography/": "site/root/pages/032-reference/pages/020-configuration/pages/110-typography/content.md",
25
- "configuration/sections/": "site/root/pages/032-reference/pages/020-configuration/pages/120-sections/content.md",
26
- "configuration/shared-content/": "site/root/pages/032-reference/pages/020-configuration/pages/130-shared-content/content.md",
27
- "content/": "site/root/pages/032-reference/pages/030-content/content.md",
28
- "content/markdown/": "site/root/pages/032-reference/pages/030-content/pages/010-markdown/content.md",
29
- "content/structured-blocks/": "site/root/pages/032-reference/pages/030-content/pages/015-structured-blocks/content.md",
30
- "content/images/": "site/root/pages/032-reference/pages/030-content/pages/020-images/content.md",
31
- "content/cards/": "site/root/pages/032-reference/pages/030-content/pages/030-cards/content.md",
32
- "content/child-lists/": "site/root/pages/032-reference/pages/030-content/pages/040-child-lists/content.md",
33
- "content/callouts/": "site/root/pages/032-reference/pages/030-content/pages/050-callouts/content.md",
34
- "content/tabs/": "site/root/pages/032-reference/pages/030-content/pages/060-tabs/content.md",
35
- "content/details/": "site/root/pages/032-reference/pages/030-content/pages/070-details/content.md",
36
- "content/tables/": "site/root/pages/032-reference/pages/030-content/pages/080-tables/content.md",
37
- "content/code/": "site/root/pages/032-reference/pages/030-content/pages/090-code/content.md",
38
- "content/sidenotes/": "site/root/pages/032-reference/pages/030-content/pages/100-sidenotes/content.md",
39
- "content/footnotes/": "site/root/pages/032-reference/pages/030-content/pages/110-footnotes/content.md",
40
- "commands/": "site/root/pages/032-reference/pages/040-commands/content.md",
41
- "commands/invocation/": "site/root/pages/032-reference/pages/040-commands/pages/010-invocation/content.md",
42
- "commands/init/": "site/root/pages/032-reference/pages/040-commands/pages/020-init/content.md",
43
- "commands/create/": "site/root/pages/032-reference/pages/040-commands/pages/030-create/content.md",
44
- "commands/move/": "site/root/pages/032-reference/pages/040-commands/pages/040-move/content.md",
45
- "commands/validate/": "site/root/pages/032-reference/pages/040-commands/pages/050-validate/content.md",
46
- "commands/navigation/": "site/root/pages/032-reference/pages/040-commands/pages/055-navigation/content.md",
47
- "commands/development/": "site/root/pages/032-reference/pages/040-commands/pages/060-development/content.md",
48
- "commands/build/": "site/root/pages/032-reference/pages/040-commands/pages/070-build/content.md",
49
- "commands/publishing/": "site/root/pages/032-reference/pages/040-commands/pages/080-publishing/content.md",
50
- "commands/theme/": "site/root/pages/032-reference/pages/040-commands/pages/090-theme/content.md",
51
- "commands/engine/": "site/root/pages/032-reference/pages/040-commands/pages/100-engine/content.md",
52
- "commands/migration-audit/": "site/root/pages/032-reference/pages/040-commands/pages/110-migration-audit/content.md",
53
- "reader/": "site/root/pages/032-reference/pages/050-reader/content.md",
54
- "reader/display/": "site/root/pages/032-reference/pages/050-reader/pages/010-display/content.md",
55
- "reader/accessibility/": "site/root/pages/032-reference/pages/050-reader/pages/020-accessibility/content.md",
56
- "workflows/": "site/root/pages/032-reference/pages/060-workflows/content.md",
57
- "workflows/editor/": "site/root/pages/032-reference/pages/060-workflows/pages/010-editor/content.md",
58
- "workflows/editor-ai-suggestions/": "site/root/pages/032-reference/pages/060-workflows/pages/015-editor-ai-suggestions/content.md",
59
- "workflows/publishing/": "site/root/pages/032-reference/pages/060-workflows/pages/020-publishing/content.md",
60
- "workflows/embedded-publishing/": "site/root/pages/032-reference/pages/060-workflows/pages/030-embedded-publishing/content.md",
61
- "workflows/upgrading/": "site/root/pages/032-reference/pages/060-workflows/pages/040-upgrading/content.md",
62
- "workflows/legacy-source/": "site/root/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"
63
64
  },
64
65
  "legacy": {
65
66
  "site-files.md": "site/files/",
@@ -155,10 +156,10 @@
155
156
  "public-files.md#navigation-logo": "site/public-files/#recognized-files",
156
157
  "public-files.md#browser-icons": "site/public-files/#recognized-files",
157
158
  "public-files.md#social-sharing-image": "site/public-files/#social-previews",
158
- "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",
159
160
  "public-files.md#generated-sitemap": "site/public-files/#generated-sitemap",
160
161
  "public-files.md#generated-404-page": "site/public-files/#generated-404-page",
161
- "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",
162
163
  "public-files.md#generated-copy": "site/public-files/#preparation-and-reserved-paths",
163
164
  "theme.md#theme-presets": "configuration/presets/",
164
165
  "theme.md#choose-a-preset": "configuration/theme/#presets-and-root-overrides",
@@ -313,11 +313,11 @@ const inspectPage = async ({ absolutePath, relativePath, root }) => {
313
313
  if (node.type === 'code') codeRanges.push(nodeRange(node));
314
314
  if (node.type === 'image') {
315
315
  collector.add({
316
- 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.",
317
317
  classification: 'rewrite',
318
318
  code: 'managed-image-rewrite',
319
319
  line: nodeRange(node).line,
320
- 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.',
321
321
  });
322
322
  }
323
323
  if (node.type === 'html') inspectHtmlNode(node, collector);
@@ -560,12 +560,12 @@ const renderReportHome = (report) => {
560
560
  + (problemRows || '| None | No findings | 0 | copy |') + '\n';
561
561
  };
562
562
 
563
- const renderProblemPage = (report, problem) => {
563
+ const renderProblemPage = (report, problem, order) => {
564
564
  const rows = problem.sample.map((occurrence) => {
565
565
  const link = sourceLink(report, occurrence);
566
566
  return '| ' + occurrence.pageTitle.replaceAll('|', '\\|') + ' | ' + codeMark + occurrence.sourceFile + codeMark + ' | ' + occurrence.line + ' | ' + (link ? '[Open source](' + link + ')' : 'Unavailable') + ' |';
567
567
  }).join('\n');
568
- return '---\npage:\n description: ' + problemDescription(problem) + '.\n---\n\n'
568
+ return '---\nnavigation:\n order: ' + order + '\npage:\n description: ' + problemDescription(problem) + '.\n---\n\n'
569
569
  + '# ' + problem.id + '\n\n'
570
570
  + problem.message + '\n\n'
571
571
  + 'Classification: **' + problem.classification + '**. Occurrences: **' + problem.occurrences + '**.\n\n'
@@ -588,17 +588,17 @@ export const writeMigrationReport = async (report, reportDir, { sourceRoot = nul
588
588
  const source = getSiteSourcePaths(resolvedReportDir);
589
589
  await mkdir(source.pages, { recursive: true });
590
590
  await mkdir(path.join(resolvedReportDir, 'site-config'), { recursive: true });
591
- await writeFile(path.join(resolvedReportDir, 'site-config/settings.yaml'), 'url: https://example.invalid/\n');
591
+ await writeFile(path.join(resolvedReportDir, 'site-config/settings.yaml'), 'sourceFormat: 2\nurl: https://example.invalid/\n');
592
592
  await writeFile(path.join(resolvedReportDir, 'root', 'tree-theme.yaml'), 'preset: documentation\n');
593
593
  await writeFile(source.content, renderReportHome(report));
594
594
  if (report.problemTypes.length > 0) {
595
- const categoryDir = path.join(source.pages, '010-problems');
595
+ const categoryDir = path.join(source.pages, 'problems');
596
596
  await mkdir(path.join(categoryDir, 'pages'), { recursive: true });
597
- await writeFile(path.join(categoryDir, 'content.md'), '---\npage:\n listChildren: true\n---\n\n# Problems\n');
597
+ await writeFile(path.join(categoryDir, 'content.md'), '---\nnavigation:\n order: 1000\npage:\n listChildren: true\n---\n\n# Problems\n');
598
598
  for (const [index, problem] of report.problemTypes.entries()) {
599
- const pageDir = path.join(categoryDir, 'pages', String((index + 1) * 10).padStart(3, '0') + '-' + problemPageId(problem));
599
+ const pageDir = path.join(categoryDir, 'pages', problemPageId(problem));
600
600
  await mkdir(pageDir, { recursive: true });
601
- await writeFile(path.join(pageDir, 'content.md'), renderProblemPage(report, problem));
601
+ await writeFile(path.join(pageDir, 'content.md'), renderProblemPage(report, problem, (index + 1) * 1000));
602
602
  }
603
603
  }
604
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
+ };
@@ -1,5 +1,6 @@
1
1
  import { getSiteSourcePaths } from './site-conventions.mjs';
2
2
  import path from 'node:path';
3
+ import { getSourcePageLocation } from './source-files.mjs';
3
4
  import { getSourceFileDefinition } from './source-files.mjs';
4
5
  import { isInside } from './editor-site-links.mjs';
5
6
 
@@ -14,5 +15,6 @@ export const editorFileRemovalPolicy = ({ siteRoot, sourcePath, filePath }) => {
14
15
  if (siteOwner && isInside(path.join(siteRoot, 'public'), filePath)) return {
15
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.',
16
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.' };
17
19
  return null;
18
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';
@@ -564,7 +565,9 @@ const getEditorImageReferences = (source) => {
564
565
  }
565
566
  for (const child of node.children ?? []) visit(child);
566
567
  };
567
- 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);
568
571
  return names;
569
572
  };
570
573
 
@@ -616,10 +619,17 @@ export const getImageCompletionContext = async ({ documentPath, source, line })
616
619
  };
617
620
  };
618
621
 
619
- export const getImageDefinitionContext = async ({ documentPath, source, line }) => {
622
+ export const getImageDefinitionContext = async ({ documentPath, source, line, character = 0 }) => {
620
623
  const context = getNornaBlockFieldContext({ source, line });
621
- if (context?.key !== 'image' || typeof context.value !== 'string') return null;
622
- 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;
623
633
  if (!/^[a-z0-9][a-z0-9.-]*\.(jpe?g|png|svg)$/i.test(filename)) return null;
624
634
  const siteRoot = await findNornaSiteRoot(documentPath);
625
635
  const page = siteRoot ? getPageContext(siteRoot, documentPath) : null;
@@ -627,6 +637,7 @@ export const getImageDefinitionContext = async ({ documentPath, source, line })
627
637
 
628
638
  const expectedPath = path.join(page.imagesRoot, filename);
629
639
  if (await fileExists(expectedPath)) return { files: [expectedPath], filename, siteRoot };
640
+ if (markdown) return { files: [], filename, siteRoot };
630
641
 
631
642
  const index = await createSiteImageIndex(siteRoot);
632
643
  return {
@@ -678,14 +689,6 @@ export const getMarkdownDiagnostics = async ({ documentPath, source }) => {
678
689
  : diagnostic.message,
679
690
  })),
680
691
  ];
681
- for (const reference of document.markdownImages) {
682
- diagnostics.push({
683
- code: 'local-markdown-image',
684
- severity: 'warning',
685
- line: reference.line,
686
- 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.`,
687
- });
688
- }
689
692
 
690
693
  const siteRoot = await findNornaSiteRoot(documentPath);
691
694
  const pageContext = siteRoot ? getPageContext(siteRoot, documentPath) : null;
@@ -703,16 +706,16 @@ export const getMarkdownDiagnostics = async ({ documentPath, source }) => {
703
706
  code: 'missing-image',
704
707
  severity: 'error',
705
708
  line: reference.line,
706
- 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)}.`,
707
710
  });
708
711
  continue;
709
712
  }
710
713
  if (candidates.length > 1) {
711
714
  diagnostics.push({
712
715
  code: 'ambiguous-image',
713
- severity: 'warning',
716
+ severity: 'error',
714
717
  line: reference.line,
715
- 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(', ')}.`,
716
719
  });
717
720
  continue;
718
721
  }
@@ -727,11 +730,11 @@ export const getMarkdownDiagnostics = async ({ documentPath, source }) => {
727
730
  imagePath: candidate.absolutePath,
728
731
  siteRoot,
729
732
  },
730
- severity: 'warning',
733
+ severity: 'error',
731
734
  line: reference.line,
732
- message: otherReferences.length > 0
735
+ message: `Image "${filename}" is missing at ${toPosixPath(expectedPath)}. ` + (otherReferences.length > 0
733
736
  ? `Image "${filename}" is stored at ${toPosixPath(path.relative(siteRoot, candidate.absolutePath))} and is still referenced by ${otherReferences.join(', ')}. Norna cannot relocate it safely.`
734
- : `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.`),
735
738
  });
736
739
  }
737
740
 
@@ -6,6 +6,7 @@ import { createPageMovePlan } from './page-move-plan.mjs';
6
6
  import { applyPageMovePlan } from './page-move-apply.mjs';
7
7
 
8
8
  export const siteAddressApiVersion = 1;
9
+ export const sitePageAddressOptionsApiVersion = 1;
9
10
 
10
11
  export const getEditorPageAddresses = async (options) => {
11
12
  const { structure, graph, settings, incomplete } = await readEditorLinkState(options);
@@ -21,7 +22,8 @@ export const getEditorPageAddresses = async (options) => {
21
22
  };
22
23
  };
23
24
 
24
- export const planEditorPageAddress = async ({ siteRoot, sourcePath, segment, sources = new Map() }) => {
25
+ export const planEditorPageAddress = async ({ siteRoot, sourcePath, segment, preserveAliases = true, sources = new Map() }) => {
26
+ if (typeof preserveAliases !== 'boolean') throw new Error('Choose whether to preserve old addresses as aliases.');
25
27
  await checkedPath(siteRoot, sourcePath);
26
28
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment)) throw new Error('Use lowercase letters, numbers and single hyphens, for example install-norna. Enter one URL segment, without slashes.');
27
29
  const state = await readEditorLinkState({ siteRoot, sources });
@@ -32,7 +34,7 @@ export const planEditorPageAddress = async ({ siteRoot, sourcePath, segment, sou
32
34
  if (sourcePaths.some((filename) => sources.has(filename))) throw new Error('Save or undo unsaved page, category and site-settings edits before changing an address.');
33
35
  for (const filename of sourcePaths) await checkedPath(siteRoot, filename);
34
36
  const to = `/${node.parentPagePath ? node.parentPagePath + '/' : ''}${segment}/`;
35
- const plan = await createPageMovePlan({ from: `/${node.pagePath}/`, to,
37
+ const plan = await createPageMovePlan({ from: `/${node.pagePath}/`, to, preserveAliases,
36
38
  graph: state.graph, publicFiles: state.publicFiles, siteStructure: state.structure,
37
39
  sitePagesDir: getSiteSourcePaths(siteRoot).pages, sitePagesLabel: `${siteRoot}/root/pages`, generatedRoutes: state.generatedRoutes });
38
40
  await checkedPath(siteRoot, plan.destinationDirectory);
@@ -43,7 +45,7 @@ export const planEditorPageAddress = async ({ siteRoot, sourcePath, segment, sou
43
45
  return { ...plan, siteRoot, sourcePath, segment, generatedRoutes: state.generatedRoutes,
44
46
  webFrom: addresses.webAddress,
45
47
  webTo: new URL(to.slice(1), state.settings.url.replace(/\/$/, '') + '/').href,
46
- fingerprint: JSON.stringify([await snapshot(plan.sourceDirectory), snapshots, plan.to, plan.fileChanges.map(({ updatedSource }) => updatedSource)]) };
48
+ fingerprint: JSON.stringify([await snapshot(plan.sourceDirectory), snapshots, plan.to, preserveAliases, plan.fileChanges.map(({ updatedSource }) => updatedSource)]) };
47
49
  };
48
50
 
49
51
  export const applyEditorPageAddress = async (plan, { renameDirectory } = {}) => {