@janga/norna 0.7.0 → 0.7.2
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 +18 -4
- package/astro.config.mjs +2 -0
- package/bin/norna-cli.mjs +170 -0
- package/bin/norna.mjs +149 -150
- package/docs/README.md +36 -16
- package/docs/commands.md +18 -5
- package/docs/configuration.md +37 -2
- package/docs/content.md +98 -257
- package/docs/{command-organization.md → design/command-organization.md} +9 -5
- package/docs/{site-examples-structure-note.md → design/site-examples-structure.md} +17 -22
- package/docs/engine-development.md +33 -5
- package/docs/getting-started.md +40 -4
- package/docs/local-development.md +13 -0
- package/docs/publishing.md +23 -0
- package/docs/routes.md +90 -0
- package/docs/site-structure.md +15 -8
- package/docs/theme.md +150 -0
- package/docs/typography.md +125 -0
- package/examples/dog-gallery/site/.norna/generated-images.json +226 -0
- package/examples/dog-gallery/site/config.mjs +97 -0
- package/examples/dog-gallery/site/content.md +146 -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 +35 -0
- package/examples/dog-gallery/site/theme.md +55 -0
- package/fixtures/basic/site/config.mjs +1 -0
- package/package.json +8 -7
- package/scripts/check-config.mjs +1 -0
- package/scripts/dev-local.mjs +64 -15
- package/scripts/init-site.mjs +1 -1
- package/scripts/lib/project-config.mjs +22 -0
- package/scripts/lib/site-content.mjs +2 -1
- package/scripts/lib/site-paths.mjs +18 -3
- package/scripts/lib/typography.mjs +5 -5
- package/scripts/show-typography.mjs +252 -29
- package/scripts/test-cli-discovery.mjs +124 -0
- package/scripts/test-engine-commands.mjs +18 -4
- package/scripts/test-navigation.mjs +10 -6
- package/scripts/test-package-check.mjs +32 -4
- package/src/components/SiteNavigation.astro +7 -5
- package/src/components/SitePage.astro +3 -0
- package/src/components/SiteSection.astro +24 -24
- package/src/content.config.ts +4 -0
- package/src/layouts/BaseLayout.astro +2 -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 +8 -8
- package/starters/basic/.github/workflows/deploy.yml +3 -3
- package/starters/basic/README.md +21 -0
- package/starters/basic/package.json +1 -1
- package/starters/basic/site/config.mjs +1 -0
- package/starters/basic/site/content.md +5 -3
- package/starters/basic/site/theme.md +4 -0
package/scripts/dev-local.mjs
CHANGED
|
@@ -5,18 +5,26 @@ import { networkInterfaces } from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { getAstroArgs, runAstroInherit } from './lib/astro-command.mjs';
|
|
8
|
+
import projectConfig from './lib/project-config.mjs';
|
|
8
9
|
import { astroCacheDir, engineRoot, siteProjectRoot } from './lib/site-paths.mjs';
|
|
9
10
|
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
|
|
12
13
|
const port = 4321;
|
|
13
|
-
const localHost = '
|
|
14
|
-
const localUrl = `http://${localHost}:${port}
|
|
15
|
-
const probeUrls = [
|
|
14
|
+
const localHost = '127.0.0.1';
|
|
15
|
+
const localUrl = `http://${localHost}:${port}${projectConfig.site.basePath}`;
|
|
16
|
+
const probeUrls = [
|
|
17
|
+
localUrl,
|
|
18
|
+
`http://localhost:${port}${projectConfig.site.basePath}`,
|
|
19
|
+
];
|
|
16
20
|
const skipOpen = process.env.WALDE_NO_OPEN === '1';
|
|
17
21
|
const statePath = path.join(astroCacheDir, 'dev-local.json');
|
|
18
22
|
const logPath = path.join(astroCacheDir, 'dev.log');
|
|
19
|
-
const
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const knownCommands = new Set(['start', 'lan', 'status', 'logs', 'restart', 'stop']);
|
|
25
|
+
const command = args.find((arg) => knownCommands.has(arg)) ?? args.find((arg) => !arg.startsWith('-')) ?? 'start';
|
|
26
|
+
const shouldKillBlockingPort = args.includes('--kill');
|
|
27
|
+
const shouldFollowLogs = args.includes('--follow');
|
|
20
28
|
|
|
21
29
|
const runAstro = async (args, options = {}) => execFileAsync(process.execPath, getAstroArgs(args), {
|
|
22
30
|
cwd: siteProjectRoot,
|
|
@@ -57,7 +65,7 @@ const readState = async () => {
|
|
|
57
65
|
const getLanUrls = () => Object.values(networkInterfaces())
|
|
58
66
|
.flat()
|
|
59
67
|
.filter((network) => network?.family === 'IPv4' && !network.internal)
|
|
60
|
-
.map((network) => `http://${network.address}:${port}
|
|
68
|
+
.map((network) => `http://${network.address}:${port}${projectConfig.site.basePath}`);
|
|
61
69
|
|
|
62
70
|
const writeState = async (pid, host) => {
|
|
63
71
|
await mkdir(path.dirname(statePath), { recursive: true });
|
|
@@ -98,7 +106,7 @@ const isPortFreeOnHost = (loopbackHost) => new Promise((resolve, reject) => {
|
|
|
98
106
|
});
|
|
99
107
|
|
|
100
108
|
const isPortFree = async () => {
|
|
101
|
-
for (const loopbackHost of ['127.0.0.1'
|
|
109
|
+
for (const loopbackHost of ['127.0.0.1']) {
|
|
102
110
|
if (!(await isPortFreeOnHost(loopbackHost))) {
|
|
103
111
|
return false;
|
|
104
112
|
}
|
|
@@ -155,6 +163,35 @@ const waitForPidToStopListening = async (pid) => {
|
|
|
155
163
|
return false;
|
|
156
164
|
};
|
|
157
165
|
|
|
166
|
+
const killPortPids = async (pids) => {
|
|
167
|
+
for (const pid of pids) {
|
|
168
|
+
console.log(`Stopping process ${pid} on port ${port}.`);
|
|
169
|
+
try {
|
|
170
|
+
process.kill(pid, 'SIGTERM');
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error.code !== 'ESRCH') {
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const pid of pids) {
|
|
179
|
+
if (await waitForPidToStopListening(pid)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`Process ${pid} did not stop; sending SIGKILL.`);
|
|
184
|
+
try {
|
|
185
|
+
process.kill(pid, 'SIGKILL');
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error.code !== 'ESRCH') {
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
await waitForPidToStopListening(pid);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
158
195
|
const openBrowser = async () => {
|
|
159
196
|
if (skipOpen) {
|
|
160
197
|
console.log(`Browser open skipped. Open ${localUrl}`);
|
|
@@ -210,12 +247,22 @@ const stopServer = async ({ quiet = false } = {}) => {
|
|
|
210
247
|
}
|
|
211
248
|
};
|
|
212
249
|
|
|
213
|
-
const startServer = async ({ host = localHost, open = true } = {}) => {
|
|
250
|
+
const startServer = async ({ host = localHost, open = true, killBlockingPort = false } = {}) => {
|
|
214
251
|
await stopServer({ quiet: true });
|
|
215
252
|
|
|
216
|
-
|
|
253
|
+
let existingPids = await getPortPids();
|
|
217
254
|
if (existingPids.length > 0 || !(await isPortFree())) {
|
|
218
|
-
|
|
255
|
+
if (!killBlockingPort || existingPids.length === 0) {
|
|
256
|
+
throw new Error(`Port ${port} is already in use. Stop the process using it, then rerun the dev command, or pass --kill.`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await killPortPids(existingPids);
|
|
260
|
+
await removeState();
|
|
261
|
+
existingPids = await getPortPids();
|
|
262
|
+
|
|
263
|
+
if (existingPids.length > 0 || !(await isPortFree())) {
|
|
264
|
+
throw new Error(`Port ${port} is still in use after trying to stop ${existingPids.join(', ')}.`);
|
|
265
|
+
}
|
|
219
266
|
}
|
|
220
267
|
|
|
221
268
|
await syncSitePublic();
|
|
@@ -271,9 +318,7 @@ const showStatus = async () => {
|
|
|
271
318
|
};
|
|
272
319
|
|
|
273
320
|
const showLogs = async () => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (shouldFollow) {
|
|
321
|
+
if (shouldFollowLogs) {
|
|
277
322
|
const tail = spawn('tail', ['-n', '80', '-f', logPath], { stdio: 'inherit' });
|
|
278
323
|
await new Promise((resolve, reject) => {
|
|
279
324
|
tail.once('exit', resolve);
|
|
@@ -296,16 +341,20 @@ const showLogs = async () => {
|
|
|
296
341
|
};
|
|
297
342
|
|
|
298
343
|
if (command === 'start') {
|
|
299
|
-
await startServer({ open: !skipOpen });
|
|
344
|
+
await startServer({ open: !skipOpen, killBlockingPort: shouldKillBlockingPort });
|
|
300
345
|
} else if (command === 'lan') {
|
|
301
|
-
await startServer({ host: '0.0.0.0', open: !skipOpen });
|
|
346
|
+
await startServer({ host: '0.0.0.0', open: !skipOpen, killBlockingPort: shouldKillBlockingPort });
|
|
302
347
|
} else if (command === 'status') {
|
|
303
348
|
await showStatus();
|
|
304
349
|
} else if (command === 'logs') {
|
|
305
350
|
await showLogs();
|
|
306
351
|
} else if (command === 'restart') {
|
|
307
352
|
const state = await readState();
|
|
308
|
-
await startServer({
|
|
353
|
+
await startServer({
|
|
354
|
+
host: state?.host === '0.0.0.0' ? '0.0.0.0' : localHost,
|
|
355
|
+
open: false,
|
|
356
|
+
killBlockingPort: shouldKillBlockingPort,
|
|
357
|
+
});
|
|
309
358
|
} else if (command === 'stop') {
|
|
310
359
|
await stopServer();
|
|
311
360
|
} else {
|
package/scripts/init-site.mjs
CHANGED
|
@@ -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
|
};
|
|
@@ -176,6 +176,27 @@ const readUrl = (object, key, path) => {
|
|
|
176
176
|
}
|
|
177
177
|
};
|
|
178
178
|
|
|
179
|
+
const readBasePath = (object, key, path) => {
|
|
180
|
+
const value = object[key] ?? '/';
|
|
181
|
+
|
|
182
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
183
|
+
throw new Error(`${path}.${key} must be a non-empty URL path in ${siteConfigLabel}.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const normalizedValue = value.trim();
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
!normalizedValue.startsWith('/')
|
|
190
|
+
|| !normalizedValue.endsWith('/')
|
|
191
|
+
|| normalizedValue.includes('//')
|
|
192
|
+
|| /[\s?#]/.test(normalizedValue)
|
|
193
|
+
) {
|
|
194
|
+
throw new Error(`${path}.${key} must start and end with "/" and must not contain whitespace, "?", "#", or "//" in ${siteConfigLabel}.`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return normalizedValue;
|
|
198
|
+
};
|
|
199
|
+
|
|
179
200
|
const readSmoothScroll = (navigation) => {
|
|
180
201
|
const rawSmoothScroll = assertObject(navigation.smoothScroll ?? {}, 'navigation.smoothScroll');
|
|
181
202
|
const minimumDurationMs = readPositiveInteger(rawSmoothScroll, 'minimumDurationMs', 'navigation.smoothScroll', 2_000);
|
|
@@ -274,6 +295,7 @@ const defaultFontFamily = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
|
|
|
274
295
|
|
|
275
296
|
export const projectConfig = Object.freeze({
|
|
276
297
|
site: Object.freeze({
|
|
298
|
+
basePath: readBasePath(rawSite, 'basePath', 'site'),
|
|
277
299
|
url: readUrl(rawSite, 'url', 'site'),
|
|
278
300
|
}),
|
|
279
301
|
layout: Object.freeze({
|
|
@@ -18,12 +18,13 @@ 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', 'presentation', 'frame']);
|
|
22
22
|
const knownNestedFrontmatterKeys = new Set([
|
|
23
23
|
'align',
|
|
24
24
|
'alt',
|
|
25
25
|
'backgroundColor',
|
|
26
26
|
'body',
|
|
27
|
+
'brand',
|
|
27
28
|
'caption',
|
|
28
29
|
'carousel',
|
|
29
30
|
'color',
|
|
@@ -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
|
|
|
@@ -29,13 +29,13 @@ export const typographyPresets = {
|
|
|
29
29
|
'compact-gallery': {
|
|
30
30
|
heading: {
|
|
31
31
|
align: { desktop: 'left', mobile: 'left' },
|
|
32
|
-
size: '
|
|
32
|
+
size: 'medium',
|
|
33
33
|
lineHeight: 1.08,
|
|
34
34
|
spacing: '0.45em',
|
|
35
35
|
},
|
|
36
36
|
body: {
|
|
37
37
|
align: { desktop: 'left', mobile: 'left' },
|
|
38
|
-
size: '
|
|
38
|
+
size: 'medium',
|
|
39
39
|
lineHeight: 1.42,
|
|
40
40
|
paragraphSpacing: '0.6em',
|
|
41
41
|
},
|
|
@@ -55,7 +55,7 @@ export const typographyPresets = {
|
|
|
55
55
|
},
|
|
56
56
|
body: {
|
|
57
57
|
align: { desktop: 'left', mobile: 'left' },
|
|
58
|
-
size: '
|
|
58
|
+
size: 'medium',
|
|
59
59
|
lineHeight: 1.62,
|
|
60
60
|
paragraphSpacing: '1em',
|
|
61
61
|
},
|
|
@@ -69,13 +69,13 @@ export const typographyPresets = {
|
|
|
69
69
|
statement: {
|
|
70
70
|
heading: {
|
|
71
71
|
align: { desktop: 'left', mobile: 'left' },
|
|
72
|
-
size: '
|
|
72
|
+
size: 'medium',
|
|
73
73
|
lineHeight: 1.04,
|
|
74
74
|
spacing: '0.5em',
|
|
75
75
|
},
|
|
76
76
|
body: {
|
|
77
77
|
align: { desktop: 'left', mobile: 'left' },
|
|
78
|
-
size: '
|
|
78
|
+
size: 'medium',
|
|
79
79
|
lineHeight: 1.42,
|
|
80
80
|
paragraphSpacing: '0.75em',
|
|
81
81
|
},
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
typographyPresets,
|
|
8
8
|
} from './lib/typography.mjs';
|
|
9
9
|
import {
|
|
10
|
+
getContentFiles,
|
|
11
|
+
readSiteFile,
|
|
10
12
|
splitSiteFile,
|
|
11
13
|
validateContentFrontmatterStructure,
|
|
12
14
|
validateFrontmatterIndentation,
|
|
@@ -21,6 +23,14 @@ import {
|
|
|
21
23
|
|
|
22
24
|
const mode = process.argv[2] ?? 'show';
|
|
23
25
|
|
|
26
|
+
const typographyRoles = ['heading', 'body', 'caption'];
|
|
27
|
+
const typographyFields = {
|
|
28
|
+
heading: ['align', 'size', 'lineHeight', 'spacing'],
|
|
29
|
+
body: ['align', 'size', 'lineHeight', 'paragraphSpacing'],
|
|
30
|
+
caption: ['align', 'size', 'lineHeight', 'spacing'],
|
|
31
|
+
};
|
|
32
|
+
const responsiveFields = new Set(['align']);
|
|
33
|
+
|
|
24
34
|
const countIndent = (line) => line.match(/^\s*/)?.[0].length ?? 0;
|
|
25
35
|
|
|
26
36
|
const parseScalar = (rawValue) => {
|
|
@@ -83,6 +93,213 @@ const findMap = (lines, label, parentStart = 0, parentEnd = lines.length, requir
|
|
|
83
93
|
return null;
|
|
84
94
|
};
|
|
85
95
|
|
|
96
|
+
const isPlainObject = (value) => (
|
|
97
|
+
value !== null &&
|
|
98
|
+
typeof value === 'object' &&
|
|
99
|
+
!Array.isArray(value)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const hasPath = (value, path) => {
|
|
103
|
+
let current = value;
|
|
104
|
+
|
|
105
|
+
for (const segment of path) {
|
|
106
|
+
if (!isPlainObject(current) || !(segment in current)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
current = current[segment];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const getPath = (value, path) => path.reduce((current, segment) => current?.[segment], value);
|
|
117
|
+
|
|
118
|
+
const setPath = (value, path, entry) => {
|
|
119
|
+
let current = value;
|
|
120
|
+
|
|
121
|
+
for (const segment of path.slice(0, -1)) {
|
|
122
|
+
current[segment] ??= {};
|
|
123
|
+
current = current[segment];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
current[path.at(-1)] = entry;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const annotateResolvedValues = (resolved, sources) => {
|
|
130
|
+
const annotated = {};
|
|
131
|
+
|
|
132
|
+
for (const role of typographyRoles) {
|
|
133
|
+
for (const field of typographyFields[role]) {
|
|
134
|
+
if (responsiveFields.has(field)) {
|
|
135
|
+
for (const viewport of ['desktop', 'mobile']) {
|
|
136
|
+
const path = [role, field, viewport];
|
|
137
|
+
const source = getPath(sources, path);
|
|
138
|
+
setPath(annotated, path, {
|
|
139
|
+
value: getPath(resolved.values, path),
|
|
140
|
+
source: source.source,
|
|
141
|
+
...(source.inherited ? { inherited: true } : {}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const path = [role, field];
|
|
148
|
+
const source = getPath(sources, path);
|
|
149
|
+
setPath(annotated, path, {
|
|
150
|
+
value: getPath(resolved.values, path),
|
|
151
|
+
source: source.source,
|
|
152
|
+
...(source.inherited ? { inherited: true } : {}),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return annotated;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const presetSources = (presetName) => {
|
|
161
|
+
const sources = {};
|
|
162
|
+
|
|
163
|
+
for (const role of typographyRoles) {
|
|
164
|
+
for (const field of typographyFields[role]) {
|
|
165
|
+
if (responsiveFields.has(field)) {
|
|
166
|
+
for (const viewport of ['desktop', 'mobile']) {
|
|
167
|
+
setPath(sources, [role, field, viewport], {
|
|
168
|
+
source: `preset:${presetName}`,
|
|
169
|
+
inherited: false,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setPath(sources, [role, field], {
|
|
176
|
+
source: `preset:${presetName}`,
|
|
177
|
+
inherited: false,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return sources;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const applyOverrideSources = (sources, typographyConfig, sourceLabel) => {
|
|
186
|
+
const overrides = typographyConfig?.overrides;
|
|
187
|
+
if (!overrides) return sources;
|
|
188
|
+
|
|
189
|
+
for (const role of typographyRoles) {
|
|
190
|
+
for (const field of typographyFields[role]) {
|
|
191
|
+
if (responsiveFields.has(field)) {
|
|
192
|
+
for (const viewport of ['desktop', 'mobile']) {
|
|
193
|
+
const path = [role, field, viewport];
|
|
194
|
+
if (hasPath(overrides, path)) {
|
|
195
|
+
setPath(sources, path, {
|
|
196
|
+
source: `${sourceLabel} override`,
|
|
197
|
+
inherited: false,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const path = [role, field];
|
|
205
|
+
if (hasPath(overrides, path)) {
|
|
206
|
+
setPath(sources, path, {
|
|
207
|
+
source: `${sourceLabel} override`,
|
|
208
|
+
inherited: false,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return sources;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const inheritedSources = (sources) => {
|
|
218
|
+
const inherited = structuredClone(sources);
|
|
219
|
+
|
|
220
|
+
for (const role of typographyRoles) {
|
|
221
|
+
for (const field of typographyFields[role]) {
|
|
222
|
+
if (responsiveFields.has(field)) {
|
|
223
|
+
for (const viewport of ['desktop', 'mobile']) {
|
|
224
|
+
const path = [role, field, viewport];
|
|
225
|
+
setPath(inherited, path, {
|
|
226
|
+
...getPath(sources, path),
|
|
227
|
+
inherited: true,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const path = [role, field];
|
|
234
|
+
setPath(inherited, path, {
|
|
235
|
+
...getPath(sources, path),
|
|
236
|
+
inherited: true,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return inherited;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const resolveAnnotatedTypographyConfig = (typographyConfig, sourceLabel) => {
|
|
245
|
+
const resolved = resolveTypographyConfig(typographyConfig ?? defaultTypography);
|
|
246
|
+
const sources = applyOverrideSources(
|
|
247
|
+
presetSources(resolved.preset),
|
|
248
|
+
typographyConfig,
|
|
249
|
+
sourceLabel,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
preset: {
|
|
254
|
+
value: resolved.preset,
|
|
255
|
+
source: typographyConfig?.preset ? sourceLabel : 'engine default',
|
|
256
|
+
...(typographyConfig?.preset ? {} : { inherited: true }),
|
|
257
|
+
},
|
|
258
|
+
resolved,
|
|
259
|
+
sources,
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sourceLabel) => {
|
|
264
|
+
if (typographyConfig?.preset) {
|
|
265
|
+
return resolveAnnotatedTypographyConfig(typographyConfig, sourceLabel);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (typographyConfig?.overrides) {
|
|
269
|
+
const resolved = resolveTypographyOverride(baseAnnotated.resolved, typographyConfig);
|
|
270
|
+
const sources = applyOverrideSources(
|
|
271
|
+
inheritedSources(baseAnnotated.sources),
|
|
272
|
+
typographyConfig,
|
|
273
|
+
sourceLabel,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
preset: {
|
|
278
|
+
value: resolved.preset,
|
|
279
|
+
source: baseAnnotated.preset.source,
|
|
280
|
+
inherited: true,
|
|
281
|
+
},
|
|
282
|
+
resolved,
|
|
283
|
+
sources,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
preset: {
|
|
289
|
+
value: baseAnnotated.resolved.preset,
|
|
290
|
+
source: baseAnnotated.preset.source,
|
|
291
|
+
inherited: true,
|
|
292
|
+
},
|
|
293
|
+
resolved: baseAnnotated.resolved,
|
|
294
|
+
sources: inheritedSources(baseAnnotated.sources),
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const formatAnnotatedTypography = (annotated) => ({
|
|
299
|
+
preset: annotated.preset,
|
|
300
|
+
resolved: annotateResolvedValues(annotated.resolved, annotated.sources),
|
|
301
|
+
});
|
|
302
|
+
|
|
86
303
|
const getSectionBlocks = (lines) => {
|
|
87
304
|
const sectionsMap = findMap(lines, 'sections');
|
|
88
305
|
if (!sectionsMap) return [];
|
|
@@ -118,8 +335,7 @@ const getSectionBlocks = (lines) => {
|
|
|
118
335
|
return sections;
|
|
119
336
|
};
|
|
120
337
|
|
|
121
|
-
const
|
|
122
|
-
const { frontmatter, frontmatterBody } = splitSiteFile(await readFile(siteContentPath, 'utf8'));
|
|
338
|
+
const readThemeTypography = async () => {
|
|
123
339
|
const themeFile = await readFile(siteThemePath, 'utf8').catch((error) => {
|
|
124
340
|
if (error?.code === 'ENOENT') {
|
|
125
341
|
return '---\n---\n';
|
|
@@ -129,29 +345,41 @@ const readSiteTypography = async () => {
|
|
|
129
345
|
});
|
|
130
346
|
const { frontmatter: themeFrontmatter, frontmatterBody: themeFrontmatterBody } = splitSiteFile(themeFile, siteThemeLabel);
|
|
131
347
|
const indentationIssues = [];
|
|
132
|
-
validateFrontmatterIndentation(frontmatter, (issue) => indentationIssues.push(issue));
|
|
133
|
-
validateContentFrontmatterStructure(frontmatter, (issue) => indentationIssues.push(issue));
|
|
134
348
|
validateFrontmatterIndentation(themeFrontmatter, (issue) => indentationIssues.push(issue));
|
|
135
349
|
validateThemeFrontmatterStructure(themeFrontmatter, (issue) => indentationIssues.push(issue));
|
|
136
350
|
if (indentationIssues.length > 0) {
|
|
137
351
|
throw new Error([
|
|
138
|
-
`Cannot inspect typography because ${
|
|
352
|
+
`Cannot inspect typography because ${siteThemeLabel} has invalid frontmatter.`,
|
|
139
353
|
...indentationIssues.map((issue) => `- ${issue.message}`),
|
|
140
354
|
].join('\n'));
|
|
141
355
|
}
|
|
142
356
|
|
|
143
|
-
const lines = frontmatterBody.split(/\r?\n/);
|
|
144
357
|
const themeLines = themeFrontmatterBody.split(/\r?\n/);
|
|
145
358
|
const themePresentation = findMap(themeLines, 'presentation', 0, themeLines.length, 0);
|
|
146
359
|
const themeTypographyConfig = themePresentation
|
|
147
360
|
? findMap(themeLines, 'typography', themePresentation.index + 1, themePresentation.nextIndex)?.value
|
|
148
361
|
: null;
|
|
362
|
+
return resolveAnnotatedTypographyConfig(themeTypographyConfig ?? defaultTypography, siteThemeLabel);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const readPageTypography = async (contentFile, themeTypography) => {
|
|
366
|
+
const { frontmatter, frontmatterBody } = await readSiteFile(contentFile.contentPath, contentFile.contentLabel);
|
|
367
|
+
const indentationIssues = [];
|
|
368
|
+
validateFrontmatterIndentation(frontmatter, (issue) => indentationIssues.push(issue));
|
|
369
|
+
validateContentFrontmatterStructure(frontmatter, (issue) => indentationIssues.push(issue));
|
|
370
|
+
if (indentationIssues.length > 0) {
|
|
371
|
+
throw new Error([
|
|
372
|
+
`Cannot inspect typography because ${contentFile.contentLabel} has invalid frontmatter.`,
|
|
373
|
+
...indentationIssues.map((issue) => `- ${issue.message}`),
|
|
374
|
+
].join('\n'));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const lines = frontmatterBody.split(/\r?\n/);
|
|
149
378
|
const pagePresentation = findMap(lines, 'presentation', 0, lines.length, 0);
|
|
150
379
|
const pageTypographyConfig = pagePresentation
|
|
151
380
|
? findMap(lines, 'typography', pagePresentation.index + 1, pagePresentation.nextIndex)?.value
|
|
152
381
|
: null;
|
|
153
|
-
const
|
|
154
|
-
const pageTypography = resolveTypographyOverride(themeTypography, pageTypographyConfig ?? undefined);
|
|
382
|
+
const pageTypography = resolveAnnotatedTypographyOverride(themeTypography, pageTypographyConfig ?? undefined, contentFile.contentLabel);
|
|
155
383
|
const sections = getSectionBlocks(lines).map((section) => {
|
|
156
384
|
const presentation = findMap(lines, 'presentation', section.start + 1, section.end);
|
|
157
385
|
const typography = presentation
|
|
@@ -160,13 +388,13 @@ const readSiteTypography = async () => {
|
|
|
160
388
|
|
|
161
389
|
return {
|
|
162
390
|
id: section.id,
|
|
163
|
-
typography,
|
|
164
|
-
resolved: resolveTypographyOverride(pageTypography, typography ?? undefined),
|
|
391
|
+
typography: resolveAnnotatedTypographyOverride(pageTypography, typography ?? undefined, `${contentFile.contentLabel} sections.${section.id}`),
|
|
165
392
|
};
|
|
166
393
|
});
|
|
167
394
|
|
|
168
395
|
return {
|
|
169
|
-
|
|
396
|
+
source: contentFile.contentLabel,
|
|
397
|
+
route: contentFile.isHome ? '/' : `/${contentFile.routeFolder}/`,
|
|
170
398
|
pageTypography,
|
|
171
399
|
sections,
|
|
172
400
|
};
|
|
@@ -175,31 +403,26 @@ const readSiteTypography = async () => {
|
|
|
175
403
|
if (mode === 'presets') {
|
|
176
404
|
console.log(toYamlLines(typographyPresets).join('\n'));
|
|
177
405
|
} else if (mode === 'show') {
|
|
178
|
-
const
|
|
406
|
+
const themeTypography = await readThemeTypography();
|
|
407
|
+
const pages = await Promise.all((await getContentFiles()).map((contentFile) => readPageTypography(contentFile, themeTypography)));
|
|
179
408
|
const output = {
|
|
180
|
-
source: siteContentLabel,
|
|
181
409
|
theme: {
|
|
182
410
|
source: siteThemeLabel,
|
|
183
411
|
presentation: {
|
|
184
|
-
typography:
|
|
185
|
-
preset: siteTypography.themeTypography.preset,
|
|
186
|
-
resolved: siteTypography.themeTypography.values,
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
page: {
|
|
191
|
-
typography: {
|
|
192
|
-
preset: siteTypography.pageTypography.preset,
|
|
193
|
-
resolved: siteTypography.pageTypography.values,
|
|
412
|
+
typography: formatAnnotatedTypography(themeTypography),
|
|
194
413
|
},
|
|
195
414
|
},
|
|
196
|
-
|
|
197
|
-
|
|
415
|
+
pages: Object.fromEntries(pages.map((page) => [
|
|
416
|
+
page.route,
|
|
198
417
|
{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
418
|
+
source: page.source,
|
|
419
|
+
typography: formatAnnotatedTypography(page.pageTypography),
|
|
420
|
+
sections: Object.fromEntries(page.sections.map((section) => [
|
|
421
|
+
section.id,
|
|
422
|
+
{
|
|
423
|
+
typography: formatAnnotatedTypography(section.typography),
|
|
424
|
+
},
|
|
425
|
+
])),
|
|
203
426
|
},
|
|
204
427
|
])),
|
|
205
428
|
};
|