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