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,11renderMarkdownDocument,12replaceMarkdownImagesWithPlaceholders,13resolveColorToken,14resolveContentImages,15serializeFrontmatter,16stripWrappingQuotes,17} from "baoyu-md";1819interface ImageInfo {20placeholder: string;21localPath: string;22originalPath: string;23}2425interface ParsedResult {26title: string;27author: string;28summary: string;29htmlPath: string;30contentImages: ImageInfo[];31}3233export async function convertMarkdown(34markdownPath: string,35options?: { title?: string; theme?: string; color?: string; citeStatus?: boolean },36): Promise<ParsedResult> {37const baseDir = path.dirname(markdownPath);38const content = fs.readFileSync(markdownPath, "utf-8");39const citeStatus = options?.citeStatus ?? true;4041const { frontmatter, body } = parseFrontmatter(content);4243let title = stripWrappingQuotes(options?.title ?? "")44|| stripWrappingQuotes(frontmatter.title ?? "")45|| extractTitleFromMarkdown(body);46if (!title) {47title = path.basename(markdownPath, path.extname(markdownPath));48}4950const author = stripWrappingQuotes(frontmatter.author ?? "");51const frontmatterSummary = stripWrappingQuotes(frontmatter.description ?? "")52|| stripWrappingQuotes(frontmatter.summary ?? "");53let summary = cleanSummaryText(frontmatterSummary);54if (!summary) {55summary = extractSummaryFromBody(body, 120);56}5758const { images, markdown: rewrittenBody } = replaceMarkdownImagesWithPlaceholders(59body,60"WECHATIMGPH_",61);62const rewrittenMarkdown = `${serializeFrontmatter(frontmatter)}${rewrittenBody}`;6364const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "wechat-article-images-"));65const htmlPath = path.join(tempDir, "temp-article.html");6667console.error(68`[md-to-wechat] Rendering markdown with theme: ${options?.theme ?? "default"}${options?.color ? `, color: ${options.color}` : ""}, citeStatus: ${citeStatus}`,69);7071const { html } = await renderMarkdownDocument(rewrittenMarkdown, {72citeStatus,73defaultTitle: title,74keepTitle: false,75primaryColor: resolveColorToken(options?.color),76theme: options?.theme,77});78fs.writeFileSync(htmlPath, html, "utf-8");7980const contentImages = await resolveContentImages(images, baseDir, tempDir, "md-to-wechat");8182return {83title,84author,85summary,86htmlPath,87contentImages,88};89}9091function printUsage(): never {92console.log(`Convert Markdown to WeChat-ready HTML with image placeholders9394Usage:95npx -y bun md-to-wechat.ts <markdown_file> [options]9697Options:98--title <title> Override title99--theme <name> Theme name (default, grace, simple, modern)100--color <name|hex> Primary color (blue, green, vermilion, etc. or hex)101--no-cite Disable bottom citations for ordinary external links102--help Show this help103104Output JSON format:105{106"title": "Article Title",107"htmlPath": "/tmp/wechat-article-images/temp-article.html",108"contentImages": [109{110"placeholder": "WECHATIMGPH_1",111"localPath": "/tmp/wechat-image/img.png",112"originalPath": "imgs/image.png"113}114]115}116117Example:118npx -y bun md-to-wechat.ts article.md119npx -y bun md-to-wechat.ts article.md --theme grace120npx -y bun md-to-wechat.ts article.md --theme modern --color blue121npx -y bun md-to-wechat.ts article.md --no-cite122`);123process.exit(0);124}125126async function main(): Promise<void> {127const args = process.argv.slice(2);128if (args.length === 0 || args.includes("--help") || args.includes("-h")) {129printUsage();130}131132let markdownPath: string | undefined;133let title: string | undefined;134let theme: string | undefined;135let color: string | undefined;136let citeStatus = true;137138for (let i = 0; i < args.length; i++) {139const arg = args[i]!;140if (arg === "--title" && args[i + 1]) {141title = args[++i];142} else if (arg === "--theme" && args[i + 1]) {143theme = args[++i];144} else if (arg === "--color" && args[i + 1]) {145color = args[++i];146} else if (arg === "--cite") {147citeStatus = true;148} else if (arg === "--no-cite") {149citeStatus = false;150} else if (!arg.startsWith("-")) {151markdownPath = arg;152}153}154155if (!markdownPath) {156console.error("Error: Markdown file path is required");157process.exit(1);158}159160if (!fs.existsSync(markdownPath)) {161console.error(`Error: File not found: ${markdownPath}`);162process.exit(1);163}164165const result = await convertMarkdown(markdownPath, { title, theme, color, citeStatus });166console.log(JSON.stringify(result, null, 2));167}168169await main().catch((error) => {170console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);171process.exit(1);172});173