@janga/norna 0.7.22 → 0.7.24
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 +14 -18
- package/astro.config.mjs +15 -0
- package/bin/norna-cli.mjs +8 -2
- package/package.json +28 -3
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +52 -24
- package/schemas/content-frontmatter.schema.json +25 -5
- package/schemas/page-theme.schema.json +53 -25
- package/schemas/sitewide-content.schema.json +19 -18
- package/schemas/theme.schema.json +385 -252
- package/scripts/build-site.mjs +1 -0
- package/scripts/check-config.mjs +36 -5
- package/scripts/deploy-site.mjs +0 -1
- package/scripts/dev-local.mjs +175 -112
- package/scripts/generate-search-index.mjs +57 -0
- package/scripts/init-site.mjs +1 -0
- package/scripts/lib/code-fence-metadata.mjs +223 -0
- package/scripts/lib/content-sync-apply.mjs +34 -0
- package/scripts/lib/content-sync-plan.mjs +179 -0
- package/scripts/lib/edit-source-link.mjs +49 -0
- package/scripts/lib/editor-language-service.mjs +160 -12
- package/scripts/lib/heading-ids.mjs +1 -1
- package/scripts/lib/image-presentation.mjs +6 -0
- package/scripts/lib/markdown-links.mjs +182 -0
- package/scripts/lib/navigation-model.mjs +44 -12
- package/scripts/lib/navigation-review.mjs +396 -0
- package/scripts/lib/norna-markdown-blocks.mjs +74 -26
- package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
- package/scripts/lib/page-aliases.mjs +184 -0
- package/scripts/lib/page-markdown.mjs +88 -7
- package/scripts/lib/page-move-plan.mjs +659 -0
- package/scripts/lib/presentation-palette-metadata.mjs +1 -1
- package/scripts/lib/presentation.mjs +10 -37
- package/scripts/lib/project-config.mjs +146 -19
- package/scripts/lib/public-asset-conventions.mjs +36 -2
- package/scripts/lib/schema-definitions.mjs +43 -15
- package/scripts/lib/schema-editor-metadata.mjs +60 -32
- package/scripts/lib/schema-value-definitions.mjs +6 -2
- package/scripts/lib/semantic-callouts.mjs +128 -0
- package/scripts/lib/site-content.mjs +28 -5
- package/scripts/lib/site-link-graph.mjs +357 -0
- package/scripts/lib/site-navigation-tree.mjs +85 -0
- package/scripts/lib/site-page-urls.mjs +10 -0
- package/scripts/lib/sitemap.mjs +34 -0
- package/scripts/lib/social-image-assets.mjs +30 -0
- package/scripts/lib/theme-presets.mjs +53 -25
- package/scripts/lib/theme-profiles.mjs +6 -8
- package/scripts/lib/yaml-config.mjs +6 -2
- package/scripts/move-site-page.mjs +256 -0
- package/scripts/review-navigation.mjs +32 -0
- package/scripts/sync-content-sections.mjs +182 -216
- package/scripts/sync-site-public.mjs +43 -3
- package/src/components/CodeBlockCopyScript.astro +97 -0
- package/src/components/DisplaySettings.astro +4 -4
- package/src/components/EditSourceLink.astro +17 -0
- package/src/components/ImageCarousel.astro +6 -3
- package/src/components/NavigationPageTree.astro +81 -86
- package/src/components/NavigationTreeControls.astro +77 -0
- package/src/components/PageAliasRedirect.astro +49 -0
- package/src/components/PageContentsNavigation.astro +21 -0
- package/src/components/PageList.astro +33 -0
- package/src/components/PageSequenceNavigation.astro +37 -0
- package/src/components/SearchPage.astro +151 -0
- package/src/components/SectionNavigationScript.astro +62 -6
- package/src/components/SiteNavigation.astro +50 -11
- package/src/components/SitePage.astro +113 -20
- package/src/components/SiteSection.astro +16 -3
- package/src/components/SiteTreeNavigation.astro +8 -2
- package/src/components/TreeNavigationScript.astro +207 -26
- package/src/content.config.ts +5 -2
- package/src/layouts/BaseLayout.astro +71 -10
- package/src/lib/readerPreferencesScript.mjs +2 -2
- package/src/lib/sectionContent.ts +32 -3
- package/src/lib/siteNavigation.ts +31 -52
- package/src/lib/sitePages.ts +3 -2
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/pages/404.astro +110 -0
- package/src/pages/[...slug].astro +45 -8
- package/src/styles/content.css +718 -0
- package/src/styles/foundations.css +284 -0
- package/src/styles/global.css +6 -2510
- package/src/styles/media.css +518 -0
- package/src/styles/navigation.css +526 -0
- package/src/styles/page-layout.css +786 -0
- package/src/styles/responsive.css +594 -0
- package/starters/basic/README.md +6 -19
- package/starters/basic/package.json +1 -0
- package/starters/basic/site/pages/000-home/content.md +13 -50
|
@@ -2,16 +2,12 @@ import { freezeDeep, mergeDeep } from './object.mjs';
|
|
|
2
2
|
|
|
3
3
|
export const themeProfileDefinitions = freezeDeep({
|
|
4
4
|
color: {
|
|
5
|
-
'near-monochrome-dark': {
|
|
6
|
-
colorMode: { default: 'dark' },
|
|
7
|
-
palette: 'near-monochrome',
|
|
8
|
-
},
|
|
9
5
|
'near-monochrome-adaptive': {
|
|
10
|
-
|
|
6
|
+
appearance: { default: 'system' },
|
|
11
7
|
palette: 'near-monochrome',
|
|
12
8
|
},
|
|
13
9
|
'warm-paper-adaptive': {
|
|
14
|
-
|
|
10
|
+
appearance: { default: 'system' },
|
|
15
11
|
palette: 'warm-paper',
|
|
16
12
|
},
|
|
17
13
|
},
|
|
@@ -112,6 +108,7 @@ export const themeProfileDefinitions = freezeDeep({
|
|
|
112
108
|
media: {
|
|
113
109
|
prominent: {
|
|
114
110
|
images: {
|
|
111
|
+
presentation: 'centered-fit',
|
|
115
112
|
width: '1000px',
|
|
116
113
|
maxAvailableWidthPercent: { desktop: 100, mobile: 100 },
|
|
117
114
|
maxAvailableHeightPercent: { desktop: 78, mobile: 68 },
|
|
@@ -119,20 +116,21 @@ export const themeProfileDefinitions = freezeDeep({
|
|
|
119
116
|
},
|
|
120
117
|
supporting: {
|
|
121
118
|
images: {
|
|
119
|
+
presentation: 'prose-aligned',
|
|
122
120
|
width: '920px',
|
|
123
121
|
maxAvailableWidthPercent: { desktop: 100, mobile: 100 },
|
|
124
|
-
maxAvailableHeightPercent: { desktop: 74, mobile: 68 },
|
|
125
122
|
},
|
|
126
123
|
},
|
|
127
124
|
balanced: {
|
|
128
125
|
images: {
|
|
126
|
+
presentation: 'prose-aligned',
|
|
129
127
|
width: '840px',
|
|
130
128
|
maxAvailableWidthPercent: { desktop: 100, mobile: 100 },
|
|
131
|
-
maxAvailableHeightPercent: { desktop: 70, mobile: 62 },
|
|
132
129
|
},
|
|
133
130
|
},
|
|
134
131
|
immersive: {
|
|
135
132
|
images: {
|
|
133
|
+
presentation: 'centered-fit',
|
|
136
134
|
width: '1080px',
|
|
137
135
|
maxAvailableWidthPercent: { desktop: 100, mobile: 100 },
|
|
138
136
|
maxAvailableHeightPercent: { desktop: 80, mobile: 70 },
|
|
@@ -32,8 +32,12 @@ const getLegacyThemeHint = (issue, data) => {
|
|
|
32
32
|
return 'Section background pattern "cycling" was replaced by "accented".';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
if (location === '
|
|
36
|
-
return '
|
|
35
|
+
if (location === '' && issue.keys?.includes('colorMode')) {
|
|
36
|
+
return 'Theme setting "colorMode" was replaced by "appearance".';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (location === 'readerControls' && issue.keys?.includes('colorMode')) {
|
|
40
|
+
return 'Reader control "colorMode" was replaced by "appearance".';
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
if (location === 'readerControls' && issue.keys?.includes('readingWidth')) {
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
access,
|
|
4
|
+
mkdir,
|
|
5
|
+
readFile,
|
|
6
|
+
rename,
|
|
7
|
+
rm,
|
|
8
|
+
rmdir,
|
|
9
|
+
writeFile,
|
|
10
|
+
} from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import {
|
|
13
|
+
createPageMovePlan,
|
|
14
|
+
formatPageMovePlan,
|
|
15
|
+
} from './lib/page-move-plan.mjs';
|
|
16
|
+
import {
|
|
17
|
+
getSiteLinkGraph,
|
|
18
|
+
getSitePublicFiles,
|
|
19
|
+
} from './lib/site-link-graph.mjs';
|
|
20
|
+
import {
|
|
21
|
+
sitePagesDir,
|
|
22
|
+
sitePagesLabel,
|
|
23
|
+
siteProjectRoot,
|
|
24
|
+
} from './lib/site-paths.mjs';
|
|
25
|
+
import { getSiteStructure } from './lib/site-structure.mjs';
|
|
26
|
+
|
|
27
|
+
const usage = `
|
|
28
|
+
Usage:
|
|
29
|
+
norna page:move <old-url> <new-url> [--order <NNN>] [--no-aliases] [--write]
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
norna page:move /guides/install/ /guides/installation/
|
|
33
|
+
norna page:move /guides/install/ /reference/install/ --write
|
|
34
|
+
|
|
35
|
+
The command is a dry run unless --write is present. If the old page has
|
|
36
|
+
already been moved by hand, the same command reconciles its links and aliases.
|
|
37
|
+
`.trim();
|
|
38
|
+
|
|
39
|
+
const parseOrder = (value) => {
|
|
40
|
+
if (!/^\d{1,3}$/.test(value ?? '')) {
|
|
41
|
+
throw new Error(`Invalid --order "${value ?? ''}". Use an integer from 1 to 999.\n${usage}`);
|
|
42
|
+
}
|
|
43
|
+
const order = Number.parseInt(value, 10);
|
|
44
|
+
if (order < 1 || order > 999) {
|
|
45
|
+
throw new Error(`Invalid --order "${value}". Use an integer from 1 to 999.\n${usage}`);
|
|
46
|
+
}
|
|
47
|
+
return order;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const parseArgs = (rawArgs) => {
|
|
51
|
+
const options = {
|
|
52
|
+
help: false,
|
|
53
|
+
order: null,
|
|
54
|
+
preserveAliases: true,
|
|
55
|
+
urls: [],
|
|
56
|
+
write: false,
|
|
57
|
+
};
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
|
|
60
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
61
|
+
const arg = rawArgs[index];
|
|
62
|
+
if (arg === '-h' || arg === '--help') {
|
|
63
|
+
options.help = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg === '--write') {
|
|
67
|
+
if (seen.has('write')) throw new Error(`--write may be specified only once.\n${usage}`);
|
|
68
|
+
seen.add('write');
|
|
69
|
+
options.write = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg === '--no-aliases') {
|
|
73
|
+
if (seen.has('aliases')) throw new Error(`--no-aliases may be specified only once.\n${usage}`);
|
|
74
|
+
seen.add('aliases');
|
|
75
|
+
options.preserveAliases = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg === '--order' || arg.startsWith('--order=')) {
|
|
79
|
+
if (seen.has('order')) throw new Error(`--order may be specified only once.\n${usage}`);
|
|
80
|
+
seen.add('order');
|
|
81
|
+
const inlineValue = arg.startsWith('--order=') ? arg.slice('--order='.length) : null;
|
|
82
|
+
const value = inlineValue ?? rawArgs[index + 1];
|
|
83
|
+
if (inlineValue === null) index += 1;
|
|
84
|
+
options.order = parseOrder(value);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (arg.startsWith('-')) throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
88
|
+
options.urls.push(arg);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!options.help && options.urls.length !== 2) {
|
|
92
|
+
throw new Error(`page:move requires an old URL and a new URL.\n${usage}`);
|
|
93
|
+
}
|
|
94
|
+
return options;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const toDisplayPath = (filePath) => {
|
|
98
|
+
const relativePath = path.relative(siteProjectRoot, filePath);
|
|
99
|
+
return (relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
|
|
100
|
+
? relativePath
|
|
101
|
+
: filePath).split(path.sep).join('/');
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const fileExists = async (filePath) => access(filePath).then(() => true, () => false);
|
|
105
|
+
|
|
106
|
+
const writeFileAtomically = async (filePath, source, mode) => {
|
|
107
|
+
const temporaryPath = path.join(
|
|
108
|
+
path.dirname(filePath),
|
|
109
|
+
`.${path.basename(filePath)}.norna-${process.pid}-${randomUUID()}.tmp`,
|
|
110
|
+
);
|
|
111
|
+
try {
|
|
112
|
+
await writeFile(temporaryPath, source, { mode: mode & 0o777 });
|
|
113
|
+
await rename(temporaryPath, filePath);
|
|
114
|
+
} finally {
|
|
115
|
+
await rm(temporaryPath, { force: true });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const assertSourcesUnchanged = async (plan) => {
|
|
120
|
+
for (const file of plan.sourceFiles) {
|
|
121
|
+
const currentSource = await readFile(file.contentPath, 'utf8');
|
|
122
|
+
if (currentSource !== file.source) {
|
|
123
|
+
throw new Error([
|
|
124
|
+
`Page move stopped because ${file.contentLabel} changed after the plan was created.`,
|
|
125
|
+
'Run the command again to build a fresh plan. No file was changed.',
|
|
126
|
+
].join('\n'));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const assertFinalSite = async () => {
|
|
132
|
+
const structure = await getSiteStructure();
|
|
133
|
+
const graph = await getSiteLinkGraph({ siteStructure: structure });
|
|
134
|
+
if (graph.diagnostics.length === 0) return;
|
|
135
|
+
throw new Error([
|
|
136
|
+
'Post-move link validation failed.',
|
|
137
|
+
...graph.diagnostics.flatMap((diagnostic) => [
|
|
138
|
+
`- ${diagnostic.message}`,
|
|
139
|
+
...(diagnostic.fix ? [` Fix: ${diagnostic.fix}`] : []),
|
|
140
|
+
]),
|
|
141
|
+
].join('\n'));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const applyPlan = async (plan) => {
|
|
145
|
+
await assertSourcesUnchanged(plan);
|
|
146
|
+
let collectionCreated = false;
|
|
147
|
+
let directoryMoved = false;
|
|
148
|
+
let fileWritten = false;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
if (plan.mode === 'move') {
|
|
152
|
+
if (!plan.destinationCollectionExists) {
|
|
153
|
+
await mkdir(plan.destinationCollectionDirectory);
|
|
154
|
+
collectionCreated = true;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await rename(plan.sourceDirectory, plan.destinationDirectory);
|
|
158
|
+
directoryMoved = true;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error?.code === 'EXDEV') {
|
|
161
|
+
throw new Error('Page directories must stay on one filesystem so Norna can move the complete subtree atomically.');
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const change of plan.fileChanges) {
|
|
168
|
+
await writeFileAtomically(change.finalPath, change.updatedSource, change.mode);
|
|
169
|
+
fileWritten = true;
|
|
170
|
+
}
|
|
171
|
+
await assertFinalSite();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const changesStarted = directoryMoved || fileWritten;
|
|
174
|
+
const rollbackErrors = [];
|
|
175
|
+
if (directoryMoved) {
|
|
176
|
+
try {
|
|
177
|
+
await rename(plan.destinationDirectory, plan.sourceDirectory);
|
|
178
|
+
directoryMoved = false;
|
|
179
|
+
} catch (rollbackError) {
|
|
180
|
+
rollbackErrors.push(`Could not restore the page directory: ${rollbackError.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const change of plan.fileChanges) {
|
|
185
|
+
try {
|
|
186
|
+
const restorePath = directoryMoved && change.finalPath !== change.originalPath
|
|
187
|
+
? change.finalPath
|
|
188
|
+
: change.originalPath;
|
|
189
|
+
await writeFileAtomically(restorePath, change.originalSource, change.mode);
|
|
190
|
+
} catch (rollbackError) {
|
|
191
|
+
rollbackErrors.push(`Could not restore ${change.contentLabel}: ${rollbackError.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (collectionCreated && !directoryMoved) {
|
|
196
|
+
try {
|
|
197
|
+
await rmdir(plan.destinationCollectionDirectory);
|
|
198
|
+
} catch (rollbackError) {
|
|
199
|
+
if (rollbackError?.code !== 'ENOENT' && rollbackError?.code !== 'ENOTEMPTY') {
|
|
200
|
+
rollbackErrors.push(`Could not remove the newly created pages directory: ${rollbackError.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
throw new Error([
|
|
206
|
+
error instanceof Error ? error.message : String(error),
|
|
207
|
+
rollbackErrors.length === 0
|
|
208
|
+
? (changesStarted
|
|
209
|
+
? 'The original page directory and content files were restored.'
|
|
210
|
+
: 'No source file was changed.')
|
|
211
|
+
: 'Rollback was incomplete. Inspect these paths before running page:move again:',
|
|
212
|
+
...rollbackErrors.map((message) => `- ${message}`),
|
|
213
|
+
].join('\n'));
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const main = async () => {
|
|
218
|
+
const options = parseArgs(process.argv.slice(2));
|
|
219
|
+
if (options.help) {
|
|
220
|
+
console.log(usage);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const siteStructure = await getSiteStructure();
|
|
225
|
+
const [graph, publicFiles] = await Promise.all([
|
|
226
|
+
getSiteLinkGraph({ siteStructure }),
|
|
227
|
+
getSitePublicFiles(),
|
|
228
|
+
]);
|
|
229
|
+
const plan = await createPageMovePlan({
|
|
230
|
+
from: options.urls[0],
|
|
231
|
+
graph,
|
|
232
|
+
order: options.order,
|
|
233
|
+
preserveAliases: options.preserveAliases,
|
|
234
|
+
publicFiles,
|
|
235
|
+
sitePagesDir,
|
|
236
|
+
sitePagesLabel,
|
|
237
|
+
siteStructure,
|
|
238
|
+
to: options.urls[1],
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
console.log(formatPageMovePlan(plan, toDisplayPath).join('\n'));
|
|
242
|
+
if (!options.write) {
|
|
243
|
+
console.log('\nDry run only. Run the same command with --write to apply this plan.');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await applyPlan(plan);
|
|
248
|
+
console.log(`\nPage move completed: ${plan.from} -> ${plan.to}`);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
await main();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatNavigationReviewText,
|
|
3
|
+
getNavigationReview,
|
|
4
|
+
parseNavigationReviewArgs,
|
|
5
|
+
} from './lib/navigation-review.mjs';
|
|
6
|
+
|
|
7
|
+
const usage = `
|
|
8
|
+
Usage: norna navigation:review [--format text|json]
|
|
9
|
+
|
|
10
|
+
Read the selected site and report its page hierarchy, heading outlines,
|
|
11
|
+
internal page links, effective navigation modes, and structural review prompts.
|
|
12
|
+
No files are changed.
|
|
13
|
+
`.trim();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const { format, help } = parseNavigationReviewArgs(process.argv.slice(2));
|
|
17
|
+
if (help) {
|
|
18
|
+
console.log(usage);
|
|
19
|
+
} else {
|
|
20
|
+
const { projectConfig } = await import('./lib/project-config.mjs');
|
|
21
|
+
const review = await getNavigationReview({
|
|
22
|
+
requestedNavigationMode: projectConfig.navigation.mode,
|
|
23
|
+
});
|
|
24
|
+
process.stdout.write(format === 'json'
|
|
25
|
+
? `${JSON.stringify(review, null, 2)}\n`
|
|
26
|
+
: formatNavigationReviewText(review));
|
|
27
|
+
if (review.errors.length > 0) process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|