@janga/norna 0.7.13 → 0.7.15
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/.astro/data-store.json +1 -1
- 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 +28 -28
- 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/SectionNavigationScript.astro +28 -0
- package/src/components/SiteNavigation.astro +6 -5
- package/src/components/SitePage.astro +8 -8
- package/src/components/SiteSection.astro +18 -21
- 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/src/components/MobileSectionNavigationScript.astro +0 -14
- /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,46 +56,50 @@ 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
|
-
title:
|
|
75
|
-
description: A single page
|
|
67
|
+
title: Section navigation
|
|
68
|
+
description: A single page with sticky section navigation.
|
|
76
69
|
sections:
|
|
77
70
|
intro: {}
|
|
78
71
|
more: {}
|
|
79
72
|
---
|
|
80
73
|
## Intro {#intro}
|
|
81
74
|
|
|
82
|
-
|
|
75
|
+
The section heading is a native anchor target.
|
|
83
76
|
|
|
84
77
|
## More {#more}
|
|
85
78
|
|
|
86
|
-
|
|
79
|
+
The navigation enhancement keeps native anchors clear of the sticky header.
|
|
87
80
|
`);
|
|
88
81
|
|
|
89
82
|
runBuild();
|
|
90
83
|
const instantHtml = await readPage();
|
|
91
|
-
|
|
84
|
+
assert.equal(
|
|
85
|
+
getScripts(instantHtml).length,
|
|
86
|
+
1,
|
|
87
|
+
'Sticky section navigation should load only its anchor-offset enhancement.',
|
|
88
|
+
);
|
|
89
|
+
assert.match(getScripts(instantHtml)[0], /site-top-anchor-offset/);
|
|
92
90
|
assertScrollBehavior(instantHtml, 'auto', 'The default instant scroll mode');
|
|
93
91
|
assert.match(instantHtml, /<nav class="page-nav"/);
|
|
94
92
|
assert.match(instantHtml, /href="#intro"/);
|
|
95
93
|
assert.match(instantHtml, /href="#more"/);
|
|
96
94
|
|
|
97
|
-
await writeConfig('
|
|
95
|
+
await writeConfig('smooth');
|
|
98
96
|
runBuild();
|
|
99
97
|
const browserSmoothHtml = await readPage();
|
|
100
|
-
|
|
98
|
+
assert.equal(
|
|
99
|
+
getScripts(browserSmoothHtml).length,
|
|
100
|
+
1,
|
|
101
|
+
'Browser smooth scrolling should use the same anchor-offset enhancement.',
|
|
102
|
+
);
|
|
101
103
|
assertScrollBehavior(browserSmoothHtml, 'smooth', 'The browser smooth scroll mode');
|
|
102
104
|
|
|
103
105
|
await writeConfig();
|
|
@@ -115,7 +117,7 @@ sections:
|
|
|
115
117
|
|
|
116
118
|
Ordinary Markdown needs no client-side JavaScript.
|
|
117
119
|
`);
|
|
118
|
-
await writeFile(path.join(routeDir, '
|
|
120
|
+
await writeFile(path.join(routeDir, 'content.md'), `---
|
|
119
121
|
title: Details
|
|
120
122
|
description: Static managed images.
|
|
121
123
|
sections:
|
|
@@ -157,7 +159,7 @@ This sentence has additional context.{note-ref}
|
|
|
157
159
|
|
|
158
160
|
{note: The note remains readable without JavaScript, while alignment is enhanced when JavaScript runs.}
|
|
159
161
|
`);
|
|
160
|
-
await writeFile(path.join(routeDir, '
|
|
162
|
+
await writeFile(path.join(routeDir, 'content.md'), `---
|
|
161
163
|
title: Carousel
|
|
162
164
|
description: An interactive image carousel.
|
|
163
165
|
sections:
|
|
@@ -192,7 +194,7 @@ sections:
|
|
|
192
194
|
|
|
193
195
|
The only interactive feature is the banner.
|
|
194
196
|
`);
|
|
195
|
-
await writeFile(path.join(routeDir, '
|
|
197
|
+
await writeFile(path.join(routeDir, 'content.md'), `---
|
|
196
198
|
title: Details
|
|
197
199
|
description: A second plain page.
|
|
198
200
|
sections:
|
|
@@ -202,12 +204,10 @@ sections:
|
|
|
202
204
|
|
|
203
205
|
Plain route content.
|
|
204
206
|
`);
|
|
205
|
-
await writeFile(path.join(siteDir, 'sitewide-content.
|
|
206
|
-
banners:
|
|
207
|
+
await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `banners:
|
|
207
208
|
- id: test-banner
|
|
208
209
|
title: Test banner
|
|
209
210
|
text: This banner can be dismissed.
|
|
210
|
-
---
|
|
211
211
|
`);
|
|
212
212
|
|
|
213
213
|
runBuild();
|