@janga/norna 0.7.27 → 0.7.28
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 +37 -28
- package/astro.config.mjs +19 -1
- package/bin/norna-cli.mjs +5 -3
- package/package.json +9 -5
- package/schemas/card-list.schema.json +1 -1
- package/schemas/config.schema.json +66 -66
- package/schemas/content-frontmatter.schema.json +16 -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 +13 -161
- package/scripts/build-site.mjs +11 -6
- package/scripts/check-config.mjs +7 -23
- package/scripts/dev-local.mjs +17 -10
- package/scripts/export-theme-preset.mjs +2 -2
- package/scripts/generate-images.mjs +7 -6
- package/scripts/generate-schemas.mjs +9 -6
- package/scripts/lib/category-destinations.mjs +13 -12
- package/scripts/lib/documentation-links.mjs +1 -1
- package/scripts/lib/documentation-routes.json +67 -60
- package/scripts/lib/docusaurus-migration-audit.mjs +9 -6
- package/scripts/lib/editor-file-policy.mjs +18 -0
- package/scripts/lib/editor-language-service.mjs +53 -55
- package/scripts/lib/editor-page-addresses.mjs +55 -0
- package/scripts/lib/editor-page-placement.mjs +172 -0
- package/scripts/lib/editor-site-editing-tree.mjs +146 -0
- package/scripts/lib/editor-site-files.mjs +153 -0
- package/scripts/lib/editor-site-links.mjs +79 -0
- package/scripts/lib/editor-site-tree.mjs +151 -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/navigation-model.mjs +2 -44
- package/scripts/lib/navigation-review.mjs +14 -50
- package/scripts/lib/norna-markdown-blocks.mjs +1 -1
- package/scripts/lib/page-aliases.mjs +3 -1
- package/scripts/lib/page-model.mjs +17 -20
- package/scripts/lib/page-move-apply.mjs +117 -0
- package/scripts/lib/page-move-plan.mjs +27 -59
- package/scripts/lib/presentation.mjs +3 -3
- package/scripts/lib/project-config.mjs +7 -8
- package/scripts/lib/schema-definitions.mjs +11 -38
- package/scripts/lib/schema-editor-metadata.mjs +12 -59
- package/scripts/lib/schema-value-definitions.mjs +1 -1
- package/scripts/lib/site-content.mjs +7 -24
- package/scripts/lib/site-conventions.mjs +19 -1
- package/scripts/lib/site-navigation-tree.mjs +2 -7
- package/scripts/lib/site-node-create.mjs +169 -0
- package/scripts/lib/site-paths.mjs +22 -10
- package/scripts/lib/site-structure.mjs +72 -82
- package/scripts/lib/site-upgrade.mjs +30 -0
- package/scripts/lib/source-files.mjs +34 -0
- 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 +3 -124
- package/scripts/show-typography.mjs +32 -17
- package/scripts/sync-content-sections.mjs +9 -7
- package/scripts/sync-site-public.mjs +1 -1
- 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/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 +9 -1
- package/src/components/SectionNavigationScript.astro +106 -26
- package/src/components/SiteNavigation.astro +45 -20
- package/src/components/SitePage.astro +37 -61
- 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 +10 -8
- package/src/layouts/BaseLayout.astro +15 -39
- package/src/lib/areaNavigation.ts +42 -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 +3 -2
- package/src/lib/siteNavigation.ts +6 -0
- package/src/lib/sitePages.ts +9 -6
- package/src/pages/404.astro +1 -1
- package/src/pages/[...slug].astro +5 -15
- package/src/pages/index.astro +1 -3
- package/src/styles/area-navigation.css +399 -0
- package/src/styles/content.css +2 -29
- 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/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/{config.yaml → site-config/settings.yaml} +0 -0
- /package/starters/basic/site/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getSiteLinkGraph } from './site-link-graph.mjs';
|
|
5
|
+
import { getSiteStructure } from './site-structure.mjs';
|
|
6
|
+
|
|
7
|
+
const writeFileAtomically = async (filePath, source, mode) => {
|
|
8
|
+
const temporaryPath = path.join(
|
|
9
|
+
path.dirname(filePath),
|
|
10
|
+
`.${path.basename(filePath)}.norna-${process.pid}-${randomUUID()}.tmp`,
|
|
11
|
+
);
|
|
12
|
+
try {
|
|
13
|
+
await writeFile(temporaryPath, source, { mode: mode & 0o777 });
|
|
14
|
+
await rename(temporaryPath, filePath);
|
|
15
|
+
} finally {
|
|
16
|
+
await rm(temporaryPath, { force: true });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const assertSourcesUnchanged = async (plan) => {
|
|
21
|
+
for (const file of plan.sourceFiles) {
|
|
22
|
+
const currentSource = await readFile(file.contentPath, 'utf8');
|
|
23
|
+
if (currentSource !== file.source) {
|
|
24
|
+
throw new Error([
|
|
25
|
+
`Page move stopped because ${file.contentLabel} changed after the plan was created.`,
|
|
26
|
+
'Run the command again to build a fresh plan. No file was changed.',
|
|
27
|
+
].join('\n'));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const assertFinalSite = async (siteRoot, generatedRoutes) => {
|
|
33
|
+
const structure = await getSiteStructure({ siteRoot });
|
|
34
|
+
const graph = await getSiteLinkGraph({ siteStructure: structure, publicDir: path.join(siteRoot, 'public'), generatedRoutes });
|
|
35
|
+
const errors = graph.diagnostics.filter(({ severity }) => severity === 'error');
|
|
36
|
+
if (errors.length === 0) return;
|
|
37
|
+
throw new Error([
|
|
38
|
+
'Post-move link validation failed.',
|
|
39
|
+
...errors.flatMap((diagnostic) => [
|
|
40
|
+
`- ${diagnostic.message}`,
|
|
41
|
+
...(diagnostic.fix ? [` Fix: ${diagnostic.fix}`] : []),
|
|
42
|
+
]),
|
|
43
|
+
].join('\n'));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const applyPageMovePlan = async (plan, { siteRoot, generatedRoutes, renameDirectory = rename }) => {
|
|
47
|
+
await assertSourcesUnchanged(plan);
|
|
48
|
+
let collectionCreated = false;
|
|
49
|
+
let directoryMoved = false;
|
|
50
|
+
let fileWritten = false;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (plan.mode === 'move') {
|
|
54
|
+
if (!plan.destinationCollectionExists) {
|
|
55
|
+
await mkdir(plan.destinationCollectionDirectory);
|
|
56
|
+
collectionCreated = true;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await renameDirectory(plan.sourceDirectory, plan.destinationDirectory);
|
|
60
|
+
directoryMoved = true;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error?.code === 'EXDEV') {
|
|
63
|
+
throw new Error('Page directories must stay on one filesystem so Norna can move the complete subtree atomically.');
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const change of plan.fileChanges) {
|
|
70
|
+
await writeFileAtomically(change.finalPath, change.updatedSource, change.mode);
|
|
71
|
+
fileWritten = true;
|
|
72
|
+
}
|
|
73
|
+
await assertFinalSite(siteRoot, generatedRoutes);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const changesStarted = directoryMoved || fileWritten;
|
|
76
|
+
const rollbackErrors = [];
|
|
77
|
+
if (directoryMoved) {
|
|
78
|
+
try {
|
|
79
|
+
await renameDirectory(plan.destinationDirectory, plan.sourceDirectory);
|
|
80
|
+
directoryMoved = false;
|
|
81
|
+
} catch (rollbackError) {
|
|
82
|
+
rollbackErrors.push(`Could not restore the page directory: ${rollbackError.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const change of changesStarted ? plan.fileChanges : []) {
|
|
87
|
+
try {
|
|
88
|
+
const restorePath = directoryMoved && change.finalPath !== change.originalPath
|
|
89
|
+
? change.finalPath
|
|
90
|
+
: change.originalPath;
|
|
91
|
+
await writeFileAtomically(restorePath, change.originalSource, change.mode);
|
|
92
|
+
} catch (rollbackError) {
|
|
93
|
+
rollbackErrors.push(`Could not restore ${change.contentLabel}: ${rollbackError.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (collectionCreated && !directoryMoved) {
|
|
98
|
+
try {
|
|
99
|
+
await rmdir(plan.destinationCollectionDirectory);
|
|
100
|
+
} catch (rollbackError) {
|
|
101
|
+
if (rollbackError?.code !== 'ENOENT' && rollbackError?.code !== 'ENOTEMPTY') {
|
|
102
|
+
rollbackErrors.push(`Could not remove the newly created pages directory: ${rollbackError.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw new Error([
|
|
108
|
+
error instanceof Error ? error.message : String(error),
|
|
109
|
+
rollbackErrors.length === 0
|
|
110
|
+
? (changesStarted
|
|
111
|
+
? 'The original page directory and content files were restored.'
|
|
112
|
+
: 'No source file was changed.')
|
|
113
|
+
: 'Rollback was incomplete. Inspect these paths before running page:move again:',
|
|
114
|
+
...rollbackErrors.map((message) => `- ${message}`),
|
|
115
|
+
].join('\n'));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
toPosixPath,
|
|
12
12
|
} from './site-content.mjs';
|
|
13
13
|
import { getSiteNodePathname } from './site-page-urls.mjs';
|
|
14
|
+
import { changeYamlField } from './yaml-source-edit.mjs';
|
|
14
15
|
|
|
15
16
|
const pagePathPattern = /^\/(?:[a-z0-9]+(?:-[a-z0-9]+)*\/)+$/;
|
|
16
17
|
|
|
@@ -61,12 +62,6 @@ const applyReplacements = (source, replacements) => {
|
|
|
61
62
|
return updated;
|
|
62
63
|
};
|
|
63
64
|
|
|
64
|
-
const insertBeforeTrailingBlankLines = (lines, additions) => {
|
|
65
|
-
let index = lines.length;
|
|
66
|
-
while (index > 0 && !lines[index - 1].trim()) index -= 1;
|
|
67
|
-
lines.splice(index, 0, ...additions);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
65
|
const addPageAliasToSource = (source, alias, label) => {
|
|
71
66
|
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
72
67
|
const { body, frontmatter, frontmatterBody } = splitSiteFile(source, label);
|
|
@@ -88,64 +83,31 @@ const addPageAliasToSource = (source, alias, label) => {
|
|
|
88
83
|
};
|
|
89
84
|
}
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
'Use block YAML beginning with "page:" on its own line, or rerun with --no-aliases.',
|
|
97
|
-
].join('\n'));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (pageIndex < 0) {
|
|
101
|
-
insertBeforeTrailingBlankLines(lines, [
|
|
102
|
-
'page:',
|
|
103
|
-
' aliases:',
|
|
104
|
-
` - ${alias}`,
|
|
105
|
-
]);
|
|
106
|
-
} else {
|
|
107
|
-
let pageEnd = lines.length;
|
|
108
|
-
for (let index = pageIndex + 1; index < lines.length; index += 1) {
|
|
109
|
-
const line = lines[index];
|
|
110
|
-
if (!line.trim() || line.trimStart().startsWith('#')) continue;
|
|
111
|
-
if (!line.startsWith(' ')) {
|
|
112
|
-
pageEnd = index;
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const aliasesIndex = lines.findIndex((line, index) => (
|
|
118
|
-
index > pageIndex
|
|
119
|
-
&& index < pageEnd
|
|
120
|
-
&& /^ {2}aliases:\s*(?:#.*)?$/.test(line)
|
|
121
|
-
));
|
|
122
|
-
if (data.page?.aliases && aliasesIndex < 0) {
|
|
123
|
-
throw new Error([
|
|
124
|
-
`Cannot add ${alias} to ${label} without rewriting its flow-style aliases list.`,
|
|
125
|
-
'Use a block list below " aliases:", or rerun with --no-aliases.',
|
|
126
|
-
].join('\n'));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (aliasesIndex < 0) {
|
|
130
|
-
lines.splice(pageIndex + 1, 0, ' aliases:', ` - ${alias}`);
|
|
131
|
-
} else {
|
|
132
|
-
let lastAliasIndex = -1;
|
|
133
|
-
for (let index = aliasesIndex + 1; index < pageEnd; index += 1) {
|
|
134
|
-
if (/^ {4}-\s+/.test(lines[index])) lastAliasIndex = index;
|
|
135
|
-
}
|
|
136
|
-
if (lastAliasIndex < 0) {
|
|
137
|
-
throw new Error(`Cannot find the block-list entries below "aliases:" in ${label}.`);
|
|
138
|
-
}
|
|
139
|
-
lines.splice(lastAliasIndex + 1, 0, ` - ${alias}`);
|
|
140
|
-
}
|
|
86
|
+
let yaml;
|
|
87
|
+
try {
|
|
88
|
+
yaml = changeYamlField(frontmatterBody, ['page', 'aliases'], [...(data.page?.aliases ?? []), alias], eol);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw Object.assign(new Error(`Cannot preserve ${alias} in ${label}. ${error.message}`), { sourcePath: label });
|
|
141
91
|
}
|
|
142
92
|
|
|
143
93
|
return {
|
|
144
94
|
changed: true,
|
|
145
|
-
source: `---${eol}${
|
|
95
|
+
source: `---${eol}${yaml.replace(/\r?\n$/, '')}${eol}---${eol}${body}`,
|
|
146
96
|
};
|
|
147
97
|
};
|
|
148
98
|
|
|
99
|
+
const removePageAliasFromSource = (source, alias, label) => {
|
|
100
|
+
const { body, frontmatter, frontmatterBody } = splitSiteFile(source, label);
|
|
101
|
+
if (!frontmatter) return source;
|
|
102
|
+
const data = parseContentFrontmatter(frontmatterBody, label);
|
|
103
|
+
if (!data.page?.aliases?.includes(alias)) return source;
|
|
104
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
105
|
+
const remaining = data.page.aliases.filter((entry) => entry !== alias);
|
|
106
|
+
const yaml = changeYamlField(frontmatterBody, ['page', 'aliases'], remaining.length ? remaining : undefined, eol);
|
|
107
|
+
if (!yaml.trim()) return body.replace(/^\r?\n/, '');
|
|
108
|
+
return `---${eol}${yaml.replace(/\r?\n$/, '')}${eol}---${eol}${body}`;
|
|
109
|
+
};
|
|
110
|
+
|
|
149
111
|
const normalizeMovePathname = (value, optionName) => {
|
|
150
112
|
if (value === '/') return { pagePath: '', pathname: '/' };
|
|
151
113
|
if (typeof value !== 'string' || !pagePathPattern.test(value)) {
|
|
@@ -396,6 +358,7 @@ const formatGraphDiagnostics = (diagnostics) => diagnostics.flatMap((diagnostic)
|
|
|
396
358
|
export const createPageMovePlan = async ({
|
|
397
359
|
from,
|
|
398
360
|
graph,
|
|
361
|
+
generatedRoutes,
|
|
399
362
|
order = null,
|
|
400
363
|
preserveAliases = true,
|
|
401
364
|
publicFiles,
|
|
@@ -559,6 +522,9 @@ export const createPageMovePlan = async ({
|
|
|
559
522
|
})));
|
|
560
523
|
|
|
561
524
|
const mapping = mappingByContentPath.get(contentPath);
|
|
525
|
+
if (mapping && page.aliases.includes(mapping.newPathname)) {
|
|
526
|
+
updatedSource = removePageAliasFromSource(updatedSource, mapping.newPathname, page.contentFile.contentLabel);
|
|
527
|
+
}
|
|
562
528
|
if (mapping && preserveAliases) {
|
|
563
529
|
const aliasResult = addPageAliasToSource(updatedSource, mapping.oldPathname, page.contentFile.contentLabel);
|
|
564
530
|
updatedSource = aliasResult.source;
|
|
@@ -614,11 +580,13 @@ export const createPageMovePlan = async ({
|
|
|
614
580
|
pageDocuments: virtualPageDocuments,
|
|
615
581
|
publicFiles,
|
|
616
582
|
siteStructure: virtualStructure,
|
|
583
|
+
generatedRoutes,
|
|
617
584
|
});
|
|
618
|
-
|
|
585
|
+
const errors = virtualGraph.diagnostics.filter(({ severity }) => severity === 'error');
|
|
586
|
+
if (errors.length > 0) {
|
|
619
587
|
throw new Error([
|
|
620
588
|
'Page move preflight failed. No file was changed.',
|
|
621
|
-
...formatGraphDiagnostics(
|
|
589
|
+
...formatGraphDiagnostics(errors),
|
|
622
590
|
].join('\n'));
|
|
623
591
|
}
|
|
624
592
|
|
|
@@ -453,7 +453,7 @@ export const getPresentationCssVariables = (presentation) => {
|
|
|
453
453
|
return variables;
|
|
454
454
|
};
|
|
455
455
|
|
|
456
|
-
const getSectionSurfaces = (pattern = 'uniform', sourceLabel = 'theme.yaml') => {
|
|
456
|
+
const getSectionSurfaces = (pattern = 'uniform', sourceLabel = 'root/tree-theme.yaml') => {
|
|
457
457
|
const surfaces = sectionBackgroundPatterns[pattern];
|
|
458
458
|
if (!surfaces) {
|
|
459
459
|
throw new Error(`sections.backgroundPattern must be one of ${Object.keys(sectionBackgroundPatterns).join(', ')} in ${sourceLabel}.`);
|
|
@@ -462,7 +462,7 @@ const getSectionSurfaces = (pattern = 'uniform', sourceLabel = 'theme.yaml') =>
|
|
|
462
462
|
return surfaces;
|
|
463
463
|
};
|
|
464
464
|
|
|
465
|
-
export const resolveThemePresentation = (theme, sourceLabel = 'theme.yaml') => {
|
|
465
|
+
export const resolveThemePresentation = (theme, sourceLabel = 'root/tree-theme.yaml') => {
|
|
466
466
|
const normalizedTheme = resolveThemeConfig(theme, sourceLabel);
|
|
467
467
|
const paletteName = normalizedTheme.palette ?? 'near-monochrome';
|
|
468
468
|
const palette = getPresentationPalette(paletteName);
|
|
@@ -509,7 +509,7 @@ export const resolveThemePresentation = (theme, sourceLabel = 'theme.yaml') => {
|
|
|
509
509
|
|
|
510
510
|
export const assertSectionBackgroundPatternCompatibility = (
|
|
511
511
|
theme,
|
|
512
|
-
sourceLabel = 'theme.yaml',
|
|
512
|
+
sourceLabel = 'root/tree-theme.yaml',
|
|
513
513
|
navigationMode = 'sections',
|
|
514
514
|
) => {
|
|
515
515
|
const requestedPattern = theme?.sections?.backgroundPattern;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import {
|
|
3
5
|
validateConfigYamlStructure,
|
|
4
6
|
} from './site-content.mjs';
|
|
@@ -11,6 +13,7 @@ import { navigationModeNames } from './navigation-model.mjs';
|
|
|
11
13
|
import {
|
|
12
14
|
siteConfigLabel,
|
|
13
15
|
siteConfigPath,
|
|
16
|
+
siteDir,
|
|
14
17
|
siteThemeLabel,
|
|
15
18
|
} from './site-paths.mjs';
|
|
16
19
|
import { localEditorNames, normalizeEditLinkBaseUrl } from './edit-source-link.mjs';
|
|
@@ -20,6 +23,9 @@ import { resolveThemeConfig } from './theme-presets.mjs';
|
|
|
20
23
|
import { parseYamlConfig } from './yaml-config.mjs';
|
|
21
24
|
|
|
22
25
|
const readSiteConfig = async () => {
|
|
26
|
+
if (existsSync(path.join(siteDir, 'site-config/site-theme.yaml')) || existsSync(path.join(siteDir, 'root/theme.yaml')) || existsSync(path.join(siteDir, 'config.yaml')) || existsSync(path.join(siteDir, 'page-theme.yaml')) || existsSync(path.join(siteDir, 'sitewide-content.yaml'))) {
|
|
27
|
+
throw new Error('This site has configuration files in the former locations. Stop its development server and follow the source-layout reference to place technical settings in site-config/ and page files in root/. Move the shared theme to root/tree-theme.yaml with an explicit preset and homepage-only overrides to root/page-theme.yaml. Automatic historical conversion is not provided.');
|
|
28
|
+
}
|
|
23
29
|
const source = await readFile(siteConfigPath, 'utf8').catch((error) => {
|
|
24
30
|
if (error?.code === 'ENOENT') {
|
|
25
31
|
throw new Error(`${siteConfigLabel} is required. Create it before running Norna.`);
|
|
@@ -302,13 +308,6 @@ export const resolveThemeVisualConfig = (theme, sourceLabel = siteThemeLabel) =>
|
|
|
302
308
|
defaultImagePresentation,
|
|
303
309
|
sourceLabel,
|
|
304
310
|
);
|
|
305
|
-
if (imagePresentation === 'prose-aligned' && rawImagesConfig.maxAvailableHeightPercent !== undefined) {
|
|
306
|
-
throw new Error([
|
|
307
|
-
`images.maxAvailableHeightPercent cannot be used with images.presentation "prose-aligned" in ${sourceLabel}.`,
|
|
308
|
-
'Prose-aligned images follow available horizontal space instead of a viewport-height limit.',
|
|
309
|
-
'Remove images.maxAvailableHeightPercent or set images.presentation to "centered-fit".',
|
|
310
|
-
].join('\n'));
|
|
311
|
-
}
|
|
312
311
|
|
|
313
312
|
return Object.freeze({
|
|
314
313
|
layout: Object.freeze({
|
|
@@ -371,7 +370,7 @@ export const resolveNavigationConfig = (config, sourceLabel = siteConfigLabel) =
|
|
|
371
370
|
if (Object.hasOwn(rawNavigation, 'sectionTracking')) {
|
|
372
371
|
throw new Error([
|
|
373
372
|
`navigation.sectionTracking is no longer supported in ${sourceLabel}.`,
|
|
374
|
-
'Section tracking is automatic on pages with
|
|
373
|
+
'Section tracking is automatic on tree-navigation pages with H2 headings.',
|
|
375
374
|
'Remove navigation.sectionTracking.',
|
|
376
375
|
].join('\n'));
|
|
377
376
|
}
|
|
@@ -167,13 +167,6 @@ const themeLayout = z.object({
|
|
|
167
167
|
gutter: responsiveCssLength.optional().describe('Horizontal page gutter as one value or separate desktop and mobile values.'),
|
|
168
168
|
spacing: themeLayoutSpacing.optional().describe('Fine-grained spacing overrides.'),
|
|
169
169
|
}).strict().describe('Optional layout overrides applied after the preset.');
|
|
170
|
-
const pageThemeLayout = z.object({
|
|
171
|
-
contentSpacing: contentSpacing.optional().describe('Page content spacing. Descendant pages inherit this value.'),
|
|
172
|
-
textWidth: textWidth.optional().describe('Body-text line length. Descendant pages inherit this value.'),
|
|
173
|
-
}).strict().refine(
|
|
174
|
-
(value) => value.contentSpacing !== undefined || value.textWidth !== undefined,
|
|
175
|
-
'Specify contentSpacing, textWidth, or both.',
|
|
176
|
-
).describe('Page-local layout settings inherited by descendant pages.');
|
|
177
170
|
const themeImages = z.object({
|
|
178
171
|
presentation: imagePresentation.optional().describe('Managed-image presentation method. Omit this to keep the selected preset or inherited page setting.'),
|
|
179
172
|
width: visualCssLength.optional().describe('Maximum managed-image width. Omit this to keep the selected preset.'),
|
|
@@ -182,25 +175,19 @@ const themeImages = z.object({
|
|
|
182
175
|
}).strict().describe('Optional defaults for placing managed images inside the page content area. Persistent navigation remains outside this area.');
|
|
183
176
|
const themeCardList = z.object({
|
|
184
177
|
width: cardListWidth,
|
|
185
|
-
}).strict().describe('
|
|
178
|
+
}).strict().describe('Card-list defaults.');
|
|
186
179
|
const themeBlocks = z.object({
|
|
187
180
|
cardList: themeCardList.optional(),
|
|
188
181
|
}).strict().refine(
|
|
189
182
|
(value) => value.cardList !== undefined,
|
|
190
183
|
'Specify cardList.',
|
|
191
|
-
).describe('Optional
|
|
184
|
+
).describe('Optional defaults for structured Norna content blocks.');
|
|
192
185
|
const configNavigation = z.object({
|
|
193
186
|
mode: navigationMode.optional().describe('How Norna presents the discovered page and heading hierarchy. Omit this to select automatic.'),
|
|
194
187
|
}).strict().describe('Site-wide navigation behavior.');
|
|
195
188
|
const themeSections = z.object({
|
|
196
189
|
backgroundPattern: backgroundPattern.optional().describe('Section background pattern. Alternating and accented create full-width bands with sections or top navigation; tree navigation requires uniform.'),
|
|
197
190
|
}).strict().describe('Defaults for page section presentation.');
|
|
198
|
-
const pageThemeSections = z.object({
|
|
199
|
-
backgroundPattern: backgroundPattern.optional().describe('Section background sequence inherited by descendant pages. Non-uniform patterns are invalid with tree navigation.'),
|
|
200
|
-
}).strict().refine(
|
|
201
|
-
(value) => value.backgroundPattern !== undefined,
|
|
202
|
-
'Specify backgroundPattern.',
|
|
203
|
-
).describe('Page-local section presentation inherited by descendant pages.');
|
|
204
191
|
const pageNavigation = z.object({
|
|
205
192
|
listed: z.boolean().optional().default(true).describe('List this page in site navigation. The page remains public when false.'),
|
|
206
193
|
}).strict();
|
|
@@ -210,6 +197,7 @@ const pageAlias = z.string().regex(
|
|
|
210
197
|
).describe('Previous site-relative URL that permanently identifies this page.');
|
|
211
198
|
const pageMetadata = z.object({
|
|
212
199
|
description: z.string().min(1).optional().describe('Page description used in metadata, social previews and generated child-page lists.'),
|
|
200
|
+
listChildren: z.boolean().optional().describe('Append links to every listed direct child page after this page\'s authored content.'),
|
|
213
201
|
aliases: z.array(pageAlias)
|
|
214
202
|
.min(1)
|
|
215
203
|
.refine((values) => new Set(values).size === values.length, 'Page aliases must be unique within the page.')
|
|
@@ -251,9 +239,9 @@ const siteShape = {
|
|
|
251
239
|
};
|
|
252
240
|
|
|
253
241
|
const themeVisualShape = {
|
|
254
|
-
preset: themePreset.optional().describe('
|
|
242
|
+
preset: themePreset.optional().describe('Select a new visual base, discarding all inherited theme values. Required in root/tree-theme.yaml; omit elsewhere to modify inherited values. Even the same preset starts a new base.'),
|
|
255
243
|
appearance: themeAppearance.optional(),
|
|
256
|
-
corners: cornerTreatment.optional().describe('
|
|
244
|
+
corners: cornerTreatment.optional().describe('Corner treatment. Omit this to use the selected preset.'),
|
|
257
245
|
layout: themeLayout.optional(),
|
|
258
246
|
images: themeImages.optional(),
|
|
259
247
|
blocks: themeBlocks.optional(),
|
|
@@ -262,44 +250,29 @@ const themeVisualShape = {
|
|
|
262
250
|
sections: themeSections.optional(),
|
|
263
251
|
};
|
|
264
252
|
|
|
265
|
-
const pageThemeShape = {
|
|
266
|
-
layout: pageThemeLayout.optional(),
|
|
267
|
-
images: themeImages.optional(),
|
|
268
|
-
sections: pageThemeSections.optional(),
|
|
269
|
-
};
|
|
270
|
-
|
|
271
253
|
const sitewideShape = {
|
|
272
254
|
logo: sitewideLogo.optional(),
|
|
273
255
|
banners: banners.describe('Site-wide dismissible notices.'),
|
|
274
256
|
footer: sitewideFooter.optional().describe('Site-wide footer content.'),
|
|
275
257
|
};
|
|
276
258
|
|
|
277
|
-
const categoryShape = {
|
|
278
|
-
label: z.string().trim().min(1).describe('Navigation label for a group of child pages without an authored page body.'),
|
|
279
|
-
};
|
|
280
|
-
|
|
281
259
|
export const schemaTopLevelKeys = Object.freeze({
|
|
282
260
|
config: Object.freeze(Object.keys(configShape)),
|
|
283
|
-
category: Object.freeze(Object.keys(categoryShape)),
|
|
284
261
|
content: Object.freeze(Object.keys(siteShape)),
|
|
285
262
|
sitewide: Object.freeze(Object.keys(sitewideShape)),
|
|
286
263
|
theme: Object.freeze(Object.keys(themeVisualShape)),
|
|
287
|
-
pageTheme: Object.freeze(Object.keys(
|
|
264
|
+
pageTheme: Object.freeze(Object.keys(themeVisualShape)),
|
|
288
265
|
});
|
|
289
266
|
|
|
290
267
|
export const configSchema = z.object(configShape).strict()
|
|
291
268
|
.describe('Technical settings for one Norna site. Additional pages cannot provide technical configuration.');
|
|
292
|
-
export const categorySchema = z.object(categoryShape).strict()
|
|
293
|
-
.describe('A navigation group without an authored page body. Opening its URL redirects when its first listed direct child is a page; otherwise it shows a generated child list.');
|
|
294
269
|
export const siteSchema = z.object(siteShape).strict()
|
|
295
270
|
.describe('Frontmatter for a homepage or additional page content.md file.');
|
|
296
271
|
export const themeVisualSchema = z.object(themeVisualShape).strict()
|
|
297
|
-
.describe('
|
|
298
|
-
export const
|
|
299
|
-
.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
)
|
|
303
|
-
.describe('Limited page presentation overrides inherited by descendant pages. Site colors, corners, typography, content-block defaults and navigation remain global.');
|
|
272
|
+
.describe('Visual settings for this page and its descendants. Omit preset to modify inherited values; an explicit preset replaces the complete inherited base, even when the preset name is unchanged.');
|
|
273
|
+
export const rootThemeSchema = z.object({ ...themeVisualShape, preset: themePreset }).strict()
|
|
274
|
+
.describe('Required root/tree-theme.yaml. Select a preset for the site, then add the visual settings you want to override.');
|
|
275
|
+
export const pageThemeSchema = z.object(themeVisualShape).strict()
|
|
276
|
+
.describe('Visual settings for this page only. Omit preset to modify the active tree theme; an explicit preset replaces that base. Descendants keep their tree theme.');
|
|
304
277
|
export const sitewideSchema = z.object(sitewideShape).strict()
|
|
305
278
|
.describe('Editorial content and optional navigation logo display settings shared by every page.');
|
|
@@ -151,7 +151,7 @@ const addSitewideLogoHelp = (jsonSchema) => {
|
|
|
151
151
|
const addConfigHelp = (jsonSchema) => {
|
|
152
152
|
jsonSchema.markdownDescription = [
|
|
153
153
|
yamlExample('url: https://example.com/'),
|
|
154
|
-
'`config.yaml` contains the few technical settings shared by the complete site.',
|
|
154
|
+
'`site-config/settings.yaml` contains the few technical settings shared by the complete site.',
|
|
155
155
|
documentationLink('Configuration reference', 'configuration.md'),
|
|
156
156
|
].join('\n\n');
|
|
157
157
|
addHelp(jsonSchema, 'url', [
|
|
@@ -283,7 +283,7 @@ const addThemeHelp = (jsonSchema) => {
|
|
|
283
283
|
], presentationPaletteNames);
|
|
284
284
|
addHelp(jsonSchema, 'appearance', [
|
|
285
285
|
yamlExample('appearance:\n default: system'),
|
|
286
|
-
'Selects the initial Appearance. The Appearance choice is always available in the site-wide Display panel. Omit this setting to
|
|
286
|
+
'Selects the initial Appearance. The Appearance choice is always available in the site-wide Display panel. Omit this setting to keep the inherited or preset value.',
|
|
287
287
|
documentationLink('Appearance', 'theme.md', 'appearance'),
|
|
288
288
|
]);
|
|
289
289
|
addFieldHelp(jsonSchema, 'appearance.default', 'appearance:\n default: system', 'theme.md', 'appearance', ['system', 'light', 'dark']);
|
|
@@ -463,49 +463,10 @@ const addThemeHelp = (jsonSchema) => {
|
|
|
463
463
|
})]);
|
|
464
464
|
};
|
|
465
465
|
|
|
466
|
-
const addPageThemeHelp = (jsonSchema) => {
|
|
467
|
-
jsonSchema.markdownDescription = [
|
|
468
|
-
yamlExample('layout:\n textWidth: narrow\n contentSpacing: compact'),
|
|
469
|
-
'A limited theme in a page or category directory may adjust only page layout, managed-image sizing and the section background pattern. Descendant pages inherit these values. Tree navigation requires a uniform section background. Site colors, corners, typography, content-block defaults and navigation stay consistent.',
|
|
470
|
-
documentationLink('Page theme reference', 'theme.md', 'page-themes'),
|
|
471
|
-
].join('\n\n');
|
|
472
|
-
addHelp(jsonSchema, 'layout', [
|
|
473
|
-
yamlExample('layout:\n textWidth: narrow\n contentSpacing: compact'),
|
|
474
|
-
'Adjusts body-text line length and vertical content spacing for pages below this directory.',
|
|
475
|
-
documentationLink('Page theme reference', 'theme.md', 'page-themes'),
|
|
476
|
-
]);
|
|
477
|
-
addFieldHelp(jsonSchema, 'layout.textWidth', 'layout:\n textWidth: narrow', 'theme.md', 'page-themes', ['narrow', 'normal', 'wide']);
|
|
478
|
-
addFieldHelp(jsonSchema, 'layout.contentSpacing', 'layout:\n contentSpacing: compact', 'theme.md', 'page-themes', ['compact', 'normal', 'spacious']);
|
|
479
|
-
addHelp(jsonSchema, 'images', [
|
|
480
|
-
yamlExample('images:\n presentation: prose-aligned\n width: 900px'),
|
|
481
|
-
'Adjusts managed-image presentation and sizing inside the content area for pages below this directory. Persistent navigation remains outside this area.',
|
|
482
|
-
documentationLink('Image sizing reference', 'theme.md', 'image-sizing'),
|
|
483
|
-
]);
|
|
484
|
-
const imageFields = [
|
|
485
|
-
['images.presentation', 'images:\n presentation: prose-aligned'],
|
|
486
|
-
['images.width', 'images:\n width: 900px'],
|
|
487
|
-
['images.maxAvailableWidthPercent', 'images:\n maxAvailableWidthPercent:\n desktop: 100\n mobile: 100'],
|
|
488
|
-
['images.maxAvailableWidthPercent.desktop', 'images:\n maxAvailableWidthPercent:\n desktop: 100'],
|
|
489
|
-
['images.maxAvailableWidthPercent.mobile', 'images:\n maxAvailableWidthPercent:\n mobile: 100'],
|
|
490
|
-
['images.maxAvailableHeightPercent', 'images:\n maxAvailableHeightPercent:\n desktop: 74\n mobile: 68'],
|
|
491
|
-
['images.maxAvailableHeightPercent.desktop', 'images:\n maxAvailableHeightPercent:\n desktop: 74'],
|
|
492
|
-
['images.maxAvailableHeightPercent.mobile', 'images:\n maxAvailableHeightPercent:\n mobile: 68'],
|
|
493
|
-
];
|
|
494
|
-
for (const [propertyPath, example] of imageFields) {
|
|
495
|
-
addFieldHelp(jsonSchema, propertyPath, example, 'theme.md', 'image-sizing');
|
|
496
|
-
}
|
|
497
|
-
addHelp(jsonSchema, 'sections', [
|
|
498
|
-
yamlExample('sections:\n backgroundPattern: alternating'),
|
|
499
|
-
'Adjusts the H2 section background pattern for pages below this directory. Alternating and accented create full-width bands but are invalid when navigation resolves to tree.',
|
|
500
|
-
documentationLink('Page theme reference', 'theme.md', 'page-themes'),
|
|
501
|
-
]);
|
|
502
|
-
addFieldHelp(jsonSchema, 'sections.backgroundPattern', 'sections:\n backgroundPattern: alternating', 'theme.md', 'page-themes', ['uniform', 'alternating', 'accented']);
|
|
503
|
-
};
|
|
504
|
-
|
|
505
466
|
const addSitewideHelp = (jsonSchema) => {
|
|
506
467
|
jsonSchema.markdownDescription = [
|
|
507
468
|
yamlExample('footer:\n copyrightMessage: Copyright Example Owner.'),
|
|
508
|
-
'`
|
|
469
|
+
'`site-config/shared-content.yaml` optionally defines banners, footer content, and logo display settings shared by every page.',
|
|
509
470
|
documentationLink('Site-wide content reference', 'sitewide-content.md'),
|
|
510
471
|
].join('\n\n');
|
|
511
472
|
addSitewideLogoHelp(jsonSchema);
|
|
@@ -583,9 +544,10 @@ const addContentHelp = (jsonSchema) => {
|
|
|
583
544
|
]);
|
|
584
545
|
addHelp(jsonSchema, 'page.description', [
|
|
585
546
|
yamlExample('page:\n description: About this project.'),
|
|
586
|
-
'A page-specific description used in metadata and social previews. Generated child-page
|
|
547
|
+
'A page-specific description used in metadata and social previews. Generated child-page lists can show it beside the page title; it is not inserted into the page body.',
|
|
587
548
|
documentationLink('Page title and frontmatter reference', 'content.md', 'page-title-and-frontmatter'),
|
|
588
549
|
], ['About this project.']);
|
|
550
|
+
addFieldHelp(jsonSchema, 'page.listChildren', 'page:\n listChildren: true', 'pages.md', 'list-child-pages', [true, false]);
|
|
589
551
|
addHelp(jsonSchema, 'page.aliases', [
|
|
590
552
|
yamlExample('page:\n aliases:\n - /old-about/'),
|
|
591
553
|
'Lists previous site-relative URLs that permanently identify this page. The page containing the list is always the target. Paths omit the configured base path and are excluded from the sitemap.',
|
|
@@ -604,28 +566,19 @@ const addContentHelp = (jsonSchema) => {
|
|
|
604
566
|
addFieldHelp(jsonSchema, 'navigation.listed', 'navigation:\n listed: false', 'pages.md', 'navigation', [true, false]);
|
|
605
567
|
};
|
|
606
568
|
|
|
607
|
-
const addCategoryHelp = (jsonSchema) => {
|
|
608
|
-
jsonSchema.markdownDescription = [
|
|
609
|
-
yamlExample('label: Guides'),
|
|
610
|
-
'`category.yaml` groups child pages without editorial content. Its URL opens the first listed direct page, or a generated child list when the first child is a category.',
|
|
611
|
-
documentationLink('Navigation category reference', 'pages.md', 'navigation-categories'),
|
|
612
|
-
].join('\n\n');
|
|
613
|
-
addHelp(jsonSchema, 'label', [
|
|
614
|
-
yamlExample('label: Guides'),
|
|
615
|
-
'The category label shown in global navigation, tree navigation, mobile navigation, and breadcrumbs.',
|
|
616
|
-
documentationLink('Navigation category reference', 'pages.md', 'navigation-categories'),
|
|
617
|
-
]);
|
|
618
|
-
};
|
|
619
|
-
|
|
620
569
|
export const applySchemaEditorMetadata = (filename, jsonSchema) => {
|
|
621
570
|
addValueDescriptions(jsonSchema);
|
|
622
|
-
if (filename === 'category.schema.json') addCategoryHelp(jsonSchema);
|
|
623
571
|
if (filename === 'config.schema.json') {
|
|
624
572
|
addLanguageSuggestions(jsonSchema);
|
|
625
573
|
addConfigHelp(jsonSchema);
|
|
626
574
|
}
|
|
627
|
-
if (
|
|
628
|
-
|
|
575
|
+
if (['root-theme.schema.json', 'theme.schema.json', 'page-theme.schema.json'].includes(filename)) {
|
|
576
|
+
addThemeHelp(jsonSchema);
|
|
577
|
+
jsonSchema.markdownDescription = [yamlExample(filename === 'root-theme.schema.json' ? 'preset: documentation' : 'layout:\n textWidth: wide'), jsonSchema.description, documentationLink('Theme inheritance', 'theme.md', 'page-themes')].join('\n\n');
|
|
578
|
+
addHelp(jsonSchema, 'preset', [yamlExample('preset: documentation'),
|
|
579
|
+
'Selects a new base and discards inherited theme values, even when the preset name is unchanged. Required in root/tree-theme.yaml. Omit in other theme files to modify inherited values.',
|
|
580
|
+
documentationLink('Theme inheritance', 'theme.md', 'page-themes'), documentationLink('Preset reference', 'theme.md', 'theme-presets')]);
|
|
581
|
+
}
|
|
629
582
|
if (filename === 'sitewide-content.schema.json') addSitewideHelp(jsonSchema);
|
|
630
583
|
if (filename === 'content-frontmatter.schema.json') addContentHelp(jsonSchema);
|
|
631
584
|
return jsonSchema;
|
|
@@ -26,7 +26,7 @@ export const schemaValueDefinitions = Object.freeze([
|
|
|
26
26
|
automatic: option('Automatic', 'Choose one site-wide navigation presentation from the listed page hierarchy. This is the default.'),
|
|
27
27
|
sections: option('Sections', 'Keep a one-page site title and its H2 sections in sticky page navigation.'),
|
|
28
28
|
top: option('Top', 'Keep top-level pages in the global row and give the current page section navigation when needed.'),
|
|
29
|
-
tree: option('Tree', '
|
|
29
|
+
tree: option('Tree', 'Group destinations in menus and show the selected group’s pages and H2 links in a left tree.'),
|
|
30
30
|
}),
|
|
31
31
|
definition(['vscode'], {
|
|
32
32
|
vscode: option('Visual Studio Code', 'Open the current page source in the locally installed Visual Studio Code application.'),
|
|
@@ -22,9 +22,6 @@ const frontmatterDelimiterRegex = /^---\s*$/;
|
|
|
22
22
|
const knownConfigTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.config);
|
|
23
23
|
const knownContentTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.content);
|
|
24
24
|
const knownThemeTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.theme);
|
|
25
|
-
const knownPageThemeTopLevelFrontmatterKeys = new Set(
|
|
26
|
-
schemaTopLevelKeys.pageTheme,
|
|
27
|
-
);
|
|
28
25
|
const knownSitewideTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.sitewide);
|
|
29
26
|
const knownNestedFrontmatterKeys = new Set([
|
|
30
27
|
'align',
|
|
@@ -57,6 +54,7 @@ const knownNestedFrontmatterKeys = new Set([
|
|
|
57
54
|
'listed',
|
|
58
55
|
'logo',
|
|
59
56
|
'lineHeight',
|
|
57
|
+
'listChildren',
|
|
60
58
|
'maxAvailableHeightPercent',
|
|
61
59
|
'maxAvailableWidthPercent',
|
|
62
60
|
'mobile',
|
|
@@ -154,28 +152,19 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
|
|
|
154
152
|
} else if (fileKind === 'content' && key === 'images') {
|
|
155
153
|
fix = 'Put local image references in image-stack or image-carousel blocks in the Markdown body.';
|
|
156
154
|
} else if (fileKind === 'content' && knownThemeTopLevelFrontmatterKeys.has(key)) {
|
|
157
|
-
fix = `Move "${key}:" to theme.yaml. Visual settings do not belong in content frontmatter.`;
|
|
155
|
+
fix = `Move "${key}:" to site-config/site-theme.yaml. Visual settings do not belong in content frontmatter.`;
|
|
158
156
|
} else if ((fileKind === 'theme' || fileKind === 'page theme') && key === 'navigation') {
|
|
159
|
-
message = `Frontmatter line ${lineNumber}: navigation is technical, site-wide configuration and does not belong in ${fileKind}. Set it in config.yaml.`;
|
|
160
|
-
fix = 'Set the navigation mode under "navigation:" in config.yaml.';
|
|
157
|
+
message = `Frontmatter line ${lineNumber}: navigation is technical, site-wide configuration and does not belong in ${fileKind}. Set it in site-config/settings.yaml.`;
|
|
158
|
+
fix = 'Set the navigation mode under "navigation:" in site-config/settings.yaml.';
|
|
161
159
|
} else if ((fileKind === 'theme' || fileKind === 'page theme') && key === 'shape') {
|
|
162
|
-
message =
|
|
163
|
-
|
|
164
|
-
: `Frontmatter line ${lineNumber}: "shape" was replaced by "corners". Use "square" or replace the old "soft" value with "rounded".`;
|
|
165
|
-
fix = fileKind === 'page theme'
|
|
166
|
-
? 'Remove "shape:" from this page theme. Set "corners: square" or "corners: rounded" in the root theme.yaml when an override is needed.'
|
|
167
|
-
: 'Replace "shape:" with "corners:". Use "square" or replace the old "soft" value with "rounded".';
|
|
160
|
+
message = `Frontmatter line ${lineNumber}: "shape" was replaced by "corners".`;
|
|
161
|
+
fix = 'Use "corners: square" or "corners: rounded"; replace the old "soft" value with "rounded".';
|
|
168
162
|
} else if ((fileKind === 'theme' || fileKind === 'page theme') && key === 'colorMode') {
|
|
169
163
|
message = `Frontmatter line ${lineNumber}: Theme setting "colorMode" was replaced by "appearance".`;
|
|
170
|
-
fix =
|
|
171
|
-
? 'Remove "colorMode:" from this page theme. Set "appearance:" in the root theme.yaml when an override is needed.'
|
|
172
|
-
: 'Replace "colorMode:" with "appearance:".';
|
|
164
|
+
fix = 'Replace "colorMode:" with "appearance:".';
|
|
173
165
|
} else if ((fileKind === 'theme' || fileKind === 'page theme') && key === 'readerControls') {
|
|
174
166
|
message = `Frontmatter line ${lineNumber}: Theme setting "readerControls" was removed. Appearance and reading width are always available; Focus reading follows tree navigation.`;
|
|
175
167
|
fix = 'Remove "readerControls:". Use "appearance.default" for the initial Appearance; reading width is always available and tree navigation provides Focus reading.';
|
|
176
|
-
} else if (fileKind === 'page theme' && ['preset', 'appearance', 'corners', 'palette', 'typography'].includes(key)) {
|
|
177
|
-
message = `Frontmatter line ${lineNumber}: page themes may not define site-wide visual identity through "${key}".`;
|
|
178
|
-
fix = `Move "${key}:" to the root theme.yaml. Page themes may set only layout.textWidth, layout.contentSpacing, images, and sections.backgroundPattern.`;
|
|
179
168
|
} else if ((fileKind === 'theme' || fileKind === 'page theme') && ['logo', 'site'].includes(key)) {
|
|
180
169
|
message = `Frontmatter line ${lineNumber}: ${fileKind} may not define navigation logo settings. Optional logo display settings belong under "logo:" in ${sitewideContentLabel}.`;
|
|
181
170
|
fix = `Move only the logo display settings under "logo:" in ${sitewideContentLabel}; the homepage Markdown H1 supplies navigation text and logo alternative text.`;
|
|
@@ -223,12 +212,6 @@ export const validateThemeYamlStructure = (frontmatter, addIssue) =>
|
|
|
223
212
|
fileKind: 'theme',
|
|
224
213
|
});
|
|
225
214
|
|
|
226
|
-
export const validatePageThemeYamlStructure = (frontmatter, addIssue) =>
|
|
227
|
-
validateFrontmatterStructure(frontmatter, addIssue, {
|
|
228
|
-
knownTopLevelFrontmatterKeys: knownPageThemeTopLevelFrontmatterKeys,
|
|
229
|
-
fileKind: 'page theme',
|
|
230
|
-
});
|
|
231
|
-
|
|
232
215
|
export const validateSitewideYamlStructure = (frontmatter, addIssue) =>
|
|
233
216
|
validateFrontmatterStructure(frontmatter, addIssue, {
|
|
234
217
|
knownTopLevelFrontmatterKeys: knownSitewideTopLevelFrontmatterKeys,
|