@janga/norna 0.7.23 → 0.7.25
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 +14 -18
- package/astro.config.mjs +14 -0
- package/bin/norna-cli.mjs +6 -0
- package/package.json +25 -2
- package/schemas/category.schema.json +2 -2
- package/schemas/config.schema.json +80 -15
- package/schemas/content-frontmatter.schema.json +7 -7
- package/schemas/page-theme.schema.json +26 -26
- package/schemas/sitewide-content.schema.json +18 -18
- package/schemas/theme.schema.json +246 -246
- package/scripts/build-site.mjs +1 -0
- package/scripts/check-config.mjs +16 -1
- package/scripts/dev-local.mjs +106 -11
- package/scripts/generate-search-index.mjs +57 -0
- package/scripts/init-site.mjs +3 -0
- package/scripts/lib/code-fence-metadata.mjs +228 -0
- package/scripts/lib/edit-source-link.mjs +102 -0
- package/scripts/lib/editor-language-service.mjs +32 -12
- package/scripts/lib/image-presentation.mjs +4 -0
- package/scripts/lib/navigation-model.mjs +34 -13
- package/scripts/lib/navigation-review.mjs +396 -0
- package/scripts/lib/norna-markdown-blocks.mjs +164 -26
- package/scripts/lib/norna-markdown-render-plugin.mjs +64 -1
- package/scripts/lib/page-aliases.mjs +21 -1
- package/scripts/lib/page-markdown.mjs +32 -1
- package/scripts/lib/page-move-plan.mjs +659 -0
- package/scripts/lib/presentation-palette-metadata.mjs +1 -1
- package/scripts/lib/presentation.mjs +1 -10
- package/scripts/lib/project-config.mjs +141 -5
- package/scripts/lib/public-asset-conventions.mjs +36 -2
- package/scripts/lib/schema-definitions.mjs +30 -5
- package/scripts/lib/schema-editor-metadata.mjs +36 -4
- package/scripts/lib/schema-value-definitions.mjs +4 -1
- package/scripts/lib/semantic-callouts.mjs +128 -0
- package/scripts/lib/site-content.mjs +2 -1
- package/scripts/lib/site-link-graph.mjs +33 -2
- package/scripts/lib/site-navigation-tree.mjs +85 -0
- package/scripts/lib/site-paths.mjs +14 -2
- package/scripts/lib/social-image-assets.mjs +30 -0
- package/scripts/lib/table-render-plugin.mjs +33 -0
- package/scripts/lib/theme-presets.mjs +2 -2
- package/scripts/lib/theme-profiles.mjs +0 -4
- package/scripts/move-site-page.mjs +256 -0
- package/scripts/review-navigation.mjs +32 -0
- package/scripts/sync-content-sections.mjs +67 -4
- package/scripts/sync-site-public.mjs +24 -5
- package/src/components/CardList.astro +1 -0
- package/src/components/CodeBlockCopyScript.astro +11 -5
- package/src/components/EditSourceLink.astro +17 -0
- package/src/components/ImageCarousel.astro +9 -7
- package/src/components/ImageStack.astro +34 -9
- package/src/components/ImageStackEnhancement.astro +331 -0
- package/src/components/NavigationPageTree.astro +66 -55
- package/src/components/NavigationTreeControls.astro +77 -0
- package/src/components/PageAliasRedirect.astro +1 -1
- package/src/components/PageContentsNavigation.astro +2 -3
- package/src/components/PageList.astro +33 -0
- package/src/components/PageSequenceNavigation.astro +37 -0
- package/src/components/SearchPage.astro +151 -0
- package/src/components/SectionNavigationScript.astro +45 -7
- package/src/components/SiteNavigation.astro +102 -51
- package/src/components/SitePage.astro +107 -39
- package/src/components/SiteSection.astro +11 -3
- package/src/components/SiteTreeNavigation.astro +11 -1
- package/src/components/TableOverflowScript.astro +251 -0
- package/src/components/TreeNavigationScript.astro +271 -34
- package/src/layouts/BaseLayout.astro +83 -2
- package/src/lib/generatedImages.ts +18 -0
- package/src/lib/sectionContent.ts +36 -30
- package/src/lib/siteNavigation.ts +31 -52
- package/src/lib/sitePages.ts +6 -3
- package/src/lib/sitePublicAssets.ts +1 -0
- package/src/pages/404.astro +110 -0
- package/src/pages/[...slug].astro +15 -5
- package/src/styles/content.css +538 -39
- package/src/styles/media.css +187 -8
- package/src/styles/navigation.css +199 -3
- package/src/styles/page-layout.css +481 -100
- package/src/styles/responsive.css +144 -28
- package/starters/basic/README.md +6 -19
- package/starters/basic/package.json +1 -0
- package/starters/basic/site/pages/000-home/content.md +13 -50
|
@@ -4,7 +4,7 @@ const field = (description, options = {}) => Object.freeze({ description, ...opt
|
|
|
4
4
|
const value = (title, description) => Object.freeze({ title, description });
|
|
5
5
|
|
|
6
6
|
export const nornaMarkdownBlockDefinitions = Object.freeze({
|
|
7
|
-
'
|
|
7
|
+
'image-stack': Object.freeze({
|
|
8
8
|
description: 'Display one or more managed images in a vertical stack.',
|
|
9
9
|
documentation: documentationLink('Image stack reference', 'content.md', 'image-stack'),
|
|
10
10
|
item: Object.freeze({
|
|
@@ -15,7 +15,7 @@ export const nornaMarkdownBlockDefinitions = Object.freeze({
|
|
|
15
15
|
}),
|
|
16
16
|
}),
|
|
17
17
|
}),
|
|
18
|
-
'
|
|
18
|
+
'image-carousel': Object.freeze({
|
|
19
19
|
description: 'Display two or more managed images in an interactive carousel.',
|
|
20
20
|
documentation: documentationLink('Image carousel reference', 'content.md', 'image-carousel'),
|
|
21
21
|
item: Object.freeze({
|
|
@@ -26,7 +26,7 @@ export const nornaMarkdownBlockDefinitions = Object.freeze({
|
|
|
26
26
|
}),
|
|
27
27
|
}),
|
|
28
28
|
}),
|
|
29
|
-
'
|
|
29
|
+
'card-list': Object.freeze({
|
|
30
30
|
description: 'Display a structured list of cards.',
|
|
31
31
|
documentation: documentationLink('Card list reference', 'content.md', 'card-list'),
|
|
32
32
|
options: Object.freeze({
|
|
@@ -73,25 +73,45 @@ export const nornaMarkdownBlockDefinitions = Object.freeze({
|
|
|
73
73
|
}),
|
|
74
74
|
}),
|
|
75
75
|
}),
|
|
76
|
+
'page-list': Object.freeze({
|
|
77
|
+
description: 'Display the current page\'s listed direct child pages in navigation order.',
|
|
78
|
+
documentation: documentationLink('Child page list reference', 'content.md', 'child-page-list'),
|
|
79
|
+
}),
|
|
76
80
|
});
|
|
77
81
|
|
|
78
82
|
export const nornaBlockTypes = new Set(Object.keys(nornaMarkdownBlockDefinitions));
|
|
79
83
|
|
|
84
|
+
const renamedNornaBlockTypes = Object.freeze({
|
|
85
|
+
'norna-image-stack': 'image-stack',
|
|
86
|
+
'norna-image-carousel': 'image-carousel',
|
|
87
|
+
'norna-carousel': 'image-carousel',
|
|
88
|
+
carousel: 'image-carousel',
|
|
89
|
+
'norna-card-list': 'card-list',
|
|
90
|
+
'norna-page-list': 'page-list',
|
|
91
|
+
});
|
|
92
|
+
|
|
80
93
|
const blockTypeLabels = {
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
94
|
+
'image-stack': 'image-stack',
|
|
95
|
+
'image-carousel': 'image-carousel',
|
|
96
|
+
'card-list': 'card-list',
|
|
97
|
+
'page-list': 'page-list',
|
|
84
98
|
};
|
|
85
99
|
|
|
86
100
|
const knownBlockTypeList = Array.from(nornaBlockTypes).join(', ');
|
|
87
101
|
const imageNameRegex = /^[a-z0-9][a-z0-9.-]*\.(jpe?g|png|svg)$/i;
|
|
88
102
|
const imageStackExample = [
|
|
89
|
-
'```
|
|
103
|
+
'```image-stack',
|
|
90
104
|
'- image: filename.jpg',
|
|
91
105
|
'```',
|
|
92
106
|
].join('\n');
|
|
107
|
+
const carouselExample = [
|
|
108
|
+
'```image-carousel',
|
|
109
|
+
'- image: first.jpg',
|
|
110
|
+
'- image: second.jpg',
|
|
111
|
+
'```',
|
|
112
|
+
].join('\n');
|
|
93
113
|
const cardListExample = [
|
|
94
|
-
'```
|
|
114
|
+
'```card-list',
|
|
95
115
|
'layout: image-top',
|
|
96
116
|
'flow: grid',
|
|
97
117
|
'size: m',
|
|
@@ -103,7 +123,17 @@ const cardListExample = [
|
|
|
103
123
|
' badge-text: Recommended',
|
|
104
124
|
'```',
|
|
105
125
|
].join('\n');
|
|
106
|
-
const
|
|
126
|
+
const pageListExample = [
|
|
127
|
+
'```page-list',
|
|
128
|
+
'```',
|
|
129
|
+
].join('\n');
|
|
130
|
+
const blockExamples = Object.freeze({
|
|
131
|
+
'image-stack': imageStackExample,
|
|
132
|
+
'image-carousel': carouselExample,
|
|
133
|
+
'card-list': cardListExample,
|
|
134
|
+
'page-list': pageListExample,
|
|
135
|
+
});
|
|
136
|
+
const cardListDefinition = nornaMarkdownBlockDefinitions['card-list'];
|
|
107
137
|
const cardListLayouts = new Set(Object.keys(cardListDefinition.options.layout.values));
|
|
108
138
|
const cardListFlows = new Set(Object.keys(cardListDefinition.options.flow.values));
|
|
109
139
|
const cardListSizes = new Set(Object.keys(cardListDefinition.options.size.values));
|
|
@@ -120,18 +150,20 @@ const fail = (message, options) => {
|
|
|
120
150
|
};
|
|
121
151
|
|
|
122
152
|
const getUnknownNornaBlockMessage = (type) => [
|
|
123
|
-
|
|
153
|
+
renamedNornaBlockTypes[type]
|
|
154
|
+
? `Norna block "${type}" was renamed to "${renamedNornaBlockTypes[type]}".`
|
|
155
|
+
: `Unknown Norna block "${type}". Use one of: ${knownBlockTypeList}.`,
|
|
124
156
|
type === 'norna-gallery-stack'
|
|
125
|
-
? 'Use
|
|
157
|
+
? 'Use image-stack for one or more stacked images.'
|
|
126
158
|
: null,
|
|
127
|
-
type === 'norna-carousel'
|
|
128
|
-
? 'Use
|
|
159
|
+
type === 'norna-image-carousel' || type === 'norna-carousel' || type === 'carousel'
|
|
160
|
+
? 'Use image-carousel for an image carousel.'
|
|
129
161
|
: null,
|
|
130
162
|
type === 'norna-image'
|
|
131
|
-
? 'Use
|
|
163
|
+
? 'Use image-stack for a single image or a stacked list of images.'
|
|
132
164
|
: null,
|
|
133
165
|
'Example:',
|
|
134
|
-
imageStackExample,
|
|
166
|
+
blockExamples[renamedNornaBlockTypes[type] ?? type] ?? imageStackExample,
|
|
135
167
|
].filter(Boolean).join(' ');
|
|
136
168
|
|
|
137
169
|
const decodeScalar = (value) => {
|
|
@@ -229,17 +261,23 @@ export const getOpenMarkdownFenceAtLine = (markdown, lineIndex) => {
|
|
|
229
261
|
};
|
|
230
262
|
|
|
231
263
|
const getNornaLineAttempt = (line) => {
|
|
232
|
-
const match = line.match(/^ {0,3}([`~]{0,2})(
|
|
264
|
+
const match = line.match(/^ {0,3}([`~]{0,2})([a-z][a-z0-9-]+)\s*$/);
|
|
233
265
|
if (!match) return null;
|
|
266
|
+
const marker = match[1];
|
|
267
|
+
const type = match[2];
|
|
268
|
+
const isNamespacedAttempt = type.startsWith('norna-');
|
|
269
|
+
if (!isNamespacedAttempt && !nornaBlockTypes.has(type) && !renamedNornaBlockTypes[type]) return null;
|
|
270
|
+
if (!marker && type === 'carousel') return null;
|
|
234
271
|
|
|
235
272
|
return {
|
|
236
|
-
marker
|
|
237
|
-
type
|
|
273
|
+
marker,
|
|
274
|
+
type,
|
|
238
275
|
};
|
|
239
276
|
};
|
|
240
277
|
|
|
241
278
|
const getNornaFenceStartMessage = (type, marker = '') => {
|
|
242
|
-
const
|
|
279
|
+
const supportedType = renamedNornaBlockTypes[type] ?? type;
|
|
280
|
+
const example = blockExamples[supportedType] ?? imageStackExample;
|
|
243
281
|
if (marker) {
|
|
244
282
|
return `Invalid Norna block start for "${type}". Use three backticks or three tildes. Example:\n${example}`;
|
|
245
283
|
}
|
|
@@ -317,7 +355,7 @@ const scanMarkdownFencedBlocks = (markdown, options = {}) => {
|
|
|
317
355
|
}
|
|
318
356
|
}
|
|
319
357
|
|
|
320
|
-
const isNornaLike = type.startsWith('norna-');
|
|
358
|
+
const isNornaLike = type.startsWith('norna-') || nornaBlockTypes.has(type) || Boolean(renamedNornaBlockTypes[type]);
|
|
321
359
|
if (closingIndex === -1) {
|
|
322
360
|
if (isNornaLike) {
|
|
323
361
|
errors.push({
|
|
@@ -350,7 +388,7 @@ const scanMarkdownFencedBlocks = (markdown, options = {}) => {
|
|
|
350
388
|
} else {
|
|
351
389
|
errors.push({
|
|
352
390
|
blockType: type,
|
|
353
|
-
code: 'unknown-norna-block',
|
|
391
|
+
code: renamedNornaBlockTypes[type] ? 'renamed-norna-block' : 'unknown-norna-block',
|
|
354
392
|
line: lineNumber,
|
|
355
393
|
source,
|
|
356
394
|
message: failMessage(getUnknownNornaBlockMessage(type), { ...options, line: lineNumber }),
|
|
@@ -406,10 +444,10 @@ const parseImageListBlock = (source, options = {}) => {
|
|
|
406
444
|
}
|
|
407
445
|
|
|
408
446
|
if (images.length === 0) {
|
|
409
|
-
fail(`${options.type} must contain at least one image. Example:\n${
|
|
447
|
+
fail(`${options.type} must contain at least one image. Example:\n${blockExamples[options.type]}`, options);
|
|
410
448
|
}
|
|
411
449
|
|
|
412
|
-
return { type: options.type === '
|
|
450
|
+
return { type: options.type === 'image-carousel' ? 'image-carousel' : 'image-stack', images };
|
|
413
451
|
};
|
|
414
452
|
|
|
415
453
|
const parseCardListBlock = (source, options = {}) => {
|
|
@@ -517,15 +555,23 @@ const parseCardListBlock = (source, options = {}) => {
|
|
|
517
555
|
return { type: 'card-list', layout, flow, size, width, cards };
|
|
518
556
|
};
|
|
519
557
|
|
|
558
|
+
const parsePageListBlock = (source, options = {}) => {
|
|
559
|
+
if (source.trim()) {
|
|
560
|
+
fail(`${options.type} does not accept options or items. Leave the block empty. Example:\n${pageListExample}`, options);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return { type: 'page-list' };
|
|
564
|
+
};
|
|
565
|
+
|
|
520
566
|
export const parseNornaMarkdownBlock = (type, source, options = {}) => {
|
|
521
567
|
if (!nornaBlockTypes.has(type)) {
|
|
522
568
|
fail(getUnknownNornaBlockMessage(type), options);
|
|
523
569
|
}
|
|
524
570
|
|
|
525
571
|
const parseOptions = { ...options, type: blockTypeLabels[type] };
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
572
|
+
if (type === 'card-list') return parseCardListBlock(source, parseOptions);
|
|
573
|
+
if (type === 'page-list') return parsePageListBlock(source, parseOptions);
|
|
574
|
+
return parseImageListBlock(source, parseOptions);
|
|
529
575
|
};
|
|
530
576
|
|
|
531
577
|
const getLineNumber = (source, index) => source.slice(0, index).split(/\r?\n/).length;
|
|
@@ -561,6 +607,97 @@ export const extractNornaMarkdownBlockDiagnostics = (markdown, options = {}) =>
|
|
|
561
607
|
return { blocks, errors };
|
|
562
608
|
};
|
|
563
609
|
|
|
610
|
+
const stripRenderedTags = (value) => value.replace(/<[^>]*>/g, '');
|
|
611
|
+
const decodeRenderedEntities = (value) => value
|
|
612
|
+
.replace(/&/g, '&')
|
|
613
|
+
.replace(/</g, '<')
|
|
614
|
+
.replace(/>/g, '>')
|
|
615
|
+
.replace(/"/g, '"')
|
|
616
|
+
.replace(/'/g, "'");
|
|
617
|
+
const normalizeRenderedBlockSource = (value) => value.replace(/\r\n?/g, '\n').trim();
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Splits rendered HTML at Norna markers. During an Astro content hot reload,
|
|
621
|
+
* custom code nodes can occasionally arrive as highlighted code instead of
|
|
622
|
+
* their marker; match that output against the already validated source block.
|
|
623
|
+
*
|
|
624
|
+
* @template {{ type: string, source?: string }} Block
|
|
625
|
+
* @param {string} html
|
|
626
|
+
* @param {Block[]} blocks
|
|
627
|
+
* @returns {Array<{ type: 'html', html: string } | Block>}
|
|
628
|
+
*/
|
|
629
|
+
export const splitNornaRenderedBlocks = (html, blocks) => {
|
|
630
|
+
const markerRegex = /<norna-block\s+data-index="(\d+)"\s*><\/norna-block>/g;
|
|
631
|
+
const markerMatches = [...html.matchAll(markerRegex)];
|
|
632
|
+
let replacements;
|
|
633
|
+
|
|
634
|
+
if (markerMatches.length > 0) {
|
|
635
|
+
const seen = new Set();
|
|
636
|
+
replacements = markerMatches.map((match) => {
|
|
637
|
+
const blockIndex = Number.parseInt(match[1] ?? '', 10);
|
|
638
|
+
if (!blocks[blockIndex]) {
|
|
639
|
+
throw new Error(`Rendered Norna block ${blockIndex + 1} has no matching parsed block.`);
|
|
640
|
+
}
|
|
641
|
+
if (seen.has(blockIndex)) {
|
|
642
|
+
throw new Error(`Rendered Norna block ${blockIndex + 1} appears more than once.`);
|
|
643
|
+
}
|
|
644
|
+
seen.add(blockIndex);
|
|
645
|
+
return { match, blockIndex };
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
if (seen.size !== blocks.length) {
|
|
649
|
+
throw new Error(`Rendered Markdown contains ${seen.size} Norna block markers, but ${blocks.length} blocks were parsed.`);
|
|
650
|
+
}
|
|
651
|
+
} else {
|
|
652
|
+
const codeRegex = /<pre\b[^>]*>\s*<code\b[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/g;
|
|
653
|
+
const candidates = [...html.matchAll(codeRegex)].map((match) => ({
|
|
654
|
+
match,
|
|
655
|
+
source: normalizeRenderedBlockSource(decodeRenderedEntities(stripRenderedTags(match[1] ?? ''))),
|
|
656
|
+
}));
|
|
657
|
+
const solutions = [];
|
|
658
|
+
const findSolutions = (blockIndex, candidateIndex, selected) => {
|
|
659
|
+
if (solutions.length > 1) return;
|
|
660
|
+
if (blockIndex === blocks.length) {
|
|
661
|
+
solutions.push([...selected]);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const source = normalizeRenderedBlockSource(blocks[blockIndex]?.source ?? '');
|
|
666
|
+
for (let index = candidateIndex; index < candidates.length; index += 1) {
|
|
667
|
+
if (candidates[index].source !== source) continue;
|
|
668
|
+
selected.push(index);
|
|
669
|
+
findSolutions(blockIndex + 1, index + 1, selected);
|
|
670
|
+
selected.pop();
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
findSolutions(0, 0, []);
|
|
674
|
+
|
|
675
|
+
if (solutions.length === 0) {
|
|
676
|
+
throw new Error(`Rendered Markdown contains 0 Norna block markers, but ${blocks.length} blocks were parsed.`);
|
|
677
|
+
}
|
|
678
|
+
if (solutions.length > 1) {
|
|
679
|
+
throw new Error('Rendered Markdown contains ambiguous plain-code matches for Norna blocks. Restart the local preview to rebuild the Markdown content cache.');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
replacements = solutions[0].map((candidateIndex, blockIndex) => ({
|
|
683
|
+
match: candidates[candidateIndex].match,
|
|
684
|
+
blockIndex,
|
|
685
|
+
}));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const result = [];
|
|
689
|
+
let cursor = 0;
|
|
690
|
+
for (const { match, blockIndex } of replacements) {
|
|
691
|
+
const start = match.index ?? 0;
|
|
692
|
+
if (start > cursor) result.push({ type: 'html', html: html.slice(cursor, start) });
|
|
693
|
+
result.push(blocks[blockIndex]);
|
|
694
|
+
cursor = start + match[0].length;
|
|
695
|
+
}
|
|
696
|
+
if (cursor < html.length) result.push({ type: 'html', html: html.slice(cursor) });
|
|
697
|
+
|
|
698
|
+
return result.filter((block) => block.type !== 'html' || block.html.trim());
|
|
699
|
+
};
|
|
700
|
+
|
|
564
701
|
const maskFencedCodeBlocks = (markdown) => {
|
|
565
702
|
const lines = normalizeLines(markdown);
|
|
566
703
|
const maskedLines = [];
|
|
@@ -631,6 +768,7 @@ export const getNornaBlockImageReferences = (blocks) =>
|
|
|
631
768
|
line: card.line ?? block.line,
|
|
632
769
|
}));
|
|
633
770
|
}
|
|
771
|
+
if (block.type === 'page-list') return [];
|
|
634
772
|
|
|
635
773
|
return block.images.map((image) => ({
|
|
636
774
|
...image,
|
|
@@ -1,17 +1,80 @@
|
|
|
1
1
|
import { defineMdastPlugin } from 'satteri';
|
|
2
2
|
import { nornaBlockTypes } from './norna-markdown-blocks.mjs';
|
|
3
|
+
import projectConfig from './project-config.mjs';
|
|
4
|
+
import { getSemanticCalloutMarker } from './semantic-callouts.mjs';
|
|
3
5
|
|
|
4
6
|
const stateKey = 'nornaMarkdownRender';
|
|
5
7
|
|
|
6
8
|
const getState = (context) => {
|
|
7
9
|
if (!context.data[stateKey]) {
|
|
8
|
-
context.data[stateKey] = { blockIndex: 0, regionIndex: 0 };
|
|
10
|
+
context.data[stateKey] = { blockIndex: 0, calloutIndex: 0, regionIndex: 0 };
|
|
9
11
|
}
|
|
10
12
|
return context.data[stateKey];
|
|
11
13
|
};
|
|
12
14
|
|
|
15
|
+
const calloutLabelKeys = Object.freeze({
|
|
16
|
+
CAUTION: 'calloutCaution',
|
|
17
|
+
DANGER: 'calloutDanger',
|
|
18
|
+
IMPORTANT: 'calloutImportant',
|
|
19
|
+
NOTE: 'calloutNote',
|
|
20
|
+
TIP: 'calloutTip',
|
|
21
|
+
WARNING: 'calloutWarning',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const hasBlockquoteAncestor = (node, context) => {
|
|
25
|
+
let parent = context.parent(node);
|
|
26
|
+
while (parent) {
|
|
27
|
+
if (parent.type === 'blockquote') return true;
|
|
28
|
+
parent = context.parent(parent);
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
};
|
|
32
|
+
|
|
13
33
|
export const nornaMarkdownRenderPlugin = defineMdastPlugin({
|
|
14
34
|
name: 'norna-markdown-render',
|
|
35
|
+
blockquote(node, context) {
|
|
36
|
+
const marker = getSemanticCalloutMarker(node);
|
|
37
|
+
if (
|
|
38
|
+
!marker
|
|
39
|
+
|| marker.malformed
|
|
40
|
+
|| !marker.supported
|
|
41
|
+
|| marker.rawType !== marker.type
|
|
42
|
+
|| marker.title
|
|
43
|
+
|| !marker.hasBody
|
|
44
|
+
|| hasBlockquoteAncestor(node, context)
|
|
45
|
+
) return;
|
|
46
|
+
|
|
47
|
+
const state = getState(context);
|
|
48
|
+
const labelKey = calloutLabelKeys[marker.type];
|
|
49
|
+
const label = projectConfig.locale.labels[labelKey];
|
|
50
|
+
const labelId = `norna-callout-label-${state.calloutIndex}`;
|
|
51
|
+
state.calloutIndex += 1;
|
|
52
|
+
|
|
53
|
+
context.setProperty(node, 'data', {
|
|
54
|
+
hName: 'aside',
|
|
55
|
+
hProperties: {
|
|
56
|
+
'aria-labelledby': labelId,
|
|
57
|
+
className: ['norna-callout', `norna-callout-${marker.type.toLowerCase()}`],
|
|
58
|
+
role: 'note',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!marker.firstTextRemainder && marker.firstParagraph.children.length === 1) {
|
|
63
|
+
context.removeChildAt(node, 0);
|
|
64
|
+
} else {
|
|
65
|
+
context.setProperty(marker.firstText, 'value', marker.firstTextRemainder);
|
|
66
|
+
}
|
|
67
|
+
context.prependChild(node, {
|
|
68
|
+
type: 'paragraph',
|
|
69
|
+
data: {
|
|
70
|
+
hProperties: {
|
|
71
|
+
className: ['norna-callout-label'],
|
|
72
|
+
id: labelId,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
children: [{ type: 'text', value: label }],
|
|
76
|
+
});
|
|
77
|
+
},
|
|
15
78
|
code(node, context) {
|
|
16
79
|
if (!nornaBlockTypes.has(node.lang)) return;
|
|
17
80
|
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { getSiteNodePathname } from './site-page-urls.mjs';
|
|
2
2
|
import { sitemapFilename } from './sitemap.mjs';
|
|
3
3
|
|
|
4
|
-
export const
|
|
4
|
+
export const getGeneratedSiteRoutes = ({ searchEnabled = false } = {}) => Object.freeze([
|
|
5
5
|
Object.freeze({
|
|
6
6
|
kind: 'generated-route',
|
|
7
7
|
label: `Norna generated ${sitemapFilename}`,
|
|
8
8
|
pathname: `/${sitemapFilename}`,
|
|
9
9
|
}),
|
|
10
|
+
...(searchEnabled ? [Object.freeze({
|
|
11
|
+
kind: 'generated-route',
|
|
12
|
+
label: 'Norna generated search page',
|
|
13
|
+
pathname: '/search/',
|
|
14
|
+
})] : []),
|
|
10
15
|
]);
|
|
11
16
|
|
|
17
|
+
export const generatedSiteRoutes = getGeneratedSiteRoutes();
|
|
18
|
+
|
|
12
19
|
const getPagePathname = (page) => page.pathname ?? getSiteNodePathname(page.contentFile ?? page);
|
|
13
20
|
const getPageLabel = (page) => page.contentLabel ?? page.contentFile?.contentLabel ?? page.entry?.id ?? getPagePathname(page);
|
|
14
21
|
const getPageAliases = (page) => page.aliases ?? page.data?.page?.aliases ?? [];
|
|
@@ -99,6 +106,19 @@ export const createPageAliasModel = ({
|
|
|
99
106
|
for (const identity of getPublicFileIdentities(file)) addIdentity(identity);
|
|
100
107
|
}
|
|
101
108
|
for (const route of generatedRoutes) {
|
|
109
|
+
const conflict = identitiesByPathname.get(route.pathname);
|
|
110
|
+
if (conflict) {
|
|
111
|
+
diagnostics.push({
|
|
112
|
+
code: 'generated-route-collision',
|
|
113
|
+
contentFile: conflict.kind === 'page'
|
|
114
|
+
? conflict.source.contentFile ?? conflict.source
|
|
115
|
+
: undefined,
|
|
116
|
+
fix: `Move or rename the conflicting source. ${route.label} owns ${route.pathname} while the feature is enabled.`,
|
|
117
|
+
message: `Generated route "${route.pathname}" from ${route.label} conflicts with ${describeIdentity(conflict)}.`,
|
|
118
|
+
severity: 'error',
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
102
122
|
addIdentity(createIdentity({ ...route, source: route }));
|
|
103
123
|
}
|
|
104
124
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getCodeFenceMetadataDiagnostics } from './code-fence-metadata.mjs';
|
|
1
2
|
import {
|
|
2
3
|
getHeadingIdentifierIssues,
|
|
3
4
|
getMarkdownHeadings,
|
|
@@ -11,7 +12,9 @@ import {
|
|
|
11
12
|
extractMarkdownImageReferences,
|
|
12
13
|
extractNornaMarkdownBlockDiagnostics,
|
|
13
14
|
getNornaBlockImageReferences,
|
|
15
|
+
nornaBlockTypes,
|
|
14
16
|
} from './norna-markdown-blocks.mjs';
|
|
17
|
+
import { getSemanticCalloutDiagnostics } from './semantic-callouts.mjs';
|
|
15
18
|
|
|
16
19
|
const normalizeMarkdownWithOffsets = (source) => {
|
|
17
20
|
const input = String(source);
|
|
@@ -172,7 +175,7 @@ const getRegionContent = (regionMarkdown, blocks, regionOffset) => {
|
|
|
172
175
|
return content.filter((item) => item.kind !== 'markdown' || item.markdown.length > 0);
|
|
173
176
|
};
|
|
174
177
|
|
|
175
|
-
const createRegion = ({ heading, nextHeading, headings, source, label, lineOffset }) => {
|
|
178
|
+
const createRegion = ({ calloutErrors, codeFenceErrors, heading, nextHeading, headings, source, label, lineOffset }) => {
|
|
176
179
|
const endOffset = nextHeading?.index ?? source.length;
|
|
177
180
|
const markdown = source.slice(heading.index, endOffset).trimEnd();
|
|
178
181
|
const regionLineOffset = lineOffset + heading.line - 1;
|
|
@@ -197,6 +200,8 @@ const createRegion = ({ heading, nextHeading, headings, source, label, lineOffse
|
|
|
197
200
|
bodyMarkdown: source.slice(heading.index + heading.source.length, endOffset).trimEnd(),
|
|
198
201
|
blocks: blockResult.blocks,
|
|
199
202
|
blockErrors: blockResult.errors,
|
|
203
|
+
calloutErrors: calloutErrors.filter((error) => error.offset >= heading.index && error.offset < endOffset),
|
|
204
|
+
codeFenceErrors: codeFenceErrors.filter((error) => error.offset >= heading.index && error.offset < endOffset),
|
|
200
205
|
content: getRegionContent(markdown, blockResult.blocks, heading.index),
|
|
201
206
|
endOffset,
|
|
202
207
|
heading,
|
|
@@ -220,12 +225,20 @@ export const parsePageMarkdown = async (markdown, options = {}) => {
|
|
|
220
225
|
const label = options.label ?? 'Markdown';
|
|
221
226
|
const lineOffset = options.lineOffset ?? 0;
|
|
222
227
|
const { headings, tree } = await getMarkdownHeadings(source);
|
|
228
|
+
const calloutErrors = getSemanticCalloutDiagnostics(tree, { label, lineOffset });
|
|
229
|
+
const codeFenceErrors = getCodeFenceMetadataDiagnostics(tree, {
|
|
230
|
+
excludedLanguages: nornaBlockTypes,
|
|
231
|
+
label,
|
|
232
|
+
lineOffset,
|
|
233
|
+
});
|
|
223
234
|
const structuralHeadings = headings.filter((heading) => heading.depth <= 2);
|
|
224
235
|
const prelude = structuralHeadings.length > 0
|
|
225
236
|
? source.slice(0, structuralHeadings[0].index)
|
|
226
237
|
: source;
|
|
227
238
|
const pageHeadings = structuralHeadings.filter((heading) => heading.depth === 1);
|
|
228
239
|
const regions = structuralHeadings.map((heading, index) => createRegion({
|
|
240
|
+
calloutErrors,
|
|
241
|
+
codeFenceErrors,
|
|
229
242
|
heading,
|
|
230
243
|
nextHeading: structuralHeadings[index + 1],
|
|
231
244
|
headings,
|
|
@@ -250,6 +263,22 @@ export const parsePageMarkdown = async (markdown, options = {}) => {
|
|
|
250
263
|
regionId: region.id,
|
|
251
264
|
severity: 'error',
|
|
252
265
|
})));
|
|
266
|
+
const calloutDiagnostics = regions.flatMap((region) => region.calloutErrors.map((error) => ({
|
|
267
|
+
code: error.code,
|
|
268
|
+
fix: error.fix,
|
|
269
|
+
line: error.line,
|
|
270
|
+
message: error.message,
|
|
271
|
+
regionId: region.id,
|
|
272
|
+
severity: 'error',
|
|
273
|
+
})));
|
|
274
|
+
const codeFenceDiagnostics = regions.flatMap((region) => region.codeFenceErrors.map((error) => ({
|
|
275
|
+
code: error.code,
|
|
276
|
+
fix: error.fix,
|
|
277
|
+
line: error.line,
|
|
278
|
+
message: error.message,
|
|
279
|
+
regionId: region.id,
|
|
280
|
+
severity: 'error',
|
|
281
|
+
})));
|
|
253
282
|
const blocks = regions.flatMap((region) => region.blocks);
|
|
254
283
|
const links = [
|
|
255
284
|
...extractMarkdownLinks({ source, tree, lineOffset }),
|
|
@@ -279,6 +308,8 @@ export const parsePageMarkdown = async (markdown, options = {}) => {
|
|
|
279
308
|
...headingDiagnostics,
|
|
280
309
|
...blockDiagnostics,
|
|
281
310
|
...noteDiagnostics,
|
|
311
|
+
...calloutDiagnostics,
|
|
312
|
+
...codeFenceDiagnostics,
|
|
282
313
|
],
|
|
283
314
|
headings,
|
|
284
315
|
headingIssues,
|