sourcey 3.4.4 → 3.4.6
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 +23 -16
- package/dist/components/layout/Page.d.ts.map +1 -1
- package/dist/components/layout/Page.js +9 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -1
- package/dist/core/doxygen-loader.d.ts +4 -0
- package/dist/core/doxygen-loader.d.ts.map +1 -1
- package/dist/core/doxygen-loader.js +49 -5
- package/dist/core/markdown-loader.d.ts +2 -0
- package/dist/core/markdown-loader.d.ts.map +1 -1
- package/dist/core/markdown-loader.js +715 -192
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -11
- package/dist/renderer/html-builder.d.ts.map +1 -1
- package/dist/renderer/html-builder.js +2 -1
- package/dist/themes/default/sourcey.css +13 -0
- package/dist/utils/icons.d.ts +2 -2
- package/dist/utils/icons.d.ts.map +1 -1
- package/dist/utils/icons.js +24 -42
- package/package.json +3 -2
|
@@ -3,7 +3,7 @@ import { basename, extname, relative } from "node:path";
|
|
|
3
3
|
import { load as parseYaml } from "js-yaml";
|
|
4
4
|
import { htmlId } from "../utils/html-id.js";
|
|
5
5
|
import { renderIcon } from "../utils/icons.js";
|
|
6
|
-
import { renderMarkdown, extractHeadings } from "../utils/markdown.js";
|
|
6
|
+
import { renderCodeBlock, renderMarkdown, renderMarkdownInline, extractHeadings, } from "../utils/markdown.js";
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Frontmatter parsing
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
@@ -17,6 +17,7 @@ function parseFrontmatter(raw) {
|
|
|
17
17
|
return { meta, body: match[2] };
|
|
18
18
|
}
|
|
19
19
|
const FENCED_BLOCK_TOKEN = "@@SOURCEY_FENCED_BLOCK_";
|
|
20
|
+
const INLINE_CODE_TOKEN = "@@SOURCEY_INLINE_CODE_";
|
|
20
21
|
function protectFencedCodeBlocks(input) {
|
|
21
22
|
const blocks = [];
|
|
22
23
|
const output = [];
|
|
@@ -57,100 +58,417 @@ function protectFencedCodeBlocks(input) {
|
|
|
57
58
|
function restoreFencedCodeBlocks(input, blocks) {
|
|
58
59
|
return blocks.reduce((text, block, index) => text.replaceAll(`${FENCED_BLOCK_TOKEN}${index}@@`, block), input);
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
function protectInlineCodeSpans(input) {
|
|
62
|
+
const spans = [];
|
|
63
|
+
let output = "";
|
|
64
|
+
let i = 0;
|
|
65
|
+
while (i < input.length) {
|
|
66
|
+
if (input[i] !== "`") {
|
|
67
|
+
output += input[i];
|
|
68
|
+
i += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
let tickEnd = i + 1;
|
|
72
|
+
while (tickEnd < input.length && input[tickEnd] === "`")
|
|
73
|
+
tickEnd += 1;
|
|
74
|
+
const delimiter = input.slice(i, tickEnd);
|
|
75
|
+
const closeIndex = input.indexOf(delimiter, tickEnd);
|
|
76
|
+
if (closeIndex === -1) {
|
|
77
|
+
output += input.slice(i);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
const span = input.slice(i, closeIndex + delimiter.length);
|
|
81
|
+
const index = spans.push(span) - 1;
|
|
82
|
+
output += `${INLINE_CODE_TOKEN}${index}@@`;
|
|
83
|
+
i = closeIndex + delimiter.length;
|
|
84
|
+
}
|
|
85
|
+
return { text: output, spans };
|
|
86
|
+
}
|
|
87
|
+
function restoreInlineCodeSpans(input, spans) {
|
|
88
|
+
return spans.reduce((text, span, index) => text.replaceAll(`${INLINE_CODE_TOKEN}${index}@@`, span), input);
|
|
89
|
+
}
|
|
90
|
+
function skipWhitespace(input, index) {
|
|
91
|
+
let i = index;
|
|
92
|
+
while (i < input.length && /\s/.test(input[i]))
|
|
93
|
+
i += 1;
|
|
94
|
+
return i;
|
|
95
|
+
}
|
|
96
|
+
function parseQuotedAttrValue(input, index) {
|
|
97
|
+
const quote = input[index];
|
|
98
|
+
if (quote !== `"` && quote !== `'`)
|
|
99
|
+
return null;
|
|
100
|
+
let value = "";
|
|
101
|
+
let i = index + 1;
|
|
102
|
+
while (i < input.length) {
|
|
103
|
+
const ch = input[i];
|
|
104
|
+
if (ch === "\\") {
|
|
105
|
+
if (i + 1 < input.length) {
|
|
106
|
+
value += input[i + 1];
|
|
107
|
+
i += 2;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
i += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (ch === quote) {
|
|
114
|
+
return { value, nextIndex: i + 1 };
|
|
115
|
+
}
|
|
116
|
+
value += ch;
|
|
117
|
+
i += 1;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function parseBracedAttrValue(input, index) {
|
|
122
|
+
if (input[index] !== "{")
|
|
123
|
+
return null;
|
|
124
|
+
let depth = 1;
|
|
125
|
+
let i = index + 1;
|
|
126
|
+
while (i < input.length) {
|
|
127
|
+
const ch = input[i];
|
|
128
|
+
if (ch === `"` || ch === `'`) {
|
|
129
|
+
const quoted = parseQuotedAttrValue(input, i);
|
|
130
|
+
if (!quoted)
|
|
131
|
+
return null;
|
|
132
|
+
i = quoted.nextIndex;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === "{") {
|
|
136
|
+
depth += 1;
|
|
137
|
+
}
|
|
138
|
+
else if (ch === "}") {
|
|
139
|
+
depth -= 1;
|
|
140
|
+
if (depth === 0) {
|
|
141
|
+
return {
|
|
142
|
+
value: input.slice(index + 1, i).trim(),
|
|
143
|
+
nextIndex: i + 1,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
i += 1;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function parseKeyValueAttrs(raw, options) {
|
|
152
|
+
const attrs = {};
|
|
153
|
+
let i = 0;
|
|
154
|
+
while (i < raw.length) {
|
|
155
|
+
i = skipWhitespace(raw, i);
|
|
156
|
+
if (i >= raw.length)
|
|
157
|
+
break;
|
|
158
|
+
const keyMatch = raw.slice(i).match(/^([A-Za-z_][A-Za-z0-9_-]*)/);
|
|
159
|
+
if (!keyMatch)
|
|
160
|
+
break;
|
|
161
|
+
const key = keyMatch[1];
|
|
162
|
+
i += key.length;
|
|
163
|
+
i = skipWhitespace(raw, i);
|
|
164
|
+
if (raw[i] !== "=")
|
|
165
|
+
break;
|
|
166
|
+
i += 1;
|
|
167
|
+
i = skipWhitespace(raw, i);
|
|
168
|
+
const parsedValue = raw[i] === "{" && options.allowBraces
|
|
169
|
+
? parseBracedAttrValue(raw, i)
|
|
170
|
+
: parseQuotedAttrValue(raw, i);
|
|
171
|
+
if (!parsedValue)
|
|
172
|
+
break;
|
|
173
|
+
attrs[key] = parsedValue.value;
|
|
174
|
+
i = parsedValue.nextIndex;
|
|
175
|
+
}
|
|
176
|
+
return attrs;
|
|
177
|
+
}
|
|
178
|
+
const SUPPORTED_COMPONENT_TAGS = new Set([
|
|
179
|
+
"Steps",
|
|
180
|
+
"Step",
|
|
181
|
+
"CardGroup",
|
|
182
|
+
"Card",
|
|
183
|
+
"AccordionGroup",
|
|
184
|
+
"Accordion",
|
|
185
|
+
"Tabs",
|
|
186
|
+
"Tab",
|
|
187
|
+
"CodeGroup",
|
|
188
|
+
"Note",
|
|
189
|
+
"Warning",
|
|
190
|
+
"Tip",
|
|
191
|
+
"Info",
|
|
192
|
+
"Video",
|
|
193
|
+
"Iframe",
|
|
194
|
+
]);
|
|
195
|
+
const SELF_CLOSING_COMPONENT_TAGS = new Set(["Video", "Iframe"]);
|
|
196
|
+
function escapeDirectiveAttr(value) {
|
|
197
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
198
|
+
}
|
|
199
|
+
function buildDirectiveAttrList(entries) {
|
|
200
|
+
const attrs = entries
|
|
201
|
+
.filter(([, value]) => value !== undefined && value !== "")
|
|
202
|
+
.map(([key, value]) => `${key}="${escapeDirectiveAttr(value)}"`);
|
|
203
|
+
return attrs.length > 0 ? `{${attrs.join(" ")}}` : "";
|
|
204
|
+
}
|
|
205
|
+
function indentBlock(text, prefix) {
|
|
206
|
+
return text
|
|
207
|
+
.split("\n")
|
|
208
|
+
.map((line) => `${prefix}${line}`)
|
|
209
|
+
.join("\n");
|
|
210
|
+
}
|
|
211
|
+
function isWhitespaceOnlyText(node) {
|
|
212
|
+
return node.kind === "text" && node.value.trim() === "";
|
|
213
|
+
}
|
|
214
|
+
function findComponentTagEnd(input, start) {
|
|
215
|
+
let quote = null;
|
|
216
|
+
let braceDepth = 0;
|
|
217
|
+
let i = start + 1;
|
|
218
|
+
while (i < input.length) {
|
|
219
|
+
const ch = input[i];
|
|
220
|
+
if (quote) {
|
|
221
|
+
if (ch === "\\") {
|
|
222
|
+
i += 2;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (ch === quote)
|
|
226
|
+
quote = null;
|
|
227
|
+
i += 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (ch === `"` || ch === `'`) {
|
|
231
|
+
quote = ch;
|
|
232
|
+
i += 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (ch === "{") {
|
|
236
|
+
braceDepth += 1;
|
|
237
|
+
i += 1;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (ch === "}" && braceDepth > 0) {
|
|
241
|
+
braceDepth -= 1;
|
|
242
|
+
i += 1;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (ch === ">" && braceDepth === 0) {
|
|
246
|
+
return i + 1;
|
|
247
|
+
}
|
|
248
|
+
i += 1;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
function tryParseComponentTag(input, start) {
|
|
253
|
+
if (input[start] !== "<")
|
|
254
|
+
return null;
|
|
255
|
+
const end = findComponentTagEnd(input, start);
|
|
256
|
+
if (end === null)
|
|
257
|
+
return null;
|
|
258
|
+
const raw = input.slice(start, end);
|
|
259
|
+
const inner = input.slice(start + 1, end - 1).trim();
|
|
260
|
+
if (!inner)
|
|
261
|
+
return null;
|
|
262
|
+
if (inner.startsWith("/")) {
|
|
263
|
+
const closeName = inner.slice(1).trim();
|
|
264
|
+
if (!SUPPORTED_COMPONENT_TAGS.has(closeName))
|
|
265
|
+
return null;
|
|
266
|
+
return {
|
|
267
|
+
name: closeName,
|
|
268
|
+
kind: "close",
|
|
269
|
+
attrs: {},
|
|
270
|
+
raw,
|
|
271
|
+
start,
|
|
272
|
+
end,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const selfClosing = inner.endsWith("/");
|
|
276
|
+
const content = selfClosing ? inner.slice(0, -1).trimEnd() : inner;
|
|
277
|
+
const nameMatch = content.match(/^([A-Za-z][A-Za-z0-9]*)/);
|
|
278
|
+
if (!nameMatch || !SUPPORTED_COMPONENT_TAGS.has(nameMatch[1])) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const name = nameMatch[1];
|
|
282
|
+
const attrSource = content.slice(name.length);
|
|
283
|
+
return {
|
|
284
|
+
name,
|
|
285
|
+
kind: selfClosing || SELF_CLOSING_COMPONENT_TAGS.has(name) ? "self" : "open",
|
|
286
|
+
attrs: parseKeyValueAttrs(attrSource, { allowBraces: true }),
|
|
287
|
+
raw,
|
|
288
|
+
start,
|
|
289
|
+
end,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function parseComponentNodes(input, startIndex = 0, untilTag) {
|
|
293
|
+
const nodes = [];
|
|
294
|
+
let cursor = startIndex;
|
|
295
|
+
while (cursor < input.length) {
|
|
296
|
+
const nextTag = input.indexOf("<", cursor);
|
|
297
|
+
if (nextTag === -1) {
|
|
298
|
+
if (cursor < input.length) {
|
|
299
|
+
nodes.push({ kind: "text", value: input.slice(cursor) });
|
|
300
|
+
}
|
|
301
|
+
return { nodes, nextIndex: input.length };
|
|
302
|
+
}
|
|
303
|
+
if (nextTag > cursor) {
|
|
304
|
+
nodes.push({ kind: "text", value: input.slice(cursor, nextTag) });
|
|
305
|
+
}
|
|
306
|
+
const tag = tryParseComponentTag(input, nextTag);
|
|
307
|
+
if (!tag) {
|
|
308
|
+
nodes.push({ kind: "text", value: "<" });
|
|
309
|
+
cursor = nextTag + 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (tag.kind === "close") {
|
|
313
|
+
if (untilTag && tag.name === untilTag) {
|
|
314
|
+
return {
|
|
315
|
+
nodes,
|
|
316
|
+
nextIndex: tag.end,
|
|
317
|
+
matchedClose: tag,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
nodes.push({ kind: "text", value: tag.raw });
|
|
321
|
+
cursor = tag.end;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (tag.kind === "self") {
|
|
325
|
+
nodes.push({
|
|
326
|
+
kind: "component",
|
|
327
|
+
name: tag.name,
|
|
328
|
+
attrs: tag.attrs,
|
|
329
|
+
children: [],
|
|
330
|
+
original: tag.raw,
|
|
331
|
+
});
|
|
332
|
+
cursor = tag.end;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const childResult = parseComponentNodes(input, tag.end, tag.name);
|
|
336
|
+
if (!childResult.matchedClose) {
|
|
337
|
+
nodes.push({
|
|
338
|
+
kind: "text",
|
|
339
|
+
value: input.slice(nextTag, childResult.nextIndex),
|
|
340
|
+
});
|
|
341
|
+
cursor = childResult.nextIndex;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
nodes.push({
|
|
345
|
+
kind: "component",
|
|
346
|
+
name: tag.name,
|
|
347
|
+
attrs: tag.attrs,
|
|
348
|
+
children: childResult.nodes,
|
|
349
|
+
original: input.slice(nextTag, childResult.nextIndex),
|
|
350
|
+
});
|
|
351
|
+
cursor = childResult.nextIndex;
|
|
352
|
+
}
|
|
353
|
+
return { nodes, nextIndex: cursor };
|
|
354
|
+
}
|
|
355
|
+
function renderParsedComponentNodes(nodes) {
|
|
356
|
+
return nodes.map(renderParsedComponentNode).join("");
|
|
357
|
+
}
|
|
358
|
+
function collectChildComponents(children, expected) {
|
|
359
|
+
const matches = [];
|
|
360
|
+
for (const child of children) {
|
|
361
|
+
if (isWhitespaceOnlyText(child))
|
|
362
|
+
continue;
|
|
363
|
+
if (child.kind !== "component" || child.name !== expected) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
matches.push(child);
|
|
367
|
+
}
|
|
368
|
+
return matches;
|
|
369
|
+
}
|
|
370
|
+
function renderStandaloneAccordion(node) {
|
|
371
|
+
const title = node.attrs.title || "";
|
|
372
|
+
const body = renderParsedComponentNodes(node.children).trim();
|
|
373
|
+
return `:::accordion${buildDirectiveAttrList([["title", title]])}\n${body}\n:::`;
|
|
374
|
+
}
|
|
375
|
+
function renderParsedComponentElement(node) {
|
|
376
|
+
if (node.name === "Steps") {
|
|
377
|
+
const steps = collectChildComponents(node.children, "Step");
|
|
378
|
+
if (!steps)
|
|
379
|
+
return null;
|
|
380
|
+
const body = steps
|
|
381
|
+
.map((step, index) => {
|
|
382
|
+
const title = step.attrs.title || "";
|
|
383
|
+
const content = renderParsedComponentNodes(step.children).trim();
|
|
384
|
+
return `${index + 1}. ${title}${content ? `\n${indentBlock(content, " ")}` : ""}`;
|
|
385
|
+
})
|
|
386
|
+
.join("\n");
|
|
387
|
+
return `:::steps\n${body}\n:::`;
|
|
388
|
+
}
|
|
389
|
+
if (node.name === "CardGroup") {
|
|
390
|
+
const cards = collectChildComponents(node.children, "Card");
|
|
391
|
+
if (!cards)
|
|
392
|
+
return null;
|
|
393
|
+
const body = cards
|
|
394
|
+
.map((card) => {
|
|
395
|
+
const attrs = buildDirectiveAttrList([
|
|
396
|
+
["title", card.attrs.title || ""],
|
|
397
|
+
["icon", card.attrs.icon || ""],
|
|
398
|
+
["href", card.attrs.href],
|
|
399
|
+
]);
|
|
400
|
+
const content = renderParsedComponentNodes(card.children).trim();
|
|
401
|
+
return `::card${attrs}\n${content}\n::`;
|
|
402
|
+
})
|
|
403
|
+
.join("\n");
|
|
404
|
+
return `:::card-group${buildDirectiveAttrList([["cols", node.attrs.cols || "2"]])}\n${body}\n:::`;
|
|
405
|
+
}
|
|
406
|
+
if (node.name === "AccordionGroup") {
|
|
407
|
+
const accordions = collectChildComponents(node.children, "Accordion");
|
|
408
|
+
if (!accordions)
|
|
409
|
+
return null;
|
|
410
|
+
return accordions.map(renderStandaloneAccordion).join("\n\n");
|
|
411
|
+
}
|
|
412
|
+
if (node.name === "Accordion") {
|
|
413
|
+
return renderStandaloneAccordion(node);
|
|
414
|
+
}
|
|
415
|
+
if (node.name === "Tabs") {
|
|
416
|
+
const tabs = collectChildComponents(node.children, "Tab");
|
|
417
|
+
if (!tabs)
|
|
418
|
+
return null;
|
|
419
|
+
const body = tabs
|
|
420
|
+
.map((tab) => {
|
|
421
|
+
const content = renderParsedComponentNodes(tab.children).trim();
|
|
422
|
+
return `::tab${buildDirectiveAttrList([["title", tab.attrs.title || ""]])}\n${content}\n::`;
|
|
423
|
+
})
|
|
424
|
+
.join("\n");
|
|
425
|
+
return `:::tabs\n${body}\n:::`;
|
|
426
|
+
}
|
|
427
|
+
if (node.name === "CodeGroup") {
|
|
428
|
+
const content = renderParsedComponentNodes(node.children).trim();
|
|
429
|
+
return `:::code-group\n${content}\n:::`;
|
|
430
|
+
}
|
|
431
|
+
if (node.name === "Note" || node.name === "Warning" || node.name === "Tip" || node.name === "Info") {
|
|
432
|
+
const directive = node.name.toLowerCase();
|
|
433
|
+
const title = node.attrs.title ? ` ${node.attrs.title}` : "";
|
|
434
|
+
const content = renderParsedComponentNodes(node.children).trim();
|
|
435
|
+
return `:::${directive}${title}\n${content}\n:::`;
|
|
436
|
+
}
|
|
437
|
+
if (node.name === "Video") {
|
|
438
|
+
const titleAttr = node.attrs.title ? `{title="${escapeDirectiveAttr(node.attrs.title)}"}` : "";
|
|
439
|
+
return `::video[${node.attrs.src || ""}]${titleAttr}`;
|
|
440
|
+
}
|
|
441
|
+
if (node.name === "Iframe") {
|
|
442
|
+
const attrString = buildDirectiveAttrList([
|
|
443
|
+
["title", node.attrs.title],
|
|
444
|
+
["height", node.attrs.height],
|
|
445
|
+
]);
|
|
446
|
+
return `::iframe[${node.attrs.src || ""}]${attrString}`;
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
function renderParsedComponentNode(node) {
|
|
451
|
+
if (node.kind === "text")
|
|
452
|
+
return node.value;
|
|
453
|
+
return renderParsedComponentElement(node) ?? node.original;
|
|
454
|
+
}
|
|
63
455
|
/**
|
|
64
456
|
* Convert JSX-style components to directive syntax so they go through
|
|
65
457
|
* a single rendering path in preprocessDirectives.
|
|
66
458
|
*/
|
|
67
459
|
function preprocessComponents(body) {
|
|
68
|
-
const { text, blocks } = protectFencedCodeBlocks(body);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
inner.replace(/\s*<Step\s+title="([^"]*)">\s*([\s\S]*?)\s*<\/Step>/g, (_sm, title, content) => {
|
|
74
|
-
steps.push({ title, content: content.trim() });
|
|
75
|
-
return "";
|
|
76
|
-
});
|
|
77
|
-
const list = steps
|
|
78
|
-
.map((s, i) => `${i + 1}. ${s.title}\n ${s.content}`)
|
|
79
|
-
.join("\n");
|
|
80
|
-
return `:::steps\n${list}\n:::`;
|
|
81
|
-
});
|
|
82
|
-
// <CardGroup cols={N}> <Card ...> ... </Card> ... </CardGroup> → :::card-group
|
|
83
|
-
html = html.replace(/<CardGroup\s+cols=\{(\d+)\}>\s*([\s\S]*?)\s*<\/CardGroup>/g, (_m, cols, inner) => {
|
|
84
|
-
const cards = inner.replace(/\s*<Card\s+title="([^"]*)"\s+icon="([^"]*)"(?:\s+href="([^"]*)")?\s*>\s*([\s\S]*?)\s*<\/Card>/g, (_cm, title, icon, href, content) => {
|
|
85
|
-
const hrefAttr = href ? ` href="${href}"` : "";
|
|
86
|
-
return `\n::card{title="${title}" icon="${icon}"${hrefAttr}}\n${content.trim()}\n::`;
|
|
87
|
-
}).trim();
|
|
88
|
-
return `:::card-group{cols="${cols}"}\n${cards}\n:::`;
|
|
89
|
-
});
|
|
90
|
-
// <AccordionGroup> <Accordion ...> ... </AccordionGroup> → wrap in accordion-group
|
|
91
|
-
html = html.replace(/<AccordionGroup>\s*([\s\S]*?)\s*<\/AccordionGroup>/g, (_m, inner) => {
|
|
92
|
-
const items = inner.replace(/\s*<Accordion\s+title="([^"]*)">\s*([\s\S]*?)\s*<\/Accordion>/g, (_am, title, content) => {
|
|
93
|
-
return `\n:::accordion{title="${title}"}\n${content.trim()}\n:::`;
|
|
94
|
-
}).trim();
|
|
95
|
-
return `<div class="accordion-group not-prose">\n${items}\n</div>`;
|
|
96
|
-
});
|
|
97
|
-
// Standalone <Accordion> outside of group
|
|
98
|
-
html = html.replace(/<Accordion\s+title="([^"]*)">\s*([\s\S]*?)\s*<\/Accordion>/g, (_m, title, content) => {
|
|
99
|
-
return `:::accordion{title="${title}"}\n${content.trim()}\n:::`;
|
|
100
|
-
});
|
|
101
|
-
// <Tabs> <Tab title="...">content</Tab> ... </Tabs> → :::tabs with ::tab children
|
|
102
|
-
html = html.replace(/<Tabs>\s*([\s\S]*?)\s*<\/Tabs>/g, (_m, inner) => {
|
|
103
|
-
const tabs = [];
|
|
104
|
-
inner.replace(/\s*<Tab\s+title="([^"]*)">\s*([\s\S]*?)\s*<\/Tab>/g, (_tm, title, content) => {
|
|
105
|
-
tabs.push(`::tab{title="${title}"}\n${content.trim()}\n::`);
|
|
106
|
-
return "";
|
|
107
|
-
});
|
|
108
|
-
return `:::tabs\n${tabs.join("\n")}\n:::`;
|
|
109
|
-
});
|
|
110
|
-
// <CodeGroup> with titled fenced code blocks → :::code-group
|
|
111
|
-
html = html.replace(/<CodeGroup>\s*([\s\S]*?)\s*<\/CodeGroup>/g, (_m, inner) => `:::code-group\n${inner.trim()}\n:::`);
|
|
112
|
-
// <Note>, <Warning>, <Tip>, <Info> → :::callout directives
|
|
113
|
-
for (const type of ["note", "warning", "tip", "info"]) {
|
|
114
|
-
const tag = type.charAt(0).toUpperCase() + type.slice(1);
|
|
115
|
-
html = html.replace(new RegExp(`<${tag}(?:\\s+title="([^"]*)")?\\s*>\\s*([\\s\\S]*?)\\s*<\\/${tag}>`, "g"), (_m, title, content) => {
|
|
116
|
-
const titleSuffix = title ? ` ${title}` : "";
|
|
117
|
-
return `:::${type}${titleSuffix}\n${content.trim()}\n:::`;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
// <Video src="..." title="..." /> → ::video[url]{title="..."}
|
|
121
|
-
html = html.replace(/<Video\s+([^>]*?)\s*\/?\s*>/g, (_m, attrs) => {
|
|
122
|
-
const src = attrs.match(/src="([^"]*)"/)?.[1] ?? "";
|
|
123
|
-
const title = attrs.match(/title="([^"]*)"/)?.[1];
|
|
124
|
-
const titleAttr = title ? `{title="${title}"}` : "";
|
|
125
|
-
return `::video[${src}]${titleAttr}`;
|
|
126
|
-
});
|
|
127
|
-
// <Iframe src="..." title="..." height="..." /> → ::iframe[url]{attrs}
|
|
128
|
-
html = html.replace(/<Iframe\s+([^>]*?)\s*\/?\s*>/g, (_m, attrs) => {
|
|
129
|
-
const src = attrs.match(/src="([^"]*)"/)?.[1] ?? "";
|
|
130
|
-
const title = attrs.match(/title="([^"]*)"/)?.[1];
|
|
131
|
-
const height = attrs.match(/height="([^"]*)"/)?.[1];
|
|
132
|
-
const parts = [];
|
|
133
|
-
if (title)
|
|
134
|
-
parts.push(`title="${title}"`);
|
|
135
|
-
if (height)
|
|
136
|
-
parts.push(`height="${height}"`);
|
|
137
|
-
const attrStr = parts.length ? `{${parts.join(" ")}}` : "";
|
|
138
|
-
return `::iframe[${src}]${attrStr}`;
|
|
139
|
-
});
|
|
140
|
-
return restoreFencedCodeBlocks(html, blocks);
|
|
460
|
+
const { text: fencedText, blocks } = protectFencedCodeBlocks(body);
|
|
461
|
+
const { text, spans } = protectInlineCodeSpans(fencedText);
|
|
462
|
+
const parsed = parseComponentNodes(text);
|
|
463
|
+
const restoredInline = restoreInlineCodeSpans(renderParsedComponentNodes(parsed.nodes), spans);
|
|
464
|
+
return restoreFencedCodeBlocks(restoredInline, blocks);
|
|
141
465
|
}
|
|
142
466
|
// ---------------------------------------------------------------------------
|
|
143
467
|
// Directive preprocessor: transforms :::directive blocks into HTML
|
|
144
468
|
// ---------------------------------------------------------------------------
|
|
145
469
|
/** Parse {key="value" key2="value2"} attribute strings. */
|
|
146
470
|
function parseAttrs(raw) {
|
|
147
|
-
|
|
148
|
-
if (!raw)
|
|
149
|
-
return attrs;
|
|
150
|
-
for (const m of raw.matchAll(/(\w+)="([^"]*)"/g)) {
|
|
151
|
-
attrs[m[1]] = m[2];
|
|
152
|
-
}
|
|
153
|
-
return attrs;
|
|
471
|
+
return parseKeyValueAttrs(raw, { allowBraces: false });
|
|
154
472
|
}
|
|
155
473
|
/** Deterministic tab-group ID; reset per page. */
|
|
156
474
|
let directiveCounter = 0;
|
|
@@ -161,6 +479,31 @@ function nextId(prefix) {
|
|
|
161
479
|
function resetDirectiveCounter() {
|
|
162
480
|
directiveCounter = 0;
|
|
163
481
|
}
|
|
482
|
+
function escapeRegExp(value) {
|
|
483
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
484
|
+
}
|
|
485
|
+
function escapeHtmlAttr(value) {
|
|
486
|
+
return value
|
|
487
|
+
.replace(/&/g, "&")
|
|
488
|
+
.replace(/"/g, """)
|
|
489
|
+
.replace(/</g, "<");
|
|
490
|
+
}
|
|
491
|
+
function stripDirectiveIndent(line) {
|
|
492
|
+
return line.replace(/^ {0,3}/, "");
|
|
493
|
+
}
|
|
494
|
+
function isFenceStart(line) {
|
|
495
|
+
const match = stripDirectiveIndent(line).match(/^(`{3,}|~{3,})(.*)$/);
|
|
496
|
+
if (!match)
|
|
497
|
+
return null;
|
|
498
|
+
return { char: match[1][0], length: match[1].length };
|
|
499
|
+
}
|
|
500
|
+
function closesFence(line, fence) {
|
|
501
|
+
const match = stripDirectiveIndent(line).match(/^(`{3,}|~{3,})(.*)$/);
|
|
502
|
+
return !!(match &&
|
|
503
|
+
match[1][0] === fence.char &&
|
|
504
|
+
match[1].length >= fence.length &&
|
|
505
|
+
!match[2].trim());
|
|
506
|
+
}
|
|
164
507
|
/** Build tabbed UI HTML (shared by :::tabs and :::code-group). */
|
|
165
508
|
function buildTabbedHtml(tabs, id, extraClass) {
|
|
166
509
|
const cls = extraClass
|
|
@@ -187,146 +530,316 @@ ${panels}
|
|
|
187
530
|
*/
|
|
188
531
|
function splitChildren(content, marker) {
|
|
189
532
|
const children = [];
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
533
|
+
const lines = content.split("\n");
|
|
534
|
+
const startRe = new RegExp(`^::${escapeRegExp(marker)}(?:\\{([^}]*)\\})?\\s*$`);
|
|
535
|
+
let i = 0;
|
|
536
|
+
while (i < lines.length) {
|
|
537
|
+
const start = stripDirectiveIndent(lines[i]).trimEnd().match(startRe);
|
|
538
|
+
if (!start) {
|
|
539
|
+
i += 1;
|
|
193
540
|
continue;
|
|
194
|
-
const m = part.match(/^\{([^}]*)\}\s*\n?([\s\S]*)/);
|
|
195
|
-
if (m) {
|
|
196
|
-
// Strip trailing :: child-close marker
|
|
197
|
-
const body = m[2].trim().replace(/\n?::$/m, "").trim();
|
|
198
|
-
children.push({ attrs: parseAttrs(m[1]), body });
|
|
199
541
|
}
|
|
542
|
+
let fence = null;
|
|
543
|
+
let depth = 1;
|
|
544
|
+
let j = i + 1;
|
|
545
|
+
while (j < lines.length) {
|
|
546
|
+
const line = lines[j];
|
|
547
|
+
if (fence) {
|
|
548
|
+
if (closesFence(line, fence))
|
|
549
|
+
fence = null;
|
|
550
|
+
j += 1;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const nextFence = isFenceStart(line);
|
|
554
|
+
if (nextFence) {
|
|
555
|
+
fence = nextFence;
|
|
556
|
+
j += 1;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const stripped = stripDirectiveIndent(line).trimEnd();
|
|
560
|
+
if (startRe.test(stripped)) {
|
|
561
|
+
depth += 1;
|
|
562
|
+
}
|
|
563
|
+
else if (/^::\s*$/.test(stripped)) {
|
|
564
|
+
depth -= 1;
|
|
565
|
+
if (depth === 0)
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
j += 1;
|
|
569
|
+
}
|
|
570
|
+
if (depth !== 0) {
|
|
571
|
+
i += 1;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
children.push({
|
|
575
|
+
attrs: parseAttrs(start[1] ?? ""),
|
|
576
|
+
body: lines.slice(i + 1, j).join("\n").trim(),
|
|
577
|
+
});
|
|
578
|
+
i = j + 1;
|
|
200
579
|
}
|
|
201
580
|
return children;
|
|
202
581
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
582
|
+
function parseTitledCodeBlocks(content) {
|
|
583
|
+
const blocks = [];
|
|
584
|
+
const lines = content.split("\n");
|
|
585
|
+
let i = 0;
|
|
586
|
+
while (i < lines.length) {
|
|
587
|
+
const line = stripDirectiveIndent(lines[i]).trimEnd();
|
|
588
|
+
const open = line.match(/^(`{3,}|~{3,})(\S+)?(?:\s+title="([^"]*)")?\s*$/);
|
|
589
|
+
if (!open) {
|
|
590
|
+
i += 1;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const fence = { char: open[1][0], length: open[1].length };
|
|
594
|
+
let j = i + 1;
|
|
595
|
+
while (j < lines.length && !closesFence(lines[j], fence))
|
|
596
|
+
j += 1;
|
|
597
|
+
if (j >= lines.length)
|
|
598
|
+
break;
|
|
599
|
+
blocks.push({
|
|
600
|
+
title: open[3] ?? "",
|
|
601
|
+
lang: open[2] ?? "",
|
|
602
|
+
body: lines.slice(i + 1, j).join("\n"),
|
|
603
|
+
});
|
|
604
|
+
i = j + 1;
|
|
605
|
+
}
|
|
606
|
+
return blocks;
|
|
607
|
+
}
|
|
608
|
+
const SUPPORTED_DIRECTIVES = new Set([
|
|
609
|
+
"note",
|
|
610
|
+
"warning",
|
|
611
|
+
"tip",
|
|
612
|
+
"info",
|
|
613
|
+
"steps",
|
|
614
|
+
"tabs",
|
|
615
|
+
"code-group",
|
|
616
|
+
"card-group",
|
|
617
|
+
"accordion",
|
|
618
|
+
]);
|
|
619
|
+
function matchDirectiveStart(line) {
|
|
620
|
+
const match = stripDirectiveIndent(line).trimEnd().match(/^:::(\w[\w-]*)(.*)$/);
|
|
621
|
+
if (!match || !SUPPORTED_DIRECTIVES.has(match[1]))
|
|
622
|
+
return null;
|
|
623
|
+
return {
|
|
624
|
+
type: match[1],
|
|
625
|
+
meta: match[2].trim(),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function isAnyDirectiveStart(line) {
|
|
629
|
+
return /^:::\w[\w-]*(?:\{[^}]*\}|.*)?$/.test(stripDirectiveIndent(line).trimEnd());
|
|
630
|
+
}
|
|
631
|
+
function renderDirectiveMarkdown(content) {
|
|
632
|
+
const trimmed = content.trim();
|
|
633
|
+
if (!trimmed)
|
|
634
|
+
return "";
|
|
635
|
+
return renderMarkdown(preprocessDirectives(preprocessComponents(trimmed))).trim();
|
|
636
|
+
}
|
|
637
|
+
function collectDirectiveBlock(lines, startIndex) {
|
|
638
|
+
let fence = null;
|
|
639
|
+
let depth = 1;
|
|
640
|
+
let i = startIndex + 1;
|
|
641
|
+
while (i < lines.length) {
|
|
642
|
+
const line = lines[i];
|
|
643
|
+
if (fence) {
|
|
644
|
+
if (closesFence(line, fence))
|
|
645
|
+
fence = null;
|
|
646
|
+
i += 1;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const nextFence = isFenceStart(line);
|
|
650
|
+
if (nextFence) {
|
|
651
|
+
fence = nextFence;
|
|
652
|
+
i += 1;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const stripped = stripDirectiveIndent(line).trimEnd();
|
|
656
|
+
if (isAnyDirectiveStart(stripped)) {
|
|
657
|
+
depth += 1;
|
|
658
|
+
}
|
|
659
|
+
else if (/^:::\s*$/.test(stripped)) {
|
|
660
|
+
depth -= 1;
|
|
661
|
+
if (depth === 0) {
|
|
662
|
+
return {
|
|
663
|
+
content: lines.slice(startIndex + 1, i).join("\n"),
|
|
664
|
+
nextLine: i + 1,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
i += 1;
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
function renderDirectiveBlock(type, meta, content) {
|
|
673
|
+
if (type === "note" || type === "warning" || type === "tip" || type === "info") {
|
|
674
|
+
const label = renderMarkdownInline(meta.trim() || type.charAt(0).toUpperCase() + type.slice(1)).trim();
|
|
675
|
+
const body = renderDirectiveMarkdown(content);
|
|
218
676
|
return `\n\n<div class="callout callout-${type} not-prose">
|
|
219
677
|
<div class="callout-title">${label}</div>
|
|
220
|
-
${body ? `<div class="callout-content">\n
|
|
221
|
-
</div>\n
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
html = html.replace(/^:::steps\s*\n([\s\S]*?)^:::\s*$/gm, (_m, content) => {
|
|
678
|
+
${body ? `<div class="callout-content">\n${body}\n</div>` : ""}
|
|
679
|
+
</div>\n`;
|
|
680
|
+
}
|
|
681
|
+
if (type === "steps") {
|
|
225
682
|
const lines = content.trim().split("\n");
|
|
226
683
|
const steps = [];
|
|
227
|
-
let
|
|
684
|
+
let fence = null;
|
|
228
685
|
for (const line of lines) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
else if (fm[1][0] === fenceMarker[0] &&
|
|
239
|
-
fm[1].length >= fenceMarker.length &&
|
|
240
|
-
!fm[2].trim()) {
|
|
241
|
-
fenceMarker = "";
|
|
686
|
+
if (fence) {
|
|
687
|
+
if (closesFence(line, fence))
|
|
688
|
+
fence = null;
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
const nextFence = isFenceStart(line);
|
|
692
|
+
if (nextFence) {
|
|
693
|
+
fence = nextFence;
|
|
242
694
|
}
|
|
243
695
|
}
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
696
|
+
const stripped = line.replace(/^ {1,3}/, "");
|
|
697
|
+
const stepMatch = !fence && line.match(/^\d+\.\s+(.+)/);
|
|
698
|
+
if (stepMatch) {
|
|
699
|
+
steps.push({ title: stepMatch[1], body: [] });
|
|
247
700
|
}
|
|
248
701
|
else if (steps.length > 0) {
|
|
249
|
-
// Strip up to 3 leading spaces (markdown list indent)
|
|
250
702
|
steps[steps.length - 1].body.push(stripped);
|
|
251
703
|
}
|
|
252
704
|
}
|
|
705
|
+
if (steps.length === 0)
|
|
706
|
+
return renderDirectiveMarkdown(content);
|
|
253
707
|
const items = steps
|
|
254
|
-
.map((
|
|
255
|
-
|
|
708
|
+
.map((step, index) => {
|
|
709
|
+
const title = renderMarkdownInline(step.title).trim();
|
|
710
|
+
const body = renderDirectiveMarkdown(step.body.join("\n").trim());
|
|
711
|
+
return `<div role="listitem" class="step-item">
|
|
712
|
+
<div class="step-number">${index + 1}</div>
|
|
256
713
|
<div class="step-body">
|
|
257
|
-
<p class="step-title">${
|
|
714
|
+
<p class="step-title">${title}</p>
|
|
258
715
|
<div class="step-content">
|
|
259
|
-
|
|
260
|
-
${s.body.join("\n").trim()}
|
|
261
|
-
|
|
716
|
+
${body}
|
|
262
717
|
</div>
|
|
263
718
|
</div>
|
|
264
|
-
</div
|
|
719
|
+
</div>`;
|
|
720
|
+
})
|
|
265
721
|
.join("\n");
|
|
266
|
-
return `\n\n<div role="list" class="steps not-prose">\n${items}\n</div>\n
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
722
|
+
return `\n\n<div role="list" class="steps not-prose">\n${items}\n</div>\n`;
|
|
723
|
+
}
|
|
724
|
+
if (type === "tabs") {
|
|
725
|
+
const tabs = splitChildren(content, "tab").map((child) => ({
|
|
726
|
+
title: renderMarkdownInline(child.attrs.title || "").trim(),
|
|
727
|
+
body: renderDirectiveMarkdown(child.body),
|
|
728
|
+
}));
|
|
729
|
+
if (tabs.length === 0)
|
|
730
|
+
return renderDirectiveMarkdown(content);
|
|
731
|
+
return `\n\n${buildTabbedHtml(tabs, nextId("tabs"))}\n`;
|
|
732
|
+
}
|
|
733
|
+
if (type === "code-group") {
|
|
734
|
+
const codeBlocks = parseTitledCodeBlocks(content).map((block) => ({
|
|
735
|
+
title: renderMarkdownInline(block.title).trim(),
|
|
736
|
+
body: renderCodeBlock(block.body, block.lang),
|
|
273
737
|
}));
|
|
274
|
-
return `\n\n${buildTabbedHtml(tabs, nextId("tabs"))}\n\n`;
|
|
275
|
-
});
|
|
276
|
-
// Code Group: :::code-group with titled fenced code blocks
|
|
277
|
-
html = html.replace(/^:::code-group\s*\n([\s\S]*?)^:::\s*$/gm, (_m, content) => {
|
|
278
|
-
const restoredContent = restoreFencedCodeBlocks(content, blocks);
|
|
279
|
-
const codeBlocks = [];
|
|
280
|
-
const re = /```(\w+)\s+title="([^"]*)"\s*\n([\s\S]*?)```/g;
|
|
281
|
-
let match;
|
|
282
|
-
while ((match = re.exec(restoredContent)) !== null) {
|
|
283
|
-
codeBlocks.push({ title: match[2], body: `\`\`\`${match[1]}\n${match[3]}\`\`\`` });
|
|
284
|
-
}
|
|
285
738
|
if (codeBlocks.length === 0)
|
|
286
|
-
return
|
|
287
|
-
return `\n\n${buildTabbedHtml(codeBlocks, nextId("cg"), "directive-code-group")}\n
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const
|
|
739
|
+
return renderDirectiveMarkdown(content);
|
|
740
|
+
return `\n\n${buildTabbedHtml(codeBlocks, nextId("cg"), "directive-code-group")}\n`;
|
|
741
|
+
}
|
|
742
|
+
if (type === "card-group") {
|
|
743
|
+
const cols = parseAttrs(meta.match(/^\{([^}]*)\}$/)?.[1] ?? "").cols || "2";
|
|
744
|
+
const cards = splitChildren(content, "card").map((child) => {
|
|
745
|
+
const tag = child.attrs.href ? "a" : "div";
|
|
746
|
+
const href = child.attrs.href ? ` href="${escapeHtmlAttr(child.attrs.href)}"` : "";
|
|
747
|
+
const iconHtml = renderIcon(child.attrs.icon || "");
|
|
748
|
+
const title = renderMarkdownInline(child.attrs.title || "").trim();
|
|
749
|
+
const body = renderDirectiveMarkdown(child.body);
|
|
296
750
|
return `<${tag}${href} class="card-item">
|
|
297
751
|
<div class="card-item-inner">
|
|
298
752
|
${iconHtml}
|
|
299
|
-
<h3 class="card-item-title">${
|
|
753
|
+
<h3 class="card-item-title">${title}</h3>
|
|
300
754
|
<div class="card-item-content">
|
|
301
|
-
|
|
302
|
-
${c.body}
|
|
303
|
-
|
|
755
|
+
${body}
|
|
304
756
|
</div>
|
|
305
757
|
</div>
|
|
306
758
|
</${tag}>`;
|
|
307
759
|
});
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
760
|
+
if (cards.length === 0)
|
|
761
|
+
return renderDirectiveMarkdown(content);
|
|
762
|
+
return `\n\n<div class="card-group not-prose" data-cols="${escapeHtmlAttr(cols)}">\n${cards.join("\n")}\n</div>\n`;
|
|
763
|
+
}
|
|
764
|
+
const title = renderMarkdownInline(parseAttrs(meta.match(/^\{([^}]*)\}$/)?.[1] ?? "").title || "").trim();
|
|
765
|
+
const body = renderDirectiveMarkdown(content);
|
|
766
|
+
return `\n\n<details class="accordion-item">
|
|
314
767
|
<summary class="accordion-trigger">${title}</summary>
|
|
315
768
|
<div class="accordion-content">
|
|
316
|
-
|
|
317
|
-
${content.trim()}
|
|
318
|
-
|
|
769
|
+
${body}
|
|
319
770
|
</div>
|
|
320
|
-
</details>\n
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
771
|
+
</details>\n`;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Process :::directive blocks. Handles callouts, steps, tabs,
|
|
775
|
+
* code-group, card-group, and accordion.
|
|
776
|
+
*
|
|
777
|
+
* Matching strategy: process one block type at a time. Each regex
|
|
778
|
+
* matches a specific `:::type` opener so closing `:::` markers
|
|
779
|
+
* from other directive types cannot collide.
|
|
780
|
+
*/
|
|
781
|
+
function preprocessDirectives(body) {
|
|
782
|
+
const lines = body.split("\n");
|
|
783
|
+
const out = [];
|
|
784
|
+
let i = 0;
|
|
785
|
+
let fence = null;
|
|
786
|
+
while (i < lines.length) {
|
|
787
|
+
if (fence) {
|
|
788
|
+
out.push(lines[i]);
|
|
789
|
+
if (closesFence(lines[i], fence))
|
|
790
|
+
fence = null;
|
|
791
|
+
i += 1;
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const nextFence = isFenceStart(lines[i]);
|
|
795
|
+
if (nextFence) {
|
|
796
|
+
fence = nextFence;
|
|
797
|
+
out.push(lines[i]);
|
|
798
|
+
i += 1;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const start = matchDirectiveStart(lines[i]);
|
|
802
|
+
if (!start) {
|
|
803
|
+
out.push(lines[i]);
|
|
804
|
+
i += 1;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (start.type === "accordion") {
|
|
808
|
+
const accordions = [];
|
|
809
|
+
let nextLine = i;
|
|
810
|
+
while (true) {
|
|
811
|
+
const accordionStart = matchDirectiveStart(lines[nextLine]);
|
|
812
|
+
if (!accordionStart || accordionStart.type !== "accordion")
|
|
813
|
+
break;
|
|
814
|
+
const accordionBlock = collectDirectiveBlock(lines, nextLine);
|
|
815
|
+
if (!accordionBlock)
|
|
816
|
+
break;
|
|
817
|
+
accordions.push(renderDirectiveBlock("accordion", accordionStart.meta, accordionBlock.content));
|
|
818
|
+
nextLine = accordionBlock.nextLine;
|
|
819
|
+
let scan = nextLine;
|
|
820
|
+
while (scan < lines.length && /^\s*$/.test(lines[scan]))
|
|
821
|
+
scan += 1;
|
|
822
|
+
const nextAccordion = scan < lines.length ? matchDirectiveStart(lines[scan]) : null;
|
|
823
|
+
if (!nextAccordion || nextAccordion.type !== "accordion")
|
|
824
|
+
break;
|
|
825
|
+
nextLine = scan;
|
|
826
|
+
}
|
|
827
|
+
if (accordions.length > 1) {
|
|
828
|
+
out.push(`\n\n<div class="accordion-group not-prose">\n${accordions.join("\n")}\n</div>\n`);
|
|
829
|
+
i = nextLine;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const block = collectDirectiveBlock(lines, i);
|
|
834
|
+
if (!block) {
|
|
835
|
+
out.push(lines[i]);
|
|
836
|
+
i += 1;
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
out.push(renderDirectiveBlock(start.type, start.meta, block.content));
|
|
840
|
+
i = block.nextLine;
|
|
841
|
+
}
|
|
842
|
+
return out.join("\n");
|
|
330
843
|
}
|
|
331
844
|
// ---------------------------------------------------------------------------
|
|
332
845
|
// Public API
|
|
@@ -338,14 +851,24 @@ export async function loadMarkdownPage(filePath, slug) {
|
|
|
338
851
|
const raw = await readFile(filePath, "utf-8");
|
|
339
852
|
const { meta, body } = parseFrontmatter(raw);
|
|
340
853
|
resetDirectiveCounter();
|
|
341
|
-
const
|
|
342
|
-
const
|
|
854
|
+
const componentPreprocessed = preprocessComponents(body);
|
|
855
|
+
const preprocessed = preprocessDirectives(componentPreprocessed);
|
|
856
|
+
const headings = extractHeadings(componentPreprocessed);
|
|
343
857
|
const html = renderMarkdown(preprocessed);
|
|
344
|
-
const title = meta.title ?? extractFirstHeading(
|
|
858
|
+
const title = meta.title ?? extractFirstHeading(componentPreprocessed) ?? slug;
|
|
345
859
|
const description = meta.description ?? "";
|
|
346
860
|
// Strip leading h1 from rendered HTML when it duplicates the page title
|
|
347
861
|
const cleanHtml = html.replace(/^\s*<h1[^>]*>(.*?)<\/h1>\s*/i, (_m, inner) => inner.replace(/<[^>]+>/g, "").trim() === title ? "" : _m);
|
|
348
|
-
|
|
862
|
+
const sourcePath = relative(process.cwd(), filePath);
|
|
863
|
+
return {
|
|
864
|
+
title,
|
|
865
|
+
description,
|
|
866
|
+
slug,
|
|
867
|
+
html: cleanHtml,
|
|
868
|
+
headings,
|
|
869
|
+
sourcePath,
|
|
870
|
+
editPath: sourcePath,
|
|
871
|
+
};
|
|
349
872
|
}
|
|
350
873
|
/**
|
|
351
874
|
* Extract the first # heading from markdown as a fallback title.
|