@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
|
@@ -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
|
|
|
@@ -51,14 +51,9 @@ const createTempSite = async ({ underRepoCache = false } = {}) => {
|
|
|
51
51
|
const root = await mkdtemp(path.join(tempParent, 'norna-content-model-v2-'));
|
|
52
52
|
const siteDir = path.join(root, 'site');
|
|
53
53
|
await mkdir(siteDir, { recursive: true });
|
|
54
|
-
await writeFile(path.join(siteDir, 'config.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
`);
|
|
58
|
-
await writeFile(path.join(siteDir, 'theme.md'), `---
|
|
59
|
-
typography:
|
|
60
|
-
preset: text-forward
|
|
61
|
-
---
|
|
54
|
+
await writeFile(path.join(siteDir, 'config.yaml'), 'url: https://example.com/\n');
|
|
55
|
+
await writeFile(path.join(siteDir, 'theme.yaml'), `typography:
|
|
56
|
+
profile: reading
|
|
62
57
|
`);
|
|
63
58
|
return { root, siteDir };
|
|
64
59
|
};
|
|
@@ -124,15 +119,11 @@ test('content model v2 fixture checks and builds', async () => {
|
|
|
124
119
|
test('route theme replaces visual theme while site identity stays at the root', async () => {
|
|
125
120
|
const { root, siteDir } = await createTempSite({ underRepoCache: true });
|
|
126
121
|
try {
|
|
127
|
-
await writeFile(path.join(siteDir, 'theme.
|
|
128
|
-
|
|
129
|
-
preset: text-forward
|
|
130
|
-
---
|
|
122
|
+
await writeFile(path.join(siteDir, 'theme.yaml'), `typography:
|
|
123
|
+
profile: reading
|
|
131
124
|
`);
|
|
132
|
-
await writeFile(path.join(siteDir, 'sitewide-content.
|
|
133
|
-
|
|
134
|
-
brand: Root Brand
|
|
135
|
-
---
|
|
125
|
+
await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `navigation:
|
|
126
|
+
label: Root Brand
|
|
136
127
|
`);
|
|
137
128
|
await mkdir(path.join(siteDir, 'routes', '010-guide'), { recursive: true });
|
|
138
129
|
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
@@ -144,7 +135,7 @@ description: Root page
|
|
|
144
135
|
|
|
145
136
|
Root content.
|
|
146
137
|
`);
|
|
147
|
-
await writeFile(path.join(siteDir, 'routes', '010-guide', '
|
|
138
|
+
await writeFile(path.join(siteDir, 'routes', '010-guide', 'content.md'), `---
|
|
148
139
|
title: Guide
|
|
149
140
|
description: Guide page
|
|
150
141
|
navigation:
|
|
@@ -155,12 +146,9 @@ navigation:
|
|
|
155
146
|
|
|
156
147
|
Route content.
|
|
157
148
|
`);
|
|
158
|
-
await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
presentation:
|
|
162
|
-
palette: light
|
|
163
|
-
---
|
|
149
|
+
await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.yaml'), `typography:
|
|
150
|
+
profile: statement
|
|
151
|
+
palette: light
|
|
164
152
|
`);
|
|
165
153
|
|
|
166
154
|
await runNorna(['--site-dir', siteDir, 'build']);
|
|
@@ -187,7 +175,7 @@ description: Root page
|
|
|
187
175
|
|
|
188
176
|
Root content.
|
|
189
177
|
`);
|
|
190
|
-
await writeFile(path.join(siteDir, 'routes', '010-guide', '
|
|
178
|
+
await writeFile(path.join(siteDir, 'routes', '010-guide', 'content.md'), `---
|
|
191
179
|
title: Guide
|
|
192
180
|
description: Guide page
|
|
193
181
|
---
|
|
@@ -196,16 +184,60 @@ description: Guide page
|
|
|
196
184
|
|
|
197
185
|
Route content.
|
|
198
186
|
`);
|
|
199
|
-
await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
187
|
+
await writeFile(path.join(siteDir, 'routes', '010-guide', 'theme.yaml'), `navigation:
|
|
188
|
+
label: Route Brand
|
|
189
|
+
`);
|
|
190
|
+
|
|
191
|
+
await assert.rejects(
|
|
192
|
+
runNorna(['--site-dir', siteDir, 'config:check']),
|
|
193
|
+
(error) => {
|
|
194
|
+
assert.match(error.output, /may not define navigation\. The site label and logo settings belong in site\/sitewide-content\.yaml/);
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
} finally {
|
|
199
|
+
await rm(root, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('navigation logo settings require one conventional logo file', async () => {
|
|
204
|
+
const { root, siteDir } = await createTempSite();
|
|
205
|
+
try {
|
|
206
|
+
await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `navigation:
|
|
207
|
+
label: Missing logo
|
|
208
|
+
logo:
|
|
209
|
+
height: 2rem
|
|
210
|
+
`);
|
|
211
|
+
|
|
212
|
+
await assert.rejects(
|
|
213
|
+
runNorna(['--site-dir', siteDir, 'config:check']),
|
|
214
|
+
(error) => {
|
|
215
|
+
assert.match(error.output, /navigation\.logo is configured, but no logo file was found/);
|
|
216
|
+
assert.match(error.output, /site\/public\/logo\.svg/);
|
|
217
|
+
return true;
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
} finally {
|
|
221
|
+
await rm(root, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('multiple conventional navigation logo files stop config validation', async () => {
|
|
226
|
+
const { root, siteDir } = await createTempSite();
|
|
227
|
+
try {
|
|
228
|
+
await writeFile(path.join(siteDir, 'sitewide-content.yaml'), `navigation:
|
|
229
|
+
label: Conflicting logos
|
|
203
230
|
`);
|
|
231
|
+
await mkdir(path.join(siteDir, 'public'), { recursive: true });
|
|
232
|
+
await writeFile(path.join(siteDir, 'public', 'logo.svg'), '<svg/>');
|
|
233
|
+
await writeFile(path.join(siteDir, 'public', 'logo.png'), 'png');
|
|
204
234
|
|
|
205
235
|
await assert.rejects(
|
|
206
236
|
runNorna(['--site-dir', siteDir, 'config:check']),
|
|
207
237
|
(error) => {
|
|
208
|
-
assert.match(error.output, /
|
|
238
|
+
assert.match(error.output, /Found multiple logo files/);
|
|
239
|
+
assert.match(error.output, /site\/public\/logo\.svg/);
|
|
240
|
+
assert.match(error.output, /site\/public\/logo\.png/);
|
|
209
241
|
return true;
|
|
210
242
|
},
|
|
211
243
|
);
|
|
@@ -286,7 +318,7 @@ description: Fixture
|
|
|
286
318
|
|
|
287
319
|
The first paragraph points to an explanation.{note-ref}
|
|
288
320
|
|
|
289
|
-
{note: This explanation names \`sitewide-content.
|
|
321
|
+
{note: This explanation names \`sitewide-content.yaml\` and stays beside the
|
|
290
322
|
paragraph on wide screens.}
|
|
291
323
|
|
|
292
324
|
The second paragraph has its own explanation.{note-ref}
|
|
@@ -302,7 +334,7 @@ The second paragraph has its own explanation.{note-ref}
|
|
|
302
334
|
assert.match(html, /<sup class="section-note-ref"><a id="note-ref-home-intro-1" href="#note-home-intro-1" aria-label="Note 1" aria-describedby="note-home-intro-1" data-note-id="note-home-intro-1">1<\/a><\/sup>/);
|
|
303
335
|
assert.match(html, /<aside class="section-note section-note-margin" id="note-home-intro-1" aria-label="Note 1" role="note" data-note-id="note-home-intro-1" style="grid-row: 1;">/);
|
|
304
336
|
assert.match(html, /<a class="section-note-number" href="#note-ref-home-intro-1" aria-label="Note 1">\s*1\s*<\/a>/);
|
|
305
|
-
assert.match(html, /This explanation names <code>sitewide-content\.
|
|
337
|
+
assert.match(html, /This explanation names <code>sitewide-content\.yaml<\/code> and stays beside the paragraph on wide screens\./);
|
|
306
338
|
assert.match(html, /<sup class="section-note-ref"><a id="note-ref-home-intro-2" href="#note-home-intro-2" aria-label="Note 2" aria-describedby="note-home-intro-2" data-note-id="note-home-intro-2">2<\/a><\/sup>/);
|
|
307
339
|
assert.match(html, /<aside class="section-note section-note-margin" id="note-home-intro-2" aria-label="Note 2" role="note" data-note-id="note-home-intro-2" style="grid-row: 2;">/);
|
|
308
340
|
assert.match(html, /This is the second explanation\./);
|
|
@@ -652,7 +684,7 @@ description: Fixture
|
|
|
652
684
|
|
|
653
685
|
Text.
|
|
654
686
|
`);
|
|
655
|
-
await writeFile(path.join(sourceRouteDir, '
|
|
687
|
+
await writeFile(path.join(sourceRouteDir, 'content.md'), `---
|
|
656
688
|
title: Source
|
|
657
689
|
description: Fixture
|
|
658
690
|
---
|
|
@@ -661,7 +693,7 @@ description: Fixture
|
|
|
661
693
|
|
|
662
694
|
Text.
|
|
663
695
|
`);
|
|
664
|
-
await writeFile(path.join(targetRouteDir, '
|
|
696
|
+
await writeFile(path.join(targetRouteDir, 'content.md'), `---
|
|
665
697
|
title: Target
|
|
666
698
|
description: Fixture
|
|
667
699
|
---
|
|
@@ -700,7 +732,7 @@ description: Fixture
|
|
|
700
732
|
|
|
701
733
|
Text.
|
|
702
734
|
`);
|
|
703
|
-
await writeFile(path.join(sourceRouteDir, '
|
|
735
|
+
await writeFile(path.join(sourceRouteDir, 'content.md'), `---
|
|
704
736
|
title: Source
|
|
705
737
|
description: Fixture
|
|
706
738
|
---
|
|
@@ -709,7 +741,7 @@ description: Fixture
|
|
|
709
741
|
|
|
710
742
|
Text.
|
|
711
743
|
`);
|
|
712
|
-
await writeFile(path.join(targetRouteDir, '
|
|
744
|
+
await writeFile(path.join(targetRouteDir, 'content.md'), `---
|
|
713
745
|
title: Target
|
|
714
746
|
description: Fixture
|
|
715
747
|
---
|
|
@@ -756,7 +788,7 @@ description: Fixture
|
|
|
756
788
|
|
|
757
789
|
Text.
|
|
758
790
|
`);
|
|
759
|
-
await writeFile(path.join(sourceRouteDir, '
|
|
791
|
+
await writeFile(path.join(sourceRouteDir, 'content.md'), `---
|
|
760
792
|
title: Source
|
|
761
793
|
description: Fixture
|
|
762
794
|
---
|
|
@@ -765,7 +797,7 @@ description: Fixture
|
|
|
765
797
|
|
|
766
798
|
Text.
|
|
767
799
|
`);
|
|
768
|
-
await writeFile(path.join(targetRouteDir, '
|
|
800
|
+
await writeFile(path.join(targetRouteDir, 'content.md'), `---
|
|
769
801
|
title: Target
|
|
770
802
|
description: Fixture
|
|
771
803
|
---
|
|
@@ -809,7 +841,7 @@ description: Fixture
|
|
|
809
841
|
|
|
810
842
|
Text.
|
|
811
843
|
`);
|
|
812
|
-
await writeFile(path.join(sourceRouteDir, '
|
|
844
|
+
await writeFile(path.join(sourceRouteDir, 'content.md'), `---
|
|
813
845
|
title: Source
|
|
814
846
|
description: Fixture
|
|
815
847
|
---
|
|
@@ -820,7 +852,7 @@ description: Fixture
|
|
|
820
852
|
- image: shared.jpg
|
|
821
853
|
\`\`\`
|
|
822
854
|
`);
|
|
823
|
-
await writeFile(path.join(targetRouteDir, '
|
|
855
|
+
await writeFile(path.join(targetRouteDir, 'content.md'), `---
|
|
824
856
|
title: Target
|
|
825
857
|
description: Fixture
|
|
826
858
|
---
|
|
@@ -836,7 +868,7 @@ description: Fixture
|
|
|
836
868
|
await assert.rejects(
|
|
837
869
|
() => runContentScript(siteDir, ['--write', '--yes']),
|
|
838
870
|
(error) => {
|
|
839
|
-
assert.match(error.output, /Cannot relocate "shared\.jpg" from .*site\/routes\/010-source\/images\/old\/shared\.jpg because it is still referenced from .*site\/routes\/010-source\/
|
|
871
|
+
assert.match(error.output, /Cannot relocate "shared\.jpg" from .*site\/routes\/010-source\/images\/old\/shared\.jpg because it is still referenced from .*site\/routes\/010-source\/content\.md \[old\]\./);
|
|
840
872
|
return true;
|
|
841
873
|
},
|
|
842
874
|
);
|
|
@@ -8,6 +8,20 @@ const repoRoot = path.resolve(import.meta.dirname, '..');
|
|
|
8
8
|
const cliPath = path.join(repoRoot, 'bin', 'norna.mjs');
|
|
9
9
|
const tutorialPath = path.join(repoRoot, 'docs', 'getting-started.md');
|
|
10
10
|
const scratchRoot = path.join(repoRoot, 'todos');
|
|
11
|
+
const obsoleteSourceReferences = [
|
|
12
|
+
'site/config.md',
|
|
13
|
+
'site/theme.md',
|
|
14
|
+
'site/sitewide-content.md',
|
|
15
|
+
'route-content.md',
|
|
16
|
+
'docs/site-structure.md',
|
|
17
|
+
];
|
|
18
|
+
const obsoleteSiteFilenames = new Set([
|
|
19
|
+
'config.md',
|
|
20
|
+
'theme.md',
|
|
21
|
+
'sitewide-content.md',
|
|
22
|
+
'route-content.md',
|
|
23
|
+
]);
|
|
24
|
+
const ignoredSiteDirectories = new Set(['.astro', '.norna', 'dist', 'node_modules']);
|
|
11
25
|
await mkdir(scratchRoot, { recursive: true });
|
|
12
26
|
const tempRoot = await mkdtemp(path.join(scratchRoot, 'norna-documentation-'));
|
|
13
27
|
const tutorialSiteRoot = path.join(tempRoot, 'my-site');
|
|
@@ -42,6 +56,20 @@ const collectMarkdownFiles = async (directory) => {
|
|
|
42
56
|
return files;
|
|
43
57
|
};
|
|
44
58
|
|
|
59
|
+
const collectFiles = async (directory) => {
|
|
60
|
+
const files = [];
|
|
61
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
62
|
+
const entryPath = path.join(directory, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
if (ignoredSiteDirectories.has(entry.name)) continue;
|
|
65
|
+
files.push(...await collectFiles(entryPath));
|
|
66
|
+
} else {
|
|
67
|
+
files.push(entryPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
};
|
|
72
|
+
|
|
45
73
|
const checkLocalMarkdownLinks = async () => {
|
|
46
74
|
const markdownFiles = [
|
|
47
75
|
path.join(repoRoot, 'README.md'),
|
|
@@ -71,6 +99,42 @@ const checkLocalMarkdownLinks = async () => {
|
|
|
71
99
|
assert.deepEqual(missing, [], `Broken local Markdown links:\n${missing.join('\n')}`);
|
|
72
100
|
};
|
|
73
101
|
|
|
102
|
+
const checkObsoleteDocumentationReferences = async () => {
|
|
103
|
+
const markdownFiles = [
|
|
104
|
+
path.join(repoRoot, 'README.md'),
|
|
105
|
+
...await collectMarkdownFiles(path.join(repoRoot, 'docs')),
|
|
106
|
+
...await collectMarkdownFiles(path.join(repoRoot, 'examples')),
|
|
107
|
+
...await collectMarkdownFiles(path.join(repoRoot, 'starters')),
|
|
108
|
+
];
|
|
109
|
+
const obsolete = [];
|
|
110
|
+
|
|
111
|
+
for (const markdownPath of markdownFiles) {
|
|
112
|
+
const source = await readFile(markdownPath, 'utf8');
|
|
113
|
+
for (const reference of obsoleteSourceReferences) {
|
|
114
|
+
if (source.includes(reference)) {
|
|
115
|
+
obsolete.push(`${path.relative(repoRoot, markdownPath)} -> ${reference}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
assert.deepEqual(obsolete, [], `Obsolete Norna source references:\n${obsolete.join('\n')}`);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const checkObsoleteSiteFiles = async () => {
|
|
124
|
+
const siteRoots = [
|
|
125
|
+
path.join(repoRoot, 'site'),
|
|
126
|
+
path.join(repoRoot, 'starters'),
|
|
127
|
+
path.join(repoRoot, 'examples'),
|
|
128
|
+
path.join(repoRoot, 'fixtures'),
|
|
129
|
+
];
|
|
130
|
+
const files = (await Promise.all(siteRoots.map(collectFiles))).flat();
|
|
131
|
+
const obsolete = files
|
|
132
|
+
.filter((filePath) => obsoleteSiteFilenames.has(path.basename(filePath)))
|
|
133
|
+
.map((filePath) => path.relative(repoRoot, filePath));
|
|
134
|
+
|
|
135
|
+
assert.deepEqual(obsolete, [], `Obsolete Norna site files:\n${obsolete.join('\n')}`);
|
|
136
|
+
};
|
|
137
|
+
|
|
74
138
|
try {
|
|
75
139
|
const tutorial = await readFile(tutorialPath, 'utf8');
|
|
76
140
|
const initResult = runCli(['init', tutorialSiteRoot]);
|
|
@@ -84,12 +148,12 @@ try {
|
|
|
84
148
|
extractTutorialFile(tutorial, 'Replace `site/content.md` with:', 'md'),
|
|
85
149
|
),
|
|
86
150
|
writeFile(
|
|
87
|
-
path.join(tutorialSiteRoot, 'site', 'sitewide-content.
|
|
88
|
-
extractTutorialFile(tutorial, 'Replace `site/sitewide-content.
|
|
151
|
+
path.join(tutorialSiteRoot, 'site', 'sitewide-content.yaml'),
|
|
152
|
+
extractTutorialFile(tutorial, 'Replace `site/sitewide-content.yaml` with:', 'yaml'),
|
|
89
153
|
),
|
|
90
154
|
writeFile(
|
|
91
|
-
path.join(tutorialSiteRoot, 'site', 'theme.
|
|
92
|
-
extractTutorialFile(tutorial, 'Replace `site/theme.
|
|
155
|
+
path.join(tutorialSiteRoot, 'site', 'theme.yaml'),
|
|
156
|
+
extractTutorialFile(tutorial, 'Replace `site/theme.yaml` with:', 'yaml'),
|
|
93
157
|
),
|
|
94
158
|
]);
|
|
95
159
|
|
|
@@ -104,6 +168,8 @@ try {
|
|
|
104
168
|
assert.match(html, /Next/);
|
|
105
169
|
|
|
106
170
|
await checkLocalMarkdownLinks();
|
|
171
|
+
await checkObsoleteDocumentationReferences();
|
|
172
|
+
await checkObsoleteSiteFiles();
|
|
107
173
|
|
|
108
174
|
const llms = await readFile(path.join(repoRoot, 'site', 'public', 'llms.txt'), 'utf8');
|
|
109
175
|
for (const match of llms.matchAll(/https:\/\/raw\.githubusercontent\.com\/janga\/norna\/main\/([^\s)]+)/g)) {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { documentationRef } from './lib/documentation-links.mjs';
|
|
6
|
+
import {
|
|
7
|
+
findNornaSiteRoot,
|
|
8
|
+
getImageDefinitionContext,
|
|
9
|
+
getImageCompletionContext,
|
|
10
|
+
getMarkdownDiagnostics,
|
|
11
|
+
getNornaBlockCompletionContext,
|
|
12
|
+
getNornaBlockFieldContext,
|
|
13
|
+
getSitePublicAssetStatus,
|
|
14
|
+
nornaBlockDefinitions,
|
|
15
|
+
} from './lib/editor-language-service.mjs';
|
|
16
|
+
|
|
17
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'norna-editor-language-'));
|
|
18
|
+
const siteRoot = path.join(root, 'site');
|
|
19
|
+
const homeContentPath = path.join(siteRoot, 'content.md');
|
|
20
|
+
const routeContentPath = path.join(siteRoot, 'routes', '010-about', 'content.md');
|
|
21
|
+
|
|
22
|
+
const homeSource = `---
|
|
23
|
+
title: Editor fixture
|
|
24
|
+
description: Exercises project-local image discovery.
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Intro {#intro}
|
|
28
|
+
|
|
29
|
+
Text with a missing note {note-ref}.
|
|
30
|
+
|
|
31
|
+
\`\`\`norna-image-stack
|
|
32
|
+
- image: local.jpg
|
|
33
|
+
- image: portrait.jpg
|
|
34
|
+
\`\`\`
|
|
35
|
+
`;
|
|
36
|
+
const routeSource = `---
|
|
37
|
+
title: About
|
|
38
|
+
description: About this fixture.
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Team {#team}
|
|
42
|
+
|
|
43
|
+
Route content.
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await mkdir(path.join(siteRoot, 'images', 'intro'), { recursive: true });
|
|
48
|
+
await mkdir(path.join(siteRoot, 'routes', '010-about', 'images', 'team'), { recursive: true });
|
|
49
|
+
await mkdir(path.join(siteRoot, 'public'), { recursive: true });
|
|
50
|
+
await writeFile(path.join(siteRoot, 'config.yaml'), 'url: https://example.com/\n');
|
|
51
|
+
await writeFile(path.join(siteRoot, 'sitewide-content.yaml'), `navigation:
|
|
52
|
+
label: Editor fixture
|
|
53
|
+
logo:
|
|
54
|
+
height: 2rem
|
|
55
|
+
`);
|
|
56
|
+
await writeFile(path.join(siteRoot, 'images', 'intro', 'local.jpg'), 'local');
|
|
57
|
+
await writeFile(path.join(siteRoot, 'routes', '010-about', 'images', 'team', 'portrait.jpg'), 'portrait');
|
|
58
|
+
await writeFile(path.join(siteRoot, 'public', 'logo.svg'), '<svg/>');
|
|
59
|
+
await writeFile(path.join(siteRoot, 'public', 'logo.png'), 'logo');
|
|
60
|
+
await writeFile(path.join(siteRoot, 'public', 'favicon.svg'), '<svg/>');
|
|
61
|
+
await writeFile(path.join(siteRoot, 'public', 'Logo.JPG'), 'wrong case');
|
|
62
|
+
await writeFile(path.join(siteRoot, 'public', 'favion.ico'), 'typo');
|
|
63
|
+
await writeFile(path.join(siteRoot, 'public', 'favicon-32x32.png'), 'unrecognized');
|
|
64
|
+
await writeFile(homeContentPath, homeSource);
|
|
65
|
+
await writeFile(routeContentPath, routeSource);
|
|
66
|
+
|
|
67
|
+
assert.equal(await findNornaSiteRoot(homeContentPath), siteRoot);
|
|
68
|
+
const publicAssetStatus = await getSitePublicAssetStatus(homeContentPath);
|
|
69
|
+
assert.deepEqual(publicAssetStatus.logos, ['logo.png', 'logo.svg']);
|
|
70
|
+
assert.deepEqual(publicAssetStatus.browserIcons, ['favicon.svg']);
|
|
71
|
+
assert.equal(publicAssetStatus.issues.filter(({ code }) => code === 'multiple-logo-files').length, 2);
|
|
72
|
+
assert.ok(publicAssetStatus.issues.some(({ code }) => code === 'possible-favicon-typo'));
|
|
73
|
+
assert.ok(publicAssetStatus.issues.some(({ code }) => code === 'unrecognized-favicon-file'));
|
|
74
|
+
assert.ok(publicAssetStatus.issues.some(({ code, filename }) => code === 'public-asset-case' && filename === 'Logo.JPG'));
|
|
75
|
+
await rm(path.join(siteRoot, 'public', 'logo.svg'), { force: true });
|
|
76
|
+
await rm(path.join(siteRoot, 'public', 'logo.png'), { force: true });
|
|
77
|
+
const missingLogoStatus = await getSitePublicAssetStatus(homeContentPath);
|
|
78
|
+
assert.ok(missingLogoStatus.issues.some(({ code, line }) => code === 'missing-logo-file' && line === 3));
|
|
79
|
+
assert.equal(nornaBlockDefinitions['norna-image-stack'].description.includes('vertical stack'), true);
|
|
80
|
+
assert.match(
|
|
81
|
+
nornaBlockDefinitions['norna-image-stack'].documentation,
|
|
82
|
+
new RegExp(`/blob/${documentationRef.replaceAll('.', '\\.')}\/docs/content\\.md#norna-blocks`),
|
|
83
|
+
);
|
|
84
|
+
assert.equal(nornaBlockDefinitions['norna-card-list'].options.layout.default, 'image-top');
|
|
85
|
+
|
|
86
|
+
const stackFieldSource = homeSource.replace('- image: portrait.jpg', '- image: portrait.jpg\n ');
|
|
87
|
+
const stackFieldLine = stackFieldSource.split('\n').findIndex((line) => line === ' ');
|
|
88
|
+
const stackFieldCompletion = getNornaBlockCompletionContext({ source: stackFieldSource, line: stackFieldLine });
|
|
89
|
+
assert.deepEqual(stackFieldCompletion.candidates.map(({ key }) => key), ['alt', 'caption']);
|
|
90
|
+
|
|
91
|
+
const cardValueSource = `${homeSource}\n\`\`\`norna-card-list\nlayout: \n\`\`\`\n`;
|
|
92
|
+
const cardValueLine = cardValueSource.split('\n').findIndex((line) => line === 'layout: ');
|
|
93
|
+
const cardValueCompletion = getNornaBlockCompletionContext({ source: cardValueSource, line: cardValueLine });
|
|
94
|
+
assert.equal(cardValueCompletion.mode, 'value');
|
|
95
|
+
assert.deepEqual(cardValueCompletion.candidates.map(({ label }) => label), ['image-top', 'image-left', 'image-right']);
|
|
96
|
+
assert.equal(
|
|
97
|
+
getNornaBlockFieldContext({ source: cardValueSource, line: cardValueLine }).field.default,
|
|
98
|
+
'image-top',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const completionSource = homeSource.replace('- image: portrait.jpg', '- image: ');
|
|
102
|
+
const completionLine = completionSource.split('\n').findIndex((line) => line === '- image: ');
|
|
103
|
+
const completion = await getImageCompletionContext({
|
|
104
|
+
documentPath: homeContentPath,
|
|
105
|
+
line: completionLine,
|
|
106
|
+
source: completionSource,
|
|
107
|
+
});
|
|
108
|
+
assert.ok(completion);
|
|
109
|
+
assert.deepEqual(
|
|
110
|
+
completion.candidates.map(({ filename, isExpected }) => [filename, isExpected]),
|
|
111
|
+
[['local.jpg', true], ['portrait.jpg', false]],
|
|
112
|
+
);
|
|
113
|
+
const localDefinitionLine = homeSource.split('\n').findIndex((line) => line === '- image: local.jpg');
|
|
114
|
+
const localDefinition = await getImageDefinitionContext({
|
|
115
|
+
documentPath: homeContentPath,
|
|
116
|
+
line: localDefinitionLine,
|
|
117
|
+
source: homeSource,
|
|
118
|
+
});
|
|
119
|
+
assert.deepEqual(localDefinition.files, [path.join(siteRoot, 'images', 'intro', 'local.jpg')]);
|
|
120
|
+
|
|
121
|
+
const diagnostics = await getMarkdownDiagnostics({ documentPath: homeContentPath, source: homeSource });
|
|
122
|
+
assert.ok(diagnostics.some(({ code, message }) => code === 'image-needs-sync' && message.includes('Run "norna content:sync"')));
|
|
123
|
+
assert.ok(diagnostics.some(({ message }) => message.includes('has no following "{note: ...}"')));
|
|
124
|
+
const missingIdDiagnostics = await getMarkdownDiagnostics({
|
|
125
|
+
documentPath: homeContentPath,
|
|
126
|
+
source: homeSource.replace('## Intro {#intro}', '## Intro'),
|
|
127
|
+
});
|
|
128
|
+
assert.ok(missingIdDiagnostics.some(({ code }) => code === 'missing-section-id'));
|
|
129
|
+
const unclosedBlockDiagnostics = await getMarkdownDiagnostics({
|
|
130
|
+
documentPath: homeContentPath,
|
|
131
|
+
source: homeSource.replace(/\n```\n$/, '\n'),
|
|
132
|
+
});
|
|
133
|
+
assert.ok(unclosedBlockDiagnostics.some(({ code }) => code === 'unclosed-norna-block'));
|
|
134
|
+
const markdownImageDiagnostics = await getMarkdownDiagnostics({
|
|
135
|
+
documentPath: homeContentPath,
|
|
136
|
+
source: homeSource.replace('Text with a missing note {note-ref}.', ''),
|
|
137
|
+
});
|
|
138
|
+
assert.ok(markdownImageDiagnostics.some(({ code }) => code === 'local-markdown-image'));
|
|
139
|
+
|
|
140
|
+
await writeFile(routeContentPath, routeSource.replace('Route content.', `\`\`\`norna-image-stack
|
|
141
|
+
- image: portrait.jpg
|
|
142
|
+
\`\`\``));
|
|
143
|
+
const sharedDiagnostics = await getMarkdownDiagnostics({ documentPath: homeContentPath, source: homeSource });
|
|
144
|
+
assert.ok(sharedDiagnostics.some(({ message }) => message.includes('is still referenced by')));
|
|
145
|
+
|
|
146
|
+
await mkdir(path.join(siteRoot, 'routes', '020-contact', 'images', 'people'), { recursive: true });
|
|
147
|
+
await writeFile(path.join(siteRoot, 'routes', '020-contact', 'images', 'people', 'portrait.jpg'), 'duplicate');
|
|
148
|
+
const ambiguousDiagnostics = await getMarkdownDiagnostics({ documentPath: homeContentPath, source: homeSource });
|
|
149
|
+
assert.ok(ambiguousDiagnostics.some(({ message }) => message.includes('is ambiguous')));
|
|
150
|
+
|
|
151
|
+
const missingSource = homeSource.replace('portrait.jpg', 'missing.jpg');
|
|
152
|
+
const missingDiagnostics = await getMarkdownDiagnostics({ documentPath: homeContentPath, source: missingSource });
|
|
153
|
+
assert.ok(missingDiagnostics.some(({ message }) => message.includes('was not found')));
|
|
154
|
+
|
|
155
|
+
console.log('Editor language service tests passed.');
|
|
156
|
+
} finally {
|
|
157
|
+
await rm(root, { recursive: true, force: true });
|
|
158
|
+
}
|