diff --git a/skills/baoyu-markdown-to-html/scripts/md/render.ts b/skills/baoyu-markdown-to-html/scripts/md/render.ts index b703ca7..70755f8 100644 --- a/skills/baoyu-markdown-to-html/scripts/md/render.ts +++ b/skills/baoyu-markdown-to-html/scripts/md/render.ts @@ -1,6 +1,7 @@ #!/usr/bin/env npx tsx import fs from "node:fs"; +import { homedir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import frontMatter from "front-matter"; @@ -40,10 +41,47 @@ const EXTERNAL_THEME_DIR = || path.resolve(path.dirname(EXTERNAL_THEME_CONFIG_PATH), "theme-css"); const FALLBACK_THEMES: ThemeName[] = ["default", "grace", "simple"]; -const DEFAULT_STYLE = { +const FONT_FAMILY_MAP: Record = { + sans: `-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif`, + serif: `Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif`, + mono: `Menlo, Monaco, 'Courier New', monospace`, +}; + +const FONT_SIZE_OPTIONS = ["14px", "15px", "16px", "17px", "18px"]; + +const CODE_BLOCK_THEMES = [ + "1c-light", "a11y-dark", "a11y-light", "agate", "an-old-hope", + "androidstudio", "arduino-light", "arta", "ascetic", + "atom-one-dark-reasonable", "atom-one-dark", "atom-one-light", + "brown-paper", "codepen-embed", "color-brewer", "dark", "default", + "devibeans", "docco", "far", "felipec", "foundation", + "github-dark-dimmed", "github-dark", "github", "gml", "googlecode", + "gradient-dark", "gradient-light", "grayscale", "hybrid", "idea", + "intellij-light", "ir-black", "isbl-editor-dark", "isbl-editor-light", + "kimbie-dark", "kimbie-light", "lightfair", "lioshi", "magula", + "mono-blue", "monokai-sublime", "monokai", "night-owl", "nnfx-dark", + "nnfx-light", "nord", "obsidian", "panda-syntax-dark", + "panda-syntax-light", "paraiso-dark", "paraiso-light", "pojoaque", + "purebasic", "qtcreator-dark", "qtcreator-light", "rainbow", "routeros", + "school-book", "shades-of-purple", "srcery", "stackoverflow-dark", + "stackoverflow-light", "sunburst", "tokyo-night-dark", "tokyo-night-light", + "tomorrow-night-blue", "tomorrow-night-bright", "vs", "vs2015", "xcode", + "xt256", +]; + +const HLJS_CDN_BASE = "https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/npm/highlightjs/11.11.1"; + +interface StyleConfig { + primaryColor: string; + fontFamily: string; + fontSize: string; + foreground: string; + blockquoteBackground: string; +} + +const DEFAULT_STYLE: StyleConfig = { primaryColor: "#0F4C81", - fontFamily: - "-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif", + fontFamily: FONT_FAMILY_MAP.sans!, fontSize: "16px", foreground: "0 0% 3.9%", blockquoteBackground: "#f7f7f7", @@ -490,46 +528,133 @@ function printUsage(): void { console.error( [ "Usage:", - " npx tsx src/md/render.ts [--theme ]", + " npx tsx src/md/render.ts [options]", "", "Options:", - ` --theme Theme name (${THEME_NAMES.join(", ")})`, + ` --theme Theme (${THEME_NAMES.join(", ")})`, + ` --color Primary color (default: ${DEFAULT_STYLE.primaryColor})`, + ` --font-family Font: sans, serif, mono, or CSS value`, + ` --font-size Font size: ${FONT_SIZE_OPTIONS.join(", ")} (default: 16px)`, + ` --code-theme Code highlight theme (default: github)`, + ` --mac-code-block Show Mac-style code block header`, + ` --line-number Show line numbers in code blocks`, + ` --cite Enable footnote citations`, + ` --count Show reading time / word count`, + ` --legend Image caption: title-alt, alt-title, title, alt, none`, + ` --keep-title Keep the first heading in output`, ].join("\n") ); } +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 resolveFontFamily(value: string): string { + return FONT_FAMILY_MAP[value] ?? value; +} + function parseArgs(argv: string[]): CliOptions | null { + const ext = loadExtendConfig(); + let inputPath = ""; - let theme: ThemeName = "default"; - let keepTitle = false; + let theme: ThemeName = ext.default_theme ?? "default"; + let keepTitle = ext.keep_title ?? false; + let primaryColor = ext.default_color ?? DEFAULT_STYLE.primaryColor; + let fontFamily = ext.default_font_family ? resolveFontFamily(ext.default_font_family) : DEFAULT_STYLE.fontFamily; + let fontSize = ext.default_font_size ?? DEFAULT_STYLE.fontSize; + let codeTheme = ext.default_code_theme ?? "github"; + let isMacCodeBlock = ext.mac_code_block ?? true; + let isShowLineNumber = ext.show_line_number ?? false; + let citeStatus = ext.cite ?? false; + let countStatus = ext.count ?? false; + let legend = ext.legend ?? "alt"; for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i]; + const arg = argv[i]!; + if (!arg.startsWith("--") && !inputPath) { inputPath = arg; continue; } - if (arg === "--theme") { - theme = (argv[i + 1] || "") as ThemeName; - i += 1; - continue; - } - - if (arg.startsWith("--theme=")) { - theme = arg.slice("--theme=".length) as ThemeName; - continue; - } - - if (arg === "--keep-title") { - keepTitle = true; - continue; - } - if (arg === "--help" || arg === "-h") { return null; } + if (arg === "--keep-title") { keepTitle = true; continue; } + if (arg === "--mac-code-block") { isMacCodeBlock = true; continue; } + if (arg === "--no-mac-code-block") { isMacCodeBlock = false; continue; } + if (arg === "--line-number") { isShowLineNumber = true; continue; } + if (arg === "--cite") { citeStatus = true; continue; } + if (arg === "--count") { countStatus = true; continue; } + + if (arg === "--theme" || arg.startsWith("--theme=")) { + const val = parseArgValue(argv, i, "--theme"); + if (!val) { console.error("Missing value for --theme"); return null; } + theme = val as ThemeName; + if (!arg.includes("=")) i += 1; + continue; + } + + if (arg === "--color" || arg.startsWith("--color=")) { + const val = parseArgValue(argv, i, "--color"); + if (!val) { console.error("Missing value for --color"); return null; } + primaryColor = val; + if (!arg.includes("=")) i += 1; + continue; + } + + if (arg === "--font-family" || arg.startsWith("--font-family=")) { + const val = parseArgValue(argv, i, "--font-family"); + if (!val) { console.error("Missing value for --font-family"); return null; } + fontFamily = resolveFontFamily(val); + if (!arg.includes("=")) i += 1; + continue; + } + + if (arg === "--font-size" || arg.startsWith("--font-size=")) { + const val = parseArgValue(argv, i, "--font-size"); + if (!val) { console.error("Missing value for --font-size"); return null; } + fontSize = val.endsWith("px") ? val : `${val}px`; + if (!FONT_SIZE_OPTIONS.includes(fontSize)) { + console.error(`Invalid font size: ${fontSize}. Valid: ${FONT_SIZE_OPTIONS.join(", ")}`); + return null; + } + if (!arg.includes("=")) i += 1; + continue; + } + + if (arg === "--code-theme" || arg.startsWith("--code-theme=")) { + const val = parseArgValue(argv, i, "--code-theme"); + if (!val) { console.error("Missing value for --code-theme"); return null; } + codeTheme = val; + if (!CODE_BLOCK_THEMES.includes(codeTheme)) { + console.error(`Unknown code theme: ${codeTheme}`); + return null; + } + if (!arg.includes("=")) i += 1; + continue; + } + + if (arg === "--legend" || arg.startsWith("--legend=")) { + const val = parseArgValue(argv, i, "--legend"); + if (!val) { console.error("Missing value for --legend"); return null; } + const valid = ["title-alt", "alt-title", "title", "alt", "none"]; + if (!valid.includes(val)) { + console.error(`Invalid legend: ${val}. Valid: ${valid.join(", ")}`); + return null; + } + legend = val; + if (!arg.includes("=")) i += 1; + continue; + } + console.error(`Unknown argument: ${arg}`); return null; } @@ -544,9 +669,8 @@ function parseArgs(argv: string[]): CliOptions | null { } return { - inputPath, - theme, - keepTitle, + inputPath, theme, keepTitle, primaryColor, fontFamily, fontSize, + codeTheme, isMacCodeBlock, isShowLineNumber, citeStatus, countStatus, legend, }; } @@ -554,6 +678,78 @@ interface CliOptions { inputPath: string; theme: ThemeName; keepTitle: boolean; + primaryColor: string; + fontFamily: string; + fontSize: string; + codeTheme: string; + isMacCodeBlock: boolean; + isShowLineNumber: boolean; + citeStatus: boolean; + countStatus: boolean; + legend: string; +} + +interface ExtendConfig { + default_theme: string | null; + default_color: string | null; + default_font_family: string | null; + default_font_size: string | null; + default_code_theme: string | null; + mac_code_block: boolean | null; + show_line_number: boolean | null; + cite: boolean | null; + count: boolean | null; + legend: string | null; + keep_title: boolean | null; +} + +function extractYamlFrontMatter(content: string): string | null { + const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*$/m); + return match ? match[1]! : null; +} + +function parseExtendYaml(yaml: string): Partial { + const config: Partial = {}; + for (const line of yaml.split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const colonIdx = trimmed.indexOf(":"); + if (colonIdx < 0) continue; + const key = trimmed.slice(0, colonIdx).trim(); + let value = trimmed.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, ""); + if (value === "null" || value === "") continue; + + if (key === "default_theme") config.default_theme = value; + else if (key === "default_color") config.default_color = value; + else if (key === "default_font_family") config.default_font_family = value; + else if (key === "default_font_size") config.default_font_size = value.endsWith("px") ? value : `${value}px`; + else if (key === "default_code_theme") config.default_code_theme = value; + else if (key === "mac_code_block") config.mac_code_block = value === "true"; + else if (key === "show_line_number") config.show_line_number = value === "true"; + else if (key === "cite") config.cite = value === "true"; + else if (key === "count") config.count = value === "true"; + else if (key === "legend") config.legend = value; + else if (key === "keep_title") config.keep_title = value === "true"; + } + return config; +} + +function loadExtendConfig(): Partial { + const paths = [ + path.join(process.cwd(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"), + path.join(homedir(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"), + ]; + for (const p of paths) { + try { + const content = fs.readFileSync(p, "utf-8"); + const yaml = extractYamlFrontMatter(content); + if (!yaml) continue; + return parseExtendYaml(yaml); + } catch { + continue; + } + } + return {}; } function preprocessCjkEmphasis(markdown: string): string { @@ -679,14 +875,14 @@ function loadThemeCss(theme: ThemeName): { }; } -function buildCss(baseCss: string, themeCss: string): string { +function buildCss(baseCss: string, themeCss: string, style: StyleConfig = DEFAULT_STYLE): string { const variables = ` :root { - --md-primary-color: ${DEFAULT_STYLE.primaryColor}; - --md-font-family: ${DEFAULT_STYLE.fontFamily}; - --md-font-size: ${DEFAULT_STYLE.fontSize}; - --foreground: ${DEFAULT_STYLE.foreground}; - --blockquote-background: ${DEFAULT_STYLE.blockquoteBackground}; + --md-primary-color: ${style.primaryColor}; + --md-font-family: ${style.fontFamily}; + --md-font-size: ${style.fontSize}; + --foreground: ${style.foreground}; + --blockquote-background: ${style.blockquoteBackground}; } body { @@ -708,13 +904,28 @@ function normalizeThemeCss(css: string): string { return stripOutputScope(css); } +async function fetchCodeThemeCss(themeName: string): Promise { + const url = `${HLJS_CDN_BASE}/styles/${themeName}.min.css`; + try { + const res = await fetch(url); + if (!res.ok) { + console.error(`Failed to fetch code theme CSS: ${res.status} ${url}`); + return ""; + } + return await res.text(); + } catch (error) { + console.error(`Failed to fetch code theme CSS from ${url}:`, error); + return ""; + } +} + interface HtmlDocumentMeta { title: string; author?: string; description?: string; } -function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string): string { +function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string, codeThemeCss?: string): string { const lines = [ "", "", @@ -729,8 +940,11 @@ function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string): s if (meta.description) { lines.push(` `); } + lines.push(` `); + if (codeThemeCss) { + lines.push(` `); + } lines.push( - ` `, "", "", '
', @@ -758,12 +972,12 @@ async function inlineCss(html: string): Promise { } } -function normalizeCssText(cssText: string): string { +function normalizeCssText(cssText: string, style: StyleConfig = DEFAULT_STYLE): string { return cssText - .replace(/var\(--md-primary-color\)/g, DEFAULT_STYLE.primaryColor) - .replace(/var\(--md-font-family\)/g, DEFAULT_STYLE.fontFamily) - .replace(/var\(--md-font-size\)/g, DEFAULT_STYLE.fontSize) - .replace(/var\(--blockquote-background\)/g, DEFAULT_STYLE.blockquoteBackground) + .replace(/var\(--md-primary-color\)/g, style.primaryColor) + .replace(/var\(--md-font-family\)/g, style.fontFamily) + .replace(/var\(--md-font-size\)/g, style.fontSize) + .replace(/var\(--blockquote-background\)/g, style.blockquoteBackground) .replace(/hsl\(var\(--foreground\)\)/g, "#3f3f3f") .replace(/--md-primary-color:\s*[^;"']+;?/g, "") .replace(/--md-font-family:\s*[^;"']+;?/g, "") @@ -772,20 +986,20 @@ function normalizeCssText(cssText: string): string { .replace(/--foreground:\s*[^;"']+;?/g, ""); } -function normalizeInlineCss(html: string): string { +function normalizeInlineCss(html: string, style: StyleConfig = DEFAULT_STYLE): string { let output = html; output = output.replace( /]*)>([\s\S]*?)<\/style>/gi, (_match, attrs: string, cssText: string) => - `${normalizeCssText(cssText)}` + `${normalizeCssText(cssText, style)}` ); output = output.replace( /style="([^"]*)"/gi, - (_match, cssText: string) => `style="${normalizeCssText(cssText)}"` + (_match, cssText: string) => `style="${normalizeCssText(cssText, style)}"` ); output = output.replace( /style='([^']*)'/gi, - (_match, cssText: string) => `style='${normalizeCssText(cssText)}'` + (_match, cssText: string) => `style='${normalizeCssText(cssText, style)}'` ); return output; } @@ -824,11 +1038,27 @@ async function main(): Promise { options.inputPath.replace(/\.md$/i, ".html") ); + const style: StyleConfig = { + ...DEFAULT_STYLE, + primaryColor: options.primaryColor, + fontFamily: options.fontFamily, + fontSize: options.fontSize, + }; + const { baseCss, themeCss } = loadThemeCss(options.theme); - const css = normalizeThemeCss(buildCss(baseCss, themeCss)); + const css = normalizeThemeCss(buildCss(baseCss, themeCss, style)); + + const codeThemeCss = await fetchCodeThemeCss(options.codeTheme); + const markdown = fs.readFileSync(inputPath, "utf-8"); - const renderer = initRenderer({}); + const renderer = initRenderer({ + legend: options.legend, + citeStatus: options.citeStatus, + countStatus: options.countStatus, + isMacCodeBlock: options.isMacCodeBlock, + isShowLineNumber: options.isShowLineNumber, + }); const { yamlData } = renderer.parseFrontMatterAndContent(markdown); const { html: baseHtml, readingTime: readingTimeResult } = renderMarkdown( markdown, @@ -855,8 +1085,8 @@ async function main(): Promise { author: stripQuotes(yamlData.author), description: stripQuotes(yamlData.description) || stripQuotes(yamlData.summary), }; - const html = buildHtmlDocument(meta, css, content); - const inlinedHtml = normalizeInlineCss(await inlineCss(html)); + const html = buildHtmlDocument(meta, css, content, codeThemeCss); + const inlinedHtml = normalizeInlineCss(await inlineCss(html), style); const finalHtml = modifyHtmlStructure(inlinedHtml); let backupPath = ""; diff --git a/skills/baoyu-markdown-to-html/scripts/md/themes/base.css b/skills/baoyu-markdown-to-html/scripts/md/themes/base.css index 51ea92b..3ec1fe4 100644 --- a/skills/baoyu-markdown-to-html/scripts/md/themes/base.css +++ b/skills/baoyu-markdown-to-html/scripts/md/themes/base.css @@ -32,3 +32,8 @@ blockquote { #output section > :first-child { margin-top: 0 !important; } + +.mermaid-diagram .nodeLabel p { + color: unset !important; + letter-spacing: unset !important; +} diff --git a/skills/baoyu-markdown-to-html/scripts/md/themes/default.css b/skills/baoyu-markdown-to-html/scripts/md/themes/default.css index e30fcbf..b9982f6 100644 --- a/skills/baoyu-markdown-to-html/scripts/md/themes/default.css +++ b/skills/baoyu-markdown-to-html/scripts/md/themes/default.css @@ -269,6 +269,7 @@ pre.code__pre, padding: 0 !important; line-height: 1.5; margin: 10px 8px; + box-shadow: inset 0 0 10px rgba(0,0,0,0.05); } /* ==================== 图片 ==================== */ diff --git a/skills/baoyu-markdown-to-html/scripts/md/themes/orange.css b/skills/baoyu-markdown-to-html/scripts/md/themes/orange.css index 70d4fcc..6fb764c 100644 --- a/skills/baoyu-markdown-to-html/scripts/md/themes/orange.css +++ b/skills/baoyu-markdown-to-html/scripts/md/themes/orange.css @@ -114,6 +114,7 @@ blockquote > p { pre.code__pre, .hljs.code__pre { border-radius: 10px; + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05); } /* ==================== 图片 ==================== */ diff --git a/skills/baoyu-markdown-to-html/scripts/md/themes/red.css b/skills/baoyu-markdown-to-html/scripts/md/themes/red.css index 32e8747..2e88e79 100644 --- a/skills/baoyu-markdown-to-html/scripts/md/themes/red.css +++ b/skills/baoyu-markdown-to-html/scripts/md/themes/red.css @@ -114,6 +114,7 @@ pre.code__pre, .hljs.code__pre { font-size: 13px; border: 1px solid #D4AC0D; + box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05); } /* ==================== 图片 ==================== */