import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import process from "node:process"; import { extractSummaryFromBody, extractTitleFromMarkdown, parseFrontmatter, pickFirstString, renderMarkdownDocument, replaceMarkdownImagesWithPlaceholders, resolveColorToken, resolveContentImages, resolveImagePath, serializeFrontmatter, stripWrappingQuotes, } from "baoyu-md"; interface ImageInfo { placeholder: string; localPath: string; originalPath: string; alt?: string; } interface ParsedMarkdown { title: string; summary: string; shortSummary: string; coverImage: string | null; contentImages: ImageInfo[]; html: string; } export async function parseMarkdown( markdownPath: string, options?: { coverImage?: string; title?: string; tempDir?: string; theme?: string; color?: string; citeStatus?: boolean; }, ): Promise { const content = fs.readFileSync(markdownPath, "utf-8"); const baseDir = path.dirname(markdownPath); const tempDir = options?.tempDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "weibo-article-images-")); const { frontmatter, body } = parseFrontmatter(content); let title = stripWrappingQuotes(options?.title ?? "") || stripWrappingQuotes(frontmatter.title ?? "") || extractTitleFromMarkdown(body); if (!title) { title = path.basename(markdownPath, path.extname(markdownPath)); } let summary = stripWrappingQuotes(frontmatter.summary ?? "") || stripWrappingQuotes(frontmatter.description ?? "") || stripWrappingQuotes(frontmatter.excerpt ?? ""); if (!summary) { summary = extractSummaryFromBody(body, 44); } const shortSummary = extractSummaryFromBody(body, 44); const coverImagePath = stripWrappingQuotes(options?.coverImage ?? "") || pickFirstString(frontmatter, ["featureImage", "cover_image", "coverImage", "cover", "image"]) || null; const { images, markdown: rewrittenBody } = replaceMarkdownImagesWithPlaceholders( body, "WBIMGPH_", ); const rewrittenMarkdown = `${serializeFrontmatter(frontmatter)}${rewrittenBody}`; const { html } = await renderMarkdownDocument(rewrittenMarkdown, { citeStatus: options?.citeStatus ?? false, defaultTitle: title, keepTitle: false, primaryColor: resolveColorToken(options?.color), theme: options?.theme, }); const contentImages = await resolveContentImages(images, baseDir, tempDir, "md-to-html"); let resolvedCoverImage: string | null = null; if (coverImagePath) { resolvedCoverImage = await resolveImagePath(coverImagePath, baseDir, tempDir, "md-to-html"); } return { title, summary, shortSummary, coverImage: resolvedCoverImage, contentImages, html, }; } async function main(): Promise { const args = process.argv.slice(2); if (args.length === 0 || args.includes("--help") || args.includes("-h")) { console.log(`Convert Markdown to HTML for Weibo article publishing Usage: npx -y bun md-to-html.ts [options] Options: --title Override title --cover <image> Override cover image --output <json|html> Output format (default: json) --html-only Output only the HTML content --save-html <path> Save HTML to file --help Show this help `); process.exit(0); } let markdownPath: string | undefined; let title: string | undefined; let coverImage: string | undefined; let outputFormat: "json" | "html" = "json"; let htmlOnly = false; let saveHtmlPath: string | undefined; for (let i = 0; i < args.length; i++) { const arg = args[i]!; if (arg === "--title" && args[i + 1]) { title = args[++i]; } else if (arg === "--cover" && args[i + 1]) { coverImage = args[++i]; } else if (arg === "--output" && args[i + 1]) { outputFormat = args[++i] as "json" | "html"; } else if (arg === "--html-only") { htmlOnly = true; } else if (arg === "--save-html" && args[i + 1]) { saveHtmlPath = args[++i]; } else if (!arg.startsWith("-")) { markdownPath = arg; } } if (!markdownPath || !fs.existsSync(markdownPath)) { console.error("Error: Valid markdown file path required"); process.exit(1); } const result = await parseMarkdown(markdownPath, { title, coverImage }); if (saveHtmlPath) { fs.writeFileSync(saveHtmlPath, result.html, "utf-8"); console.error(`[md-to-html] HTML saved to: ${saveHtmlPath}`); } if (htmlOnly || outputFormat === "html") { console.log(result.html); } else { console.log(JSON.stringify(result, null, 2)); } } if (import.meta.main ?? (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename ?? ""))) { await main().catch((error) => { console.error(`Error: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); }); }