From fe3b3d91256cb3b48e5b552cbe3856cf4dc9fbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Liu=20=E5=AE=9D=E7=8E=89?= Date: Fri, 20 Mar 2026 23:17:10 -0500 Subject: [PATCH] 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 --- skills/baoyu-markdown-to-html/SKILL.md | 2 +- .../scripts/main.test.ts | 53 ++++++++ skills/baoyu-markdown-to-html/scripts/main.ts | 120 +++++++++++++----- 3 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 skills/baoyu-markdown-to-html/scripts/main.test.ts diff --git a/skills/baoyu-markdown-to-html/SKILL.md b/skills/baoyu-markdown-to-html/SKILL.md index d3e60e8..0bb55ca 100644 --- a/skills/baoyu-markdown-to-html/SKILL.md +++ b/skills/baoyu-markdown-to-html/SKILL.md @@ -1,6 +1,6 @@ --- 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 metadata: openclaw: diff --git a/skills/baoyu-markdown-to-html/scripts/main.test.ts b/skills/baoyu-markdown-to-html/scripts/main.test.ts new file mode 100644 index 0000000..e5286f0 --- /dev/null +++ b/skills/baoyu-markdown-to-html/scripts/main.test.ts @@ -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 { + 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, /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/, + ); +}); diff --git a/skills/baoyu-markdown-to-html/scripts/main.ts b/skills/baoyu-markdown-to-html/scripts/main.ts index 9b25c56..b95a0c1 100644 --- a/skills/baoyu-markdown-to-html/scripts/main.ts +++ b/skills/baoyu-markdown-to-html/scripts/main.ts @@ -4,16 +4,22 @@ import path from "node:path"; import process from "node:process"; import { + COLOR_PRESETS, + FONT_FAMILY_MAP, + FONT_SIZE_OPTIONS, + THEME_NAMES, extractSummaryFromBody, extractTitleFromMarkdown, formatTimestamp, + parseArgs, parseFrontmatter, renderMarkdownDocument, replaceMarkdownImagesWithPlaceholders, resolveContentImages, serializeFrontmatter, stripWrappingQuotes, -} from "baoyu-md"; +} from "./vendor/baoyu-md/src/index.ts"; +import type { CliOptions } from "./vendor/baoyu-md/src/types.ts"; interface ImageInfo { placeholder: string; @@ -30,9 +36,13 @@ interface ParsedResult { contentImages: ImageInfo[]; } +type ConvertMarkdownOptions = Partial<Omit<CliOptions, "inputPath">> & { + title?: string; +}; + export async function convertMarkdown( markdownPath: string, - options?: { title?: string; theme?: string; keepTitle?: boolean; citeStatus?: boolean }, + options?: ConvertMarkdownOptions, ): Promise<ParsedResult> { const baseDir = path.dirname(markdownPath); const content = fs.readFileSync(markdownPath, "utf-8"); @@ -56,20 +66,32 @@ export async function convertMarkdown( summary = extractSummaryFromBody(body, 120); } + const effectiveFrontmatter = options?.title + ? { ...frontmatter, title } + : frontmatter; + const { images, markdown: rewrittenBody } = replaceMarkdownImagesWithPlaceholders( body, "MDTOHTMLIMGPH_", ); - const rewrittenMarkdown = `${serializeFrontmatter(frontmatter)}${rewrittenBody}`; + const rewrittenMarkdown = `${serializeFrontmatter(effectiveFrontmatter)}${rewrittenBody}`; console.error( `[markdown-to-html] Rendering with theme: ${theme ?? "default"}, keepTitle: ${keepTitle}, citeStatus: ${citeStatus}`, ); const { html } = await renderMarkdownDocument(rewrittenMarkdown, { + codeTheme: options?.codeTheme, + countStatus: options?.countStatus, citeStatus, defaultTitle: title, + fontFamily: options?.fontFamily, + fontSize: options?.fontSize, + isMacCodeBlock: options?.isMacCodeBlock, + isShowLineNumber: options?.isShowLineNumber, keepTitle, + legend: options?.legend, + primaryColor: options?.primaryColor, 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 Usage: npx -y bun main.ts <markdown_file> [options] Options: - --title <title> Override title - --theme <name> Theme name (default, grace, simple). Default: default - --cite Convert ordinary external links to bottom citations. Default: off - --keep-title Keep the first heading in content. Default: false (removed) - --help Show this help + --title <title> Override title + --theme <name> Theme name (${THEME_NAMES.join(", ")}). Default: default + --color <name|hex> Primary color: ${colorNames} + --font-family <name> Font: ${fontFamilyNames}, or CSS value + --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: HTML file saved to same directory as input markdown file. @@ -142,40 +176,60 @@ Output JSON format: Example: npx -y bun main.ts article.md 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 `); - 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> { const args = process.argv.slice(2); if (args.length === 0 || args.includes("--help") || args.includes("-h")) { - printUsage(); + printUsage(0); } - let markdownPath: string | undefined; - let title: string | undefined; - let theme: string | undefined; - let citeStatus = false; - 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; - } + const { renderArgs, title } = extractTitleArg(args); + const options = parseArgs(renderArgs); + if (!options) { + printUsage(1); } - if (!markdownPath) { - console.error("Error: Markdown file path is required"); + const markdownPath = path.resolve(process.cwd(), options.inputPath); + if (!markdownPath.toLowerCase().endsWith(".md")) { + console.error("Input file must end with .md"); process.exit(1); } @@ -184,7 +238,7 @@ async function main(): Promise<void> { 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)); }