import type TurndownService from "turndown"; /** Module-level Turndown instance — built lazily on first use. */ let turndownPromise: Promise | undefined; type TurndownListParent = { nodeName: string; getAttribute(name: string): string | null; children: ArrayLike; }; function getTurndown(): Promise { turndownPromise ||= initTurndown(); return turndownPromise; } async function initTurndown(): Promise { const [{ default: TurndownService }, { gfm }] = await Promise.all([ import("turndown"), import("turndown-plugin-gfm"), ]); const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced", bulletListMarker: "-", }); turndown.use(gfm); turndown.addRule("strikethrough", { filter: ["del", "s", "strike"], replacement(content) { return `~~${content}~~`; }, }); turndown.addRule("heading", { filter: ["h1", "h2", "h3", "h4", "h5", "h6"], replacement(content, node) { const level = Number(node.nodeName.charAt(1)); const prefix = "#".repeat(level); const cleaned = content.replace(/\\([.])/g, "$1").trim(); return `\n\n${prefix} ${cleaned}\n\n`; }, }); turndown.addRule("listItem", { filter: "li", replacement(content, node, options) { content = content.replace(/^\n+/, "").replace(/\n+$/, "\n").replace(/\n/gm, "\n "); const parent = node.parentNode as unknown as TurndownListParent | null; let prefix = `${options.bulletListMarker} `; if (parent?.nodeName === "OL") { const start = parent.getAttribute("start"); const index = Array.prototype.indexOf.call(parent.children, node); prefix = `${(start ? Number(start) : 1) + index}. `; } return prefix + content + (node.nextSibling ? "\n" : ""); }, }); return turndown; } /** * Convert HTML to markdown using Turndown with GFM support. * Strips script/style tags before conversion. */ export async function htmlToBasicMarkdown(html: string): Promise { const cleaned = html.replace(//gi, "").replace(//gi, ""); const turndown = await getTurndown(); return turndown.turndown(cleaned).trim(); }