Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Post text, images, videos, and long-form articles to Weibo (微博) via Chrome browser automation.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/md-to-html.ts
1import fs from "node:fs";2import os from "node:os";3import path from "node:path";4import process from "node:process";56import {7extractSummaryFromBody,8extractTitleFromMarkdown,9parseFrontmatter,10pickFirstString,11preprocessMermaidInMarkdown,12renderMarkdownDocument,13replaceMarkdownImagesWithPlaceholders,14resolveColorToken,15resolveContentImages,16resolveImagePath,17serializeFrontmatter,18stripWrappingQuotes,19} from "baoyu-md";20import { closeRenderer, renderMermaidToPng } from "baoyu-chrome-cdp/mermaid";2122interface ImageInfo {23placeholder: string;24localPath: string;25originalPath: string;26alt?: string;27}2829interface ParsedMarkdown {30title: string;31summary: string;32shortSummary: string;33coverImage: string | null;34contentImages: ImageInfo[];35html: string;36}3738export async function parseMarkdown(39markdownPath: string,40options?: {41coverImage?: string;42title?: string;43tempDir?: string;44theme?: string;45color?: string;46citeStatus?: boolean;47},48): Promise<ParsedMarkdown> {49const content = fs.readFileSync(markdownPath, "utf-8");50const baseDir = path.dirname(markdownPath);51const tempDir = options?.tempDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "weibo-article-images-"));5253const { frontmatter, body } = parseFrontmatter(content);5455let title = stripWrappingQuotes(options?.title ?? "")56|| stripWrappingQuotes(frontmatter.title ?? "")57|| extractTitleFromMarkdown(body);58if (!title) {59title = path.basename(markdownPath, path.extname(markdownPath));60}6162let summary = stripWrappingQuotes(frontmatter.summary ?? "")63|| stripWrappingQuotes(frontmatter.description ?? "")64|| stripWrappingQuotes(frontmatter.excerpt ?? "");65if (!summary) {66summary = extractSummaryFromBody(body, 44);67}68const shortSummary = extractSummaryFromBody(body, 44);6970const coverImagePath = stripWrappingQuotes(options?.coverImage ?? "")71|| pickFirstString(frontmatter, ["featureImage", "cover_image", "coverImage", "cover", "image"])72|| null;7374const { markdown: mermaidProcessedBody, images: mermaidImages } =75await preprocessMermaidInMarkdown(body, {76baseDir,77renderFn: renderMermaidToPng,78onError: (error, block) => {79const message = error instanceof Error ? error.message : String(error);80console.error(81`[md-to-html] mermaid render failed (${block.code.slice(0, 40).replace(/\s+/g, " ")}…): ${message}`,82);83},84});8586if (mermaidImages.length > 0) {87const fresh = mermaidImages.filter((image) => !image.cached).length;88console.error(89`[md-to-html] mermaid: ${mermaidImages.length} block(s), ${fresh} rendered, ${mermaidImages.length - fresh} cached`,90);91}9293const { images, markdown: rewrittenBody } = replaceMarkdownImagesWithPlaceholders(94mermaidProcessedBody,95"WBIMGPH_",96);97const rewrittenMarkdown = `${serializeFrontmatter(frontmatter)}${rewrittenBody}`;9899const { html } = await renderMarkdownDocument(rewrittenMarkdown, {100citeStatus: options?.citeStatus ?? false,101defaultTitle: title,102keepTitle: false,103primaryColor: resolveColorToken(options?.color),104theme: options?.theme,105});106107const contentImages = await resolveContentImages(images, baseDir, tempDir, "md-to-html");108109let resolvedCoverImage: string | null = null;110if (coverImagePath) {111resolvedCoverImage = await resolveImagePath(coverImagePath, baseDir, tempDir, "md-to-html");112}113114return {115title,116summary,117shortSummary,118coverImage: resolvedCoverImage,119contentImages,120html,121};122}123124async function main(): Promise<void> {125const args = process.argv.slice(2);126if (args.length === 0 || args.includes("--help") || args.includes("-h")) {127console.log(`Convert Markdown to HTML for Weibo article publishing128129Usage:130npx -y bun md-to-html.ts <markdown_file> [options]131132Options:133--title <title> Override title134--cover <image> Override cover image135--output <json|html> Output format (default: json)136--html-only Output only the HTML content137--save-html <path> Save HTML to file138--help Show this help139`);140process.exit(0);141}142143let markdownPath: string | undefined;144let title: string | undefined;145let coverImage: string | undefined;146let outputFormat: "json" | "html" = "json";147let htmlOnly = false;148let saveHtmlPath: string | undefined;149150for (let i = 0; i < args.length; i++) {151const arg = args[i]!;152if (arg === "--title" && args[i + 1]) {153title = args[++i];154} else if (arg === "--cover" && args[i + 1]) {155coverImage = args[++i];156} else if (arg === "--output" && args[i + 1]) {157outputFormat = args[++i] as "json" | "html";158} else if (arg === "--html-only") {159htmlOnly = true;160} else if (arg === "--save-html" && args[i + 1]) {161saveHtmlPath = args[++i];162} else if (!arg.startsWith("-")) {163markdownPath = arg;164}165}166167if (!markdownPath || !fs.existsSync(markdownPath)) {168console.error("Error: Valid markdown file path required");169process.exit(1);170}171172const result = await parseMarkdown(markdownPath, { title, coverImage });173174if (saveHtmlPath) {175fs.writeFileSync(saveHtmlPath, result.html, "utf-8");176console.error(`[md-to-html] HTML saved to: ${saveHtmlPath}`);177}178179if (htmlOnly || outputFormat === "html") {180console.log(result.html);181} else {182console.log(JSON.stringify(result, null, 2));183}184}185186if (import.meta.main ?? (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename ?? ""))) {187try {188await main();189} catch (error) {190console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);191process.exitCode = 1;192} finally {193await closeRenderer();194}195}196