import type { JSONContent } from "@tiptap/vue-3"; export function serializeToMarkdown(doc: JSONContent): string { if (doc.type !== "doc") return ""; return serializeNodes(doc.content || []).replace(/\n{3,}/g, "\n\n").trimEnd(); } function serializeNodes(nodes: JSONContent[], listIndent = 0): string { let out = ""; for (let i = 0; i < nodes.length; i++) { out += serializeNode(nodes[i], listIndent); } return out; } function serializeNode(node: JSONContent, listIndent = 0): string { switch (node.type) { case "paragraph": return serializeInline(node.content || []) + "\n\n"; case "heading": { const level = node.attrs?.level ?? 1; const prefix = "#".repeat(level) + " "; return prefix + serializeInline(node.content || []) + "\n\n"; } case "bulletList": return ( serializeBulletList(node.content || [], listIndent) + (listIndent === 0 ? "\n" : "") ); case "orderedList": return ( serializeOrderedList(node.content || [], listIndent, node.attrs?.start ?? 1) + (listIndent === 0 ? "\n" : "") ); case "taskList": return ( serializeTaskList(node.content || [], listIndent) + (listIndent === 0 ? "\n" : "") ); case "listItem": case "taskItem": // Handled by their respective list serializers return ""; case "codeBlock": { const lang = node.attrs?.language || ""; const code = getTextContent(node); return "```" + lang + "\n" + code + "\n```\n\n"; } case "blockquote": { const inner = serializeNodes(node.content || []); return ( inner .trimEnd() .split("\n") .map((line) => "> " + line) .join("\n") + "\n\n" ); } case "horizontalRule": return "---\n\n"; case "hardBreak": return "\n"; case "text": return applyMarks(node.text || "", node.marks || []); default: // Unknown node — try to serialize children if (node.content) { return serializeNodes(node.content); } return node.text || ""; } } function serializeBulletList( items: JSONContent[], indent: number ): string { let out = ""; for (const item of items) { if (item.type !== "listItem") continue; const children = item.content || []; const prefix = " ".repeat(indent) + "- "; out += serializeListItem(children, prefix, indent); } return out; } function serializeOrderedList( items: JSONContent[], indent: number, start: number ): string { let out = ""; let num = start; for (const item of items) { if (item.type !== "listItem") continue; const children = item.content || []; const prefix = " ".repeat(indent) + num + ". "; out += serializeListItem(children, prefix, indent); num++; } return out; } function serializeTaskList(items: JSONContent[], indent: number): string { let out = ""; for (const item of items) { if (item.type !== "taskItem") continue; const checked = item.attrs?.checked ? "x" : " "; const prefix = " ".repeat(indent) + `- [${checked}] `; out += serializeListItem(item.content || [], prefix, indent); } return out; } function serializeListItem( children: JSONContent[], prefix: string, indent: number ): string { let out = ""; for (let i = 0; i < children.length; i++) { const child = children[i]; if (child.type === "paragraph") { if (i === 0) { out += prefix + serializeInline(child.content || []) + "\n"; } else { out += " ".repeat(indent + 1) + serializeInline(child.content || []) + "\n"; } } else if (child.type === "bulletList") { out += serializeBulletList(child.content || [], indent + 1); } else if (child.type === "orderedList") { out += serializeOrderedList( child.content || [], indent + 1, child.attrs?.start ?? 1 ); } else if (child.type === "taskList") { out += serializeTaskList(child.content || [], indent + 1); } else { out += serializeNode(child, indent + 1); } } return out; } function serializeInline(nodes: JSONContent[]): string { let out = ""; for (const node of nodes) { if (node.type === "text") { out += applyMarks(node.text || "", node.marks || []); } else if (node.type === "hardBreak") { out += "\n"; } else if (node.content) { out += serializeInline(node.content); } } return out; } function applyMarks(text: string, marks: NonNullable): string { let result = text; for (const mark of marks) { switch (mark.type) { case "bold": result = `**${result}**`; break; case "italic": result = `_${result}_`; break; case "strike": result = `~~${result}~~`; break; case "code": result = "`" + result + "`"; break; case "link": result = `[${result}](${mark.attrs?.href || ""})`; break; } } return result; } function getTextContent(node: JSONContent): string { if (node.text) return node.text; if (!node.content) return ""; return node.content.map(getTextContent).join(""); }