@janga/norna 0.7.1 → 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 +32 -18
- package/astro.config.mjs +2 -0
- package/bin/norna-cli.mjs +175 -0
- package/bin/norna.mjs +149 -150
- package/docs/README.md +41 -22
- package/docs/commands.md +25 -12
- package/docs/configuration.md +38 -135
- package/docs/content.md +108 -265
- package/docs/{command-organization.md → design/command-organization.md} +64 -60
- package/docs/{site-examples-structure-note.md → design/site-examples-structure.md} +19 -24
- package/docs/engine-development.md +35 -7
- package/docs/getting-started.md +55 -26
- package/docs/images-and-metadata.md +1 -1
- package/docs/local-development.md +14 -1
- package/docs/publishing.md +24 -1
- package/docs/routes.md +90 -0
- package/docs/site-structure.md +18 -10
- package/docs/theme.md +292 -0
- package/docs/typography.md +144 -0
- 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/.norna/generated-images.json +226 -0
- package/examples/dog-gallery/site/config.mjs +67 -0
- package/examples/dog-gallery/site/content.md +168 -0
- package/examples/dog-gallery/site/images/black-dogs/black-puppy-meadow.png +0 -0
- package/examples/dog-gallery/site/images/black-dogs/photo-of-a-black-dog.jpg +0 -0
- package/examples/dog-gallery/site/images/brown-dogs/brown-dog.jpg +0 -0
- package/examples/dog-gallery/site/images/brown-dogs/dog-accompanies-master.jpg +0 -0
- package/examples/dog-gallery/site/images/golden-dogs/golden-retriever.jpg +0 -0
- package/examples/dog-gallery/site/images/golden-dogs/toller-puppy.jpg +0 -0
- package/examples/dog-gallery/site/images/white-dogs/white-cute-dog.jpg +0 -0
- package/examples/dog-gallery/site/images/white-dogs/white-puppy-garden.png +0 -0
- package/examples/dog-gallery/site/public/favicon.svg +7 -0
- package/examples/dog-gallery/site/public/robots.txt +2 -0
- package/examples/dog-gallery/site/routes/dog-care/route-content.md +49 -0
- package/examples/dog-gallery/site/theme.md +114 -0
- package/fixtures/basic/site/config.mjs +3 -22
- package/fixtures/basic/site/content.md +1 -1
- package/fixtures/basic/site/theme.md +7 -3
- package/package.json +10 -9
- package/scripts/check-config.mjs +4 -3
- package/scripts/deploy-site.mjs +3 -3
- package/scripts/dev-local.mjs +64 -15
- package/scripts/init-site.mjs +11 -9
- package/scripts/lib/frontmatter-yaml.mjs +85 -0
- package/scripts/lib/presentation.mjs +10 -11
- package/scripts/lib/project-config.mjs +113 -45
- package/scripts/lib/site-content.mjs +21 -2
- package/scripts/lib/site-paths.mjs +18 -3
- package/scripts/lib/theme-config.mjs +34 -0
- package/scripts/lib/typography.mjs +103 -23
- package/scripts/show-typography.mjs +202 -75
- package/scripts/sync-content-sections.mjs +6 -6
- package/scripts/test-cli-discovery.mjs +124 -0
- package/scripts/test-content-check.mjs +3 -4
- package/scripts/test-engine-commands.mjs +20 -6
- package/scripts/test-navigation.mjs +10 -6
- package/scripts/test-package-check.mjs +40 -11
- package/scripts/test-temporary-visibility.mjs +2 -3
- package/src/components/SiteNavigation.astro +7 -5
- package/src/components/SitePage.astro +5 -3
- package/src/components/SiteSection.astro +63 -29
- package/src/content.config.ts +64 -2
- package/src/layouts/BaseLayout.astro +19 -1
- package/src/lib/basePath.ts +21 -0
- package/src/lib/generatedImages.ts +8 -4
- package/src/lib/sectionContent.ts +6 -1
- package/src/lib/sitePublicAssets.ts +8 -1
- package/src/styles/global.css +59 -55
- package/starters/basic/.github/workflows/deploy.yml +3 -3
- package/starters/basic/README.md +30 -14
- package/starters/basic/package.json +3 -3
- package/starters/basic/site/config.mjs +4 -24
- package/starters/basic/site/content.md +44 -9
- package/starters/basic/site/theme.md +113 -16
- 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
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'),
|
|
@@ -156,7 +156,7 @@ const addGalleryDependency = (packageJson, version) => {
|
|
|
156
156
|
const addGalleryScripts = (packageJson, scripts, { includePureAliases }) => {
|
|
157
157
|
packageJson.scripts ??= {};
|
|
158
158
|
const wantedScripts = {
|
|
159
|
-
...(includePureAliases ? { dev: 'npm run norna:dev' } : {}),
|
|
159
|
+
...(includePureAliases ? { dev: 'npm run norna:dev --' } : {}),
|
|
160
160
|
...scripts,
|
|
161
161
|
...(includePureAliases ? { build: 'npm run norna:build' } : {}),
|
|
162
162
|
};
|
|
@@ -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
|
|
|
@@ -176,6 +181,27 @@ const readUrl = (object, key, path) => {
|
|
|
176
181
|
}
|
|
177
182
|
};
|
|
178
183
|
|
|
184
|
+
const readBasePath = (object, key, path) => {
|
|
185
|
+
const value = object[key] ?? '/';
|
|
186
|
+
|
|
187
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
188
|
+
throw new Error(`${path}.${key} must be a non-empty URL path in ${siteConfigLabel}.`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const normalizedValue = value.trim();
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
!normalizedValue.startsWith('/')
|
|
195
|
+
|| !normalizedValue.endsWith('/')
|
|
196
|
+
|| normalizedValue.includes('//')
|
|
197
|
+
|| /[\s?#]/.test(normalizedValue)
|
|
198
|
+
) {
|
|
199
|
+
throw new Error(`${path}.${key} must start and end with "/" and must not contain whitespace, "?", "#", or "//" in ${siteConfigLabel}.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return normalizedValue;
|
|
203
|
+
};
|
|
204
|
+
|
|
179
205
|
const readSmoothScroll = (navigation) => {
|
|
180
206
|
const rawSmoothScroll = assertObject(navigation.smoothScroll ?? {}, 'navigation.smoothScroll');
|
|
181
207
|
const minimumDurationMs = readPositiveInteger(rawSmoothScroll, 'minimumDurationMs', 'navigation.smoothScroll', 2_000);
|
|
@@ -208,7 +234,7 @@ const readLocale = (rawLocale) => {
|
|
|
208
234
|
closeMenu: readString({ closeMenu: labels.closeMenu ?? 'Close menu' }, 'closeMenu', 'locale.labels'),
|
|
209
235
|
skipToContent: readString({ skipToContent: labels.skipToContent ?? 'Skip to content' }, 'skipToContent', 'locale.labels'),
|
|
210
236
|
sectionNavigation: readString({ sectionNavigation: labels.sectionNavigation ?? 'Sections' }, 'sectionNavigation', 'locale.labels'),
|
|
211
|
-
gallery: readString({ gallery: labels.gallery ?? '
|
|
237
|
+
gallery: readString({ gallery: labels.gallery ?? 'Images' }, 'gallery', 'locale.labels'),
|
|
212
238
|
menu: readString({ menu: labels.menu ?? 'Menu' }, 'menu', 'locale.labels'),
|
|
213
239
|
pageNavigation: readString({ pageNavigation: labels.pageNavigation ?? 'On this page' }, 'pageNavigation', 'locale.labels'),
|
|
214
240
|
siteNavigation: readString({ siteNavigation: labels.siteNavigation ?? 'Pages' }, 'siteNavigation', 'locale.labels'),
|
|
@@ -259,10 +285,17 @@ const readBuildInfo = (footer) => {
|
|
|
259
285
|
};
|
|
260
286
|
|
|
261
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);
|
|
262
294
|
const rawSite = assertObject(rawConfig.site, 'site');
|
|
263
|
-
const rawLayout = assertObject(
|
|
264
|
-
const
|
|
265
|
-
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);
|
|
266
299
|
const rawNavigation = assertObject(rawConfig.navigation ?? {}, 'navigation');
|
|
267
300
|
const rawLocale = rawConfig.locale ?? {};
|
|
268
301
|
const rawFooter = assertObject(rawConfig.footer ?? {}, 'footer');
|
|
@@ -274,34 +307,69 @@ const defaultFontFamily = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
|
|
|
274
307
|
|
|
275
308
|
export const projectConfig = Object.freeze({
|
|
276
309
|
site: Object.freeze({
|
|
310
|
+
basePath: readBasePath(rawSite, 'basePath', 'site'),
|
|
277
311
|
url: readUrl(rawSite, 'url', 'site'),
|
|
278
312
|
}),
|
|
279
313
|
layout: Object.freeze({
|
|
280
314
|
gutter: readResponsiveCssLength(rawLayout, 'gutter', 'layout', Object.freeze({
|
|
281
315
|
desktop: 'clamp(1.25rem, 4vw, 3rem)',
|
|
282
316
|
mobile: '1rem',
|
|
283
|
-
})),
|
|
284
|
-
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
|
+
}),
|
|
285
353
|
}),
|
|
286
354
|
gallery: Object.freeze({
|
|
287
355
|
maxAvailableHeightPercent: readResponsivePercent(rawGallery, 'maxAvailableHeightPercent', 'gallery', Object.freeze({
|
|
288
356
|
desktop: 74,
|
|
289
357
|
mobile: 68,
|
|
290
|
-
})),
|
|
358
|
+
}), siteThemeLabel),
|
|
291
359
|
maxAvailableWidthPercent: readResponsivePercent(rawGallery, 'maxAvailableWidthPercent', 'gallery', Object.freeze({
|
|
292
360
|
desktop: 100,
|
|
293
361
|
mobile: 100,
|
|
294
|
-
})),
|
|
295
|
-
width: readCssLength(rawGallery, 'width', 'gallery', '900px'),
|
|
362
|
+
}), siteThemeLabel),
|
|
363
|
+
width: readCssLength(rawGallery, 'width', 'gallery', '900px', siteThemeLabel),
|
|
296
364
|
}),
|
|
297
365
|
typography: Object.freeze({
|
|
298
|
-
fontFamily: readFontFamily(rawTypography, 'fontFamily', 'typography', defaultFontFamily),
|
|
366
|
+
fontFamily: readFontFamily(rawTypography, 'fontFamily', 'typography', defaultFontFamily, siteThemeLabel),
|
|
299
367
|
}),
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
368
|
+
navigation: Object.freeze({
|
|
369
|
+
smoothScroll: readSmoothScroll(rawNavigation),
|
|
370
|
+
}),
|
|
371
|
+
locale: readLocale(rawLocale),
|
|
372
|
+
footer: Object.freeze({
|
|
305
373
|
buildInfo: readBuildInfo(rawFooter),
|
|
306
374
|
copyrightMessage: readOptionalString(rawFooter, 'copyrightMessage', 'footer'),
|
|
307
375
|
}),
|
|
@@ -18,41 +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(['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',
|
|
28
|
+
'brand',
|
|
27
29
|
'caption',
|
|
28
30
|
'carousel',
|
|
29
31
|
'color',
|
|
30
32
|
'colors',
|
|
31
33
|
'desktop',
|
|
34
|
+
'fontFamily',
|
|
32
35
|
'from',
|
|
36
|
+
'finalSectionBottom',
|
|
33
37
|
'gallery',
|
|
38
|
+
'gutter',
|
|
39
|
+
'h1',
|
|
40
|
+
'h2',
|
|
41
|
+
'h3',
|
|
42
|
+
'h4',
|
|
34
43
|
'heading',
|
|
44
|
+
'headings',
|
|
35
45
|
'id',
|
|
36
46
|
'include',
|
|
37
47
|
'image',
|
|
48
|
+
'imageGap',
|
|
38
49
|
'inlineStyles',
|
|
39
50
|
'label',
|
|
40
51
|
'lineHeight',
|
|
52
|
+
'maxAvailableHeightPercent',
|
|
53
|
+
'maxAvailableWidthPercent',
|
|
41
54
|
'mobile',
|
|
42
55
|
'navigation',
|
|
43
56
|
'overrides',
|
|
44
57
|
'order',
|
|
45
58
|
'paragraphSpacing',
|
|
59
|
+
'pageWidth',
|
|
46
60
|
'preset',
|
|
47
61
|
'presentation',
|
|
48
62
|
'sections',
|
|
63
|
+
'sectionGap',
|
|
64
|
+
'sectionHeadingToBody',
|
|
49
65
|
'size',
|
|
50
66
|
'spacing',
|
|
67
|
+
'subheadingRuleTop',
|
|
68
|
+
'subheadingTop',
|
|
51
69
|
'textColor',
|
|
52
70
|
'theme',
|
|
53
71
|
'typography',
|
|
54
72
|
'until',
|
|
55
73
|
'visible',
|
|
74
|
+
'width',
|
|
56
75
|
]);
|
|
57
76
|
|
|
58
77
|
export const toPosixPath = (filePath) => filePath.split(path.sep).join('/');
|
|
@@ -223,7 +242,7 @@ export const validateFrontmatterStructure = (frontmatter, addIssue, {
|
|
|
223
242
|
if (knownTopLevelFrontmatterKeys.has(key)) continue;
|
|
224
243
|
|
|
225
244
|
const fix = knownNestedFrontmatterKeys.has(key)
|
|
226
|
-
? `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.`
|
|
227
246
|
: `Move "${key}:" under the correct parent key, or remove it if it is not part of the ${fileKind} schema.`;
|
|
228
247
|
|
|
229
248
|
addIssue({
|
|
@@ -20,15 +20,19 @@ export const invocationRoot = configuredInvocationRoot
|
|
|
20
20
|
? path.resolve(configuredInvocationRoot)
|
|
21
21
|
: process.cwd();
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
const siteDir = path.resolve(projectRoot, siteDirectory);
|
|
25
|
-
|
|
23
|
+
const hasSiteFilesInDirectory = (siteDir) => {
|
|
26
24
|
return (
|
|
27
25
|
existsSync(path.join(siteDir, 'config.mjs'))
|
|
28
26
|
&& existsSync(path.join(siteDir, 'content.md'))
|
|
29
27
|
);
|
|
30
28
|
};
|
|
31
29
|
|
|
30
|
+
const hasSiteFiles = (projectRoot, siteDirectory) => {
|
|
31
|
+
const siteDir = path.resolve(projectRoot, siteDirectory);
|
|
32
|
+
|
|
33
|
+
return hasSiteFilesInDirectory(siteDir);
|
|
34
|
+
};
|
|
35
|
+
|
|
32
36
|
const findSiteProjectRoot = (startDirectory, siteDirectory) => {
|
|
33
37
|
let current = path.resolve(startDirectory);
|
|
34
38
|
|
|
@@ -67,6 +71,17 @@ const resolveSitePaths = () => {
|
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
if (!hasConfiguredSiteDirectory && hasSiteFilesInDirectory(invocationRoot)) {
|
|
75
|
+
const siteDir = invocationRoot;
|
|
76
|
+
const siteProjectRoot = path.dirname(siteDir);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
siteDirectory: path.relative(siteProjectRoot, siteDir) || path.basename(siteDir),
|
|
80
|
+
siteDir,
|
|
81
|
+
siteProjectRoot,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
const discoveredRoot = findSiteProjectRoot(invocationRoot, fallbackSiteDirectory);
|
|
71
86
|
const siteProjectRoot = discoveredRoot ?? invocationRoot;
|
|
72
87
|
|
|
@@ -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
|
+
};
|