@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.
- package/README.md +8 -5
- package/astro.config.mjs +14 -2
- package/package.json +10 -8
- package/schemas/config.schema.json +80 -65
- package/schemas/content-frontmatter.schema.json +15 -8
- package/schemas/manifest.json +1 -1
- package/schemas/page-theme.schema.json +237 -237
- package/schemas/root-theme.schema.json +237 -237
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +237 -237
- package/scripts/add-site-node.mjs +29 -100
- package/scripts/dev-local.mjs +34 -7
- package/scripts/generate-images.mjs +83 -65
- package/scripts/generate-search-index.mjs +3 -2
- package/scripts/lib/attachment-render.mjs +23 -0
- package/scripts/lib/dev-server-identity.mjs +30 -0
- package/scripts/lib/documentation-routes.json +63 -62
- package/scripts/lib/docusaurus-migration-audit.mjs +9 -9
- package/scripts/lib/editor-attachments.mjs +69 -0
- package/scripts/lib/editor-file-policy.mjs +2 -0
- package/scripts/lib/editor-language-service.mjs +21 -18
- package/scripts/lib/editor-page-addresses.mjs +5 -3
- package/scripts/lib/editor-page-placement.mjs +28 -98
- package/scripts/lib/editor-resource-actions.mjs +216 -0
- package/scripts/lib/editor-site-editing-tree.mjs +44 -9
- package/scripts/lib/editor-site-files.mjs +3 -2
- package/scripts/lib/editor-site-links.mjs +6 -4
- package/scripts/lib/editor-site-preview.mjs +149 -0
- package/scripts/lib/editor-site-tree.mjs +10 -3
- package/scripts/lib/editor-source-files.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/norna-markdown-blocks.mjs +0 -57
- package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
- package/scripts/lib/page-aliases.mjs +5 -0
- package/scripts/lib/page-attachments.mjs +59 -0
- package/scripts/lib/page-markdown.mjs +11 -7
- package/scripts/lib/page-model.mjs +8 -22
- package/scripts/lib/page-move-apply.mjs +6 -4
- package/scripts/lib/page-move-plan.mjs +37 -19
- 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/project-config.mjs +3 -0
- package/scripts/lib/publication-paths.mjs +72 -0
- package/scripts/lib/schema-definitions.mjs +4 -0
- package/scripts/lib/schema-editor-metadata.mjs +4 -1
- package/scripts/lib/site-content.mjs +1 -1
- package/scripts/lib/site-link-graph.mjs +88 -18
- package/scripts/lib/site-node-create.mjs +47 -34
- package/scripts/lib/site-paths.mjs +2 -2
- package/scripts/lib/site-source-format.mjs +12 -0
- package/scripts/lib/site-structure.mjs +14 -13
- package/scripts/lib/site-upgrade.mjs +2 -0
- package/scripts/lib/social-image-assets.mjs +2 -1
- package/scripts/lib/source-files.mjs +1 -1
- package/scripts/lib/structured-blocks.mjs +3 -1
- package/scripts/move-site-page.mjs +46 -133
- package/scripts/sync-content-sections.mjs +8 -10
- package/scripts/sync-site-public.mjs +44 -66
- package/src/components/ContentBlocks.astro +2 -1
- package/src/components/ImageStack.astro +7 -5
- package/src/components/SearchPage.astro +2 -1
- package/src/components/SitePage.astro +1 -0
- package/src/components/SiteSection.astro +1 -0
- package/src/content.config.ts +1 -4
- package/src/lib/imagePresentation.ts +3 -0
- package/src/lib/markdownImages.ts +101 -0
- package/src/lib/sectionContent.ts +64 -19
- package/src/lib/sitePages.ts +4 -30
- package/src/lib/sitePublicAssets.ts +2 -1
- package/src/pages/[...slug].astro +2 -1
- package/src/styles/area-navigation.css +14 -5
- package/src/styles/content.css +6 -0
- package/src/styles/media.css +24 -0
- package/starters/basic/site/site-config/settings.yaml +1 -0
|
@@ -1,65 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sources": {
|
|
3
|
-
"": "site/root/pages/
|
|
4
|
-
"site/": "site/root/pages/
|
|
5
|
-
"site/files/": "site/root/pages/
|
|
6
|
-
"site/pages/": "site/root/pages/
|
|
7
|
-
"site/urls/": "site/root/pages/
|
|
8
|
-
"site/metadata/": "site/root/pages/
|
|
9
|
-
"site/images/": "site/root/pages/
|
|
10
|
-
"site/
|
|
11
|
-
"site/
|
|
12
|
-
"
|
|
13
|
-
"configuration/
|
|
14
|
-
"configuration/
|
|
15
|
-
"configuration/
|
|
16
|
-
"configuration/
|
|
17
|
-
"configuration/
|
|
18
|
-
"configuration/
|
|
19
|
-
"configuration/
|
|
20
|
-
"configuration/
|
|
21
|
-
"configuration/
|
|
22
|
-
"configuration/
|
|
23
|
-
"configuration/
|
|
24
|
-
"configuration/
|
|
25
|
-
"configuration/
|
|
26
|
-
"configuration/
|
|
27
|
-
"content/": "site/root/pages/
|
|
28
|
-
"content/
|
|
29
|
-
"content/
|
|
30
|
-
"content/
|
|
31
|
-
"content/
|
|
32
|
-
"content/
|
|
33
|
-
"content/
|
|
34
|
-
"content/
|
|
35
|
-
"content/
|
|
36
|
-
"content/
|
|
37
|
-
"content/
|
|
38
|
-
"content/
|
|
39
|
-
"content/
|
|
40
|
-
"
|
|
41
|
-
"commands/
|
|
42
|
-
"commands/
|
|
43
|
-
"commands/
|
|
44
|
-
"commands/
|
|
45
|
-
"commands/
|
|
46
|
-
"commands/
|
|
47
|
-
"commands/
|
|
48
|
-
"commands/
|
|
49
|
-
"commands/
|
|
50
|
-
"commands/
|
|
51
|
-
"commands/
|
|
52
|
-
"commands/
|
|
53
|
-
"
|
|
54
|
-
"reader/
|
|
55
|
-
"reader/
|
|
56
|
-
"
|
|
57
|
-
"workflows/
|
|
58
|
-
"workflows/editor
|
|
59
|
-
"workflows/
|
|
60
|
-
"workflows/
|
|
61
|
-
"workflows/
|
|
62
|
-
"workflows/
|
|
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/#
|
|
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/#
|
|
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:
|
|
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: '
|
|
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'), '
|
|
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, '
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
622
|
-
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;
|
|
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
|
|
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: '
|
|
716
|
+
severity: 'error',
|
|
714
717
|
line: reference.line,
|
|
715
|
-
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(', ')}.`,
|
|
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: '
|
|
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 } = {}) => {
|