JimLiu-baoyu-skills/skills/baoyu-post-to-wechat/scripts/md-to-wechat.ts

173 lines
4.8 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import {
cleanSummaryText,
extractSummaryFromBody,
extractTitleFromMarkdown,
parseFrontmatter,
renderMarkdownDocument,
replaceMarkdownImagesWithPlaceholders,
resolveColorToken,
resolveContentImages,
serializeFrontmatter,
stripWrappingQuotes,
} from "baoyu-md";
interface ImageInfo {
placeholder: string;
localPath: string;
originalPath: string;
}
interface ParsedResult {
title: string;
author: string;
summary: string;
htmlPath: string;
contentImages: ImageInfo[];
}
export async function convertMarkdown(
markdownPath: string,
options?: { title?: string; theme?: string; color?: string; citeStatus?: boolean },
): Promise<ParsedResult> {
const baseDir = path.dirname(markdownPath);
const content = fs.readFileSync(markdownPath, "utf-8");
const citeStatus = options?.citeStatus ?? true;
const { frontmatter, body } = parseFrontmatter(content);
let title = stripWrappingQuotes(options?.title ?? "")
|| stripWrappingQuotes(frontmatter.title ?? "")
|| extractTitleFromMarkdown(body);
if (!title) {
title = path.basename(markdownPath, path.extname(markdownPath));
}
const author = stripWrappingQuotes(frontmatter.author ?? "");
const frontmatterSummary = stripWrappingQuotes(frontmatter.description ?? "")
|| stripWrappingQuotes(frontmatter.summary ?? "");
let summary = cleanSummaryText(frontmatterSummary);
if (!summary) {
summary = extractSummaryFromBody(body, 120);
}
const { images, markdown: rewrittenBody } = replaceMarkdownImagesWithPlaceholders(
body,
"WECHATIMGPH_",
);
const rewrittenMarkdown = `${serializeFrontmatter(frontmatter)}${rewrittenBody}`;
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "wechat-article-images-"));
const htmlPath = path.join(tempDir, "temp-article.html");
console.error(
`[md-to-wechat] Rendering markdown with theme: ${options?.theme ?? "default"}${options?.color ? `, color: ${options.color}` : ""}, citeStatus: ${citeStatus}`,
);
const { html } = await renderMarkdownDocument(rewrittenMarkdown, {
citeStatus,
defaultTitle: title,
keepTitle: false,
primaryColor: resolveColorToken(options?.color),
theme: options?.theme,
});
fs.writeFileSync(htmlPath, html, "utf-8");
const contentImages = await resolveContentImages(images, baseDir, tempDir, "md-to-wechat");
return {
title,
author,
summary,
htmlPath,
contentImages,
};
}
function printUsage(): never {
console.log(`Convert Markdown to WeChat-ready HTML with image placeholders
Usage:
npx -y bun md-to-wechat.ts <markdown_file> [options]
Options:
--title <title> Override title
--theme <name> Theme name (default, grace, simple, modern)
--color <name|hex> Primary color (blue, green, vermilion, etc. or hex)
--no-cite Disable bottom citations for ordinary external links
--help Show this help
Output JSON format:
{
"title": "Article Title",
"htmlPath": "/tmp/wechat-article-images/temp-article.html",
"contentImages": [
{
"placeholder": "WECHATIMGPH_1",
"localPath": "/tmp/wechat-image/img.png",
"originalPath": "imgs/image.png"
}
]
}
Example:
npx -y bun md-to-wechat.ts article.md
npx -y bun md-to-wechat.ts article.md --theme grace
npx -y bun md-to-wechat.ts article.md --theme modern --color blue
npx -y bun md-to-wechat.ts article.md --no-cite
`);
process.exit(0);
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
printUsage();
}
let markdownPath: string | undefined;
let title: string | undefined;
let theme: string | undefined;
let color: string | undefined;
let citeStatus = true;
for (let i = 0; i < args.length; i++) {
const arg = args[i]!;
if (arg === "--title" && args[i + 1]) {
title = args[++i];
} else if (arg === "--theme" && args[i + 1]) {
theme = args[++i];
} else if (arg === "--color" && args[i + 1]) {
color = args[++i];
} else if (arg === "--cite") {
citeStatus = true;
} else if (arg === "--no-cite") {
citeStatus = false;
} else if (!arg.startsWith("-")) {
markdownPath = arg;
}
}
if (!markdownPath) {
console.error("Error: Markdown file path is required");
process.exit(1);
}
if (!fs.existsSync(markdownPath)) {
console.error(`Error: File not found: ${markdownPath}`);
process.exit(1);
}
const result = await convertMarkdown(markdownPath, { title, theme, color, citeStatus });
console.log(JSON.stringify(result, null, 2));
}
await main().catch((error) => {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});