@janga/norna 0.7.23 → 0.7.25
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 +14 -0
- package/bin/norna-cli.mjs +6 -0
- package/package.json +25 -2
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +80 -15
- package/schemas/content-frontmatter.schema.json +7 -7
- package/schemas/page-theme.schema.json +26 -26
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +246 -246
- package/scripts/build-site.mjs +1 -0
- package/scripts/check-config.mjs +16 -1
- package/scripts/dev-local.mjs +106 -11
- package/scripts/generate-search-index.mjs +57 -0
- package/scripts/init-site.mjs +3 -0
- package/scripts/lib/code-fence-metadata.mjs +228 -0
- package/scripts/lib/edit-source-link.mjs +102 -0
- package/scripts/lib/editor-language-service.mjs +32 -12
- package/scripts/lib/image-presentation.mjs +4 -0
- package/scripts/lib/navigation-model.mjs +34 -13
- package/scripts/lib/navigation-review.mjs +396 -0
- package/scripts/lib/norna-markdown-blocks.mjs +164 -26
- package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
- package/scripts/lib/page-aliases.mjs +21 -1
- package/scripts/lib/page-markdown.mjs +32 -1
- package/scripts/lib/page-move-plan.mjs +659 -0
- package/scripts/lib/presentation-palette-metadata.mjs +1 -1
- package/scripts/lib/presentation.mjs +1 -10
- package/scripts/lib/project-config.mjs +141 -5
- package/scripts/lib/public-asset-conventions.mjs +36 -2
- package/scripts/lib/schema-definitions.mjs +30 -5
- package/scripts/lib/schema-editor-metadata.mjs +36 -4
- package/scripts/lib/schema-value-definitions.mjs +4 -1
- package/scripts/lib/semantic-callouts.mjs +128 -0
- package/scripts/lib/site-content.mjs +2 -1
- package/scripts/lib/site-link-graph.mjs +33 -2
- package/scripts/lib/site-navigation-tree.mjs +85 -0
- package/scripts/lib/site-paths.mjs +14 -2
- package/scripts/lib/social-image-assets.mjs +30 -0
- package/scripts/lib/table-render-plugin.mjs +33 -0
- package/scripts/lib/theme-presets.mjs +2 -2
- package/scripts/lib/theme-profiles.mjs +0 -4
- package/scripts/move-site-page.mjs +256 -0
- package/scripts/review-navigation.mjs +32 -0
- package/scripts/sync-content-sections.mjs +67 -4
- package/scripts/sync-site-public.mjs +24 -5
- package/src/components/CardList.astro +1 -0
- package/src/components/CodeBlockCopyScript.astro +11 -5
- package/src/components/EditSourceLink.astro +17 -0
- package/src/components/ImageCarousel.astro +9 -7
- package/src/components/ImageStack.astro +34 -9
- package/src/components/ImageStackEnhancement.astro +331 -0
- package/src/components/NavigationPageTree.astro +66 -55
- package/src/components/NavigationTreeControls.astro +77 -0
- package/src/components/PageAliasRedirect.astro +1 -1
- package/src/components/PageContentsNavigation.astro +2 -3
- 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 +45 -7
- package/src/components/SiteNavigation.astro +102 -51
- package/src/components/SitePage.astro +107 -39
- package/src/components/SiteSection.astro +11 -3
- package/src/components/SiteTreeNavigation.astro +11 -1
- package/src/components/TableOverflowScript.astro +251 -0
- package/src/components/TreeNavigationScript.astro +271 -34
- package/src/layouts/BaseLayout.astro +83 -2
- package/src/lib/generatedImages.ts +18 -0
- package/src/lib/sectionContent.ts +36 -30
- package/src/lib/siteNavigation.ts +31 -52
- package/src/lib/sitePages.ts +6 -3
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/pages/404.astro +110 -0
- package/src/pages/[...slug].astro +15 -5
- package/src/styles/content.css +538 -39
- package/src/styles/media.css +187 -8
- package/src/styles/navigation.css +199 -3
- package/src/styles/page-layout.css +481 -100
- package/src/styles/responsive.css +144 -28
- 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
|
@@ -44,7 +44,6 @@ const createPaletteMode = ({ appearance, page, surfaces, frame, css }) => {
|
|
|
44
44
|
|
|
45
45
|
const presentationPalettes = Object.freeze({
|
|
46
46
|
'near-monochrome': Object.freeze({
|
|
47
|
-
defaultAppearance: 'dark',
|
|
48
47
|
modes: Object.freeze({
|
|
49
48
|
light: createPaletteMode({
|
|
50
49
|
appearance: 'light',
|
|
@@ -85,7 +84,6 @@ const presentationPalettes = Object.freeze({
|
|
|
85
84
|
}),
|
|
86
85
|
}),
|
|
87
86
|
'arctic-blue': Object.freeze({
|
|
88
|
-
defaultAppearance: 'light',
|
|
89
87
|
modes: Object.freeze({
|
|
90
88
|
light: createPaletteMode({
|
|
91
89
|
appearance: 'light',
|
|
@@ -126,7 +124,6 @@ const presentationPalettes = Object.freeze({
|
|
|
126
124
|
}),
|
|
127
125
|
}),
|
|
128
126
|
'mineral-teal': Object.freeze({
|
|
129
|
-
defaultAppearance: 'light',
|
|
130
127
|
modes: Object.freeze({
|
|
131
128
|
light: createPaletteMode({
|
|
132
129
|
appearance: 'light',
|
|
@@ -167,7 +164,6 @@ const presentationPalettes = Object.freeze({
|
|
|
167
164
|
}),
|
|
168
165
|
}),
|
|
169
166
|
'soft-lavender': Object.freeze({
|
|
170
|
-
defaultAppearance: 'light',
|
|
171
167
|
modes: Object.freeze({
|
|
172
168
|
light: createPaletteMode({
|
|
173
169
|
appearance: 'light',
|
|
@@ -208,7 +204,6 @@ const presentationPalettes = Object.freeze({
|
|
|
208
204
|
}),
|
|
209
205
|
}),
|
|
210
206
|
'warm-paper': Object.freeze({
|
|
211
|
-
defaultAppearance: 'light',
|
|
212
207
|
modes: Object.freeze({
|
|
213
208
|
light: createPaletteMode({
|
|
214
209
|
appearance: 'light',
|
|
@@ -249,7 +244,6 @@ const presentationPalettes = Object.freeze({
|
|
|
249
244
|
}),
|
|
250
245
|
}),
|
|
251
246
|
'retro-earth': Object.freeze({
|
|
252
|
-
defaultAppearance: 'light',
|
|
253
247
|
modes: Object.freeze({
|
|
254
248
|
light: createPaletteMode({
|
|
255
249
|
appearance: 'light',
|
|
@@ -290,7 +284,6 @@ const presentationPalettes = Object.freeze({
|
|
|
290
284
|
}),
|
|
291
285
|
}),
|
|
292
286
|
'clay-rose': Object.freeze({
|
|
293
|
-
defaultAppearance: 'light',
|
|
294
287
|
modes: Object.freeze({
|
|
295
288
|
light: createPaletteMode({
|
|
296
289
|
appearance: 'light',
|
|
@@ -331,7 +324,6 @@ const presentationPalettes = Object.freeze({
|
|
|
331
324
|
}),
|
|
332
325
|
}),
|
|
333
326
|
'forest-moss': Object.freeze({
|
|
334
|
-
defaultAppearance: 'light',
|
|
335
327
|
modes: Object.freeze({
|
|
336
328
|
light: createPaletteMode({
|
|
337
329
|
appearance: 'light',
|
|
@@ -372,7 +364,6 @@ const presentationPalettes = Object.freeze({
|
|
|
372
364
|
}),
|
|
373
365
|
}),
|
|
374
366
|
'vivid-night': Object.freeze({
|
|
375
|
-
defaultAppearance: 'dark',
|
|
376
367
|
modes: Object.freeze({
|
|
377
368
|
light: createPaletteMode({
|
|
378
369
|
appearance: 'light',
|
|
@@ -477,7 +468,7 @@ export const resolveThemePresentation = (theme, sourceLabel = 'theme.yaml') => {
|
|
|
477
468
|
const palette = getPresentationPalette(paletteName);
|
|
478
469
|
const appearance = normalizedTheme.appearance ?? {};
|
|
479
470
|
const readerControls = normalizedTheme.readerControls ?? {};
|
|
480
|
-
const defaultAppearance = appearance.default ??
|
|
471
|
+
const defaultAppearance = appearance.default ?? 'system';
|
|
481
472
|
if (!appearanceNames.includes(defaultAppearance)) {
|
|
482
473
|
throw new Error(`appearance.default must be one of ${appearanceNames.join(', ')} in ${sourceLabel}.`);
|
|
483
474
|
}
|
|
@@ -2,13 +2,18 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import {
|
|
3
3
|
validateConfigYamlStructure,
|
|
4
4
|
} from './site-content.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
defaultImageMaxAvailableHeightPercent,
|
|
7
|
+
defaultImagePresentation,
|
|
8
|
+
imagePresentationNames,
|
|
9
|
+
} from './image-presentation.mjs';
|
|
6
10
|
import { navigationModeNames } from './navigation-model.mjs';
|
|
7
11
|
import {
|
|
8
12
|
siteConfigLabel,
|
|
9
13
|
siteConfigPath,
|
|
10
14
|
siteThemeLabel,
|
|
11
15
|
} from './site-paths.mjs';
|
|
16
|
+
import { localEditorNames, normalizeEditLinkBaseUrl } from './edit-source-link.mjs';
|
|
12
17
|
import { readThemeConfig } from './theme-config.mjs';
|
|
13
18
|
import { resolveThemeConfig } from './theme-presets.mjs';
|
|
14
19
|
import { parseYamlConfig } from './yaml-config.mjs';
|
|
@@ -52,6 +57,16 @@ const readEnum = (object, key, path, allowedValues, fallback, sourceLabel = site
|
|
|
52
57
|
return value;
|
|
53
58
|
};
|
|
54
59
|
|
|
60
|
+
const readBoolean = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
61
|
+
const value = object[key] ?? fallback;
|
|
62
|
+
|
|
63
|
+
if (typeof value !== 'boolean') {
|
|
64
|
+
throw new Error(`${path}.${key} must be true or false in ${sourceLabel}.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return value;
|
|
68
|
+
};
|
|
69
|
+
|
|
55
70
|
const readFontFamily = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
56
71
|
const value = object[key] ?? fallback;
|
|
57
72
|
|
|
@@ -192,8 +207,18 @@ const localeLabels = Object.freeze({
|
|
|
192
207
|
codeCopied: 'Copied',
|
|
193
208
|
codeCopyFailed: 'Could not copy code',
|
|
194
209
|
copyCode: 'Copy code',
|
|
210
|
+
calloutCaution: 'Caution',
|
|
211
|
+
calloutDanger: 'Danger',
|
|
212
|
+
calloutImportant: 'Important',
|
|
213
|
+
calloutNote: 'Note',
|
|
214
|
+
calloutTip: 'Tip',
|
|
215
|
+
calloutWarning: 'Warning',
|
|
216
|
+
closeNavigation: 'Close navigation',
|
|
217
|
+
editSource: 'Edit this page',
|
|
195
218
|
displaySettings: 'Display',
|
|
196
219
|
focusReading: 'Focus reading',
|
|
220
|
+
footnoteBackReference: 'Back to reference {reference}',
|
|
221
|
+
footnotes: 'Footnotes',
|
|
197
222
|
readingWidth: 'Reading width',
|
|
198
223
|
readingWidthNarrow: 'Narrow',
|
|
199
224
|
readingWidthStandard: 'Standard',
|
|
@@ -203,17 +228,49 @@ const localeLabels = Object.freeze({
|
|
|
203
228
|
built: 'Built',
|
|
204
229
|
images: 'Images',
|
|
205
230
|
imageCarousel: 'image carousel',
|
|
231
|
+
imageInspection: 'Image inspection',
|
|
232
|
+
imageInspectionActualSize: 'Show actual size',
|
|
233
|
+
imageInspectionClose: 'Close image inspection',
|
|
234
|
+
imageInspectionFit: 'Fit image to window',
|
|
235
|
+
inspectImage: 'Inspect image: {description}',
|
|
236
|
+
navigationCollapsedAll: 'All navigation items collapsed.',
|
|
237
|
+
navigationCollapseAll: 'Collapse all',
|
|
238
|
+
navigationControls: 'Navigation tree controls',
|
|
206
239
|
navigationChildren: 'Child pages',
|
|
240
|
+
navigationExpandedAll: 'All navigation items expanded.',
|
|
241
|
+
navigationExpandAll: 'Expand all',
|
|
242
|
+
navigationFilter: 'Filter pages and groups',
|
|
243
|
+
navigationFilterEmpty: 'No matching pages or groups. The current page remains available.',
|
|
244
|
+
navigationFilterMatches: 'Matching navigation items: {count}',
|
|
245
|
+
navigationLocatedCurrent: 'Current page located.',
|
|
246
|
+
navigationLocateCurrent: 'Locate current page',
|
|
207
247
|
navigationMenu: 'Menu',
|
|
208
248
|
nextImage: 'Next image',
|
|
249
|
+
nextPage: 'Next page',
|
|
209
250
|
note: 'Note',
|
|
251
|
+
openInVsCode: 'Open in VS Code',
|
|
252
|
+
notFound: 'Page not found',
|
|
253
|
+
notFoundText: 'The requested page does not exist or may have moved.',
|
|
210
254
|
pageMoved: 'Page moved',
|
|
211
255
|
pageMovedText: 'This address now identifies',
|
|
212
256
|
pageNavigation: 'Page contents',
|
|
257
|
+
pageSequence: 'Page sequence',
|
|
258
|
+
pageSections: 'Sections',
|
|
213
259
|
previousImage: 'Previous image',
|
|
260
|
+
previousPage: 'Previous page',
|
|
261
|
+
returnHome: 'Go to the homepage',
|
|
262
|
+
search: 'Search',
|
|
263
|
+
searchDescription: 'Search the published content on this site.',
|
|
264
|
+
searchLoading: 'Loading search…',
|
|
265
|
+
searchNoScript: 'Search requires JavaScript. Use the page navigation when JavaScript is unavailable.',
|
|
266
|
+
searchUnavailable: 'Search is unavailable. During local work, run norna build:local to create or refresh the search index.',
|
|
214
267
|
siteBanners: 'Site notices',
|
|
215
268
|
siteNavigation: 'Pages',
|
|
216
269
|
skipToContent: 'Skip to content',
|
|
270
|
+
tableColumns: 'Table columns',
|
|
271
|
+
tableNextColumns: 'Show next columns',
|
|
272
|
+
tableOverflowDescription: 'More table columns are available horizontally.',
|
|
273
|
+
tablePreviousColumns: 'Show previous columns',
|
|
217
274
|
}),
|
|
218
275
|
sv: Object.freeze({
|
|
219
276
|
breadcrumb: 'Brödsmulor',
|
|
@@ -224,8 +281,18 @@ const localeLabels = Object.freeze({
|
|
|
224
281
|
codeCopied: 'Kopierat',
|
|
225
282
|
codeCopyFailed: 'Kunde inte kopiera koden',
|
|
226
283
|
copyCode: 'Kopiera kod',
|
|
284
|
+
calloutCaution: 'Var försiktig',
|
|
285
|
+
calloutDanger: 'Fara',
|
|
286
|
+
calloutImportant: 'Viktigt',
|
|
287
|
+
calloutNote: 'Notera',
|
|
288
|
+
calloutTip: 'Tips',
|
|
289
|
+
calloutWarning: 'Varning',
|
|
290
|
+
closeNavigation: 'Stäng navigationen',
|
|
291
|
+
editSource: 'Redigera den här sidan',
|
|
227
292
|
displaySettings: 'Visning',
|
|
228
293
|
focusReading: 'Fokuserad läsning',
|
|
294
|
+
footnoteBackReference: 'Tillbaka till referens {reference}',
|
|
295
|
+
footnotes: 'Fotnoter',
|
|
229
296
|
readingWidth: 'Textbredd',
|
|
230
297
|
readingWidthNarrow: 'Smal',
|
|
231
298
|
readingWidthStandard: 'Standard',
|
|
@@ -235,17 +302,49 @@ const localeLabels = Object.freeze({
|
|
|
235
302
|
built: 'Byggd',
|
|
236
303
|
images: 'Bilder',
|
|
237
304
|
imageCarousel: 'bildkarusell',
|
|
305
|
+
imageInspection: 'Bildgranskning',
|
|
306
|
+
imageInspectionActualSize: 'Visa faktisk storlek',
|
|
307
|
+
imageInspectionClose: 'Stäng bildgranskning',
|
|
308
|
+
imageInspectionFit: 'Anpassa bilden till fönstret',
|
|
309
|
+
inspectImage: 'Granska bild: {description}',
|
|
310
|
+
navigationCollapsedAll: 'Alla navigationsposter har fällts ihop.',
|
|
311
|
+
navigationCollapseAll: 'Fäll ihop alla',
|
|
312
|
+
navigationControls: 'Kontroller för navigationsträdet',
|
|
238
313
|
navigationChildren: 'Undersidor',
|
|
314
|
+
navigationExpandedAll: 'Alla navigationsposter har fällts ut.',
|
|
315
|
+
navigationExpandAll: 'Fäll ut alla',
|
|
316
|
+
navigationFilter: 'Filtrera sidor och grupper',
|
|
317
|
+
navigationFilterEmpty: 'Inga sidor eller grupper matchar. Den aktuella sidan är fortfarande tillgänglig.',
|
|
318
|
+
navigationFilterMatches: 'Matchande navigationsposter: {count}',
|
|
319
|
+
navigationLocatedCurrent: 'Den aktuella sidan har hittats.',
|
|
320
|
+
navigationLocateCurrent: 'Hitta aktuell sida',
|
|
239
321
|
navigationMenu: 'Meny',
|
|
240
322
|
nextImage: 'Nästa bild',
|
|
323
|
+
nextPage: 'Nästa sida',
|
|
241
324
|
note: 'Not',
|
|
325
|
+
openInVsCode: 'Öppna i VS Code',
|
|
326
|
+
notFound: 'Sidan hittades inte',
|
|
327
|
+
notFoundText: 'Den begärda sidan finns inte eller kan ha flyttats.',
|
|
242
328
|
pageMoved: 'Sidan har flyttats',
|
|
243
329
|
pageMovedText: 'Den här adressen identifierar nu',
|
|
244
330
|
pageNavigation: 'Sidinnehåll',
|
|
331
|
+
pageSequence: 'Sidföljd',
|
|
332
|
+
pageSections: 'Avsnitt',
|
|
245
333
|
previousImage: 'Föregående bild',
|
|
334
|
+
previousPage: 'Föregående sida',
|
|
335
|
+
returnHome: 'Gå till startsidan',
|
|
336
|
+
search: 'Sök',
|
|
337
|
+
searchDescription: 'Sök i det publicerade innehållet på webbplatsen.',
|
|
338
|
+
searchLoading: 'Laddar sökning…',
|
|
339
|
+
searchNoScript: 'Sökning kräver JavaScript. Använd sidnavigeringen när JavaScript inte är tillgängligt.',
|
|
340
|
+
searchUnavailable: 'Sökningen är inte tillgänglig. Kör norna build:local under lokalt arbete för att skapa eller uppdatera sökindexet.',
|
|
246
341
|
siteBanners: 'Meddelanden',
|
|
247
342
|
siteNavigation: 'Sidor',
|
|
248
343
|
skipToContent: 'Hoppa till innehållet',
|
|
344
|
+
tableColumns: 'Tabellkolumner',
|
|
345
|
+
tableNextColumns: 'Visa nästa kolumner',
|
|
346
|
+
tableOverflowDescription: 'Fler tabellkolumner är tillgängliga i sidled.',
|
|
347
|
+
tablePreviousColumns: 'Visa föregående kolumner',
|
|
249
348
|
}),
|
|
250
349
|
});
|
|
251
350
|
|
|
@@ -411,10 +510,13 @@ export const resolveThemeVisualConfig = (theme, sourceLabel = siteThemeLabel) =>
|
|
|
411
510
|
presentation: imagePresentation,
|
|
412
511
|
...(imagePresentation === 'centered-fit'
|
|
413
512
|
? {
|
|
414
|
-
maxAvailableHeightPercent: readResponsivePercent(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
513
|
+
maxAvailableHeightPercent: readResponsivePercent(
|
|
514
|
+
rawImagesConfig,
|
|
515
|
+
'maxAvailableHeightPercent',
|
|
516
|
+
'images',
|
|
517
|
+
defaultImageMaxAvailableHeightPercent,
|
|
518
|
+
sourceLabel,
|
|
519
|
+
),
|
|
418
520
|
}
|
|
419
521
|
: {}),
|
|
420
522
|
maxAvailableWidthPercent: readResponsivePercent(rawImagesConfig, 'maxAvailableWidthPercent', 'images', Object.freeze({
|
|
@@ -457,12 +559,46 @@ export const resolveNavigationConfig = (config, sourceLabel = siteConfigLabel) =
|
|
|
457
559
|
});
|
|
458
560
|
};
|
|
459
561
|
|
|
562
|
+
export const resolveEditLinkConfig = (config, sourceLabel = siteConfigLabel) => {
|
|
563
|
+
if (config.editLink === undefined) return null;
|
|
564
|
+
|
|
565
|
+
const rawEditLink = assertObject(config.editLink, 'editLink', sourceLabel);
|
|
566
|
+
const unknownKeys = Object.keys(rawEditLink).filter((key) => !['baseUrl', 'localEditor'].includes(key));
|
|
567
|
+
if (unknownKeys.length > 0) {
|
|
568
|
+
throw new Error(`editLink.${unknownKeys[0]} is not a valid setting in ${sourceLabel}.`);
|
|
569
|
+
}
|
|
570
|
+
if (!Object.hasOwn(rawEditLink, 'baseUrl') && !Object.hasOwn(rawEditLink, 'localEditor')) {
|
|
571
|
+
throw new Error(`editLink must specify baseUrl, localEditor, or both in ${sourceLabel}.`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let baseUrl = null;
|
|
575
|
+
if (Object.hasOwn(rawEditLink, 'baseUrl')) {
|
|
576
|
+
try {
|
|
577
|
+
baseUrl = normalizeEditLinkBaseUrl(rawEditLink.baseUrl, 'editLink.baseUrl');
|
|
578
|
+
} catch (error) {
|
|
579
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
580
|
+
throw new Error(`${message.replace(/\.$/, '')} in ${sourceLabel}.`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return Object.freeze({
|
|
585
|
+
baseUrl,
|
|
586
|
+
localEditor: Object.hasOwn(rawEditLink, 'localEditor')
|
|
587
|
+
? readEnum(rawEditLink, 'localEditor', 'editLink', localEditorNames, undefined, sourceLabel)
|
|
588
|
+
: null,
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
|
|
460
592
|
export const projectConfig = Object.freeze({
|
|
461
593
|
site: Object.freeze({
|
|
462
594
|
basePath: siteUrl.pathname,
|
|
463
595
|
url: siteUrl.href,
|
|
464
596
|
}),
|
|
465
597
|
...resolveThemeVisualConfig(rawTheme, siteThemeLabel),
|
|
598
|
+
editLink: resolveEditLinkConfig(rawConfig, siteConfigLabel),
|
|
599
|
+
search: Object.freeze({
|
|
600
|
+
enabled: readBoolean(rawConfig, 'search', 'config YAML', false),
|
|
601
|
+
}),
|
|
466
602
|
navigation: Object.freeze({
|
|
467
603
|
...resolveNavigationConfig(rawConfig, siteConfigLabel),
|
|
468
604
|
scrollBehavior: readEnum(
|
|
@@ -18,10 +18,22 @@ export const browserIconAssetFilenames = Object.freeze(
|
|
|
18
18
|
browserIconAssetDefinitions.map(({ filename }) => filename),
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
+
export const socialImageAssetDefinitions = Object.freeze([
|
|
22
|
+
Object.freeze({ filename: 'social-image.png', type: 'image/png' }),
|
|
23
|
+
Object.freeze({ filename: 'social-image.jpg', type: 'image/jpeg' }),
|
|
24
|
+
Object.freeze({ filename: 'social-image.jpeg', type: 'image/jpeg' }),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export const socialImageAssetFilenames = Object.freeze(
|
|
28
|
+
socialImageAssetDefinitions.map(({ filename }) => filename),
|
|
29
|
+
);
|
|
30
|
+
|
|
21
31
|
const logoExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg']);
|
|
22
32
|
const faviconExtensions = new Set(['.svg', '.png', '.ico']);
|
|
33
|
+
const socialImageExtensions = new Set(['.png', '.jpg', '.jpeg']);
|
|
23
34
|
const exactLogoNames = new Set(logoAssetFilenames);
|
|
24
35
|
const exactBrowserIconNames = new Set(browserIconAssetFilenames);
|
|
36
|
+
const exactSocialImageNames = new Set(socialImageAssetFilenames);
|
|
25
37
|
|
|
26
38
|
export const isLogoAssetFilename = (filename) => exactLogoNames.has(filename);
|
|
27
39
|
|
|
@@ -60,6 +72,13 @@ const getSuspiciousAsset = (filename) => {
|
|
|
60
72
|
};
|
|
61
73
|
}
|
|
62
74
|
|
|
75
|
+
if (exactSocialImageNames.has(lowerFilename) && filename !== lowerFilename) {
|
|
76
|
+
return {
|
|
77
|
+
code: 'public-asset-case',
|
|
78
|
+
message: `Norna does not recognize "${filename}" as the social sharing image on case-sensitive file systems. Rename it to "${lowerFilename}".`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
if (parsed.name === 'logo' && !logoExtensions.has(parsed.ext)) {
|
|
64
83
|
return {
|
|
65
84
|
code: 'unsupported-logo-file',
|
|
@@ -74,6 +93,13 @@ const getSuspiciousAsset = (filename) => {
|
|
|
74
93
|
};
|
|
75
94
|
}
|
|
76
95
|
|
|
96
|
+
if (parsed.name === 'social-image' && !socialImageExtensions.has(parsed.ext)) {
|
|
97
|
+
return {
|
|
98
|
+
code: 'unsupported-social-image-file',
|
|
99
|
+
message: `Norna does not recognize "${filename}" as the social sharing image. Use exactly one of ${socialImageAssetFilenames.join(', ')}.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
if (/^favicon[-_.]/.test(lowerFilename) && !exactBrowserIconNames.has(lowerFilename)) {
|
|
78
104
|
return {
|
|
79
105
|
code: 'unrecognized-favicon-file',
|
|
@@ -95,6 +121,13 @@ const getSuspiciousAsset = (filename) => {
|
|
|
95
121
|
};
|
|
96
122
|
}
|
|
97
123
|
|
|
124
|
+
if (socialImageExtensions.has(parsed.ext) && editDistance(parsed.name, 'social-image') <= 2) {
|
|
125
|
+
return {
|
|
126
|
+
code: 'possible-social-image-typo',
|
|
127
|
+
message: `"${filename}" looks like a misspelled social sharing image filename. Norna recognizes ${socialImageAssetFilenames.join(', ')}.`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
98
131
|
return null;
|
|
99
132
|
};
|
|
100
133
|
|
|
@@ -102,11 +135,12 @@ export const inspectPublicAssetFilenames = (filenames) => {
|
|
|
102
135
|
const files = [...filenames].sort((left, right) => left.localeCompare(right, 'en'));
|
|
103
136
|
const logos = files.filter(isLogoAssetFilename);
|
|
104
137
|
const browserIcons = files.filter((filename) => exactBrowserIconNames.has(filename));
|
|
138
|
+
const socialImages = files.filter((filename) => exactSocialImageNames.has(filename));
|
|
105
139
|
const suspicious = files.flatMap((filename) => {
|
|
106
|
-
if (isLogoAssetFilename(filename) || exactBrowserIconNames.has(filename)) return [];
|
|
140
|
+
if (isLogoAssetFilename(filename) || exactBrowserIconNames.has(filename) || exactSocialImageNames.has(filename)) return [];
|
|
107
141
|
const issue = getSuspiciousAsset(filename);
|
|
108
142
|
return issue ? [{ filename, ...issue }] : [];
|
|
109
143
|
});
|
|
110
144
|
|
|
111
|
-
return { browserIcons, files, logos, suspicious };
|
|
145
|
+
return { browserIcons, files, logos, socialImages, suspicious };
|
|
112
146
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'astro/zod';
|
|
2
|
+
import { localEditorNames } from './edit-source-link.mjs';
|
|
2
3
|
import { imagePresentationNames } from './image-presentation.mjs';
|
|
3
4
|
import { navigationModeNames } from './navigation-model.mjs';
|
|
4
5
|
import { presentationPaletteNames } from './presentation-palette-metadata.mjs';
|
|
@@ -17,13 +18,13 @@ const textWidth = z.enum(['narrow', 'normal', 'wide']).describe('Maximum width o
|
|
|
17
18
|
const headingWeight = z.union([z.literal(400), z.literal(500), z.literal(600), z.literal(700)]).describe('CSS font weight.');
|
|
18
19
|
const typographyProfile = z.enum(['restrained', 'dense', 'reading', 'statement']).describe('Coordinated typography defaults. Omit this to use the selected preset.');
|
|
19
20
|
const themePreset = z.enum(themePresetNames).describe('Complete Norna visual preset. Start here and add overrides only when needed.');
|
|
20
|
-
const presentationPalette = z.enum(presentationPaletteNames).describe('Coordinated site color palette. Every palette provides light and dark variants. Omit this to use the selected preset.');
|
|
21
|
+
const presentationPalette = z.enum(presentationPaletteNames).describe('Coordinated site color palette. Every palette provides light and dark variants without changing the initial appearance. Omit this to use the selected preset.');
|
|
21
22
|
const themeAppearance = z.object({
|
|
22
|
-
default: z.enum(['system', 'light', 'dark']).optional().describe('Initial appearance.
|
|
23
|
+
default: z.enum(['system', 'light', 'dark']).optional().describe('Initial appearance. Omit this setting to follow the visitor\'s operating-system preference.'),
|
|
23
24
|
}).strict().refine(
|
|
24
25
|
(value) => value.default !== undefined,
|
|
25
26
|
'Specify default.',
|
|
26
|
-
).describe('Site-wide initial appearance.');
|
|
27
|
+
).describe('Site-wide initial appearance. The default is system.');
|
|
27
28
|
const readerControls = z.object({
|
|
28
29
|
appearance: z.boolean().optional().describe('Show an Appearance control that lets readers choose System, Light, or Dark.'),
|
|
29
30
|
focusReading: z.boolean().optional().describe('Show a Focus reading control that lets readers hide navigation, breadcrumbs, and the footer. Sites with tree navigation always show this control.'),
|
|
@@ -35,9 +36,31 @@ const spacingDensity = z.enum(['compact', 'normal', 'airy']).describe('Coordinat
|
|
|
35
36
|
const contentSpacing = z.enum(['compact', 'normal', 'spacious']).describe('Vertical spacing between page sections and structured content blocks.');
|
|
36
37
|
const backgroundPattern = z.enum(['uniform', 'alternating', 'accented']).describe('How coordinated backgrounds are assigned to H2 sections. Non-uniform patterns are unavailable with tree navigation.');
|
|
37
38
|
const cornerTreatment = z.enum(['square', 'rounded']).describe('Site-wide corner treatment for navigation, cards and framed content.');
|
|
38
|
-
const cardListWidth = z.enum(['text', 'narrow', 'normal', 'wide']).describe('Default maximum width for card lists. A width written in a
|
|
39
|
+
const cardListWidth = z.enum(['text', 'narrow', 'normal', 'wide']).describe('Default maximum width for card lists. A width written in a card-list block overrides this value.');
|
|
39
40
|
const imagePresentation = z.enum(imagePresentationNames).describe('How managed image stacks and carousels are placed on the page. Prose-aligned starts them at the body-text edge; centered-fit centers them and constrains them by available width and viewport height.');
|
|
40
|
-
const navigationMode = z.enum(navigationModeNames).describe('Site-wide navigation policy. Automatic
|
|
41
|
+
const navigationMode = z.enum(navigationModeNames).describe('Site-wide navigation policy. Automatic uses sections for one page, top navigation for a flat multi-page site, and a stable left rail throughout a hierarchical site.');
|
|
42
|
+
const editLink = z.object({
|
|
43
|
+
baseUrl: z.string().url().refine((value) => {
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(value);
|
|
46
|
+
return ['http:', 'https:'].includes(url.protocol)
|
|
47
|
+
&& !url.username
|
|
48
|
+
&& !url.password
|
|
49
|
+
&& !url.search
|
|
50
|
+
&& !url.hash;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}, 'Use an absolute http or https URL without credentials, a query string, or a fragment.')
|
|
55
|
+
.describe('Absolute edit URL prefix containing the repository, branch, and any repository subdirectory.')
|
|
56
|
+
.optional(),
|
|
57
|
+
localEditor: z.enum(localEditorNames)
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Editor used by source links in a development preview opened through a loopback address.'),
|
|
60
|
+
}).strict().refine(
|
|
61
|
+
(value) => value.baseUrl !== undefined || value.localEditor !== undefined,
|
|
62
|
+
'Specify baseUrl, localEditor, or both.',
|
|
63
|
+
).describe('Optional local and remote links from rendered pages to their Markdown source files.');
|
|
41
64
|
const createLineHeight = (minimum, role) => z.number()
|
|
42
65
|
.min(minimum, `Use a unitless ${role} line height of at least ${minimum}.`)
|
|
43
66
|
.max(3, `Use a unitless ${role} line height of at most 3.`)
|
|
@@ -222,7 +245,9 @@ const sitewideFooter = z.object({
|
|
|
222
245
|
const configShape = {
|
|
223
246
|
url: z.string().url().describe('Absolute public URL for the built site.'),
|
|
224
247
|
language: z.string().regex(/^(?:en|sv)(?:-[a-zA-Z0-9]+)*$/).optional().describe('Site language tag using Norna\'s English or Swedish interface text; the default is en.'),
|
|
248
|
+
editLink: editLink.optional(),
|
|
225
249
|
navigation: configNavigation.optional(),
|
|
250
|
+
search: z.boolean().optional().default(false).describe('Generate a static site search page and Pagefind index. The default is false.'),
|
|
226
251
|
scrollBehavior: z.enum(['instant', 'smooth']).optional().default('instant').describe('Use instant anchors by default or the browser\'s native smooth scrolling.'),
|
|
227
252
|
};
|
|
228
253
|
|
|
@@ -159,11 +159,43 @@ const addConfigHelp = (jsonSchema) => {
|
|
|
159
159
|
'Sets the page language and selects Norna\'s built-in English or Swedish interface text. The default is `en`.',
|
|
160
160
|
documentationLink('Language reference', 'configuration.md', 'language'),
|
|
161
161
|
], ['en', 'sv', 'en-GB', 'sv-SE']);
|
|
162
|
+
addHelp(jsonSchema, 'editLink', [
|
|
163
|
+
yamlExample('editLink:\n localEditor: vscode\n baseUrl: https://github.com/owner/repository/edit/main/'),
|
|
164
|
+
'Links each rendered page to its `content.md` source. A loopback development preview uses the explicitly selected local editor; published output and LAN previews use the optional remote base URL. Specify either destination or both.',
|
|
165
|
+
documentationLink('Edit-link reference', 'configuration.md', 'edit-link'),
|
|
166
|
+
]);
|
|
167
|
+
const editLink = schemaProperty(jsonSchema, 'editLink');
|
|
168
|
+
editLink.anyOf = [{ required: ['localEditor'] }, { required: ['baseUrl'] }];
|
|
169
|
+
addSnippets(jsonSchema, 'editLink', [schemaSnippet({
|
|
170
|
+
label: 'Local and remote source links',
|
|
171
|
+
body: {
|
|
172
|
+
localEditor: 'vscode',
|
|
173
|
+
baseUrl: 'https://github.com/${1:owner}/${2:repository}/edit/${3:main}/',
|
|
174
|
+
},
|
|
175
|
+
description: 'Open page source in VS Code during same-computer development and on the remote source host elsewhere.',
|
|
176
|
+
file: 'configuration.md',
|
|
177
|
+
anchor: 'edit-link',
|
|
178
|
+
})]);
|
|
179
|
+
addHelp(jsonSchema, 'editLink.baseUrl', [
|
|
180
|
+
yamlExample('editLink:\n baseUrl: https://github.com/owner/repository/edit/main/'),
|
|
181
|
+
'Optional absolute remote edit URL prefix. Include any repository subdirectory and branch. Norna adds the project-relative path to each page\'s `content.md` file. This destination is used by published output and LAN previews.',
|
|
182
|
+
documentationLink('Edit-link reference', 'configuration.md', 'edit-link'),
|
|
183
|
+
], ['https://github.com/owner/repository/edit/main/']);
|
|
184
|
+
addHelp(jsonSchema, 'editLink.localEditor', [
|
|
185
|
+
yamlExample('editLink:\n localEditor: vscode'),
|
|
186
|
+
'Optional editor for a development preview opened through localhost or another loopback address. `vscode` opens the absolute page source path through VS Code\'s registered URL handler. The setting is never emitted as a local path in published output.',
|
|
187
|
+
documentationLink('Edit-link reference', 'configuration.md', 'edit-link'),
|
|
188
|
+
], ['vscode']);
|
|
162
189
|
addHelp(jsonSchema, 'navigation', [
|
|
163
190
|
yamlExample('navigation:\n mode: automatic'),
|
|
164
|
-
'Sets the site-wide navigation policy. `automatic`
|
|
191
|
+
'Sets the site-wide navigation policy. `automatic` uses sections for one page, top navigation for a flat multi-page site, and a stable left rail throughout a hierarchical site.',
|
|
165
192
|
documentationLink('Navigation reference', 'pages.md', 'navigation'),
|
|
166
193
|
]);
|
|
194
|
+
addHelp(jsonSchema, 'search', [
|
|
195
|
+
yamlExample('search: true'),
|
|
196
|
+
'Generates a localized `/search/` page and a static Pagefind index during each build. Search is off by default, and only the search page loads the search JavaScript.',
|
|
197
|
+
documentationLink('Search reference', 'configuration.md', 'search'),
|
|
198
|
+
], [true, false]);
|
|
167
199
|
addFieldHelp(
|
|
168
200
|
jsonSchema,
|
|
169
201
|
'navigation.mode',
|
|
@@ -212,7 +244,7 @@ const addThemeHelp = (jsonSchema) => {
|
|
|
212
244
|
]);
|
|
213
245
|
addHelp(jsonSchema, 'blocks.cardList', [
|
|
214
246
|
yamlExample('blocks:\n cardList:\n width: text'),
|
|
215
|
-
'Sets site-wide defaults for card lists. A `width` option in an individual `
|
|
247
|
+
'Sets site-wide defaults for card lists. A `width` option in an individual `card-list` block overrides this setting.',
|
|
216
248
|
documentationLink('Content block defaults', 'theme.md', 'content-block-defaults'),
|
|
217
249
|
]);
|
|
218
250
|
addFieldHelp(
|
|
@@ -241,12 +273,12 @@ const addThemeHelp = (jsonSchema) => {
|
|
|
241
273
|
]);
|
|
242
274
|
addHelp(jsonSchema, 'palette', [
|
|
243
275
|
yamlExample('palette: warm-paper'),
|
|
244
|
-
'Chooses the site color character. Every palette provides coordinated light and dark variants.',
|
|
276
|
+
'Chooses the site color character. Every palette provides coordinated light and dark variants without changing the initial Appearance.',
|
|
245
277
|
documentationLink('Palette and appearance', 'theme.md', 'palette-and-appearance'),
|
|
246
278
|
], presentationPaletteNames);
|
|
247
279
|
addHelp(jsonSchema, 'appearance', [
|
|
248
280
|
yamlExample('appearance:\n default: system'),
|
|
249
|
-
'Selects the initial
|
|
281
|
+
'Selects the initial Appearance. Omit this setting to follow the visitor\'s system preference. Enable the reader-facing choice under readerControls.',
|
|
250
282
|
documentationLink('Appearance', 'theme.md', 'appearance'),
|
|
251
283
|
]);
|
|
252
284
|
addFieldHelp(jsonSchema, 'appearance.default', 'appearance:\n default: system', 'theme.md', 'appearance', ['system', 'light', 'dark']);
|
|
@@ -23,7 +23,7 @@ export const schemaValueDefinitions = Object.freeze([
|
|
|
23
23
|
smooth: option('Browser smooth', 'Use the browser\'s native smooth scrolling for anchor navigation.'),
|
|
24
24
|
}),
|
|
25
25
|
definition(['automatic', 'sections', 'top', 'tree'], {
|
|
26
|
-
automatic: option('Automatic', 'Choose sections for one page, top navigation for
|
|
26
|
+
automatic: option('Automatic', 'Choose sections for one page, top navigation for a flat multi-page site, and a stable left rail throughout a site with listed child pages or categories.'),
|
|
27
27
|
sections: option('Sections', 'Navigate the sections of the current page.'),
|
|
28
28
|
top: option('Top', 'Use pages in the top navigation and sections below them.'),
|
|
29
29
|
tree: option('Tree', 'Use a left page rail and a separate right H2/H3 contents rail where the page has enough headings.'),
|
|
@@ -32,6 +32,9 @@ export const schemaValueDefinitions = Object.freeze([
|
|
|
32
32
|
en: option('English', 'Use Norna\'s built-in English interface text.'),
|
|
33
33
|
sv: option('Swedish', 'Use Norna\'s built-in Swedish interface text.'),
|
|
34
34
|
}),
|
|
35
|
+
definition(['vscode'], {
|
|
36
|
+
vscode: option('Visual Studio Code', 'Open the current page source in the locally installed Visual Studio Code application.'),
|
|
37
|
+
}),
|
|
35
38
|
definition(['left', 'center', 'right'], {
|
|
36
39
|
left: option('Left', 'Align text with the left edge of its text area.'),
|
|
37
40
|
center: option('Center', 'Center text within its text area.'),
|