@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
|
@@ -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 = ``;
|
|
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
|