@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.
Files changed (88) hide show
  1. package/README.md +14 -18
  2. package/astro.config.mjs +15 -0
  3. package/bin/norna-cli.mjs +8 -2
  4. package/package.json +28 -3
  5. package/schemas/category.schema.json +2 -2
  6. package/schemas/config.schema.json +52 -24
  7. package/schemas/content-frontmatter.schema.json +25 -5
  8. package/schemas/page-theme.schema.json +53 -25
  9. package/schemas/sitewide-content.schema.json +19 -18
  10. package/schemas/theme.schema.json +385 -252
  11. package/scripts/build-site.mjs +1 -0
  12. package/scripts/check-config.mjs +36 -5
  13. package/scripts/deploy-site.mjs +0 -1
  14. package/scripts/dev-local.mjs +175 -112
  15. package/scripts/generate-search-index.mjs +57 -0
  16. package/scripts/init-site.mjs +1 -0
  17. package/scripts/lib/code-fence-metadata.mjs +223 -0
  18. package/scripts/lib/content-sync-apply.mjs +34 -0
  19. package/scripts/lib/content-sync-plan.mjs +179 -0
  20. package/scripts/lib/edit-source-link.mjs +49 -0
  21. package/scripts/lib/editor-language-service.mjs +160 -12
  22. package/scripts/lib/heading-ids.mjs +1 -1
  23. package/scripts/lib/image-presentation.mjs +6 -0
  24. package/scripts/lib/markdown-links.mjs +182 -0
  25. package/scripts/lib/navigation-model.mjs +44 -12
  26. package/scripts/lib/navigation-review.mjs +396 -0
  27. package/scripts/lib/norna-markdown-blocks.mjs +74 -26
  28. package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
  29. package/scripts/lib/page-aliases.mjs +184 -0
  30. package/scripts/lib/page-markdown.mjs +88 -7
  31. package/scripts/lib/page-move-plan.mjs +659 -0
  32. package/scripts/lib/presentation-palette-metadata.mjs +1 -1
  33. package/scripts/lib/presentation.mjs +10 -37
  34. package/scripts/lib/project-config.mjs +146 -19
  35. package/scripts/lib/public-asset-conventions.mjs +36 -2
  36. package/scripts/lib/schema-definitions.mjs +43 -15
  37. package/scripts/lib/schema-editor-metadata.mjs +60 -32
  38. package/scripts/lib/schema-value-definitions.mjs +6 -2
  39. package/scripts/lib/semantic-callouts.mjs +128 -0
  40. package/scripts/lib/site-content.mjs +28 -5
  41. package/scripts/lib/site-link-graph.mjs +357 -0
  42. package/scripts/lib/site-navigation-tree.mjs +85 -0
  43. package/scripts/lib/site-page-urls.mjs +10 -0
  44. package/scripts/lib/sitemap.mjs +34 -0
  45. package/scripts/lib/social-image-assets.mjs +30 -0
  46. package/scripts/lib/theme-presets.mjs +53 -25
  47. package/scripts/lib/theme-profiles.mjs +6 -8
  48. package/scripts/lib/yaml-config.mjs +6 -2
  49. package/scripts/move-site-page.mjs +256 -0
  50. package/scripts/review-navigation.mjs +32 -0
  51. package/scripts/sync-content-sections.mjs +182 -216
  52. package/scripts/sync-site-public.mjs +43 -3
  53. package/src/components/CodeBlockCopyScript.astro +97 -0
  54. package/src/components/DisplaySettings.astro +4 -4
  55. package/src/components/EditSourceLink.astro +17 -0
  56. package/src/components/ImageCarousel.astro +6 -3
  57. package/src/components/NavigationPageTree.astro +81 -86
  58. package/src/components/NavigationTreeControls.astro +77 -0
  59. package/src/components/PageAliasRedirect.astro +49 -0
  60. package/src/components/PageContentsNavigation.astro +21 -0
  61. package/src/components/PageList.astro +33 -0
  62. package/src/components/PageSequenceNavigation.astro +37 -0
  63. package/src/components/SearchPage.astro +151 -0
  64. package/src/components/SectionNavigationScript.astro +62 -6
  65. package/src/components/SiteNavigation.astro +50 -11
  66. package/src/components/SitePage.astro +113 -20
  67. package/src/components/SiteSection.astro +16 -3
  68. package/src/components/SiteTreeNavigation.astro +8 -2
  69. package/src/components/TreeNavigationScript.astro +207 -26
  70. package/src/content.config.ts +5 -2
  71. package/src/layouts/BaseLayout.astro +71 -10
  72. package/src/lib/readerPreferencesScript.mjs +2 -2
  73. package/src/lib/sectionContent.ts +32 -3
  74. package/src/lib/siteNavigation.ts +31 -52
  75. package/src/lib/sitePages.ts +3 -2
  76. package/src/lib/sitePublicAssets.ts +1 -0
  77. package/src/pages/404.astro +110 -0
  78. package/src/pages/[...slug].astro +45 -8
  79. package/src/styles/content.css +718 -0
  80. package/src/styles/foundations.css +284 -0
  81. package/src/styles/global.css +6 -2510
  82. package/src/styles/media.css +518 -0
  83. package/src/styles/navigation.css +526 -0
  84. package/src/styles/page-layout.css +786 -0
  85. package/src/styles/responsive.css +594 -0
  86. package/starters/basic/README.md +6 -19
  87. package/starters/basic/package.json +1 -0
  88. 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
- colorMode: { default: 'system' },
6
+ appearance: { default: 'system' },
11
7
  palette: 'near-monochrome',
12
8
  },
13
9
  'warm-paper-adaptive': {
14
- colorMode: { default: 'system' },
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 === 'readerControls' && issue.keys?.includes('appearance')) {
36
- return 'Reader control "appearance" was replaced by "colorMode".';
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
+ }