mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-31 14:19:47 +08:00
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:
@@ -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/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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,7 +133,10 @@ 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:
|
||||||
@@ -119,8 +144,17 @@ Usage:
|
|||||||
|
|
||||||
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
|
||||||
|
--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
|
--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)
|
--keep-title Keep the first heading in content. Default: false (removed)
|
||||||
--help Show this help
|
--help Show this help
|
||||||
|
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user