@janga/norna 0.7.22 → 0.7.23
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/astro.config.mjs +3 -0
- package/bin/norna-cli.mjs +2 -2
- package/package.json +17 -3
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +22 -24
- package/schemas/content-frontmatter.schema.json +25 -5
- package/schemas/page-theme.schema.json +53 -25
- package/schemas/sitewide-content.schema.json +19 -18
- package/schemas/theme.schema.json +383 -250
- package/scripts/check-config.mjs +24 -4
- package/scripts/deploy-site.mjs +0 -1
- package/scripts/dev-local.mjs +175 -112
- package/scripts/lib/content-sync-apply.mjs +34 -0
- package/scripts/lib/content-sync-plan.mjs +179 -0
- package/scripts/lib/editor-language-service.mjs +128 -0
- package/scripts/lib/heading-ids.mjs +1 -1
- package/scripts/lib/image-presentation.mjs +2 -0
- package/scripts/lib/markdown-links.mjs +182 -0
- package/scripts/lib/navigation-model.mjs +25 -14
- package/scripts/lib/norna-markdown-blocks.mjs +1 -0
- package/scripts/lib/page-aliases.mjs +164 -0
- package/scripts/lib/page-markdown.mjs +56 -6
- package/scripts/lib/presentation.mjs +19 -37
- package/scripts/lib/project-config.mjs +50 -29
- package/scripts/lib/schema-definitions.mjs +24 -13
- package/scripts/lib/schema-editor-metadata.mjs +43 -30
- package/scripts/lib/schema-value-definitions.mjs +6 -2
- package/scripts/lib/site-content.mjs +26 -4
- package/scripts/lib/site-link-graph.mjs +326 -0
- package/scripts/lib/site-page-urls.mjs +10 -0
- package/scripts/lib/sitemap.mjs +34 -0
- package/scripts/lib/theme-presets.mjs +51 -23
- package/scripts/lib/theme-profiles.mjs +7 -5
- package/scripts/lib/yaml-config.mjs +6 -2
- package/scripts/sync-content-sections.mjs +116 -213
- package/scripts/sync-site-public.mjs +23 -2
- package/src/components/CodeBlockCopyScript.astro +92 -0
- package/src/components/DisplaySettings.astro +4 -4
- package/src/components/ImageCarousel.astro +7 -3
- package/src/components/NavigationPageTree.astro +44 -60
- package/src/components/PageAliasRedirect.astro +49 -0
- package/src/components/PageContentsNavigation.astro +21 -0
- package/src/components/SectionNavigationScript.astro +62 -6
- package/src/components/SiteNavigation.astro +15 -3
- package/src/components/SitePage.astro +42 -2
- package/src/components/SiteSection.astro +5 -0
- package/src/components/SiteTreeNavigation.astro +0 -1
- package/src/content.config.ts +5 -2
- package/src/layouts/BaseLayout.astro +14 -8
- package/src/lib/readerPreferencesScript.mjs +2 -2
- package/src/lib/sitePages.ts +3 -2
- package/src/pages/[...slug].astro +35 -8
- package/src/styles/content.css +494 -0
- package/src/styles/foundations.css +284 -0
- package/src/styles/global.css +6 -2510
- package/src/styles/media.css +523 -0
- package/src/styles/navigation.css +495 -0
- package/src/styles/page-layout.css +437 -0
- package/src/styles/responsive.css +525 -0
- package/starters/basic/README.md +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'astro/zod';
|
|
2
|
+
import { imagePresentationNames } from './image-presentation.mjs';
|
|
2
3
|
import { navigationModeNames } from './navigation-model.mjs';
|
|
3
4
|
import { presentationPaletteNames } from './presentation-palette-metadata.mjs';
|
|
4
5
|
import { themePresetNames } from './theme-presets.mjs';
|
|
@@ -17,25 +18,26 @@ const headingWeight = z.union([z.literal(400), z.literal(500), z.literal(600), z
|
|
|
17
18
|
const typographyProfile = z.enum(['restrained', 'dense', 'reading', 'statement']).describe('Coordinated typography defaults. Omit this to use the selected preset.');
|
|
18
19
|
const themePreset = z.enum(themePresetNames).describe('Complete Norna visual preset. Start here and add overrides only when needed.');
|
|
19
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.');
|
|
20
|
-
const
|
|
21
|
-
default: z.enum(['system', 'light', 'dark']).optional().describe('Initial
|
|
21
|
+
const themeAppearance = z.object({
|
|
22
|
+
default: z.enum(['system', 'light', 'dark']).optional().describe('Initial appearance. System follows the visitor\'s operating-system preference.'),
|
|
22
23
|
}).strict().refine(
|
|
23
24
|
(value) => value.default !== undefined,
|
|
24
25
|
'Specify default.',
|
|
25
|
-
).describe('Site-wide
|
|
26
|
+
).describe('Site-wide initial appearance.');
|
|
26
27
|
const readerControls = z.object({
|
|
27
|
-
|
|
28
|
-
focusReading: z.boolean().optional().describe('
|
|
28
|
+
appearance: z.boolean().optional().describe('Show an Appearance control that lets readers choose System, Light, or Dark.'),
|
|
29
|
+
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.'),
|
|
29
30
|
}).strict().refine(
|
|
30
|
-
(value) => value.
|
|
31
|
-
'Specify
|
|
32
|
-
).describe('Optional
|
|
31
|
+
(value) => value.appearance !== undefined || value.focusReading !== undefined,
|
|
32
|
+
'Specify appearance, focusReading, or both.',
|
|
33
|
+
).describe('Optional controls for the site-wide Display panel.');
|
|
33
34
|
const spacingDensity = z.enum(['compact', 'normal', 'airy']).describe('Coordinated spacing density. Omit this to use the selected preset.');
|
|
34
35
|
const contentSpacing = z.enum(['compact', 'normal', 'spacious']).describe('Vertical spacing between page sections and structured content blocks.');
|
|
35
36
|
const backgroundPattern = z.enum(['uniform', 'alternating', 'accented']).describe('How coordinated backgrounds are assigned to H2 sections. Non-uniform patterns are unavailable with tree navigation.');
|
|
36
37
|
const cornerTreatment = z.enum(['square', 'rounded']).describe('Site-wide corner treatment for navigation, cards and framed content.');
|
|
37
38
|
const cardListWidth = z.enum(['text', 'narrow', 'normal', 'wide']).describe('Default maximum width for card lists. A width written in a norna-card-list block overrides this value.');
|
|
38
|
-
const
|
|
39
|
+
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 selects an effective mode for each page from its active top-level branch.');
|
|
39
41
|
const createLineHeight = (minimum, role) => z.number()
|
|
40
42
|
.min(minimum, `Use a unitless ${role} line height of at least ${minimum}.`)
|
|
41
43
|
.max(3, `Use a unitless ${role} line height of at most 3.`)
|
|
@@ -156,10 +158,11 @@ const pageThemeLayout = z.object({
|
|
|
156
158
|
'Specify contentSpacing, textWidth, or both.',
|
|
157
159
|
).describe('Page-local layout settings inherited by descendant pages.');
|
|
158
160
|
const themeImages = z.object({
|
|
161
|
+
presentation: imagePresentation.optional().describe('Managed-image presentation method. Omit this to keep the selected preset or inherited page setting.'),
|
|
159
162
|
width: visualCssLength.optional().describe('Maximum managed-image width. Omit this to keep the selected preset.'),
|
|
160
163
|
maxAvailableWidthPercent: responsivePercent.optional().describe('Maximum percentage of available horizontal space used by managed images.'),
|
|
161
|
-
maxAvailableHeightPercent: responsivePercent.optional().describe('Maximum percentage of viewport height used by
|
|
162
|
-
}).strict().describe('Optional defaults for managed
|
|
164
|
+
maxAvailableHeightPercent: responsivePercent.optional().describe('Maximum percentage of viewport height used by centered-fit images. This setting is unavailable with prose-aligned presentation.'),
|
|
165
|
+
}).strict().describe('Optional defaults for placing managed images inside the page content area. Persistent navigation remains outside this area.');
|
|
163
166
|
const themeCardList = z.object({
|
|
164
167
|
width: cardListWidth,
|
|
165
168
|
}).strict().describe('Site-wide card-list defaults.');
|
|
@@ -171,7 +174,6 @@ const themeBlocks = z.object({
|
|
|
171
174
|
).describe('Optional site-wide defaults for structured Norna content blocks.');
|
|
172
175
|
const configNavigation = z.object({
|
|
173
176
|
mode: navigationMode.optional().describe('Navigation model. Omit this to let Norna choose from the site structure.'),
|
|
174
|
-
sectionTracking: z.boolean().optional().default(false).describe('As the reader scrolls in tree navigation, mark the last H2 or H3 that has reached the reading area below the sticky header. The URL and keyboard focus do not change.'),
|
|
175
177
|
}).strict().describe('Site-wide navigation behavior.');
|
|
176
178
|
const themeSections = z.object({
|
|
177
179
|
backgroundPattern: backgroundPattern.optional().describe('Section background pattern. Alternating and accented create full-width bands with sections or top navigation; tree navigation requires uniform.'),
|
|
@@ -185,8 +187,17 @@ const pageThemeSections = z.object({
|
|
|
185
187
|
const pageNavigation = z.object({
|
|
186
188
|
listed: z.boolean().optional().default(true).describe('List this page in site navigation. The page remains public when false.'),
|
|
187
189
|
}).strict();
|
|
190
|
+
const pageAlias = z.string().regex(
|
|
191
|
+
/^\/(?:[a-z0-9]+(?:-[a-z0-9]+)*\/)+$/,
|
|
192
|
+
'Use an old site-relative Norna page URL such as "/guides/install/". Start and end with / and use lowercase letters, numbers, and single hyphens.',
|
|
193
|
+
).describe('Previous site-relative URL that permanently identifies this page.');
|
|
188
194
|
const pageMetadata = z.object({
|
|
189
195
|
description: z.string().min(1).optional().describe('Optional page-specific meta description.'),
|
|
196
|
+
aliases: z.array(pageAlias)
|
|
197
|
+
.min(1)
|
|
198
|
+
.refine((values) => new Set(values).size === values.length, 'Page aliases must be unique within the page.')
|
|
199
|
+
.optional()
|
|
200
|
+
.describe('Previous site-relative URLs that permanently identify this page. The current page is always the target.'),
|
|
190
201
|
}).strict().describe('Optional metadata for this homepage or additional page. The Markdown H1 supplies the page title.');
|
|
191
202
|
const sitewideLogo = z.object({
|
|
192
203
|
height: visualCssLength.optional().describe('Displayed logo height on wider screens. Omit it to use 2.6rem; narrow screens cap the height at 2.15rem. Width follows the intrinsic aspect ratio.'),
|
|
@@ -222,7 +233,7 @@ const siteShape = {
|
|
|
222
233
|
|
|
223
234
|
const themeVisualShape = {
|
|
224
235
|
preset: themePreset.optional().describe('Complete visual starting point. Add only the overrides the site actually needs.'),
|
|
225
|
-
|
|
236
|
+
appearance: themeAppearance.optional(),
|
|
226
237
|
readerControls: readerControls.optional(),
|
|
227
238
|
corners: cornerTreatment.optional().describe('Site-wide corner treatment. Omit this to use the selected preset.'),
|
|
228
239
|
layout: themeLayout.optional(),
|
|
@@ -33,6 +33,13 @@ const schemaProperty = (schema, propertyPath) => schemaProperties(schema, proper
|
|
|
33
33
|
const addHelp = (schema, propertyPath, paragraphs, examples = []) => {
|
|
34
34
|
for (const property of schemaProperties(schema, propertyPath)) {
|
|
35
35
|
property.markdownDescription = paragraphs.join('\n\n');
|
|
36
|
+
const reference = paragraphs.findLast((paragraph) => /^\[[^\]]+\]\(https:\/\//.test(paragraph));
|
|
37
|
+
if (reference && Array.isArray(property.oneOf)) {
|
|
38
|
+
for (const candidate of property.oneOf) {
|
|
39
|
+
if (!Object.hasOwn(candidate, 'const') || !candidate.description) continue;
|
|
40
|
+
candidate.markdownDescription = [candidate.description, reference].join('\n\n');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
36
43
|
if (examples.length > 0) property.examples = examples;
|
|
37
44
|
}
|
|
38
45
|
};
|
|
@@ -154,7 +161,7 @@ const addConfigHelp = (jsonSchema) => {
|
|
|
154
161
|
], ['en', 'sv', 'en-GB', 'sv-SE']);
|
|
155
162
|
addHelp(jsonSchema, 'navigation', [
|
|
156
163
|
yamlExample('navigation:\n mode: automatic'),
|
|
157
|
-
'Sets
|
|
164
|
+
'Sets the site-wide navigation policy. `automatic` selects an effective mode for each page from its active top-level branch.',
|
|
158
165
|
documentationLink('Navigation reference', 'pages.md', 'navigation'),
|
|
159
166
|
]);
|
|
160
167
|
addFieldHelp(
|
|
@@ -165,14 +172,6 @@ const addConfigHelp = (jsonSchema) => {
|
|
|
165
172
|
'navigation',
|
|
166
173
|
['automatic', 'sections', 'top', 'tree'],
|
|
167
174
|
);
|
|
168
|
-
addFieldHelp(
|
|
169
|
-
jsonSchema,
|
|
170
|
-
'navigation.sectionTracking',
|
|
171
|
-
'navigation:\n sectionTracking: true',
|
|
172
|
-
'configuration.md',
|
|
173
|
-
'navigationsectiontracking',
|
|
174
|
-
[true, false],
|
|
175
|
-
);
|
|
176
175
|
addHelp(jsonSchema, 'scrollBehavior', [
|
|
177
176
|
yamlExample('scrollBehavior: smooth'),
|
|
178
177
|
'Controls same-page anchor movement. `instant` is the default; `smooth` uses the browser\'s native smooth scrolling.',
|
|
@@ -202,8 +201,8 @@ const addThemeHelp = (jsonSchema) => {
|
|
|
202
201
|
documentationLink('Layout reference', 'theme.md', 'layout'),
|
|
203
202
|
]);
|
|
204
203
|
addHelp(jsonSchema, 'images', [
|
|
205
|
-
yamlExample('images:\n
|
|
206
|
-
'Overrides the
|
|
204
|
+
yamlExample('images:\n presentation: prose-aligned\n width: 900px'),
|
|
205
|
+
'Overrides how Norna-managed image stacks and carousels align inside the page content area and how much space they may use. Persistent navigation remains outside this area.',
|
|
207
206
|
documentationLink('Image sizing reference', 'theme.md', 'image-sizing'),
|
|
208
207
|
]);
|
|
209
208
|
addHelp(jsonSchema, 'blocks', [
|
|
@@ -243,28 +242,30 @@ const addThemeHelp = (jsonSchema) => {
|
|
|
243
242
|
addHelp(jsonSchema, 'palette', [
|
|
244
243
|
yamlExample('palette: warm-paper'),
|
|
245
244
|
'Chooses the site color character. Every palette provides coordinated light and dark variants.',
|
|
246
|
-
documentationLink('Palette and
|
|
245
|
+
documentationLink('Palette and appearance', 'theme.md', 'palette-and-appearance'),
|
|
247
246
|
], presentationPaletteNames);
|
|
248
|
-
addHelp(jsonSchema, '
|
|
249
|
-
yamlExample('
|
|
250
|
-
'
|
|
251
|
-
documentationLink('
|
|
247
|
+
addHelp(jsonSchema, 'appearance', [
|
|
248
|
+
yamlExample('appearance:\n default: system'),
|
|
249
|
+
'Selects the initial appearance. Enable the reader-facing choice under readerControls.',
|
|
250
|
+
documentationLink('Appearance', 'theme.md', 'appearance'),
|
|
252
251
|
]);
|
|
253
|
-
addFieldHelp(jsonSchema, '
|
|
254
|
-
addSnippets(jsonSchema, '
|
|
255
|
-
label: 'Set the initial
|
|
256
|
-
body: '
|
|
252
|
+
addFieldHelp(jsonSchema, 'appearance.default', 'appearance:\n default: system', 'theme.md', 'appearance', ['system', 'light', 'dark']);
|
|
253
|
+
addSnippets(jsonSchema, 'appearance', [schemaSnippet({
|
|
254
|
+
label: 'Set the initial appearance',
|
|
255
|
+
body: 'appearance:\n default: ${1:system}',
|
|
257
256
|
description: 'Follow the system color preference or choose a fixed initial appearance.',
|
|
258
257
|
file: 'theme.md',
|
|
259
|
-
anchor: '
|
|
258
|
+
anchor: 'appearance',
|
|
260
259
|
})]);
|
|
261
260
|
addHelp(jsonSchema, 'readerControls', [
|
|
262
|
-
yamlExample('readerControls:\n
|
|
263
|
-
'
|
|
261
|
+
yamlExample('readerControls:\n appearance: true\n focusReading: true'),
|
|
262
|
+
'Choose which optional controls readers can use in the site-wide Display panel.',
|
|
263
|
+
'`appearance: true` lets readers choose System, Light, or Dark. `focusReading: true` lets them hide navigation, breadcrumbs, and the footer while reading.',
|
|
264
|
+
'Reading width is always included. Sites with tree navigation always include Focus reading, even when it is not enabled here.',
|
|
264
265
|
documentationLink('Reader Display controls', 'theme.md', 'reader-display-controls'),
|
|
265
266
|
]);
|
|
266
267
|
for (const [propertyPath, example] of [
|
|
267
|
-
['readerControls.
|
|
268
|
+
['readerControls.appearance', 'readerControls:\n appearance: true'],
|
|
268
269
|
['readerControls.focusReading', 'readerControls:\n focusReading: true'],
|
|
269
270
|
]) {
|
|
270
271
|
addFieldHelp(
|
|
@@ -277,8 +278,8 @@ const addThemeHelp = (jsonSchema) => {
|
|
|
277
278
|
}
|
|
278
279
|
addSnippets(jsonSchema, 'readerControls', [schemaSnippet({
|
|
279
280
|
label: 'Configure the Display panel',
|
|
280
|
-
body: 'readerControls:\n
|
|
281
|
-
description: '
|
|
281
|
+
body: 'readerControls:\n appearance: ${1:true}\n focusReading: ${2:true}',
|
|
282
|
+
description: 'Show optional Appearance and Focus reading controls. Reading width is already included, and tree navigation always includes Focus reading.',
|
|
282
283
|
file: 'theme.md',
|
|
283
284
|
anchor: 'reader-display-controls',
|
|
284
285
|
})]);
|
|
@@ -352,6 +353,7 @@ const addThemeHelp = (jsonSchema) => {
|
|
|
352
353
|
})]);
|
|
353
354
|
|
|
354
355
|
const imageFields = [
|
|
356
|
+
['images.presentation', 'images:\n presentation: prose-aligned'],
|
|
355
357
|
['images.width', 'images:\n width: 900px'],
|
|
356
358
|
['images.maxAvailableWidthPercent', 'images:\n maxAvailableWidthPercent:\n desktop: 100\n mobile: 100'],
|
|
357
359
|
['images.maxAvailableWidthPercent.desktop', 'images:\n maxAvailableWidthPercent:\n desktop: 100'],
|
|
@@ -464,11 +466,12 @@ const addPageThemeHelp = (jsonSchema) => {
|
|
|
464
466
|
addFieldHelp(jsonSchema, 'layout.textWidth', 'layout:\n textWidth: narrow', 'theme.md', 'page-themes', ['narrow', 'normal', 'wide']);
|
|
465
467
|
addFieldHelp(jsonSchema, 'layout.contentSpacing', 'layout:\n contentSpacing: compact', 'theme.md', 'page-themes', ['compact', 'normal', 'spacious']);
|
|
466
468
|
addHelp(jsonSchema, 'images', [
|
|
467
|
-
yamlExample('images:\n width: 900px'),
|
|
468
|
-
'Adjusts managed-image sizing for pages below this directory.',
|
|
469
|
+
yamlExample('images:\n presentation: prose-aligned\n width: 900px'),
|
|
470
|
+
'Adjusts managed-image presentation and sizing inside the content area for pages below this directory. Persistent navigation remains outside this area.',
|
|
469
471
|
documentationLink('Image sizing reference', 'theme.md', 'image-sizing'),
|
|
470
472
|
]);
|
|
471
473
|
const imageFields = [
|
|
474
|
+
['images.presentation', 'images:\n presentation: prose-aligned'],
|
|
472
475
|
['images.width', 'images:\n width: 900px'],
|
|
473
476
|
['images.maxAvailableWidthPercent', 'images:\n maxAvailableWidthPercent:\n desktop: 100\n mobile: 100'],
|
|
474
477
|
['images.maxAvailableWidthPercent.desktop', 'images:\n maxAvailableWidthPercent:\n desktop: 100'],
|
|
@@ -563,8 +566,8 @@ const addContentHelp = (jsonSchema) => {
|
|
|
563
566
|
documentationLink('Page title and frontmatter reference', 'content.md', 'page-title-and-frontmatter'),
|
|
564
567
|
].join('\n\n');
|
|
565
568
|
addHelp(jsonSchema, 'page', [
|
|
566
|
-
yamlExample('page:\n description: About this project
|
|
567
|
-
'Contains optional metadata for the current homepage or additional page. The Markdown H1 supplies its title.',
|
|
569
|
+
yamlExample('page:\n description: About this project.\n aliases:\n - /old-about/'),
|
|
570
|
+
'Contains optional metadata for the current homepage or additional page. The Markdown H1 supplies its title. An alias records an old URL for this same page.',
|
|
568
571
|
documentationLink('Page title and frontmatter reference', 'content.md', 'page-title-and-frontmatter'),
|
|
569
572
|
]);
|
|
570
573
|
addHelp(jsonSchema, 'page.description', [
|
|
@@ -572,6 +575,16 @@ const addContentHelp = (jsonSchema) => {
|
|
|
572
575
|
'An optional page-specific description rendered as `<meta name="description">`. It is not visible in page content.',
|
|
573
576
|
documentationLink('Page title and frontmatter reference', 'content.md', 'page-title-and-frontmatter'),
|
|
574
577
|
], ['About this project.']);
|
|
578
|
+
addHelp(jsonSchema, 'page.aliases', [
|
|
579
|
+
yamlExample('page:\n aliases:\n - /old-about/'),
|
|
580
|
+
'Lists previous site-relative URLs that permanently identify this page. The page containing the list is always the target. Paths omit the configured base path and are excluded from the sitemap.',
|
|
581
|
+
documentationLink('Preserve old page URLs', 'pages.md', 'preserve-old-page-urls'),
|
|
582
|
+
], [['/old-about/']]);
|
|
583
|
+
addHelp(jsonSchema, 'page.aliases.[]', [
|
|
584
|
+
yamlExample('page:\n aliases:\n - /old-about/'),
|
|
585
|
+
'Use a previous Norna page URL. Start and end with `/`; use lowercase ASCII letters, numbers, and single hyphens; do not include the configured base path, a query string, or a fragment.',
|
|
586
|
+
documentationLink('Alias path rules', 'pages.md', 'preserve-old-page-urls'),
|
|
587
|
+
], ['/old-about/']);
|
|
575
588
|
addHelp(jsonSchema, 'navigation', [
|
|
576
589
|
yamlExample('navigation:\n listed: false'),
|
|
577
590
|
'Optionally excludes an additional page from site navigation without removing its public URL.',
|
|
@@ -23,10 +23,10 @@ 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 Home and independent top-level pages, and tree navigation inside branches with listed children 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
|
-
tree: option('Tree', 'Use a
|
|
29
|
+
tree: option('Tree', 'Use a left page rail and a separate right H2/H3 contents rail where the page has enough headings.'),
|
|
30
30
|
}),
|
|
31
31
|
definition(['en', 'sv'], {
|
|
32
32
|
en: option('English', 'Use Norna\'s built-in English interface text.'),
|
|
@@ -54,6 +54,10 @@ export const schemaValueDefinitions = Object.freeze([
|
|
|
54
54
|
normal: option('Normal', 'Limit the complete card list to at most 56rem.'),
|
|
55
55
|
wide: option('Wide', 'Allow the complete card list to use the available page-layout width.'),
|
|
56
56
|
}),
|
|
57
|
+
definition(['prose-aligned', 'centered-fit'], {
|
|
58
|
+
'prose-aligned': option('Prose-aligned', 'Start managed images and captions at the body-text edge and size them from the page content area.'),
|
|
59
|
+
'centered-fit': option('Centered fit', 'Center managed images and captions in the page media area and constrain them by available width and viewport height.'),
|
|
60
|
+
}),
|
|
57
61
|
definition([400, 500, 600, 700], {
|
|
58
62
|
400: option('Regular', 'Use regular font weight.'),
|
|
59
63
|
500: option('Medium', 'Use medium font weight.'),
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
schemaTopLevelKeys,
|
|
5
|
+
siteSchema,
|
|
6
|
+
} from './schema-definitions.mjs';
|
|
3
7
|
import {
|
|
4
8
|
siteContentLabel,
|
|
5
9
|
sitewideContentLabel,
|
|
6
10
|
} from './site-paths.mjs';
|
|
7
|
-
import { schemaTopLevelKeys } from './schema-definitions.mjs';
|
|
8
11
|
import { getIndentInfo, validateYamlIndentation } from './yaml-indentation.mjs';
|
|
12
|
+
import { parseYamlConfig } from './yaml-config.mjs';
|
|
9
13
|
|
|
10
14
|
export { getContentFiles } from './site-structure.mjs';
|
|
11
15
|
|
|
@@ -24,11 +28,12 @@ const knownPageThemeTopLevelFrontmatterKeys = new Set(
|
|
|
24
28
|
const knownSitewideTopLevelFrontmatterKeys = new Set(schemaTopLevelKeys.sitewide);
|
|
25
29
|
const knownNestedFrontmatterKeys = new Set([
|
|
26
30
|
'align',
|
|
31
|
+
'aliases',
|
|
27
32
|
'alt',
|
|
28
33
|
'body',
|
|
29
34
|
'blockGap',
|
|
30
35
|
'caption',
|
|
31
|
-
'
|
|
36
|
+
'appearance',
|
|
32
37
|
'carousel',
|
|
33
38
|
'contentSpacing',
|
|
34
39
|
'desktop',
|
|
@@ -104,7 +109,10 @@ export const splitSiteFile = (source, label = siteContentLabel) => {
|
|
|
104
109
|
};
|
|
105
110
|
};
|
|
106
111
|
|
|
107
|
-
export const readSiteFile = async (sitePath, label = siteContentLabel) =>
|
|
112
|
+
export const readSiteFile = async (sitePath, label = siteContentLabel) => {
|
|
113
|
+
const source = await readFile(sitePath, 'utf8');
|
|
114
|
+
return { ...splitSiteFile(source, label), source };
|
|
115
|
+
};
|
|
108
116
|
|
|
109
117
|
export const validateFrontmatterIndentation = (frontmatter, addIssue) => {
|
|
110
118
|
validateYamlIndentation(frontmatter, (issue) => addIssue({
|
|
@@ -157,7 +165,12 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
|
|
|
157
165
|
fix = fileKind === 'page theme'
|
|
158
166
|
? 'Remove "shape:" from this page theme. Set "corners: square" or "corners: rounded" in the root theme.yaml when an override is needed.'
|
|
159
167
|
: 'Replace "shape:" with "corners:". Use "square" or replace the old "soft" value with "rounded".';
|
|
160
|
-
} else if (fileKind === 'page theme' &&
|
|
168
|
+
} else if ((fileKind === 'theme' || fileKind === 'page theme') && key === 'colorMode') {
|
|
169
|
+
message = `Frontmatter line ${lineNumber}: Theme setting "colorMode" was replaced by "appearance".`;
|
|
170
|
+
fix = fileKind === 'page theme'
|
|
171
|
+
? 'Remove "colorMode:" from this page theme. Set "appearance:" in the root theme.yaml when an override is needed.'
|
|
172
|
+
: 'Replace "colorMode:" with "appearance:".';
|
|
173
|
+
} else if (fileKind === 'page theme' && ['preset', 'appearance', 'readerControls', 'corners', 'palette', 'typography'].includes(key)) {
|
|
161
174
|
message = `Frontmatter line ${lineNumber}: page themes may not define site-wide visual identity through "${key}".`;
|
|
162
175
|
fix = `Move "${key}:" to the root theme.yaml. Page themes may set only layout.textWidth, layout.contentSpacing, images, and sections.backgroundPattern.`;
|
|
163
176
|
} else if ((fileKind === 'theme' || fileKind === 'page theme') && ['logo', 'site'].includes(key)) {
|
|
@@ -186,6 +199,15 @@ export const validateContentFrontmatterStructure = (frontmatter, addIssue) =>
|
|
|
186
199
|
fileKind: 'content',
|
|
187
200
|
});
|
|
188
201
|
|
|
202
|
+
export const parseContentFrontmatter = (frontmatterBody, label = siteContentLabel) => parseYamlConfig(
|
|
203
|
+
frontmatterBody,
|
|
204
|
+
`${label} frontmatter`,
|
|
205
|
+
{
|
|
206
|
+
schema: siteSchema,
|
|
207
|
+
validateStructure: validateContentFrontmatterStructure,
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
|
|
189
211
|
export const validateConfigYamlStructure = (frontmatter, addIssue) =>
|
|
190
212
|
validateFrontmatterStructure(frontmatter, addIssue, {
|
|
191
213
|
knownTopLevelFrontmatterKeys: knownConfigTopLevelFrontmatterKeys,
|