@janga/norna 0.7.7 → 0.7.9
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 +6 -5
- package/docs/README.md +5 -1
- package/docs/commands.md +4 -3
- package/docs/configuration.md +1 -1
- package/docs/content.md +64 -14
- package/docs/design/norna-diagram-design.md +156 -0
- package/docs/getting-started.md +48 -7
- package/docs/images-and-metadata.md +11 -6
- package/docs/routes.md +14 -3
- package/docs/site-structure.md +6 -3
- package/docs/theme.md +7 -4
- package/docs/typography.md +4 -5
- package/examples/dog-gallery/site/content.md +1 -36
- package/examples/dog-gallery/site/routes/010-dog-care/route-content.md +0 -8
- package/examples/typography-presets/.astro/collections/site.schema.json +776 -0
- package/examples/typography-presets/.astro/collections/theme.schema.json +668 -0
- package/examples/typography-presets/.astro/content-assets.mjs +4 -0
- package/examples/typography-presets/.astro/content-modules.mjs +1 -0
- package/examples/typography-presets/.astro/content.d.ts +175 -0
- package/examples/typography-presets/.astro/data-store.json +1 -0
- package/examples/typography-presets/.astro/dev-local.json +8 -0
- package/examples/typography-presets/.astro/dev.json +15 -0
- package/examples/typography-presets/.astro/dev.log +24 -0
- package/examples/typography-presets/.astro/settings.json +5 -0
- package/examples/typography-presets/.astro/types.d.ts +2 -0
- package/examples/typography-presets/node_modules/.vite/deps/_metadata.json +80 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js +434 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro-D_GIpJIE.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js +2702 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
- package/examples/typography-presets/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js +1534 -0
- package/examples/typography-presets/node_modules/.vite/deps/audit-DM9Hkl5B.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js +1389 -0
- package/examples/typography-presets/node_modules/.vite/deps/embla-carousel.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js +48 -0
- package/examples/typography-presets/node_modules/.vite/deps/highlight-DnZ1ULOL.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js +42 -0
- package/examples/typography-presets/node_modules/.vite/deps/icons-RyebKi45.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/package.json +3 -0
- package/examples/typography-presets/node_modules/.vite/deps/rolldown-runtime-BvCyGRYZ.js +4 -0
- package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js +190 -0
- package/examples/typography-presets/node_modules/.vite/deps/settings-DaU-gNnK.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js +458 -0
- package/examples/typography-presets/node_modules/.vite/deps/toolbar-63PA619H.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js +1016 -0
- package/examples/typography-presets/node_modules/.vite/deps/ui-library--bkcNSYz.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js +32 -0
- package/examples/typography-presets/node_modules/.vite/deps/window-jPkpKVcy.js.map +1 -0
- package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js +111 -0
- package/examples/typography-presets/node_modules/.vite/deps/xray-BXCHpEJP.js.map +1 -0
- package/examples/typography-presets/site/.norna/generated-images.json +290 -0
- package/examples/typography-presets/site/config.mjs +42 -0
- package/examples/typography-presets/site/content.md +134 -0
- package/examples/typography-presets/site/images/compact-gallery/black-schnauzer.jpg +0 -0
- package/examples/typography-presets/site/images/compact-gallery/retriever-portrait.jpg +0 -0
- package/examples/typography-presets/site/images/compact-gallery/schnauzer-portrait.jpg +0 -0
- package/examples/typography-presets/site/images/quiet-gallery/puppy-portrait.jpg +0 -0
- package/examples/typography-presets/site/images/quiet-gallery/shepherd-portrait.jpg +0 -0
- package/examples/typography-presets/site/images/quiet-gallery/white-puppy.jpg +0 -0
- package/examples/typography-presets/site/images/statement/black-pedigree.jpg +0 -0
- package/examples/typography-presets/site/images/statement/sheepdog-grass.jpg +0 -0
- package/examples/typography-presets/site/images/statement/sheepdog-wide.jpg +0 -0
- package/examples/typography-presets/site/images/text-forward/boxer-portrait.jpg +0 -0
- package/examples/typography-presets/site/images/text-forward/grass-puppy.jpg +0 -0
- package/examples/typography-presets/site/images/text-forward/grey-street-dog.jpg +0 -0
- package/examples/typography-presets/site/public/robots.txt +2 -0
- package/examples/typography-presets/site/theme.md +43 -0
- package/package.json +1 -1
- package/scripts/lib/site-content.mjs +76 -0
- package/scripts/lib/theme-config.mjs +2 -0
- package/scripts/sync-content-sections.mjs +158 -22
- package/scripts/test-content-check.mjs +31 -1
- package/scripts/test-content-model-v2.mjs +238 -6
- package/scripts/test-package-check.mjs +17 -0
- package/src/components/SiteSection.astro +1 -1
- package/src/lib/sectionContent.ts +10 -2
- package/src/styles/global.css +1 -1
- package/starters/basic/README.md +4 -2
- package/starters/basic/site/content.md +2 -8
- package/starters/project/README.md +7 -3
|
@@ -129,7 +129,7 @@ test('content:check groups section issues, global issues, and unreferenced image
|
|
|
129
129
|
assert.match(output, /^Content check failed\./m);
|
|
130
130
|
assert.match(output, /Content Issues\n\n\[site\/content\.md \[min-konst\]\]\n Errors:/);
|
|
131
131
|
assert.match(output, /Image "vav\.jpeg" is used here but is located in site\/images\/mitt-hem\/vav\.jpeg\./);
|
|
132
|
-
assert.match(output, /Image "missing\.jpeg" does not exist at site\/images\/min-konst\/missing\.jpeg or anywhere under
|
|
132
|
+
assert.match(output, /Image "missing\.jpeg" does not exist at site\/images\/min-konst\/missing\.jpeg or anywhere under any page or route image root\./);
|
|
133
133
|
assert.match(output, /Unreferenced Images\nThese files are kept under page or route image roots but are not referenced by Norna-managed image references:/);
|
|
134
134
|
assert.match(output, /site\/images\/karin-walde\/unreferenced\.jpg/);
|
|
135
135
|
});
|
|
@@ -204,6 +204,36 @@ test('content:check fails when Markdown uses undefined inline styles', async ()
|
|
|
204
204
|
});
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
const invalidInlineStyleColorTheme = `---
|
|
208
|
+
presentation:
|
|
209
|
+
inlineStyles:
|
|
210
|
+
yellow:
|
|
211
|
+
color: "#ffd844d"
|
|
212
|
+
---
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
test('content:check explains invalid inline style colors', async () => {
|
|
216
|
+
await withTempProject({
|
|
217
|
+
site: `---
|
|
218
|
+
title: Invalid Inline Color
|
|
219
|
+
---
|
|
220
|
+
## Intro {#intro}
|
|
221
|
+
This has [yellow text]{.yellow}.
|
|
222
|
+
`,
|
|
223
|
+
theme: invalidInlineStyleColorTheme,
|
|
224
|
+
files: [],
|
|
225
|
+
}, async (root) => {
|
|
226
|
+
const result = runContentScript(root, ['--check']);
|
|
227
|
+
const output = getOutput(result);
|
|
228
|
+
|
|
229
|
+
assert.equal(result.status, 1, output);
|
|
230
|
+
assert.match(output, /^Content check failed\./m);
|
|
231
|
+
assert.match(output, /site\/theme\.md line 5 defines presentation\.inlineStyles\.yellow\.color with invalid color value "#ffd844d"\./);
|
|
232
|
+
assert.match(output, /Use a quoted hex color in #rgb, #rrggbb, or #rrggbbaa form, for example color: "#ffd84d"\./);
|
|
233
|
+
assert.doesNotMatch(output, /Inline style "\.yellow" is used in site\/content\.md but is not defined/);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
207
237
|
const badWhitespaceSite = `---
|
|
208
238
|
presentation:
|
|
209
239
|
typography:
|
|
@@ -73,6 +73,26 @@ typography:
|
|
|
73
73
|
|
|
74
74
|
const fileExists = async (filePath) => access(filePath).then(() => true, () => false);
|
|
75
75
|
|
|
76
|
+
const runGit = async (cwd, args) => execFileAsync('git', args, {
|
|
77
|
+
cwd,
|
|
78
|
+
env: {
|
|
79
|
+
...process.env,
|
|
80
|
+
GIT_AUTHOR_NAME: 'Norna Test',
|
|
81
|
+
GIT_AUTHOR_EMAIL: 'norna@example.test',
|
|
82
|
+
GIT_COMMITTER_NAME: 'Norna Test',
|
|
83
|
+
GIT_COMMITTER_EMAIL: 'norna@example.test',
|
|
84
|
+
},
|
|
85
|
+
maxBuffer: 1024 * 1024,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const initCleanGitWorktree = async (root) => {
|
|
89
|
+
await runGit(root, ['init']);
|
|
90
|
+
await runGit(root, ['config', 'user.email', 'norna@example.test']);
|
|
91
|
+
await runGit(root, ['config', 'user.name', 'Norna Test']);
|
|
92
|
+
await runGit(root, ['add', '.']);
|
|
93
|
+
await runGit(root, ['commit', '-m', 'initial']);
|
|
94
|
+
};
|
|
95
|
+
|
|
76
96
|
const createTempFixtureCopy = async () => {
|
|
77
97
|
const tempParent = path.join(repoRoot, 'node_modules', '.cache');
|
|
78
98
|
await mkdir(tempParent, { recursive: true });
|
|
@@ -310,7 +330,7 @@ description: Fixture
|
|
|
310
330
|
await assert.rejects(
|
|
311
331
|
() => runContentScript(siteDir, ['--check']),
|
|
312
332
|
(error) => {
|
|
313
|
-
assert.match(error.output, /Image "missing\.svg" does not exist at .*site\/images\/help\/missing\.svg or anywhere under
|
|
333
|
+
assert.match(error.output, /Image "missing\.svg" does not exist at .*site\/images\/help\/missing\.svg or anywhere under any page or route image root\./);
|
|
314
334
|
return true;
|
|
315
335
|
},
|
|
316
336
|
);
|
|
@@ -493,6 +513,218 @@ description: Fixture
|
|
|
493
513
|
}
|
|
494
514
|
});
|
|
495
515
|
|
|
516
|
+
test('content:sync moves Norna-managed images across route image roots when git is clean', async () => {
|
|
517
|
+
const { root, siteDir } = await createTempSite();
|
|
518
|
+
try {
|
|
519
|
+
const sourceRouteDir = path.join(siteDir, 'routes', '010-source');
|
|
520
|
+
const targetRouteDir = path.join(siteDir, 'routes', '020-target');
|
|
521
|
+
await mkdir(path.join(sourceRouteDir, 'images', 'old'), { recursive: true });
|
|
522
|
+
await mkdir(targetRouteDir, { recursive: true });
|
|
523
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
524
|
+
title: Cross Route Sync
|
|
525
|
+
description: Fixture
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Home {#home}
|
|
529
|
+
|
|
530
|
+
Text.
|
|
531
|
+
`);
|
|
532
|
+
await writeFile(path.join(sourceRouteDir, 'route-content.md'), `---
|
|
533
|
+
title: Source
|
|
534
|
+
description: Fixture
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Old {#old}
|
|
538
|
+
|
|
539
|
+
Text.
|
|
540
|
+
`);
|
|
541
|
+
await writeFile(path.join(targetRouteDir, 'route-content.md'), `---
|
|
542
|
+
title: Target
|
|
543
|
+
description: Fixture
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Work {#work}
|
|
547
|
+
|
|
548
|
+
\`\`\`norna-image-stack
|
|
549
|
+
- image: moved.jpg
|
|
550
|
+
\`\`\`
|
|
551
|
+
`);
|
|
552
|
+
await writeFile(path.join(sourceRouteDir, 'images', 'old', 'moved.jpg'), 'route image');
|
|
553
|
+
await initCleanGitWorktree(root);
|
|
554
|
+
|
|
555
|
+
await runContentScript(siteDir, ['--write', '--yes']);
|
|
556
|
+
|
|
557
|
+
assert.equal(await fileExists(path.join(targetRouteDir, 'images', 'work', 'moved.jpg')), true);
|
|
558
|
+
assert.equal(await fileExists(path.join(sourceRouteDir, 'images', 'old', 'moved.jpg')), false);
|
|
559
|
+
} finally {
|
|
560
|
+
await rm(root, { recursive: true, force: true });
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test('content:sync refuses cross-route image moves when git status is dirty', async () => {
|
|
565
|
+
const { root, siteDir } = await createTempSite();
|
|
566
|
+
try {
|
|
567
|
+
const sourceRouteDir = path.join(siteDir, 'routes', '010-source');
|
|
568
|
+
const targetRouteDir = path.join(siteDir, 'routes', '020-target');
|
|
569
|
+
await mkdir(path.join(sourceRouteDir, 'images', 'old'), { recursive: true });
|
|
570
|
+
await mkdir(targetRouteDir, { recursive: true });
|
|
571
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
572
|
+
title: Dirty Cross Route Sync
|
|
573
|
+
description: Fixture
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Home {#home}
|
|
577
|
+
|
|
578
|
+
Text.
|
|
579
|
+
`);
|
|
580
|
+
await writeFile(path.join(sourceRouteDir, 'route-content.md'), `---
|
|
581
|
+
title: Source
|
|
582
|
+
description: Fixture
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
## Old {#old}
|
|
586
|
+
|
|
587
|
+
Text.
|
|
588
|
+
`);
|
|
589
|
+
await writeFile(path.join(targetRouteDir, 'route-content.md'), `---
|
|
590
|
+
title: Target
|
|
591
|
+
description: Fixture
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## Work {#work}
|
|
595
|
+
|
|
596
|
+
\`\`\`norna-image-stack
|
|
597
|
+
- image: moved.jpg
|
|
598
|
+
\`\`\`
|
|
599
|
+
`);
|
|
600
|
+
await writeFile(path.join(sourceRouteDir, 'images', 'old', 'moved.jpg'), 'route image');
|
|
601
|
+
await initCleanGitWorktree(root);
|
|
602
|
+
await writeFile(path.join(root, 'dirty.txt'), 'dirty');
|
|
603
|
+
|
|
604
|
+
await assert.rejects(
|
|
605
|
+
() => runContentScript(siteDir, ['--write', '--yes']),
|
|
606
|
+
(error) => {
|
|
607
|
+
assert.match(error.output, /Cross-route content sync requires a clean git working tree before moving files between page or route image roots\./);
|
|
608
|
+
assert.match(error.output, /dirty\.txt/);
|
|
609
|
+
return true;
|
|
610
|
+
},
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
assert.equal(await fileExists(path.join(targetRouteDir, 'images', 'work', 'moved.jpg')), false);
|
|
614
|
+
assert.equal(await fileExists(path.join(sourceRouteDir, 'images', 'old', 'moved.jpg')), true);
|
|
615
|
+
} finally {
|
|
616
|
+
await rm(root, { recursive: true, force: true });
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test('content:check reports cross-route image moves without requiring clean git status', async () => {
|
|
621
|
+
const { root, siteDir } = await createTempSite();
|
|
622
|
+
try {
|
|
623
|
+
const sourceRouteDir = path.join(siteDir, 'routes', '010-source');
|
|
624
|
+
const targetRouteDir = path.join(siteDir, 'routes', '020-target');
|
|
625
|
+
await mkdir(path.join(sourceRouteDir, 'images', 'old'), { recursive: true });
|
|
626
|
+
await mkdir(targetRouteDir, { recursive: true });
|
|
627
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
628
|
+
title: Cross Route Check
|
|
629
|
+
description: Fixture
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## Home {#home}
|
|
633
|
+
|
|
634
|
+
Text.
|
|
635
|
+
`);
|
|
636
|
+
await writeFile(path.join(sourceRouteDir, 'route-content.md'), `---
|
|
637
|
+
title: Source
|
|
638
|
+
description: Fixture
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Old {#old}
|
|
642
|
+
|
|
643
|
+
Text.
|
|
644
|
+
`);
|
|
645
|
+
await writeFile(path.join(targetRouteDir, 'route-content.md'), `---
|
|
646
|
+
title: Target
|
|
647
|
+
description: Fixture
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
## Work {#work}
|
|
651
|
+
|
|
652
|
+
\`\`\`norna-image-stack
|
|
653
|
+
- image: moved.jpg
|
|
654
|
+
\`\`\`
|
|
655
|
+
`);
|
|
656
|
+
await writeFile(path.join(sourceRouteDir, 'images', 'old', 'moved.jpg'), 'route image');
|
|
657
|
+
await writeFile(path.join(root, 'dirty.txt'), 'dirty');
|
|
658
|
+
|
|
659
|
+
await assert.rejects(
|
|
660
|
+
() => runContentScript(siteDir, ['--check']),
|
|
661
|
+
(error) => {
|
|
662
|
+
assert.match(error.output, /Image "moved\.jpg" is used here but is located in .*site\/routes\/010-source\/images\/old\/moved\.jpg\./);
|
|
663
|
+
assert.match(error.output, /Run norna content:sync to move it from .*site\/routes\/010-source to .*site\/routes\/020-target\./);
|
|
664
|
+
assert.doesNotMatch(error.output, /requires a clean git working tree before moving files/);
|
|
665
|
+
return true;
|
|
666
|
+
},
|
|
667
|
+
);
|
|
668
|
+
} finally {
|
|
669
|
+
await rm(root, { recursive: true, force: true });
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test('content:sync refuses to move a cross-route image still referenced by its current section', async () => {
|
|
674
|
+
const { root, siteDir } = await createTempSite();
|
|
675
|
+
try {
|
|
676
|
+
const sourceRouteDir = path.join(siteDir, 'routes', '010-source');
|
|
677
|
+
const targetRouteDir = path.join(siteDir, 'routes', '020-target');
|
|
678
|
+
await mkdir(path.join(sourceRouteDir, 'images', 'old'), { recursive: true });
|
|
679
|
+
await mkdir(targetRouteDir, { recursive: true });
|
|
680
|
+
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
681
|
+
title: Cross Route Copy
|
|
682
|
+
description: Fixture
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
## Home {#home}
|
|
686
|
+
|
|
687
|
+
Text.
|
|
688
|
+
`);
|
|
689
|
+
await writeFile(path.join(sourceRouteDir, 'route-content.md'), `---
|
|
690
|
+
title: Source
|
|
691
|
+
description: Fixture
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## Old {#old}
|
|
695
|
+
|
|
696
|
+
\`\`\`norna-image-stack
|
|
697
|
+
- image: shared.jpg
|
|
698
|
+
\`\`\`
|
|
699
|
+
`);
|
|
700
|
+
await writeFile(path.join(targetRouteDir, 'route-content.md'), `---
|
|
701
|
+
title: Target
|
|
702
|
+
description: Fixture
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## Work {#work}
|
|
706
|
+
|
|
707
|
+
\`\`\`norna-image-stack
|
|
708
|
+
- image: shared.jpg
|
|
709
|
+
\`\`\`
|
|
710
|
+
`);
|
|
711
|
+
await writeFile(path.join(sourceRouteDir, 'images', 'old', 'shared.jpg'), 'route image');
|
|
712
|
+
|
|
713
|
+
await assert.rejects(
|
|
714
|
+
() => runContentScript(siteDir, ['--write', '--yes']),
|
|
715
|
+
(error) => {
|
|
716
|
+
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\/route-content\.md \[old\]\./);
|
|
717
|
+
return true;
|
|
718
|
+
},
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
assert.equal(await fileExists(path.join(targetRouteDir, 'images', 'work', 'shared.jpg')), false);
|
|
722
|
+
assert.equal(await fileExists(path.join(sourceRouteDir, 'images', 'old', 'shared.jpg')), true);
|
|
723
|
+
} finally {
|
|
724
|
+
await rm(root, { recursive: true, force: true });
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
496
728
|
test('CLI content:sync refreshes generated images after moving a carousel image', async () => {
|
|
497
729
|
const { root, siteDir } = await createTempSite();
|
|
498
730
|
try {
|
|
@@ -773,8 +1005,8 @@ image broken.jpg
|
|
|
773
1005
|
() => runContentScript(siteDir, ['--check']),
|
|
774
1006
|
(error) => {
|
|
775
1007
|
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
|
|
777
|
-
assert.match(error.output, /Image "missing-more\.jpg" does not exist at .*site\/images\/more\/missing-more\.jpg or anywhere under
|
|
1008
|
+
assert.match(error.output, /Image "missing-intro\.jpg" does not exist at .*site\/images\/intro\/missing-intro\.jpg or anywhere under any page or route image root\./);
|
|
1009
|
+
assert.match(error.output, /Image "missing-more\.jpg" does not exist at .*site\/images\/more\/missing-more\.jpg or anywhere under any page or route image root\./);
|
|
778
1010
|
return true;
|
|
779
1011
|
},
|
|
780
1012
|
);
|
|
@@ -826,7 +1058,7 @@ description: Fixture
|
|
|
826
1058
|
() => runContentScript(siteDir, ['--check']),
|
|
827
1059
|
(error) => {
|
|
828
1060
|
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
|
|
1061
|
+
assert.match(error.output, /Image "foo\.jpg" does not exist at .*site\/images\/plain\/foo\.jpg or anywhere under any page or route image root\./);
|
|
830
1062
|
return true;
|
|
831
1063
|
},
|
|
832
1064
|
);
|
|
@@ -854,7 +1086,7 @@ description: Fixture
|
|
|
854
1086
|
await assert.rejects(
|
|
855
1087
|
() => runContentScript(siteDir, ['--check']),
|
|
856
1088
|
(error) => {
|
|
857
|
-
assert.match(error.output, /Image "missing\.jpg" does not exist at .*site\/images\/intro\/missing\.jpg or anywhere under
|
|
1089
|
+
assert.match(error.output, /Image "missing\.jpg" does not exist at .*site\/images\/intro\/missing\.jpg or anywhere under any page or route image root\./);
|
|
858
1090
|
return true;
|
|
859
1091
|
},
|
|
860
1092
|
);
|
|
@@ -919,7 +1151,7 @@ Text.
|
|
|
919
1151
|
}
|
|
920
1152
|
});
|
|
921
1153
|
|
|
922
|
-
test('content:sync refuses ambiguous
|
|
1154
|
+
test('content:sync refuses ambiguous image relocation', async () => {
|
|
923
1155
|
const { root, siteDir } = await createTempSite();
|
|
924
1156
|
try {
|
|
925
1157
|
await writeFile(path.join(siteDir, 'content.md'), `---
|
|
@@ -195,6 +195,7 @@ title: Package Check Site
|
|
|
195
195
|
description: Site used by package checks.
|
|
196
196
|
sections:
|
|
197
197
|
intro: {}
|
|
198
|
+
warning: {}
|
|
198
199
|
work: {}
|
|
199
200
|
---
|
|
200
201
|
|
|
@@ -207,6 +208,10 @@ image: package-check.svg
|
|
|
207
208
|
source: package check fixture
|
|
208
209
|
-->
|
|
209
210
|
|
|
211
|
+
## [**Warning**]{.highlight} {#warning}
|
|
212
|
+
|
|
213
|
+
This section verifies styled section headings.
|
|
214
|
+
|
|
210
215
|
## Work {#work}
|
|
211
216
|
|
|
212
217
|
This section keeps the package check independent from user-facing starter copy.
|
|
@@ -317,6 +322,18 @@ This route verifies that packaged norna sites can build route pages.
|
|
|
317
322
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
318
323
|
'class="inline-style inline-style-highlight" style="--inline-style-color: #ffd84d"',
|
|
319
324
|
);
|
|
325
|
+
await assertFileIncludes(
|
|
326
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
327
|
+
'<h2><span class="inline-style inline-style-highlight" style="--inline-style-color: #ffd84d"><strong>Warning</strong></span></h2>',
|
|
328
|
+
);
|
|
329
|
+
await assertFileIncludes(
|
|
330
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
331
|
+
'data-section-title="Warning"',
|
|
332
|
+
);
|
|
333
|
+
await assertFileIncludes(
|
|
334
|
+
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
335
|
+
'href="#warning">Warning</a>',
|
|
336
|
+
);
|
|
320
337
|
await assertFileIncludes(
|
|
321
338
|
path.join(siteProjectRoot, 'dist', 'index.html'),
|
|
322
339
|
'--section-background-color: #000000',
|
|
@@ -256,7 +256,7 @@ const sectionStyle = [
|
|
|
256
256
|
>
|
|
257
257
|
<div class="section-card">
|
|
258
258
|
<header class="section-header">
|
|
259
|
-
<HeadingTag
|
|
259
|
+
<HeadingTag set:html={section.titleHtml} />
|
|
260
260
|
</header>
|
|
261
261
|
|
|
262
262
|
<div class:list={['section-body', { 'section-body-has-gallery': hasMedia, 'section-body-text-only': !hasMedia }]}>
|
|
@@ -38,12 +38,13 @@ type SectionContentBlock =
|
|
|
38
38
|
export type ResolvedSection = SiteSectionMetadata & {
|
|
39
39
|
id: string;
|
|
40
40
|
title: string;
|
|
41
|
+
titleHtml: string;
|
|
41
42
|
contentBlocks: SectionContentBlock[];
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
const headingRegex = /<h2\b([^>]*)>([\s\S]*?)<\/h2>/gi;
|
|
45
46
|
const explicitHeadingIdRegex = /\s*\{#([a-z0-9-]+)\}\s*$/;
|
|
46
|
-
const inlineStyleReferenceRegex = /\[([
|
|
47
|
+
const inlineStyleReferenceRegex = /\[([\s\S]*?)\]\{\.([a-z][a-z0-9-]*)\}/g;
|
|
47
48
|
const markdownH2Regex = /^##\s+.*$/gm;
|
|
48
49
|
const imageProvenanceCommentRegex = /<!--\s*norna-image-provenance:[\s\S]*?-->/gi;
|
|
49
50
|
|
|
@@ -81,8 +82,14 @@ const getHeadingId = (attributes: string, headingHtml: string) => {
|
|
|
81
82
|
const getExplicitHeadingId = (headingHtml: string) =>
|
|
82
83
|
decodeHtmlEntities(stripTags(headingHtml)).trim().match(explicitHeadingIdRegex)?.[1];
|
|
83
84
|
|
|
85
|
+
const stripInlineStyleMarkers = (value: string) =>
|
|
86
|
+
value.replace(inlineStyleReferenceRegex, '$1');
|
|
87
|
+
|
|
84
88
|
const getHeadingTitle = (headingHtml: string) =>
|
|
85
|
-
decodeHtmlEntities(stripTags(headingHtml)).replace(explicitHeadingIdRegex, '').trim();
|
|
89
|
+
stripInlineStyleMarkers(decodeHtmlEntities(stripTags(headingHtml)).replace(explicitHeadingIdRegex, '').trim());
|
|
90
|
+
|
|
91
|
+
const getHeadingTitleHtml = (headingHtml: string, inlineStyles: InlineStyles | undefined) =>
|
|
92
|
+
prepareContentHtml(headingHtml.replace(explicitHeadingIdRegex, '').trim(), inlineStyles);
|
|
86
93
|
|
|
87
94
|
const applyInlineStyles = (html: string, inlineStyles: InlineStyles | undefined) =>
|
|
88
95
|
html.replace(inlineStyleReferenceRegex, (_match, text: string, styleName: string) => {
|
|
@@ -209,6 +216,7 @@ export const getSectionsContent = (
|
|
|
209
216
|
...(sectionMetadata[id] ?? {}),
|
|
210
217
|
id,
|
|
211
218
|
title,
|
|
219
|
+
titleHtml: getHeadingTitleHtml(headingHtml, inlineStyles),
|
|
212
220
|
contentBlocks: resolveContentBlocks(content, rawSections.get(id) ?? '', page, id, inlineStyles),
|
|
213
221
|
});
|
|
214
222
|
}
|
package/src/styles/global.css
CHANGED
package/starters/basic/README.md
CHANGED
|
@@ -17,8 +17,10 @@ npm run dev -- --kill
|
|
|
17
17
|
|
|
18
18
|
Edit site-wide visual defaults in `site/theme.md`, homepage content and section
|
|
19
19
|
overrides in `site/content.md`, technical settings such as URL and locale
|
|
20
|
-
labels in `site/config.mjs`, source images under
|
|
21
|
-
|
|
20
|
+
labels in `site/config.mjs`, source images under `site/images/<section-id>/`,
|
|
21
|
+
and static public files under `site/public/`. The image folder name should
|
|
22
|
+
match the section id in Markdown, for example `## Work {#work}` uses
|
|
23
|
+
`site/images/work/`.
|
|
22
24
|
|
|
23
25
|
Use `site.basePath: '/'` for a custom domain or root-hosted site. For a GitHub
|
|
24
26
|
Pages project site without a custom domain, set both values:
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Example Site
|
|
3
3
|
description: Minimal norna starter site.
|
|
4
|
-
sections:
|
|
5
|
-
intro:
|
|
6
|
-
presentation:
|
|
7
|
-
typography:
|
|
8
|
-
preset: statement
|
|
9
|
-
|
|
10
4
|
---
|
|
11
5
|
|
|
12
6
|
## Intro {#intro}
|
|
@@ -15,8 +9,8 @@ routes defined, the navigation menu will be a sticky bar with anchor links for
|
|
|
15
9
|
ease of use on phones etc. [Highlighted inline text]{.highlight} when a short
|
|
16
10
|
phrase needs emphasis
|
|
17
11
|
## Work {#work}
|
|
18
|
-
Add Norna
|
|
19
|
-
`images/work/`.
|
|
12
|
+
Add Norna managed media blocks in Markdown where the media should appear. Put
|
|
13
|
+
matching source images under `images/work/`.
|
|
20
14
|
|
|
21
15
|
````
|
|
22
16
|
```norna-image-stack
|
|
@@ -23,7 +23,8 @@ npm run dev -- --kill
|
|
|
23
23
|
- `site/theme.md`: site-wide visual settings such as brand text, layout
|
|
24
24
|
density, typography rhythm, colors, frame colors, and inline text styles.
|
|
25
25
|
- `site/content.md`: homepage title, sections, placeholders, project summary,
|
|
26
|
-
links, install command, example usage, benefits, next steps,
|
|
26
|
+
links, install command, example usage, benefits, next steps, managed media
|
|
27
|
+
blocks, and license.
|
|
27
28
|
- `site/routes/010-guide/route-content.md`: short secondary page with realistic
|
|
28
29
|
project guide content.
|
|
29
30
|
- `site/public/robots.txt`: static public file copied into the built site.
|
|
@@ -40,15 +41,18 @@ GitHub Pages workflow uses `npm ci`.
|
|
|
40
41
|
delete the route if the homepage is enough.
|
|
41
42
|
3. Edit `site/theme.md` for brand name, layout density, typography rhythm,
|
|
42
43
|
colors, frame colors, and inline text styles.
|
|
43
|
-
4.
|
|
44
|
+
4. Put any local source images in the image folder for the Markdown section
|
|
45
|
+
that references them, for example `site/images/intro/`.
|
|
46
|
+
5. Edit `site/config.mjs` for public URL, GitHub repository, footer text, and
|
|
44
47
|
deploy workflow name.
|
|
45
|
-
|
|
48
|
+
6. Update `package.json` with the site's package name and keep
|
|
46
49
|
`package-lock.json` committed.
|
|
47
50
|
|
|
48
51
|
## Common Commands
|
|
49
52
|
|
|
50
53
|
```sh
|
|
51
54
|
npm run norna:check
|
|
55
|
+
npm run norna:sync
|
|
52
56
|
npm run norna:typography:show
|
|
53
57
|
npm run build
|
|
54
58
|
```
|