@janga/norna 0.7.6 → 0.7.7
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 +5 -5
- package/astro.config.mjs +76 -0
- package/bin/norna-cli.mjs +2 -2
- package/docs/README.md +3 -3
- package/docs/commands.md +6 -6
- package/docs/configuration.md +3 -2
- package/docs/content.md +152 -91
- package/docs/design/site-examples-structure.md +1 -1
- package/docs/engine-development.md +3 -3
- package/docs/getting-started.md +4 -3
- package/docs/images-and-metadata.md +112 -20
- package/docs/routes.md +9 -12
- package/docs/site-structure.md +4 -3
- package/docs/theme.md +8 -7
- package/docs/typography.md +5 -5
- package/examples/dog-gallery/site/content.md +41 -45
- package/examples/dog-gallery/site/routes/010-dog-care/route-content.md +1 -3
- package/fixtures/basic/site/content.md +2 -4
- package/fixtures/content-model-v2/.astro/collections/site.schema.json +776 -0
- package/fixtures/content-model-v2/.astro/collections/theme.schema.json +668 -0
- package/fixtures/content-model-v2/.astro/content-assets.mjs +4 -0
- package/fixtures/content-model-v2/.astro/content-modules.mjs +1 -0
- package/fixtures/content-model-v2/.astro/content.d.ts +175 -0
- package/fixtures/content-model-v2/.astro/data-store.json +1 -0
- package/fixtures/content-model-v2/.astro/dev-local.json +8 -0
- package/fixtures/content-model-v2/.astro/dev.json +13 -0
- package/fixtures/content-model-v2/.astro/dev.log +231 -0
- package/fixtures/content-model-v2/.astro/settings.json +5 -0
- package/fixtures/content-model-v2/.astro/types.d.ts +2 -0
- package/fixtures/content-model-v2/dist/_astro/GalleryGrid.astro_astro_type_script_index_0_lang.B0kxDyGz.js +1 -0
- package/fixtures/content-model-v2/dist/_astro/SitePage.BbfZub4g.css +1 -0
- package/fixtures/content-model-v2/dist/guide/index.html +3 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/detail-27930b14-971.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/duplicate-49c98f6c-971.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/intro/hero-6d588a83-971.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/plain/image-3770d763-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/routes/010-guide/images/intro/duplicate-5cf614d2-960.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-one-3770d763-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-three-5bdec65e-768.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1080.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1440.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-1600.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-480.webp +0 -0
- package/fixtures/content-model-v2/dist/images/generated/timed/slide-two-26c08aee-768.webp +0 -0
- package/fixtures/content-model-v2/dist/index.html +8 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/_metadata.json +80 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js +1389 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/embla-carousel.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/package.json +3 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
- package/fixtures/content-model-v2/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
- package/fixtures/content-model-v2/site/.norna/generated-images.json +210 -0
- package/fixtures/content-model-v2/site/config.mjs +11 -0
- package/fixtures/content-model-v2/site/content.md +84 -0
- package/fixtures/content-model-v2/site/images/card-section/adopt.svg +9 -0
- package/fixtures/content-model-v2/site/images/card-section/foster.svg +12 -0
- package/fixtures/content-model-v2/site/images/intro/detail.jpg +0 -0
- package/fixtures/content-model-v2/site/images/intro/duplicate.jpg +0 -0
- package/fixtures/content-model-v2/site/images/intro/hero.jpg +0 -0
- package/fixtures/content-model-v2/site/images/plain/image.jpg +0 -0
- package/fixtures/content-model-v2/site/images/timed/slide-one.jpg +0 -0
- package/fixtures/content-model-v2/site/images/timed/slide-three.jpg +0 -0
- package/fixtures/content-model-v2/site/images/timed/slide-two.jpg +0 -0
- package/fixtures/content-model-v2/site/routes/010-guide/images/intro/duplicate.jpg +0 -0
- package/fixtures/content-model-v2/site/routes/010-guide/route-content.md +17 -0
- package/fixtures/content-model-v2/site/theme.md +9 -0
- package/package.json +3 -2
- package/scripts/deploy-site.mjs +11 -4
- package/scripts/dev-local.mjs +6 -0
- package/scripts/generate-images.mjs +132 -56
- package/scripts/lib/image-dimensions.mjs +26 -0
- package/scripts/lib/norna-markdown-blocks.mjs +643 -0
- package/scripts/lib/site-content.mjs +31 -29
- package/scripts/show-typography.mjs +17 -9
- package/scripts/sync-content-sections.mjs +280 -411
- package/scripts/test-content-check.mjs +59 -45
- package/scripts/test-content-model-v2.mjs +998 -0
- package/scripts/test-engine-commands.mjs +1 -1
- package/scripts/test-package-check.mjs +36 -8
- package/scripts/test-temporary-visibility.mjs +3 -6
- package/src/components/CardList.astro +77 -0
- package/src/components/GalleryGrid.astro +22 -12
- package/src/components/SiteNavigation.astro +5 -4
- package/src/components/SitePage.astro +9 -2
- package/src/components/SiteSection.astro +35 -13
- package/src/content.config.ts +4 -17
- package/src/lib/generatedImages.ts +50 -10
- package/src/lib/sectionContent.ts +130 -35
- package/src/styles/global.css +187 -0
- package/starters/basic/site/content.md +10 -6
- package/starters/project/site/content.md +0 -9
- package/starters/project/site/routes/010-guide/route-content.md +0 -5
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { access, cp, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const repoRoot = path.resolve(import.meta.dirname, '..');
|
|
11
|
+
const nodeBin = process.execPath;
|
|
12
|
+
const nornaBin = path.join(repoRoot, 'bin', 'norna.mjs');
|
|
13
|
+
const contentScript = path.join(repoRoot, 'scripts', 'sync-content-sections.mjs');
|
|
14
|
+
const fixtureSiteDir = path.join(repoRoot, 'fixtures', 'content-model-v2', 'site');
|
|
15
|
+
const fixtureRoot = path.dirname(fixtureSiteDir);
|
|
16
|
+
|
|
17
|
+
const runNorna = async (args, options = {}) => {
|
|
18
|
+
try {
|
|
19
|
+
return await execFileAsync(nodeBin, [nornaBin, ...args], {
|
|
20
|
+
cwd: repoRoot,
|
|
21
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
22
|
+
...options,
|
|
23
|
+
});
|
|
24
|
+
} catch (error) {
|
|
25
|
+
error.output = `${error.stdout ?? ''}${error.stderr ?? ''}`;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const runContentScript = async (siteDir, args) => {
|
|
31
|
+
try {
|
|
32
|
+
return await execFileAsync(nodeBin, [contentScript, ...args], {
|
|
33
|
+
cwd: repoRoot,
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
NORNA_SITE_DIR: siteDir,
|
|
37
|
+
},
|
|
38
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
error.output = `${error.stdout ?? ''}${error.stderr ?? ''}`;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const createTempSite = async ({ underRepoCache = false } = {}) => {
|
|
47
|
+
const tempParent = underRepoCache
|
|
48
|
+
? path.join(repoRoot, 'node_modules', '.cache')
|
|
49
|
+
: os.tmpdir();
|
|
50
|
+
await mkdir(tempParent, { recursive: true });
|
|
51
|
+
const root = await mkdtemp(path.join(tempParent, 'norna-content-model-v2-'));
|
|
52
|
+
const siteDir = path.join(root, 'site');
|
|
53
|
+
await mkdir(siteDir, { recursive: true });
|
|
54
|
+
await writeFile(path.join(siteDir, 'config.mjs'), `export default {
|
|
55
|
+
site: {
|
|
56
|
+
url: 'https://example.com',
|
|
57
|
+
basePath: '/',
|
|
58
|
+
},
|
|
59
|
+
github: {
|
|
60
|
+
branch: 'main',
|
|
61
|
+
repo: 'owner/repo',
|
|
62
|
+
pagesWorkflow: 'Deploy to GitHub Pages',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
`);
|
|
66
|
+
await writeFile(path.join(siteDir, 'theme.md'), `---
|
|
67
|
+
typography:
|
|
68
|
+
preset: text-forward
|
|
69
|
+
---
|
|
70
|
+
`);
|
|
71
|
+
return { root, siteDir };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const fileExists = async (filePath) => access(filePath).then(() => true, () => false);
|
|
75
|
+
|
|
76
|
+
const createTempFixtureCopy = async () => {
|
|
77
|
+
const tempParent = path.join(repoRoot, 'node_modules', '.cache');
|
|
78
|
+
await mkdir(tempParent, { recursive: true });
|
|
79
|
+
const root = await mkdtemp(path.join(tempParent, 'norna-content-model-v2-fixture-'));
|
|
80
|
+
const fixtureCopyRoot = path.join(root, 'content-model-v2');
|
|
81
|
+
|
|
82
|
+
await cp(fixtureRoot, fixtureCopyRoot, {
|
|
83
|
+
recursive: true,
|
|
84
|
+
filter: (source) => ![
|
|
85
|
+
path.join(fixtureRoot, '.astro'),
|
|
86
|
+
path.join(fixtureRoot, 'dist'),
|
|
87
|
+
path.join(fixtureRoot, 'node_modules'),
|
|
88
|
+
path.join(fixtureSiteDir, '.norna'),
|
|
89
|
+
].some((ignoredPath) => source === ignoredPath || source.startsWith(`${ignoredPath}${path.sep}`)),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
root,
|
|
94
|
+
siteDir: path.join(fixtureCopyRoot, 'site'),
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
test('content model v2 fixture checks and builds', async () => {
|
|
99
|
+
const { root, siteDir } = await createTempFixtureCopy();
|
|
100
|
+
try {
|
|
101
|
+
await runNorna(['--site-dir', siteDir, 'content:check']);
|
|
102
|
+
await runNorna(['--site-dir', siteDir, 'build']);
|
|
103
|
+
|
|
104
|
+
const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
|
|
105
|
+
assert.ok(manifest['images/intro/duplicate.jpg']);
|
|
106
|
+
assert.ok(manifest['routes/010-guide/images/intro/duplicate.jpg']);
|
|
107
|
+
} finally {
|
|
108
|
+
await rm(root, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('content:check warns for local Markdown images but not external Markdown images', async () => {
|
|
113
|
+
const { root, siteDir } = await createTempSite();
|
|
114
|
+
try {
|
|
115
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
116
|
+
title: Markdown Image Warning
|
|
117
|
+
description: Fixture
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Intro {#intro}
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+

|
|
125
|
+
|
|
126
|
+

|
|
127
|
+
`);
|
|
128
|
+
|
|
129
|
+
const { stdout } = await runContentScript(siteDir, ['--check']);
|
|
130
|
+
assert.match(stdout, /Markdown image "portrait\.jpg" references a local image that is not managed by Norna\./);
|
|
131
|
+
assert.doesNotMatch(stdout, /https:\/\/example\.com\/portrait\.jpg/);
|
|
132
|
+
assert.doesNotMatch(stdout, /\/favicon\.svg/);
|
|
133
|
+
} finally {
|
|
134
|
+
await rm(root, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('managed SVG images are copied as static image output', async () => {
|
|
139
|
+
const { root, siteDir } = await createTempSite();
|
|
140
|
+
try {
|
|
141
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
142
|
+
title: SVG Fixture
|
|
143
|
+
description: Fixture
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Intro {#intro}
|
|
147
|
+
|
|
148
|
+
\`\`\`norna-image-stack
|
|
149
|
+
- image: diagram.svg
|
|
150
|
+
alt: Diagram
|
|
151
|
+
\`\`\`
|
|
152
|
+
`);
|
|
153
|
+
await mkdir(path.join(siteDir, 'images', 'intro'), { recursive: true });
|
|
154
|
+
await writeFile(path.join(siteDir, 'images', 'intro', 'diagram.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620"/></svg>\n');
|
|
155
|
+
|
|
156
|
+
await runContentScript(siteDir, ['--check']);
|
|
157
|
+
await runNorna(['--site-dir', siteDir, 'images']);
|
|
158
|
+
|
|
159
|
+
const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
|
|
160
|
+
const entry = manifest['images/intro/diagram.svg'];
|
|
161
|
+
assert.equal(entry.kind, 'static');
|
|
162
|
+
assert.equal(entry.width, 900);
|
|
163
|
+
assert.equal(entry.height, 620);
|
|
164
|
+
assert.match(entry.src, /^\/images\/original\/images\/intro\/diagram-[a-f0-9]{8}\.svg$/);
|
|
165
|
+
assert.equal(entry.variants, undefined);
|
|
166
|
+
assert.equal(await fileExists(path.join(siteDir, '.norna', 'public', entry.src.replace(/^\//, ''))), true);
|
|
167
|
+
} finally {
|
|
168
|
+
await rm(root, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('updated managed SVG images get updated static output', async () => {
|
|
173
|
+
const { root, siteDir } = await createTempSite();
|
|
174
|
+
try {
|
|
175
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
176
|
+
title: SVG Update Fixture
|
|
177
|
+
description: Fixture
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Intro {#intro}
|
|
181
|
+
|
|
182
|
+
\`\`\`norna-image-stack
|
|
183
|
+
- image: diagram.svg
|
|
184
|
+
alt: Diagram
|
|
185
|
+
\`\`\`
|
|
186
|
+
`);
|
|
187
|
+
await mkdir(path.join(siteDir, 'images', 'intro'), { recursive: true });
|
|
188
|
+
const sourcePath = path.join(siteDir, 'images', 'intro', 'diagram.svg');
|
|
189
|
+
await writeFile(sourcePath, '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620" fill="red"/></svg>\n');
|
|
190
|
+
|
|
191
|
+
await runContentScript(siteDir, ['--check']);
|
|
192
|
+
await runNorna(['--site-dir', siteDir, 'images']);
|
|
193
|
+
|
|
194
|
+
const firstManifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
|
|
195
|
+
const firstEntry = firstManifest['images/intro/diagram.svg'];
|
|
196
|
+
const firstOutputPath = path.join(siteDir, '.norna', 'public', firstEntry.src.replace(/^\//, ''));
|
|
197
|
+
|
|
198
|
+
await writeFile(sourcePath, '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620" fill="blue"/></svg>\n');
|
|
199
|
+
await runNorna(['--site-dir', siteDir, 'images']);
|
|
200
|
+
|
|
201
|
+
const secondManifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
|
|
202
|
+
const secondEntry = secondManifest['images/intro/diagram.svg'];
|
|
203
|
+
const secondOutputPath = path.join(siteDir, '.norna', 'public', secondEntry.src.replace(/^\//, ''));
|
|
204
|
+
|
|
205
|
+
assert.notEqual(secondEntry.sourceHash, firstEntry.sourceHash);
|
|
206
|
+
assert.notEqual(secondEntry.src, firstEntry.src);
|
|
207
|
+
assert.equal(await fileExists(firstOutputPath), false);
|
|
208
|
+
assert.match(await readFile(secondOutputPath, 'utf8'), /fill="blue"/);
|
|
209
|
+
} finally {
|
|
210
|
+
await rm(root, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('content:check warns when carousel SVG images have no intrinsic aspect ratio', async () => {
|
|
215
|
+
const { root, siteDir } = await createTempSite();
|
|
216
|
+
try {
|
|
217
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
218
|
+
title: SVG Carousel Fixture
|
|
219
|
+
description: Fixture
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Intro {#intro}
|
|
223
|
+
|
|
224
|
+
\`\`\`norna-image-carousel
|
|
225
|
+
- image: first.svg
|
|
226
|
+
alt: First
|
|
227
|
+
- image: second.svg
|
|
228
|
+
alt: Second
|
|
229
|
+
\`\`\`
|
|
230
|
+
`);
|
|
231
|
+
await mkdir(path.join(siteDir, 'images', 'intro'), { recursive: true });
|
|
232
|
+
await writeFile(path.join(siteDir, 'images', 'intro', 'first.svg'), '<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100"/></svg>\n');
|
|
233
|
+
await writeFile(path.join(siteDir, 'images', 'intro', 'second.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100"/></svg>\n');
|
|
234
|
+
|
|
235
|
+
const { stdout } = await runContentScript(siteDir, ['--check']);
|
|
236
|
+
assert.match(stdout, /Content check completed with warnings\./);
|
|
237
|
+
assert.match(stdout, /Carousel on line \d+ uses images without an intrinsic aspect ratio: first\.svg\./);
|
|
238
|
+
} finally {
|
|
239
|
+
await rm(root, { recursive: true, force: true });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('norna-card-list images are managed image references', async () => {
|
|
244
|
+
const { root, siteDir } = await createTempSite({ underRepoCache: true });
|
|
245
|
+
try {
|
|
246
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
247
|
+
title: Card List Fixture
|
|
248
|
+
description: Fixture
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Help {#help}
|
|
252
|
+
|
|
253
|
+
\`\`\`norna-card-list
|
|
254
|
+
layout: image-left
|
|
255
|
+
flow: stack
|
|
256
|
+
size: l
|
|
257
|
+
width: narrow
|
|
258
|
+
|
|
259
|
+
- title: Adopt
|
|
260
|
+
text: Give a dog a new home.
|
|
261
|
+
image: adopt.svg
|
|
262
|
+
link: /adopt/
|
|
263
|
+
badge-text: Recommended
|
|
264
|
+
- title: Donate
|
|
265
|
+
text: Support the shelter.
|
|
266
|
+
\`\`\`
|
|
267
|
+
`);
|
|
268
|
+
await mkdir(path.join(siteDir, 'images', 'help'), { recursive: true });
|
|
269
|
+
await writeFile(path.join(siteDir, 'images', 'help', 'adopt.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 100"><rect width="160" height="100"/></svg>\n');
|
|
270
|
+
|
|
271
|
+
const { stdout } = await runContentScript(siteDir, ['--check']);
|
|
272
|
+
assert.match(stdout, /Content check passed\./);
|
|
273
|
+
|
|
274
|
+
await runNorna(['--site-dir', siteDir, 'images']);
|
|
275
|
+
const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
|
|
276
|
+
assert.equal(manifest['images/help/adopt.svg'].kind, 'static');
|
|
277
|
+
|
|
278
|
+
await runNorna(['--site-dir', siteDir, 'build']);
|
|
279
|
+
const html = await readFile(path.join(root, 'dist', 'index.html'), 'utf8');
|
|
280
|
+
assert.match(html, /card-list-layout-image-left/);
|
|
281
|
+
assert.match(html, /card-list-flow-stack/);
|
|
282
|
+
assert.match(html, /card-list-size-l/);
|
|
283
|
+
assert.match(html, /card-list-width-narrow/);
|
|
284
|
+
assert.match(html, /card-list-item-has-image/);
|
|
285
|
+
assert.match(html, /class="card-list-badge">Recommended<\/p>/);
|
|
286
|
+
assert.match(html, /Give a dog a new home\./);
|
|
287
|
+
assert.match(html, /href="\/adopt\/"/);
|
|
288
|
+
assert.match(html, /src="\/images\/original\/images\/help\/adopt-[a-f0-9]{8}\.svg"/);
|
|
289
|
+
} finally {
|
|
290
|
+
await rm(root, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('content:check fails when a norna-card-list image file is missing', async () => {
|
|
295
|
+
const { root, siteDir } = await createTempSite();
|
|
296
|
+
try {
|
|
297
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
298
|
+
title: Missing Card Image
|
|
299
|
+
description: Fixture
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Help {#help}
|
|
303
|
+
|
|
304
|
+
\`\`\`norna-card-list
|
|
305
|
+
- title: Adopt
|
|
306
|
+
image: missing.svg
|
|
307
|
+
\`\`\`
|
|
308
|
+
`);
|
|
309
|
+
|
|
310
|
+
await assert.rejects(
|
|
311
|
+
() => runContentScript(siteDir, ['--check']),
|
|
312
|
+
(error) => {
|
|
313
|
+
assert.match(error.output, /Image "missing\.svg" does not exist at .*site\/images\/help\/missing\.svg or anywhere under .*site\/images\//);
|
|
314
|
+
return true;
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
} finally {
|
|
318
|
+
await rm(root, { recursive: true, force: true });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('content:check fails for malformed norna-card-list blocks', async () => {
|
|
323
|
+
const { root, siteDir } = await createTempSite();
|
|
324
|
+
try {
|
|
325
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
326
|
+
title: Malformed Card List
|
|
327
|
+
description: Fixture
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Help {#help}
|
|
331
|
+
|
|
332
|
+
\`\`\`norna-card-list
|
|
333
|
+
- text: Missing title
|
|
334
|
+
\`\`\`
|
|
335
|
+
`);
|
|
336
|
+
|
|
337
|
+
await assert.rejects(
|
|
338
|
+
() => runContentScript(siteDir, ['--check']),
|
|
339
|
+
(error) => {
|
|
340
|
+
assert.match(error.output, /Invalid norna-card-list entry "- text: Missing title"\. Start each card with "- title: Card title"\./);
|
|
341
|
+
return true;
|
|
342
|
+
},
|
|
343
|
+
);
|
|
344
|
+
} finally {
|
|
345
|
+
await rm(root, { recursive: true, force: true });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('content:check fails for invalid norna-card-list options', async () => {
|
|
350
|
+
const { root, siteDir } = await createTempSite();
|
|
351
|
+
try {
|
|
352
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
353
|
+
title: Invalid Card List Options
|
|
354
|
+
description: Fixture
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Help {#help}
|
|
358
|
+
|
|
359
|
+
\`\`\`norna-card-list
|
|
360
|
+
layout: floating
|
|
361
|
+
flow: list
|
|
362
|
+
size: huge
|
|
363
|
+
width: full
|
|
364
|
+
|
|
365
|
+
- title: Adopt
|
|
366
|
+
text: Give a dog a new home.
|
|
367
|
+
\`\`\`
|
|
368
|
+
`);
|
|
369
|
+
|
|
370
|
+
await assert.rejects(
|
|
371
|
+
() => runContentScript(siteDir, ['--check']),
|
|
372
|
+
(error) => {
|
|
373
|
+
assert.match(error.output, /Invalid norna-card-list layout "floating"\. Use one of: image-top, image-left, image-right\./);
|
|
374
|
+
return true;
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
} finally {
|
|
378
|
+
await rm(root, { recursive: true, force: true });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('content:check fails for invalid norna-card-list width', async () => {
|
|
383
|
+
const { root, siteDir } = await createTempSite();
|
|
384
|
+
try {
|
|
385
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
386
|
+
title: Invalid Card List Width
|
|
387
|
+
description: Fixture
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Help {#help}
|
|
391
|
+
|
|
392
|
+
\`\`\`norna-card-list
|
|
393
|
+
width: full
|
|
394
|
+
|
|
395
|
+
- title: Adopt
|
|
396
|
+
text: Give a dog a new home.
|
|
397
|
+
\`\`\`
|
|
398
|
+
`);
|
|
399
|
+
|
|
400
|
+
await assert.rejects(
|
|
401
|
+
() => runContentScript(siteDir, ['--check']),
|
|
402
|
+
(error) => {
|
|
403
|
+
assert.match(error.output, /Invalid norna-card-list width "full"\. Use one of: text, narrow, normal, wide\./);
|
|
404
|
+
return true;
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
} finally {
|
|
408
|
+
await rm(root, { recursive: true, force: true });
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('content:check fails for invalid norna-card-list size', async () => {
|
|
413
|
+
const { root, siteDir } = await createTempSite();
|
|
414
|
+
try {
|
|
415
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
416
|
+
title: Invalid Card List Size
|
|
417
|
+
description: Fixture
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Help {#help}
|
|
421
|
+
|
|
422
|
+
\`\`\`norna-card-list
|
|
423
|
+
size: huge
|
|
424
|
+
|
|
425
|
+
- title: Adopt
|
|
426
|
+
text: Give a dog a new home.
|
|
427
|
+
\`\`\`
|
|
428
|
+
`);
|
|
429
|
+
|
|
430
|
+
await assert.rejects(
|
|
431
|
+
() => runContentScript(siteDir, ['--check']),
|
|
432
|
+
(error) => {
|
|
433
|
+
assert.match(error.output, /Invalid norna-card-list size "huge"\. Use one of: s, m, l, xl\./);
|
|
434
|
+
return true;
|
|
435
|
+
},
|
|
436
|
+
);
|
|
437
|
+
} finally {
|
|
438
|
+
await rm(root, { recursive: true, force: true });
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('content:sync moves Norna-managed images inside the same page image root', async () => {
|
|
443
|
+
const { root, siteDir } = await createTempSite();
|
|
444
|
+
try {
|
|
445
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
446
|
+
title: Sync Fixture
|
|
447
|
+
description: Fixture
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## Work {#work}
|
|
451
|
+
|
|
452
|
+
\`\`\`norna-image-stack
|
|
453
|
+
- image: moved.jpg
|
|
454
|
+
alt: Moved image
|
|
455
|
+
\`\`\`
|
|
456
|
+
`);
|
|
457
|
+
await mkdir(path.join(siteDir, 'images', 'old'), { recursive: true });
|
|
458
|
+
await writeFile(path.join(siteDir, 'images', 'old', 'moved.jpg'), 'fixture image');
|
|
459
|
+
|
|
460
|
+
await runContentScript(siteDir, ['--write', '--yes']);
|
|
461
|
+
|
|
462
|
+
const moved = await readFile(path.join(siteDir, 'images', 'work', 'moved.jpg'), 'utf8');
|
|
463
|
+
assert.equal(moved, 'fixture image');
|
|
464
|
+
} finally {
|
|
465
|
+
await rm(root, { recursive: true, force: true });
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test('content:sync moves managed SVG images inside the same page image root', async () => {
|
|
470
|
+
const { root, siteDir } = await createTempSite();
|
|
471
|
+
try {
|
|
472
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
473
|
+
title: SVG Sync Fixture
|
|
474
|
+
description: Fixture
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Work {#work}
|
|
478
|
+
|
|
479
|
+
\`\`\`norna-image-stack
|
|
480
|
+
- image: moved.svg
|
|
481
|
+
alt: Moved diagram
|
|
482
|
+
\`\`\`
|
|
483
|
+
`);
|
|
484
|
+
await mkdir(path.join(siteDir, 'images', 'old'), { recursive: true });
|
|
485
|
+
await writeFile(path.join(siteDir, 'images', 'old', 'moved.svg'), '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"></svg>\n');
|
|
486
|
+
|
|
487
|
+
await runContentScript(siteDir, ['--write', '--yes']);
|
|
488
|
+
|
|
489
|
+
const moved = await readFile(path.join(siteDir, 'images', 'work', 'moved.svg'), 'utf8');
|
|
490
|
+
assert.match(moved, /viewBox="0 0 10 10"/);
|
|
491
|
+
} finally {
|
|
492
|
+
await rm(root, { recursive: true, force: true });
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('CLI content:sync refreshes generated images after moving a carousel image', async () => {
|
|
497
|
+
const { root, siteDir } = await createTempSite();
|
|
498
|
+
try {
|
|
499
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
500
|
+
title: Sync Fixture
|
|
501
|
+
description: Fixture
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Work {#work}
|
|
505
|
+
|
|
506
|
+
\`\`\`norna-image-carousel
|
|
507
|
+
- image: slide-one.jpg
|
|
508
|
+
alt: Slide one
|
|
509
|
+
- image: slide-two.jpg
|
|
510
|
+
alt: Slide two
|
|
511
|
+
\`\`\`
|
|
512
|
+
`);
|
|
513
|
+
await mkdir(path.join(siteDir, 'images', 'old'), { recursive: true });
|
|
514
|
+
await mkdir(path.join(siteDir, 'images', 'work'), { recursive: true });
|
|
515
|
+
await cp(path.join(fixtureSiteDir, 'images', 'intro', 'hero.jpg'), path.join(siteDir, 'images', 'old', 'slide-one.jpg'));
|
|
516
|
+
await cp(path.join(fixtureSiteDir, 'images', 'intro', 'detail.jpg'), path.join(siteDir, 'images', 'work', 'slide-two.jpg'), { force: true });
|
|
517
|
+
|
|
518
|
+
await runNorna(['--site-dir', siteDir, 'content:sync', '--yes']);
|
|
519
|
+
|
|
520
|
+
const manifest = JSON.parse(await readFile(path.join(siteDir, '.norna', 'generated-images.json'), 'utf8'));
|
|
521
|
+
assert.equal(await fileExists(path.join(siteDir, 'images', 'work', 'slide-one.jpg')), true);
|
|
522
|
+
assert.ok(manifest['images/work/slide-one.jpg']);
|
|
523
|
+
assert.ok(manifest['images/work/slide-two.jpg']);
|
|
524
|
+
} finally {
|
|
525
|
+
await rm(root, { recursive: true, force: true });
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test('content:check fails when a level 2 section is missing an explicit id', async () => {
|
|
530
|
+
const { root, siteDir } = await createTempSite();
|
|
531
|
+
try {
|
|
532
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
533
|
+
title: Missing Id
|
|
534
|
+
description: Fixture
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Intro
|
|
538
|
+
|
|
539
|
+
This section has no explicit id.
|
|
540
|
+
`);
|
|
541
|
+
|
|
542
|
+
await assert.rejects(
|
|
543
|
+
() => runContentScript(siteDir, ['--check']),
|
|
544
|
+
(error) => {
|
|
545
|
+
assert.match(error.output, /Section heading "Intro" is missing an explicit id\./);
|
|
546
|
+
return true;
|
|
547
|
+
},
|
|
548
|
+
);
|
|
549
|
+
} finally {
|
|
550
|
+
await rm(root, { recursive: true, force: true });
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test('content:check fails for malformed Norna image blocks', async () => {
|
|
555
|
+
const { root, siteDir } = await createTempSite();
|
|
556
|
+
try {
|
|
557
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
558
|
+
title: Malformed Block
|
|
559
|
+
description: Fixture
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## Intro {#intro}
|
|
563
|
+
|
|
564
|
+
\`\`\`norna-image-stack
|
|
565
|
+
image hero.jpg
|
|
566
|
+
alt: Hero
|
|
567
|
+
\`\`\`
|
|
568
|
+
`);
|
|
569
|
+
|
|
570
|
+
await assert.rejects(
|
|
571
|
+
() => runContentScript(siteDir, ['--check']),
|
|
572
|
+
(error) => {
|
|
573
|
+
assert.match(error.output, /Invalid norna-image-stack entry "image hero\.jpg"\. Start each image with "- image: filename\.jpg"\./);
|
|
574
|
+
return true;
|
|
575
|
+
},
|
|
576
|
+
);
|
|
577
|
+
} finally {
|
|
578
|
+
await rm(root, { recursive: true, force: true });
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test('content:check fails for unknown Norna block names', async () => {
|
|
583
|
+
const { root, siteDir } = await createTempSite();
|
|
584
|
+
try {
|
|
585
|
+
await mkdir(path.join(siteDir, 'images', 'plain'), { recursive: true });
|
|
586
|
+
await writeFile(path.join(siteDir, 'images', 'plain', 'image.jpg'), 'fixture image');
|
|
587
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
588
|
+
title: Unknown Block
|
|
589
|
+
description: Fixture
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## Plain {#plain}
|
|
593
|
+
|
|
594
|
+
\`\`\`norna-gallery-stack
|
|
595
|
+
- image: image.jpg
|
|
596
|
+
\`\`\`
|
|
597
|
+
`);
|
|
598
|
+
|
|
599
|
+
await assert.rejects(
|
|
600
|
+
() => runContentScript(siteDir, ['--check']),
|
|
601
|
+
(error) => {
|
|
602
|
+
assert.match(error.output, /Unknown Norna block "norna-gallery-stack"\. Use one of: norna-image-stack, norna-image-carousel, norna-card-list\./);
|
|
603
|
+
assert.match(error.output, /Use norna-image-stack for one or more stacked images\./);
|
|
604
|
+
assert.match(error.output, /Example: ```norna-image-stack\n- image: filename\.jpg\n```/);
|
|
605
|
+
return true;
|
|
606
|
+
},
|
|
607
|
+
);
|
|
608
|
+
} finally {
|
|
609
|
+
await rm(root, { recursive: true, force: true });
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test('content:check supports tilde fenced Norna blocks', async () => {
|
|
614
|
+
const { root, siteDir } = await createTempSite();
|
|
615
|
+
try {
|
|
616
|
+
await mkdir(path.join(siteDir, 'images', 'intro'), { recursive: true });
|
|
617
|
+
await writeFile(path.join(siteDir, 'images', 'intro', 'hero.jpg'), 'fixture image');
|
|
618
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
619
|
+
title: Tilde Fence
|
|
620
|
+
description: Fixture
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## Intro {#intro}
|
|
624
|
+
|
|
625
|
+
~~~norna-image-stack
|
|
626
|
+
- image: hero.jpg
|
|
627
|
+
~~~
|
|
628
|
+
`);
|
|
629
|
+
|
|
630
|
+
const result = await runContentScript(siteDir, ['--check']);
|
|
631
|
+
assert.match(result.stdout, /Content check passed\./);
|
|
632
|
+
} finally {
|
|
633
|
+
await rm(root, { recursive: true, force: true });
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test('content:check ignores Norna blocks shown inside longer code fences', async () => {
|
|
638
|
+
const { root, siteDir } = await createTempSite();
|
|
639
|
+
try {
|
|
640
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
641
|
+
title: Code Example
|
|
642
|
+
description: Fixture
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Intro {#intro}
|
|
646
|
+
|
|
647
|
+
\`\`\`\`md
|
|
648
|
+
\`\`\`norna-image-stack
|
|
649
|
+
- image: missing-example.jpg
|
|
650
|
+
\`\`\`
|
|
651
|
+
\`\`\`\`
|
|
652
|
+
`);
|
|
653
|
+
|
|
654
|
+
const result = await runContentScript(siteDir, ['--check']);
|
|
655
|
+
assert.match(result.stdout, /Content check passed\./);
|
|
656
|
+
} finally {
|
|
657
|
+
await rm(root, { recursive: true, force: true });
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test('content:check fails for likely Norna block names outside fences', async () => {
|
|
662
|
+
const { root, siteDir } = await createTempSite();
|
|
663
|
+
try {
|
|
664
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
665
|
+
title: Missing Fence
|
|
666
|
+
description: Fixture
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## Intro {#intro}
|
|
670
|
+
|
|
671
|
+
norna-image-stack
|
|
672
|
+
- image: hero.jpg
|
|
673
|
+
`);
|
|
674
|
+
|
|
675
|
+
await assert.rejects(
|
|
676
|
+
() => runContentScript(siteDir, ['--check']),
|
|
677
|
+
(error) => {
|
|
678
|
+
assert.match(error.output, /Found "norna-image-stack" outside a code block\./);
|
|
679
|
+
assert.match(error.output, /Start the block like this:\n```norna-image-stack\n- image: filename\.jpg\n```/);
|
|
680
|
+
return true;
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
} finally {
|
|
684
|
+
await rm(root, { recursive: true, force: true });
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test('content:check fails for short Norna block fences', async () => {
|
|
689
|
+
const { root, siteDir } = await createTempSite();
|
|
690
|
+
try {
|
|
691
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
692
|
+
title: Short Fence
|
|
693
|
+
description: Fixture
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## Intro {#intro}
|
|
697
|
+
|
|
698
|
+
\`\`norna-image-stack
|
|
699
|
+
- image: hero.jpg
|
|
700
|
+
\`\`
|
|
701
|
+
`);
|
|
702
|
+
|
|
703
|
+
await assert.rejects(
|
|
704
|
+
() => runContentScript(siteDir, ['--check']),
|
|
705
|
+
(error) => {
|
|
706
|
+
assert.match(error.output, /Invalid Norna block start for "norna-image-stack"\./);
|
|
707
|
+
assert.match(error.output, /Use three backticks or three tildes/);
|
|
708
|
+
return true;
|
|
709
|
+
},
|
|
710
|
+
);
|
|
711
|
+
} finally {
|
|
712
|
+
await rm(root, { recursive: true, force: true });
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test('content:check fails for unclosed Norna blocks', async () => {
|
|
717
|
+
const { root, siteDir } = await createTempSite();
|
|
718
|
+
try {
|
|
719
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
720
|
+
title: Unclosed Fence
|
|
721
|
+
description: Fixture
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
## Intro {#intro}
|
|
725
|
+
|
|
726
|
+
\`\`\`norna-image-stack
|
|
727
|
+
- image: hero.jpg
|
|
728
|
+
~~~
|
|
729
|
+
`);
|
|
730
|
+
|
|
731
|
+
await assert.rejects(
|
|
732
|
+
() => runContentScript(siteDir, ['--check']),
|
|
733
|
+
(error) => {
|
|
734
|
+
assert.match(error.output, /This Norna block was started on line \d+ but not closed\./);
|
|
735
|
+
assert.match(error.output, /Add a closing ``` line after the last entry\./);
|
|
736
|
+
return true;
|
|
737
|
+
},
|
|
738
|
+
);
|
|
739
|
+
} finally {
|
|
740
|
+
await rm(root, { recursive: true, force: true });
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test('content:check continues after malformed Norna blocks', async () => {
|
|
745
|
+
const { root, siteDir } = await createTempSite();
|
|
746
|
+
try {
|
|
747
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
748
|
+
title: Multiple Issues
|
|
749
|
+
description: Fixture
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
## Intro {#intro}
|
|
753
|
+
|
|
754
|
+
\`\`\`norna-image-stack
|
|
755
|
+
image broken.jpg
|
|
756
|
+
alt: Broken
|
|
757
|
+
\`\`\`
|
|
758
|
+
|
|
759
|
+
\`\`\`norna-image-stack
|
|
760
|
+
- image: missing-intro.jpg
|
|
761
|
+
alt: Missing intro
|
|
762
|
+
\`\`\`
|
|
763
|
+
|
|
764
|
+
## More {#more}
|
|
765
|
+
|
|
766
|
+
\`\`\`norna-image-stack
|
|
767
|
+
- image: missing-more.jpg
|
|
768
|
+
alt: Missing more
|
|
769
|
+
\`\`\`
|
|
770
|
+
`);
|
|
771
|
+
|
|
772
|
+
await assert.rejects(
|
|
773
|
+
() => runContentScript(siteDir, ['--check']),
|
|
774
|
+
(error) => {
|
|
775
|
+
assert.match(error.output, /Invalid norna-image-stack entry "image broken\.jpg"\. Start each image with "- image: filename\.jpg"\./);
|
|
776
|
+
assert.match(error.output, /Image "missing-intro\.jpg" does not exist at .*site\/images\/intro\/missing-intro\.jpg or anywhere under .*site\/images\//);
|
|
777
|
+
assert.match(error.output, /Image "missing-more\.jpg" does not exist at .*site\/images\/more\/missing-more\.jpg or anywhere under .*site\/images\//);
|
|
778
|
+
return true;
|
|
779
|
+
},
|
|
780
|
+
);
|
|
781
|
+
} finally {
|
|
782
|
+
await rm(root, { recursive: true, force: true });
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test('content:check allows Norna images without alt text', async () => {
|
|
787
|
+
const { root, siteDir } = await createTempSite();
|
|
788
|
+
try {
|
|
789
|
+
await mkdir(path.join(siteDir, 'images', 'intro'), { recursive: true });
|
|
790
|
+
await writeFile(path.join(siteDir, 'images', 'intro', 'hero.jpg'), 'fixture image');
|
|
791
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
792
|
+
title: Missing Alt
|
|
793
|
+
description: Fixture
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## Intro {#intro}
|
|
797
|
+
|
|
798
|
+
\`\`\`norna-image-stack
|
|
799
|
+
- image: hero.jpg
|
|
800
|
+
\`\`\`
|
|
801
|
+
`);
|
|
802
|
+
|
|
803
|
+
const result = await runContentScript(siteDir, ['--check']);
|
|
804
|
+
assert.match(result.stdout, /Content check passed\./);
|
|
805
|
+
} finally {
|
|
806
|
+
await rm(root, { recursive: true, force: true });
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test('content:check reports single-image carousel and missing file together', async () => {
|
|
811
|
+
const { root, siteDir } = await createTempSite();
|
|
812
|
+
try {
|
|
813
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
814
|
+
title: Beginner Errors
|
|
815
|
+
description: Fixture
|
|
816
|
+
---
|
|
817
|
+
|
|
818
|
+
## Plain {#plain}
|
|
819
|
+
|
|
820
|
+
\`\`\`norna-image-carousel
|
|
821
|
+
- image: foo.jpg
|
|
822
|
+
\`\`\`
|
|
823
|
+
`);
|
|
824
|
+
|
|
825
|
+
await assert.rejects(
|
|
826
|
+
() => runContentScript(siteDir, ['--check']),
|
|
827
|
+
(error) => {
|
|
828
|
+
assert.match(error.output, /norna-image-carousel on line \d+ contains 1 image\. A carousel needs at least two images\./);
|
|
829
|
+
assert.match(error.output, /Image "foo\.jpg" does not exist at .*site\/images\/plain\/foo\.jpg or anywhere under .*site\/images\//);
|
|
830
|
+
return true;
|
|
831
|
+
},
|
|
832
|
+
);
|
|
833
|
+
} finally {
|
|
834
|
+
await rm(root, { recursive: true, force: true });
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test('content:check fails when a Norna-managed image file is missing', async () => {
|
|
839
|
+
const { root, siteDir } = await createTempSite();
|
|
840
|
+
try {
|
|
841
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
842
|
+
title: Missing Image
|
|
843
|
+
description: Fixture
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
## Intro {#intro}
|
|
847
|
+
|
|
848
|
+
\`\`\`norna-image-stack
|
|
849
|
+
- image: missing.jpg
|
|
850
|
+
alt: Missing
|
|
851
|
+
\`\`\`
|
|
852
|
+
`);
|
|
853
|
+
|
|
854
|
+
await assert.rejects(
|
|
855
|
+
() => runContentScript(siteDir, ['--check']),
|
|
856
|
+
(error) => {
|
|
857
|
+
assert.match(error.output, /Image "missing\.jpg" does not exist at .*site\/images\/intro\/missing\.jpg or anywhere under .*site\/images\//);
|
|
858
|
+
return true;
|
|
859
|
+
},
|
|
860
|
+
);
|
|
861
|
+
} finally {
|
|
862
|
+
await rm(root, { recursive: true, force: true });
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
test('content:check fails when a page repeats a section id', async () => {
|
|
867
|
+
const { root, siteDir } = await createTempSite();
|
|
868
|
+
try {
|
|
869
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
870
|
+
title: Duplicate Section
|
|
871
|
+
description: Fixture
|
|
872
|
+
---
|
|
873
|
+
|
|
874
|
+
## Intro {#intro}
|
|
875
|
+
|
|
876
|
+
First.
|
|
877
|
+
|
|
878
|
+
## Again {#intro}
|
|
879
|
+
|
|
880
|
+
Second.
|
|
881
|
+
`);
|
|
882
|
+
|
|
883
|
+
await assert.rejects(
|
|
884
|
+
() => runContentScript(siteDir, ['--check']),
|
|
885
|
+
(error) => {
|
|
886
|
+
assert.match(error.output, /Duplicate Markdown section heading id "intro"\./);
|
|
887
|
+
return true;
|
|
888
|
+
},
|
|
889
|
+
);
|
|
890
|
+
} finally {
|
|
891
|
+
await rm(root, { recursive: true, force: true });
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
test('content:check fails when section metadata has no Markdown section', async () => {
|
|
896
|
+
const { root, siteDir } = await createTempSite();
|
|
897
|
+
try {
|
|
898
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
899
|
+
title: Orphaned Metadata
|
|
900
|
+
description: Fixture
|
|
901
|
+
sections:
|
|
902
|
+
missing: {}
|
|
903
|
+
---
|
|
904
|
+
|
|
905
|
+
## Intro {#intro}
|
|
906
|
+
|
|
907
|
+
Text.
|
|
908
|
+
`);
|
|
909
|
+
|
|
910
|
+
await assert.rejects(
|
|
911
|
+
() => runContentScript(siteDir, ['--check']),
|
|
912
|
+
(error) => {
|
|
913
|
+
assert.match(error.output, /Section metadata "missing" does not match any Markdown section\./);
|
|
914
|
+
return true;
|
|
915
|
+
},
|
|
916
|
+
);
|
|
917
|
+
} finally {
|
|
918
|
+
await rm(root, { recursive: true, force: true });
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
test('content:sync refuses ambiguous page-local image relocation', async () => {
|
|
923
|
+
const { root, siteDir } = await createTempSite();
|
|
924
|
+
try {
|
|
925
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
926
|
+
title: Ambiguous Move
|
|
927
|
+
description: Fixture
|
|
928
|
+
---
|
|
929
|
+
|
|
930
|
+
## Work {#work}
|
|
931
|
+
|
|
932
|
+
\`\`\`norna-image-stack
|
|
933
|
+
- image: shared.jpg
|
|
934
|
+
alt: Shared
|
|
935
|
+
\`\`\`
|
|
936
|
+
`);
|
|
937
|
+
await mkdir(path.join(siteDir, 'images', 'old-a'), { recursive: true });
|
|
938
|
+
await mkdir(path.join(siteDir, 'images', 'old-b'), { recursive: true });
|
|
939
|
+
await writeFile(path.join(siteDir, 'images', 'old-a', 'shared.jpg'), 'a');
|
|
940
|
+
await writeFile(path.join(siteDir, 'images', 'old-b', 'shared.jpg'), 'b');
|
|
941
|
+
|
|
942
|
+
await assert.rejects(
|
|
943
|
+
() => runContentScript(siteDir, ['--write', '--yes']),
|
|
944
|
+
(error) => {
|
|
945
|
+
assert.match(error.output, /Cannot relocate "shared\.jpg"\. Multiple files with this filename were found:/);
|
|
946
|
+
assert.match(error.output, /site\/images\/old-a\/shared\.jpg/);
|
|
947
|
+
assert.match(error.output, /site\/images\/old-b\/shared\.jpg/);
|
|
948
|
+
return true;
|
|
949
|
+
},
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
assert.equal(await fileExists(path.join(siteDir, 'images', 'work', 'shared.jpg')), false);
|
|
953
|
+
assert.equal(await fileExists(path.join(siteDir, 'images', 'old-a', 'shared.jpg')), true);
|
|
954
|
+
assert.equal(await fileExists(path.join(siteDir, 'images', 'old-b', 'shared.jpg')), true);
|
|
955
|
+
} finally {
|
|
956
|
+
await rm(root, { recursive: true, force: true });
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
test('content:sync refuses to move an image still referenced by its current section', async () => {
|
|
961
|
+
const { root, siteDir } = await createTempSite();
|
|
962
|
+
try {
|
|
963
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
964
|
+
title: Copied Image Reference
|
|
965
|
+
description: Fixture
|
|
966
|
+
---
|
|
967
|
+
|
|
968
|
+
## Intro {#intro}
|
|
969
|
+
|
|
970
|
+
\`\`\`norna-image-stack
|
|
971
|
+
- image: shared.jpg
|
|
972
|
+
alt: Shared original
|
|
973
|
+
\`\`\`
|
|
974
|
+
|
|
975
|
+
## Plain {#plain}
|
|
976
|
+
|
|
977
|
+
\`\`\`norna-image-stack
|
|
978
|
+
- image: shared.jpg
|
|
979
|
+
alt: Shared copy
|
|
980
|
+
\`\`\`
|
|
981
|
+
`);
|
|
982
|
+
await mkdir(path.join(siteDir, 'images', 'intro'), { recursive: true });
|
|
983
|
+
await writeFile(path.join(siteDir, 'images', 'intro', 'shared.jpg'), 'shared image');
|
|
984
|
+
|
|
985
|
+
await assert.rejects(
|
|
986
|
+
() => runContentScript(siteDir, ['--write', '--yes']),
|
|
987
|
+
(error) => {
|
|
988
|
+
assert.match(error.output, /Cannot relocate "shared\.jpg" from .*site\/images\/intro\/shared\.jpg because it is still referenced from .*site\/content\.md \[intro\]\./);
|
|
989
|
+
return true;
|
|
990
|
+
},
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
assert.equal(await fileExists(path.join(siteDir, 'images', 'intro', 'shared.jpg')), true);
|
|
994
|
+
assert.equal(await fileExists(path.join(siteDir, 'images', 'plain', 'shared.jpg')), false);
|
|
995
|
+
} finally {
|
|
996
|
+
await rm(root, { recursive: true, force: true });
|
|
997
|
+
}
|
|
998
|
+
});
|