@janga/norna 0.7.2 → 0.7.3
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 +16 -16
- package/bin/norna-cli.mjs +12 -7
- package/docs/README.md +10 -11
- package/docs/commands.md +8 -8
- package/docs/configuration.md +13 -145
- package/docs/content.md +17 -15
- package/docs/design/command-organization.md +55 -55
- package/docs/design/site-examples-structure.md +2 -2
- package/docs/engine-development.md +5 -5
- package/docs/getting-started.md +19 -26
- package/docs/images-and-metadata.md +1 -1
- package/docs/local-development.md +2 -2
- package/docs/publishing.md +1 -1
- package/docs/routes.md +2 -2
- package/docs/site-structure.md +8 -7
- package/docs/theme.md +149 -7
- package/docs/typography.md +47 -28
- package/examples/dog-gallery/.astro/collections/site.schema.json +802 -0
- package/examples/dog-gallery/.astro/collections/theme.schema.json +714 -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 +49 -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.BFHZm5UG.css +1 -0
- package/examples/dog-gallery/dist/dog-care/index.html +15 -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 +43 -0
- package/examples/dog-gallery/dist/robots.txt +2 -0
- package/examples/dog-gallery/node_modules/.vite/deps/_metadata.json +74 -0
- package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
- package/examples/dog-gallery/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/examples/dog-gallery/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
- package/examples/dog-gallery/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/examples/dog-gallery/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/examples/dog-gallery/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
- package/examples/dog-gallery/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
- package/examples/dog-gallery/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
- package/examples/dog-gallery/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/package.json +3 -0
- package/examples/dog-gallery/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
- package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
- package/examples/dog-gallery/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
- package/examples/dog-gallery/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
- package/examples/dog-gallery/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
- package/examples/dog-gallery/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
- package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
- package/examples/dog-gallery/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
- package/examples/dog-gallery/site/config.mjs +12 -42
- package/examples/dog-gallery/site/content.md +27 -5
- package/examples/dog-gallery/site/routes/dog-care/route-content.md +17 -3
- package/examples/dog-gallery/site/theme.md +85 -26
- package/fixtures/basic/site/config.mjs +2 -22
- package/fixtures/basic/site/content.md +1 -1
- package/fixtures/basic/site/theme.md +7 -3
- package/package.json +3 -3
- package/scripts/check-config.mjs +3 -3
- package/scripts/deploy-site.mjs +3 -3
- package/scripts/init-site.mjs +10 -8
- package/scripts/lib/frontmatter-yaml.mjs +85 -0
- package/scripts/lib/presentation.mjs +10 -11
- package/scripts/lib/project-config.mjs +91 -45
- package/scripts/lib/site-content.mjs +20 -2
- package/scripts/lib/theme-config.mjs +34 -0
- package/scripts/lib/typography.mjs +100 -20
- package/scripts/show-typography.mjs +58 -154
- package/scripts/sync-content-sections.mjs +6 -6
- package/scripts/test-content-check.mjs +3 -4
- package/scripts/test-engine-commands.mjs +2 -2
- package/scripts/test-package-check.mjs +17 -16
- package/scripts/test-temporary-visibility.mjs +2 -3
- package/src/components/SitePage.astro +2 -3
- package/src/components/SiteSection.astro +39 -5
- package/src/content.config.ts +60 -2
- package/src/layouts/BaseLayout.astro +17 -0
- package/src/styles/global.css +54 -50
- package/starters/basic/README.md +10 -15
- package/starters/basic/package.json +2 -2
- package/starters/basic/site/config.mjs +3 -24
- package/starters/basic/site/content.md +40 -7
- package/starters/basic/site/theme.md +110 -17
- package/starters/project/.github/workflows/deploy.yml +65 -0
- package/starters/project/README.md +72 -0
- package/starters/project/package-lock.json +4296 -0
- package/starters/project/package.json +35 -0
- package/starters/project/site/config.mjs +43 -0
- package/starters/project/site/content.md +88 -0
- package/starters/project/site/public/robots.txt +2 -0
- package/starters/project/site/routes/guide/route-content.md +58 -0
- package/starters/project/site/theme.md +15 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
layout:
|
|
3
|
+
pageWidth: 1180px
|
|
4
|
+
gallery:
|
|
5
|
+
width: 900px
|
|
6
|
+
typography:
|
|
7
|
+
fontFamily: "Arial, 'Helvetica Neue', Helvetica, sans-serif"
|
|
8
|
+
preset: quiet-gallery
|
|
5
9
|
frame:
|
|
6
10
|
colors: presentation
|
|
7
11
|
---
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@janga/norna",
|
|
3
3
|
"description": "A command-line publishing workflow for simple static sites.",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.3",
|
|
6
6
|
"license": "GPL-3.0-only",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"config:check": "node bin/norna.mjs config:check",
|
|
52
52
|
"content:check": "node bin/norna.mjs content:check",
|
|
53
53
|
"content:sync": "node bin/norna.mjs content:sync",
|
|
54
|
-
"typography:presets": "node bin/norna.mjs typography
|
|
55
|
-
"typography:show": "node bin/norna.mjs typography
|
|
54
|
+
"typography:presets": "node bin/norna.mjs typography presets",
|
|
55
|
+
"typography:show": "node bin/norna.mjs typography show",
|
|
56
56
|
"site:public": "node bin/norna.mjs site:public",
|
|
57
57
|
"images": "node bin/norna.mjs images",
|
|
58
58
|
"build": "node bin/norna.mjs build",
|
package/scripts/check-config.mjs
CHANGED
|
@@ -23,9 +23,9 @@ try {
|
|
|
23
23
|
console.log(`Base path: ${projectConfig.site.basePath}`);
|
|
24
24
|
console.log(`Page width: ${projectConfig.layout.pageWidth}`);
|
|
25
25
|
console.log(`Gutter: desktop ${projectConfig.layout.gutter.desktop}, mobile ${projectConfig.layout.gutter.mobile}`);
|
|
26
|
-
console.log(`
|
|
27
|
-
console.log(`
|
|
28
|
-
console.log(`
|
|
26
|
+
console.log(`Image area width: ${projectConfig.gallery.width}`);
|
|
27
|
+
console.log(`Image max available width: desktop ${projectConfig.gallery.maxAvailableWidthPercent.desktop}%, mobile ${projectConfig.gallery.maxAvailableWidthPercent.mobile}%`);
|
|
28
|
+
console.log(`Image max available height: desktop ${projectConfig.gallery.maxAvailableHeightPercent.desktop}%, mobile ${projectConfig.gallery.maxAvailableHeightPercent.mobile}%`);
|
|
29
29
|
console.log(`Font family: ${projectConfig.typography.fontFamily}`);
|
|
30
30
|
console.log(`Language: ${projectConfig.locale.lang}`);
|
|
31
31
|
console.log(`GitHub repo: ${projectConfig.github.repo}`);
|
package/scripts/deploy-site.mjs
CHANGED
|
@@ -196,7 +196,7 @@ const assertMainBranch = async () => {
|
|
|
196
196
|
|
|
197
197
|
const assertDeployableStatus = async (entries, expectedImagePaths) => {
|
|
198
198
|
if (entries.length === 0) {
|
|
199
|
-
fail('Refusing to deploy: no changes to commit after the
|
|
199
|
+
fail('Refusing to deploy: no changes to commit after the Norna build.');
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
const unexpectedUntracked = entries.filter((entry) => (
|
|
@@ -206,7 +206,7 @@ const assertDeployableStatus = async (entries, expectedImagePaths) => {
|
|
|
206
206
|
if (unexpectedUntracked.length > 0) {
|
|
207
207
|
fail([
|
|
208
208
|
'Refusing to deploy: unexpected untracked files are present.',
|
|
209
|
-
`Only new referenced
|
|
209
|
+
`Only new referenced images under ${siteImagesLabel}/<section-id>/ or ${siteRoutesLabel}/<route-folder>/images/<section-id>/ are staged automatically.`,
|
|
210
210
|
...unexpectedUntracked.map((entry) => `- ${formatEntry(entry)}`),
|
|
211
211
|
].join('\n'));
|
|
212
212
|
}
|
|
@@ -331,7 +331,7 @@ const deployCommittedMain = async () => {
|
|
|
331
331
|
await assertPushableBranch();
|
|
332
332
|
await runBuild();
|
|
333
333
|
await printStatusShort();
|
|
334
|
-
await assertCleanWorktree('Refusing to deploy: the
|
|
334
|
+
await assertCleanWorktree('Refusing to deploy: the Norna build produced uncommitted changes. Commit them before deploying.');
|
|
335
335
|
await fetchRemoteMain();
|
|
336
336
|
|
|
337
337
|
const relation = await assertPushableBranch();
|
package/scripts/init-site.mjs
CHANGED
|
@@ -13,12 +13,12 @@ const cliExecutableName = 'norna';
|
|
|
13
13
|
const usage = `
|
|
14
14
|
Usage: norna init <target-dir> [--type pure|embedded] [--site-dir <path>]
|
|
15
15
|
|
|
16
|
-
Creates a new site project or adds a
|
|
16
|
+
Creates a new site project or adds a Norna site directory to an existing
|
|
17
17
|
project.
|
|
18
18
|
|
|
19
19
|
Examples:
|
|
20
|
-
norna init my-
|
|
21
|
-
norna init my-
|
|
20
|
+
norna init my-site
|
|
21
|
+
norna init my-site --type pure
|
|
22
22
|
norna init . --type embedded --site-dir presentation
|
|
23
23
|
`.trim();
|
|
24
24
|
|
|
@@ -133,8 +133,8 @@ const galleryScripts = {
|
|
|
133
133
|
'norna:config:check': cliCommand('config:check'),
|
|
134
134
|
'norna:content:check': cliCommand('content:check'),
|
|
135
135
|
'norna:sync': cliCommand('content:sync'),
|
|
136
|
-
'norna:typography:presets': cliCommand('typography
|
|
137
|
-
'norna:typography:show': cliCommand('typography
|
|
136
|
+
'norna:typography:presets': cliCommand('typography presets'),
|
|
137
|
+
'norna:typography:show': cliCommand('typography show'),
|
|
138
138
|
'norna:public': cliCommand('site:public'),
|
|
139
139
|
'norna:images': cliCommand('images'),
|
|
140
140
|
'norna:build': cliCommand('build'),
|
|
@@ -180,7 +180,7 @@ const existingEntries = await readDirectoryEntries(targetRoot);
|
|
|
180
180
|
if (type === 'pure') {
|
|
181
181
|
if (existingEntries && existingEntries.length > 0) {
|
|
182
182
|
const existingPackageJson = existingEntries.includes('package.json')
|
|
183
|
-
? ' The target already contains package.json; use --type embedded to add a
|
|
183
|
+
? ' The target already contains package.json; use --type embedded to add a Norna site to an existing project.'
|
|
184
184
|
: '';
|
|
185
185
|
throw new Error(`Target directory must be empty for --type pure: ${targetRoot}.${existingPackageJson}`);
|
|
186
186
|
}
|
|
@@ -218,7 +218,7 @@ if (type === 'pure') {
|
|
|
218
218
|
const existingSiteEntries = await readDirectoryEntries(targetSiteDir);
|
|
219
219
|
|
|
220
220
|
if (existingSiteEntries && existingSiteEntries.length > 0) {
|
|
221
|
-
throw new Error(`
|
|
221
|
+
throw new Error(`Norna site directory must be empty for --type embedded: ${targetSiteDir}`);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
addGalleryDependency(targetPackageJson, enginePackageJson.version);
|
|
@@ -246,4 +246,6 @@ console.log(` cd ${targetRoot}`);
|
|
|
246
246
|
console.log(type === 'embedded'
|
|
247
247
|
? ' npm install'
|
|
248
248
|
: ' npm install');
|
|
249
|
-
console.log(
|
|
249
|
+
console.log(type === 'embedded'
|
|
250
|
+
? ' npm run norna:dev'
|
|
251
|
+
: ' npm run dev');
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const countIndent = (line) => line.match(/^\s*/)?.[0].length ?? 0;
|
|
2
|
+
|
|
3
|
+
const stripInlineComment = (value) => {
|
|
4
|
+
let quote = null;
|
|
5
|
+
|
|
6
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
7
|
+
const character = value[index];
|
|
8
|
+
const previous = value[index - 1];
|
|
9
|
+
|
|
10
|
+
if ((character === '"' || character === "'") && previous !== '\\') {
|
|
11
|
+
quote = quote === character ? null : quote ?? character;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (character === '#' && quote === null && /\s/.test(previous ?? '')) {
|
|
16
|
+
return value.slice(0, index).trimEnd();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return value;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const parseScalar = (rawValue) => {
|
|
24
|
+
const value = stripInlineComment(rawValue.trim());
|
|
25
|
+
|
|
26
|
+
if (value === 'true') return true;
|
|
27
|
+
if (value === 'false') return false;
|
|
28
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
|
|
29
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
30
|
+
return value.slice(1, -1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const parseMapping = (lines, startIndex = 0, baseIndent = -1) => {
|
|
37
|
+
const value = {};
|
|
38
|
+
let index = startIndex;
|
|
39
|
+
|
|
40
|
+
while (index < lines.length) {
|
|
41
|
+
const line = lines[index];
|
|
42
|
+
if (!line.trim() || line.trim().startsWith('#')) {
|
|
43
|
+
index += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const indent = countIndent(line);
|
|
48
|
+
if (indent <= baseIndent) break;
|
|
49
|
+
if (line.trim().startsWith('- ')) break;
|
|
50
|
+
|
|
51
|
+
const match = line.trim().match(/^([a-zA-Z][a-zA-Z0-9-]*):(?:\s+(.*))?$/);
|
|
52
|
+
if (!match) break;
|
|
53
|
+
|
|
54
|
+
const [, key, rawValue] = match;
|
|
55
|
+
if (rawValue === undefined) {
|
|
56
|
+
const parsed = parseMapping(lines, index + 1, indent);
|
|
57
|
+
value[key] = parsed.value;
|
|
58
|
+
index = parsed.nextIndex;
|
|
59
|
+
} else {
|
|
60
|
+
value[key] = parseScalar(rawValue);
|
|
61
|
+
index += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { value, nextIndex: index };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const parseYamlMapping = (source) => parseMapping(source.split(/\r?\n/)).value;
|
|
69
|
+
|
|
70
|
+
export const findMap = (lines, label, parentStart = 0, parentEnd = lines.length, requiredIndent = null) => {
|
|
71
|
+
for (let index = parentStart; index < parentEnd; index += 1) {
|
|
72
|
+
const line = lines[index];
|
|
73
|
+
const match = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9-]*):\s*$/);
|
|
74
|
+
if (!match || match[2] !== label) continue;
|
|
75
|
+
if (requiredIndent !== null && match[1].length !== requiredIndent) continue;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
index,
|
|
79
|
+
indent: match[1].length,
|
|
80
|
+
...parseMapping(lines, index + 1, match[1].length),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
@@ -11,19 +11,20 @@ export const fallbackPresentationColors = Object.freeze({
|
|
|
11
11
|
|
|
12
12
|
const normalizePresentation = (presentation) => presentation ?? {};
|
|
13
13
|
|
|
14
|
-
export const resolveThemePresentation = (
|
|
15
|
-
const
|
|
14
|
+
export const resolveThemePresentation = (theme) => {
|
|
15
|
+
const normalizedTheme = theme ?? {};
|
|
16
|
+
const normalizedThemePresentation = normalizePresentation(normalizedTheme.presentation);
|
|
16
17
|
|
|
17
18
|
return {
|
|
18
19
|
backgroundColor: normalizedThemePresentation.backgroundColor ?? fallbackPresentationColors.backgroundColor,
|
|
19
20
|
textColor: normalizedThemePresentation.textColor ?? fallbackPresentationColors.textColor,
|
|
20
21
|
inlineStyles: normalizedThemePresentation.inlineStyles ?? {},
|
|
21
|
-
typography: resolveTypographyConfig(
|
|
22
|
+
typography: resolveTypographyConfig(normalizedTheme.typography ?? defaultTypography),
|
|
22
23
|
};
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
export const resolvePagePresentation = (
|
|
26
|
-
const resolvedThemePresentation = resolveThemePresentation(
|
|
26
|
+
export const resolvePagePresentation = (theme, pagePresentation) => {
|
|
27
|
+
const resolvedThemePresentation = resolveThemePresentation(theme);
|
|
27
28
|
const normalizedPagePresentation = normalizePresentation(pagePresentation);
|
|
28
29
|
|
|
29
30
|
return {
|
|
@@ -42,7 +43,7 @@ const getColorsFromPresentation = (presentation) => ({
|
|
|
42
43
|
textColor: presentation.textColor,
|
|
43
44
|
});
|
|
44
45
|
|
|
45
|
-
const resolveColorSource = ({ colors,
|
|
46
|
+
const resolveColorSource = ({ colors, themeFrameColors, pagePresentation }) => {
|
|
46
47
|
if (!colors || colors === 'theme') {
|
|
47
48
|
return themeFrameColors;
|
|
48
49
|
}
|
|
@@ -54,18 +55,16 @@ const resolveColorSource = ({ colors, themePresentation, themeFrameColors, pageP
|
|
|
54
55
|
return colors;
|
|
55
56
|
};
|
|
56
57
|
|
|
57
|
-
export const resolveFrameColors = ({
|
|
58
|
-
const resolvedThemePresentation = resolveThemePresentation(
|
|
58
|
+
export const resolveFrameColors = ({ theme, pagePresentation, pageFrame }) => {
|
|
59
|
+
const resolvedThemePresentation = resolveThemePresentation(theme);
|
|
59
60
|
const themeFrameColors = resolveColorSource({
|
|
60
|
-
colors:
|
|
61
|
-
themePresentation: resolvedThemePresentation,
|
|
61
|
+
colors: theme?.frame?.colors ?? 'presentation',
|
|
62
62
|
themeFrameColors: getColorsFromPresentation(resolvedThemePresentation),
|
|
63
63
|
pagePresentation: resolvedThemePresentation,
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
return resolveColorSource({
|
|
67
67
|
colors: pageFrame?.colors ?? 'theme',
|
|
68
|
-
themePresentation: resolvedThemePresentation,
|
|
69
68
|
themeFrameColors,
|
|
70
69
|
pagePresentation,
|
|
71
70
|
});
|
|
@@ -1,31 +1,33 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
-
import { siteConfigLabel, siteConfigPath } from './site-paths.mjs';
|
|
2
|
+
import { siteConfigLabel, siteConfigPath, siteThemeLabel } from './site-paths.mjs';
|
|
3
|
+
import { readThemeConfig } from './theme-config.mjs';
|
|
3
4
|
|
|
4
5
|
const { default: siteConfig } = await import(/* @vite-ignore */ pathToFileURL(siteConfigPath).href);
|
|
6
|
+
const themeConfig = await readThemeConfig();
|
|
5
7
|
|
|
6
8
|
const cssLengthPattern = String.raw`(?:\d+|\d*\.\d+)(?:px|rem|em|vw|vh|vmin|vmax|ch|%)`;
|
|
7
9
|
const simpleCssLengthPattern = new RegExp(`^${cssLengthPattern}$`);
|
|
8
10
|
const clampCssLengthPattern = new RegExp(`^clamp\\(\\s*${cssLengthPattern}\\s*,\\s*${cssLengthPattern}\\s*,\\s*${cssLengthPattern}\\s*\\)$`);
|
|
9
11
|
|
|
10
|
-
const assertObject = (value, path) => {
|
|
12
|
+
const assertObject = (value, path, sourceLabel = siteConfigLabel) => {
|
|
11
13
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
12
|
-
throw new Error(`${path} must be an object in ${
|
|
14
|
+
throw new Error(`${path} must be an object in ${sourceLabel}.`);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
return value;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
|
-
const readString = (object, key, path) => {
|
|
20
|
+
const readString = (object, key, path, sourceLabel = siteConfigLabel) => {
|
|
19
21
|
const value = object[key];
|
|
20
22
|
|
|
21
23
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
22
|
-
throw new Error(`${path}.${key} must be a non-empty string in ${
|
|
24
|
+
throw new Error(`${path}.${key} must be a non-empty string in ${sourceLabel}.`);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
return value.trim();
|
|
26
28
|
};
|
|
27
29
|
|
|
28
|
-
const readOptionalString = (object, key, path) => {
|
|
30
|
+
const readOptionalString = (object, key, path, sourceLabel = siteConfigLabel) => {
|
|
29
31
|
const value = object[key];
|
|
30
32
|
|
|
31
33
|
if (value === undefined || value === null || value === '') {
|
|
@@ -33,66 +35,69 @@ const readOptionalString = (object, key, path) => {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
36
|
-
throw new Error(`${path}.${key} must be a non-empty string when set in ${
|
|
38
|
+
throw new Error(`${path}.${key} must be a non-empty string when set in ${sourceLabel}.`);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
return value.trim();
|
|
40
42
|
};
|
|
41
43
|
|
|
42
|
-
const readFontFamily = (object, key, path, fallback) => {
|
|
44
|
+
const readFontFamily = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
43
45
|
const value = object[key] ?? fallback;
|
|
44
46
|
|
|
45
47
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
46
|
-
throw new Error(`${path}.${key} must be a non-empty CSS font-family value in ${
|
|
48
|
+
throw new Error(`${path}.${key} must be a non-empty CSS font-family value in ${sourceLabel}.`);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const normalizedValue = value.trim();
|
|
50
52
|
|
|
51
53
|
if (/[\n\r;{}]/.test(normalizedValue)) {
|
|
52
|
-
throw new Error(`${path}.${key} must not contain semicolons, braces, or line breaks in ${
|
|
54
|
+
throw new Error(`${path}.${key} must not contain semicolons, braces, or line breaks in ${sourceLabel}.`);
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
return normalizedValue;
|
|
56
58
|
};
|
|
57
59
|
|
|
58
|
-
const readCssLength = (object, key, path, fallback) => {
|
|
60
|
+
const readCssLength = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
59
61
|
const value = object[key] ?? fallback;
|
|
60
62
|
|
|
61
63
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
62
|
-
throw new Error(`${path}.${key} must be a non-empty CSS length in ${
|
|
64
|
+
throw new Error(`${path}.${key} must be a non-empty CSS length in ${sourceLabel}.`);
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
const normalizedValue = value.trim();
|
|
66
68
|
|
|
67
69
|
if (!simpleCssLengthPattern.test(normalizedValue) || parseFloat(normalizedValue) <= 0) {
|
|
68
|
-
throw new Error(`${path}.${key} must be a CSS length such as "900px", "56rem", or "90%" in ${
|
|
70
|
+
throw new Error(`${path}.${key} must be a CSS length such as "900px", "56rem", or "90%" in ${sourceLabel}.`);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
return normalizedValue;
|
|
72
74
|
};
|
|
73
75
|
|
|
74
|
-
const readCssLengthValue = (value, path) => {
|
|
76
|
+
const readCssLengthValue = (value, path, sourceLabel = siteConfigLabel) => {
|
|
75
77
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
76
|
-
throw new Error(`${path} must be a non-empty CSS length in ${
|
|
78
|
+
throw new Error(`${path} must be a non-empty CSS length in ${sourceLabel}.`);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
const normalizedValue = value.trim();
|
|
80
82
|
|
|
81
83
|
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
normalizedValue !== '0'
|
|
85
|
+
&& (
|
|
86
|
+
(!simpleCssLengthPattern.test(normalizedValue) && !clampCssLengthPattern.test(normalizedValue))
|
|
87
|
+
|| parseFloat(normalizedValue) < 0
|
|
88
|
+
)
|
|
84
89
|
) {
|
|
85
|
-
throw new Error(`${path} must be a CSS length such as "48px", "3rem", "4vw", or a clamp() of those lengths in ${
|
|
90
|
+
throw new Error(`${path} must be a CSS length such as "0", "48px", "3rem", "4vw", or a clamp() of those lengths in ${sourceLabel}.`);
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
return normalizedValue;
|
|
89
94
|
};
|
|
90
95
|
|
|
91
|
-
const readResponsiveCssLength = (object, key, path, fallback) => {
|
|
96
|
+
const readResponsiveCssLength = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
92
97
|
const value = object[key] ?? fallback;
|
|
93
98
|
|
|
94
99
|
if (typeof value === 'string') {
|
|
95
|
-
const length = readCssLengthValue(value, `${path}.${key}
|
|
100
|
+
const length = readCssLengthValue(value, `${path}.${key}`, sourceLabel);
|
|
96
101
|
|
|
97
102
|
return Object.freeze({
|
|
98
103
|
desktop: length,
|
|
@@ -100,27 +105,27 @@ const readResponsiveCssLength = (object, key, path, fallback) => {
|
|
|
100
105
|
});
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
const responsiveValue = assertObject(value, `${path}.${key}
|
|
108
|
+
const responsiveValue = assertObject(value, `${path}.${key}`, sourceLabel);
|
|
104
109
|
|
|
105
110
|
return Object.freeze({
|
|
106
|
-
desktop: readCssLengthValue(responsiveValue.desktop ?? fallback.desktop, `${path}.${key}.desktop
|
|
107
|
-
mobile: readCssLengthValue(responsiveValue.mobile ?? fallback.mobile, `${path}.${key}.mobile
|
|
111
|
+
desktop: readCssLengthValue(responsiveValue.desktop ?? fallback.desktop, `${path}.${key}.desktop`, sourceLabel),
|
|
112
|
+
mobile: readCssLengthValue(responsiveValue.mobile ?? fallback.mobile, `${path}.${key}.mobile`, sourceLabel),
|
|
108
113
|
});
|
|
109
114
|
};
|
|
110
115
|
|
|
111
|
-
const readPercentValue = (value, path) => {
|
|
116
|
+
const readPercentValue = (value, path, sourceLabel = siteConfigLabel) => {
|
|
112
117
|
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 100) {
|
|
113
|
-
throw new Error(`${path} must be a number greater than 0 and less than or equal to 100 in ${
|
|
118
|
+
throw new Error(`${path} must be a number greater than 0 and less than or equal to 100 in ${sourceLabel}.`);
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
return value;
|
|
117
122
|
};
|
|
118
123
|
|
|
119
|
-
const readResponsivePercent = (object, key, path, fallback) => {
|
|
124
|
+
const readResponsivePercent = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
120
125
|
const value = object[key] ?? fallback;
|
|
121
126
|
|
|
122
127
|
if (typeof value === 'number') {
|
|
123
|
-
const percent = readPercentValue(value, `${path}.${key}
|
|
128
|
+
const percent = readPercentValue(value, `${path}.${key}`, sourceLabel);
|
|
124
129
|
|
|
125
130
|
return Object.freeze({
|
|
126
131
|
desktop: percent,
|
|
@@ -128,11 +133,11 @@ const readResponsivePercent = (object, key, path, fallback) => {
|
|
|
128
133
|
});
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
const responsiveValue = assertObject(value, `${path}.${key}
|
|
136
|
+
const responsiveValue = assertObject(value, `${path}.${key}`, sourceLabel);
|
|
132
137
|
|
|
133
138
|
return Object.freeze({
|
|
134
|
-
desktop: readPercentValue(responsiveValue.desktop ?? fallback.desktop, `${path}.${key}.desktop
|
|
135
|
-
mobile: readPercentValue(responsiveValue.mobile ?? fallback.mobile, `${path}.${key}.mobile
|
|
139
|
+
desktop: readPercentValue(responsiveValue.desktop ?? fallback.desktop, `${path}.${key}.desktop`, sourceLabel),
|
|
140
|
+
mobile: readPercentValue(responsiveValue.mobile ?? fallback.mobile, `${path}.${key}.mobile`, sourceLabel),
|
|
136
141
|
});
|
|
137
142
|
};
|
|
138
143
|
|
|
@@ -229,7 +234,7 @@ const readLocale = (rawLocale) => {
|
|
|
229
234
|
closeMenu: readString({ closeMenu: labels.closeMenu ?? 'Close menu' }, 'closeMenu', 'locale.labels'),
|
|
230
235
|
skipToContent: readString({ skipToContent: labels.skipToContent ?? 'Skip to content' }, 'skipToContent', 'locale.labels'),
|
|
231
236
|
sectionNavigation: readString({ sectionNavigation: labels.sectionNavigation ?? 'Sections' }, 'sectionNavigation', 'locale.labels'),
|
|
232
|
-
gallery: readString({ gallery: labels.gallery ?? '
|
|
237
|
+
gallery: readString({ gallery: labels.gallery ?? 'Images' }, 'gallery', 'locale.labels'),
|
|
233
238
|
menu: readString({ menu: labels.menu ?? 'Menu' }, 'menu', 'locale.labels'),
|
|
234
239
|
pageNavigation: readString({ pageNavigation: labels.pageNavigation ?? 'On this page' }, 'pageNavigation', 'locale.labels'),
|
|
235
240
|
siteNavigation: readString({ siteNavigation: labels.siteNavigation ?? 'Pages' }, 'siteNavigation', 'locale.labels'),
|
|
@@ -280,10 +285,17 @@ const readBuildInfo = (footer) => {
|
|
|
280
285
|
};
|
|
281
286
|
|
|
282
287
|
const rawConfig = assertObject(siteConfig, 'default export');
|
|
288
|
+
const misplacedThemeKeys = ['layout', 'gallery', 'typography'].filter((key) => key in rawConfig);
|
|
289
|
+
if (misplacedThemeKeys.length > 0) {
|
|
290
|
+
throw new Error(`${misplacedThemeKeys.join(', ')} belong in ${siteThemeLabel}, not ${siteConfigLabel}.`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const rawTheme = assertObject(themeConfig, 'theme frontmatter', siteThemeLabel);
|
|
283
294
|
const rawSite = assertObject(rawConfig.site, 'site');
|
|
284
|
-
const rawLayout = assertObject(
|
|
285
|
-
const
|
|
286
|
-
const
|
|
295
|
+
const rawLayout = assertObject(rawTheme.layout ?? {}, 'layout', siteThemeLabel);
|
|
296
|
+
const rawLayoutSpacing = assertObject(rawLayout.spacing ?? {}, 'layout.spacing', siteThemeLabel);
|
|
297
|
+
const rawGallery = assertObject(rawTheme.gallery ?? {}, 'gallery', siteThemeLabel);
|
|
298
|
+
const rawTypography = assertObject(rawTheme.typography ?? {}, 'typography', siteThemeLabel);
|
|
287
299
|
const rawNavigation = assertObject(rawConfig.navigation ?? {}, 'navigation');
|
|
288
300
|
const rawLocale = rawConfig.locale ?? {};
|
|
289
301
|
const rawFooter = assertObject(rawConfig.footer ?? {}, 'footer');
|
|
@@ -302,28 +314,62 @@ export const projectConfig = Object.freeze({
|
|
|
302
314
|
gutter: readResponsiveCssLength(rawLayout, 'gutter', 'layout', Object.freeze({
|
|
303
315
|
desktop: 'clamp(1.25rem, 4vw, 3rem)',
|
|
304
316
|
mobile: '1rem',
|
|
305
|
-
})),
|
|
306
|
-
pageWidth: readCssLength(rawLayout, 'pageWidth', 'layout', '1180px'),
|
|
317
|
+
}), siteThemeLabel),
|
|
318
|
+
pageWidth: readCssLength(rawLayout, 'pageWidth', 'layout', '1180px', siteThemeLabel),
|
|
319
|
+
spacing: Object.freeze({
|
|
320
|
+
bodyToImages: readResponsiveCssLength(rawLayoutSpacing, 'bodyToImages', 'layout.spacing', Object.freeze({
|
|
321
|
+
desktop: 'clamp(1.25rem, 2.5vw, 2rem)',
|
|
322
|
+
mobile: '1.25rem',
|
|
323
|
+
}), siteThemeLabel),
|
|
324
|
+
finalSectionBottom: readResponsiveCssLength(rawLayoutSpacing, 'finalSectionBottom', 'layout.spacing', Object.freeze({
|
|
325
|
+
desktop: 'clamp(2.55rem, 4.2vw, 3.9rem)',
|
|
326
|
+
mobile: '2.25rem',
|
|
327
|
+
}), siteThemeLabel),
|
|
328
|
+
firstSectionTop: readResponsiveCssLength(rawLayoutSpacing, 'firstSectionTop', 'layout.spacing', Object.freeze({
|
|
329
|
+
desktop: 'clamp(1.875rem, 3vw, 2.75rem)',
|
|
330
|
+
mobile: '1.375rem',
|
|
331
|
+
}), siteThemeLabel),
|
|
332
|
+
imageGap: readResponsiveCssLength(rawLayoutSpacing, 'imageGap', 'layout.spacing', Object.freeze({
|
|
333
|
+
desktop: 'clamp(1.5rem, 3.5vw, 2.75rem)',
|
|
334
|
+
mobile: '2.75rem',
|
|
335
|
+
}), siteThemeLabel),
|
|
336
|
+
sectionGap: readResponsiveCssLength(rawLayoutSpacing, 'sectionGap', 'layout.spacing', Object.freeze({
|
|
337
|
+
desktop: 'clamp(2.55rem, 4.2vw, 3.9rem)',
|
|
338
|
+
mobile: '2.25rem',
|
|
339
|
+
}), siteThemeLabel),
|
|
340
|
+
sectionHeadingToBody: readResponsiveCssLength(rawLayoutSpacing, 'sectionHeadingToBody', 'layout.spacing', Object.freeze({
|
|
341
|
+
desktop: 'clamp(0.5625rem, 1.25vw, 0.75rem)',
|
|
342
|
+
mobile: '0.4375rem',
|
|
343
|
+
}), siteThemeLabel),
|
|
344
|
+
subheadingRuleTop: readResponsiveCssLength(rawLayoutSpacing, 'subheadingRuleTop', 'layout.spacing', Object.freeze({
|
|
345
|
+
desktop: '1rem',
|
|
346
|
+
mobile: '0.8rem',
|
|
347
|
+
}), siteThemeLabel),
|
|
348
|
+
subheadingTop: readResponsiveCssLength(rawLayoutSpacing, 'subheadingTop', 'layout.spacing', Object.freeze({
|
|
349
|
+
desktop: '2rem',
|
|
350
|
+
mobile: '1.6rem',
|
|
351
|
+
}), siteThemeLabel),
|
|
352
|
+
}),
|
|
307
353
|
}),
|
|
308
354
|
gallery: Object.freeze({
|
|
309
355
|
maxAvailableHeightPercent: readResponsivePercent(rawGallery, 'maxAvailableHeightPercent', 'gallery', Object.freeze({
|
|
310
356
|
desktop: 74,
|
|
311
357
|
mobile: 68,
|
|
312
|
-
})),
|
|
358
|
+
}), siteThemeLabel),
|
|
313
359
|
maxAvailableWidthPercent: readResponsivePercent(rawGallery, 'maxAvailableWidthPercent', 'gallery', Object.freeze({
|
|
314
360
|
desktop: 100,
|
|
315
361
|
mobile: 100,
|
|
316
|
-
})),
|
|
317
|
-
width: readCssLength(rawGallery, 'width', 'gallery', '900px'),
|
|
362
|
+
}), siteThemeLabel),
|
|
363
|
+
width: readCssLength(rawGallery, 'width', 'gallery', '900px', siteThemeLabel),
|
|
318
364
|
}),
|
|
319
365
|
typography: Object.freeze({
|
|
320
|
-
fontFamily: readFontFamily(rawTypography, 'fontFamily', 'typography', defaultFontFamily),
|
|
366
|
+
fontFamily: readFontFamily(rawTypography, 'fontFamily', 'typography', defaultFontFamily, siteThemeLabel),
|
|
321
367
|
}),
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
368
|
+
navigation: Object.freeze({
|
|
369
|
+
smoothScroll: readSmoothScroll(rawNavigation),
|
|
370
|
+
}),
|
|
371
|
+
locale: readLocale(rawLocale),
|
|
372
|
+
footer: Object.freeze({
|
|
327
373
|
buildInfo: readBuildInfo(rawFooter),
|
|
328
374
|
copyrightMessage: readOptionalString(rawFooter, 'copyrightMessage', 'footer'),
|
|
329
375
|
}),
|
|
@@ -18,42 +18,60 @@ const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
|
|
|
18
18
|
const inlineStyleReferenceRegex = /\[[^\]\n]+\]\{\.([a-z][a-z0-9-]*)\}/g;
|
|
19
19
|
const frontmatterDelimiterRegex = /^---\s*$/;
|
|
20
20
|
const knownContentTopLevelFrontmatterKeys = new Set(['title', 'description', 'slug', 'navigation', 'presentation', 'frame', 'sections']);
|
|
21
|
-
const knownThemeTopLevelFrontmatterKeys = new Set(['navigation', 'presentation', 'frame']);
|
|
21
|
+
const knownThemeTopLevelFrontmatterKeys = new Set(['navigation', 'layout', 'gallery', 'typography', 'presentation', 'frame']);
|
|
22
22
|
const knownNestedFrontmatterKeys = new Set([
|
|
23
23
|
'align',
|
|
24
24
|
'alt',
|
|
25
25
|
'backgroundColor',
|
|
26
26
|
'body',
|
|
27
|
+
'bodyToImages',
|
|
27
28
|
'brand',
|
|
28
29
|
'caption',
|
|
29
30
|
'carousel',
|
|
30
31
|
'color',
|
|
31
32
|
'colors',
|
|
32
33
|
'desktop',
|
|
34
|
+
'fontFamily',
|
|
33
35
|
'from',
|
|
36
|
+
'finalSectionBottom',
|
|
34
37
|
'gallery',
|
|
38
|
+
'gutter',
|
|
39
|
+
'h1',
|
|
40
|
+
'h2',
|
|
41
|
+
'h3',
|
|
42
|
+
'h4',
|
|
35
43
|
'heading',
|
|
44
|
+
'headings',
|
|
36
45
|
'id',
|
|
37
46
|
'include',
|
|
38
47
|
'image',
|
|
48
|
+
'imageGap',
|
|
39
49
|
'inlineStyles',
|
|
40
50
|
'label',
|
|
41
51
|
'lineHeight',
|
|
52
|
+
'maxAvailableHeightPercent',
|
|
53
|
+
'maxAvailableWidthPercent',
|
|
42
54
|
'mobile',
|
|
43
55
|
'navigation',
|
|
44
56
|
'overrides',
|
|
45
57
|
'order',
|
|
46
58
|
'paragraphSpacing',
|
|
59
|
+
'pageWidth',
|
|
47
60
|
'preset',
|
|
48
61
|
'presentation',
|
|
49
62
|
'sections',
|
|
63
|
+
'sectionGap',
|
|
64
|
+
'sectionHeadingToBody',
|
|
50
65
|
'size',
|
|
51
66
|
'spacing',
|
|
67
|
+
'subheadingRuleTop',
|
|
68
|
+
'subheadingTop',
|
|
52
69
|
'textColor',
|
|
53
70
|
'theme',
|
|
54
71
|
'typography',
|
|
55
72
|
'until',
|
|
56
73
|
'visible',
|
|
74
|
+
'width',
|
|
57
75
|
]);
|
|
58
76
|
|
|
59
77
|
export const toPosixPath = (filePath) => filePath.split(path.sep).join('/');
|
|
@@ -224,7 +242,7 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
|
|
|
224
242
|
if (knownTopLevelFrontmatterKeys.has(key)) continue;
|
|
225
243
|
|
|
226
244
|
const fix = knownNestedFrontmatterKeys.has(key)
|
|
227
|
-
? `Indent "${key}:" under the section or object it belongs to. For
|
|
245
|
+
? `Indent "${key}:" under the section or object it belongs to. For image rows, "gallery:" normally belongs under a "sections" item.`
|
|
228
246
|
: `Move "${key}:" under the correct parent key, or remove it if it is not part of the ${fileKind} schema.`;
|
|
229
247
|
|
|
230
248
|
addIssue({
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { parseYamlMapping } from './frontmatter-yaml.mjs';
|
|
3
|
+
import {
|
|
4
|
+
splitSiteFile,
|
|
5
|
+
validateFrontmatterIndentation,
|
|
6
|
+
validateThemeFrontmatterStructure,
|
|
7
|
+
} from './site-content.mjs';
|
|
8
|
+
import { siteThemeLabel, siteThemePath } from './site-paths.mjs';
|
|
9
|
+
|
|
10
|
+
const emptyThemeFile = '---\n---\n';
|
|
11
|
+
|
|
12
|
+
export const readThemeConfig = async () => {
|
|
13
|
+
const themeFile = await readFile(siteThemePath, 'utf8').catch((error) => {
|
|
14
|
+
if (error?.code === 'ENOENT') {
|
|
15
|
+
return emptyThemeFile;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw error;
|
|
19
|
+
});
|
|
20
|
+
const { frontmatter, frontmatterBody } = splitSiteFile(themeFile, siteThemeLabel);
|
|
21
|
+
const issues = [];
|
|
22
|
+
|
|
23
|
+
validateFrontmatterIndentation(frontmatter, (issue) => issues.push(issue));
|
|
24
|
+
validateThemeFrontmatterStructure(frontmatter, (issue) => issues.push(issue));
|
|
25
|
+
|
|
26
|
+
if (issues.length > 0) {
|
|
27
|
+
throw new Error([
|
|
28
|
+
`${siteThemeLabel} has invalid frontmatter.`,
|
|
29
|
+
...issues.map((issue) => `- ${issue.message}`),
|
|
30
|
+
].join('\n'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return parseYamlMapping(frontmatterBody);
|
|
34
|
+
};
|