@janga/norna 0.7.9 → 0.7.10
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 +1 -1
- package/docs/README.md +14 -11
- package/docs/configuration.md +9 -54
- package/docs/content.md +6 -7
- package/docs/design/norna-diagram-design.md +2 -0
- package/docs/getting-started.md +18 -16
- package/docs/routes.md +7 -6
- package/docs/site-structure.md +4 -2
- package/docs/sitewide-content.md +74 -0
- package/docs/theme.md +61 -128
- package/docs/typography.md +45 -33
- package/examples/001-dog-shelter-single-page/README.md +9 -0
- package/examples/001-dog-shelter-single-page/site/config.mjs +27 -0
- package/examples/001-dog-shelter-single-page/site/content.md +38 -0
- package/examples/001-dog-shelter-single-page/site/images/what-we-do/dog-house.svg +25 -0
- package/examples/001-dog-shelter-single-page/site/images/you-can-help/heart.svg +13 -0
- package/examples/001-dog-shelter-single-page/site/public/favicon.svg +10 -0
- package/examples/001-dog-shelter-single-page/site/public/robots.txt +2 -0
- package/examples/001-dog-shelter-single-page/site/sitewide-content.md +6 -0
- package/examples/001-dog-shelter-single-page/site/theme.md +25 -0
- package/examples/002-dog-shelter-multi-page/README.md +9 -0
- package/examples/002-dog-shelter-multi-page/site/config.mjs +27 -0
- package/examples/002-dog-shelter-multi-page/site/content.md +12 -0
- package/examples/002-dog-shelter-multi-page/site/public/favicon.svg +10 -0
- package/examples/002-dog-shelter-multi-page/site/public/robots.txt +2 -0
- package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rex.svg +19 -0
- package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/images/dogs/rover.svg +19 -0
- package/examples/002-dog-shelter-multi-page/site/routes/010-dogs/route-content.md +30 -0
- package/examples/002-dog-shelter-multi-page/site/routes/020-adopt/route-content.md +12 -0
- package/examples/002-dog-shelter-multi-page/site/sitewide-content.md +6 -0
- package/examples/002-dog-shelter-multi-page/site/theme.md +25 -0
- package/examples/dog-gallery/.astro/collections/site.schema.json +776 -0
- package/examples/dog-gallery/.astro/collections/theme.schema.json +672 -0
- package/examples/dog-gallery/.astro/content-assets.mjs +4 -0
- package/examples/dog-gallery/.astro/content-modules.mjs +1 -0
- package/examples/dog-gallery/.astro/content.d.ts +175 -0
- package/examples/dog-gallery/.astro/data-store.json +1 -0
- package/examples/dog-gallery/.astro/dev-local.json +8 -0
- package/examples/dog-gallery/.astro/dev.json +13 -0
- package/examples/dog-gallery/.astro/dev.log +95 -0
- package/examples/dog-gallery/.astro/settings.json +5 -0
- package/examples/dog-gallery/.astro/types.d.ts +2 -0
- package/examples/dog-gallery/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
- package/examples/dog-gallery/dist/_astro/SitePage.DicGh_5N.css +1 -0
- package/examples/dog-gallery/dist/dog-care/index.html +15 -0
- package/examples/dog-gallery/dist/dog-shelter-logo.svg +14 -0
- package/examples/dog-gallery/dist/favicon.svg +7 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-1536.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/black-puppy-meadow-7255e3d9-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/black-dogs/photo-of-a-black-dog-5cf614d2-960.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-1920.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-3939.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/brown-dog-50ef114e-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-1920.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-2400.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/brown-dogs/dog-accompanies-master-7ef804d5-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-1600.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/golden-retriever-f797fd88-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-1920.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-2576.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/golden-dogs/toller-puppy-a2a8fbc0-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-1277.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-cute-dog-077a0689-768.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1080.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1440.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-1536.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-480.webp +0 -0
- package/examples/dog-gallery/dist/images/generated/white-dogs/white-puppy-garden-d3ab2ca0-768.webp +0 -0
- package/examples/dog-gallery/dist/index.html +45 -0
- package/examples/dog-gallery/dist/robots.txt +2 -0
- package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +7 -7
- package/examples/dog-gallery/site/.norna/generated-images.json +8 -8
- package/examples/dog-gallery/site/config.mjs +0 -20
- package/examples/dog-gallery/site/content.md +4 -5
- package/examples/dog-gallery/site/public/logo.svg +14 -0
- package/examples/dog-gallery/site/sitewide-content.md +16 -0
- package/examples/dog-gallery/site/theme.md +13 -24
- package/examples/presentation/README.md +15 -0
- package/examples/presentation/site/.norna/generated-images.json +66 -0
- package/examples/presentation/site/config.mjs +29 -0
- package/examples/presentation/site/content.md +67 -0
- package/examples/presentation/site/public/robots.txt +2 -0
- package/examples/presentation/site/routes/010-presets/route-content.md +63 -0
- package/examples/presentation/site/routes/010-presets/theme.md +4 -0
- package/examples/presentation/site/routes/020-media/images/cards/card-carousel.svg +11 -0
- package/examples/presentation/site/routes/020-media/images/cards/card-stack.svg +9 -0
- package/examples/presentation/site/routes/020-media/images/cards/card-surfaces.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/carousel/carousel-one.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/carousel/carousel-three.svg +13 -0
- package/examples/presentation/site/routes/020-media/images/carousel/carousel-two.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/stack/stack-one.svg +8 -0
- package/examples/presentation/site/routes/020-media/images/stack/stack-two.svg +9 -0
- package/examples/presentation/site/routes/020-media/route-content.md +77 -0
- package/examples/presentation/site/routes/020-media/theme.md +4 -0
- package/examples/presentation/site/routes/030-surfaces/route-content.md +44 -0
- package/examples/presentation/site/routes/030-surfaces/theme.md +7 -0
- package/examples/presentation/site/sitewide-content.md +6 -0
- package/examples/presentation/site/theme.md +43 -0
- package/examples/typography-presets/.astro/collections/routeTheme.schema.json +746 -0
- package/examples/typography-presets/.astro/collections/site.schema.json +0 -717
- package/examples/typography-presets/.astro/collections/sitewide.schema.json +114 -0
- package/examples/typography-presets/.astro/collections/theme.schema.json +161 -59
- package/examples/typography-presets/.astro/content.d.ts +8 -0
- package/examples/typography-presets/.astro/data-store.json +1 -1
- package/examples/typography-presets/.astro/dev-local.json +2 -2
- package/examples/typography-presets/.astro/dev.json +2 -2
- package/examples/typography-presets/.astro/dev.log +37 -16
- package/examples/typography-presets/site/.norna/generated-images.json +76 -76
- package/examples/typography-presets/site/config.mjs +0 -13
- package/examples/typography-presets/site/content.md +8 -126
- package/examples/typography-presets/site/routes/010-quiet-gallery/route-content.md +34 -0
- package/examples/typography-presets/site/routes/010-quiet-gallery/theme.md +4 -0
- package/examples/typography-presets/site/routes/020-compact-gallery/route-content.md +33 -0
- package/examples/typography-presets/site/routes/020-compact-gallery/theme.md +4 -0
- package/examples/typography-presets/site/routes/030-text-forward/route-content.md +34 -0
- package/examples/typography-presets/site/routes/030-text-forward/theme.md +4 -0
- package/examples/typography-presets/site/routes/040-statement/route-content.md +34 -0
- package/examples/typography-presets/site/routes/040-statement/theme.md +4 -0
- package/examples/typography-presets/site/sitewide-content.md +14 -0
- package/examples/typography-presets/site/theme.md +9 -9
- package/fixtures/basic/site/config.mjs +0 -13
- package/fixtures/basic/site/sitewide-content.md +12 -0
- package/fixtures/basic/site/theme.md +2 -2
- package/fixtures/content-model-v2/site/content.md +2 -5
- package/fixtures/content-model-v2/site/theme.md +1 -3
- package/package.json +3 -2
- package/scripts/check-config.mjs +22 -1
- package/scripts/lib/frontmatter-yaml.mjs +28 -0
- package/scripts/lib/logo-assets.mjs +35 -0
- package/scripts/lib/norna-markdown-blocks.mjs +166 -0
- package/scripts/lib/presentation.mjs +104 -46
- package/scripts/lib/project-config.mjs +67 -105
- package/scripts/lib/site-content.mjs +26 -118
- package/scripts/lib/site-paths.mjs +2 -0
- package/scripts/lib/sitewide-config.mjs +34 -0
- package/scripts/lib/theme-config.mjs +72 -8
- package/scripts/lib/typography.mjs +20 -26
- package/scripts/show-typography.mjs +40 -147
- package/scripts/sync-content-sections.mjs +19 -17
- package/scripts/test-content-check.mjs +59 -27
- package/scripts/test-content-model-v2.mjs +154 -0
- package/scripts/test-engine-commands.mjs +3 -1
- package/scripts/test-package-check.mjs +66 -24
- package/scripts/test-temporary-visibility.mjs +19 -2
- package/src/components/CardList.astro +5 -2
- package/src/components/GalleryGrid.astro +37 -16
- package/src/components/SiteBanners.astro +100 -0
- package/src/components/SiteNavigation.astro +48 -31
- package/src/components/SitePage.astro +55 -13
- package/src/components/SiteSection.astro +193 -35
- package/src/content.config.ts +78 -48
- package/src/layouts/BaseLayout.astro +64 -14
- package/src/lib/routeThemes.ts +61 -0
- package/src/lib/sectionContent.ts +235 -51
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/styles/global.css +282 -93
- package/starters/basic/.github/workflows/deploy.yml +1 -1
- package/starters/basic/README.md +7 -6
- package/starters/basic/site/config.mjs +0 -13
- package/starters/basic/site/content.md +1 -1
- package/starters/basic/site/sitewide-content.md +14 -0
- package/starters/basic/site/theme.md +13 -24
- package/starters/project/.github/workflows/deploy.yml +1 -1
- package/starters/project/README.md +10 -8
- package/starters/project/site/config.mjs +0 -13
- package/starters/project/site/sitewide-content.md +14 -0
- package/starters/project/site/theme.md +3 -11
- /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/puppy-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/shepherd-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/010-quiet-gallery/images}/quiet-gallery/white-puppy.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/black-schnauzer.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/retriever-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/020-compact-gallery/images}/compact-gallery/schnauzer-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/boxer-portrait.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/grass-puppy.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/030-text-forward/images}/text-forward/grey-street-dog.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/black-pedigree.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/sheepdog-grass.jpg +0 -0
- /package/examples/typography-presets/site/{images → routes/040-statement/images}/statement/sheepdog-wide.jpg +0 -0
|
@@ -37,20 +37,6 @@ const readString = (object, key, path, sourceLabel = siteConfigLabel) => {
|
|
|
37
37
|
return value.trim();
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
const readOptionalString = (object, key, path, sourceLabel = siteConfigLabel) => {
|
|
41
|
-
const value = object[key];
|
|
42
|
-
|
|
43
|
-
if (value === undefined || value === null || value === '') {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (typeof value !== 'string' || value.trim() === '') {
|
|
48
|
-
throw new Error(`${path}.${key} must be a non-empty string when set in ${sourceLabel}.`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return value.trim();
|
|
52
|
-
};
|
|
53
|
-
|
|
54
40
|
const readFontFamily = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
55
41
|
const value = object[key] ?? fallback;
|
|
56
42
|
|
|
@@ -242,58 +228,19 @@ const readLocale = (rawLocale) => {
|
|
|
242
228
|
lang: lang.trim(),
|
|
243
229
|
labels: Object.freeze({
|
|
244
230
|
closeMenu: readString({ closeMenu: labels.closeMenu ?? 'Close menu' }, 'closeMenu', 'locale.labels'),
|
|
231
|
+
dismissBanner: readString({ dismissBanner: labels.dismissBanner ?? 'Dismiss notice' }, 'dismissBanner', 'locale.labels'),
|
|
245
232
|
skipToContent: readString({ skipToContent: labels.skipToContent ?? 'Skip to content' }, 'skipToContent', 'locale.labels'),
|
|
246
233
|
sectionNavigation: readString({ sectionNavigation: labels.sectionNavigation ?? 'Sections' }, 'sectionNavigation', 'locale.labels'),
|
|
247
234
|
gallery: readString({ gallery: labels.gallery ?? 'Images' }, 'gallery', 'locale.labels'),
|
|
235
|
+
note: readString({ note: labels.note ?? 'Note' }, 'note', 'locale.labels'),
|
|
248
236
|
menu: readString({ menu: labels.menu ?? 'Menu' }, 'menu', 'locale.labels'),
|
|
249
237
|
pageNavigation: readString({ pageNavigation: labels.pageNavigation ?? 'On this page' }, 'pageNavigation', 'locale.labels'),
|
|
250
238
|
siteNavigation: readString({ siteNavigation: labels.siteNavigation ?? 'Pages' }, 'siteNavigation', 'locale.labels'),
|
|
239
|
+
siteBanners: readString({ siteBanners: labels.siteBanners ?? 'Site notices' }, 'siteBanners', 'locale.labels'),
|
|
251
240
|
}),
|
|
252
241
|
});
|
|
253
242
|
};
|
|
254
243
|
|
|
255
|
-
const readDateTimeFormat = (object, path) => {
|
|
256
|
-
const dateTimeFormat = assertObject(object, path);
|
|
257
|
-
const locale = readString(dateTimeFormat, 'locale', path);
|
|
258
|
-
const timeZone = readString(dateTimeFormat, 'timeZone', path);
|
|
259
|
-
const dateStyle = readString(dateTimeFormat, 'dateStyle', path);
|
|
260
|
-
const timeStyle = readString(dateTimeFormat, 'timeStyle', path);
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
new Intl.DateTimeFormat(locale, {
|
|
264
|
-
dateStyle,
|
|
265
|
-
timeStyle,
|
|
266
|
-
timeZone,
|
|
267
|
-
});
|
|
268
|
-
} catch (error) {
|
|
269
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
270
|
-
throw new Error(`${path} must be a valid Intl.DateTimeFormat configuration in ${siteConfigLabel}: ${message}`);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return Object.freeze({
|
|
274
|
-
dateStyle,
|
|
275
|
-
locale,
|
|
276
|
-
timeStyle,
|
|
277
|
-
timeZone,
|
|
278
|
-
});
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const readBuildInfo = (footer) => {
|
|
282
|
-
const value = footer.buildInfo;
|
|
283
|
-
|
|
284
|
-
if (value === undefined || value === null || value === false) {
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const buildInfo = assertObject(value, 'footer.buildInfo');
|
|
289
|
-
|
|
290
|
-
return Object.freeze({
|
|
291
|
-
enabled: readBoolean(buildInfo, 'enabled', 'footer.buildInfo', true),
|
|
292
|
-
text: readString(buildInfo, 'text', 'footer.buildInfo'),
|
|
293
|
-
dateTimeFormat: readDateTimeFormat(buildInfo.dateTimeFormat, 'footer.buildInfo.dateTimeFormat'),
|
|
294
|
-
});
|
|
295
|
-
};
|
|
296
|
-
|
|
297
244
|
const rawConfig = assertObject(siteConfig, 'default export');
|
|
298
245
|
const misplacedThemeKeys = ['layout', 'gallery', 'typography'].filter((key) => key in rawConfig);
|
|
299
246
|
if (misplacedThemeKeys.length > 0) {
|
|
@@ -302,13 +249,8 @@ if (misplacedThemeKeys.length > 0) {
|
|
|
302
249
|
|
|
303
250
|
const rawTheme = assertObject(themeConfig, 'theme frontmatter', siteThemeLabel);
|
|
304
251
|
const rawSite = assertObject(rawConfig.site, 'site');
|
|
305
|
-
const rawLayout = assertObject(rawTheme.layout ?? {}, 'layout', siteThemeLabel);
|
|
306
|
-
const rawLayoutSpacing = assertObject(rawLayout.spacing ?? {}, 'layout.spacing', siteThemeLabel);
|
|
307
|
-
const rawGallery = assertObject(rawTheme.gallery ?? {}, 'gallery', siteThemeLabel);
|
|
308
|
-
const rawTypography = assertObject(rawTheme.typography ?? {}, 'typography', siteThemeLabel);
|
|
309
252
|
const rawNavigation = assertObject(rawConfig.navigation ?? {}, 'navigation');
|
|
310
253
|
const rawLocale = rawConfig.locale ?? {};
|
|
311
|
-
const rawFooter = assertObject(rawConfig.footer ?? {}, 'footer');
|
|
312
254
|
const rawGithub = assertObject(rawConfig.github, 'github');
|
|
313
255
|
const rawDeploy = assertObject(rawConfig.deploy ?? {}, 'deploy');
|
|
314
256
|
const rawDeployWatch = assertObject(rawDeploy.watch ?? {}, 'deploy.watch');
|
|
@@ -317,9 +259,9 @@ const defaultFontFamily = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
|
|
|
317
259
|
const layoutDensityNames = ['compact', 'normal', 'airy'];
|
|
318
260
|
const layoutDensityProfiles = Object.freeze({
|
|
319
261
|
compact: Object.freeze({
|
|
320
|
-
|
|
321
|
-
desktop: '
|
|
322
|
-
mobile: '
|
|
262
|
+
blockGap: Object.freeze({
|
|
263
|
+
desktop: '1.25em',
|
|
264
|
+
mobile: '1.1em',
|
|
323
265
|
}),
|
|
324
266
|
finalSectionBottom: Object.freeze({
|
|
325
267
|
desktop: 'clamp(1.2rem, 2.4vw, 2.25rem)',
|
|
@@ -329,6 +271,10 @@ const layoutDensityProfiles = Object.freeze({
|
|
|
329
271
|
desktop: 'clamp(1rem, 2vw, 1.75rem)',
|
|
330
272
|
mobile: '1rem',
|
|
331
273
|
}),
|
|
274
|
+
headingToBlock: Object.freeze({
|
|
275
|
+
desktop: '0.65em',
|
|
276
|
+
mobile: '0.6em',
|
|
277
|
+
}),
|
|
332
278
|
imageGap: Object.freeze({
|
|
333
279
|
desktop: 'clamp(1rem, 2vw, 1.5rem)',
|
|
334
280
|
mobile: '1.25rem',
|
|
@@ -339,9 +285,9 @@ const layoutDensityProfiles = Object.freeze({
|
|
|
339
285
|
}),
|
|
340
286
|
}),
|
|
341
287
|
normal: Object.freeze({
|
|
342
|
-
|
|
343
|
-
desktop: '1.
|
|
344
|
-
mobile: '
|
|
288
|
+
blockGap: Object.freeze({
|
|
289
|
+
desktop: '1.5em',
|
|
290
|
+
mobile: '1.25em',
|
|
345
291
|
}),
|
|
346
292
|
finalSectionBottom: Object.freeze({
|
|
347
293
|
desktop: 'clamp(1.4rem, 3vw, 2.75rem)',
|
|
@@ -351,6 +297,10 @@ const layoutDensityProfiles = Object.freeze({
|
|
|
351
297
|
desktop: 'clamp(1.25rem, 3vw, 2.5rem)',
|
|
352
298
|
mobile: '1.25rem',
|
|
353
299
|
}),
|
|
300
|
+
headingToBlock: Object.freeze({
|
|
301
|
+
desktop: '0.75em',
|
|
302
|
+
mobile: '0.7em',
|
|
303
|
+
}),
|
|
354
304
|
imageGap: Object.freeze({
|
|
355
305
|
desktop: 'clamp(1.25rem, 2.8vw, 2rem)',
|
|
356
306
|
mobile: '1.5rem',
|
|
@@ -361,9 +311,9 @@ const layoutDensityProfiles = Object.freeze({
|
|
|
361
311
|
}),
|
|
362
312
|
}),
|
|
363
313
|
airy: Object.freeze({
|
|
364
|
-
|
|
365
|
-
desktop: '1.
|
|
366
|
-
mobile: '1.
|
|
314
|
+
blockGap: Object.freeze({
|
|
315
|
+
desktop: '1.75em',
|
|
316
|
+
mobile: '1.5em',
|
|
367
317
|
}),
|
|
368
318
|
finalSectionBottom: Object.freeze({
|
|
369
319
|
desktop: 'clamp(2.25rem, 5vw, 4.5rem)',
|
|
@@ -373,6 +323,10 @@ const layoutDensityProfiles = Object.freeze({
|
|
|
373
323
|
desktop: 'clamp(2rem, 5vw, 4rem)',
|
|
374
324
|
mobile: '1.5rem',
|
|
375
325
|
}),
|
|
326
|
+
headingToBlock: Object.freeze({
|
|
327
|
+
desktop: '0.9em',
|
|
328
|
+
mobile: '0.8em',
|
|
329
|
+
}),
|
|
376
330
|
imageGap: Object.freeze({
|
|
377
331
|
desktop: 'clamp(1.5rem, 3.5vw, 2.75rem)',
|
|
378
332
|
mobile: '2rem',
|
|
@@ -383,51 +337,59 @@ const layoutDensityProfiles = Object.freeze({
|
|
|
383
337
|
}),
|
|
384
338
|
}),
|
|
385
339
|
});
|
|
386
|
-
const
|
|
387
|
-
const
|
|
340
|
+
export const resolveThemeVisualConfig = (theme, sourceLabel = siteThemeLabel) => {
|
|
341
|
+
const rawThemeConfig = assertObject(theme ?? {}, 'theme frontmatter', sourceLabel);
|
|
342
|
+
const rawLayoutConfig = assertObject(rawThemeConfig.layout ?? {}, 'layout', sourceLabel);
|
|
343
|
+
const rawLayoutSpacingConfig = assertObject(rawLayoutConfig.spacing ?? {}, 'layout.spacing', sourceLabel);
|
|
344
|
+
const rawGalleryConfig = assertObject(rawThemeConfig.gallery ?? {}, 'gallery', sourceLabel);
|
|
345
|
+
const rawTypographyConfig = assertObject(rawThemeConfig.typography ?? {}, 'typography', sourceLabel);
|
|
346
|
+
const resolvedLayoutDensity = readEnum(rawLayoutConfig, 'density', 'layout', layoutDensityNames, 'normal', sourceLabel);
|
|
347
|
+
const resolvedLayoutSpacingDefaults = layoutDensityProfiles[resolvedLayoutDensity];
|
|
348
|
+
|
|
349
|
+
return Object.freeze({
|
|
350
|
+
layout: Object.freeze({
|
|
351
|
+
density: resolvedLayoutDensity,
|
|
352
|
+
gutter: readResponsiveCssLength(rawLayoutConfig, 'gutter', 'layout', Object.freeze({
|
|
353
|
+
desktop: 'clamp(1.25rem, 4vw, 3rem)',
|
|
354
|
+
mobile: '1rem',
|
|
355
|
+
}), sourceLabel),
|
|
356
|
+
pageWidth: readCssLength(rawLayoutConfig, 'pageWidth', 'layout', '1180px', sourceLabel),
|
|
357
|
+
spacing: Object.freeze({
|
|
358
|
+
blockGap: readResponsiveCssLength(rawLayoutSpacingConfig, 'blockGap', 'layout.spacing', resolvedLayoutSpacingDefaults.blockGap, sourceLabel),
|
|
359
|
+
finalSectionBottom: readResponsiveCssLength(rawLayoutSpacingConfig, 'finalSectionBottom', 'layout.spacing', resolvedLayoutSpacingDefaults.finalSectionBottom, sourceLabel),
|
|
360
|
+
firstSectionTop: readResponsiveCssLength(rawLayoutSpacingConfig, 'firstSectionTop', 'layout.spacing', resolvedLayoutSpacingDefaults.firstSectionTop, sourceLabel),
|
|
361
|
+
headingToBlock: readResponsiveCssLength(rawLayoutSpacingConfig, 'headingToBlock', 'layout.spacing', resolvedLayoutSpacingDefaults.headingToBlock, sourceLabel),
|
|
362
|
+
imageGap: readResponsiveCssLength(rawLayoutSpacingConfig, 'imageGap', 'layout.spacing', resolvedLayoutSpacingDefaults.imageGap, sourceLabel),
|
|
363
|
+
sectionGap: readResponsiveCssLength(rawLayoutSpacingConfig, 'sectionGap', 'layout.spacing', resolvedLayoutSpacingDefaults.sectionGap, sourceLabel),
|
|
364
|
+
}),
|
|
365
|
+
}),
|
|
366
|
+
gallery: Object.freeze({
|
|
367
|
+
maxAvailableHeightPercent: readResponsivePercent(rawGalleryConfig, 'maxAvailableHeightPercent', 'gallery', Object.freeze({
|
|
368
|
+
desktop: 74,
|
|
369
|
+
mobile: 68,
|
|
370
|
+
}), sourceLabel),
|
|
371
|
+
maxAvailableWidthPercent: readResponsivePercent(rawGalleryConfig, 'maxAvailableWidthPercent', 'gallery', Object.freeze({
|
|
372
|
+
desktop: 100,
|
|
373
|
+
mobile: 100,
|
|
374
|
+
}), sourceLabel),
|
|
375
|
+
width: readCssLength(rawGalleryConfig, 'width', 'gallery', '900px', sourceLabel),
|
|
376
|
+
}),
|
|
377
|
+
typography: Object.freeze({
|
|
378
|
+
fontFamily: readFontFamily(rawTypographyConfig, 'fontFamily', 'typography', defaultFontFamily, sourceLabel),
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
};
|
|
388
382
|
|
|
389
383
|
export const projectConfig = Object.freeze({
|
|
390
384
|
site: Object.freeze({
|
|
391
385
|
basePath: readBasePath(rawSite, 'basePath', 'site'),
|
|
392
386
|
url: readUrl(rawSite, 'url', 'site'),
|
|
393
387
|
}),
|
|
394
|
-
|
|
395
|
-
density: layoutDensity,
|
|
396
|
-
gutter: readResponsiveCssLength(rawLayout, 'gutter', 'layout', Object.freeze({
|
|
397
|
-
desktop: 'clamp(1.25rem, 4vw, 3rem)',
|
|
398
|
-
mobile: '1rem',
|
|
399
|
-
}), siteThemeLabel),
|
|
400
|
-
pageWidth: readCssLength(rawLayout, 'pageWidth', 'layout', '1180px', siteThemeLabel),
|
|
401
|
-
spacing: Object.freeze({
|
|
402
|
-
bodyToImages: readResponsiveCssLength(rawLayoutSpacing, 'bodyToImages', 'layout.spacing', layoutSpacingDefaults.bodyToImages, siteThemeLabel),
|
|
403
|
-
finalSectionBottom: readResponsiveCssLength(rawLayoutSpacing, 'finalSectionBottom', 'layout.spacing', layoutSpacingDefaults.finalSectionBottom, siteThemeLabel),
|
|
404
|
-
firstSectionTop: readResponsiveCssLength(rawLayoutSpacing, 'firstSectionTop', 'layout.spacing', layoutSpacingDefaults.firstSectionTop, siteThemeLabel),
|
|
405
|
-
imageGap: readResponsiveCssLength(rawLayoutSpacing, 'imageGap', 'layout.spacing', layoutSpacingDefaults.imageGap, siteThemeLabel),
|
|
406
|
-
sectionGap: readResponsiveCssLength(rawLayoutSpacing, 'sectionGap', 'layout.spacing', layoutSpacingDefaults.sectionGap, siteThemeLabel),
|
|
407
|
-
}),
|
|
408
|
-
}),
|
|
409
|
-
gallery: Object.freeze({
|
|
410
|
-
maxAvailableHeightPercent: readResponsivePercent(rawGallery, 'maxAvailableHeightPercent', 'gallery', Object.freeze({
|
|
411
|
-
desktop: 74,
|
|
412
|
-
mobile: 68,
|
|
413
|
-
}), siteThemeLabel),
|
|
414
|
-
maxAvailableWidthPercent: readResponsivePercent(rawGallery, 'maxAvailableWidthPercent', 'gallery', Object.freeze({
|
|
415
|
-
desktop: 100,
|
|
416
|
-
mobile: 100,
|
|
417
|
-
}), siteThemeLabel),
|
|
418
|
-
width: readCssLength(rawGallery, 'width', 'gallery', '900px', siteThemeLabel),
|
|
419
|
-
}),
|
|
420
|
-
typography: Object.freeze({
|
|
421
|
-
fontFamily: readFontFamily(rawTypography, 'fontFamily', 'typography', defaultFontFamily, siteThemeLabel),
|
|
422
|
-
}),
|
|
388
|
+
...resolveThemeVisualConfig(rawTheme, siteThemeLabel),
|
|
423
389
|
navigation: Object.freeze({
|
|
424
390
|
smoothScroll: readSmoothScroll(rawNavigation),
|
|
425
391
|
}),
|
|
426
392
|
locale: readLocale(rawLocale),
|
|
427
|
-
footer: Object.freeze({
|
|
428
|
-
buildInfo: readBuildInfo(rawFooter),
|
|
429
|
-
copyrightMessage: readOptionalString(rawFooter, 'copyrightMessage', 'footer'),
|
|
430
|
-
}),
|
|
431
393
|
github: Object.freeze({
|
|
432
394
|
repo: readString(rawGithub, 'repo', 'github'),
|
|
433
395
|
branch: readString(rawGithub, 'branch', 'github'),
|
|
@@ -18,24 +18,20 @@ export const supportedImageExtensions = new Set([...rasterImageExtensions, ...st
|
|
|
18
18
|
|
|
19
19
|
const h2Regex = /^##\s+.*$/gm;
|
|
20
20
|
const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
|
|
21
|
-
const
|
|
21
|
+
const deprecatedInlineStyleReferenceRegex = /\[[^\]\n]+\]\{\.([a-z][a-z0-9-]*)\}/g;
|
|
22
22
|
const frontmatterDelimiterRegex = /^---\s*$/;
|
|
23
|
-
const knownContentTopLevelFrontmatterKeys = new Set(['title', 'description', 'navigation', '
|
|
24
|
-
const knownThemeTopLevelFrontmatterKeys = new Set(['navigation', 'layout', 'gallery', 'typography', 'presentation'
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const unquotedHexColorRegex = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
23
|
+
const knownContentTopLevelFrontmatterKeys = new Set(['title', 'description', 'navigation', 'sections']);
|
|
24
|
+
const knownThemeTopLevelFrontmatterKeys = new Set(['navigation', 'layout', 'gallery', 'typography', 'presentation']);
|
|
25
|
+
const knownRouteThemeTopLevelFrontmatterKeys = new Set(['navigation', 'layout', 'gallery', 'typography', 'presentation']);
|
|
26
|
+
const knownSitewideTopLevelFrontmatterKeys = new Set(['navigation', 'banners', 'footer']);
|
|
28
27
|
const knownNestedFrontmatterKeys = new Set([
|
|
29
28
|
'align',
|
|
30
29
|
'alt',
|
|
31
|
-
'backgroundColor',
|
|
32
30
|
'body',
|
|
33
|
-
'
|
|
31
|
+
'blockGap',
|
|
34
32
|
'brand',
|
|
35
33
|
'caption',
|
|
36
34
|
'carousel',
|
|
37
|
-
'color',
|
|
38
|
-
'colors',
|
|
39
35
|
'density',
|
|
40
36
|
'desktop',
|
|
41
37
|
'fontFamily',
|
|
@@ -43,6 +39,7 @@ const knownNestedFrontmatterKeys = new Set([
|
|
|
43
39
|
'finalSectionBottom',
|
|
44
40
|
'gallery',
|
|
45
41
|
'gutter',
|
|
42
|
+
'headingToBlock',
|
|
46
43
|
'h1',
|
|
47
44
|
'h2',
|
|
48
45
|
'h3',
|
|
@@ -53,8 +50,8 @@ const knownNestedFrontmatterKeys = new Set([
|
|
|
53
50
|
'include',
|
|
54
51
|
'image',
|
|
55
52
|
'imageGap',
|
|
56
|
-
'inlineStyles',
|
|
57
53
|
'label',
|
|
54
|
+
'logo',
|
|
58
55
|
'lineHeight',
|
|
59
56
|
'maxAvailableHeightPercent',
|
|
60
57
|
'maxAvailableWidthPercent',
|
|
@@ -63,15 +60,18 @@ const knownNestedFrontmatterKeys = new Set([
|
|
|
63
60
|
'overrides',
|
|
64
61
|
'paragraphSpacing',
|
|
65
62
|
'pageWidth',
|
|
63
|
+
'palette',
|
|
66
64
|
'preset',
|
|
67
65
|
'presentation',
|
|
68
66
|
'rhythm',
|
|
67
|
+
'sequence',
|
|
69
68
|
'sections',
|
|
70
69
|
'sectionGap',
|
|
70
|
+
'sectionSurfaces',
|
|
71
71
|
'size',
|
|
72
72
|
'spacingAfter',
|
|
73
73
|
'spacingBefore',
|
|
74
|
-
'
|
|
74
|
+
'surface',
|
|
75
75
|
'theme',
|
|
76
76
|
'typography',
|
|
77
77
|
'until',
|
|
@@ -264,79 +264,6 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
|
|
|
264
264
|
}
|
|
265
265
|
};
|
|
266
266
|
|
|
267
|
-
const stripInlineYamlComment = (value) => {
|
|
268
|
-
let quote = null;
|
|
269
|
-
|
|
270
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
271
|
-
const character = value[index];
|
|
272
|
-
const previous = value[index - 1];
|
|
273
|
-
|
|
274
|
-
if ((character === '"' || character === "'") && previous !== '\\') {
|
|
275
|
-
quote = quote === character ? null : quote ?? character;
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (character === '#' && quote === null && /\s/.test(previous ?? '')) {
|
|
280
|
-
return value.slice(0, index).trimEnd();
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return value.trim();
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
export const validateFrontmatterColorValues = (frontmatter, addIssue, label = 'Frontmatter') => {
|
|
288
|
-
const lines = frontmatter.split(/\r?\n/);
|
|
289
|
-
const pathStack = [];
|
|
290
|
-
|
|
291
|
-
for (const [index, line] of lines.entries()) {
|
|
292
|
-
const lineNumber = index + 1;
|
|
293
|
-
const trimmed = line.trim();
|
|
294
|
-
|
|
295
|
-
if (!trimmed || trimmed.startsWith('#') || frontmatterDelimiterRegex.test(line)) {
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const match = line.match(/^(\s*)([A-Za-z][A-Za-z0-9-]*):\s*(.*?)\s*$/);
|
|
300
|
-
if (!match) continue;
|
|
301
|
-
|
|
302
|
-
const indent = match[1].length;
|
|
303
|
-
const key = match[2];
|
|
304
|
-
const rawValue = match[3].trim();
|
|
305
|
-
|
|
306
|
-
while (pathStack.length > 0 && pathStack[pathStack.length - 1].indent >= indent) {
|
|
307
|
-
pathStack.pop();
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const pathLabel = [...pathStack.map((entry) => entry.key), key].join('.');
|
|
311
|
-
|
|
312
|
-
if (!frontmatterColorKeys.has(key)) {
|
|
313
|
-
if (!rawValue) {
|
|
314
|
-
pathStack.push({ indent, key });
|
|
315
|
-
}
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const value = stripInlineYamlComment(rawValue);
|
|
320
|
-
|
|
321
|
-
if (quotedHexColorRegex.test(value)) continue;
|
|
322
|
-
|
|
323
|
-
if (unquotedHexColorRegex.test(rawValue)) {
|
|
324
|
-
addIssue({
|
|
325
|
-
severity: 'error',
|
|
326
|
-
message: `${label} line ${lineNumber} defines ${pathLabel} as an unquoted hex color. YAML treats "#" as the start of a comment unless the value is quoted.`,
|
|
327
|
-
fix: `Write it as ${key}: "#ffd84d" or another quoted hex color.`,
|
|
328
|
-
});
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
addIssue({
|
|
333
|
-
severity: 'error',
|
|
334
|
-
message: `${label} line ${lineNumber} defines ${pathLabel} with invalid color value ${rawValue || '(empty)'}.`,
|
|
335
|
-
fix: `Use a quoted hex color in #rgb, #rrggbb, or #rrggbbaa form, for example ${key}: "#ffd84d".`,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
|
|
340
267
|
export const validateContentFrontmatterStructure = (frontmatter, addIssue) =>
|
|
341
268
|
validateFrontmatterStructure(frontmatter, addIssue, {
|
|
342
269
|
knownTopLevelFrontmatterKeys: knownContentTopLevelFrontmatterKeys,
|
|
@@ -349,6 +276,18 @@ export const validateThemeFrontmatterStructure = (frontmatter, addIssue) =>
|
|
|
349
276
|
fileKind: 'theme',
|
|
350
277
|
});
|
|
351
278
|
|
|
279
|
+
export const validateRouteThemeFrontmatterStructure = (frontmatter, addIssue) =>
|
|
280
|
+
validateFrontmatterStructure(frontmatter, addIssue, {
|
|
281
|
+
knownTopLevelFrontmatterKeys: knownRouteThemeTopLevelFrontmatterKeys,
|
|
282
|
+
fileKind: 'route theme',
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
export const validateSitewideFrontmatterStructure = (frontmatter, addIssue) =>
|
|
286
|
+
validateFrontmatterStructure(frontmatter, addIssue, {
|
|
287
|
+
knownTopLevelFrontmatterKeys: knownSitewideTopLevelFrontmatterKeys,
|
|
288
|
+
fileKind: 'sitewide content',
|
|
289
|
+
});
|
|
290
|
+
|
|
352
291
|
export const readThemeFile = async (sitePath) => readSiteFile(sitePath, siteThemeLabel);
|
|
353
292
|
|
|
354
293
|
export const getFrontmatterSections = (frontmatter) => {
|
|
@@ -374,42 +313,11 @@ export const getFrontmatterSections = (frontmatter) => {
|
|
|
374
313
|
return sections;
|
|
375
314
|
};
|
|
376
315
|
|
|
377
|
-
export const
|
|
378
|
-
const names = new Set();
|
|
379
|
-
const lines = frontmatter.split(/\r?\n/);
|
|
380
|
-
let inlineStylesIndent = null;
|
|
381
|
-
|
|
382
|
-
for (const line of lines) {
|
|
383
|
-
const inlineStylesMatch = line.match(/^(\s*)inlineStyles:\s*$/);
|
|
384
|
-
|
|
385
|
-
if (inlineStylesMatch) {
|
|
386
|
-
inlineStylesIndent = inlineStylesMatch[1].length;
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (inlineStylesIndent === null) continue;
|
|
391
|
-
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
392
|
-
|
|
393
|
-
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
394
|
-
if (indent <= inlineStylesIndent) {
|
|
395
|
-
inlineStylesIndent = null;
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const nameMatch = line.match(new RegExp(`^\\s{${inlineStylesIndent + 2}}([a-z][a-z0-9-]*):\\s*$`));
|
|
400
|
-
if (nameMatch) {
|
|
401
|
-
names.add(nameMatch[1]);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return names;
|
|
406
|
-
};
|
|
316
|
+
export const getHeadingId = (heading) => heading.match(explicitHeadingIdRegex)?.[1];
|
|
407
317
|
|
|
408
|
-
export const
|
|
318
|
+
export const getDeprecatedInlineStyleReferences = (body) => Array.from(body.matchAll(deprecatedInlineStyleReferenceRegex))
|
|
409
319
|
.map((match) => match[1]);
|
|
410
320
|
|
|
411
|
-
export const getHeadingId = (heading) => heading.match(explicitHeadingIdRegex)?.[1];
|
|
412
|
-
|
|
413
321
|
export const getBodySections = (body) => {
|
|
414
322
|
const matches = Array.from(body.matchAll(h2Regex));
|
|
415
323
|
const prelude = matches.length > 0 ? body.slice(0, matches[0].index) : body;
|
|
@@ -117,6 +117,7 @@ export const siteDir = resolvedSitePaths.siteDir;
|
|
|
117
117
|
export const siteConfigPath = path.join(siteDir, 'config.mjs');
|
|
118
118
|
export const siteThemePath = path.join(siteDir, 'theme.md');
|
|
119
119
|
export const siteContentPath = path.join(siteDir, 'content.md');
|
|
120
|
+
export const sitewideContentPath = path.join(siteDir, 'sitewide-content.md');
|
|
120
121
|
export const siteImagesDir = path.join(siteDir, 'images');
|
|
121
122
|
export const siteRoutesDir = path.join(siteDir, 'routes');
|
|
122
123
|
export const sitePublicDir = path.join(siteDir, 'public');
|
|
@@ -135,6 +136,7 @@ export const siteDirLabel = getPathLabel(siteDir);
|
|
|
135
136
|
export const siteConfigLabel = getPathLabel(siteConfigPath);
|
|
136
137
|
export const siteThemeLabel = getPathLabel(siteThemePath);
|
|
137
138
|
export const siteContentLabel = getPathLabel(siteContentPath);
|
|
139
|
+
export const sitewideContentLabel = getPathLabel(sitewideContentPath);
|
|
138
140
|
export const siteImagesLabel = getPathLabel(siteImagesDir);
|
|
139
141
|
export const siteRoutesLabel = getPathLabel(siteRoutesDir);
|
|
140
142
|
export const sitePublicLabel = getPathLabel(sitePublicDir);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { parseYamlMapping } from './frontmatter-yaml.mjs';
|
|
3
|
+
import {
|
|
4
|
+
sitewideContentLabel,
|
|
5
|
+
sitewideContentPath,
|
|
6
|
+
} from './site-paths.mjs';
|
|
7
|
+
import {
|
|
8
|
+
splitSiteFile,
|
|
9
|
+
validateFrontmatterIndentation,
|
|
10
|
+
validateSitewideFrontmatterStructure,
|
|
11
|
+
} from './site-content.mjs';
|
|
12
|
+
|
|
13
|
+
export const readSitewideConfig = async () => {
|
|
14
|
+
const source = await readFile(sitewideContentPath, 'utf8').catch((error) => {
|
|
15
|
+
if (error?.code === 'ENOENT') return null;
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!source) return {};
|
|
20
|
+
|
|
21
|
+
const { frontmatter, frontmatterBody } = splitSiteFile(source, sitewideContentLabel);
|
|
22
|
+
const issues = [];
|
|
23
|
+
validateFrontmatterIndentation(frontmatter, (issue) => issues.push(issue));
|
|
24
|
+
validateSitewideFrontmatterStructure(frontmatter, (issue) => issues.push(issue));
|
|
25
|
+
|
|
26
|
+
if (issues.length > 0) {
|
|
27
|
+
throw new Error([
|
|
28
|
+
`${sitewideContentLabel} has invalid frontmatter.`,
|
|
29
|
+
...issues.map((issue) => `- ${issue.message}`),
|
|
30
|
+
].join('\n'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return parseYamlMapping(frontmatterBody);
|
|
34
|
+
};
|
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { parseYamlMapping } from './frontmatter-yaml.mjs';
|
|
3
4
|
import {
|
|
4
5
|
splitSiteFile,
|
|
5
|
-
validateFrontmatterColorValues,
|
|
6
6
|
validateFrontmatterIndentation,
|
|
7
|
+
validateRouteThemeFrontmatterStructure,
|
|
7
8
|
validateThemeFrontmatterStructure,
|
|
8
9
|
} from './site-content.mjs';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
siteRoutesDir,
|
|
12
|
+
siteRoutesLabel,
|
|
13
|
+
siteThemeLabel,
|
|
14
|
+
siteThemePath,
|
|
15
|
+
sitewideContentLabel,
|
|
16
|
+
} from './site-paths.mjs';
|
|
12
17
|
|
|
13
18
|
export const readThemeConfig = async () => {
|
|
14
19
|
const themeFile = await readFile(siteThemePath, 'utf8').catch((error) => {
|
|
15
20
|
if (error?.code === 'ENOENT') {
|
|
16
|
-
|
|
21
|
+
throw new Error(`${siteThemeLabel} is required. Create it before running Norna.`);
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
throw error;
|
|
@@ -23,7 +28,6 @@ export const readThemeConfig = async () => {
|
|
|
23
28
|
|
|
24
29
|
validateFrontmatterIndentation(frontmatter, (issue) => issues.push(issue));
|
|
25
30
|
validateThemeFrontmatterStructure(frontmatter, (issue) => issues.push(issue));
|
|
26
|
-
validateFrontmatterColorValues(frontmatter, (issue) => issues.push(issue), siteThemeLabel);
|
|
27
31
|
|
|
28
32
|
if (issues.length > 0) {
|
|
29
33
|
throw new Error([
|
|
@@ -32,5 +36,65 @@ export const readThemeConfig = async () => {
|
|
|
32
36
|
].join('\n'));
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
const config = parseYamlMapping(frontmatterBody);
|
|
40
|
+
if (Object.hasOwn(config, 'navigation')) {
|
|
41
|
+
throw new Error(`${siteThemeLabel} may not define navigation. Brand and logo belong in ${sitewideContentLabel}.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return config;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const getRouteThemeFiles = async (directory, relativeDirectory = '') => {
|
|
48
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
49
|
+
if (error?.code === 'ENOENT') return [];
|
|
50
|
+
throw error;
|
|
51
|
+
});
|
|
52
|
+
const files = [];
|
|
53
|
+
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const relativePath = path.join(relativeDirectory, entry.name);
|
|
56
|
+
const absolutePath = path.join(directory, entry.name);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
files.push(...await getRouteThemeFiles(absolutePath, relativePath));
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (entry.name === 'theme.md') {
|
|
63
|
+
files.push({
|
|
64
|
+
path: absolutePath,
|
|
65
|
+
label: `${siteRoutesLabel}/${relativePath.split(path.sep).join('/')}`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return files;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const validateRouteThemeFiles = async () => {
|
|
74
|
+
const files = await getRouteThemeFiles(siteRoutesDir);
|
|
75
|
+
const configs = [];
|
|
76
|
+
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const source = await readFile(file.path, 'utf8');
|
|
79
|
+
const { frontmatter, frontmatterBody } = splitSiteFile(source, file.label);
|
|
80
|
+
const issues = [];
|
|
81
|
+
validateFrontmatterIndentation(frontmatter, (issue) => issues.push(issue));
|
|
82
|
+
validateRouteThemeFrontmatterStructure(frontmatter, (issue) => issues.push(issue));
|
|
83
|
+
|
|
84
|
+
if (issues.length > 0) {
|
|
85
|
+
throw new Error([
|
|
86
|
+
`${file.label} has invalid frontmatter.`,
|
|
87
|
+
...issues.map((issue) => `- ${issue.message}`),
|
|
88
|
+
].join('\n'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const config = parseYamlMapping(frontmatterBody);
|
|
92
|
+
if (Object.hasOwn(config, 'navigation')) {
|
|
93
|
+
throw new Error(`${file.label} may not define navigation. Brand and logo belong in ${sitewideContentLabel}.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
configs.push({ ...file, config });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return configs;
|
|
36
100
|
};
|