feat(baoyu-markdown-to-html): pass through all rendering options from CLI

- CLI now supports --color, --font-family, --font-size, --code-theme, --mac-code-block, --line-number, --count, --legend
- convertMarkdown accepts full CliOptions instead of limited subset
- Dynamic help text showing available theme/color/font options
This commit is contained in:
Jim Liu 宝玉 2026-03-20 23:17:10 -05:00
parent 105339cf3f
commit fe3b3d9125
3 changed files with 141 additions and 34 deletions

View File

@ -1,6 +1,6 @@
--- ---
name: baoyu-markdown-to-html name: baoyu-markdown-to-html
description: Converts Markdown to styled HTML with WeChat-compatible themes. Supports code highlighting, math, PlantUML, footnotes, alerts, infographics, and optional bottom citations for external links. Use when user asks for "markdown to html", "convert md to html", "md转html", "微信外链转底部引用", or needs styled HTML output from markdown. description: Converts Markdown to styled HTML with WeChat-compatible themes. Supports code highlighting, math, PlantUML, footnotes, alerts, infographics, and optional bottom citations for external links. Use when user asks for "markdown to html", "convert md to html", "md html", "微信外链转底部引用", or needs styled HTML output from markdown.
version: 1.56.1 version: 1.56.1
metadata: metadata:
openclaw: openclaw:

View File

@ -0,0 +1,53 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT_PATH = path.join(SCRIPT_DIR, "main.ts");
async function makeTempDir(prefix: string): Promise<string> {
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
}
test("CLI forwards wrapper title and vendor render options", async () => {
const root = await makeTempDir("baoyu-markdown-to-html-cli-");
const markdownPath = path.join(root, "article.md");
await fs.writeFile(markdownPath, "## Section\n\nParagraph with **bold** text.\n", "utf-8");
const { stdout } = await execFileAsync(
"bun",
[
SCRIPT_PATH,
markdownPath,
"--theme", "grace",
"--color", "red",
"--font-family", "mono",
"--font-size", "18",
"--keep-title",
"--title", "Overridden",
],
{ cwd: SCRIPT_DIR },
);
const result = JSON.parse(stdout.trim()) as {
htmlPath: string;
title: string;
};
assert.equal(result.title, "Overridden");
const html = await fs.readFile(result.htmlPath, "utf-8");
assert.match(html, /<title>Overridden<\/title>/);
assert.match(html, /<h2[^>]*style="[^"]*background: #A93226/);
assert.match(html, /<strong[^>]*style="[^"]*color: #A93226/);
assert.match(
html,
/<body[^>]*style="[^"]*font-family: Menlo, Monaco, 'Courier New', monospace;[^"]*font-size: 18px/,
);
});

View File

