@janga/norna 0.7.26 → 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 +51 -35
- package/astro.config.mjs +19 -1
- package/bin/norna-cli.mjs +6 -4
- 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 +17 -8
- 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 +33 -2
- package/scripts/lib/documentation-routes.json +229 -0
- 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 +39 -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 +12 -39
- 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/PageAliasRedirect.astro +44 -3
- 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 +15 -16
- 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 +12 -23
- 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 +5 -31
- 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 +10 -9
- 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
|
@@ -1,26 +1,15 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, realpath, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
|
|
3
1
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { pageDirectoryPattern } from './lib/page-model.mjs';
|
|
7
|
-
import {
|
|
8
|
-
invocationRoot,
|
|
9
|
-
siteDir,
|
|
10
|
-
sitePagesDir,
|
|
11
|
-
siteProjectRoot,
|
|
12
|
-
} from './lib/site-paths.mjs';
|
|
13
|
-
import { getSiteStructure } from './lib/site-structure.mjs';
|
|
2
|
+
import { invocationRoot, siteDir, siteProjectRoot } from './lib/site-paths.mjs';
|
|
3
|
+
import { createSiteNode, planSiteNodeCreation } from './lib/site-node-create.mjs';
|
|
14
4
|
|
|
15
5
|
const usage = `
|
|
16
6
|
Usage:
|
|
17
7
|
norna page:add <title> [--parent <path>] [--slug <slug>] [--order <NNN>] [--dry-run]
|
|
18
|
-
norna category:add <label> [--parent <path>] [--slug <slug>] [--order <NNN>] [--dry-run]
|
|
19
8
|
|
|
20
9
|
Parent selection:
|
|
21
10
|
--parent / Add a top-level node
|
|
22
11
|
--parent /guides/ Add a child below the guides node
|
|
23
|
-
no --parent Use the current node directory, or site/pages/
|
|
12
|
+
no --parent Use the site root, current node directory, or site/pages/
|
|
24
13
|
`.trim();
|
|
25
14
|
|
|
26
15
|
const parseValueOption = (rawArgs, index, name) => {
|
|
@@ -85,38 +74,6 @@ const parseArgs = (rawArgs) => {
|
|
|
85
74
|
return options;
|
|
86
75
|
};
|
|
87
76
|
|
|
88
|
-
const parseOrder = (value) => {
|
|
89
|
-
if (!/^\d{1,3}$/.test(value)) {
|
|
90
|
-
throw new Error(`Invalid --order "${value}". Use an integer from 1 to 999.`);
|
|
91
|
-
}
|
|
92
|
-
const order = Number.parseInt(value, 10);
|
|
93
|
-
if (order < 1 || order > 999) {
|
|
94
|
-
throw new Error(`Invalid --order "${value}". Use an integer from 1 to 999.`);
|
|
95
|
-
}
|
|
96
|
-
return order;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const normalizeParentPath = (value) => {
|
|
100
|
-
if (value === '/') return '';
|
|
101
|
-
if (value.includes('\\') || value.includes('?') || value.includes('#')) {
|
|
102
|
-
throw new Error(`Invalid --parent "${value}". Use a logical path such as /guides/installation/.`);
|
|
103
|
-
}
|
|
104
|
-
const normalized = value.replace(/^\/+|\/+$/g, '');
|
|
105
|
-
if (!normalized || normalized.split('/').some((segment) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(segment))) {
|
|
106
|
-
throw new Error(`Invalid --parent "${value}". Use / for the top level or a logical path such as /guides/installation/.`);
|
|
107
|
-
}
|
|
108
|
-
return normalized;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const getNextOrder = (siblings) => {
|
|
112
|
-
const highestOrder = siblings.reduce((highest, node) => Math.max(highest, node.pageOrder), 0);
|
|
113
|
-
const nextOrder = Math.floor(highestOrder / 10) * 10 + 10;
|
|
114
|
-
if (nextOrder > 999) {
|
|
115
|
-
throw new Error('No automatic page order remains below 1000. Reorder the sibling nodes before adding another one.');
|
|
116
|
-
}
|
|
117
|
-
return nextOrder;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
77
|
const toDisplayPath = (filePath) => {
|
|
121
78
|
const relativePath = path.relative(siteProjectRoot, filePath);
|
|
122
79
|
return (relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
|
|
@@ -124,85 +81,8 @@ const toDisplayPath = (filePath) => {
|
|
|
124
81
|
: filePath).split(path.sep).join('/');
|
|
125
82
|
};
|
|
126
83
|
|
|
127
|
-
const escapeMarkdownHeading = (value) => value.replace(/([\\`*_[\]{}<>#])/g, '\\$1');
|
|
128
|
-
|
|
129
|
-
const canonicalDirectory = async (directory) => realpath(directory).catch((error) => {
|
|
130
|
-
if (error?.code === 'ENOENT') return path.resolve(directory);
|
|
131
|
-
throw error;
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const resolveParent = async ({ nodes, parent }) => {
|
|
135
|
-
if (parent !== null) {
|
|
136
|
-
const parentPath = normalizeParentPath(parent);
|
|
137
|
-
if (!parentPath) {
|
|
138
|
-
return { collectionDir: sitePagesDir, node: null, pagePath: '' };
|
|
139
|
-
}
|
|
140
|
-
const node = nodes.find((candidate) => candidate.pagePath === parentPath);
|
|
141
|
-
if (!node) {
|
|
142
|
-
throw new Error(`Cannot find parent "${parent}". Use an existing logical page/category path, or / for the top level.`);
|
|
143
|
-
}
|
|
144
|
-
if (node.isHome) throw new Error('The homepage cannot contain child pages or categories.');
|
|
145
|
-
return { collectionDir: path.join(node.nodeDir, 'pages'), node, pagePath: node.pagePath };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const currentDirectory = await canonicalDirectory(invocationRoot);
|
|
149
|
-
if (currentDirectory === await canonicalDirectory(sitePagesDir)) {
|
|
150
|
-
return { collectionDir: sitePagesDir, node: null, pagePath: '' };
|
|
151
|
-
}
|
|
152
|
-
let node = null;
|
|
153
|
-
for (const candidate of nodes) {
|
|
154
|
-
if (await canonicalDirectory(candidate.nodeDir) === currentDirectory) {
|
|
155
|
-
node = candidate;
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (!node) {
|
|
160
|
-
throw new Error([
|
|
161
|
-
'Cannot infer where to add the node from the current directory.',
|
|
162
|
-
`Run the command from ${toDisplayPath(sitePagesDir)} for a top-level node, from an existing page/category directory for a child, or pass --parent.`,
|
|
163
|
-
].join('\n'));
|
|
164
|
-
}
|
|
165
|
-
if (node.isHome) throw new Error('The homepage cannot contain child pages or categories.');
|
|
166
|
-
return { collectionDir: path.join(node.nodeDir, 'pages'), node, pagePath: node.pagePath };
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const assertCollectionDirectory = async (collectionDir) => {
|
|
170
|
-
try {
|
|
171
|
-
const info = await stat(collectionDir);
|
|
172
|
-
if (!info.isDirectory()) throw new Error(`${toDisplayPath(collectionDir)} exists but is not a directory.`);
|
|
173
|
-
return false;
|
|
174
|
-
} catch (error) {
|
|
175
|
-
if (error?.code !== 'ENOENT') throw error;
|
|
176
|
-
await mkdir(collectionDir);
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const writeNode = async ({ destination, kind, title }) => {
|
|
182
|
-
const stagingRoot = path.join(siteDir, '.norna', 'create');
|
|
183
|
-
await mkdir(stagingRoot, { recursive: true });
|
|
184
|
-
const stagingDirectory = path.join(stagingRoot, `node-${process.pid}-${randomUUID()}`);
|
|
185
|
-
await mkdir(stagingDirectory);
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
if (kind === 'page') {
|
|
189
|
-
await writeFile(path.join(stagingDirectory, 'content.md'), `# ${escapeMarkdownHeading(title)}\n\n## Introduction\n\nStart writing here.\n`);
|
|
190
|
-
await mkdir(path.join(stagingDirectory, 'images'));
|
|
191
|
-
} else {
|
|
192
|
-
await writeFile(path.join(stagingDirectory, 'category.yaml'), dumpYaml({ label: title }, {
|
|
193
|
-
lineWidth: -1,
|
|
194
|
-
noRefs: true,
|
|
195
|
-
}));
|
|
196
|
-
await mkdir(path.join(stagingDirectory, 'pages'));
|
|
197
|
-
}
|
|
198
|
-
await rename(stagingDirectory, destination);
|
|
199
|
-
} finally {
|
|
200
|
-
await rm(stagingDirectory, { force: true, recursive: true });
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
|
|
204
84
|
const [kind, ...rawArgs] = process.argv.slice(2);
|
|
205
|
-
if (
|
|
85
|
+
if (kind !== 'page') throw new Error(usage);
|
|
206
86
|
const options = parseArgs(rawArgs);
|
|
207
87
|
if (options.help) {
|
|
208
88
|
console.log(usage);
|
|
@@ -210,42 +90,14 @@ if (options.help) {
|
|
|
210
90
|
}
|
|
211
91
|
|
|
212
92
|
const title = options.titleParts.join(' ').trim();
|
|
213
|
-
if (!title || /[\r\n]/.test(title)) throw new Error(`A one-line
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const structure = await getSiteStructure();
|
|
220
|
-
const parent = await resolveParent({ nodes: structure.nodes, parent: options.parent });
|
|
221
|
-
const siblings = structure.nodes.filter((node) => node.parentPagePath === (parent.pagePath || null) && !node.isHome);
|
|
222
|
-
if (siblings.some((node) => node.pageId === slug)) {
|
|
223
|
-
throw new Error(`Cannot create "${slug}" below ${parent.pagePath ? `/${parent.pagePath}/` : '/'}. A sibling with that slug already exists.`);
|
|
224
|
-
}
|
|
225
|
-
const order = options.order === null ? getNextOrder(siblings) : parseOrder(options.order);
|
|
226
|
-
if (siblings.some((node) => node.pageOrder === order)) {
|
|
227
|
-
throw new Error(`Cannot use order ${String(order).padStart(3, '0')} below ${parent.pagePath ? `/${parent.pagePath}/` : '/'}. A sibling already uses it.`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const directoryName = `${String(order).padStart(3, '0')}-${slug}`;
|
|
231
|
-
const destination = path.join(parent.collectionDir, directoryName);
|
|
232
|
-
const pagePath = [parent.pagePath, slug].filter(Boolean).join('/');
|
|
93
|
+
if (!title || /[\r\n]/.test(title)) throw new Error(`A one-line page title is required.\n${usage}`);
|
|
94
|
+
const plan = await planSiteNodeCreation({
|
|
95
|
+
siteRoot: siteDir, kind, title, slug: options.slug, order: options.order,
|
|
96
|
+
parentPath: options.parent, invocationDirectory: invocationRoot,
|
|
97
|
+
});
|
|
98
|
+
const { destination, pagePath } = plan;
|
|
233
99
|
const action = options.dryRun ? 'Would create' : 'Created';
|
|
100
|
+
if (!options.dryRun) await createSiteNode(plan);
|
|
234
101
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
await writeNode({ destination, kind, title });
|
|
239
|
-
} catch (error) {
|
|
240
|
-
if (createdCollection) await rmdir(parent.collectionDir).catch(() => {});
|
|
241
|
-
throw error;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
console.log(`${action} ${kind} "${title}" at ${toDisplayPath(destination)}.`);
|
|
246
|
-
if (kind === 'page') {
|
|
247
|
-
console.log(`URL: /${pagePath}/`);
|
|
248
|
-
} else {
|
|
249
|
-
console.log('This navigation category has no page or URL of its own.');
|
|
250
|
-
console.log(`Child URL prefix: /${pagePath}/`);
|
|
251
|
-
}
|
|
102
|
+
console.log(`${action} page "${title}" at ${toDisplayPath(destination)}.`);
|
|
103
|
+
console.log(`URL: /${pagePath}/`);
|
package/scripts/build-site.mjs
CHANGED
|
@@ -9,9 +9,14 @@ const runScript = (relativePath, args = []) => runInherit(
|
|
|
9
9
|
{ cwd: siteProjectRoot },
|
|
10
10
|
);
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
await runScript('scripts/
|
|
14
|
-
await runScript('scripts/sync-
|
|
15
|
-
await runScript('scripts/
|
|
16
|
-
await
|
|
17
|
-
await
|
|
12
|
+
try {
|
|
13
|
+
await runScript('scripts/check-config.mjs');
|
|
14
|
+
await runScript('scripts/sync-content-sections.mjs', ['--check']);
|
|
15
|
+
await runScript('scripts/sync-site-public.mjs');
|
|
16
|
+
await runScript('scripts/generate-images.mjs');
|
|
17
|
+
await runAstroInherit(['build']);
|
|
18
|
+
await runScript('scripts/generate-search-index.mjs');
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(error.message);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
}
|
package/scripts/check-config.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { siteConfigLabel, sitePagesDir, sitePublicLabel, siteThemeLabel } from './lib/site-paths.mjs';
|
|
1
|
+
import { siteConfigLabel, siteDir, sitePublicLabel } from './lib/site-paths.mjs';
|
|
3
2
|
import { getLogoAssets, getPublicAssetInspection } from './lib/logo-assets.mjs';
|
|
4
3
|
import { logoAssetFilenames, socialImageAssetFilenames } from './lib/public-asset-conventions.mjs';
|
|
5
4
|
import { getSocialImageAssets } from './lib/social-image-assets.mjs';
|
|
6
5
|
import { readSitewideContent } from './lib/sitewide-content.mjs';
|
|
7
6
|
import { assertSectionBackgroundPatternCompatibility } from './lib/presentation.mjs';
|
|
8
7
|
import { readThemeConfig, validatePageThemeFiles } from './lib/theme-config.mjs';
|
|
9
|
-
import {
|
|
8
|
+
import { selectPageTheme } from './lib/theme-packages.mjs';
|
|
10
9
|
|
|
11
10
|
const formatErrorMessage = (error) => {
|
|
12
11
|
if (error instanceof Error) {
|
|
@@ -21,26 +20,11 @@ try {
|
|
|
21
20
|
const themeConfig = await readThemeConfig();
|
|
22
21
|
const sitewideContent = await readSitewideContent();
|
|
23
22
|
const pageThemeFiles = await validatePageThemeFiles();
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const ancestorTheme = pageThemesByDirectory.get(directory);
|
|
30
|
-
if (ancestorTheme) ancestorThemes.unshift(ancestorTheme);
|
|
31
|
-
directory = path.dirname(directory);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const resolvedPageTheme = ancestorThemes.reduce(
|
|
35
|
-
(current, ancestor) => mergePageThemeConfig(current, ancestor.config),
|
|
36
|
-
themeConfig,
|
|
37
|
-
);
|
|
38
|
-
resolveThemeVisualConfig(resolvedPageTheme, pageThemeFile.label);
|
|
39
|
-
}
|
|
40
|
-
if (projectConfig.navigation.mode === 'tree') {
|
|
41
|
-
assertSectionBackgroundPatternCompatibility(themeConfig, siteThemeLabel, 'tree');
|
|
42
|
-
for (const pageThemeFile of pageThemeFiles) {
|
|
43
|
-
assertSectionBackgroundPatternCompatibility(pageThemeFile.config, pageThemeFile.label, 'tree');
|
|
23
|
+
for (const pageDirectory of new Set(['.', ...pageThemeFiles.map((file) => file.pageDirectory)])) {
|
|
24
|
+
const { treeTheme, selected } = await selectPageTheme({ siteRoot: siteDir, pageDirectory });
|
|
25
|
+
for (const theme of [treeTheme, selected]) {
|
|
26
|
+
resolveThemeVisualConfig(theme.config, theme.label);
|
|
27
|
+
if (projectConfig.navigation.mode === 'tree') assertSectionBackgroundPatternCompatibility(theme.config, theme.label, 'tree');
|
|
44
28
|
}
|
|
45
29
|
}
|
|
46
30
|
const logoAssets = getLogoAssets();
|
package/scripts/dev-local.mjs
CHANGED
|
@@ -18,12 +18,24 @@ const port = Number.parseInt(process.env.NORNA_DEV_PORT ?? '4321', 10);
|
|
|
18
18
|
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
19
19
|
throw new Error('NORNA_DEV_PORT must be an integer from 1 through 65535.');
|
|
20
20
|
}
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const knownCommands = new Set(['start', 'lan', 'status', 'logs', 'restart', 'stop']);
|
|
23
|
+
const command = args.find((arg) => knownCommands.has(arg)) ?? args.find((arg) => !arg.startsWith('-')) ?? 'start';
|
|
24
|
+
const shouldKillBlockingPort = args.includes('--kill');
|
|
25
|
+
const shouldFollowLogs = args.includes('--follow');
|
|
26
|
+
|
|
21
27
|
let projectConfig;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
if (['stop', 'status', 'logs'].includes(command)) {
|
|
29
|
+
// Recovery commands use the recorded server even while sources need conversion.
|
|
30
|
+
const recorded = await readFile(path.join(siteStateDir, 'dev', 'state.json'), 'utf8').then(JSON.parse).catch(() => null);
|
|
31
|
+
projectConfig = { site: { basePath: recorded?.url ? new URL(recorded.url).pathname : '/' } };
|
|
32
|
+
} else {
|
|
33
|
+
try {
|
|
34
|
+
({ default: projectConfig } = await import('./lib/project-config.mjs'));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
27
39
|
}
|
|
28
40
|
const localHost = '127.0.0.1';
|
|
29
41
|
const localUrl = `http://${localHost}:${port}${projectConfig.site.basePath}`;
|
|
@@ -38,11 +50,6 @@ const legacyStatePath = path.join(astroCacheDir, 'dev-local.json');
|
|
|
38
50
|
const astroStatePath = path.join(astroCacheDir, 'dev.json');
|
|
39
51
|
const logPath = path.join(astroCacheDir, 'dev.log');
|
|
40
52
|
const preparationLogPath = path.join(stateDirectory, 'preparation.log');
|
|
41
|
-
const args = process.argv.slice(2);
|
|
42
|
-
const knownCommands = new Set(['start', 'lan', 'status', 'logs', 'restart', 'stop']);
|
|
43
|
-
const command = args.find((arg) => knownCommands.has(arg)) ?? args.find((arg) => !arg.startsWith('-')) ?? 'start';
|
|
44
|
-
const shouldKillBlockingPort = args.includes('--kill');
|
|
45
|
-
const shouldFollowLogs = args.includes('--follow');
|
|
46
53
|
|
|
47
54
|
const runAstro = async (args, options = {}) => execFileAsync(process.execPath, getAstroArgs(args), {
|
|
48
55
|
cwd: siteProjectRoot,
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
renderThemePresetReference,
|
|
5
5
|
themePresetNames,
|
|
6
6
|
} from './lib/theme-presets.mjs';
|
|
7
|
-
import { siteDir, siteThemeLabel } from './lib/site-paths.mjs';
|
|
7
|
+
import { siteDir, siteDirLabel, siteThemeLabel } from './lib/site-paths.mjs';
|
|
8
8
|
|
|
9
9
|
const [presetName, ...extraArgs] = process.argv.slice(2);
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ if (!presetName || extraArgs.length > 0) {
|
|
|
14
14
|
|
|
15
15
|
const filename = `orig-${presetName}-theme.yaml`;
|
|
16
16
|
const outputPath = path.join(siteDir, filename);
|
|
17
|
-
const outputLabel = path.posix.join(
|
|
17
|
+
const outputLabel = path.posix.join(siteDirLabel, filename);
|
|
18
18
|
const source = renderThemePresetReference(presetName, siteThemeLabel);
|
|
19
19
|
|
|
20
20
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getPageImageSourceKey } from './lib/site-conventions.mjs';
|
|
1
2
|
import { execFile } from 'node:child_process';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
@@ -84,12 +85,12 @@ const getFilePathFromPublicPath = (publicPath) => path.join(astroPublicDir, publ
|
|
|
84
85
|
const getSourceHashSlug = (sourceHash) => sourceHash.slice(0, sourceHashSlugLength);
|
|
85
86
|
|
|
86
87
|
const getGeneratedPath = (sourceKey, sourceHash, width) => {
|
|
87
|
-
const parsed = path.parse(sourceKey);
|
|
88
|
+
const parsed = path.parse(sourceKey.replace(/^root\//, ''));
|
|
88
89
|
return path.join(generatedImagesDir, parsed.dir, `${parsed.name}-${getSourceHashSlug(sourceHash)}-${width}.webp`);
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
const getOriginalPath = (sourceKey, sourceHash) => {
|
|
92
|
-
const parsed = path.parse(sourceKey);
|
|
93
|
+
const parsed = path.parse(sourceKey.replace(/^root\//, ''));
|
|
93
94
|
return path.join(originalImagesDir, parsed.dir, `${parsed.name}-${getSourceHashSlug(sourceHash)}${parsed.ext}`);
|
|
94
95
|
};
|
|
95
96
|
|
|
@@ -97,7 +98,7 @@ const fail = (message) => {
|
|
|
97
98
|
throw new Error(message);
|
|
98
99
|
};
|
|
99
100
|
|
|
100
|
-
const getImageSourceKey = (contentFile, image) =>
|
|
101
|
+
const getImageSourceKey = (contentFile, image) => getPageImageSourceKey(contentFile.pageDirectory, image);
|
|
101
102
|
|
|
102
103
|
const getReferencedImages = async (contentFile) => {
|
|
103
104
|
const { body } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
|
|
@@ -262,7 +263,7 @@ const hasGeneratedVariants = async (variants) => {
|
|
|
262
263
|
return true;
|
|
263
264
|
};
|
|
264
265
|
|
|
265
|
-
const getReusableEntry = async (
|
|
266
|
+
const getReusableEntry = async (sourceKey, previousEntry, sourceHash) => {
|
|
266
267
|
if (
|
|
267
268
|
previousEntry?.sourceHash !== sourceHash
|
|
268
269
|
|| previousEntry?.outputVersion !== imageOutputVersion
|
|
@@ -272,7 +273,7 @@ const getReusableEntry = async (sourcePath, previousEntry, sourceHash) => {
|
|
|
272
273
|
return null;
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
const variants = getVariants(
|
|
276
|
+
const variants = getVariants(sourceKey, sourceHash, getVariantWidths(previousEntry));
|
|
276
277
|
|
|
277
278
|
if (!(await hasGeneratedVariants(variants))) {
|
|
278
279
|
return null;
|
|
@@ -393,7 +394,7 @@ for (const { sourcePath, sourceKey } of sources) {
|
|
|
393
394
|
continue;
|
|
394
395
|
}
|
|
395
396
|
|
|
396
|
-
const reusableEntry = await getReusableEntry(
|
|
397
|
+
const reusableEntry = await getReusableEntry(sourceKey, previousManifest[sourceKey], sourceHash);
|
|
397
398
|
|
|
398
399
|
if (reusableEntry) {
|
|
399
400
|
manifest[sourceKey] = reusableEntry;
|
|
@@ -3,22 +3,23 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { z } from 'astro/zod';
|
|
5
5
|
import {
|
|
6
|
-
categorySchema,
|
|
7
6
|
configSchema,
|
|
7
|
+
rootThemeSchema,
|
|
8
8
|
pageThemeSchema,
|
|
9
9
|
siteSchema,
|
|
10
10
|
sitewideSchema,
|
|
11
11
|
themeVisualSchema,
|
|
12
12
|
} from './lib/schema-definitions.mjs';
|
|
13
13
|
import { applySchemaEditorMetadata } from './lib/schema-editor-metadata.mjs';
|
|
14
|
+
import { sourceFileDefinitions, sourcePageDirectoryPattern } from './lib/source-files.mjs';
|
|
14
15
|
import { getNornaBlockSchema } from './lib/norna-markdown-blocks.mjs';
|
|
15
16
|
|
|
16
17
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
18
|
const schemaDirectory = path.join(root, 'schemas');
|
|
18
19
|
const definitions = [
|
|
19
|
-
['category.schema.json', 'Norna page category', categorySchema],
|
|
20
20
|
['config.schema.json', 'Norna site configuration', configSchema],
|
|
21
|
-
['theme.schema.json', 'Norna
|
|
21
|
+
['theme.schema.json', 'Norna inherited tree theme', themeVisualSchema],
|
|
22
|
+
['root-theme.schema.json', 'Norna root tree theme', rootThemeSchema],
|
|
22
23
|
['page-theme.schema.json', 'Norna page theme', pageThemeSchema],
|
|
23
24
|
['sitewide-content.schema.json', 'Norna site-wide content', sitewideSchema],
|
|
24
25
|
['content-frontmatter.schema.json', 'Norna page frontmatter', siteSchema],
|
|
@@ -44,15 +45,17 @@ for (const type of blockTypes) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
schemaFiles.set('manifest.json', `${JSON.stringify({
|
|
47
|
-
editorApiVersion:
|
|
48
|
-
schemaVersion:
|
|
48
|
+
editorApiVersion: 3,
|
|
49
|
+
schemaVersion: 6,
|
|
50
|
+
sourceFiles: sourceFileDefinitions,
|
|
51
|
+
pageDirectoryPattern: sourcePageDirectoryPattern,
|
|
49
52
|
blockSchemas: Object.fromEntries(blockTypes.map((type) => [type, `${type}.schema.json`])),
|
|
50
53
|
files: {
|
|
51
|
-
category: 'category.schema.json',
|
|
52
54
|
config: 'config.schema.json',
|
|
53
55
|
contentFrontmatter: 'content-frontmatter.schema.json',
|
|
54
56
|
sitewideContent: 'sitewide-content.schema.json',
|
|
55
57
|
theme: 'theme.schema.json',
|
|
58
|
+
rootTheme: 'root-theme.schema.json',
|
|
56
59
|
pageTheme: 'page-theme.schema.json',
|
|
57
60
|
},
|
|
58
61
|
}, null, 2)}\n`);
|
|
@@ -9,28 +9,28 @@ export const createCategoryDestinationModel = (nodes) => {
|
|
|
9
9
|
const visit = (branch) => {
|
|
10
10
|
const { node } = branch;
|
|
11
11
|
if (node.navigation?.listed === false) return false;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Empty child categories retain their URLs but do not become menu choices
|
|
13
|
+
// or redirect targets for a parent that still has reachable pages.
|
|
14
|
+
const children = branch.children.filter(visit);
|
|
14
15
|
if (node.kind !== 'category') return true;
|
|
15
|
-
if (
|
|
16
|
+
if (children.length === 0) {
|
|
16
17
|
const source = node.categorySourceLabel ?? node.nodeLabel ?? node.pageDirectory;
|
|
17
18
|
diagnostics.push({
|
|
18
19
|
code: 'category-without-listed-content',
|
|
19
|
-
severity: '
|
|
20
|
+
severity: 'warning',
|
|
20
21
|
message: `${source} has no listed reachable content page.`,
|
|
21
|
-
fix: 'Add a listed page
|
|
22
|
+
fix: 'Add a listed page to include this category in navigation, or leave it empty while reorganizing.',
|
|
22
23
|
});
|
|
23
|
-
return false;
|
|
24
24
|
}
|
|
25
|
-
const first = children[0]
|
|
25
|
+
const first = children[0]?.node;
|
|
26
26
|
destinations.push({
|
|
27
27
|
category: node,
|
|
28
28
|
pathname: getSiteNodePathname(node),
|
|
29
|
-
kind: first
|
|
30
|
-
target: first
|
|
29
|
+
kind: first?.kind === 'page' ? 'redirect' : 'listing',
|
|
30
|
+
target: first?.kind === 'page' ? first : null,
|
|
31
31
|
children: children.map(({ node: child }) => child),
|
|
32
32
|
});
|
|
33
|
-
return
|
|
33
|
+
return children.length > 0;
|
|
34
34
|
};
|
|
35
35
|
getSiteNavigationTree(nodes.map((node) => ({ node }))).forEach(visit);
|
|
36
36
|
return {
|
|
@@ -41,8 +41,9 @@ export const createCategoryDestinationModel = (nodes) => {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
export const assertCategoryDestinationModel = (model) => {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const errors = model.diagnostics.filter(({ severity }) => severity === 'error');
|
|
45
|
+
if (errors.length) {
|
|
46
|
+
throw new Error(errors.map(({ message, fix }) => `${message}\n${fix}`).join('\n'));
|
|
46
47
|
}
|
|
47
48
|
return model;
|
|
48
49
|
};
|
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import packageJson from '../../package.json' with { type: 'json' };
|
|
2
|
+
import routes from './documentation-routes.json' with { type: 'json' };
|
|
3
|
+
|
|
4
|
+
const websiteRoot = 'https://janga.github.io/norna/reference/';
|
|
5
|
+
const repositoryRoot = 'https://github.com/janga/norna/blob/';
|
|
6
|
+
|
|
7
|
+
// Release tags through 0.7.26 contain the reference under docs/. Newer tags
|
|
8
|
+
// contain its single authored source in the documentation site's page tree.
|
|
9
|
+
export const usesLegacyReference = (version) => {
|
|
10
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version ?? '');
|
|
11
|
+
if (!match) return false;
|
|
12
|
+
const [major, minor, patch] = match.slice(1).map(Number);
|
|
13
|
+
return major === 0 && (minor < 7 || (minor === 7 && patch <= 26));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const referenceDestination = (file, anchor) => {
|
|
17
|
+
const target = routes.legacy[`${file}#${anchor}`] ?? routes.legacy[file];
|
|
18
|
+
if (!target) throw new Error(`Unknown documentation destination: ${file}${anchor ? `#${anchor}` : ''}`);
|
|
19
|
+
const [route, targetAnchor] = target.split('#');
|
|
20
|
+
const source = routes.sources[route];
|
|
21
|
+
if (!source) throw new Error(`Missing documentation source for ${route}`);
|
|
22
|
+
return { route, source, anchor: targetAnchor, url: websiteRoot + target };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const documentationLinkForVersion = (version, label, file, anchor) => {
|
|
26
|
+
const destination = referenceDestination(file, anchor);
|
|
27
|
+
const reference = version ? `v${version}` : 'main';
|
|
28
|
+
const source = usesLegacyReference(version)
|
|
29
|
+
? `docs/${file}${anchor ? `#${anchor}` : ''}`
|
|
30
|
+
: `${version === '0.7.27' ? destination.source.replace('site/root/pages/', 'site/pages/') : destination.source}${destination.anchor ? `#${destination.anchor}` : ''}`;
|
|
31
|
+
const sourceLabel = version ? `Norna ${version} source reference` : 'Reference source';
|
|
32
|
+
return `[${label} (current)](${destination.url})\n\n[${sourceLabel}](${repositoryRoot}${reference}/${source})`;
|
|
33
|
+
};
|
|
2
34
|
|
|
3
35
|
export const documentationRef = `v${packageJson.version}`;
|
|
4
|
-
export const documentationRoot = `https://github.com/janga/norna/blob/${documentationRef}/docs`;
|
|
5
36
|
|
|
6
37
|
export const documentationLink = (label, file, anchor) => (
|
|
7
|
-
|
|
38
|
+
documentationLinkForVersion(packageJson.version, label, file, anchor)
|
|
8
39
|
);
|