@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.
Files changed (65) hide show
  1. package/dist/attributes.d.ts +33 -0
  2. package/dist/attributes.d.ts.map +1 -0
  3. package/dist/attributes.js +83 -0
  4. package/dist/attributes.js.map +1 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +129 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/directives.d.ts +19 -0
  10. package/dist/directives.d.ts.map +1 -0
  11. package/dist/directives.js +76 -0
  12. package/dist/directives.js.map +1 -0
  13. package/dist/escape.d.ts +42 -0
  14. package/dist/escape.d.ts.map +1 -0
  15. package/dist/escape.js +79 -0
  16. package/dist/escape.js.map +1 -0
  17. package/dist/index.d.ts +10 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +10 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/inline.d.ts +9 -0
  22. package/dist/inline.d.ts.map +1 -0
  23. package/dist/inline.js +122 -0
  24. package/dist/inline.js.map +1 -0
  25. package/dist/nav-file.d.ts +38 -0
  26. package/dist/nav-file.d.ts.map +1 -0
  27. package/dist/nav-file.js +257 -0
  28. package/dist/nav-file.js.map +1 -0
  29. package/dist/nav.d.ts +34 -0
  30. package/dist/nav.d.ts.map +1 -0
  31. package/dist/nav.js +169 -0
  32. package/dist/nav.js.map +1 -0
  33. package/dist/parse-attrs.d.ts +24 -0
  34. package/dist/parse-attrs.d.ts.map +1 -0
  35. package/dist/parse-attrs.js +117 -0
  36. package/dist/parse-attrs.js.map +1 -0
  37. package/dist/parse.d.ts +18 -0
  38. package/dist/parse.d.ts.map +1 -0
  39. package/dist/parse.js +1076 -0
  40. package/dist/parse.js.map +1 -0
  41. package/dist/plugin-block-leaf.d.ts +19 -0
  42. package/dist/plugin-block-leaf.d.ts.map +1 -0
  43. package/dist/plugin-block-leaf.js +81 -0
  44. package/dist/plugin-block-leaf.js.map +1 -0
  45. package/dist/plugin-containers.d.ts +11 -0
  46. package/dist/plugin-containers.d.ts.map +1 -0
  47. package/dist/plugin-containers.js +63 -0
  48. package/dist/plugin-containers.js.map +1 -0
  49. package/dist/plugin-inline-directives.d.ts +18 -0
  50. package/dist/plugin-inline-directives.d.ts.map +1 -0
  51. package/dist/plugin-inline-directives.js +121 -0
  52. package/dist/plugin-inline-directives.js.map +1 -0
  53. package/dist/serialize.d.ts +25 -0
  54. package/dist/serialize.d.ts.map +1 -0
  55. package/dist/serialize.js +712 -0
  56. package/dist/serialize.js.map +1 -0
  57. package/dist/types.d.ts +40 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +10 -0
  60. package/dist/types.js.map +1 -0
  61. package/dist/yaml.d.ts +22 -0
  62. package/dist/yaml.d.ts.map +1 -0
  63. package/dist/yaml.js +113 -0
  64. package/dist/yaml.js.map +1 -0
  65. 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