@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
@@ -0,0 +1,712 @@
1
+ import { inlineToDogsbayMd } from "./inline.js";
2
+ import { renderAttrs, splitProps } from "./attributes.js";
3
+ import { pickCodeFence, pickDirectiveFence, normalizeTrailingWhitespace, } from "./escape.js";
4
+ import { resolveOptions } from "./types.js";
5
+ import { renderBlockDirective } from "./directives.js";
6
+ import { renderFrontmatter } from "./yaml.js";
7
+ /**
8
+ * Serialize a tree of nodes to Dogsbay Markdown.
9
+ */
10
+ export function treeToDogsbayMd(nodes, options) {
11
+ const ctx = resolveOptions(options);
12
+ const parts = nodes.map((node) => renderNode(node, ctx));
13
+ const body = normalizeTrailingWhitespace(parts.filter(Boolean).join("\n\n"));
14
+ if (options?.frontmatter && Object.keys(options.frontmatter).length > 0) {
15
+ return `${renderFrontmatter(options.frontmatter)}\n\n${body}`;
16
+ }
17
+ return body;
18
+ }
19
+ /**
20
+ * Serialize a single node (no trailing normalization).
21
+ * Exposed for chunker to emit one node per file.
22
+ */
23
+ export function nodeToDogsbayMd(node, options) {
24
+ return renderNode(node, resolveOptions(options));
25
+ }
26
+ function renderNode(node, ctx) {
27
+ switch (node.type) {
28
+ case "heading":
29
+ return renderHeading(node);
30
+ case "paragraph":
31
+ case "prose":
32
+ return renderParagraph(node, ctx);
33
+ case "code":
34
+ return renderCode(node);
35
+ case "hr":
36
+ case "thematic-break":
37
+ case "separator":
38
+ return "---";
39
+ case "blockquote":
40
+ return renderBlockquote(node, ctx);
41
+ case "html":
42
+ case "html-container":
43
+ return node.html ?? "";
44
+ case "unordered-list":
45
+ return renderList(node, ctx, false);
46
+ case "ordered-list":
47
+ return renderList(node, ctx, true);
48
+ case "list-item":
49
+ // Standalone list item (rare; usually rendered via list container)
50
+ return renderInlineOrChildren(node, ctx);
51
+ case "callout":
52
+ return renderCallout(node, ctx);
53
+ case "details":
54
+ return renderDetails(node, ctx);
55
+ case "table":
56
+ return renderTable(node, ctx);
57
+ case "figure":
58
+ return renderFigure(node, ctx);
59
+ case "math-block":
60
+ return renderMathBlock(node);
61
+ case "image":
62
+ return renderBlockImage(node);
63
+ case "tabs":
64
+ return renderTabs(node, ctx);
65
+ case "steps":
66
+ case "substeps":
67
+ return renderSteps(node, ctx);
68
+ case "cards":
69
+ return renderCards(node, ctx);
70
+ case "grid":
71
+ return renderGrid(node, ctx);
72
+ case "grid-item":
73
+ return renderGridItem(node, ctx);
74
+ case "card":
75
+ return renderCard(node, ctx);
76
+ case "link-button":
77
+ case "button":
78
+ return renderButton(node, ctx);
79
+ case "deflist":
80
+ return renderDeflist(node, ctx);
81
+ case "accordion":
82
+ return renderAccordion(node, ctx);
83
+ case "accordion-item":
84
+ return renderAccordionItem(node, ctx);
85
+ case "link-card":
86
+ return renderLinkCard(node, ctx);
87
+ case "avatar":
88
+ return renderAvatar(node);
89
+ case "footnote-list":
90
+ return renderFootnoteList(node, ctx);
91
+ case "footnote-def":
92
+ return renderFootnoteDef(node);
93
+ case "endpoint":
94
+ return renderEndpoint(node);
95
+ default:
96
+ return renderUnknown(node, ctx);
97
+ }
98
+ }
99
+ /**
100
+ * Serialize an OpenAPI `endpoint` TreeNode (produced by
101
+ * @dogsbay/format-openapi) as agent-readable markdown. The HTML
102
+ * version uses ApiLayout + EndpointCard + friends; the markdown
103
+ * mirror collapses the same data into headings, tables, and code
104
+ * blocks so an agent fetching the `.md` URL still gets the full
105
+ * operation reference.
106
+ */
107
+ function renderEndpoint(node) {
108
+ const props = (node.props ?? {});
109
+ const method = String(props.method ?? "get").toUpperCase();
110
+ const path = String(props.path ?? "");
111
+ const baseUrl = String(props.baseUrl ?? "");
112
+ const summary = props.summary;
113
+ const description = props.description;
114
+ const deprecated = props.deprecated === true;
115
+ const parameters = props.parameters ?? [];
116
+ const requestBody = props.requestBody ?? [];
117
+ const responses = props.responses ?? [];
118
+ const codeSamples = props.codeSamples ?? [];
119
+ const out = [];
120
+ out.push(`## ${method} ${path}`);
121
+ out.push("");
122
+ if (deprecated) {
123
+ out.push("> [!WARNING]");
124
+ out.push("> This endpoint is deprecated.");
125
+ out.push("");
126
+ }
127
+ if (baseUrl) {
128
+ out.push(`**Base URL**: \`${baseUrl}\``);
129
+ out.push("");
130
+ }
131
+ if (summary) {
132
+ out.push(summary);
133
+ out.push("");
134
+ }
135
+ if (description && description !== summary) {
136
+ out.push(description);
137
+ out.push("");
138
+ }
139
+ if (parameters.length > 0) {
140
+ out.push("### Parameters");
141
+ out.push("");
142
+ out.push("| Name | In | Type | Required | Description |");
143
+ out.push("| --- | --- | --- | --- | --- |");
144
+ for (const p of parameters) {
145
+ const desc = String(p.description ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
146
+ out.push(`| \`${p.name}\` | ${p.in} | \`${p.type}\` | ${p.required ? "yes" : "no"} | ${desc} |`);
147
+ }
148
+ out.push("");
149
+ }
150
+ if (requestBody.length > 0) {
151
+ out.push("### Request body");
152
+ out.push("");
153
+ for (const p of requestBody) {
154
+ out.push(`- \`${p.name}\` (\`${p.type}\`)${p.required ? " — required" : ""}${p.description ? ` — ${p.description}` : ""}`);
155
+ }
156
+ out.push("");
157
+ }
158
+ if (responses.length > 0) {
159
+ out.push("### Responses");
160
+ out.push("");
161
+ for (const r of responses) {
162
+ const status = String(r.status ?? "");
163
+ const desc = String(r.description ?? "");
164
+ out.push(`- **${status}** — ${desc}`);
165
+ }
166
+ out.push("");
167
+ }
168
+ if (codeSamples.length > 0) {
169
+ out.push("### Code samples");
170
+ out.push("");
171
+ for (const s of codeSamples) {
172
+ const lang = String(s.language ?? "");
173
+ const label = s.label ? ` (${s.label})` : "";
174
+ out.push(`#### ${lang}${label}`);
175
+ out.push("");
176
+ out.push("```" + lang);
177
+ out.push(String(s.code ?? ""));
178
+ out.push("```");
179
+ out.push("");
180
+ }
181
+ }
182
+ return out.join("\n").trimEnd();
183
+ }
184
+ // ── Node renderers ──────────────────────────────────────────────────────
185
+ function renderHeading(node) {
186
+ const level = Math.min(6, Math.max(1, Number(node.props?.level) || 1));
187
+ const hashes = "#".repeat(level);
188
+ const text = inlineToDogsbayMd(node.inline) || (node.html ?? "");
189
+ const attrs = splitProps(node.props, ["level", "slug"]);
190
+ // Include slug as id if provided and not auto-generated
191
+ const slug = node.props?.slug;
192
+ if (slug && !attrs.id) {
193
+ attrs.id = slug;
194
+ }
195
+ const attrStr = renderAttrs(attrs);
196
+ return attrStr ? `${hashes} ${text} ${attrStr}` : `${hashes} ${text}`;
197
+ }
198
+ function renderParagraph(node, ctx) {
199
+ if (node.inline && node.inline.length > 0) {
200
+ return inlineToDogsbayMd(node.inline);
201
+ }
202
+ // Nested children (e.g. Starlight produces paragraph > prose structure)
203
+ if (node.children && node.children.length > 0 && ctx) {
204
+ return node.children
205
+ .map((c) => renderNode(c, ctx))
206
+ .filter(Boolean)
207
+ .join("");
208
+ }
209
+ if (node.html) {
210
+ return node.html;
211
+ }
212
+ return "";
213
+ }
214
+ function renderCode(node) {
215
+ const content = (node.html ?? node.props?.code ?? "");
216
+ const lang = (node.props?.language ?? node.props?.lang ?? "");
217
+ const fence = pickCodeFence(content);
218
+ // Code-block attribute set: title + Expressive-Code-style decorations
219
+ // (highlights, ins, del, mark, collapse, lineNumbers, frame) plus standard
220
+ // passthrough (id, class). Emitted via {attrs} after the language so the
221
+ // parser reads them via markdown-it-attrs.
222
+ const attrKeys = [
223
+ "title", "highlights", "lineNumbers",
224
+ "ins", "del", "mark", "collapse", "frame",
225
+ ];
226
+ const attrParts = [];
227
+ if (node.props?.id)
228
+ attrParts.push(`#${node.props.id}`);
229
+ if (node.props?.class) {
230
+ const classes = String(node.props.class).split(/\s+/).filter(Boolean);
231
+ for (const c of classes)
232
+ attrParts.push(`.${c}`);
233
+ }
234
+ for (const key of attrKeys) {
235
+ const v = node.props?.[key];
236
+ if (v === undefined || v === null || v === false)
237
+ continue;
238
+ if (v === true) {
239
+ attrParts.push(key);
240
+ continue;
241
+ }
242
+ const s = String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
243
+ attrParts.push(`${key}="${s}"`);
244
+ }
245
+ let infoLine = lang;
246
+ if (attrParts.length > 0) {
247
+ infoLine = `${infoLine}${infoLine ? " " : ""}{${attrParts.join(" ")}}`;
248
+ }
249
+ return `${fence}${infoLine}\n${content.replace(/\n$/, "")}\n${fence}`;
250
+ }
251
+ function renderBlockquote(node, ctx) {
252
+ const inner = (node.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
253
+ return inner
254
+ .split("\n")
255
+ .map((line) => (line ? `> ${line}` : ">"))
256
+ .join("\n");
257
+ }
258
+ function renderList(node, ctx, ordered) {
259
+ const items = node.children ?? [];
260
+ const start = ordered
261
+ ? (Number(node.props?.start) || 1)
262
+ : 0;
263
+ return items
264
+ .map((item, idx) => {
265
+ const marker = ordered ? `${start + idx}.` : "-";
266
+ const body = renderListItem(item, ctx);
267
+ const [first, ...rest] = body.split("\n");
268
+ const pad = " ".repeat(marker.length + 1);
269
+ const continuation = rest.map((l) => (l ? pad + l : l)).join("\n");
270
+ return `${marker} ${first}${continuation ? "\n" + continuation : ""}`;
271
+ })
272
+ .join("\n");
273
+ }
274
+ function renderListItem(node, ctx) {
275
+ // A list item may have inline content AND/OR block children.
276
+ // Inline content first (the "primary" text), then nested blocks.
277
+ const inline = inlineToDogsbayMd(node.inline);
278
+ const children = (node.children ?? []).map((c) => renderNode(c, ctx)).filter(Boolean);
279
+ if (inline && children.length === 0)
280
+ return inline;
281
+ if (!inline && children.length > 0)
282
+ return children.join("\n\n");
283
+ return [inline, ...children].filter(Boolean).join("\n\n");
284
+ }
285
+ function renderInlineOrChildren(node, ctx) {
286
+ const inline = inlineToDogsbayMd(node.inline);
287
+ const children = (node.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
288
+ return [inline, children].filter(Boolean).join("\n\n");
289
+ }
290
+ // ── Callouts ─────────────────────────────────────────────────────────────
291
+ const GITHUB_CALLOUT_TYPES = new Set([
292
+ "note",
293
+ "tip",
294
+ "important",
295
+ "warning",
296
+ "caution",
297
+ ]);
298
+ function renderCallout(node, ctx) {
299
+ const type = String(node.props?.type ?? "note").toLowerCase();
300
+ const title = node.props?.title;
301
+ const body = (node.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
302
+ // Use GitHub blockquote form for simple standard callouts (no custom title)
303
+ if (ctx.githubCallouts && !title && GITHUB_CALLOUT_TYPES.has(type)) {
304
+ const tag = `[!${type.toUpperCase()}]`;
305
+ return [tag, body]
306
+ .join("\n")
307
+ .split("\n")
308
+ .map((l) => (l ? `> ${l}` : ">"))
309
+ .join("\n");
310
+ }
311
+ // Directive form
312
+ return renderBlockDirective({
313
+ name: type,
314
+ props: node.props,
315
+ excludeProps: ["type"],
316
+ body,
317
+ }, ctx);
318
+ }
319
+ // ── Details / Collapsible ────────────────────────────────────────────────
320
+ function renderDetails(node, ctx) {
321
+ const body = (node.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
322
+ const props = { ...(node.props ?? {}) };
323
+ // Normalize summary→title
324
+ if (props.summary && !props.title) {
325
+ props.title = props.summary;
326
+ delete props.summary;
327
+ }
328
+ return renderBlockDirective({
329
+ name: "details",
330
+ props,
331
+ excludeProps: ["summary"],
332
+ body,
333
+ }, ctx);
334
+ }
335
+ // ── Tables ───────────────────────────────────────────────────────────────
336
+ function renderTable(node, ctx) {
337
+ const children = node.children ?? [];
338
+ // Find thead/tbody/tr children. Table structure in TreeNode is either
339
+ // direct tr children or thead+tbody wrappers.
340
+ let headerRow;
341
+ const bodyRows = [];
342
+ for (const child of children) {
343
+ if (child.type === "thead") {
344
+ const rows = child.children ?? [];
345
+ if (rows.length > 0)
346
+ headerRow = rows[0];
347
+ }
348
+ else if (child.type === "tbody") {
349
+ bodyRows.push(...(child.children ?? []));
350
+ }
351
+ else if (child.type === "tr") {
352
+ if (!headerRow) {
353
+ headerRow = child;
354
+ }
355
+ else {
356
+ bodyRows.push(child);
357
+ }
358
+ }
359
+ }
360
+ if (!headerRow && bodyRows.length === 0)
361
+ return "";
362
+ const firstRow = headerRow ?? bodyRows.shift();
363
+ if (!firstRow)
364
+ return "";
365
+ const headerCells = (firstRow.children ?? []).map((c) => renderTableCell(c));
366
+ const separator = headerCells.map(() => "---");
367
+ const lines = [
368
+ "| " + headerCells.join(" | ") + " |",
369
+ "| " + separator.join(" | ") + " |",
370
+ ];
371
+ for (const row of bodyRows) {
372
+ const cells = (row.children ?? []).map((c) => renderTableCell(c));
373
+ lines.push("| " + cells.join(" | ") + " |");
374
+ }
375
+ return lines.join("\n");
376
+ }
377
+ function renderTableCell(node) {
378
+ // Table cells can carry inline directly (dogsbay-md shape) or via nested
379
+ // paragraph/prose children (Starlight shape). Collapse whatever we find to
380
+ // a single line — GFM tables don't allow block content in cells.
381
+ let text = inlineToDogsbayMd(node.inline);
382
+ if (!text && node.children) {
383
+ const parts = [];
384
+ for (const child of node.children) {
385
+ if (child.inline && child.inline.length > 0) {
386
+ parts.push(inlineToDogsbayMd(child.inline));
387
+ }
388
+ else if (child.html) {
389
+ parts.push(child.html);
390
+ }
391
+ else if (child.children) {
392
+ // Dive one more level (paragraph > prose > inline)
393
+ for (const grand of child.children) {
394
+ if (grand.inline)
395
+ parts.push(inlineToDogsbayMd(grand.inline));
396
+ else if (grand.html)
397
+ parts.push(grand.html);
398
+ }
399
+ }
400
+ }
401
+ text = parts.join(" ");
402
+ }
403
+ if (!text && node.html)
404
+ text = node.html;
405
+ return text.replace(/\|/g, "\\|").replace(/\n/g, " ").trim();
406
+ }
407
+ // ── Other blocks ─────────────────────────────────────────────────────────
408
+ function renderFigure(node, ctx) {
409
+ const caption = node.props?.caption;
410
+ const body = (node.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
411
+ if (!caption)
412
+ return body;
413
+ const fence = pickDirectiveFence(body);
414
+ return `${fence}figure${renderAttrs({ props: { caption } })}\n${body}\n${fence}`;
415
+ }
416
+ function renderMathBlock(node) {
417
+ const content = (node.html ?? node.props?.latex ?? "");
418
+ return `$$\n${content.replace(/\n$/, "")}\n$$`;
419
+ }
420
+ function renderBlockImage(node) {
421
+ const src = String(node.props?.src ?? "");
422
+ const alt = String(node.props?.alt ?? "");
423
+ const title = node.props?.title;
424
+ const titleStr = title ? ` "${title.replace(/"/g, '\\"')}"` : "";
425
+ const attrs = splitProps(node.props, ["src", "alt", "title"]);
426
+ const attrStr = renderAttrs(attrs);
427
+ const base = `![${alt}](${src}${titleStr})`;
428
+ return attrStr ? `${base}${attrStr}` : base;
429
+ }
430
+ // ── Tabs ─────────────────────────────────────────────────────────────────
431
+ function renderTabs(node, ctx) {
432
+ const children = node.children ?? [];
433
+ // Two shapes: tab children (tab flavour) OR raw child nodes representing panels
434
+ const tabs = children.filter((c) => c.type === "tab");
435
+ if (tabs.length === 0) {
436
+ // No explicit tab children — fall back to wrapping whatever's inside
437
+ const body = children.map((c) => renderNode(c, ctx)).join("\n\n");
438
+ return renderBlockDirective({ name: "tabs", props: node.props, body }, ctx);
439
+ }
440
+ // Definition list form (canonical)
441
+ const items = tabs.map((tab) => renderTabItem(tab, ctx));
442
+ const body = items.join("\n\n");
443
+ return renderBlockDirective({ name: "tabs", props: node.props, body }, ctx);
444
+ }
445
+ function renderTabItem(node, ctx) {
446
+ const label = String(node.props?.label ?? node.props?.title ?? "Tab");
447
+ const content = (node.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
448
+ // Definition list: term on one line, `: body` with continuation indented
449
+ const [first, ...rest] = content.split("\n");
450
+ const pad = " "; // 4 spaces to continue dd content
451
+ const restLines = rest.map((l) => (l ? pad + l : l)).join("\n");
452
+ return `${label}\n: ${first}${restLines ? "\n" + restLines : ""}`;
453
+ }
454
+ // ── Steps ────────────────────────────────────────────────────────────────
455
+ function renderSteps(node, ctx) {
456
+ const children = node.children ?? [];
457
+ const stepChildren = children.filter((c) => c.type === "step");
458
+ // Wrap steps in an ordered list. If children are already list-items, use as-is.
459
+ let list;
460
+ if (stepChildren.length > 0) {
461
+ list = {
462
+ type: "ordered-list",
463
+ children: stepChildren.map((step) => ({
464
+ type: "list-item",
465
+ inline: step.inline,
466
+ children: step.children,
467
+ })),
468
+ };
469
+ }
470
+ else {
471
+ // Fall back: direct children get wrapped as ordered list items
472
+ list = { type: "ordered-list", children };
473
+ }
474
+ const body = renderList(list, ctx, true);
475
+ return renderBlockDirective({ name: "steps", props: node.props, body }, ctx);
476
+ }
477
+ // ── Cards (grid of card nodes) ───────────────────────────────────────────
478
+ function renderCards(node, ctx) {
479
+ const children = node.children ?? [];
480
+ const allCards = children.length > 0 && children.every((c) => c.type === "card");
481
+ if (!allCards) {
482
+ // Mixed content inside :::cards is probably a mis-tagged :::grid — warn
483
+ // via unknown-node heuristic: pass through children but with grid semantics.
484
+ // We keep the :::cards name since that's what the source said.
485
+ const body = children.map((c) => renderNode(c, ctx)).filter(Boolean).join("\n\n");
486
+ return renderBlockDirective({ name: "cards", props: node.props, body }, ctx);
487
+ }
488
+ // List form — each card as a bulleted list item with a bold link
489
+ const items = children.map((card) => renderCardListItem(card));
490
+ const body = items.join("\n\n");
491
+ return renderBlockDirective({ name: "cards", props: node.props, body }, ctx);
492
+ }
493
+ function renderCardListItem(node) {
494
+ const title = String(node.props?.title ?? "");
495
+ const href = node.props?.href;
496
+ // Description may live on props.description (our convention) or in the
497
+ // card's children (Starlight convention puts it as a paragraph child).
498
+ const description = node.props?.description
499
+ ?? extractCardDescriptionText(node);
500
+ const titleText = href ? `**[${title}](${href})**` : `**${title}**`;
501
+ const attrs = splitProps(node.props, ["title", "href", "description"]);
502
+ const attrStr = renderAttrs(attrs);
503
+ const header = attrStr ? `${titleText}${attrStr}` : titleText;
504
+ if (description) {
505
+ // Description may itself contain newlines; collapse to single paragraph
506
+ const cleaned = description.replace(/\s+/g, " ").trim();
507
+ return `- ${header}\n ${cleaned}`;
508
+ }
509
+ return `- ${header}`;
510
+ }
511
+ /**
512
+ * Walk a card's children to find a description paragraph. Handles both
513
+ * direct `paragraph.inline` (our parser) and nested `paragraph > prose > inline`
514
+ * (Starlight importer) shapes.
515
+ */
516
+ function extractCardDescriptionText(node) {
517
+ for (const child of node.children ?? []) {
518
+ if (child.type !== "paragraph")
519
+ continue;
520
+ if (child.inline && child.inline.length > 0) {
521
+ return inlineToDogsbayMd(child.inline);
522
+ }
523
+ if (child.children) {
524
+ for (const grand of child.children) {
525
+ if (grand.inline && grand.inline.length > 0) {
526
+ return inlineToDogsbayMd(grand.inline);
527
+ }
528
+ }
529
+ }
530
+ }
531
+ return undefined;
532
+ }
533
+ // ── Grid (generic layout wrapper) ────────────────────────────────────────
534
+ /**
535
+ * Generic CSS-grid container. Unlike :::cards, grid has no requirement that
536
+ * children be cards — any components can live inside. The grid directive just
537
+ * provides layout semantics (cols, rows, gap).
538
+ */
539
+ function renderGrid(node, ctx) {
540
+ const body = (node.children ?? [])
541
+ .map((c) => renderNode(c, ctx))
542
+ .filter(Boolean)
543
+ .join("\n\n");
544
+ return renderBlockDirective({ name: "grid", props: node.props, body }, ctx);
545
+ }
546
+ /**
547
+ * Per-cell wrapper for column/row spanning inside a :::grid.
548
+ * Round-trips the span/rowSpan/start props.
549
+ *
550
+ * When the cell has no body content (only attrs), emits the leaf directive
551
+ * form `::grid-item{attrs}` — saves the writer from colons-scaling pain
552
+ * and reads more cleanly. Otherwise emits the container form
553
+ * `:::grid-item{attrs}\nbody\n:::`.
554
+ */
555
+ function renderGridItem(node, ctx) {
556
+ const body = (node.children ?? [])
557
+ .map((c) => renderNode(c, ctx))
558
+ .filter(Boolean)
559
+ .join("\n\n");
560
+ if (!body) {
561
+ // Leaf form
562
+ const attrs = splitProps(node.props, []);
563
+ const attrStr = renderAttrs(attrs);
564
+ return `::grid-item${attrStr}`;
565
+ }
566
+ return renderBlockDirective({ name: "grid-item", props: node.props, body }, ctx);
567
+ }
568
+ // ── Single card ──────────────────────────────────────────────────────────
569
+ function renderCard(node, ctx) {
570
+ // Single card may have an inline body or children
571
+ const inline = inlineToDogsbayMd(node.inline);
572
+ const childBody = (node.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
573
+ const body = [inline, childBody].filter(Boolean).join("\n\n");
574
+ return renderBlockDirective({ name: "card", props: node.props, body }, ctx);
575
+ }
576
+ // ── Button ──────────────────────────────────────────────────────────────
577
+ function renderButton(node, ctx) {
578
+ const label = inlineToDogsbayMd(node.inline);
579
+ const childBody = (node.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
580
+ const body = label || childBody || String(node.props?.label ?? "");
581
+ return renderBlockDirective({ name: "button", props: node.props, body, excludeProps: ["label"] }, ctx);
582
+ }
583
+ // ── Definition list ──────────────────────────────────────────────────────
584
+ function renderDeflist(node, ctx) {
585
+ const children = node.children ?? [];
586
+ const parts = [];
587
+ let currentTerm = null;
588
+ for (const child of children) {
589
+ if (child.type === "dt") {
590
+ currentTerm = inlineToDogsbayMd(child.inline) || (child.html ?? "");
591
+ }
592
+ else if (child.type === "dd" && currentTerm !== null) {
593
+ const def = (child.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
594
+ const inline = inlineToDogsbayMd(child.inline);
595
+ const defBody = [inline, def].filter(Boolean).join("\n\n");
596
+ const [first, ...rest] = defBody.split("\n");
597
+ const pad = " ";
598
+ const restIndented = rest.map((l) => (l ? pad + l : l)).join("\n");
599
+ parts.push(`${currentTerm}\n: ${first}${restIndented ? "\n" + restIndented : ""}`);
600
+ }
601
+ }
602
+ return parts.join("\n\n");
603
+ }
604
+ // ── Accordion ────────────────────────────────────────────────────────────
605
+ function renderAccordion(node, ctx) {
606
+ const items = (node.children ?? []).filter((c) => c.type === "accordion-item");
607
+ // Definition list form (canonical) — mirrors `:::tabs` layout.
608
+ if (items.length > 0) {
609
+ const blocks = items.map((item) => renderAccordionDefItem(item, ctx));
610
+ const body = blocks.join("\n\n");
611
+ return renderBlockDirective({ name: "accordion", props: node.props, body }, ctx);
612
+ }
613
+ // Fallback — children that aren't accordion-items get rendered through.
614
+ const body = (node.children ?? [])
615
+ .map((c) => renderNode(c, ctx))
616
+ .filter(Boolean)
617
+ .join("\n\n");
618
+ return renderBlockDirective({ name: "accordion", props: node.props, body }, ctx);
619
+ }
620
+ function renderAccordionDefItem(node, ctx) {
621
+ const label = String(node.props?.label ?? node.props?.title ?? "Item");
622
+ const content = (node.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
623
+ const [first, ...rest] = content.split("\n");
624
+ const pad = " ";
625
+ const restLines = rest.map((l) => (l ? pad + l : l)).join("\n");
626
+ return `${label}\n: ${first}${restLines ? "\n" + restLines : ""}`;
627
+ }
628
+ function renderAccordionItem(node, ctx) {
629
+ // Standalone `:::accordion-item` form (rare — most accordion items live
630
+ // inside `:::accordion`). Round-trips via the directive shell.
631
+ const body = (node.children ?? []).map((c) => renderNode(c, ctx)).join("\n\n");
632
+ return renderBlockDirective({ name: "accordion-item", props: node.props, body }, ctx);
633
+ }
634
+ // ── Link card ────────────────────────────────────────────────────────────
635
+ function renderLinkCard(node, ctx) {
636
+ // Description prop becomes the body when no rich children are present.
637
+ // Round-trips with the parser's `linkCardFromChildren` heuristic.
638
+ const description = node.props?.description;
639
+ const childBody = (node.children ?? [])
640
+ .map((c) => renderNode(c, ctx))
641
+ .filter(Boolean)
642
+ .join("\n\n");
643
+ const body = childBody || (description ?? "");
644
+ return renderBlockDirective({
645
+ name: "link-card",
646
+ props: node.props,
647
+ excludeProps: childBody ? [] : ["description"],
648
+ body,
649
+ }, ctx);
650
+ }
651
+ // ── Avatar ───────────────────────────────────────────────────────────────
652
+ function renderAvatar(node) {
653
+ // Leaf form — `::avatar{src=... alt=... fallback=...}`.
654
+ const attrs = splitProps(node.props, []);
655
+ const attrStr = renderAttrs(attrs);
656
+ return `::avatar${attrStr}`;
657
+ }
658
+ // ── Footnote block ────────────────────────────────────────────────────────
659
+ /**
660
+ * Render a footnote block — emit each child def as a `[^label]: …`
661
+ * line, separated by blank lines. The list itself has no syntax;
662
+ * its children carry the source-level constructs.
663
+ *
664
+ * The per-def `inline` content rendered via `inlineToDogsbayMd` may
665
+ * include `\n` line breaks (multi-paragraph defs, see parser). For
666
+ * the canonical single-line form, we collapse line breaks into
667
+ * indented continuation per CommonMark's link-reference-definition
668
+ * convention.
669
+ */
670
+ function renderFootnoteList(node, ctx) {
671
+ const defs = (node.children ?? []).filter((c) => c.type === "footnote-def");
672
+ return defs.map((def) => renderFootnoteDef(def)).join("\n\n");
673
+ }
674
+ function renderFootnoteDef(node) {
675
+ // Prefer the explicit `label` prop emitted by the parser; fall
676
+ // back to deriving it from the `id` field (`fn-{label}`) for
677
+ // round-tripping TreeNodes constructed by hand or by other
678
+ // importers that don't set the label prop.
679
+ let label = node.props?.label;
680
+ if (!label) {
681
+ const id = node.props?.id ?? "";
682
+ label = id.startsWith("fn-") ? id.slice(3) : id;
683
+ }
684
+ const content = inlineToDogsbayMd(node.inline) || "";
685
+ // Multi-paragraph defs (line breaks in inline) get continuation
686
+ // lines indented by 4 spaces, matching CommonMark's continuation
687
+ // rule for link reference definitions / footnote bodies.
688
+ const [first, ...rest] = content.split("\n");
689
+ if (rest.length === 0) {
690
+ return `[^${label}]: ${first}`;
691
+ }
692
+ const indented = rest.map((l) => (l ? ` ${l}` : "")).join("\n");
693
+ return `[^${label}]: ${first}\n${indented}`;
694
+ }
695
+ function renderUnknown(node, ctx) {
696
+ switch (ctx.unknownNodes) {
697
+ case "skip":
698
+ return "";
699
+ case "comment":
700
+ return `<!-- unsupported: ${node.type} -->`;
701
+ case "html":
702
+ default:
703
+ if (node.html)
704
+ return node.html;
705
+ // Render children if we have any — better than nothing
706
+ if (node.children) {
707
+ return node.children.map((c) => renderNode(c, ctx)).join("\n\n");
708
+ }
709
+ return `<!-- unsupported: ${node.type} -->`;
710
+ }
711
+ }
712
+ //# sourceMappingURL=serialize.js.map