Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Converts Markdown to styled HTML with WeChat-compatible themes, code highlighting, math, PlantUML, and footnotes.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/main.ts
1import fs from "node:fs";2import os from "node:os";3import path from "node:path";4import process from "node:process";56import {7COLOR_PRESETS,8FONT_FAMILY_MAP,9FONT_SIZE_OPTIONS,10THEME_NAMES,11extractSummaryFromBody,12extractTitleFromMarkdown,13formatTimestamp,14parseArgs,15parseFrontmatter,16renderMarkdownDocument,17replaceMarkdownImagesWithPlaceholders,18resolveContentImages,19serializeFrontmatter,20stripWrappingQuotes,21} from "baoyu-md";22import type { CliOptions } from "baoyu-md";2324interface ImageInfo {25placeholder: string;26localPath: string;27originalPath: string;28}2930interface ParsedResult {31title: string;32author: string;33summary: string;34htmlPath: string;35backupPath?: string;36contentImages: ImageInfo[];37}3839type ConvertMarkdownOptions = Partial<Omit<CliOptions, "inputPath">> & {40title?: string;41};4243export async function convertMarkdown(44markdownPath: string,45options?: ConvertMarkdownOptions,46): Promise<ParsedResult> {47const baseDir = path.dirname(markdownPath);48const content = fs.readFileSync(markdownPath, "utf-8");49const theme = options?.theme;50const keepTitle = options?.keepTitle ?? false;51const citeStatus = options?.citeStatus ?? false;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}6162const author = stripWrappingQuotes(frontmatter.author ?? "");63let summary = stripWrappingQuotes(frontmatter.description ?? "")64|| stripWrappingQuotes(frontmatter.summary ?? "");65if (!summary) {66summary = extractSummaryFromBody(body, 120);67}6869const effectiveFrontmatter = options?.title70? { ...frontmatter, title }71: frontmatter;7273const { images, markdown: rewrittenBody } = replaceMarkdownImagesWithPlaceholders(74body,75"MDTOHTMLIMGPH_",76);77const rewrittenMarkdown = `${serializeFrontmatter(effectiveFrontmatter)}${rewrittenBody}`;7879console.error(80`[markdown-to-html] Rendering with theme: ${theme ?? "default"}, keepTitle: ${keepTitle}, citeStatus: ${citeStatus}`,81);8283const { html } = await renderMarkdownDocument(rewrittenMarkdown, {84codeTheme: options?.codeTheme,85countStatus: options?.countStatus,86citeStatus,87defaultTitle: title,88fontFamily: options?.fontFamily,89fontSize: options?.fontSize,90isMacCodeBlock: options?.isMacCodeBlock,91isShowLineNumber: options?.isShowLineNumber,92keepTitle,93legend: options?.legend,94primaryColor: options?.primaryColor,95theme,96});9798const finalHtmlPath = markdownPath.replace(/\.md$/i, ".html");99let backupPath: string | undefined;100101if (fs.existsSync(finalHtmlPath)) {102backupPath = `${finalHtmlPath}.bak-${formatTimestamp()}`;103console.error(`[markdown-to-html] Backing up existing file to: ${backupPath}`);104fs.renameSync(finalHtmlPath, backupPath);105}106107fs.writeFileSync(finalHtmlPath, html, "utf-8");108109const hasRemoteImages = images.some((image) =>110image.originalPath.startsWith("http://") || image.originalPath.startsWith("https://"),111);112const tempDir = hasRemoteImages113? fs.mkdtempSync(path.join(os.tmpdir(), "markdown-to-html-"))114: baseDir;115const contentImages = await resolveContentImages(images, baseDir, tempDir, "markdown-to-html");116117let finalContent = fs.readFileSync(finalHtmlPath, "utf-8");118for (const image of contentImages) {119const imgTag = `<img src="${image.originalPath}" data-local-path="${image.localPath}" style="display: block; width: 100%; margin: 1.5em auto;">`;120finalContent = finalContent.replace(image.placeholder, imgTag);121}122fs.writeFileSync(finalHtmlPath, finalContent, "utf-8");123124console.error(`[markdown-to-html] HTML saved to: ${finalHtmlPath}`);125126return {127title,128author,129summary,130htmlPath: finalHtmlPath,131backupPath,132contentImages,133};134}135136function printUsage(exitCode = 0): never {137const colorNames = Object.keys(COLOR_PRESETS).join(", ");138const fontFamilyNames = Object.keys(FONT_FAMILY_MAP).join(", ");139140console.log(`Convert Markdown to styled HTML141142Usage:143npx -y bun main.ts <markdown_file> [options]144145Options:146--title <title> Override title147--theme <name> Theme name (${THEME_NAMES.join(", ")}). Default: default148--color <name|hex> Primary color: ${colorNames}149--font-family <name> Font: ${fontFamilyNames}, or CSS value150--font-size <N> Font size: ${FONT_SIZE_OPTIONS.join(", ")} (default: 16px)151--code-theme <name> Code highlight theme (default: github)152--mac-code-block Show Mac-style code block header153--no-mac-code-block Hide Mac-style code block header154--line-number Show line numbers in code blocks155--cite Convert ordinary external links to bottom citations. Default: off156--count Show reading time / word count157--legend <value> Image caption: title-alt, alt-title, title, alt, none158--keep-title Keep the first heading in content. Default: false (removed)159--help Show this help160161Output:162HTML file saved to same directory as input markdown file.163Example: article.md -> article.html164165If HTML file already exists, it will be backed up first:166article.html -> article.html.bak-YYYYMMDDHHMMSS167168Output JSON format:169{170"title": "Article Title",171"htmlPath": "/path/to/article.html",172"backupPath": "/path/to/article.html.bak-20260128180000",173"contentImages": [...]174}175176Example:177npx -y bun main.ts article.md178npx -y bun main.ts article.md --theme grace179npx -y bun main.ts article.md --theme modern --color red180npx -y bun main.ts article.md --cite181`);182process.exit(exitCode);183}184185function parseArgValue(argv: string[], i: number, flag: string): string | null {186const arg = argv[i]!;187if (arg.includes("=")) {188return arg.slice(flag.length + 1);189}190const next = argv[i + 1];191return next ?? null;192}193194function extractTitleArg(argv: string[]): { renderArgs: string[]; title?: string } {195let title: string | undefined;196const renderArgs: string[] = [];197198for (let i = 0; i < argv.length; i += 1) {199const arg = argv[i]!;200if (arg === "--title" || arg.startsWith("--title=")) {201const value = parseArgValue(argv, i, "--title");202if (!value) {203console.error("Missing value for --title");204printUsage(1);205}206title = value;207if (!arg.includes("=")) {208i += 1;209}210continue;211}212renderArgs.push(arg);213}214215return { renderArgs, title };216}217218async function main(): Promise<void> {219const args = process.argv.slice(2);220if (args.length === 0 || args.includes("--help") || args.includes("-h")) {221printUsage(0);222}223224const { renderArgs, title } = extractTitleArg(args);225const options = parseArgs(renderArgs);226if (!options) {227printUsage(1);228}229230const markdownPath = path.resolve(process.cwd(), options.inputPath);231if (!markdownPath.toLowerCase().endsWith(".md")) {232console.error("Input file must end with .md");233process.exit(1);234}235236if (!fs.existsSync(markdownPath)) {237console.error(`Error: File not found: ${markdownPath}`);238process.exit(1);239}240241const result = await convertMarkdown(markdownPath, { ...options, title });242console.log(JSON.stringify(result, null, 2));243}244245await main().catch((error) => {246console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);247process.exit(1);248});249