/** * 集合 → Markdown 接口文档。 */ import type { Collection, CollectionNode, HttpRequestData } from "../model"; import { buildUrlWithQuery } from "./curl"; export function exportMarkdown(collection: Collection): string { const lines: string[] = []; lines.push(`# ${collection.name}`, ""); if (collection.description) lines.push(collection.description, ""); lines.push("> 由 OpsxAPI 导出 · " + new Date().toLocaleString("zh-CN"), ""); const toc: string[] = []; const visitToc = (nodes: CollectionNode[], prefix: string) => { for (const node of nodes) { if (node.type === "folder") { toc.push(`${prefix}- 📁 ${node.name}`); visitToc(node.children, `${prefix} `); } else { toc.push(`${prefix}- [${node.name}](#${anchor(node.name)})`); } } }; visitToc(collection.children, ""); if (toc.length) lines.push("## 目录", "", ...toc, ""); const visit = (nodes: CollectionNode[], depth: number) => { for (const node of nodes) { const hashes = "#".repeat(Math.min(depth + 1, 6)); if (node.type === "folder") { lines.push(`${hashes} ${node.name}`, ""); if (node.description) lines.push(node.description, ""); visit(node.children, depth + 1); continue; } lines.push(`${hashes} ${node.name}`, ""); if (node.description) lines.push(node.description, ""); if (node.kind === "http") { const d: HttpRequestData = node.data; lines.push( "", `- **Method**:\`${d.method}\``, `- **URL**:\`${buildUrlWithQuery(d)}\``, ); if (d.auth.mode !== "none") { lines.push(`- **鉴权**:\`${d.auth.mode}\``); } if (d.query.filter((q) => q.enabled && q.key).length) { lines.push("", "**Query 参数**", "", "| 参数 | 示例值 | 说明 |", "| --- | --- | --- |"); for (const q of d.query) { if (!q.enabled || !q.key) continue; lines.push(`| \`${q.key}\` | \`${escapeTable(q.value)}\` | ${escapeTable(q.description ?? "")} |`); } } if (d.headers.filter((h) => h.enabled && h.key).length) { lines.push("", "**Headers**", "", "| Header | 示例值 | 说明 |", "| --- | --- | --- |"); for (const h of d.headers) { if (!h.enabled || !h.key) continue; lines.push(`| \`${h.key}\` | \`${escapeTable(h.value)}\` | ${escapeTable(h.description ?? "")} |`); } } if (d.bodyMode !== "none") { const lang = d.bodyMode === "json" ? "json" : d.bodyMode === "xml" ? "xml" : ""; if (d.bodyMode === "urlencoded" || d.bodyMode === "form-data") { lines.push("", `**请求体(${d.bodyMode})**`, "", "| 字段 | 示例值 |", "| --- | --- |"); for (const f of d.bodyForm) { if (!f.enabled || !f.key) continue; lines.push(`| \`${f.key}\` | \`${escapeTable(f.value)}\` |`); } } else if (d.bodyRaw) { lines.push("", `**请求体(${d.bodyMode})**`, "", "```" + lang, d.bodyRaw, "```"); } } lines.push(""); } else { const d = node.data as { url?: string; brokerUrl?: string }; const url = "url" in d ? d.url : d.brokerUrl ?? ""; lines.push("", `- **协议**:\`${node.kind}\``, `- **地址**:\`${url}\``); lines.push(""); } } }; visit(collection.children, 1); return lines.join("\n"); } function escapeTable(s: string): string { return s.replaceAll("|", "\\|").replaceAll("\n", " "); } function anchor(name: string): string { return name.toLowerCase().replace(/[^\p{L}\p{N}\s-]/gu, "").trim().replace(/\s+/g, "-"); }