Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Post articles and image-text content to WeChat Official Account via API or Chrome CDP, with markdown-to-WeChat HTML conversion.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/md-to-wechat.ts
1import fs from "node:fs";2import os from "node:os";3import path from "node:path";4import process from "node:process";56import {7cleanSummaryText,8extractSummaryFromBody,9extractTitleFromMarkdown,10parseFrontmatter,11preprocessMermaidInMarkdown,12renderMarkdownDocument,13replaceMarkdownImagesWithPlaceholders,14resolveColorToken,15resolveContentImages,16serializeFrontmatter,17stripWrappingQuotes,18} from "baoyu-md";19import { closeRenderer, renderMermaidToPng } from "baoyu-chrome-cdp/mermaid";2021interface ImageInfo {22placeholder: string;23localPath: string;24originalPath: string;25alt?: string;26}2728interface ParsedResult {29title: string;30author: string;31summary: string;32htmlPath: string;33contentImages: ImageInfo[];34}3536export async function convertMarkdown(37markdownPath: string,38options?: { title?: string; theme?: string; color?: string; citeStatus?: boolean },39): Promise<ParsedResult> {40const baseDir = path.dirname(markdownPath);41const content = fs.readFileSync(markdownPath, "utf-8");42const citeStatus = options?.citeStatus ?? true;4344const { frontmatter, body } = parseFrontmatter(content);4546let title = stripWrappingQuotes(options?.title ?? "")47|| stripWrappingQuotes(frontmatter.title ?? "")48|| extractTitleFromMarkdown(body);49if (!title) {50title = path.basename(markdownPath, path.extname(markdownPath));51}5253const author = stripWrappingQuotes(frontmatter.author ?? "");54const frontmatterSummary = stripWrappingQuotes(frontmatter.description ?? "")55|| stripWrappingQuotes(frontmatter.summary ?? "");56let summary = cleanSummaryText(frontmatterSummary);57if (!summary) {58summary = extractSummaryFromBody(body, 120);59}6061const { markdown: mermaidProcessedBody, images: mermaidImages } =62await preprocessMermaidInMarkdown(body, {63baseDir,64renderFn: renderMermaidToPng,65onError: (error, block) => {66const message = error instanceof Error ? error.message : String(error);67console.error(68`[md-to-wechat] mermaid render failed (${block.code.slice(0, 40).replace(/\s+/g, " ")}…): ${message}`,69);70},71});7273if (mermaidImages.length > 0) {74const fresh = mermaidImages.filter((image) => !image.cached).length;75console.error(76`[md-to-wechat] mermaid: ${mermaidImages.length} block(s), ${fresh} rendered, ${mermaidImages.length - fresh} cached`,77);78}7980const { images, markdown: rewrittenBody } = replaceMarkdownImagesWithPlaceholders(81mermaidProcessedBody,82"WECHATIMGPH_",83);84const rewrittenMarkdown = `${serializeFrontmatter(frontmatter)}${rewrittenBody}`;8586const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "wechat-article-images-"));87const htmlPath = path.join(tempDir, "temp-article.html");8889console.error(90`[md-to-wechat] Rendering markdown with theme: ${options?.theme ?? "default"}${options?.color ? `, color: ${options.color}` : ""}, citeStatus: ${citeStatus}`,91);9293const { html } = await renderMarkdownDocument(rewrittenMarkdown, {94citeStatus,95defaultTitle: title,96keepTitle: false,97primaryColor: resolveColorToken(options?.color),98theme: options?.theme,99});100fs.writeFileSync(htmlPath, html, "utf-8");101102const contentImages = await resolveContentImages(images, baseDir, tempDir, "md-to-wechat");103104return {105title,106author,107summary,108htmlPath,109contentImages,110};111}112113function printUsage(): never {114console.log(`Convert Markdown to WeChat-ready HTML with image placeholders115116Usage:117npx -y bun md-to-wechat.ts <markdown_file> [options]118119Options:120--title <title> Override title121--theme <name> Theme name (default, grace, simple, modern)122--color <name|hex> Primary color (blue, green, vermilion, etc. or hex)123--no-cite Disable bottom citations for ordinary external links124--help Show this help125126Output JSON format:127{128"title": "Article Title",129"htmlPath": "/tmp/wechat-article-images/temp-article.html",130"contentImages": [131{132"placeholder": "WECHATIMGPH_1",133"localPath": "/tmp/wechat-image/img.png",134"originalPath": "imgs/image.png"135}136]137}138139Example:140npx -y bun md-to-wechat.ts article.md141npx -y bun md-to-wechat.ts article.md --theme grace142npx -y bun md-to-wechat.ts article.md --theme modern --color blue143npx -y bun md-to-wechat.ts article.md --no-cite144`);145process.exit(0);146}147148async function main(): Promise<void> {149const args = process.argv.slice(2);150if (args.length === 0 || args.includes("--help") || args.includes("-h")) {151printUsage();152}153154let markdownPath: string | undefined;155let title: string | undefined;156let theme: string | undefined;157let color: string | undefined;158let citeStatus = true;159160for (let i = 0; i < args.length; i++) {161const arg = args[i]!;162if (arg === "--title" && args[i + 1]) {163title = args[++i];164} else if (arg === "--theme" && args[i + 1]) {165theme = args[++i];166} else if (arg === "--color" && args[i + 1]) {167color = args[++i];168} else if (arg === "--cite") {169citeStatus = true;170} else if (arg === "--no-cite") {171citeStatus = false;172} else if (!arg.startsWith("-")) {173markdownPath = arg;174}175}176177if (!markdownPath) {178console.error("Error: Markdown file path is required");179process.exit(1);180}181182if (!fs.existsSync(markdownPath)) {183console.error(`Error: File not found: ${markdownPath}`);184process.exit(1);185}186187const result = await convertMarkdown(markdownPath, { title, theme, color, citeStatus });188console.log(JSON.stringify(result, null, 2));189}190191try {192await main();193} catch (error) {194console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);195process.exitCode = 1;196} finally {197await closeRenderer();198}199