@janga/norna 0.7.2 → 0.7.4
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 +11 -12
- package/docs/commands.md +9 -9
- package/docs/configuration.md +14 -145
- package/docs/content.md +17 -15
- package/docs/design/command-organization.md +58 -57
- package/docs/design/site-examples-structure.md +6 -5
- package/docs/engine-development.md +5 -5
- package/docs/getting-started.md +20 -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 +133 -7
- package/docs/typography.md +78 -43
- 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 +28 -6
- package/examples/dog-gallery/site/routes/dog-care/route-content.md +17 -3
- package/examples/dog-gallery/site/theme.md +83 -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 +4 -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 +146 -45
- package/scripts/lib/site-content.mjs +21 -3
- package/scripts/lib/theme-config.mjs +34 -0
- package/scripts/lib/typography.mjs +155 -35
- package/scripts/show-typography.mjs +127 -159
- package/scripts/sync-content-sections.mjs +6 -6
- package/scripts/test-content-check.mjs +3 -4
- package/scripts/test-engine-commands.mjs +21 -3
- package/scripts/test-package-check.mjs +34 -17
- package/scripts/test-temporary-visibility.mjs +2 -3
- package/src/components/SitePage.astro +2 -3
- package/src/components/SiteSection.astro +115 -19
- package/src/content.config.ts +66 -6
- package/src/layouts/BaseLayout.astro +11 -0
- package/src/styles/global.css +83 -52
- package/starters/basic/README.md +11 -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 +84 -18
- 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 +128 -0
- package/starters/project/site/public/robots.txt +2 -0
- package/starters/project/site/routes/guide/route-content.md +85 -0
- package/starters/project/site/theme.md +43 -0
|
@@ -3,26 +3,6 @@ export default {
|
|
|
3
3
|
url: 'https://example.com/',
|
|
4
4
|
basePath: '/',
|
|
5
5
|
},
|
|
6
|
-
layout: {
|
|
7
|
-
pageWidth: '1180px',
|
|
8
|
-
gutter: {
|
|
9
|
-
desktop: 'clamp(1.25rem, 4vw, 3rem)',
|
|
10
|
-
mobile: '1rem',
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
gallery: {
|
|
14
|
-
maxAvailableWidthPercent: {
|
|
15
|
-
desktop: 100,
|
|
16
|
-
mobile: 100,
|
|
17
|
-
},
|
|
18
|
-
maxAvailableHeightPercent: {
|
|
19
|
-
desktop: 74,
|
|
20
|
-
mobile: 68,
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
typography: {
|
|
24
|
-
fontFamily: "Arial, 'Helvetica Neue', Helvetica, sans-serif",
|
|
25
|
-
},
|
|
26
6
|
navigation: {
|
|
27
7
|
smoothScroll: {
|
|
28
8
|
enabled: true,
|
|
@@ -36,7 +16,7 @@ export default {
|
|
|
36
16
|
labels: {
|
|
37
17
|
skipToContent: 'Skip to content',
|
|
38
18
|
sectionNavigation: 'Sections',
|
|
39
|
-
gallery: '
|
|
19
|
+
gallery: 'Images',
|
|
40
20
|
},
|
|
41
21
|
},
|
|
42
22
|
footer: {
|
|
@@ -53,7 +33,7 @@ export default {
|
|
|
53
33
|
},
|
|
54
34
|
},
|
|
55
35
|
github: {
|
|
56
|
-
repo: 'owner/fixture-
|
|
36
|
+
repo: 'owner/fixture-site',
|
|
57
37
|
branch: 'main',
|
|
58
38
|
pagesWorkflow: 'Deploy to GitHub Pages',
|
|
59
39
|
},
|
|
@@ -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.4",
|
|
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,10 @@ 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(`Layout density: ${projectConfig.layout.density}`);
|
|
27
|
+
console.log(`Image area width: ${projectConfig.gallery.width}`);
|
|
28
|
+
console.log(`Image max available width: desktop ${projectConfig.gallery.maxAvailableWidthPercent.desktop}%, mobile ${projectConfig.gallery.maxAvailableWidthPercent.mobile}%`);
|
|
29
|
+
console.log(`Image max available height: desktop ${projectConfig.gallery.maxAvailableHeightPercent.desktop}%, mobile ${projectConfig.gallery.maxAvailableHeightPercent.mobile}%`);
|
|
29
30
|
console.log(`Font family: ${projectConfig.typography.fontFamily}`);
|
|
30
31
|
console.log(`Language: ${projectConfig.locale.lang}`);
|
|
31
32
|
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,43 @@
|
|
|
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
|
|
20
|
+
const readEnum = (object, key, path, allowedValues, fallback, sourceLabel = siteConfigLabel) => {
|
|
21
|
+
const value = object[key] ?? fallback;
|
|
22
|
+
|
|
23
|
+
if (!allowedValues.includes(value)) {
|
|
24
|
+
throw new Error(`${path}.${key} must be one of ${allowedValues.join(', ')} in ${sourceLabel}.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const readString = (object, key, path, sourceLabel = siteConfigLabel) => {
|
|
19
31
|
const value = object[key];
|
|
20
32
|
|
|
21
33
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
22
|
-
throw new Error(`${path}.${key} must be a non-empty string in ${
|
|
34
|
+
throw new Error(`${path}.${key} must be a non-empty string in ${sourceLabel}.`);
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
return value.trim();
|
|
26
38
|
};
|
|
27
39
|
|
|
28
|
-
const readOptionalString = (object, key, path) => {
|
|
40
|
+
const readOptionalString = (object, key, path, sourceLabel = siteConfigLabel) => {
|
|
29
41
|
const value = object[key];
|
|
30
42
|
|
|
31
43
|
if (value === undefined || value === null || value === '') {
|
|
@@ -33,66 +45,69 @@ const readOptionalString = (object, key, path) => {
|
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
36
|
-
throw new Error(`${path}.${key} must be a non-empty string when set in ${
|
|
48
|
+
throw new Error(`${path}.${key} must be a non-empty string when set in ${sourceLabel}.`);
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
return value.trim();
|
|
40
52
|
};
|
|
41
53
|
|
|
42
|
-
const readFontFamily = (object, key, path, fallback) => {
|
|
54
|
+
const readFontFamily = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
43
55
|
const value = object[key] ?? fallback;
|
|
44
56
|
|
|
45
57
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
46
|
-
throw new Error(`${path}.${key} must be a non-empty CSS font-family value in ${
|
|
58
|
+
throw new Error(`${path}.${key} must be a non-empty CSS font-family value in ${sourceLabel}.`);
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
const normalizedValue = value.trim();
|
|
50
62
|
|
|
51
63
|
if (/[\n\r;{}]/.test(normalizedValue)) {
|
|
52
|
-
throw new Error(`${path}.${key} must not contain semicolons, braces, or line breaks in ${
|
|
64
|
+
throw new Error(`${path}.${key} must not contain semicolons, braces, or line breaks in ${sourceLabel}.`);
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
return normalizedValue;
|
|
56
68
|
};
|
|
57
69
|
|
|
58
|
-
const readCssLength = (object, key, path, fallback) => {
|
|
70
|
+
const readCssLength = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
59
71
|
const value = object[key] ?? fallback;
|
|
60
72
|
|
|
61
73
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
62
|
-
throw new Error(`${path}.${key} must be a non-empty CSS length in ${
|
|
74
|
+
throw new Error(`${path}.${key} must be a non-empty CSS length in ${sourceLabel}.`);
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
const normalizedValue = value.trim();
|
|
66
78
|
|
|
67
79
|
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 ${
|
|
80
|
+
throw new Error(`${path}.${key} must be a CSS length such as "900px", "56rem", or "90%" in ${sourceLabel}.`);
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
return normalizedValue;
|
|
72
84
|
};
|
|
73
85
|
|
|
74
|
-
const readCssLengthValue = (value, path) => {
|
|
86
|
+
const readCssLengthValue = (value, path, sourceLabel = siteConfigLabel) => {
|
|
75
87
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
76
|
-
throw new Error(`${path} must be a non-empty CSS length in ${
|
|
88
|
+
throw new Error(`${path} must be a non-empty CSS length in ${sourceLabel}.`);
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
const normalizedValue = value.trim();
|
|
80
92
|
|
|
81
93
|
if (
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
normalizedValue !== '0'
|
|
95
|
+
&& (
|
|
96
|
+
(!simpleCssLengthPattern.test(normalizedValue) && !clampCssLengthPattern.test(normalizedValue))
|
|
97
|
+
|| parseFloat(normalizedValue) < 0
|
|
98
|
+
)
|
|
84
99
|
) {
|
|
85
|
-
throw new Error(`${path} must be a CSS length such as "48px", "3rem", "4vw", or a clamp() of those lengths in ${
|
|
100
|
+
throw new Error(`${path} must be a CSS length such as "0", "48px", "3rem", "4vw", or a clamp() of those lengths in ${sourceLabel}.`);
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
return normalizedValue;
|
|
89
104
|
};
|
|
90
105
|
|
|
91
|
-
const readResponsiveCssLength = (object, key, path, fallback) => {
|
|
106
|
+
const readResponsiveCssLength = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
92
107
|
const value = object[key] ?? fallback;
|
|
93
108
|
|
|
94
109
|
if (typeof value === 'string') {
|
|
95
|
-
const length = readCssLengthValue(value, `${path}.${key}
|
|
110
|
+
const length = readCssLengthValue(value, `${path}.${key}`, sourceLabel);
|
|
96
111
|
|
|
97
112
|
return Object.freeze({
|
|
98
113
|
desktop: length,
|
|
@@ -100,27 +115,27 @@ const readResponsiveCssLength = (object, key, path, fallback) => {
|
|
|
100
115
|
});
|
|
101
116
|
}
|
|
102
117
|
|
|
103
|
-
const responsiveValue = assertObject(value, `${path}.${key}
|
|
118
|
+
const responsiveValue = assertObject(value, `${path}.${key}`, sourceLabel);
|
|
104
119
|
|
|
105
120
|
return Object.freeze({
|
|
106
|
-
desktop: readCssLengthValue(responsiveValue.desktop ?? fallback.desktop, `${path}.${key}.desktop
|
|
107
|
-
mobile: readCssLengthValue(responsiveValue.mobile ?? fallback.mobile, `${path}.${key}.mobile
|
|
121
|
+
desktop: readCssLengthValue(responsiveValue.desktop ?? fallback.desktop, `${path}.${key}.desktop`, sourceLabel),
|
|
122
|
+
mobile: readCssLengthValue(responsiveValue.mobile ?? fallback.mobile, `${path}.${key}.mobile`, sourceLabel),
|
|
108
123
|
});
|
|
109
124
|
};
|
|
110
125
|
|
|
111
|
-
const readPercentValue = (value, path) => {
|
|
126
|
+
const readPercentValue = (value, path, sourceLabel = siteConfigLabel) => {
|
|
112
127
|
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 ${
|
|
128
|
+
throw new Error(`${path} must be a number greater than 0 and less than or equal to 100 in ${sourceLabel}.`);
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
return value;
|
|
117
132
|
};
|
|
118
133
|
|
|
119
|
-
const readResponsivePercent = (object, key, path, fallback) => {
|
|
134
|
+
const readResponsivePercent = (object, key, path, fallback, sourceLabel = siteConfigLabel) => {
|
|
120
135
|
const value = object[key] ?? fallback;
|
|
121
136
|
|
|
122
137
|
if (typeof value === 'number') {
|
|
123
|
-
const percent = readPercentValue(value, `${path}.${key}
|
|
138
|
+
const percent = readPercentValue(value, `${path}.${key}`, sourceLabel);
|
|
124
139
|
|
|
125
140
|
return Object.freeze({
|
|
126
141
|
desktop: percent,
|
|
@@ -128,11 +143,11 @@ const readResponsivePercent = (object, key, path, fallback) => {
|
|
|
128
143
|
});
|
|
129
144
|
}
|
|
130
145
|
|
|
131
|
-
const responsiveValue = assertObject(value, `${path}.${key}
|
|
146
|
+
const responsiveValue = assertObject(value, `${path}.${key}`, sourceLabel);
|
|
132
147
|
|
|
133
148
|
return Object.freeze({
|
|
134
|
-
desktop: readPercentValue(responsiveValue.desktop ?? fallback.desktop, `${path}.${key}.desktop
|
|
135
|
-
mobile: readPercentValue(responsiveValue.mobile ?? fallback.mobile, `${path}.${key}.mobile
|
|
149
|
+
desktop: readPercentValue(responsiveValue.desktop ?? fallback.desktop, `${path}.${key}.desktop`, sourceLabel),
|
|
150
|
+
mobile: readPercentValue(responsiveValue.mobile ?? fallback.mobile, `${path}.${key}.mobile`, sourceLabel),
|
|
136
151
|
});
|
|
137
152
|
};
|
|
138
153
|
|
|
@@ -229,7 +244,7 @@ const readLocale = (rawLocale) => {
|
|
|
229
244
|
closeMenu: readString({ closeMenu: labels.closeMenu ?? 'Close menu' }, 'closeMenu', 'locale.labels'),
|
|
230
245
|
skipToContent: readString({ skipToContent: labels.skipToContent ?? 'Skip to content' }, 'skipToContent', 'locale.labels'),
|
|
231
246
|
sectionNavigation: readString({ sectionNavigation: labels.sectionNavigation ?? 'Sections' }, 'sectionNavigation', 'locale.labels'),
|
|
232
|
-
gallery: readString({ gallery: labels.gallery ?? '
|
|
247
|
+
gallery: readString({ gallery: labels.gallery ?? 'Images' }, 'gallery', 'locale.labels'),
|
|
233
248
|
menu: readString({ menu: labels.menu ?? 'Menu' }, 'menu', 'locale.labels'),
|
|
234
249
|
pageNavigation: readString({ pageNavigation: labels.pageNavigation ?? 'On this page' }, 'pageNavigation', 'locale.labels'),
|
|
235
250
|
siteNavigation: readString({ siteNavigation: labels.siteNavigation ?? 'Pages' }, 'siteNavigation', 'locale.labels'),
|
|
@@ -280,10 +295,17 @@ const readBuildInfo = (footer) => {
|
|
|
280
295
|
};
|
|
281
296
|
|
|
282
297
|
const rawConfig = assertObject(siteConfig, 'default export');
|
|
298
|
+
const misplacedThemeKeys = ['layout', 'gallery', 'typography'].filter((key) => key in rawConfig);
|
|
299
|
+
if (misplacedThemeKeys.length > 0) {
|
|
300
|
+
throw new Error(`${misplacedThemeKeys.join(', ')} belong in ${siteThemeLabel}, not ${siteConfigLabel}.`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const rawTheme = assertObject(themeConfig, 'theme frontmatter', siteThemeLabel);
|
|
283
304
|
const rawSite = assertObject(rawConfig.site, 'site');
|
|
284
|
-
const rawLayout = assertObject(
|
|
285
|
-
const
|
|
286
|
-
const
|
|
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);
|
|
287
309
|
const rawNavigation = assertObject(rawConfig.navigation ?? {}, 'navigation');
|
|
288
310
|
const rawLocale = rawConfig.locale ?? {};
|
|
289
311
|
const rawFooter = assertObject(rawConfig.footer ?? {}, 'footer');
|
|
@@ -292,6 +314,77 @@ const rawDeploy = assertObject(rawConfig.deploy ?? {}, 'deploy');
|
|
|
292
314
|
const rawDeployWatch = assertObject(rawDeploy.watch ?? {}, 'deploy.watch');
|
|
293
315
|
|
|
294
316
|
const defaultFontFamily = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
|
|
317
|
+
const layoutDensityNames = ['compact', 'normal', 'airy'];
|
|
318
|
+
const layoutDensityProfiles = Object.freeze({
|
|
319
|
+
compact: Object.freeze({
|
|
320
|
+
bodyToImages: Object.freeze({
|
|
321
|
+
desktop: '1rem',
|
|
322
|
+
mobile: '0.9rem',
|
|
323
|
+
}),
|
|
324
|
+
finalSectionBottom: Object.freeze({
|
|
325
|
+
desktop: 'clamp(1.2rem, 2.4vw, 2.25rem)',
|
|
326
|
+
mobile: '1.25rem',
|
|
327
|
+
}),
|
|
328
|
+
firstSectionTop: Object.freeze({
|
|
329
|
+
desktop: 'clamp(1rem, 2vw, 1.75rem)',
|
|
330
|
+
mobile: '1rem',
|
|
331
|
+
}),
|
|
332
|
+
imageGap: Object.freeze({
|
|
333
|
+
desktop: 'clamp(1rem, 2vw, 1.5rem)',
|
|
334
|
+
mobile: '1.25rem',
|
|
335
|
+
}),
|
|
336
|
+
sectionGap: Object.freeze({
|
|
337
|
+
desktop: 'clamp(1.2rem, 2.4vw, 2.25rem)',
|
|
338
|
+
mobile: '1.25rem',
|
|
339
|
+
}),
|
|
340
|
+
}),
|
|
341
|
+
normal: Object.freeze({
|
|
342
|
+
bodyToImages: Object.freeze({
|
|
343
|
+
desktop: '1.25rem',
|
|
344
|
+
mobile: '1rem',
|
|
345
|
+
}),
|
|
346
|
+
finalSectionBottom: Object.freeze({
|
|
347
|
+
desktop: 'clamp(1.4rem, 3vw, 2.75rem)',
|
|
348
|
+
mobile: '1.5rem',
|
|
349
|
+
}),
|
|
350
|
+
firstSectionTop: Object.freeze({
|
|
351
|
+
desktop: 'clamp(1.25rem, 3vw, 2.5rem)',
|
|
352
|
+
mobile: '1.25rem',
|
|
353
|
+
}),
|
|
354
|
+
imageGap: Object.freeze({
|
|
355
|
+
desktop: 'clamp(1.25rem, 2.8vw, 2rem)',
|
|
356
|
+
mobile: '1.5rem',
|
|
357
|
+
}),
|
|
358
|
+
sectionGap: Object.freeze({
|
|
359
|
+
desktop: 'clamp(1.4rem, 3vw, 2.75rem)',
|
|
360
|
+
mobile: '1.5rem',
|
|
361
|
+
}),
|
|
362
|
+
}),
|
|
363
|
+
airy: Object.freeze({
|
|
364
|
+
bodyToImages: Object.freeze({
|
|
365
|
+
desktop: '1.5rem',
|
|
366
|
+
mobile: '1.25rem',
|
|
367
|
+
}),
|
|
368
|
+
finalSectionBottom: Object.freeze({
|
|
369
|
+
desktop: 'clamp(2.25rem, 5vw, 4.5rem)',
|
|
370
|
+
mobile: '2rem',
|
|
371
|
+
}),
|
|
372
|
+
firstSectionTop: Object.freeze({
|
|
373
|
+
desktop: 'clamp(2rem, 5vw, 4rem)',
|
|
374
|
+
mobile: '1.5rem',
|
|
375
|
+
}),
|
|
376
|
+
imageGap: Object.freeze({
|
|
377
|
+
desktop: 'clamp(1.5rem, 3.5vw, 2.75rem)',
|
|
378
|
+
mobile: '2rem',
|
|
379
|
+
}),
|
|
380
|
+
sectionGap: Object.freeze({
|
|
381
|
+
desktop: 'clamp(2.25rem, 5vw, 4.5rem)',
|
|
382
|
+
mobile: '2rem',
|
|
383
|
+
}),
|
|
384
|
+
}),
|
|
385
|
+
});
|
|
386
|
+
const layoutDensity = readEnum(rawLayout, 'density', 'layout', layoutDensityNames, 'normal', siteThemeLabel);
|
|
387
|
+
const layoutSpacingDefaults = layoutDensityProfiles[layoutDensity];
|
|
295
388
|
|
|
296
389
|
export const projectConfig = Object.freeze({
|
|
297
390
|
site: Object.freeze({
|
|
@@ -299,31 +392,39 @@ export const projectConfig = Object.freeze({
|
|
|
299
392
|
url: readUrl(rawSite, 'url', 'site'),
|
|
300
393
|
}),
|
|
301
394
|
layout: Object.freeze({
|
|
395
|
+
density: layoutDensity,
|
|
302
396
|
gutter: readResponsiveCssLength(rawLayout, 'gutter', 'layout', Object.freeze({
|
|
303
397
|
desktop: 'clamp(1.25rem, 4vw, 3rem)',
|
|
304
398
|
mobile: '1rem',
|
|
305
|
-
})),
|
|
306
|
-
pageWidth: readCssLength(rawLayout, 'pageWidth', 'layout', '1180px'),
|
|
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
|
+
}),
|
|
307
408
|
}),
|
|
308
409
|
gallery: Object.freeze({
|
|
309
410
|
maxAvailableHeightPercent: readResponsivePercent(rawGallery, 'maxAvailableHeightPercent', 'gallery', Object.freeze({
|
|
310
411
|
desktop: 74,
|
|
311
412
|
mobile: 68,
|
|
312
|
-
})),
|
|
413
|
+
}), siteThemeLabel),
|
|
313
414
|
maxAvailableWidthPercent: readResponsivePercent(rawGallery, 'maxAvailableWidthPercent', 'gallery', Object.freeze({
|
|
314
415
|
desktop: 100,
|
|
315
416
|
mobile: 100,
|
|
316
|
-
})),
|
|
317
|
-
width: readCssLength(rawGallery, 'width', 'gallery', '900px'),
|
|
417
|
+
}), siteThemeLabel),
|
|
418
|
+
width: readCssLength(rawGallery, 'width', 'gallery', '900px', siteThemeLabel),
|
|
318
419
|
}),
|
|
319
420
|
typography: Object.freeze({
|
|
320
|
-
fontFamily: readFontFamily(rawTypography, 'fontFamily', 'typography', defaultFontFamily),
|
|
421
|
+
fontFamily: readFontFamily(rawTypography, 'fontFamily', 'typography', defaultFontFamily, siteThemeLabel),
|
|
321
422
|
}),
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
423
|
+
navigation: Object.freeze({
|
|
424
|
+
smoothScroll: readSmoothScroll(rawNavigation),
|
|
425
|
+
}),
|
|
426
|
+
locale: readLocale(rawLocale),
|
|
427
|
+
footer: Object.freeze({
|
|
327
428
|
buildInfo: readBuildInfo(rawFooter),
|
|
328
429
|
copyrightMessage: readOptionalString(rawFooter, 'copyrightMessage', 'footer'),
|
|
329
430
|
}),
|