sourcey 3.4.3 → 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 +10 -5
- package/dist/components/openapi/Introduction.js +1 -1
- package/dist/components/openapi/Security.d.ts.map +1 -1
- package/dist/components/openapi/Security.js +2 -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 +760 -177
- package/dist/dev-server.js +1 -0
- 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 +19 -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
|
// ---------------------------------------------------------------------------
|
|
@@ -16,99 +16,459 @@ function parseFrontmatter(raw) {
|
|
|
16
16
|
const meta = parseYaml(match[1]) ?? {};
|
|
17
17
|
return { meta, body: match[2] };
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const FENCED_BLOCK_TOKEN = "@@SOURCEY_FENCED_BLOCK_";
|
|
20
|
+
const INLINE_CODE_TOKEN = "@@SOURCEY_INLINE_CODE_";
|
|
21
|
+
function protectFencedCodeBlocks(input) {
|
|
22
|
+
const blocks = [];
|
|
23
|
+
const output = [];
|
|
24
|
+
const lines = input.split("\n");
|
|
25
|
+
let fence = null;
|
|
26
|
+
let buffer = [];
|
|
27
|
+
const pushProtectedBlock = () => {
|
|
28
|
+
const index = blocks.push(buffer.join("\n")) - 1;
|
|
29
|
+
output.push(`${FENCED_BLOCK_TOKEN}${index}@@`);
|
|
30
|
+
buffer = [];
|
|
31
|
+
};
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const stripped = line.replace(/^ {1,3}/, "");
|
|
34
|
+
const fenceMatch = stripped.match(/^(`{3,}|~{3,})(.*)$/);
|
|
35
|
+
if (!fence) {
|
|
36
|
+
if (!fenceMatch) {
|
|
37
|
+
output.push(line);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
fence = { char: fenceMatch[1][0], length: fenceMatch[1].length };
|
|
41
|
+
buffer.push(line);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
buffer.push(line);
|
|
45
|
+
if (fenceMatch &&
|
|
46
|
+
fenceMatch[1][0] === fence.char &&
|
|
47
|
+
fenceMatch[1].length >= fence.length &&
|
|
48
|
+
!fenceMatch[2].trim()) {
|
|
49
|
+
pushProtectedBlock();
|
|
50
|
+
fence = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (buffer.length > 0) {
|
|
54
|
+
output.push(...buffer);
|
|
55
|
+
}
|
|
56
|
+
return { text: output.join("\n"), blocks };
|
|
57
|
+
}
|
|
58
|
+
function restoreFencedCodeBlocks(input, blocks) {
|
|
59
|
+
return blocks.reduce((text, block, index) => text.replaceAll(`${FENCED_BLOCK_TOKEN}${index}@@`, block), input);
|
|
60
|
+
}
|
|
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
|
+
}
|
|
22
455
|
/**
|
|
23
456
|
* Convert JSX-style components to directive syntax so they go through
|
|
24
457
|
* a single rendering path in preprocessDirectives.
|
|
25
458
|
*/
|
|
26
459
|
function preprocessComponents(body) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
steps.push({ title, content: content.trim() });
|
|
33
|
-
return "";
|
|
34
|
-
});
|
|
35
|
-
const list = steps
|
|
36
|
-
.map((s, i) => `${i + 1}. ${s.title}\n ${s.content}`)
|
|
37
|
-
.join("\n");
|
|
38
|
-
return `:::steps\n${list}\n:::`;
|
|
39
|
-
});
|
|
40
|
-
// <CardGroup cols={N}> <Card ...> ... </Card> ... </CardGroup> → :::card-group
|
|
41
|
-
html = html.replace(/<CardGroup\s+cols=\{(\d+)\}>\s*([\s\S]*?)\s*<\/CardGroup>/g, (_m, cols, inner) => {
|
|
42
|
-
const cards = inner.replace(/\s*<Card\s+title="([^"]*)"\s+icon="([^"]*)"(?:\s+href="([^"]*)")?\s*>\s*([\s\S]*?)\s*<\/Card>/g, (_cm, title, icon, href, content) => {
|
|
43
|
-
const hrefAttr = href ? ` href="${href}"` : "";
|
|
44
|
-
return `\n::card{title="${title}" icon="${icon}"${hrefAttr}}\n${content.trim()}\n::`;
|
|
45
|
-
}).trim();
|
|
46
|
-
return `:::card-group{cols="${cols}"}\n${cards}\n:::`;
|
|
47
|
-
});
|
|
48
|
-
// <AccordionGroup> <Accordion ...> ... </AccordionGroup> → wrap in accordion-group
|
|
49
|
-
html = html.replace(/<AccordionGroup>\s*([\s\S]*?)\s*<\/AccordionGroup>/g, (_m, inner) => {
|
|
50
|
-
const items = inner.replace(/\s*<Accordion\s+title="([^"]*)">\s*([\s\S]*?)\s*<\/Accordion>/g, (_am, title, content) => {
|
|
51
|
-
return `\n:::accordion{title="${title}"}\n${content.trim()}\n:::`;
|
|
52
|
-
}).trim();
|
|
53
|
-
return `<div class="accordion-group not-prose">\n${items}\n</div>`;
|
|
54
|
-
});
|
|
55
|
-
// Standalone <Accordion> outside of group
|
|
56
|
-
html = html.replace(/<Accordion\s+title="([^"]*)">\s*([\s\S]*?)\s*<\/Accordion>/g, (_m, title, content) => {
|
|
57
|
-
return `:::accordion{title="${title}"}\n${content.trim()}\n:::`;
|
|
58
|
-
});
|
|
59
|
-
// <Tabs> <Tab title="...">content</Tab> ... </Tabs> → :::tabs with ::tab children
|
|
60
|
-
html = html.replace(/<Tabs>\s*([\s\S]*?)\s*<\/Tabs>/g, (_m, inner) => {
|
|
61
|
-
const tabs = [];
|
|
62
|
-
inner.replace(/\s*<Tab\s+title="([^"]*)">\s*([\s\S]*?)\s*<\/Tab>/g, (_tm, title, content) => {
|
|
63
|
-
tabs.push(`::tab{title="${title}"}\n${content.trim()}\n::`);
|
|
64
|
-
return "";
|
|
65
|
-
});
|
|
66
|
-
return `:::tabs\n${tabs.join("\n")}\n:::`;
|
|
67
|
-
});
|
|
68
|
-
// <CodeGroup> with titled fenced code blocks → :::code-group
|
|
69
|
-
html = html.replace(/<CodeGroup>\s*([\s\S]*?)\s*<\/CodeGroup>/g, (_m, inner) => `:::code-group\n${inner.trim()}\n:::`);
|
|
70
|
-
// <Note>, <Warning>, <Tip>, <Info> → :::callout directives
|
|
71
|
-
for (const type of ["note", "warning", "tip", "info"]) {
|
|
72
|
-
const tag = type.charAt(0).toUpperCase() + type.slice(1);
|
|
73
|
-
html = html.replace(new RegExp(`<${tag}(?:\\s+title="([^"]*)")?\\s*>\\s*([\\s\\S]*?)\\s*<\\/${tag}>`, "g"), (_m, title, content) => {
|
|
74
|
-
const titleSuffix = title ? ` ${title}` : "";
|
|
75
|
-
return `:::${type}${titleSuffix}\n${content.trim()}\n:::`;
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
// <Video src="..." title="..." /> → ::video[url]{title="..."}
|
|
79
|
-
html = html.replace(/<Video\s+([^>]*?)\s*\/?\s*>/g, (_m, attrs) => {
|
|
80
|
-
const src = attrs.match(/src="([^"]*)"/)?.[1] ?? "";
|
|
81
|
-
const title = attrs.match(/title="([^"]*)"/)?.[1];
|
|
82
|
-
const titleAttr = title ? `{title="${title}"}` : "";
|
|
83
|
-
return `::video[${src}]${titleAttr}`;
|
|
84
|
-
});
|
|
85
|
-
// <Iframe src="..." title="..." height="..." /> → ::iframe[url]{attrs}
|
|
86
|
-
html = html.replace(/<Iframe\s+([^>]*?)\s*\/?\s*>/g, (_m, attrs) => {
|
|
87
|
-
const src = attrs.match(/src="([^"]*)"/)?.[1] ?? "";
|
|
88
|
-
const title = attrs.match(/title="([^"]*)"/)?.[1];
|
|
89
|
-
const height = attrs.match(/height="([^"]*)"/)?.[1];
|
|
90
|
-
const parts = [];
|
|
91
|
-
if (title)
|
|
92
|
-
parts.push(`title="${title}"`);
|
|
93
|
-
if (height)
|
|
94
|
-
parts.push(`height="${height}"`);
|
|
95
|
-
const attrStr = parts.length ? `{${parts.join(" ")}}` : "";
|
|
96
|
-
return `::iframe[${src}]${attrStr}`;
|
|
97
|
-
});
|
|
98
|
-
return html;
|
|
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);
|
|
99
465
|
}
|
|
100
466
|
// ---------------------------------------------------------------------------
|
|
101
467
|
// Directive preprocessor: transforms :::directive blocks into HTML
|
|
102
468
|
// ---------------------------------------------------------------------------
|
|
103
469
|
/** Parse {key="value" key2="value2"} attribute strings. */
|
|
104
470
|
function parseAttrs(raw) {
|
|
105
|
-
|
|
106
|
-
if (!raw)
|
|
107
|
-
return attrs;
|
|
108
|
-
for (const m of raw.matchAll(/(\w+)="([^"]*)"/g)) {
|
|
109
|
-
attrs[m[1]] = m[2];
|
|
110
|
-
}
|
|
111
|
-
return attrs;
|
|
471
|
+
return parseKeyValueAttrs(raw, { allowBraces: false });
|
|
112
472
|
}
|
|
113
473
|
/** Deterministic tab-group ID; reset per page. */
|
|
114
474
|
let directiveCounter = 0;
|
|
@@ -119,6 +479,31 @@ function nextId(prefix) {
|
|
|
119
479
|
function resetDirectiveCounter() {
|
|
120
480
|
directiveCounter = 0;
|
|
121
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
|
+
}
|
|
122
507
|
/** Build tabbed UI HTML (shared by :::tabs and :::code-group). */
|
|
123
508
|
function buildTabbedHtml(tabs, id, extraClass) {
|
|
124
509
|
const cls = extraClass
|
|
@@ -145,128 +530,316 @@ ${panels}
|
|
|
145
530
|
*/
|
|
146
531
|
function splitChildren(content, marker) {
|
|
147
532
|
const children = [];
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
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;
|
|
540
|
+
continue;
|
|
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;
|
|
151
572
|
continue;
|
|
152
|
-
const m = part.match(/^\{([^}]*)\}\s*\n?([\s\S]*)/);
|
|
153
|
-
if (m) {
|
|
154
|
-
// Strip trailing :: child-close marker
|
|
155
|
-
const body = m[2].trim().replace(/\n?::$/m, "").trim();
|
|
156
|
-
children.push({ attrs: parseAttrs(m[1]), body });
|
|
157
573
|
}
|
|
574
|
+
children.push({
|
|
575
|
+
attrs: parseAttrs(start[1] ?? ""),
|
|
576
|
+
body: lines.slice(i + 1, j).join("\n").trim(),
|
|
577
|
+
});
|
|
578
|
+
i = j + 1;
|
|
158
579
|
}
|
|
159
580
|
return children;
|
|
160
581
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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);
|
|
175
676
|
return `\n\n<div class="callout callout-${type} not-prose">
|
|
176
677
|
<div class="callout-title">${label}</div>
|
|
177
|
-
${body ? `<div class="callout-content">\n
|
|
178
|
-
</div>\n
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
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") {
|
|
182
682
|
const lines = content.trim().split("\n");
|
|
183
683
|
const steps = [];
|
|
684
|
+
let fence = null;
|
|
184
685
|
for (const line of lines) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
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: [] });
|
|
188
700
|
}
|
|
189
701
|
else if (steps.length > 0) {
|
|
190
|
-
|
|
191
|
-
steps[steps.length - 1].body.push(line.replace(/^ {1,3}/, ""));
|
|
702
|
+
steps[steps.length - 1].body.push(stripped);
|
|
192
703
|
}
|
|
193
704
|
}
|
|
705
|
+
if (steps.length === 0)
|
|
706
|
+
return renderDirectiveMarkdown(content);
|
|
194
707
|
const items = steps
|
|
195
|
-
.map((
|
|
196
|
-
|
|
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>
|
|
197
713
|
<div class="step-body">
|
|
198
|
-
<p class="step-title">${
|
|
714
|
+
<p class="step-title">${title}</p>
|
|
199
715
|
<div class="step-content">
|
|
200
|
-
|
|
201
|
-
${s.body.join("\n").trim()}
|
|
202
|
-
|
|
716
|
+
${body}
|
|
203
717
|
</div>
|
|
204
718
|
</div>
|
|
205
|
-
</div
|
|
719
|
+
</div>`;
|
|
720
|
+
})
|
|
206
721
|
.join("\n");
|
|
207
|
-
return `\n\n<div role="list" class="steps not-prose">\n${items}\n</div>\n
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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),
|
|
214
737
|
}));
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return `\n\n${buildTabbedHtml(blocks, nextId("cg"), "directive-code-group")}\n\n`;
|
|
228
|
-
});
|
|
229
|
-
// Card Group: :::card-group{cols="N"} with ::card children
|
|
230
|
-
html = html.replace(/^:::card-group(?:\{([^}]*)\})?\s*\n([\s\S]*?)^:::\s*$/gm, (_m, attrsRaw, content) => {
|
|
231
|
-
const cols = parseAttrs(attrsRaw || "").cols || "2";
|
|
232
|
-
const cards = splitChildren(content, "card").map((c) => {
|
|
233
|
-
const tag = c.attrs.href ? "a" : "div";
|
|
234
|
-
const href = c.attrs.href ? ` href="${c.attrs.href}"` : "";
|
|
235
|
-
const iconHtml = renderIcon(c.attrs.icon || "");
|
|
738
|
+
if (codeBlocks.length === 0)
|
|
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);
|
|
236
750
|
return `<${tag}${href} class="card-item">
|
|
237
751
|
<div class="card-item-inner">
|
|
238
752
|
${iconHtml}
|
|
239
|
-
<h3 class="card-item-title">${
|
|
753
|
+
<h3 class="card-item-title">${title}</h3>
|
|
240
754
|
<div class="card-item-content">
|
|
241
|
-
|
|
242
|
-
${c.body}
|
|
243
|
-
|
|
755
|
+
${body}
|
|
244
756
|
</div>
|
|
245
757
|
</div>
|
|
246
758
|
</${tag}>`;
|
|
247
759
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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">
|
|
254
767
|
<summary class="accordion-trigger">${title}</summary>
|
|
255
768
|
<div class="accordion-content">
|
|
256
|
-
|
|
257
|
-
${content.trim()}
|
|
258
|
-
|
|
769
|
+
${body}
|
|
259
770
|
</div>
|
|
260
|
-
</details>\n
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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");
|
|
270
843
|
}
|
|
271
844
|
// ---------------------------------------------------------------------------
|
|
272
845
|
// Public API
|
|
@@ -278,14 +851,24 @@ export async function loadMarkdownPage(filePath, slug) {
|
|
|
278
851
|
const raw = await readFile(filePath, "utf-8");
|
|
279
852
|
const { meta, body } = parseFrontmatter(raw);
|
|
280
853
|
resetDirectiveCounter();
|
|
281
|
-
const
|
|
282
|
-
const
|
|
854
|
+
const componentPreprocessed = preprocessComponents(body);
|
|
855
|
+
const preprocessed = preprocessDirectives(componentPreprocessed);
|
|
856
|
+
const headings = extractHeadings(componentPreprocessed);
|
|
283
857
|
const html = renderMarkdown(preprocessed);
|
|
284
|
-
const title = meta.title ?? extractFirstHeading(
|
|
858
|
+
const title = meta.title ?? extractFirstHeading(componentPreprocessed) ?? slug;
|
|
285
859
|
const description = meta.description ?? "";
|
|
286
860
|
// Strip leading h1 from rendered HTML when it duplicates the page title
|
|
287
861
|
const cleanHtml = html.replace(/^\s*<h1[^>]*>(.*?)<\/h1>\s*/i, (_m, inner) => inner.replace(/<[^>]+>/g, "").trim() === title ? "" : _m);
|
|
288
|
-
|
|
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
|
+
};
|
|
289
872
|
}
|
|
290
873
|
/**
|
|
291
874
|
* Extract the first # heading from markdown as a fallback title.
|