@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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { findMap } from './lib/frontmatter-yaml.mjs';
|
|
2
3
|
import {
|
|
3
4
|
defaultTypography,
|
|
4
5
|
resolveTypographyOverride,
|
|
5
6
|
resolveTypographyConfig,
|
|
6
7
|
toYamlLines,
|
|
7
8
|
typographyPresets,
|
|
9
|
+
typographyRhythms,
|
|
8
10
|
} from './lib/typography.mjs';
|
|
9
11
|
import {
|
|
10
12
|
getContentFiles,
|
|
@@ -23,76 +25,56 @@ import {
|
|
|
23
25
|
|
|
24
26
|
const mode = process.argv[2] ?? 'show';
|
|
25
27
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const typographyValuePaths = [
|
|
29
|
+
['headings', 'h1', 'align', 'desktop'],
|
|
30
|
+
['headings', 'h1', 'align', 'mobile'],
|
|
31
|
+
['headings', 'h1', 'size'],
|
|
32
|
+
['headings', 'h1', 'lineHeight'],
|
|
33
|
+
['headings', 'h1', 'spacingBefore'],
|
|
34
|
+
['headings', 'h1', 'spacingAfter'],
|
|
35
|
+
['headings', 'h2', 'align', 'desktop'],
|
|
36
|
+
['headings', 'h2', 'align', 'mobile'],
|
|
37
|
+
['headings', 'h2', 'size'],
|
|
38
|
+
['headings', 'h2', 'lineHeight'],
|
|
39
|
+
['headings', 'h2', 'spacingBefore'],
|
|
40
|
+
['headings', 'h2', 'spacingAfter'],
|
|
41
|
+
['headings', 'h3', 'align', 'desktop'],
|
|
42
|
+
['headings', 'h3', 'align', 'mobile'],
|
|
43
|
+
['headings', 'h3', 'size'],
|
|
44
|
+
['headings', 'h3', 'lineHeight'],
|
|
45
|
+
['headings', 'h3', 'spacingBefore'],
|
|
46
|
+
['headings', 'h3', 'spacingAfter'],
|
|
47
|
+
['headings', 'h4', 'align', 'desktop'],
|
|
48
|
+
['headings', 'h4', 'align', 'mobile'],
|
|
49
|
+
['headings', 'h4', 'size'],
|
|
50
|
+
['headings', 'h4', 'lineHeight'],
|
|
51
|
+
['headings', 'h4', 'spacingBefore'],
|
|
52
|
+
['headings', 'h4', 'spacingAfter'],
|
|
53
|
+
['body', 'align', 'desktop'],
|
|
54
|
+
['body', 'align', 'mobile'],
|
|
55
|
+
['body', 'size'],
|
|
56
|
+
['body', 'lineHeight'],
|
|
57
|
+
['body', 'paragraphSpacing'],
|
|
58
|
+
['caption', 'align', 'desktop'],
|
|
59
|
+
['caption', 'align', 'mobile'],
|
|
60
|
+
['caption', 'size'],
|
|
61
|
+
['caption', 'lineHeight'],
|
|
62
|
+
['caption', 'spacingBefore'],
|
|
63
|
+
];
|
|
64
|
+
const rhythmValuePaths = new Set([
|
|
65
|
+
'headings.h1.spacingBefore',
|
|
66
|
+
'headings.h1.spacingAfter',
|
|
67
|
+
'headings.h2.spacingBefore',
|
|
68
|
+
'headings.h2.spacingAfter',
|
|
69
|
+
'headings.h3.spacingBefore',
|
|
70
|
+
'headings.h3.spacingAfter',
|
|
71
|
+
'headings.h4.spacingBefore',
|
|
72
|
+
'headings.h4.spacingAfter',
|
|
73
|
+
'body.paragraphSpacing',
|
|
74
|
+
'caption.spacingBefore',
|
|
75
|
+
]);
|
|
34
76
|
const countIndent = (line) => line.match(/^\s*/)?.[0].length ?? 0;
|
|
35
77
|
|
|
36
|
-
const parseScalar = (rawValue) => {
|
|
37
|
-
const value = rawValue.trim();
|
|
38
|
-
|
|
39
|
-
if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
|
|
40
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
41
|
-
return value.slice(1, -1);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return value;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const parseMapping = (lines, startIndex, baseIndent) => {
|
|
48
|
-
const value = {};
|
|
49
|
-
let index = startIndex;
|
|
50
|
-
|
|
51
|
-
while (index < lines.length) {
|
|
52
|
-
const line = lines[index];
|
|
53
|
-
if (!line.trim() || line.trim().startsWith('#')) {
|
|
54
|
-
index += 1;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const indent = countIndent(line);
|
|
59
|
-
if (indent <= baseIndent) break;
|
|
60
|
-
if (line.trim().startsWith('- ')) break;
|
|
61
|
-
|
|
62
|
-
const match = line.trim().match(/^([a-zA-Z][a-zA-Z0-9-]*):(?:\s+(.*))?$/);
|
|
63
|
-
if (!match) break;
|
|
64
|
-
|
|
65
|
-
const [, key, rawValue] = match;
|
|
66
|
-
if (rawValue === undefined) {
|
|
67
|
-
const parsed = parseMapping(lines, index + 1, indent);
|
|
68
|
-
value[key] = parsed.value;
|
|
69
|
-
index = parsed.nextIndex;
|
|
70
|
-
} else {
|
|
71
|
-
value[key] = parseScalar(rawValue);
|
|
72
|
-
index += 1;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return { value, nextIndex: index };
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const findMap = (lines, label, parentStart = 0, parentEnd = lines.length, requiredIndent = null) => {
|
|
80
|
-
for (let index = parentStart; index < parentEnd; index += 1) {
|
|
81
|
-
const line = lines[index];
|
|
82
|
-
const match = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9-]*):\s*$/);
|
|
83
|
-
if (!match || match[2] !== label) continue;
|
|
84
|
-
if (requiredIndent !== null && match[1].length !== requiredIndent) continue;
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
index,
|
|
88
|
-
indent: match[1].length,
|
|
89
|
-
...parseMapping(lines, index + 1, match[1].length),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return null;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
78
|
const isPlainObject = (value) => (
|
|
97
79
|
value !== null &&
|
|
98
80
|
typeof value === 'object' &&
|
|
@@ -129,54 +111,29 @@ const setPath = (value, path, entry) => {
|
|
|
129
111
|
const annotateResolvedValues = (resolved, sources) => {
|
|
130
112
|
const annotated = {};
|
|
131
113
|
|
|
132
|
-
for (const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
}
|
|
114
|
+
for (const path of typographyValuePaths) {
|
|
115
|
+
const source = getPath(sources, path);
|
|
116
|
+
setPath(annotated, path, {
|
|
117
|
+
value: getPath(resolved.values, path),
|
|
118
|
+
source: source.source,
|
|
119
|
+
...(source.inherited ? { inherited: true } : {}),
|
|
120
|
+
});
|
|
155
121
|
}
|
|
156
122
|
|
|
157
123
|
return annotated;
|
|
158
124
|
};
|
|
159
125
|
|
|
160
|
-
const
|
|
126
|
+
const defaultSources = (presetName, rhythmName) => {
|
|
161
127
|
const sources = {};
|
|
162
128
|
|
|
163
|
-
for (const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
setPath(sources, [role, field], {
|
|
176
|
-
source: `preset:${presetName}`,
|
|
177
|
-
inherited: false,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
129
|
+
for (const path of typographyValuePaths) {
|
|
130
|
+
const pathKey = path.join('.');
|
|
131
|
+
setPath(sources, path, {
|
|
132
|
+
source: rhythmValuePaths.has(pathKey)
|
|
133
|
+
? `rhythm:${rhythmName}`
|
|
134
|
+
: `preset:${presetName}`,
|
|
135
|
+
inherited: false,
|
|
136
|
+
});
|
|
180
137
|
}
|
|
181
138
|
|
|
182
139
|
return sources;
|
|
@@ -186,28 +143,12 @@ const applyOverrideSources = (sources, typographyConfig, sourceLabel) => {
|
|
|
186
143
|
const overrides = typographyConfig?.overrides;
|
|
187
144
|
if (!overrides) return sources;
|
|
188
145
|
|
|
189
|
-
for (const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
}
|
|
146
|
+
for (const path of typographyValuePaths) {
|
|
147
|
+
if (hasPath(overrides, path)) {
|
|
148
|
+
setPath(sources, path, {
|
|
149
|
+
source: `${sourceLabel} override`,
|
|
150
|
+
inherited: false,
|
|
151
|
+
});
|
|
211
152
|
}
|
|
212
153
|
}
|
|
213
154
|
|
|
@@ -217,25 +158,11 @@ const applyOverrideSources = (sources, typographyConfig, sourceLabel) => {
|
|
|
217
158
|
const inheritedSources = (sources) => {
|
|
218
159
|
const inherited = structuredClone(sources);
|
|
219
160
|
|
|
220
|
-
for (const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
}
|
|
161
|
+
for (const path of typographyValuePaths) {
|
|
162
|
+
setPath(inherited, path, {
|
|
163
|
+
...getPath(sources, path),
|
|
164
|
+
inherited: true,
|
|
165
|
+
});
|
|
239
166
|
}
|
|
240
167
|
|
|
241
168
|
return inherited;
|
|
@@ -244,7 +171,7 @@ const inheritedSources = (sources) => {
|
|
|
244
171
|
const resolveAnnotatedTypographyConfig = (typographyConfig, sourceLabel) => {
|
|
245
172
|
const resolved = resolveTypographyConfig(typographyConfig ?? defaultTypography);
|
|
246
173
|
const sources = applyOverrideSources(
|
|
247
|
-
|
|
174
|
+
defaultSources(resolved.preset, resolved.rhythm),
|
|
248
175
|
typographyConfig,
|
|
249
176
|
sourceLabel,
|
|
250
177
|
);
|
|
@@ -255,14 +182,44 @@ const resolveAnnotatedTypographyConfig = (typographyConfig, sourceLabel) => {
|
|
|
255
182
|
source: typographyConfig?.preset ? sourceLabel : 'engine default',
|
|
256
183
|
...(typographyConfig?.preset ? {} : { inherited: true }),
|
|
257
184
|
},
|
|
185
|
+
rhythm: {
|
|
186
|
+
value: resolved.rhythm,
|
|
187
|
+
source: typographyConfig?.rhythm ? sourceLabel : 'engine default',
|
|
188
|
+
...(typographyConfig?.rhythm ? {} : { inherited: true }),
|
|
189
|
+
},
|
|
258
190
|
resolved,
|
|
259
191
|
sources,
|
|
260
192
|
};
|
|
261
193
|
};
|
|
262
194
|
|
|
263
195
|
const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sourceLabel) => {
|
|
264
|
-
if (typographyConfig?.preset) {
|
|
265
|
-
|
|
196
|
+
if (typographyConfig?.preset || typographyConfig?.rhythm) {
|
|
197
|
+
const resolvedConfig = {
|
|
198
|
+
preset: typographyConfig.preset ?? baseAnnotated.resolved.preset,
|
|
199
|
+
rhythm: typographyConfig.rhythm ?? baseAnnotated.resolved.rhythm,
|
|
200
|
+
overrides: typographyConfig.overrides,
|
|
201
|
+
};
|
|
202
|
+
const resolved = resolveTypographyConfig(resolvedConfig);
|
|
203
|
+
const sources = applyOverrideSources(
|
|
204
|
+
defaultSources(resolved.preset, resolved.rhythm),
|
|
205
|
+
typographyConfig,
|
|
206
|
+
sourceLabel,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
preset: {
|
|
211
|
+
value: resolved.preset,
|
|
212
|
+
source: typographyConfig.preset ? sourceLabel : baseAnnotated.preset.source,
|
|
213
|
+
...(typographyConfig.preset ? {} : { inherited: true }),
|
|
214
|
+
},
|
|
215
|
+
rhythm: {
|
|
216
|
+
value: resolved.rhythm,
|
|
217
|
+
source: typographyConfig.rhythm ? sourceLabel : baseAnnotated.rhythm.source,
|
|
218
|
+
...(typographyConfig.rhythm ? {} : { inherited: true }),
|
|
219
|
+
},
|
|
220
|
+
resolved,
|
|
221
|
+
sources,
|
|
222
|
+
};
|
|
266
223
|
}
|
|
267
224
|
|
|
268
225
|
if (typographyConfig?.overrides) {
|
|
@@ -279,6 +236,11 @@ const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sou
|
|
|
279
236
|
source: baseAnnotated.preset.source,
|
|
280
237
|
inherited: true,
|
|
281
238
|
},
|
|
239
|
+
rhythm: {
|
|
240
|
+
value: resolved.rhythm,
|
|
241
|
+
source: baseAnnotated.rhythm.source,
|
|
242
|
+
inherited: true,
|
|
243
|
+
},
|
|
282
244
|
resolved,
|
|
283
245
|
sources,
|
|
284
246
|
};
|
|
@@ -290,6 +252,11 @@ const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sou
|
|
|
290
252
|
source: baseAnnotated.preset.source,
|
|
291
253
|
inherited: true,
|
|
292
254
|
},
|
|
255
|
+
rhythm: {
|
|
256
|
+
value: baseAnnotated.resolved.rhythm,
|
|
257
|
+
source: baseAnnotated.rhythm.source,
|
|
258
|
+
inherited: true,
|
|
259
|
+
},
|
|
293
260
|
resolved: baseAnnotated.resolved,
|
|
294
261
|
sources: inheritedSources(baseAnnotated.sources),
|
|
295
262
|
};
|
|
@@ -297,6 +264,7 @@ const resolveAnnotatedTypographyOverride = (baseAnnotated, typographyConfig, sou
|
|
|
297
264
|
|
|
298
265
|
const formatAnnotatedTypography = (annotated) => ({
|
|
299
266
|
preset: annotated.preset,
|
|
267
|
+
rhythm: annotated.rhythm,
|
|
300
268
|
resolved: annotateResolvedValues(annotated.resolved, annotated.sources),
|
|
301
269
|
});
|
|
302
270
|
|
|
@@ -355,10 +323,7 @@ const readThemeTypography = async () => {
|
|
|
355
323
|
}
|
|
356
324
|
|
|
357
325
|
const themeLines = themeFrontmatterBody.split(/\r?\n/);
|
|
358
|
-
const
|
|
359
|
-
const themeTypographyConfig = themePresentation
|
|
360
|
-
? findMap(themeLines, 'typography', themePresentation.index + 1, themePresentation.nextIndex)?.value
|
|
361
|
-
: null;
|
|
326
|
+
const themeTypographyConfig = findMap(themeLines, 'typography', 0, themeLines.length, 0)?.value;
|
|
362
327
|
return resolveAnnotatedTypographyConfig(themeTypographyConfig ?? defaultTypography, siteThemeLabel);
|
|
363
328
|
};
|
|
364
329
|
|
|
@@ -401,7 +366,10 @@ const readPageTypography = async (contentFile, themeTypography) => {
|
|
|
401
366
|
};
|
|
402
367
|
|
|
403
368
|
if (mode === 'presets') {
|
|
404
|
-
console.log(toYamlLines(
|
|
369
|
+
console.log(toYamlLines({
|
|
370
|
+
presets: typographyPresets,
|
|
371
|
+
rhythms: typographyRhythms,
|
|
372
|
+
}).join('\n'));
|
|
405
373
|
} else if (mode === 'show') {
|
|
406
374
|
const themeTypography = await readThemeTypography();
|
|
407
375
|
const pages = await Promise.all((await getContentFiles()).map((contentFile) => readPageTypography(contentFile, themeTypography)));
|
|
@@ -429,5 +397,5 @@ if (mode === 'presets') {
|
|
|
429
397
|
|
|
430
398
|
console.log(toYamlLines(output).join('\n'));
|
|
431
399
|
} else {
|
|
432
|
-
throw new Error('Usage: norna typography
|
|
400
|
+
throw new Error('Usage: norna typography presets|show');
|
|
433
401
|
}
|
|
@@ -59,7 +59,7 @@ const promptForWrite = async () => {
|
|
|
59
59
|
if (!process.stdin.isTTY) return false;
|
|
60
60
|
|
|
61
61
|
const rl = createInterface({ input, output });
|
|
62
|
-
const answer = await rl.question(`This will rewrite Markdown sections in ${siteContentLabel} and move
|
|
62
|
+
const answer = await rl.question(`This will rewrite Markdown sections in ${siteContentLabel} and move image files if needed. Continue? [y/N] `);
|
|
63
63
|
rl.close();
|
|
64
64
|
|
|
65
65
|
return answer.trim().toLowerCase() === 'y';
|
|
@@ -269,7 +269,7 @@ const validateRouteContentFiles = async () => {
|
|
|
269
269
|
if (referencedImages.has(imageName)) {
|
|
270
270
|
const previousSectionId = referencedImages.get(imageName);
|
|
271
271
|
const message = `Image "${imageName}" is referenced more than once, in "${previousSectionId}" and "${contentFile.contentLabel}:${section.id}".`;
|
|
272
|
-
const fix = 'Each image filename can be referenced by only one
|
|
272
|
+
const fix = 'Each image filename can be referenced by only one image row.';
|
|
273
273
|
|
|
274
274
|
addIssue({ severity: 'error', sectionId: previousSectionId, message, fix });
|
|
275
275
|
addIssue({
|
|
@@ -291,7 +291,7 @@ const validateRouteContentFiles = async () => {
|
|
|
291
291
|
sectionId: `${contentFile.contentLabel}:${section.id}`,
|
|
292
292
|
sectionLabel: `${contentFile.contentLabel} [${section.id}]`,
|
|
293
293
|
message: `Image "${imageName}" does not exist anywhere under ${contentFile.imagesLabel}/.`,
|
|
294
|
-
fix: `Add the source image to ${contentFile.imagesLabel}/${section.id}/ or remove the
|
|
294
|
+
fix: `Add the source image to ${contentFile.imagesLabel}/${section.id}/ or remove the image row.`,
|
|
295
295
|
});
|
|
296
296
|
continue;
|
|
297
297
|
}
|
|
@@ -380,7 +380,7 @@ const getReportLines = (title) => {
|
|
|
380
380
|
leftKey.localeCompare(rightKey, 'sv')
|
|
381
381
|
));
|
|
382
382
|
|
|
383
|
-
lines.push('', 'Section
|
|
383
|
+
lines.push('', 'Section And Image Issues');
|
|
384
384
|
|
|
385
385
|
for (const [, group] of orderedSectionGroups) {
|
|
386
386
|
lines.push('', `[${group.label}]`);
|
|
@@ -506,7 +506,7 @@ for (const section of frontmatterSections) {
|
|
|
506
506
|
const message = previousSectionId === section.id
|
|
507
507
|
? `Image "${imageName}" is referenced more than once in this section.`
|
|
508
508
|
: `Image "${imageName}" is referenced more than once, in sections "${previousSectionId}" and "${section.id}".`;
|
|
509
|
-
const fix = 'Each image filename can be referenced by only one
|
|
509
|
+
const fix = 'Each image filename can be referenced by only one image row.';
|
|
510
510
|
|
|
511
511
|
addSectionIssue(previousSectionId, { severity: 'error', message, fix });
|
|
512
512
|
if (previousSectionId !== section.id) {
|
|
@@ -522,7 +522,7 @@ for (const section of frontmatterSections) {
|
|
|
522
522
|
addSectionIssue(section.id, {
|
|
523
523
|
severity: 'error',
|
|
524
524
|
message: `Image "${imageName}" does not exist anywhere under ${siteImagesLabel}/.`,
|
|
525
|
-
fix: `Add the source image to ${siteImagesLabel}/${section.id}/ or remove the
|
|
525
|
+
fix: `Add the source image to ${siteImagesLabel}/${section.id}/ or remove the image row.`,
|
|
526
526
|
});
|
|
527
527
|
continue;
|
|
528
528
|
}
|
|
@@ -43,9 +43,8 @@ const makePngHeader = ({ width, height }) => {
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
const defaultTheme = `---
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
preset: quiet-gallery
|
|
46
|
+
typography:
|
|
47
|
+
preset: quiet-gallery
|
|
49
48
|
frame:
|
|
50
49
|
colors: presentation
|
|
51
50
|
---
|
|
@@ -114,7 +113,7 @@ test('content:check groups section issues, global issues, and unreferenced image
|
|
|
114
113
|
|
|
115
114
|
assert.equal(result.status, 1, output);
|
|
116
115
|
assert.match(output, /^Content check failed\./m);
|
|
117
|
-
assert.match(output, /Section
|
|
116
|
+
assert.match(output, /Section And Image Issues\n\n\[min-konst\]\n Errors:/);
|
|
118
117
|
assert.match(output, /Image "vav\.jpeg" is used here but is located in site\/images\/mitt-hem\/\./);
|
|
119
118
|
assert.match(output, /Image "missing\.jpeg" does not exist anywhere under site\/images\/\./);
|
|
120
119
|
assert.match(output, /Image "duplicate\.jpg" is referenced more than once in this section\./);
|
|
@@ -4,6 +4,7 @@ import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promi
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { typographyPresets, typographyRhythms } from './lib/typography.mjs';
|
|
7
8
|
|
|
8
9
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
10
|
const cliPath = path.join(repoRoot, 'bin', 'norna.mjs');
|
|
@@ -47,12 +48,29 @@ try {
|
|
|
47
48
|
assert.equal(initializedPackageJson.scripts['engine:update'], undefined);
|
|
48
49
|
assert.equal(initializedPackageJson.scripts['engine:version'], undefined);
|
|
49
50
|
|
|
50
|
-
const presetsResult = runCli(['typography
|
|
51
|
+
const presetsResult = runCli(['typography', 'presets']);
|
|
51
52
|
assert.equal(presetsResult.status, 0, presetsResult.stderr || presetsResult.stdout);
|
|
52
53
|
assert.match(presetsResult.stdout, /quiet-gallery:/);
|
|
53
54
|
assert.match(presetsResult.stdout, /text-forward:/);
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
assert.match(presetsResult.stdout, /rhythms:/);
|
|
56
|
+
assert.match(presetsResult.stdout, /normal:/);
|
|
57
|
+
for (const [presetName, preset] of Object.entries(typographyPresets)) {
|
|
58
|
+
for (const [level, heading] of Object.entries(preset.headings)) {
|
|
59
|
+
assert.equal(heading.size, 'medium', `${presetName} ${level} should use medium size`);
|
|
60
|
+
}
|
|
61
|
+
assert.equal(preset.body.size, 'medium', `${presetName} body should use medium size`);
|
|
62
|
+
assert.equal(preset.caption.size, 'medium', `${presetName} caption should use medium size`);
|
|
63
|
+
}
|
|
64
|
+
for (const [rhythmName, rhythm] of Object.entries(typographyRhythms)) {
|
|
65
|
+
for (const [level, heading] of Object.entries(rhythm.headings)) {
|
|
66
|
+
assert.match(heading.spacingBefore, /^(0|[\d.]+em)$/, `${rhythmName} ${level} spacingBefore should be text-relative`);
|
|
67
|
+
assert.match(heading.spacingAfter, /^[\d.]+em$/, `${rhythmName} ${level} spacingAfter should be text-relative`);
|
|
68
|
+
}
|
|
69
|
+
assert.match(rhythm.body.paragraphSpacing, /^[\d.]+em$/, `${rhythmName} body paragraphSpacing should be text-relative`);
|
|
70
|
+
assert.match(rhythm.caption.spacingBefore, /^[\d.]+em$/, `${rhythmName} caption spacingBefore should be text-relative`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const showResult = runCli(['--site-dir', path.join(initializedSiteRoot, 'site'), 'typography', 'show']);
|
|
56
74
|
assert.equal(showResult.status, 0, showResult.stderr || showResult.stdout);
|
|
57
75
|
assert.match(showResult.stdout, /theme:/);
|
|
58
76
|
assert.match(showResult.stdout, /pages:/);
|
|
@@ -181,18 +181,18 @@ try {
|
|
|
181
181
|
await writeFile(
|
|
182
182
|
packageCheckConfigPath,
|
|
183
183
|
packageCheckConfig
|
|
184
|
-
.replace("url: 'https://example.com/'", "url: 'https://example.com/
|
|
185
|
-
.replace("basePath: '/'", "basePath: '/
|
|
184
|
+
.replace("url: 'https://example.com/'", "url: 'https://example.com/site/'")
|
|
185
|
+
.replace("basePath: '/'", "basePath: '/site/'"),
|
|
186
186
|
);
|
|
187
187
|
const packageCheckThemePath = path.join(siteProjectRoot, 'site', 'theme.md');
|
|
188
188
|
const packageCheckTheme = await readFile(packageCheckThemePath, 'utf8');
|
|
189
189
|
await writeFile(
|
|
190
190
|
packageCheckThemePath,
|
|
191
|
-
packageCheckTheme.replace('brand: Example
|
|
191
|
+
packageCheckTheme.replace('brand: Example Site', 'brand: Package Check Brand'),
|
|
192
192
|
);
|
|
193
193
|
await mkdir(path.join(siteProjectRoot, 'site', 'routes', 'about'), { recursive: true });
|
|
194
194
|
await writeFile(path.join(siteProjectRoot, 'site', 'routes', 'about', 'route-content.md'), `---
|
|
195
|
-
title: About the
|
|
195
|
+
title: About the site
|
|
196
196
|
description: Route used by package checks.
|
|
197
197
|
navigation:
|
|
198
198
|
label: About
|
|
@@ -237,6 +237,14 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
237
237
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
238
238
|
'--section-heading-font-size-desktop: clamp(1.35rem, 1.1rem + 0.9vw, 2rem)',
|
|
239
239
|
);
|
|
240
|
+
await assertFileIncludes(
|
|
241
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
242
|
+
'--section-heading-spacing-after: 0.55em',
|
|
243
|
+
);
|
|
244
|
+
await assertFileIncludes(
|
|
245
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
246
|
+
'--section-markdown-h3-spacing-before: 1.5em',
|
|
247
|
+
);
|
|
240
248
|
await assertFileIncludes(
|
|
241
249
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
242
250
|
"--font-sans: Arial, 'Helvetica Neue', Helvetica, sans-serif",
|
|
@@ -247,15 +255,15 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
247
255
|
);
|
|
248
256
|
await assertFileIncludes(
|
|
249
257
|
path.join(siteProjectRoot, 'dist', 'about', 'index.html'),
|
|
250
|
-
'<link rel="canonical" href="https://example.com/
|
|
258
|
+
'<link rel="canonical" href="https://example.com/site/about/">',
|
|
251
259
|
);
|
|
252
260
|
await assertFileIncludes(
|
|
253
261
|
path.join(siteProjectRoot, 'dist', 'about', 'index.html'),
|
|
254
|
-
'href="/
|
|
262
|
+
'href="/site/about/" aria-current="page"',
|
|
255
263
|
);
|
|
256
264
|
await assertFileIncludes(
|
|
257
265
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
258
|
-
'<a class="site-brand" href="/
|
|
266
|
+
'<a class="site-brand" href="/site/">Package Check Brand</a>',
|
|
259
267
|
);
|
|
260
268
|
await assertFileIncludes(
|
|
261
269
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
@@ -273,6 +281,14 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
273
281
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
274
282
|
'--section-caption-line-height: 1.35',
|
|
275
283
|
);
|
|
284
|
+
await assertFileIncludes(
|
|
285
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
286
|
+
'--section-caption-spacing-before: 0.5em',
|
|
287
|
+
);
|
|
288
|
+
await assertFileIncludes(
|
|
289
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
290
|
+
'--section-caption-font-size-desktop: clamp(0.86rem, 0.84rem + 0.08vw, 0.92rem)',
|
|
291
|
+
);
|
|
276
292
|
await assertFileIncludes(
|
|
277
293
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
278
294
|
'class="inline-style inline-style-highlight" style="--inline-style-color: #ffd84d"',
|
|
@@ -306,7 +322,7 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
306
322
|
await assertFileExists(path.join(siteProjectRoot, 'dist', 'favicon.ico'));
|
|
307
323
|
await assertFileIncludes(
|
|
308
324
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
309
|
-
'<link rel="icon" sizes="any" href="/
|
|
325
|
+
'<link rel="icon" sizes="any" href="/site/favicon.ico">',
|
|
310
326
|
);
|
|
311
327
|
await assertFileExcludes(
|
|
312
328
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
@@ -316,12 +332,12 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
316
332
|
const siteContent = await readFile(siteContentPath, 'utf8');
|
|
317
333
|
const siteThemePath = path.join(siteProjectRoot, 'site', 'theme.md');
|
|
318
334
|
const siteTheme = await readFile(siteThemePath, 'utf8');
|
|
319
|
-
await writeFile(siteThemePath, siteTheme.replace('\n
|
|
335
|
+
await writeFile(siteThemePath, siteTheme.replace('\n preset: quiet-gallery', '\n preset: noisy-gallery'));
|
|
320
336
|
await runExpectFailure(
|
|
321
337
|
npxBin,
|
|
322
338
|
['norna', 'build'],
|
|
323
339
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
324
|
-
'
|
|
340
|
+
'typography.preset',
|
|
325
341
|
);
|
|
326
342
|
await writeFile(
|
|
327
343
|
siteThemePath,
|
|
@@ -371,14 +387,14 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
371
387
|
siteContentPath,
|
|
372
388
|
siteContent.replace(
|
|
373
389
|
' typography:\n preset: statement',
|
|
374
|
-
' typography:\n overrides:\n caption:\n
|
|
390
|
+
' typography:\n overrides:\n caption:\n spacingBefore: wide',
|
|
375
391
|
),
|
|
376
392
|
);
|
|
377
393
|
await runExpectFailure(
|
|
378
394
|
npxBin,
|
|
379
395
|
['norna', 'build'],
|
|
380
396
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
381
|
-
'sections.0.presentation.typography.overrides.caption.
|
|
397
|
+
'sections.0.presentation.typography.overrides.caption.spacingBefore',
|
|
382
398
|
);
|
|
383
399
|
await writeFile(
|
|
384
400
|
siteContentPath,
|
|
@@ -390,11 +406,9 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
390
406
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
391
407
|
'sections.0.presentation.typography.preset',
|
|
392
408
|
);
|
|
393
|
-
const siteConfigPath = path.join(siteProjectRoot, 'site', 'config.mjs');
|
|
394
|
-
const siteConfig = await readFile(siteConfigPath, 'utf8');
|
|
395
409
|
await writeFile(
|
|
396
|
-
|
|
397
|
-
|
|
410
|
+
siteThemePath,
|
|
411
|
+
siteTheme.replace(
|
|
398
412
|
'fontFamily: "Arial, \'Helvetica Neue\', Helvetica, sans-serif"',
|
|
399
413
|
'fontFamily: "Arial; color: red"',
|
|
400
414
|
),
|
|
@@ -405,9 +419,12 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
405
419
|
{ cwd: siteProjectRoot, env: npmEnv },
|
|
406
420
|
'typography.fontFamily',
|
|
407
421
|
);
|
|
422
|
+
await writeFile(siteThemePath, siteTheme);
|
|
423
|
+
const siteConfigPath = path.join(siteProjectRoot, 'site', 'config.mjs');
|
|
424
|
+
const siteConfig = await readFile(siteConfigPath, 'utf8');
|
|
408
425
|
await writeFile(
|
|
409
426
|
siteConfigPath,
|
|
410
|
-
siteConfig.replace("basePath: '/
|
|
427
|
+
siteConfig.replace("basePath: '/site/'", "basePath: 'site'"),
|
|
411
428
|
);
|
|
412
429
|
await runExpectFailure(
|
|
413
430
|
npxBin,
|
|
@@ -27,10 +27,9 @@ const navigationPages = getNavigationPages(sitePages);
|
|
|
27
27
|
const homePage = sitePages.find((page) => page.isHome) ?? currentPage;
|
|
28
28
|
const themeData = theme?.data ?? {};
|
|
29
29
|
const brandLabel = themeData.navigation?.brand ?? homePage.title;
|
|
30
|
-
const pagePresentation = resolvePagePresentation(themeData
|
|
30
|
+
const pagePresentation = resolvePagePresentation(themeData, entry.data.presentation);
|
|
31
31
|
const frameColors = resolveFrameColors({
|
|
32
|
-
|
|
33
|
-
themeFrame: themeData.frame,
|
|
32
|
+
theme: themeData,
|
|
34
33
|
pagePresentation,
|
|
35
34
|
pageFrame: entry.data.frame,
|
|
36
35
|
});
|