@ -4,16 +4,22 @@ import path from "node:path";
import process from "node:process"; import process from "node:process";
import { import {
COLOR_PRESETS,
FONT_FAMILY_MAP,
FONT_SIZE_OPTIONS,
THEME_NAMES,
extractSummaryFromBody, extractSummaryFromBody,
extractTitleFromMarkdown, extractTitleFromMarkdown,
formatTimestamp, formatTimestamp,
parseArgs,
parseFrontmatter, parseFrontmatter,
renderMarkdownDocument, renderMarkdownDocument,
replaceMarkdownImagesWithPlaceholders, replaceMarkdownImagesWithPlaceholders,
resolveContentImages, resolveContentImages,
serializeFrontmatter, serializeFrontmatter,
stripWrappingQuotes, stripWrappingQuotes,
} from "baoyu-md"; } from "./vendor/baoyu-md/src/index.ts";
import type { CliOptions } from "./vendor/baoyu-md/src/types.ts";
interface ImageInfo { interface ImageInfo {
placeholder: string; placeholder: string;
@ -30,9 +36,13 @@ interface ParsedResult {
contentImages: ImageInfo[]; contentImages: ImageInfo[];
} }
type ConvertMarkdownOptions = Partial<Omit<CliOptions, "inputPath">> & {
title?: string;
};
export async function convertMarkdown( export async function convertMarkdown(
markdownPath: string, markdownPath: string,
options?: { title?: string; theme?: string; keepTitle?: boolean; citeStatus?: boolean }, options?: ConvertMarkdownOptions,
): Promise<ParsedResult> { ): Promise<ParsedResult> {
const baseDir = path.dirname(markdownPath); const baseDir = path.dirname(markdownPath);
const content = fs.readFileSync(markdownPath, "utf-8"); const content = fs.readFileSync(markdownPath, "utf-8");
@ -56,20 +66,32 @@ export async function convertMarkdown(
summary = extractSummaryFromBody(body, 120); summary = extractSummaryFromBody(body, 120);
} }
const effectiveFrontmatter = options?.title
? { ...frontmatter, title }
: frontmatter;
const { images, markdown: rewrittenBody } = replaceMarkdownImagesWithPlaceholders( const { images, markdown: rewrittenBody } = replaceMarkdownImagesWithPlaceholders(
body, body,
"MDTOHTMLIMGPH_", "MDTOHTMLIMGPH_",
); );
const rewrittenMarkdown = `${serializeFrontmatter(frontmatter)}${rewrittenBody}`; const rewrittenMarkdown = `${serializeFrontmatter(effectiveFrontmatter)}${rewrittenBody}`;
console.error( console.error(
`[markdown-to-html] Rendering with theme: ${theme ?? "default"}, keepTitle: ${keepTitle}, citeStatus: ${citeStatus}`, `[markdown-to-html] Rendering with theme: ${theme ?? "default"}, keepTitle: ${keepTitle}, citeStatus: ${citeStatus}`,
); );
const { html } = await renderMarkdownDocument(rewrittenMarkdown, { const { html } = await renderMarkdownDocument(rewrittenMarkdown, {
codeTheme: options?.codeTheme,
countStatus: options?.countStatus,
citeStatus, citeStatus,
defaultTitle: title, defaultTitle: title,
fontFamily: options?.fontFamily,
fontSize: options?.fontSize,
isMacCodeBlock: options?.isMacCodeBlock,
isShowLineNumber: options?.isShowLineNumber,
keepTitle, keepTitle,
legend: options?.legend,
primaryColor: options?.primaryColor,
theme, theme,
}); });
@ -111,18 +133,30 @@ export async function convertMarkdown(
}; };
} }
function printUsage(): never { function printUsage(exitCode = 0): never {
const colorNames = Object.keys(COLOR_PRESETS).join(", ");
const fontFamilyNames = Object.keys(FONT_FAMILY_MAP).join(", ");
console.log(`Convert Markdown to styled HTML console.log(`Convert Markdown to styled HTML
Usage: Usage:
npx -y bun main.ts <markdown_file> [options] npx -y bun main.ts <markdown_file> [options]
Options: Options:
--title <title> Override title --title <title> Override title
--theme <name> Theme name (default, grace, simple). Default: default --theme <name> Theme name (${THEME_NAMES.join(", ")}). Default: default
--cite Convert ordinary external links to bottom citations. Default: off --color <name|hex> Primary color: ${colorNames}
--keep-title Keep the first heading in content. Default: false (removed) --font-family <name> Font: ${fontFamilyNames}, or CSS value
--help Show this help --font-size <N> Font size: ${FONT_SIZE_OPTIONS.join(", ")} (default: 16px)
--code-theme <name> Code highlight theme (default: github)
--mac-code-block Show Mac-style code block header
--no-mac-code-block Hide Mac-style code block header
--line-number Show line numbers in code blocks
--cite Convert ordinary external links to bottom citations. Default: off
--count Show reading time / word count
--legend <value> Image caption: title-alt, alt-title, title, alt, none
--keep-title Keep the first heading in content. Default: false (removed)
--help Show this help
Output: Output:
HTML file saved to same directory as input markdown file. HTML file saved to same directory as input markdown file.
@ -142,40 +176,60 @@ Output JSON format:
Example: Example:
npx -y bun main.ts article.md npx -y bun main.ts article.md
npx -y bun main.ts article.md --theme grace npx -y bun main.ts article.md --theme grace
npx -y bun main.ts article.md --theme modern --color red
npx -y bun main.ts article.md --cite npx -y bun main.ts article.md --cite
`); `);
process.exit(0); process.exit(exitCode);
}
function parseArgValue(argv: string[], i: number, flag: string): string | null {
const arg = argv[i]!;
if (arg.includes("=")) {
return arg.slice(flag.length + 1);
}
const next = argv[i + 1];
return next ?? null;
}
function extractTitleArg(argv: string[]): { renderArgs: string[]; title?: string } {
let title: string | undefined;
const renderArgs: string[] = [];
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i]!;
if (arg === "--title" || arg.startsWith("--title=")) {
const value = parseArgValue(argv, i, "--title");
if (!value) {
console.error("Missing value for --title");
printUsage(1);
}
title = value;
if (!arg.includes("=")) {
i += 1;
}
continue;
}
renderArgs.push(arg);
}
return { renderArgs, title };
} }
async function main(): Promise<void> { async function main(): Promise<void> {
const args = process.argv.slice(2); const args = process.argv.slice(2);
if (args.length === 0 || args.includes("--help") || args.includes("-h")) { if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
printUsage(); printUsage(0);
} }
let markdownPath: string | undefined; const { renderArgs, title } = extractTitleArg(args);
let title: string | undefined; const options = parseArgs(renderArgs);
let theme: string | undefined; if (!options) {
let citeStatus = false; printUsage(1);
let keepTitle = false;
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 === "--cite") {
citeStatus = true;
} else if (arg === "--keep-title") {
keepTitle = true;
} else if (!arg.startsWith("-")) {
markdownPath = arg;
}
} }
if (!markdownPath) { const markdownPath = path.resolve(process.cwd(), options.inputPath);
console.error("Error: Markdown file path is required"); if (!markdownPath.toLowerCase().endsWith(".md")) {
console.error("Input file must end with .md");
process.exit(1); process.exit(1);
} }
@ -184,7 +238,7 @@ async function main(): Promise<void> {
process.exit(1); process.exit(1);
} }
const result = await convertMarkdown(markdownPath, { title, theme, keepTitle, citeStatus }); const result = await convertMarkdown(markdownPath, { ...options, title });
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
} }