@janga/norna 0.7.14 → 0.7.16
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 +15 -9
- package/bin/norna-cli.mjs +11 -4
- package/docs/README.md +71 -43
- package/docs/commands.md +34 -20
- package/docs/configuration.md +43 -62
- package/docs/content.md +34 -7
- package/docs/design/command-organization.md +15 -8
- package/docs/design/norna-diagram-design.md +6 -6
- package/docs/design/site-examples-structure.md +1 -1
- package/docs/engine-development.md +22 -9
- package/docs/getting-started.md +29 -16
- package/docs/how-to/embedded-site.md +1 -1
- package/docs/images-and-metadata.md +3 -3
- package/docs/local-development.md +11 -3
- package/docs/public-files.md +105 -0
- package/docs/publishing.md +4 -8
- package/docs/routes.md +6 -7
- package/docs/site-files.md +144 -0
- package/docs/sitewide-content.md +47 -33
- package/docs/theme.md +37 -49
- package/docs/typography.md +25 -31
- package/examples/complete-sites/dog-shelter-multi-page/README.md +8 -0
- package/examples/complete-sites/{dog-shelter-single-page/site/config.md → dog-shelter-multi-page/site/config.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.yaml +6 -0
- package/examples/complete-sites/dog-shelter-multi-page/site/{theme.md → theme.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-single-page/README.md +8 -0
- package/examples/complete-sites/{dog-shelter-multi-page/site/config.md → dog-shelter-single-page/site/config.yaml} +0 -2
- package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.yaml +6 -0
- package/examples/complete-sites/dog-shelter-single-page/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/media-and-surfaces/README.md +1 -1
- package/examples/feature-demos/media-and-surfaces/site/{config.md → config.yaml} +0 -2
- package/examples/feature-demos/media-and-surfaces/site/content.md +2 -2
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/{route-content.md → content.md} +3 -7
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.yaml +3 -0
- package/examples/feature-demos/media-and-surfaces/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/examples/feature-demos/media-and-surfaces/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/sitewide-content/README.md +8 -0
- package/examples/feature-demos/sitewide-content/site/{config.md → config.yaml} +0 -2
- package/examples/feature-demos/sitewide-content/site/routes/010-identity/{route-content.md → content.md} +3 -3
- package/examples/feature-demos/sitewide-content/site/routes/020-notices/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/sitewide-content/site/routes/030-footer/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/sitewide-content/site/{sitewide-content.md → sitewide-content.yaml} +1 -11
- package/examples/feature-demos/sitewide-content/site/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/config.yaml +1 -0
- package/examples/feature-demos/theme-presets/site/routes/010-portfolio/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/routes/020-documentation/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/routes/030-project/theme.yaml +1 -0
- package/examples/feature-demos/theme-presets/site/routes/040-statement/{route-content.md → content.md} +1 -1
- package/examples/feature-demos/theme-presets/site/routes/040-statement/{theme.md → theme.yaml} +0 -2
- package/examples/feature-demos/theme-presets/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/examples/feature-demos/theme-presets/site/theme.yaml +1 -0
- package/fixtures/basic/site/{config.md → config.yaml} +0 -2
- package/fixtures/basic/site/content.md +3 -3
- package/fixtures/basic/site/{sitewide-content.md → sitewide-content.yaml} +0 -2
- package/fixtures/basic/site/{theme.md → theme.yaml} +3 -6
- package/fixtures/content-model-v2/site/{config.md → config.yaml} +0 -2
- package/fixtures/content-model-v2/site/theme.yaml +4 -0
- package/package.json +10 -3
- package/schemas/config.schema.json +77 -0
- package/schemas/content-frontmatter.schema.json +81 -0
- package/schemas/manifest.json +9 -0
- package/schemas/sitewide-content.schema.json +215 -0
- package/schemas/theme.schema.json +1208 -0
- package/scripts/check-config.mjs +18 -11
- package/scripts/export-theme-preset.mjs +1 -1
- package/scripts/generate-schemas.mjs +283 -0
- package/scripts/init-site.mjs +22 -27
- package/scripts/lib/documentation-links.mjs +8 -0
- package/scripts/lib/editor-language-service.mjs +609 -0
- package/scripts/lib/example-sites.mjs +2 -2
- package/scripts/lib/git-status.mjs +7 -0
- package/scripts/lib/logo-assets.mjs +15 -10
- package/scripts/lib/norna-markdown-blocks.mjs +90 -11
- package/scripts/lib/presentation.mjs +23 -12
- package/scripts/lib/project-config.mjs +17 -41
- package/scripts/lib/public-asset-conventions.mjs +112 -0
- package/scripts/lib/schema-definitions.mjs +213 -0
- package/scripts/lib/schema-value-definitions.mjs +86 -0
- package/scripts/lib/site-content.mjs +24 -18
- package/scripts/lib/site-paths.mjs +4 -8
- package/scripts/lib/sitewide-content.mjs +24 -0
- package/scripts/lib/theme-config.mjs +15 -35
- package/scripts/lib/theme-presets.mjs +76 -66
- package/scripts/lib/typography.mjs +15 -15
- package/scripts/lib/yaml-config.mjs +50 -0
- package/scripts/list-theme-presets.mjs +15 -0
- package/scripts/release.mjs +84 -8
- package/scripts/show-typography.mjs +22 -46
- package/scripts/sync-content-sections.mjs +2 -2
- package/scripts/test-client-javascript.mjs +13 -22
- package/scripts/test-content-check.mjs +21 -26
- package/scripts/test-content-model-v2.mjs +72 -40
- package/scripts/test-documentation.mjs +70 -4
- package/scripts/test-editor-language-service.mjs +158 -0
- package/scripts/test-engine-commands.mjs +65 -34
- package/scripts/test-package-check.mjs +37 -30
- package/scripts/test-project-config.mjs +15 -20
- package/scripts/test-schemas.mjs +102 -0
- package/scripts/test-temporary-visibility.mjs +5 -13
- package/scripts/test-theme-presets.mjs +64 -37
- package/src/components/CardList.astro +2 -5
- package/src/components/ImageCarousel.astro +21 -21
- package/src/components/{GalleryGrid.astro → ImageStack.astro} +12 -12
- package/src/components/SiteNavigation.astro +3 -3
- package/src/components/SitePage.astro +8 -8
- package/src/components/SiteSection.astro +16 -19
- package/src/content.config.ts +17 -209
- package/src/layouts/BaseLayout.astro +9 -9
- package/src/lib/{galleryImages.ts → imagePresentation.ts} +7 -7
- package/src/lib/routeThemes.ts +9 -29
- package/src/lib/sectionContent.ts +3 -3
- package/src/lib/sitePublicAssets.ts +5 -21
- package/src/styles/global.css +73 -73
- package/starters/basic/.github/workflows/deploy.yml +1 -1
- package/starters/basic/README.md +16 -13
- package/starters/basic/package.json +3 -2
- package/starters/basic/site/{config.md → config.yaml} +0 -2
- package/starters/basic/site/content.md +1 -1
- package/starters/basic/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/starters/basic/site/{theme.md → theme.yaml} +8 -15
- package/starters/project/.github/workflows/deploy.yml +1 -1
- package/starters/project/README.md +22 -15
- package/starters/project/package.json +3 -2
- package/starters/project/site/{config.md → config.yaml} +0 -2
- package/starters/project/site/{sitewide-content.md → sitewide-content.yaml} +1 -3
- package/starters/project/site/{theme.md → theme.yaml} +4 -8
- package/docs/site-structure.md +0 -98
- package/examples/complete-sites/dog-shelter-multi-page/site/sitewide-content.md +0 -16
- package/examples/complete-sites/dog-shelter-single-page/site/sitewide-content.md +0 -16
- package/examples/feature-demos/media-and-surfaces/site/routes/020-surfaces/theme.md +0 -8
- package/examples/feature-demos/theme-presets/site/config.md +0 -3
- package/examples/feature-demos/theme-presets/site/routes/030-project/theme.md +0 -3
- package/examples/feature-demos/theme-presets/site/theme.md +0 -3
- package/fixtures/content-model-v2/site/theme.md +0 -7
- package/scripts/lib/sitewide-config.mjs +0 -34
- /package/examples/complete-sites/dog-shelter-multi-page/site/routes/010-dogs/{route-content.md → content.md} +0 -0
- /package/examples/complete-sites/dog-shelter-multi-page/site/routes/020-adopt/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/media-and-surfaces/site/routes/010-media/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/010-portfolio/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/020-documentation/{route-content.md → content.md} +0 -0
- /package/examples/feature-demos/theme-presets/site/routes/030-project/{route-content.md → content.md} +0 -0
- /package/fixtures/content-model-v2/site/routes/010-guide/{route-content.md → content.md} +0 -0
- /package/starters/project/site/routes/010-guide/{route-content.md → content.md} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export const
|
|
2
|
-
'
|
|
3
|
-
'
|
|
4
|
-
'
|
|
1
|
+
export const typographyProfileNames = [
|
|
2
|
+
'restrained',
|
|
3
|
+
'dense',
|
|
4
|
+
'reading',
|
|
5
5
|
'statement',
|
|
6
6
|
];
|
|
7
7
|
|
|
@@ -11,8 +11,8 @@ export const typographyRhythmNames = [
|
|
|
11
11
|
'airy',
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
-
export const
|
|
15
|
-
|
|
14
|
+
export const typographyProfiles = {
|
|
15
|
+
restrained: {
|
|
16
16
|
headings: {
|
|
17
17
|
h1: {
|
|
18
18
|
align: { desktop: 'left', mobile: 'left' },
|
|
@@ -51,7 +51,7 @@ export const typographyPresets = {
|
|
|
51
51
|
lineHeight: 1.35,
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
|
-
|
|
54
|
+
dense: {
|
|
55
55
|
headings: {
|
|
56
56
|
h1: {
|
|
57
57
|
align: { desktop: 'left', mobile: 'left' },
|
|
@@ -90,7 +90,7 @@ export const typographyPresets = {
|
|
|
90
90
|
lineHeight: 1.25,
|
|
91
91
|
},
|
|
92
92
|
},
|
|
93
|
-
|
|
93
|
+
reading: {
|
|
94
94
|
headings: {
|
|
95
95
|
h1: {
|
|
96
96
|
align: { desktop: 'left', mobile: 'left' },
|
|
@@ -216,7 +216,7 @@ export const typographyRhythms = {
|
|
|
216
216
|
};
|
|
217
217
|
|
|
218
218
|
export const defaultTypography = {
|
|
219
|
-
|
|
219
|
+
profile: 'restrained',
|
|
220
220
|
rhythm: 'normal',
|
|
221
221
|
};
|
|
222
222
|
|
|
@@ -243,13 +243,13 @@ export const mergeDeep = (base, override) => {
|
|
|
243
243
|
};
|
|
244
244
|
|
|
245
245
|
export const resolveTypographyConfig = (typography = defaultTypography) => {
|
|
246
|
-
const
|
|
246
|
+
const profileName = typography?.profile ?? defaultTypography.profile;
|
|
247
247
|
const rhythmName = typography?.rhythm ?? defaultTypography.rhythm;
|
|
248
|
-
const
|
|
248
|
+
const profile = typographyProfiles[profileName];
|
|
249
249
|
const rhythm = typographyRhythms[rhythmName];
|
|
250
250
|
|
|
251
|
-
if (!
|
|
252
|
-
throw new Error(`Unknown typography
|
|
251
|
+
if (!profile) {
|
|
252
|
+
throw new Error(`Unknown typography profile: ${profileName}`);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
if (!rhythm) {
|
|
@@ -257,9 +257,9 @@ export const resolveTypographyConfig = (typography = defaultTypography) => {
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
return {
|
|
260
|
-
|
|
260
|
+
profile: profileName,
|
|
261
261
|
rhythm: rhythmName,
|
|
262
|
-
values: mergeDeep(mergeDeep(
|
|
262
|
+
values: mergeDeep(mergeDeep(profile, rhythm), typography?.overrides),
|
|
263
263
|
};
|
|
264
264
|
};
|
|
265
265
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { load } from 'js-yaml';
|
|
2
|
+
import {
|
|
3
|
+
validateFrontmatterIndentation,
|
|
4
|
+
} from './site-content.mjs';
|
|
5
|
+
|
|
6
|
+
const formatSchemaIssues = (issues) => issues.map((issue) => {
|
|
7
|
+
const location = issue.path.join('.') || '(root)';
|
|
8
|
+
return `- ${location}: ${issue.message}`;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const parseYamlConfig = (source, label, {
|
|
12
|
+
schema,
|
|
13
|
+
validateStructure,
|
|
14
|
+
} = {}) => {
|
|
15
|
+
const issues = [];
|
|
16
|
+
|
|
17
|
+
validateFrontmatterIndentation(source, (issue) => issues.push(issue));
|
|
18
|
+
validateStructure?.(source, (issue) => issues.push(issue));
|
|
19
|
+
|
|
20
|
+
if (issues.length > 0) {
|
|
21
|
+
throw new Error([
|
|
22
|
+
`${label} has invalid YAML structure.`,
|
|
23
|
+
...issues.map((issue) => `- ${issue.message}`),
|
|
24
|
+
].join('\n'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let data;
|
|
28
|
+
try {
|
|
29
|
+
data = load(source) ?? {};
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
32
|
+
throw new Error(`${label} contains invalid YAML.\n${detail}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
36
|
+
throw new Error(`${label} must contain a YAML mapping at the top level.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!schema) return data;
|
|
40
|
+
|
|
41
|
+
const parsed = schema.safeParse(data);
|
|
42
|
+
if (!parsed.success) {
|
|
43
|
+
throw new Error([
|
|
44
|
+
`${label} has invalid values.`,
|
|
45
|
+
...formatSchemaIssues(parsed.error.issues),
|
|
46
|
+
].join('\n'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parsed.data;
|
|
50
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getThemePresetMetadata,
|
|
3
|
+
themePresetNames,
|
|
4
|
+
} from './lib/theme-presets.mjs';
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
if (args.length > 0) {
|
|
8
|
+
throw new Error('Usage: norna theme:presets');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
console.log('Theme presets\n');
|
|
12
|
+
for (const name of themePresetNames) {
|
|
13
|
+
const preset = getThemePresetMetadata(name);
|
|
14
|
+
console.log(`${name}\n ${preset.description}`);
|
|
15
|
+
}
|
package/scripts/release.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
|
+
import { getShortGitStatusPath, parseShortGitStatus } from './lib/git-status.mjs';
|
|
6
7
|
import { runInherit } from './lib/run-command.mjs';
|
|
7
8
|
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
@@ -17,7 +18,7 @@ const showHelp = releaseArguments.includes('--help') || releaseArguments.include
|
|
|
17
18
|
|
|
18
19
|
const printUsage = () => {
|
|
19
20
|
console.log('Usage: node scripts/release.mjs <patch|minor|major>');
|
|
20
|
-
console.log('Checks npm authentication, runs npm test, creates the
|
|
21
|
+
console.log('Checks npm authentication, prepares the version and schemas, runs npm test, creates the release commit and tag, publishes to npm, then pushes.');
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
const run = (command, args) => runInherit(command, args, { cwd: repoRoot });
|
|
@@ -27,6 +28,48 @@ const readPackageVersion = async () => {
|
|
|
27
28
|
return packageJson.version;
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
const releasePaths = ['package.json', 'package-lock.json', 'schemas'];
|
|
32
|
+
|
|
33
|
+
const getWorktreeChanges = async () => {
|
|
34
|
+
const { stdout } = await execFileAsync('git', ['status', '--short'], { cwd: repoRoot });
|
|
35
|
+
return parseShortGitStatus(stdout);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const isReleasePath = (filePath) => (
|
|
39
|
+
filePath === 'package.json'
|
|
40
|
+
|| filePath === 'package-lock.json'
|
|
41
|
+
|| filePath === 'schemas'
|
|
42
|
+
|| filePath.startsWith('schemas/')
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const assertOnlyReleaseChanges = async () => {
|
|
46
|
+
const changes = await getWorktreeChanges();
|
|
47
|
+
const unexpected = changes.filter((line) => !isReleasePath(getShortGitStatusPath(line)));
|
|
48
|
+
if (unexpected.length > 0) {
|
|
49
|
+
throw new Error(`Release checks changed unexpected files:\n${unexpected.join('\n')}`);
|
|
50
|
+
}
|
|
51
|
+
for (const requiredPath of ['package.json', 'package-lock.json']) {
|
|
52
|
+
if (!changes.some((line) => getShortGitStatusPath(line) === requiredPath)) {
|
|
53
|
+
throw new Error(`Release preparation did not update ${requiredPath}.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const assertTagDoesNotExist = async (tag) => {
|
|
59
|
+
try {
|
|
60
|
+
await execFileAsync('git', ['rev-parse', '--quiet', '--verify', `refs/tags/${tag}`], { cwd: repoRoot });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error?.code === 1) return;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Git tag ${tag} already exists.`);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const rollbackPreparedRelease = async () => {
|
|
69
|
+
await execFileAsync('git', ['restore', '--staged', '--worktree', '--', ...releasePaths], { cwd: repoRoot });
|
|
70
|
+
console.log('Release preparation failed before commit. Restored package version and generated schemas.');
|
|
71
|
+
};
|
|
72
|
+
|
|
30
73
|
const assertCleanWorktree = async (stage) => {
|
|
31
74
|
const { stdout } = await execFileAsync('git', ['status', '--short'], { cwd: repoRoot });
|
|
32
75
|
|
|
@@ -61,17 +104,50 @@ if (showHelp || !releaseType) {
|
|
|
61
104
|
throw new Error(`Unsupported release type: ${releaseType}`);
|
|
62
105
|
} else {
|
|
63
106
|
const previousVersion = await readPackageVersion();
|
|
107
|
+
let preparationStarted = false;
|
|
108
|
+
let releaseCommitCreated = false;
|
|
109
|
+
let releaseTagCreated = false;
|
|
110
|
+
let publishedVersion = null;
|
|
64
111
|
|
|
65
112
|
await assertCleanWorktree('before the release checks');
|
|
66
113
|
await assertNpmAuthenticated();
|
|
67
114
|
console.log(`Preparing a ${releaseType} release from ${previousVersion}.`);
|
|
68
|
-
await run(npmBin, ['test']);
|
|
69
|
-
await assertCleanWorktree('after the release checks');
|
|
70
|
-
await run(npmBin, ['version', releaseType, '--message', 'Release %s']);
|
|
71
115
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
116
|
+
try {
|
|
117
|
+
preparationStarted = true;
|
|
118
|
+
await run(npmBin, ['version', releaseType, '--no-git-tag-version']);
|
|
119
|
+
publishedVersion = await readPackageVersion();
|
|
120
|
+
const releaseTag = `v${publishedVersion}`;
|
|
121
|
+
if (publishedVersion === previousVersion) {
|
|
122
|
+
throw new Error(`Version did not change from ${previousVersion}.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await assertTagDoesNotExist(releaseTag);
|
|
126
|
+
await run(npmBin, ['run', 'schemas:generate']);
|
|
127
|
+
await run(npmBin, ['test']);
|
|
128
|
+
await assertOnlyReleaseChanges();
|
|
129
|
+
await run('git', ['add', ...releasePaths]);
|
|
130
|
+
await run('git', ['commit', '-m', `Release ${releaseTag}`]);
|
|
131
|
+
releaseCommitCreated = true;
|
|
132
|
+
await run('git', ['tag', releaseTag]);
|
|
133
|
+
releaseTagCreated = true;
|
|
134
|
+
await run(npmBin, ['run', 'release:publish']);
|
|
135
|
+
await run('git', ['push', '--follow-tags']);
|
|
75
136
|
|
|
76
|
-
|
|
137
|
+
console.log(`Released @janga/norna@${publishedVersion}.`);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (preparationStarted && !releaseCommitCreated) {
|
|
140
|
+
try {
|
|
141
|
+
await rollbackPreparedRelease();
|
|
142
|
+
} catch (rollbackError) {
|
|
143
|
+
throw new AggregateError([error, rollbackError], 'Release failed and automatic rollback also failed.');
|
|
144
|
+
}
|
|
145
|
+
} else if (releaseCommitCreated) {
|
|
146
|
+
const state = releaseTagCreated
|
|
147
|
+
? `Release commit and tag v${publishedVersion} remain locally.`
|
|
148
|
+
: `Release commit for v${publishedVersion} remains locally, but its tag was not created.`;
|
|
149
|
+
console.error(state);
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
77
153
|
}
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { parseYamlMapping } from './lib/frontmatter-yaml.mjs';
|
|
4
3
|
import {
|
|
5
4
|
defaultTypography,
|
|
6
5
|
resolveTypographyConfig,
|
|
7
6
|
toYamlLines,
|
|
8
|
-
|
|
7
|
+
typographyProfiles,
|
|
9
8
|
typographyRhythms,
|
|
10
9
|
} from './lib/typography.mjs';
|
|
11
10
|
import {
|
|
12
11
|
getBodySections,
|
|
13
12
|
getContentFiles,
|
|
14
13
|
readSiteFile,
|
|
15
|
-
splitSiteFile,
|
|
16
14
|
validateContentFrontmatterStructure,
|
|
17
15
|
validateFrontmatterIndentation,
|
|
18
|
-
|
|
19
|
-
validateThemeFrontmatterStructure,
|
|
16
|
+
validateRouteThemeYamlStructure,
|
|
20
17
|
} from './lib/site-content.mjs';
|
|
21
18
|
import {
|
|
22
19
|
siteContentLabel,
|
|
@@ -25,7 +22,9 @@ import {
|
|
|
25
22
|
siteThemeLabel,
|
|
26
23
|
siteThemePath,
|
|
27
24
|
} from './lib/site-paths.mjs';
|
|
25
|
+
import { readThemeConfig } from './lib/theme-config.mjs';
|
|
28
26
|
import { resolveThemeConfig } from './lib/theme-presets.mjs';
|
|
27
|
+
import { parseYamlConfig } from './lib/yaml-config.mjs';
|
|
29
28
|
|
|
30
29
|
const mode = process.argv[2] ?? 'show';
|
|
31
30
|
|
|
@@ -130,7 +129,7 @@ const annotateResolvedValues = (resolved, sources) => {
|
|
|
130
129
|
return annotated;
|
|
131
130
|
};
|
|
132
131
|
|
|
133
|
-
const defaultSources = (
|
|
132
|
+
const defaultSources = (profileName, rhythmName) => {
|
|
134
133
|
const sources = {};
|
|
135
134
|
|
|
136
135
|
for (const path of typographyValuePaths) {
|
|
@@ -138,7 +137,7 @@ const defaultSources = (presetName, rhythmName) => {
|
|
|
138
137
|
setPath(sources, path, {
|
|
139
138
|
source: rhythmValuePaths.has(pathKey)
|
|
140
139
|
? `rhythm:${rhythmName}`
|
|
141
|
-
: `
|
|
140
|
+
: `profile:${profileName}`,
|
|
142
141
|
inherited: false,
|
|
143
142
|
});
|
|
144
143
|
}
|
|
@@ -165,16 +164,16 @@ const applyOverrideSources = (sources, typographyConfig, sourceLabel) => {
|
|
|
165
164
|
const resolveAnnotatedTypographyConfig = (typographyConfig, sourceLabel) => {
|
|
166
165
|
const resolved = resolveTypographyConfig(typographyConfig ?? defaultTypography);
|
|
167
166
|
const sources = applyOverrideSources(
|
|
168
|
-
defaultSources(resolved.
|
|
167
|
+
defaultSources(resolved.profile, resolved.rhythm),
|
|
169
168
|
typographyConfig,
|
|
170
169
|
sourceLabel,
|
|
171
170
|
);
|
|
172
171
|
|
|
173
172
|
return {
|
|
174
|
-
|
|
175
|
-
value: resolved.
|
|
176
|
-
source: typographyConfig?.
|
|
177
|
-
...(typographyConfig?.
|
|
173
|
+
profile: {
|
|
174
|
+
value: resolved.profile,
|
|
175
|
+
source: typographyConfig?.profile ? sourceLabel : 'engine default',
|
|
176
|
+
...(typographyConfig?.profile ? {} : { inherited: true }),
|
|
178
177
|
},
|
|
179
178
|
rhythm: {
|
|
180
179
|
value: resolved.rhythm,
|
|
@@ -187,25 +186,13 @@ const resolveAnnotatedTypographyConfig = (typographyConfig, sourceLabel) => {
|
|
|
187
186
|
};
|
|
188
187
|
|
|
189
188
|
const formatAnnotatedTypography = (annotated) => ({
|
|
190
|
-
|
|
189
|
+
profile: annotated.profile,
|
|
191
190
|
rhythm: annotated.rhythm,
|
|
192
191
|
resolved: annotateResolvedValues(annotated.resolved, annotated.sources),
|
|
193
192
|
});
|
|
194
193
|
|
|
195
194
|
const readThemeTypography = async () => {
|
|
196
|
-
const
|
|
197
|
-
const { frontmatter: themeFrontmatter, frontmatterBody: themeFrontmatterBody } = splitSiteFile(themeFile, siteThemeLabel);
|
|
198
|
-
const indentationIssues = [];
|
|
199
|
-
validateFrontmatterIndentation(themeFrontmatter, (issue) => indentationIssues.push(issue));
|
|
200
|
-
validateThemeFrontmatterStructure(themeFrontmatter, (issue) => indentationIssues.push(issue));
|
|
201
|
-
if (indentationIssues.length > 0) {
|
|
202
|
-
throw new Error([
|
|
203
|
-
`Cannot inspect typography because ${siteThemeLabel} has invalid frontmatter.`,
|
|
204
|
-
...indentationIssues.map((issue) => `- ${issue.message}`),
|
|
205
|
-
].join('\n'));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const themeConfig = resolveThemeConfig(parseYamlMapping(themeFrontmatterBody), siteThemeLabel);
|
|
195
|
+
const themeConfig = resolveThemeConfig(await readThemeConfig(), siteThemeLabel);
|
|
209
196
|
const themeTypographyConfig = themeConfig.typography;
|
|
210
197
|
return resolveAnnotatedTypographyConfig(themeTypographyConfig ?? defaultTypography, siteThemeLabel);
|
|
211
198
|
};
|
|
@@ -213,26 +200,17 @@ const readThemeTypography = async () => {
|
|
|
213
200
|
const readRouteThemeTypography = async (contentFile, themeTypography) => {
|
|
214
201
|
if (contentFile.isHome) return themeTypography;
|
|
215
202
|
|
|
216
|
-
const routeThemePath = path.join(siteRoutesDir, contentFile.routeDirectory, 'theme.
|
|
217
|
-
const routeThemeLabel = `${contentFile.contentLabel.replace(/\/route-content\.md$/, '')}/theme.md`;
|
|
203
|
+
const routeThemePath = path.join(siteRoutesDir, contentFile.routeDirectory, 'theme.yaml');
|
|
218
204
|
const routeThemeFile = await readFile(routeThemePath, 'utf8').catch((error) => {
|
|
219
205
|
if (error?.code === 'ENOENT') return null;
|
|
220
206
|
throw error;
|
|
221
207
|
});
|
|
222
208
|
if (!routeThemeFile) return themeTypography;
|
|
209
|
+
const routeThemeLabel = `${contentFile.contentLabel.replace(/\/content\.md$/, '')}/theme.yaml`;
|
|
223
210
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
validateRouteThemeFrontmatterStructure(frontmatter, (issue) => indentationIssues.push(issue));
|
|
228
|
-
if (indentationIssues.length > 0) {
|
|
229
|
-
throw new Error([
|
|
230
|
-
`Cannot inspect typography because ${routeThemeLabel} has invalid frontmatter.`,
|
|
231
|
-
...indentationIssues.map((issue) => `- ${issue.message}`),
|
|
232
|
-
].join('\n'));
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const routeThemeConfig = resolveThemeConfig(parseYamlMapping(frontmatterBody), routeThemeLabel);
|
|
211
|
+
const routeThemeConfig = resolveThemeConfig(parseYamlConfig(routeThemeFile, routeThemeLabel, {
|
|
212
|
+
validateStructure: validateRouteThemeYamlStructure,
|
|
213
|
+
}), routeThemeLabel);
|
|
236
214
|
const typographyConfig = routeThemeConfig.typography;
|
|
237
215
|
return resolveAnnotatedTypographyConfig(typographyConfig ?? defaultTypography, routeThemeLabel);
|
|
238
216
|
};
|
|
@@ -267,9 +245,9 @@ const readPageTypography = async (contentFile, siteThemeTypography) => {
|
|
|
267
245
|
};
|
|
268
246
|
};
|
|
269
247
|
|
|
270
|
-
if (mode === '
|
|
248
|
+
if (mode === 'profiles') {
|
|
271
249
|
console.log(toYamlLines({
|
|
272
|
-
|
|
250
|
+
profiles: typographyProfiles,
|
|
273
251
|
rhythms: typographyRhythms,
|
|
274
252
|
}).join('\n'));
|
|
275
253
|
} else if (mode === 'show') {
|
|
@@ -278,9 +256,7 @@ if (mode === 'presets') {
|
|
|
278
256
|
const output = {
|
|
279
257
|
theme: {
|
|
280
258
|
source: siteThemeLabel,
|
|
281
|
-
|
|
282
|
-
typography: formatAnnotatedTypography(themeTypography),
|
|
283
|
-
},
|
|
259
|
+
typography: formatAnnotatedTypography(themeTypography),
|
|
284
260
|
},
|
|
285
261
|
pages: Object.fromEntries(pages.map((page) => [
|
|
286
262
|
page.route,
|
|
@@ -299,5 +275,5 @@ if (mode === 'presets') {
|
|
|
299
275
|
|
|
300
276
|
console.log(toYamlLines(output).join('\n'));
|
|
301
277
|
} else {
|
|
302
|
-
throw new Error('Usage: norna typography
|
|
278
|
+
throw new Error('Usage: norna typography profiles|show');
|
|
303
279
|
}
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
toPosixPath,
|
|
22
22
|
validateContentFrontmatterStructure,
|
|
23
23
|
validateFrontmatterIndentation,
|
|
24
|
-
|
|
24
|
+
validateThemeYamlStructure,
|
|
25
25
|
} from './lib/site-content.mjs';
|
|
26
26
|
import { readImageDimensions } from './lib/image-dimensions.mjs';
|
|
27
27
|
import {
|
|
@@ -393,7 +393,7 @@ let themeFrontmatter = null;
|
|
|
393
393
|
try {
|
|
394
394
|
themeFrontmatter = (await readThemeFile(siteThemePath)).frontmatter;
|
|
395
395
|
validateFrontmatterIndentation(themeFrontmatter, addIssue);
|
|
396
|
-
|
|
396
|
+
validateThemeYamlStructure(themeFrontmatter, addIssue);
|
|
397
397
|
} catch (error) {
|
|
398
398
|
if (error?.code !== 'ENOENT') {
|
|
399
399
|
throw error;
|
|
@@ -11,7 +11,7 @@ await mkdir(tempParent, { recursive: true });
|
|
|
11
11
|
const tempRoot = await mkdtemp(path.join(tempParent, 'norna-client-javascript-'));
|
|
12
12
|
const siteDir = path.join(tempRoot, 'site');
|
|
13
13
|
const routeDir = path.join(siteDir, 'routes', '010-details');
|
|
14
|
-
const configPath = path.join(siteDir, 'config.
|
|
14
|
+
const configPath = path.join(siteDir, 'config.yaml');
|
|
15
15
|
|
|
16
16
|
const runBuild = () => {
|
|
17
17
|
const result = spawnSync(process.execPath, [nornaBin, 'build'], {
|
|
@@ -44,10 +44,8 @@ const assertScrollBehavior = (html, behavior, label) => {
|
|
|
44
44
|
`${label} should render scroll-behavior: ${behavior}.`,
|
|
45
45
|
);
|
|
46
46
|
};
|
|
47
|
-
const writeConfig = (scrollBehavior) => writeFile(configPath,
|
|
48
|
-
|
|
49
|
-
${scrollBehavior ? `scrollBehavior: ${scrollBehavior}\n` : ''}---
|
|
50
|
-
`);
|
|
47
|
+
const writeConfig = (scrollBehavior) => writeFile(configPath, `url: https://example.com/
|
|
48
|
+
${scrollBehavior ? `scrollBehavior: ${scrollBehavior}\n` : ''}`);
|
|
51
49
|
const writeSvg = (filePath, color) => writeFile(filePath, [
|
|
52
50
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 90">',
|
|
53
51
|
` <rect width="160" height="90" fill="${color}"/>`,
|
|
@@ -58,17 +56,12 @@ const writeSvg = (filePath, color) => writeFile(filePath, [
|
|
|
58
56
|
try {
|
|
59
57
|
await mkdir(path.join(siteDir, 'public'), { recursive: true });
|
|
60
58
|
await writeConfig();
|
|
61
|
-
await writeFile(path.join(siteDir, 'theme.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
presentation:
|
|
65
|
-
palette: paper
|
|
66
|
-
---
|
|
59
|
+
await writeFile(path.join(siteDir, 'theme.yaml'), `typography:
|
|
60
|
+
profile: reading
|
|
61
|
+
palette: paper
|
|
67
62
|
`);
|
|
68
|
-
await writeFile(path.join(siteDir, 'sitewide-content.
|
|
69
|
-
|
|
70
|
-
brand: Test site
|
|
71
|
-
---
|
|
63
|
+
await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `navigation:
|
|
64
|
+
label: Test site
|
|
72
65
|
`);
|
|
73
66
|
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
74
67
|
title: Section navigation
|
|
@@ -99,7 +92,7 @@ The navigation enhancement keeps native anchors clear of the sticky header.
|
|
|
99
92
|
assert.match(instantHtml, /href="#intro"/);
|
|
100
93
|
assert.match(instantHtml, /href="#more"/);
|
|
101
94
|
|
|
102
|
-
await writeConfig('
|
|
95
|
+
await writeConfig('smooth');
|
|
103
96
|
runBuild();
|
|
104
97
|
const browserSmoothHtml = await readPage();
|
|
105
98
|
assert.equal(
|
|
@@ -124,7 +117,7 @@ sections:
|
|
|
124
117
|
|
|
125
118
|
Ordinary Markdown needs no client-side JavaScript.
|
|
126
119
|
`);
|
|
127
|
-
await writeFile(path.join(routeDir, '
|
|
120
|
+
await writeFile(path.join(routeDir, 'content.md'), `---
|
|
128
121
|
title: Details
|
|
129
122
|
description: Static managed images.
|
|
130
123
|
sections:
|
|
@@ -166,7 +159,7 @@ This sentence has additional context.{note-ref}
|
|
|
166
159
|
|
|
167
160
|
{note: The note remains readable without JavaScript, while alignment is enhanced when JavaScript runs.}
|
|
168
161
|
`);
|
|
169
|
-
await writeFile(path.join(routeDir, '
|
|
162
|
+
await writeFile(path.join(routeDir, 'content.md'), `---
|
|
170
163
|
title: Carousel
|
|
171
164
|
description: An interactive image carousel.
|
|
172
165
|
sections:
|
|
@@ -201,7 +194,7 @@ sections:
|
|
|
201
194
|
|
|
202
195
|
The only interactive feature is the banner.
|
|
203
196
|
`);
|
|
204
|
-
await writeFile(path.join(routeDir, '
|
|
197
|
+
await writeFile(path.join(routeDir, 'content.md'), `---
|
|
205
198
|
title: Details
|
|
206
199
|
description: A second plain page.
|
|
207
200
|
sections:
|
|
@@ -211,12 +204,10 @@ sections:
|
|
|
211
204
|
|
|
212
205
|
Plain route content.
|
|
213
206
|
`);
|
|
214
|
-
await writeFile(path.join(siteDir, 'sitewide-content.
|
|
215
|
-
banners:
|
|
207
|
+
await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `banners:
|
|
216
208
|
- id: test-banner
|
|
217
209
|
title: Test banner
|
|
218
210
|
text: This banner can be dismissed.
|
|
219
|
-
---
|
|
220
211
|
`);
|
|
221
212
|
|
|
222
213
|
runBuild();
|
|
@@ -42,12 +42,9 @@ const makePngHeader = ({ width, height }) => {
|
|
|
42
42
|
return buffer;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
const defaultTheme =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
presentation:
|
|
49
|
-
palette: dark
|
|
50
|
-
---
|
|
45
|
+
const defaultTheme = `typography:
|
|
46
|
+
profile: restrained
|
|
47
|
+
palette: dark
|
|
51
48
|
`;
|
|
52
49
|
|
|
53
50
|
const withTempProject = async ({ site, theme = defaultTheme, files, siteDirectory = 'site' }, run) => {
|
|
@@ -55,7 +52,7 @@ const withTempProject = async ({ site, theme = defaultTheme, files, siteDirector
|
|
|
55
52
|
|
|
56
53
|
try {
|
|
57
54
|
await writeFixtureFile(root, `${siteDirectory}/content.md`, site);
|
|
58
|
-
await writeFixtureFile(root, `${siteDirectory}/theme.
|
|
55
|
+
await writeFixtureFile(root, `${siteDirectory}/theme.yaml`, theme);
|
|
59
56
|
|
|
60
57
|
for (const file of files) {
|
|
61
58
|
if (typeof file === 'string') {
|
|
@@ -370,12 +367,11 @@ This paragraph has {note-ref} two references {note-ref}.
|
|
|
370
367
|
});
|
|
371
368
|
|
|
372
369
|
const badWhitespaceSite = `---
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
paragraphSpacing: 0.3em
|
|
370
|
+
typography:
|
|
371
|
+
profile: restrained
|
|
372
|
+
\u00a0\u00a0overrides:
|
|
373
|
+
body:
|
|
374
|
+
paragraphSpacing: 0.3em
|
|
379
375
|
sections:
|
|
380
376
|
intro: {}
|
|
381
377
|
|
|
@@ -394,18 +390,17 @@ test('content:check explains invalid frontmatter indentation whitespace', async
|
|
|
394
390
|
|
|
395
391
|
assert.equal(result.status, 1, output);
|
|
396
392
|
assert.match(output, /^Content check failed\./m);
|
|
397
|
-
assert.match(output, /Frontmatter line
|
|
393
|
+
assert.match(output, /Frontmatter line 4 uses tabs, non-breaking spaces, or invalid whitespace for indentation\./);
|
|
398
394
|
assert.match(output, /Replace the indentation on that line with ordinary spaces\./);
|
|
399
395
|
});
|
|
400
396
|
});
|
|
401
397
|
|
|
402
398
|
const badNestedValueSite = `---
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
paragraphSpacing: 0.3em
|
|
399
|
+
typography:
|
|
400
|
+
profile: restrained
|
|
401
|
+
overrides:
|
|
402
|
+
body:
|
|
403
|
+
paragraphSpacing: 0.3em
|
|
409
404
|
sections:
|
|
410
405
|
intro: {}
|
|
411
406
|
|
|
@@ -424,17 +419,17 @@ test('content:check explains frontmatter indentation below scalar values', async
|
|
|
424
419
|
|
|
425
420
|
assert.equal(result.status, 1, output);
|
|
426
421
|
assert.match(output, /^Content check failed\./m);
|
|
427
|
-
assert.match(output, /Frontmatter line
|
|
422
|
+
assert.match(output, /Frontmatter line 4 is indented under line 3, but line 3 already has a value\./);
|
|
428
423
|
assert.match(output, /Move the later line to the same indentation level as its sibling/);
|
|
429
424
|
});
|
|
430
425
|
});
|
|
431
426
|
|
|
432
|
-
const
|
|
427
|
+
const topLevelImagesSite = `---
|
|
433
428
|
title: Example
|
|
434
429
|
description: Example site.
|
|
435
430
|
sections:
|
|
436
431
|
intro: {}
|
|
437
|
-
|
|
432
|
+
images:
|
|
438
433
|
- image: intro.jpg
|
|
439
434
|
|
|
440
435
|
---
|
|
@@ -444,7 +439,7 @@ Text.
|
|
|
444
439
|
|
|
445
440
|
test('content:check explains likely misindented frontmatter keys', async () => {
|
|
446
441
|
await withTempProject({
|
|
447
|
-
site:
|
|
442
|
+
site: topLevelImagesSite,
|
|
448
443
|
files: ['site/images/intro/intro.jpg'],
|
|
449
444
|
}, async (root) => {
|
|
450
445
|
const result = runContentScript(root, ['--check']);
|
|
@@ -452,8 +447,8 @@ test('content:check explains likely misindented frontmatter keys', async () => {
|
|
|
452
447
|
|
|
453
448
|
assert.equal(result.status, 1, output);
|
|
454
449
|
assert.match(output, /^Content check failed\./m);
|
|
455
|
-
assert.match(output, /Frontmatter line 6 defines "
|
|
456
|
-
assert.match(output, /
|
|
450
|
+
assert.match(output, /Frontmatter line 6 defines "images" at the top level, but it is not a valid top-level content field\./);
|
|
451
|
+
assert.match(output, /Put local image references in norna-image-stack or norna-image-carousel blocks in the Markdown body\./);
|
|
457
452
|
});
|
|
458
453
|
});
|
|
459
454
|
|