@dogsbay/format-dogsbay-md 0.2.0-beta.0
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/dist/attributes.d.ts +33 -0
- package/dist/attributes.d.ts.map +1 -0
- package/dist/attributes.js +83 -0
- package/dist/attributes.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +129 -0
- package/dist/cli.js.map +1 -0
- package/dist/directives.d.ts +19 -0
- package/dist/directives.d.ts.map +1 -0
- package/dist/directives.js +76 -0
- package/dist/directives.js.map +1 -0
- package/dist/escape.d.ts +42 -0
- package/dist/escape.d.ts.map +1 -0
- package/dist/escape.js +79 -0
- package/dist/escape.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/inline.d.ts +9 -0
- package/dist/inline.d.ts.map +1 -0
- package/dist/inline.js +122 -0
- package/dist/inline.js.map +1 -0
- package/dist/nav-file.d.ts +38 -0
- package/dist/nav-file.d.ts.map +1 -0
- package/dist/nav-file.js +257 -0
- package/dist/nav-file.js.map +1 -0
- package/dist/nav.d.ts +34 -0
- package/dist/nav.d.ts.map +1 -0
- package/dist/nav.js +169 -0
- package/dist/nav.js.map +1 -0
- package/dist/parse-attrs.d.ts +24 -0
- package/dist/parse-attrs.d.ts.map +1 -0
- package/dist/parse-attrs.js +117 -0
- package/dist/parse-attrs.js.map +1 -0
- package/dist/parse.d.ts +18 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +1076 -0
- package/dist/parse.js.map +1 -0
- package/dist/plugin-block-leaf.d.ts +19 -0
- package/dist/plugin-block-leaf.d.ts.map +1 -0
- package/dist/plugin-block-leaf.js +81 -0
- package/dist/plugin-block-leaf.js.map +1 -0
- package/dist/plugin-containers.d.ts +11 -0
- package/dist/plugin-containers.d.ts.map +1 -0
- package/dist/plugin-containers.js +63 -0
- package/dist/plugin-containers.js.map +1 -0
- package/dist/plugin-inline-directives.d.ts +18 -0
- package/dist/plugin-inline-directives.d.ts.map +1 -0
- package/dist/plugin-inline-directives.js +121 -0
- package/dist/plugin-inline-directives.js.map +1 -0
- package/dist/serialize.d.ts +25 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +712 -0
- package/dist/serialize.js.map +1 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/yaml.d.ts +22 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +113 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +55 -0
package/dist/parse.js
ADDED
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dogsbay Markdown parser: Dogsbay MD string → TreeNode[]
|
|
3
|
+
*
|
|
4
|
+
* Builds on markdown-it + community plugins + custom rules for Dogsbay
|
|
5
|
+
* directives. Not aiming for byte-perfect round-trip — the parser produces
|
|
6
|
+
* a TreeNode tree that semantically reconstructs the original content,
|
|
7
|
+
* which round-trips via the serializer.
|
|
8
|
+
*/
|
|
9
|
+
import MarkdownIt from "markdown-it";
|
|
10
|
+
import mdAttrs from "markdown-it-attrs";
|
|
11
|
+
import mdDeflist from "markdown-it-deflist";
|
|
12
|
+
import mdFootnote from "markdown-it-footnote";
|
|
13
|
+
import matter from "gray-matter";
|
|
14
|
+
import { inlineDirectivesPlugin } from "./plugin-inline-directives.js";
|
|
15
|
+
import { containersPlugin } from "./plugin-containers.js";
|
|
16
|
+
import { blockLeafDirectivePlugin } from "./plugin-block-leaf.js";
|
|
17
|
+
import { attrsToProps } from "./parse-attrs.js";
|
|
18
|
+
import { treeToDogsbayMd } from "./serialize.js";
|
|
19
|
+
/**
|
|
20
|
+
* Parse Dogsbay Markdown into TreeNode[] plus extracted frontmatter.
|
|
21
|
+
*/
|
|
22
|
+
export function dogsbayMdToTree(source, _options) {
|
|
23
|
+
// Extract frontmatter via gray-matter first (more reliable than markdown-it-front-matter).
|
|
24
|
+
// Fall back to parsing source as-is if frontmatter is malformed.
|
|
25
|
+
let content = source;
|
|
26
|
+
let frontmatter = {};
|
|
27
|
+
try {
|
|
28
|
+
const result = matter(source);
|
|
29
|
+
content = result.content;
|
|
30
|
+
frontmatter = result.data;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Malformed frontmatter — treat whole source as content, no frontmatter
|
|
34
|
+
}
|
|
35
|
+
const md = createParser();
|
|
36
|
+
const tokens = md.parse(content, {});
|
|
37
|
+
const tree = tokensToTree(tokens);
|
|
38
|
+
return { tree, frontmatter };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse a single inline string to InlineNode[] (useful for testing).
|
|
42
|
+
*/
|
|
43
|
+
export function parseInline(source) {
|
|
44
|
+
const md = createParser();
|
|
45
|
+
const tokens = md.parseInline(source, {});
|
|
46
|
+
if (tokens.length === 0)
|
|
47
|
+
return [];
|
|
48
|
+
return tokensToInline(tokens[0].children ?? []);
|
|
49
|
+
}
|
|
50
|
+
function createParser() {
|
|
51
|
+
const md = new MarkdownIt({ html: true });
|
|
52
|
+
md.use(mdAttrs, {
|
|
53
|
+
leftDelimiter: "{",
|
|
54
|
+
rightDelimiter: "}",
|
|
55
|
+
allowedAttributes: [],
|
|
56
|
+
});
|
|
57
|
+
md.use(mdDeflist);
|
|
58
|
+
md.use(mdFootnote);
|
|
59
|
+
// Frontmatter extracted via gray-matter before source reaches markdown-it
|
|
60
|
+
md.use(inlineDirectivesPlugin);
|
|
61
|
+
md.use(blockLeafDirectivePlugin);
|
|
62
|
+
md.use(containersPlugin);
|
|
63
|
+
return md;
|
|
64
|
+
}
|
|
65
|
+
function tokensToTree(tokens) {
|
|
66
|
+
const nodes = [];
|
|
67
|
+
const ctx = { tokens, i: 0 };
|
|
68
|
+
while (ctx.i < ctx.tokens.length) {
|
|
69
|
+
const node = consumeBlock(ctx);
|
|
70
|
+
if (node) {
|
|
71
|
+
if (Array.isArray(node)) {
|
|
72
|
+
nodes.push(...node);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
nodes.push(node);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
ctx.i++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return nodes;
|
|
83
|
+
}
|
|
84
|
+
function consumeBlock(ctx) {
|
|
85
|
+
const tok = ctx.tokens[ctx.i];
|
|
86
|
+
if (!tok)
|
|
87
|
+
return null;
|
|
88
|
+
// Container open — custom directive
|
|
89
|
+
if (tok.type.startsWith("container_") && tok.type.endsWith("_open")) {
|
|
90
|
+
return consumeContainer(ctx);
|
|
91
|
+
}
|
|
92
|
+
// Standard block types
|
|
93
|
+
switch (tok.type) {
|
|
94
|
+
case "heading_open":
|
|
95
|
+
return consumeHeading(ctx);
|
|
96
|
+
case "paragraph_open":
|
|
97
|
+
return consumeParagraph(ctx);
|
|
98
|
+
case "bullet_list_open":
|
|
99
|
+
return consumeList(ctx, false);
|
|
100
|
+
case "ordered_list_open":
|
|
101
|
+
return consumeList(ctx, true);
|
|
102
|
+
case "blockquote_open":
|
|
103
|
+
return consumeBlockquote(ctx);
|
|
104
|
+
case "fence":
|
|
105
|
+
case "code_block":
|
|
106
|
+
return consumeCode(ctx);
|
|
107
|
+
case "hr":
|
|
108
|
+
ctx.i++;
|
|
109
|
+
return { type: "hr" };
|
|
110
|
+
case "html_block":
|
|
111
|
+
return consumeHtmlBlock(ctx);
|
|
112
|
+
case "table_open":
|
|
113
|
+
return consumeTable(ctx);
|
|
114
|
+
case "dl_open":
|
|
115
|
+
return consumeDeflist(ctx);
|
|
116
|
+
case "front_matter":
|
|
117
|
+
ctx.i++;
|
|
118
|
+
return null; // handled by gray-matter
|
|
119
|
+
case "dogsbay_block_leaf_directive":
|
|
120
|
+
return consumeBlockLeafDirective(ctx);
|
|
121
|
+
case "footnote_block_open":
|
|
122
|
+
return consumeFootnoteBlock(ctx);
|
|
123
|
+
default:
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Consume a markdown-it-footnote block:
|
|
129
|
+
*
|
|
130
|
+
* footnote_block_open
|
|
131
|
+
* footnote_open (meta.id, meta.label?)
|
|
132
|
+
* paragraph_open
|
|
133
|
+
* inline { content: "<def text>" }
|
|
134
|
+
* footnote_anchor* (one per back-reference)
|
|
135
|
+
* paragraph_close
|
|
136
|
+
* footnote_close
|
|
137
|
+
* ... more footnote_open/close pairs
|
|
138
|
+
* footnote_block_close
|
|
139
|
+
*
|
|
140
|
+
* Produces a `footnote-list` containing one `footnote-def` per
|
|
141
|
+
* footnote. The `footnote_anchor` positions are dropped — the
|
|
142
|
+
* renderer attaches a single backref via `props.backrefId` (jumping
|
|
143
|
+
* to the first reference). Multiple-backref display is deferred —
|
|
144
|
+
* see plans/footnote-support.md "Future work".
|
|
145
|
+
*/
|
|
146
|
+
function consumeFootnoteBlock(ctx) {
|
|
147
|
+
ctx.i++; // footnote_block_open
|
|
148
|
+
const defs = [];
|
|
149
|
+
while (ctx.i < ctx.tokens.length) {
|
|
150
|
+
const tok = ctx.tokens[ctx.i];
|
|
151
|
+
if (tok.type === "footnote_block_close") {
|
|
152
|
+
ctx.i++;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (tok.type === "footnote_open") {
|
|
156
|
+
defs.push(consumeFootnoteDef(ctx));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Should not happen for well-formed input, but skip safely.
|
|
160
|
+
ctx.i++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { type: "footnote-list", children: defs };
|
|
164
|
+
}
|
|
165
|
+
function consumeFootnoteDef(ctx) {
|
|
166
|
+
const open = ctx.tokens[ctx.i];
|
|
167
|
+
const meta = (open.meta ?? {});
|
|
168
|
+
// Named footnote keeps the writer's label; inline-sugar (`^[..]`)
|
|
169
|
+
// gets a 1-based numeric label so renderers display [1], [2], …
|
|
170
|
+
// even when no explicit label was authored.
|
|
171
|
+
const label = meta.label ?? String((meta.id ?? 0) + 1);
|
|
172
|
+
ctx.i++; // footnote_open
|
|
173
|
+
// Walk until matching footnote_close, collecting inline tokens
|
|
174
|
+
// from each paragraph in the def. Multi-paragraph defs are
|
|
175
|
+
// flattened into a single inline run (separated by line breaks)
|
|
176
|
+
// so the existing format-astro `<li>{inline}</li>` renderer
|
|
177
|
+
// doesn't need to learn about block-level def children.
|
|
178
|
+
const inline = [];
|
|
179
|
+
let firstParagraph = true;
|
|
180
|
+
while (ctx.i < ctx.tokens.length) {
|
|
181
|
+
const tok = ctx.tokens[ctx.i];
|
|
182
|
+
if (tok.type === "footnote_close") {
|
|
183
|
+
ctx.i++;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
if (tok.type === "paragraph_open") {
|
|
187
|
+
if (!firstParagraph)
|
|
188
|
+
inline.push({ type: "break" });
|
|
189
|
+
firstParagraph = false;
|
|
190
|
+
ctx.i++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (tok.type === "paragraph_close") {
|
|
194
|
+
ctx.i++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (tok.type === "inline" && tok.children) {
|
|
198
|
+
inline.push(...tokensToInline(tok.children));
|
|
199
|
+
ctx.i++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (tok.type === "footnote_anchor") {
|
|
203
|
+
// Single backref handled via props.backrefId — drop the
|
|
204
|
+
// per-occurrence anchor markers from the inline stream.
|
|
205
|
+
ctx.i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Anything else (nested code, etc.) — preserve as html if we
|
|
209
|
+
// can; otherwise advance. Keep this conservative: the common
|
|
210
|
+
// case is single-paragraph plain prose.
|
|
211
|
+
ctx.i++;
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
type: "footnote-def",
|
|
215
|
+
props: {
|
|
216
|
+
id: `fn-${label}`,
|
|
217
|
+
backrefId: `fnref-${label}`,
|
|
218
|
+
label,
|
|
219
|
+
},
|
|
220
|
+
inline,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function consumeBlockLeafDirective(ctx) {
|
|
224
|
+
const tok = ctx.tokens[ctx.i];
|
|
225
|
+
ctx.i++;
|
|
226
|
+
const name = tok.tag;
|
|
227
|
+
const attrs = tok.info ? JSON.parse(tok.info) : undefined;
|
|
228
|
+
// Reuse the same container-to-TreeNode mapper so leaf directives produce
|
|
229
|
+
// the same shape as their container equivalent — `::grid-item{...}` is
|
|
230
|
+
// semantically identical to `:::grid-item{...}\n:::`.
|
|
231
|
+
return mapContainerToTreeNode(name, attrs, []);
|
|
232
|
+
}
|
|
233
|
+
function consumeHeading(ctx) {
|
|
234
|
+
const open = ctx.tokens[ctx.i];
|
|
235
|
+
const level = parseInt(open.tag.slice(1), 10);
|
|
236
|
+
ctx.i++;
|
|
237
|
+
// Inline token
|
|
238
|
+
const inlineTok = ctx.tokens[ctx.i];
|
|
239
|
+
const inline = inlineTok?.children ? tokensToInline(inlineTok.children) : [];
|
|
240
|
+
ctx.i++;
|
|
241
|
+
// heading_close
|
|
242
|
+
ctx.i++;
|
|
243
|
+
const node = {
|
|
244
|
+
type: "heading",
|
|
245
|
+
props: { level },
|
|
246
|
+
inline,
|
|
247
|
+
};
|
|
248
|
+
// markdown-it-attrs puts attributes on the open token
|
|
249
|
+
const attrs = extractAttrs(open);
|
|
250
|
+
if (attrs.id) {
|
|
251
|
+
if (!node.props)
|
|
252
|
+
node.props = { level };
|
|
253
|
+
node.props.slug = attrs.id;
|
|
254
|
+
}
|
|
255
|
+
if (attrs.classes.length > 0) {
|
|
256
|
+
node.props.class = attrs.classes.join(" ");
|
|
257
|
+
}
|
|
258
|
+
for (const [k, v] of Object.entries(attrs.props)) {
|
|
259
|
+
node.props[k] = v;
|
|
260
|
+
}
|
|
261
|
+
return node;
|
|
262
|
+
}
|
|
263
|
+
function consumeParagraph(ctx) {
|
|
264
|
+
ctx.i++; // paragraph_open
|
|
265
|
+
const inlineTok = ctx.tokens[ctx.i];
|
|
266
|
+
// Check for GitHub-style alert at start of paragraph (inside blockquote)
|
|
267
|
+
// pattern: `[!NOTE]` as first token sequence in the paragraph
|
|
268
|
+
let inline = [];
|
|
269
|
+
if (inlineTok?.children) {
|
|
270
|
+
inline = tokensToInline(inlineTok.children);
|
|
271
|
+
}
|
|
272
|
+
ctx.i++; // inline
|
|
273
|
+
// paragraph_close
|
|
274
|
+
if (ctx.tokens[ctx.i]?.type === "paragraph_close")
|
|
275
|
+
ctx.i++;
|
|
276
|
+
if (inline.length === 0)
|
|
277
|
+
return null;
|
|
278
|
+
return { type: "paragraph", inline };
|
|
279
|
+
}
|
|
280
|
+
function consumeList(ctx, ordered) {
|
|
281
|
+
const open = ctx.tokens[ctx.i];
|
|
282
|
+
ctx.i++;
|
|
283
|
+
const items = [];
|
|
284
|
+
while (ctx.i < ctx.tokens.length) {
|
|
285
|
+
const tok = ctx.tokens[ctx.i];
|
|
286
|
+
if (tok.type === (ordered ? "ordered_list_close" : "bullet_list_close")) {
|
|
287
|
+
ctx.i++;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (tok.type === "list_item_open") {
|
|
291
|
+
items.push(consumeListItem(ctx));
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
ctx.i++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const node = {
|
|
298
|
+
type: ordered ? "ordered-list" : "unordered-list",
|
|
299
|
+
children: items,
|
|
300
|
+
};
|
|
301
|
+
if (ordered) {
|
|
302
|
+
const startAttr = open.attrs?.find(([k]) => k === "start");
|
|
303
|
+
if (startAttr) {
|
|
304
|
+
node.props = { start: parseInt(startAttr[1], 10) };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return node;
|
|
308
|
+
}
|
|
309
|
+
function consumeListItem(ctx) {
|
|
310
|
+
ctx.i++; // list_item_open
|
|
311
|
+
const children = [];
|
|
312
|
+
let depth = 1;
|
|
313
|
+
while (ctx.i < ctx.tokens.length && depth > 0) {
|
|
314
|
+
const tok = ctx.tokens[ctx.i];
|
|
315
|
+
if (tok.type === "list_item_open") {
|
|
316
|
+
depth++;
|
|
317
|
+
const child = consumeBlock(ctx);
|
|
318
|
+
if (child) {
|
|
319
|
+
if (Array.isArray(child))
|
|
320
|
+
children.push(...child);
|
|
321
|
+
else
|
|
322
|
+
children.push(child);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
ctx.i++;
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (tok.type === "list_item_close") {
|
|
330
|
+
depth--;
|
|
331
|
+
ctx.i++;
|
|
332
|
+
if (depth === 0)
|
|
333
|
+
break;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const child = consumeBlock(ctx);
|
|
337
|
+
if (child) {
|
|
338
|
+
if (Array.isArray(child))
|
|
339
|
+
children.push(...child);
|
|
340
|
+
else
|
|
341
|
+
children.push(child);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
ctx.i++;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Collapse single paragraph child into inline on the list-item
|
|
348
|
+
if (children.length === 1 && children[0].type === "paragraph" && children[0].inline) {
|
|
349
|
+
return { type: "list-item", inline: children[0].inline };
|
|
350
|
+
}
|
|
351
|
+
// Two children, first is paragraph (inline), rest are blocks
|
|
352
|
+
if (children.length >= 2 && children[0].type === "paragraph" && children[0].inline) {
|
|
353
|
+
return {
|
|
354
|
+
type: "list-item",
|
|
355
|
+
inline: children[0].inline,
|
|
356
|
+
children: children.slice(1),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return { type: "list-item", children };
|
|
360
|
+
}
|
|
361
|
+
function consumeBlockquote(ctx) {
|
|
362
|
+
ctx.i++; // blockquote_open
|
|
363
|
+
const children = [];
|
|
364
|
+
while (ctx.i < ctx.tokens.length) {
|
|
365
|
+
const tok = ctx.tokens[ctx.i];
|
|
366
|
+
if (tok.type === "blockquote_close") {
|
|
367
|
+
ctx.i++;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
const child = consumeBlock(ctx);
|
|
371
|
+
if (child) {
|
|
372
|
+
if (Array.isArray(child))
|
|
373
|
+
children.push(...child);
|
|
374
|
+
else
|
|
375
|
+
children.push(child);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
ctx.i++;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Detect GitHub alert pattern: first child is paragraph whose first inline text is "[!TYPE]"
|
|
382
|
+
const firstChild = children[0];
|
|
383
|
+
if (firstChild?.type === "paragraph" && firstChild.inline) {
|
|
384
|
+
const alertMatch = matchGitHubAlert(firstChild.inline);
|
|
385
|
+
if (alertMatch) {
|
|
386
|
+
const { type, rest } = alertMatch;
|
|
387
|
+
const newFirst = rest.length > 0 ? { type: "paragraph", inline: rest } : null;
|
|
388
|
+
const calloutChildren = [
|
|
389
|
+
...(newFirst ? [newFirst] : []),
|
|
390
|
+
...children.slice(1),
|
|
391
|
+
];
|
|
392
|
+
return {
|
|
393
|
+
type: "callout",
|
|
394
|
+
props: { type: type.toLowerCase() },
|
|
395
|
+
children: calloutChildren,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { type: "blockquote", children };
|
|
400
|
+
}
|
|
401
|
+
function matchGitHubAlert(inline) {
|
|
402
|
+
if (inline.length === 0 || inline[0].type !== "text")
|
|
403
|
+
return null;
|
|
404
|
+
const match = inline[0].text.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*\n?/);
|
|
405
|
+
if (!match)
|
|
406
|
+
return null;
|
|
407
|
+
const type = match[1];
|
|
408
|
+
const rest = [
|
|
409
|
+
{ ...inline[0], text: inline[0].text.slice(match[0].length) },
|
|
410
|
+
...inline.slice(1),
|
|
411
|
+
]
|
|
412
|
+
// Drop empty text nodes produced by the strip above
|
|
413
|
+
.filter((n) => !(n.type === "text" && n.text === ""));
|
|
414
|
+
// Strip leading whitespace (from the softbreak we converted to space)
|
|
415
|
+
while (rest.length > 0 && rest[0].type === "text" && rest[0].text.trim() === "") {
|
|
416
|
+
rest.shift();
|
|
417
|
+
}
|
|
418
|
+
// Also trim leading whitespace from the first remaining text node
|
|
419
|
+
if (rest.length > 0 && rest[0].type === "text") {
|
|
420
|
+
rest[0] = { ...rest[0], text: rest[0].text.replace(/^\s+/, "") };
|
|
421
|
+
}
|
|
422
|
+
return { type, rest };
|
|
423
|
+
}
|
|
424
|
+
function consumeCode(ctx) {
|
|
425
|
+
const tok = ctx.tokens[ctx.i];
|
|
426
|
+
ctx.i++;
|
|
427
|
+
const info = tok.info.trim();
|
|
428
|
+
const [lang, ...rest] = info.split(/\s+/);
|
|
429
|
+
const restInfo = rest.join(" ");
|
|
430
|
+
const props = {};
|
|
431
|
+
if (lang)
|
|
432
|
+
props.language = lang;
|
|
433
|
+
// Parse title="..." from rest of the info string (legacy/Starlight style)
|
|
434
|
+
const titleMatch = restInfo.match(/title\s*=\s*"([^"]*)"/);
|
|
435
|
+
if (titleMatch)
|
|
436
|
+
props.title = titleMatch[1];
|
|
437
|
+
// markdown-it-attrs consumes `{...}` blocks from the fence info string and
|
|
438
|
+
// attaches them to token.attrs. The Shiki/Expressive Code convention
|
|
439
|
+
// `{1,3-5}` (no key= prefix) becomes a single attr like ["1,3-5", ""] —
|
|
440
|
+
// we detect this shape and treat it as `highlights`.
|
|
441
|
+
if (tok.attrs) {
|
|
442
|
+
for (const [key, value] of tok.attrs) {
|
|
443
|
+
if (value === "" && /^[\d,\-\s]+$/.test(key)) {
|
|
444
|
+
// Shiki-style line range, no value
|
|
445
|
+
props.highlights = key;
|
|
446
|
+
}
|
|
447
|
+
else if (key === "class") {
|
|
448
|
+
props.class = value;
|
|
449
|
+
}
|
|
450
|
+
else if (key === "id") {
|
|
451
|
+
props.id = value;
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
// Standard key=value attribute (highlights, lineNumbers, ins, del,
|
|
455
|
+
// mark, collapse, title, frame, etc.)
|
|
456
|
+
props[key] = value === "" ? true : value;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
type: "code",
|
|
462
|
+
props,
|
|
463
|
+
html: tok.content.replace(/\n$/, ""),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function consumeHtmlBlock(ctx) {
|
|
467
|
+
const tok = ctx.tokens[ctx.i];
|
|
468
|
+
ctx.i++;
|
|
469
|
+
// Detect <details> HTML and convert to details node
|
|
470
|
+
const detailsMatch = tok.content.match(/^<details[^>]*>\s*<summary[^>]*>([\s\S]*?)<\/summary>\s*([\s\S]*?)<\/details>\s*$/);
|
|
471
|
+
if (detailsMatch) {
|
|
472
|
+
const summary = detailsMatch[1].trim();
|
|
473
|
+
const innerHtml = detailsMatch[2].trim();
|
|
474
|
+
// Re-parse inner as markdown? For simplicity, put as html-container child
|
|
475
|
+
return {
|
|
476
|
+
type: "details",
|
|
477
|
+
props: { title: summary },
|
|
478
|
+
children: [{ type: "html-container", html: innerHtml }],
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return { type: "html", html: tok.content };
|
|
482
|
+
}
|
|
483
|
+
function consumeTable(ctx) {
|
|
484
|
+
ctx.i++; // table_open
|
|
485
|
+
const children = [];
|
|
486
|
+
while (ctx.i < ctx.tokens.length) {
|
|
487
|
+
const tok = ctx.tokens[ctx.i];
|
|
488
|
+
if (tok.type === "table_close") {
|
|
489
|
+
ctx.i++;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
if (tok.type === "thead_open") {
|
|
493
|
+
children.push(consumeTableSection(ctx, "thead", "thead_close"));
|
|
494
|
+
}
|
|
495
|
+
else if (tok.type === "tbody_open") {
|
|
496
|
+
children.push(consumeTableSection(ctx, "tbody", "tbody_close"));
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
ctx.i++;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return { type: "table", children };
|
|
503
|
+
}
|
|
504
|
+
function consumeTableSection(ctx, sectionType, closeType) {
|
|
505
|
+
ctx.i++; // section_open
|
|
506
|
+
const rows = [];
|
|
507
|
+
while (ctx.i < ctx.tokens.length) {
|
|
508
|
+
const tok = ctx.tokens[ctx.i];
|
|
509
|
+
if (tok.type === closeType) {
|
|
510
|
+
ctx.i++;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
if (tok.type === "tr_open") {
|
|
514
|
+
rows.push(consumeTableRow(ctx));
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
ctx.i++;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return { type: sectionType, children: rows };
|
|
521
|
+
}
|
|
522
|
+
function consumeTableRow(ctx) {
|
|
523
|
+
ctx.i++; // tr_open
|
|
524
|
+
const cells = [];
|
|
525
|
+
while (ctx.i < ctx.tokens.length) {
|
|
526
|
+
const tok = ctx.tokens[ctx.i];
|
|
527
|
+
if (tok.type === "tr_close") {
|
|
528
|
+
ctx.i++;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
if (tok.type === "th_open" || tok.type === "td_open") {
|
|
532
|
+
const cellType = tok.type === "th_open" ? "th" : "td";
|
|
533
|
+
const closeType = tok.type === "th_open" ? "th_close" : "td_close";
|
|
534
|
+
ctx.i++;
|
|
535
|
+
const inlineTok = ctx.tokens[ctx.i];
|
|
536
|
+
const inline = inlineTok?.children ? tokensToInline(inlineTok.children) : [];
|
|
537
|
+
ctx.i++;
|
|
538
|
+
// skip to close
|
|
539
|
+
while (ctx.i < ctx.tokens.length && ctx.tokens[ctx.i].type !== closeType)
|
|
540
|
+
ctx.i++;
|
|
541
|
+
if (ctx.i < ctx.tokens.length)
|
|
542
|
+
ctx.i++;
|
|
543
|
+
cells.push({ type: cellType, inline });
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
ctx.i++;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return { type: "tr", children: cells };
|
|
550
|
+
}
|
|
551
|
+
function consumeDeflist(ctx) {
|
|
552
|
+
ctx.i++; // dl_open
|
|
553
|
+
const children = [];
|
|
554
|
+
while (ctx.i < ctx.tokens.length) {
|
|
555
|
+
const tok = ctx.tokens[ctx.i];
|
|
556
|
+
if (tok.type === "dl_close") {
|
|
557
|
+
ctx.i++;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
if (tok.type === "dt_open") {
|
|
561
|
+
ctx.i++;
|
|
562
|
+
const inlineTok = ctx.tokens[ctx.i];
|
|
563
|
+
const inline = inlineTok?.children ? tokensToInline(inlineTok.children) : [];
|
|
564
|
+
ctx.i++;
|
|
565
|
+
// skip to dt_close
|
|
566
|
+
while (ctx.i < ctx.tokens.length && ctx.tokens[ctx.i].type !== "dt_close")
|
|
567
|
+
ctx.i++;
|
|
568
|
+
if (ctx.i < ctx.tokens.length)
|
|
569
|
+
ctx.i++;
|
|
570
|
+
children.push({ type: "dt", inline });
|
|
571
|
+
}
|
|
572
|
+
else if (tok.type === "dd_open") {
|
|
573
|
+
ctx.i++;
|
|
574
|
+
const innerChildren = [];
|
|
575
|
+
while (ctx.i < ctx.tokens.length && ctx.tokens[ctx.i].type !== "dd_close") {
|
|
576
|
+
const c = consumeBlock(ctx);
|
|
577
|
+
if (c) {
|
|
578
|
+
if (Array.isArray(c))
|
|
579
|
+
innerChildren.push(...c);
|
|
580
|
+
else
|
|
581
|
+
innerChildren.push(c);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
ctx.i++;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (ctx.i < ctx.tokens.length)
|
|
588
|
+
ctx.i++;
|
|
589
|
+
children.push({ type: "dd", children: innerChildren });
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
ctx.i++;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return { type: "deflist", children };
|
|
596
|
+
}
|
|
597
|
+
function consumeContainer(ctx) {
|
|
598
|
+
const openTok = ctx.tokens[ctx.i];
|
|
599
|
+
// Derive name from token type: "container_{name}_open"
|
|
600
|
+
const name = openTok.type.replace(/^container_/, "").replace(/_open$/, "");
|
|
601
|
+
// Attrs are attached to the token by markdown-it-attrs
|
|
602
|
+
const attrs = extractAttrs(openTok);
|
|
603
|
+
ctx.i++;
|
|
604
|
+
const children = [];
|
|
605
|
+
const closeType = `container_${name}_close`;
|
|
606
|
+
while (ctx.i < ctx.tokens.length) {
|
|
607
|
+
const tok = ctx.tokens[ctx.i];
|
|
608
|
+
if (tok.type === closeType) {
|
|
609
|
+
ctx.i++;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
const child = consumeBlock(ctx);
|
|
613
|
+
if (child) {
|
|
614
|
+
if (Array.isArray(child))
|
|
615
|
+
children.push(...child);
|
|
616
|
+
else
|
|
617
|
+
children.push(child);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
ctx.i++;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return mapContainerToTreeNode(name, attrs, children);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Convert a container + children into the appropriate TreeNode(s).
|
|
627
|
+
* Handles callout type aliases, tabs definition-list recovery, cards list form, etc.
|
|
628
|
+
*/
|
|
629
|
+
function mapContainerToTreeNode(name, attrs, children) {
|
|
630
|
+
const props = attrs ? attrsToProps(attrs) : {};
|
|
631
|
+
// Callout type aliases — :::note, :::tip, etc. map to callout with type
|
|
632
|
+
const CALLOUT_TYPES = new Set([
|
|
633
|
+
"note",
|
|
634
|
+
"tip",
|
|
635
|
+
"info",
|
|
636
|
+
"important",
|
|
637
|
+
"warning",
|
|
638
|
+
"caution",
|
|
639
|
+
"danger",
|
|
640
|
+
"success",
|
|
641
|
+
"callout",
|
|
642
|
+
]);
|
|
643
|
+
if (CALLOUT_TYPES.has(name)) {
|
|
644
|
+
const calloutProps = { ...props };
|
|
645
|
+
if (name !== "callout")
|
|
646
|
+
calloutProps.type = name;
|
|
647
|
+
return { type: "callout", props: calloutProps, children };
|
|
648
|
+
}
|
|
649
|
+
// Tabs — if children are a single deflist, convert each dt/dd pair to a tab
|
|
650
|
+
if (name === "tabs") {
|
|
651
|
+
return tabsFromChildren(props, children);
|
|
652
|
+
}
|
|
653
|
+
// Steps — if children are a single ordered-list, each list-item becomes a step
|
|
654
|
+
if (name === "steps") {
|
|
655
|
+
return stepsFromChildren(props, children);
|
|
656
|
+
}
|
|
657
|
+
// Cards — if children are a single list, each list-item with bold link becomes a card
|
|
658
|
+
if (name === "cards") {
|
|
659
|
+
return cardsFromChildren(props, children);
|
|
660
|
+
}
|
|
661
|
+
// Grid — generic wrapper, children pass through
|
|
662
|
+
if (name === "grid") {
|
|
663
|
+
return { type: "grid", props, children };
|
|
664
|
+
}
|
|
665
|
+
// Grid item — for col/row spanning inside a :::grid
|
|
666
|
+
if (name === "grid-item") {
|
|
667
|
+
return { type: "grid-item", props, children };
|
|
668
|
+
}
|
|
669
|
+
// Details
|
|
670
|
+
if (name === "details") {
|
|
671
|
+
return { type: "details", props, children };
|
|
672
|
+
}
|
|
673
|
+
// Button
|
|
674
|
+
if (name === "button") {
|
|
675
|
+
return { type: "link-button", props, children };
|
|
676
|
+
}
|
|
677
|
+
// Example — docs-oriented: capture inner source by re-serializing children,
|
|
678
|
+
// keep children so the renderer can also show them as live preview.
|
|
679
|
+
if (name === "example") {
|
|
680
|
+
const source = treeToDogsbayMd(children);
|
|
681
|
+
return { type: "example", props: { ...props, source }, children };
|
|
682
|
+
}
|
|
683
|
+
// Accordion — definition list children become accordion-item nodes
|
|
684
|
+
if (name === "accordion") {
|
|
685
|
+
return accordionFromChildren(props, children);
|
|
686
|
+
}
|
|
687
|
+
// Accordion item — passthrough; label can come from props or first heading
|
|
688
|
+
if (name === "accordion-item") {
|
|
689
|
+
return { type: "accordion-item", props, children };
|
|
690
|
+
}
|
|
691
|
+
// Link card — single-paragraph body becomes description prop (cleaner
|
|
692
|
+
// round-trip and matches the LinkCard component's `description` attr).
|
|
693
|
+
// Multi-block bodies fall through as children.
|
|
694
|
+
if (name === "link-card") {
|
|
695
|
+
return linkCardFromChildren(props, children);
|
|
696
|
+
}
|
|
697
|
+
// Avatar — leaf-only (block-leaf or inline-leaf). No body expected;
|
|
698
|
+
// any children are dropped.
|
|
699
|
+
if (name === "avatar") {
|
|
700
|
+
return { type: "avatar", props };
|
|
701
|
+
}
|
|
702
|
+
// Generic directive — pass through as-is
|
|
703
|
+
return { type: name, props, children };
|
|
704
|
+
}
|
|
705
|
+
function accordionFromChildren(props, children) {
|
|
706
|
+
// Preferred shape: a single deflist where each dt is the trigger label
|
|
707
|
+
// and each dd is the panel body.
|
|
708
|
+
if (children.length === 1 && children[0].type === "deflist") {
|
|
709
|
+
const dl = children[0];
|
|
710
|
+
const items = [];
|
|
711
|
+
const list = dl.children ?? [];
|
|
712
|
+
let counter = 1;
|
|
713
|
+
for (let i = 0; i < list.length; i++) {
|
|
714
|
+
if (list[i].type === "dt") {
|
|
715
|
+
const label = inlineText(list[i].inline ?? []);
|
|
716
|
+
const dd = list[i + 1]?.type === "dd" ? list[i + 1] : null;
|
|
717
|
+
if (dd) {
|
|
718
|
+
items.push({
|
|
719
|
+
type: "accordion-item",
|
|
720
|
+
props: { value: `item-${counter++}`, label },
|
|
721
|
+
children: dd.children ?? [],
|
|
722
|
+
});
|
|
723
|
+
i++;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return { type: "accordion", props, children: items };
|
|
728
|
+
}
|
|
729
|
+
// Directive form: explicit `:::accordion-item` children
|
|
730
|
+
const itemKids = children.filter((c) => c.type === "accordion-item");
|
|
731
|
+
if (itemKids.length > 0) {
|
|
732
|
+
return { type: "accordion", props, children: itemKids };
|
|
733
|
+
}
|
|
734
|
+
return { type: "accordion", props, children };
|
|
735
|
+
}
|
|
736
|
+
function linkCardFromChildren(props, children) {
|
|
737
|
+
// Single-paragraph body → fold into description prop. Lets the
|
|
738
|
+
// serializer round-trip back to the same body form.
|
|
739
|
+
if (children.length === 1 && children[0].type === "paragraph") {
|
|
740
|
+
const para = children[0];
|
|
741
|
+
if (para.inline && para.inline.length > 0) {
|
|
742
|
+
const description = inlineText(para.inline).trim();
|
|
743
|
+
const merged = { ...props };
|
|
744
|
+
if (description && !merged.description)
|
|
745
|
+
merged.description = description;
|
|
746
|
+
return { type: "link-card", props: merged };
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// Otherwise keep children for richer body content.
|
|
750
|
+
return { type: "link-card", props, children };
|
|
751
|
+
}
|
|
752
|
+
function tabsFromChildren(props, children) {
|
|
753
|
+
// Preferred: children is a single deflist
|
|
754
|
+
if (children.length === 1 && children[0].type === "deflist") {
|
|
755
|
+
const dl = children[0];
|
|
756
|
+
const tabs = [];
|
|
757
|
+
const list = dl.children ?? [];
|
|
758
|
+
for (let i = 0; i < list.length; i++) {
|
|
759
|
+
if (list[i].type === "dt") {
|
|
760
|
+
const label = inlineText(list[i].inline ?? []);
|
|
761
|
+
// Find next dd
|
|
762
|
+
const dd = list[i + 1]?.type === "dd" ? list[i + 1] : null;
|
|
763
|
+
if (dd) {
|
|
764
|
+
tabs.push({
|
|
765
|
+
type: "tab",
|
|
766
|
+
props: { label },
|
|
767
|
+
children: dd.children ?? [],
|
|
768
|
+
});
|
|
769
|
+
i++;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return { type: "tabs", props, children: tabs };
|
|
774
|
+
}
|
|
775
|
+
// Directive form: children are explicit :::tab containers
|
|
776
|
+
const tabs = children.filter((c) => c.type === "tab");
|
|
777
|
+
if (tabs.length > 0) {
|
|
778
|
+
return { type: "tabs", props, children: tabs };
|
|
779
|
+
}
|
|
780
|
+
// Fallback — unknown shape
|
|
781
|
+
return { type: "tabs", props, children };
|
|
782
|
+
}
|
|
783
|
+
function stepsFromChildren(props, children) {
|
|
784
|
+
// Expected: children is a single ordered-list
|
|
785
|
+
if (children.length === 1 && children[0].type === "ordered-list") {
|
|
786
|
+
const list = children[0];
|
|
787
|
+
const steps = [];
|
|
788
|
+
for (const item of list.children ?? []) {
|
|
789
|
+
if (item.type === "list-item") {
|
|
790
|
+
steps.push({
|
|
791
|
+
type: "step",
|
|
792
|
+
inline: item.inline,
|
|
793
|
+
children: item.children,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return { type: "steps", props, children: steps };
|
|
798
|
+
}
|
|
799
|
+
// Directive form: children are explicit :::step containers
|
|
800
|
+
const steps = children.filter((c) => c.type === "step");
|
|
801
|
+
if (steps.length > 0) {
|
|
802
|
+
return { type: "steps", props, children: steps };
|
|
803
|
+
}
|
|
804
|
+
return { type: "steps", props, children };
|
|
805
|
+
}
|
|
806
|
+
function cardsFromChildren(props, children) {
|
|
807
|
+
// Expected: children is a single unordered-list with cards-in-link form
|
|
808
|
+
if (children.length === 1 && children[0].type === "unordered-list") {
|
|
809
|
+
const list = children[0];
|
|
810
|
+
const cards = [];
|
|
811
|
+
for (const item of list.children ?? []) {
|
|
812
|
+
if (item.type === "list-item") {
|
|
813
|
+
const card = parseCardListItem(item);
|
|
814
|
+
if (card)
|
|
815
|
+
cards.push(card);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (cards.length > 0) {
|
|
819
|
+
return { type: "cards", props, children: cards };
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// Directive form: children are explicit :::card containers
|
|
823
|
+
const cards = children.filter((c) => c.type === "card");
|
|
824
|
+
if (cards.length > 0) {
|
|
825
|
+
return { type: "cards", props, children: cards };
|
|
826
|
+
}
|
|
827
|
+
return { type: "cards", props, children };
|
|
828
|
+
}
|
|
829
|
+
function parseCardListItem(item) {
|
|
830
|
+
const inline = item.inline ?? [];
|
|
831
|
+
if (inline.length === 0)
|
|
832
|
+
return null;
|
|
833
|
+
// Skip leading empty text nodes (produced by bold/strong wrappers around links)
|
|
834
|
+
let idx = 0;
|
|
835
|
+
while (idx < inline.length) {
|
|
836
|
+
const n = inline[idx];
|
|
837
|
+
if (n.type === "text" && n.text === "")
|
|
838
|
+
idx++;
|
|
839
|
+
else
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
const first = inline[idx];
|
|
843
|
+
if (!first || first.type !== "link")
|
|
844
|
+
return null;
|
|
845
|
+
const titleText = inlineText(first.children);
|
|
846
|
+
const props = {
|
|
847
|
+
title: titleText,
|
|
848
|
+
href: first.href,
|
|
849
|
+
};
|
|
850
|
+
const description = inlineRestToText(inline.slice(idx + 1));
|
|
851
|
+
if (description)
|
|
852
|
+
props.description = description;
|
|
853
|
+
const childDescription = extractCardDescription(item.children ?? []);
|
|
854
|
+
if (childDescription && !props.description) {
|
|
855
|
+
props.description = childDescription;
|
|
856
|
+
}
|
|
857
|
+
return { type: "card", props };
|
|
858
|
+
}
|
|
859
|
+
function extractCardDescription(children) {
|
|
860
|
+
if (children.length === 0)
|
|
861
|
+
return "";
|
|
862
|
+
const first = children[0];
|
|
863
|
+
if (first.type === "paragraph" && first.inline) {
|
|
864
|
+
return inlineText(first.inline).trim();
|
|
865
|
+
}
|
|
866
|
+
return "";
|
|
867
|
+
}
|
|
868
|
+
function inlineText(nodes) {
|
|
869
|
+
let out = "";
|
|
870
|
+
for (const n of nodes) {
|
|
871
|
+
if (n.type === "text")
|
|
872
|
+
out += n.text;
|
|
873
|
+
else if (n.type === "link")
|
|
874
|
+
out += inlineText(n.children);
|
|
875
|
+
else if (n.type === "highlight")
|
|
876
|
+
out += inlineText(n.children);
|
|
877
|
+
else if (n.type === "code")
|
|
878
|
+
out += n.text;
|
|
879
|
+
}
|
|
880
|
+
return out;
|
|
881
|
+
}
|
|
882
|
+
function inlineRestToText(nodes) {
|
|
883
|
+
return inlineText(nodes).trim();
|
|
884
|
+
}
|
|
885
|
+
function extractAttrs(token) {
|
|
886
|
+
const result = { classes: [], props: {} };
|
|
887
|
+
if (!token.attrs)
|
|
888
|
+
return result;
|
|
889
|
+
for (const [key, value] of token.attrs) {
|
|
890
|
+
if (key === "id")
|
|
891
|
+
result.id = value;
|
|
892
|
+
else if (key === "class")
|
|
893
|
+
result.classes.push(...value.split(/\s+/).filter(Boolean));
|
|
894
|
+
else
|
|
895
|
+
result.props[key] = value;
|
|
896
|
+
}
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
// ── Tokens → InlineNode[] ────────────────────────────────────────────────
|
|
900
|
+
function tokensToInline(tokens, inheritedFmt = []) {
|
|
901
|
+
const result = [];
|
|
902
|
+
const fmtStack = [...inheritedFmt];
|
|
903
|
+
let i = 0;
|
|
904
|
+
while (i < tokens.length) {
|
|
905
|
+
const tok = tokens[i];
|
|
906
|
+
switch (tok.type) {
|
|
907
|
+
case "text": {
|
|
908
|
+
result.push(makeText(tok.content, fmtStack));
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
case "softbreak": {
|
|
912
|
+
// Treat as space in inline output
|
|
913
|
+
result.push(makeText(" ", fmtStack));
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
case "hardbreak": {
|
|
917
|
+
result.push({ type: "break" });
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
case "strong_open":
|
|
921
|
+
fmtStack.push("bold");
|
|
922
|
+
break;
|
|
923
|
+
case "strong_close":
|
|
924
|
+
fmtStack.pop();
|
|
925
|
+
break;
|
|
926
|
+
case "em_open":
|
|
927
|
+
fmtStack.push("italic");
|
|
928
|
+
break;
|
|
929
|
+
case "em_close":
|
|
930
|
+
fmtStack.pop();
|
|
931
|
+
break;
|
|
932
|
+
case "s_open":
|
|
933
|
+
fmtStack.push("strike");
|
|
934
|
+
break;
|
|
935
|
+
case "s_close":
|
|
936
|
+
fmtStack.pop();
|
|
937
|
+
break;
|
|
938
|
+
case "code_inline": {
|
|
939
|
+
result.push({ type: "code", text: tok.content });
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
case "link_open": {
|
|
943
|
+
// Collect children up to link_close. Pass formatting context so link
|
|
944
|
+
// children inside **bold** wrappers retain the bold annotation.
|
|
945
|
+
const href = tok.attrs?.find(([k]) => k === "href")?.[1] ?? "";
|
|
946
|
+
const title = tok.attrs?.find(([k]) => k === "title")?.[1];
|
|
947
|
+
const childTokens = [];
|
|
948
|
+
i++;
|
|
949
|
+
while (i < tokens.length && tokens[i].type !== "link_close") {
|
|
950
|
+
childTokens.push(tokens[i]);
|
|
951
|
+
i++;
|
|
952
|
+
}
|
|
953
|
+
const children = tokensToInline(childTokens, fmtStack);
|
|
954
|
+
const link = { type: "link", href, children };
|
|
955
|
+
if (title)
|
|
956
|
+
link.title = title;
|
|
957
|
+
result.push(link);
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
case "image": {
|
|
961
|
+
const src = tok.attrs?.find(([k]) => k === "src")?.[1] ?? "";
|
|
962
|
+
const alt = tok.content;
|
|
963
|
+
const title = tok.attrs?.find(([k]) => k === "title")?.[1];
|
|
964
|
+
const img = { type: "image", src, alt };
|
|
965
|
+
if (title)
|
|
966
|
+
img.title = title;
|
|
967
|
+
result.push(img);
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
case "dogsbay_inline_directive_open": {
|
|
971
|
+
const name = tok.tag;
|
|
972
|
+
const attrs = tok.info ? JSON.parse(tok.info) : null;
|
|
973
|
+
// Collect label tokens until matching close
|
|
974
|
+
const labelTokens = [];
|
|
975
|
+
i++;
|
|
976
|
+
let depth = 1;
|
|
977
|
+
while (i < tokens.length && depth > 0) {
|
|
978
|
+
const inner = tokens[i];
|
|
979
|
+
if (inner.type === "dogsbay_inline_directive_open")
|
|
980
|
+
depth++;
|
|
981
|
+
else if (inner.type === "dogsbay_inline_directive_close") {
|
|
982
|
+
depth--;
|
|
983
|
+
if (depth === 0)
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
labelTokens.push(inner);
|
|
987
|
+
i++;
|
|
988
|
+
}
|
|
989
|
+
const labelInline = tokensToInline(labelTokens);
|
|
990
|
+
const labelText = inlineText(labelInline);
|
|
991
|
+
const node = inlineDirectiveToNode(name, labelText, labelInline, attrs);
|
|
992
|
+
if (node)
|
|
993
|
+
result.push(node);
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
case "dogsbay_leaf_directive": {
|
|
997
|
+
const name = tok.tag;
|
|
998
|
+
const attrs = tok.info ? JSON.parse(tok.info) : null;
|
|
999
|
+
const node = inlineDirectiveToNode(name, "", [], attrs);
|
|
1000
|
+
if (node)
|
|
1001
|
+
result.push(node);
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
case "html_inline": {
|
|
1005
|
+
result.push({ type: "html-inline", html: tok.content });
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
case "footnote_ref": {
|
|
1009
|
+
// markdown-it-footnote attaches { id, subId, label? } to
|
|
1010
|
+
// the token's meta. Named footnotes keep the writer's
|
|
1011
|
+
// label; inline-sugar (^[..]) footnotes get a 1-based
|
|
1012
|
+
// numeric label. The subId field (which counts repeated
|
|
1013
|
+
// citations of the same footnote) is currently dropped —
|
|
1014
|
+
// multiple references render with duplicate anchor ids,
|
|
1015
|
+
// a known limitation tracked in plans/footnote-support.md.
|
|
1016
|
+
const fnMeta = (tok.meta ?? {});
|
|
1017
|
+
const label = fnMeta.label ?? String((fnMeta.id ?? 0) + 1);
|
|
1018
|
+
result.push({ type: "footnote-ref", label });
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
default:
|
|
1022
|
+
// Unknown inline token — skip
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
i++;
|
|
1026
|
+
}
|
|
1027
|
+
return result;
|
|
1028
|
+
}
|
|
1029
|
+
function makeText(text, fmtStack) {
|
|
1030
|
+
const node = { type: "text", text };
|
|
1031
|
+
if (fmtStack.includes("bold"))
|
|
1032
|
+
node.bold = true;
|
|
1033
|
+
if (fmtStack.includes("italic"))
|
|
1034
|
+
node.italic = true;
|
|
1035
|
+
if (fmtStack.includes("strike"))
|
|
1036
|
+
node.strikethrough = true;
|
|
1037
|
+
return node;
|
|
1038
|
+
}
|
|
1039
|
+
function inlineDirectiveToNode(name, labelText, _labelInline, attrs) {
|
|
1040
|
+
switch (name) {
|
|
1041
|
+
case "kbd": {
|
|
1042
|
+
const keys = labelText.split(/\s*\+\s*/).filter(Boolean);
|
|
1043
|
+
return { type: "kbd", keys };
|
|
1044
|
+
}
|
|
1045
|
+
case "icon": {
|
|
1046
|
+
const [library, ...rest] = labelText.split(":");
|
|
1047
|
+
if (rest.length === 0) {
|
|
1048
|
+
return { type: "icon", name: library };
|
|
1049
|
+
}
|
|
1050
|
+
return { type: "icon", name: rest.join(":"), library };
|
|
1051
|
+
}
|
|
1052
|
+
case "math": {
|
|
1053
|
+
return { type: "math", latex: labelText };
|
|
1054
|
+
}
|
|
1055
|
+
case "mark":
|
|
1056
|
+
case "highlight": {
|
|
1057
|
+
return {
|
|
1058
|
+
type: "highlight",
|
|
1059
|
+
children: [{ type: "text", text: labelText }],
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
case "button": {
|
|
1063
|
+
// Inline button renders as a link
|
|
1064
|
+
const href = attrs?.props.href ?? "";
|
|
1065
|
+
return {
|
|
1066
|
+
type: "link",
|
|
1067
|
+
href,
|
|
1068
|
+
children: [{ type: "text", text: labelText }],
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
default:
|
|
1072
|
+
// Unknown inline directive — preserve as html
|
|
1073
|
+
return { type: "html-inline", html: `:${name}[${labelText}]` };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
//# sourceMappingURL=parse.js.map
|