@janga/norna 0.7.27 → 0.7.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -29
- package/astro.config.mjs +33 -3
- package/bin/norna-cli.mjs +5 -3
- package/package.json +18 -12
- package/schemas/card-list.schema.json +1 -1
- package/schemas/config.schema.json +81 -66
- package/schemas/content-frontmatter.schema.json +23 -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 +31 -250
- package/scripts/build-site.mjs +11 -6
- package/scripts/check-config.mjs +7 -23
- package/scripts/dev-local.mjs +51 -17
- package/scripts/export-theme-preset.mjs +2 -2
- package/scripts/generate-images.mjs +89 -70
- package/scripts/generate-schemas.mjs +9 -6
- package/scripts/generate-search-index.mjs +3 -2
- package/scripts/lib/attachment-render.mjs +23 -0
- package/scripts/lib/category-destinations.mjs +13 -12
- package/scripts/lib/dev-server-identity.mjs +30 -0
- package/scripts/lib/documentation-links.mjs +1 -1
- package/scripts/lib/documentation-routes.json +70 -62
- package/scripts/lib/docusaurus-migration-audit.mjs +15 -12
- package/scripts/lib/editor-attachments.mjs +69 -0
- package/scripts/lib/editor-file-policy.mjs +20 -0
- package/scripts/lib/editor-language-service.mjs +74 -73
- package/scripts/lib/editor-page-addresses.mjs +57 -0
- package/scripts/lib/editor-page-placement.mjs +102 -0
- package/scripts/lib/editor-resource-actions.mjs +216 -0
- package/scripts/lib/editor-site-editing-tree.mjs +181 -0
- package/scripts/lib/editor-site-files.mjs +154 -0
- package/scripts/lib/editor-site-links.mjs +81 -0
- package/scripts/lib/editor-site-preview.mjs +149 -0
- package/scripts/lib/editor-site-tree.mjs +158 -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/logo-assets.mjs +2 -1
- package/scripts/lib/markdown-images.mjs +59 -0
- package/scripts/lib/markdown-links.mjs +29 -9
- package/scripts/lib/navigation-model.mjs +2 -44
- package/scripts/lib/navigation-review.mjs +14 -50
- package/scripts/lib/norna-markdown-blocks.mjs +1 -58
- package/scripts/lib/norna-markdown-render-plugin.mjs +29 -0
- package/scripts/lib/page-aliases.mjs +8 -1
- package/scripts/lib/page-attachments.mjs +59 -0
- package/scripts/lib/page-markdown.mjs +11 -7
- package/scripts/lib/page-model.mjs +19 -36
- package/scripts/lib/page-move-apply.mjs +119 -0
- package/scripts/lib/page-move-plan.mjs +64 -78
- package/scripts/lib/page-order-source.mjs +22 -0
- package/scripts/lib/page-order.mjs +45 -0
- package/scripts/lib/page-placement-cli.mjs +50 -0
- package/scripts/lib/presentation.mjs +3 -3
- package/scripts/lib/project-config.mjs +10 -8
- package/scripts/lib/publication-paths.mjs +72 -0
- package/scripts/lib/schema-definitions.mjs +15 -38
- package/scripts/lib/schema-editor-metadata.mjs +16 -60
- package/scripts/lib/schema-value-definitions.mjs +1 -1
- package/scripts/lib/site-content.mjs +8 -25
- package/scripts/lib/site-conventions.mjs +19 -1
- package/scripts/lib/site-link-graph.mjs +88 -18
- package/scripts/lib/site-navigation-tree.mjs +2 -7
- package/scripts/lib/site-node-create.mjs +182 -0
- package/scripts/lib/site-paths.mjs +24 -12
- package/scripts/lib/site-source-format.mjs +12 -0
- package/scripts/lib/site-structure.mjs +86 -95
- package/scripts/lib/site-upgrade.mjs +32 -0
- package/scripts/lib/social-image-assets.mjs +2 -1
- package/scripts/lib/source-files.mjs +34 -0
- package/scripts/lib/structured-blocks.mjs +3 -1
- 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 +46 -254
- package/scripts/show-typography.mjs +32 -17
- package/scripts/sync-content-sections.mjs +17 -17
- package/scripts/sync-site-public.mjs +44 -66
- 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/ContentBlocks.astro +2 -1
- package/src/components/ImageStack.astro +7 -5
- 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 +11 -2
- package/src/components/SectionNavigationScript.astro +106 -26
- package/src/components/SiteNavigation.astro +45 -20
- package/src/components/SitePage.astro +38 -61
- package/src/components/SiteSection.astro +1 -0
- 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 +11 -12
- package/src/layouts/BaseLayout.astro +15 -39
- package/src/lib/areaNavigation.ts +42 -0
- package/src/lib/imagePresentation.ts +3 -0
- package/src/lib/markdownImages.ts +101 -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 +67 -21
- package/src/lib/siteNavigation.ts +6 -0
- package/src/lib/sitePages.ts +13 -36
- package/src/lib/sitePublicAssets.ts +2 -1
- package/src/pages/404.astro +1 -1
- package/src/pages/[...slug].astro +7 -16
- package/src/pages/index.astro +1 -3
- package/src/styles/area-navigation.css +408 -0
- package/src/styles/content.css +8 -29
- package/src/styles/media.css +24 -0
- 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/starters/basic/site/{config.yaml → site-config/settings.yaml} +1 -0
- 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/{sitewide-content.yaml → site-config/shared-content.yaml} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { getPageImageSourceKey } from './lib/site-conventions.mjs';
|
|
1
2
|
import { execFile } from 'node:child_process';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { copyFile, mkdir, readdir, readFile, rm, stat, lstat, writeFile } from 'node:fs/promises';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { promisify } from 'node:util';
|
|
6
7
|
import {
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
|
|
24
25
|
const execFileAsync = promisify(execFile);
|
|
25
26
|
|
|
26
|
-
const imageOutputVersion =
|
|
27
|
+
const imageOutputVersion = 3;
|
|
27
28
|
const sourceHashSlugLength = 8;
|
|
28
29
|
const widths = [480, 768, 1080, 1440, 1920];
|
|
29
30
|
|
|
@@ -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
|
-
return path.join(generatedImagesDir, parsed.dir, `${parsed.
|
|
88
|
+
const parsed = path.parse(sourceKey.replace(/^root\//, ''));
|
|
89
|
+
return path.join(generatedImagesDir, parsed.dir, `${parsed.base}-${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,11 +98,13 @@ 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);
|
|
104
105
|
const page = await parsePageMarkdown(body, { label: contentFile.contentLabel });
|
|
106
|
+
const imageError = page.diagnostics.find((issue) => issue.code === 'invalid-markdown-image');
|
|
107
|
+
if (imageError) throw new Error(imageError.message + ' ' + imageError.fix);
|
|
105
108
|
const references = [];
|
|
106
109
|
|
|
107
110
|
for (const region of page.regions) {
|
|
@@ -137,6 +140,7 @@ const getContentSourcePath = ({ image, sourceKey }) => {
|
|
|
137
140
|
const getReferencedSources = async () => {
|
|
138
141
|
const contentFiles = await getContentFiles();
|
|
139
142
|
const seen = new Set();
|
|
143
|
+
const normalizedSources = new Map();
|
|
140
144
|
const sources = [];
|
|
141
145
|
|
|
142
146
|
for (const contentFile of contentFiles) {
|
|
@@ -151,7 +155,9 @@ const getReferencedSources = async () => {
|
|
|
151
155
|
continue;
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
const fileStat = await
|
|
158
|
+
const fileStat = await lstat(sourcePath).catch(() => null);
|
|
159
|
+
const directoryStat = await lstat(contentFile.imagesDir).catch(() => null);
|
|
160
|
+
if (fileStat?.isSymbolicLink() || directoryStat?.isSymbolicLink()) fail(`Image ${sourcePath} must belong to a regular images/ directory; symbolic links are not supported.`);
|
|
155
161
|
if (!fileStat?.isFile()) {
|
|
156
162
|
fail(`Image file does not exist: ${contentFile.imagesLabel}/${siteImagePath}`);
|
|
157
163
|
}
|
|
@@ -160,6 +166,10 @@ const getReferencedSources = async () => {
|
|
|
160
166
|
fail(`Image "${imageName}" is used in ${reference.sectionLabel} but is not stored directly in ${contentFile.imagesLabel}/. Run norna content:sync, or npm run norna:sync in starter-style repositories, to move it.`);
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
const normalizedKey = reference.sourceKey.normalize('NFC').toLowerCase();
|
|
170
|
+
const previous = normalizedSources.get(normalizedKey);
|
|
171
|
+
if (previous) fail(`Image sources ${previous} and ${sourcePath} conflict after case/Unicode normalization. Rename one image.`);
|
|
172
|
+
normalizedSources.set(normalizedKey, sourcePath);
|
|
163
173
|
seen.add(reference.sourceKey);
|
|
164
174
|
sources.push({ sourcePath, sourceKey: reference.sourceKey, reference });
|
|
165
175
|
}
|
|
@@ -202,13 +212,13 @@ const validateCarouselOrientations = async (manifest) => {
|
|
|
202
212
|
}
|
|
203
213
|
};
|
|
204
214
|
|
|
205
|
-
const identify = async (sourcePath) => {
|
|
215
|
+
const identify = async (sourcePath, imageMagick) => {
|
|
206
216
|
const output = await imageMagick.identify(sourcePath);
|
|
207
217
|
const [width, height] = output.split(' ').map(Number);
|
|
208
218
|
return { width, height };
|
|
209
219
|
};
|
|
210
220
|
|
|
211
|
-
const convert = async (sourcePath, outputPath, width) => {
|
|
221
|
+
const convert = async (sourcePath, outputPath, width, imageMagick) => {
|
|
212
222
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
213
223
|
await imageMagick.convert(sourcePath, outputPath, width);
|
|
214
224
|
};
|
|
@@ -262,7 +272,7 @@ const hasGeneratedVariants = async (variants) => {
|
|
|
262
272
|
return true;
|
|
263
273
|
};
|
|
264
274
|
|
|
265
|
-
const getReusableEntry = async (
|
|
275
|
+
const getReusableEntry = async (sourceKey, previousEntry, sourceHash) => {
|
|
266
276
|
if (
|
|
267
277
|
previousEntry?.sourceHash !== sourceHash
|
|
268
278
|
|| previousEntry?.outputVersion !== imageOutputVersion
|
|
@@ -272,7 +282,7 @@ const getReusableEntry = async (sourcePath, previousEntry, sourceHash) => {
|
|
|
272
282
|
return null;
|
|
273
283
|
}
|
|
274
284
|
|
|
275
|
-
const variants = getVariants(
|
|
285
|
+
const variants = getVariants(sourceKey, sourceHash, getVariantWidths(previousEntry));
|
|
276
286
|
|
|
277
287
|
if (!(await hasGeneratedVariants(variants))) {
|
|
278
288
|
return null;
|
|
@@ -287,12 +297,12 @@ const getReusableEntry = async (sourcePath, previousEntry, sourceHash) => {
|
|
|
287
297
|
};
|
|
288
298
|
};
|
|
289
299
|
|
|
290
|
-
const getReusableStaticEntry = async (previousEntry, sourceHash) => {
|
|
300
|
+
const getReusableStaticEntry = async (previousEntry, sourceHash, sourceKey) => {
|
|
291
301
|
if (
|
|
292
302
|
previousEntry?.kind !== 'static'
|
|
293
303
|
|| previousEntry?.sourceHash !== sourceHash
|
|
294
304
|
|| previousEntry?.outputVersion !== imageOutputVersion
|
|
295
|
-
||
|
|
305
|
+
|| previousEntry?.src !== getPublicPath(getOriginalPath(sourceKey, sourceHash))
|
|
296
306
|
|| !(await fileExists(getFilePathFromPublicPath(previousEntry.src)))
|
|
297
307
|
) {
|
|
298
308
|
return null;
|
|
@@ -348,80 +358,89 @@ const removeUnreferencedGeneratedFiles = async (manifest) => {
|
|
|
348
358
|
}
|
|
349
359
|
};
|
|
350
360
|
|
|
351
|
-
|
|
361
|
+
const main = async () => {
|
|
362
|
+
await mkdir(generatedImagesDir, { recursive: true });
|
|
352
363
|
|
|
353
|
-
const previousManifest = await readManifest();
|
|
354
|
-
const sources = await getReferencedSources();
|
|
355
|
-
const manifest = {};
|
|
356
|
-
const isStaticSource = (sourcePath) => staticImageExtensions.has(path.extname(sourcePath).toLowerCase());
|
|
357
|
-
const isRasterSource = (sourcePath) => rasterImageExtensions.has(path.extname(sourcePath).toLowerCase());
|
|
364
|
+
const previousManifest = await readManifest();
|
|
365
|
+
const sources = await getReferencedSources();
|
|
366
|
+
const manifest = {};
|
|
367
|
+
const isStaticSource = (sourcePath) => staticImageExtensions.has(path.extname(sourcePath).toLowerCase());
|
|
368
|
+
const isRasterSource = (sourcePath) => rasterImageExtensions.has(path.extname(sourcePath).toLowerCase());
|
|
358
369
|
|
|
359
|
-
if (sources.length === 0) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
370
|
+
if (sources.length === 0) {
|
|
371
|
+
await removeUnreferencedGeneratedFiles(manifest);
|
|
372
|
+
await mkdir(path.dirname(generatedImagesManifestPath), { recursive: true });
|
|
373
|
+
await writeFile(generatedImagesManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const imageMagick = sources.some(({ sourcePath }) => isRasterSource(sourcePath))
|
|
378
|
+
? await getImageMagick()
|
|
379
|
+
: null;
|
|
365
380
|
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
: null;
|
|
381
|
+
for (const { sourcePath, sourceKey } of sources) {
|
|
382
|
+
const sourceHash = await getSourceHash(sourcePath);
|
|
369
383
|
|
|
370
|
-
|
|
371
|
-
|
|
384
|
+
if (isStaticSource(sourcePath)) {
|
|
385
|
+
const reusableEntry = await getReusableStaticEntry(previousManifest[sourceKey], sourceHash, sourceKey);
|
|
372
386
|
|
|
373
|
-
|
|
374
|
-
|
|
387
|
+
if (reusableEntry) {
|
|
388
|
+
manifest[sourceKey] = reusableEntry;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const outputPath = getOriginalPath(sourceKey, sourceHash);
|
|
393
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
394
|
+
await copyFile(sourcePath, outputPath);
|
|
395
|
+
|
|
396
|
+
const dimensions = await readImageDimensions(sourcePath).catch(() => null);
|
|
397
|
+
manifest[sourceKey] = {
|
|
398
|
+
kind: 'static',
|
|
399
|
+
outputVersion: imageOutputVersion,
|
|
400
|
+
sourceHash,
|
|
401
|
+
src: getPublicPath(outputPath),
|
|
402
|
+
...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
|
|
403
|
+
};
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const reusableEntry = await getReusableEntry(sourceKey, previousManifest[sourceKey], sourceHash);
|
|
375
408
|
|
|
376
409
|
if (reusableEntry) {
|
|
377
410
|
manifest[sourceKey] = reusableEntry;
|
|
378
411
|
continue;
|
|
379
412
|
}
|
|
380
413
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
414
|
+
if (!imageMagick) {
|
|
415
|
+
throw new Error(`ImageMagick is missing for raster image: ${sourcePath}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const dimensions = await identify(sourcePath, imageMagick);
|
|
419
|
+
const variantWidths = getVariantWidths(dimensions);
|
|
420
|
+
const variants = getVariants(sourceKey, sourceHash, variantWidths);
|
|
421
|
+
for (const width of variantWidths) {
|
|
422
|
+
const outputPath = getGeneratedPath(sourceKey, sourceHash, width);
|
|
423
|
+
await convert(sourcePath, outputPath, width, imageMagick);
|
|
424
|
+
}
|
|
384
425
|
|
|
385
|
-
const dimensions = await readImageDimensions(sourcePath).catch(() => null);
|
|
386
426
|
manifest[sourceKey] = {
|
|
387
|
-
kind: 'static',
|
|
388
427
|
outputVersion: imageOutputVersion,
|
|
389
428
|
sourceHash,
|
|
390
|
-
|
|
391
|
-
|
|
429
|
+
width: dimensions.width,
|
|
430
|
+
height: dimensions.height,
|
|
431
|
+
variants,
|
|
392
432
|
};
|
|
393
|
-
continue;
|
|
394
433
|
}
|
|
395
434
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (!imageMagick) {
|
|
404
|
-
throw new Error(`ImageMagick is missing for raster image: ${sourcePath}`);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const dimensions = await identify(sourcePath);
|
|
408
|
-
const variantWidths = getVariantWidths(dimensions);
|
|
409
|
-
const variants = getVariants(sourceKey, sourceHash, variantWidths);
|
|
410
|
-
for (const width of variantWidths) {
|
|
411
|
-
const outputPath = getGeneratedPath(sourceKey, sourceHash, width);
|
|
412
|
-
await convert(sourcePath, outputPath, width);
|
|
413
|
-
}
|
|
435
|
+
await validateCarouselOrientations(manifest);
|
|
436
|
+
await removeUnreferencedGeneratedFiles(manifest);
|
|
437
|
+
await mkdir(path.dirname(generatedImagesManifestPath), { recursive: true });
|
|
438
|
+
await writeFile(generatedImagesManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
439
|
+
};
|
|
414
440
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
variants,
|
|
421
|
-
};
|
|
441
|
+
try {
|
|
442
|
+
await main();
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.error(error.message);
|
|
445
|
+
process.exitCode = 1;
|
|
422
446
|
}
|
|
423
|
-
|
|
424
|
-
await validateCarouselOrientations(manifest);
|
|
425
|
-
await removeUnreferencedGeneratedFiles(manifest);
|
|
426
|
-
await mkdir(path.dirname(generatedImagesManifestPath), { recursive: true });
|
|
427
|
-
await writeFile(generatedImagesManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
@@ -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`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { searchOutputDirectory } from './lib/publication-paths.mjs';
|
|
1
2
|
import { cp, mkdir, readdir, rm } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import * as pagefind from 'pagefind';
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
astroPublicDir,
|
|
8
9
|
} from './lib/site-paths.mjs';
|
|
9
10
|
|
|
10
|
-
const searchDirectoryName =
|
|
11
|
+
const searchDirectoryName = searchOutputDirectory;
|
|
11
12
|
const distSearchDirectory = path.join(astroDistDir, searchDirectoryName);
|
|
12
13
|
const localSearchDirectory = path.join(astroPublicDir, searchDirectoryName);
|
|
13
14
|
|
|
@@ -44,7 +45,7 @@ if (!projectConfig.search.enabled) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
await rm(localSearchDirectory, { force: true, recursive: true });
|
|
47
|
-
await mkdir(
|
|
48
|
+
await mkdir(path.dirname(localSearchDirectory), { recursive: true });
|
|
48
49
|
await cp(distSearchDirectory, localSearchDirectory, { recursive: true });
|
|
49
50
|
|
|
50
51
|
console.log(`Generated static search index for ${indexedPageCount} page${indexedPageCount === 1 ? '' : 's'}.`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { getSourcePageLocation } from './source-files.mjs';
|
|
4
|
+
import { parsePageMarkdownSource } from './page-markdown.mjs';
|
|
5
|
+
import { readPageAttachments, resolveAttachment, attachmentHref } from './page-attachments.mjs';
|
|
6
|
+
|
|
7
|
+
export const rewriteAttachmentLinks = async (source, fileURL, siteRoot) => {
|
|
8
|
+
if (!fileURL) return source;
|
|
9
|
+
const contentPath = fileURLToPath(fileURL);
|
|
10
|
+
const location = getSourcePageLocation(siteRoot, path.dirname(contentPath));
|
|
11
|
+
if (!location || path.basename(contentPath) !== 'content.md') return source;
|
|
12
|
+
const { files } = await readPageAttachments({ ...location, contentPath });
|
|
13
|
+
if (!files.length) return source;
|
|
14
|
+
const document = await parsePageMarkdownSource(source);
|
|
15
|
+
const edits = new Map();
|
|
16
|
+
for (const link of document.links) {
|
|
17
|
+
const file = resolveAttachment(link.target, location.isHome ? '/' : `/${location.pagePath}/`, files);
|
|
18
|
+
if (!file || !link.targetRange) continue;
|
|
19
|
+
const href = attachmentHref(file, link.target);
|
|
20
|
+
edits.set(link.targetRange.start, { ...link.targetRange, text: link.yamlScalar ? JSON.stringify(href) : href });
|
|
21
|
+
}
|
|
22
|
+
return [...edits.values()].sort((a,b) => b.start-a.start).reduce((text, edit) => text.slice(0, edit.start) + edit.text + text.slice(edit.end), source);
|
|
23
|
+
};
|
|
@@ -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
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export const identityPath = '/.well-known/norna-dev';
|
|
4
|
+
export const readDevServerIdentity = async (state, siteRoot) => {
|
|
5
|
+
if (!state?.token || !Number.isInteger(state.port) || state.port < 1 || state.port > 65535) return null;
|
|
6
|
+
try {
|
|
7
|
+
const response = await fetch(`http://127.0.0.1:${state.port}${identityPath}`, { signal: AbortSignal.timeout(1500), redirect: 'error' });
|
|
8
|
+
if (!response.ok) return null;
|
|
9
|
+
const identity = await response.json();
|
|
10
|
+
return identity.token === state.token && Number.isInteger(identity.pid) && identity.pid > 0 && identity.siteRoot === await realpath(siteRoot) ? identity : null;
|
|
11
|
+
} catch { return null; }
|
|
12
|
+
};
|
|
13
|
+
export const verifyDevServer = async (state, siteRoot) => Number.isInteger(state?.pid) && state.pid > 0
|
|
14
|
+
&& (await readDevServerIdentity(state, siteRoot))?.pid === state.pid;
|
|
15
|
+
|
|
16
|
+
export const devServerIdentityPlugin = siteRoot => ({
|
|
17
|
+
name: 'norna-dev-server-identity',
|
|
18
|
+
async configureServer(server) {
|
|
19
|
+
const canonicalRoot = await realpath(siteRoot);
|
|
20
|
+
return () => {
|
|
21
|
+
server.middlewares.stack.unshift({ route: '', handle(request, response, next) {
|
|
22
|
+
if (request.url !== identityPath || request.method !== 'GET' || !process.env.NORNA_DEV_TOKEN) return next();
|
|
23
|
+
if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(request.socket.remoteAddress)) return next();
|
|
24
|
+
response.setHeader('Content-Type', 'application/json');
|
|
25
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
26
|
+
response.end(JSON.stringify({ siteRoot: canonicalRoot, token: process.env.NORNA_DEV_TOKEN, pid: process.pid }));
|
|
27
|
+
} });
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -27,7 +27,7 @@ export const documentationLinkForVersion = (version, label, file, anchor) => {
|
|
|
27
27
|
const reference = version ? `v${version}` : 'main';
|
|
28
28
|
const source = usesLegacyReference(version)
|
|
29
29
|
? `docs/${file}${anchor ? `#${anchor}` : ''}`
|
|
30
|
-
: `${destination.source}${destination.anchor ? `#${destination.anchor}` : ''}`;
|
|
30
|
+
: `${version === '0.7.27' ? destination.source.replace('site/root/pages/', 'site/pages/') : destination.source}${destination.anchor ? `#${destination.anchor}` : ''}`;
|
|
31
31
|
const sourceLabel = version ? `Norna ${version} source reference` : 'Reference source';
|
|
32
32
|
return `[${label} (current)](${destination.url})\n\n[${sourceLabel}](${repositoryRoot}${reference}/${source})`;
|
|
33
33
|
};
|