diff --git a/skills/baoyu-markdown-to-html/scripts/main.ts b/skills/baoyu-markdown-to-html/scripts/main.ts
index 3779caa..274f333 100644
--- a/skills/baoyu-markdown-to-html/scripts/main.ts
+++ b/skills/baoyu-markdown-to-html/scripts/main.ts
@@ -1,13 +1,17 @@
import fs from 'node:fs';
import path from 'node:path';
-import { writeFile } from 'node:fs/promises';
-import os from 'node:os';
import { createHash } from 'node:crypto';
-import { fileURLToPath } from 'node:url';
import https from 'node:https';
import http from 'node:http';
-import { spawnSync } from 'node:child_process';
import process from 'node:process';
+import type { StyleConfig, HtmlDocumentMeta } from './md/types.js';
+import { DEFAULT_STYLE, THEME_STYLE_DEFAULTS } from './md/constants.js';
+import { loadThemeCss, normalizeThemeCss } from './md/themes.js';
+import { initRenderer, renderMarkdown, postProcessHtml } from './md/renderer.js';
+import {
+ buildCss, loadCodeThemeCss, buildHtmlDocument,
+ inlineCss, normalizeInlineCss, modifyHtmlStructure, removeFirstHeading,
+} from './md/html-builder.js';
interface ImageInfo {
placeholder: string;
@@ -187,33 +191,23 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
const modifiedMarkdown = `---\n${Object.entries(frontmatter).map(([k, v]) => `${k}: ${v}`).join('\n')}\n---\n${modifiedBody}`;
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'markdown-to-html-'));
- const tempMdPath = path.join(tempDir, 'temp-article.md');
- await writeFile(tempMdPath, modifiedMarkdown, 'utf-8');
-
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
- const renderScript = path.join(__dirname, 'md', 'render.ts');
-
console.error(`[markdown-to-html] Rendering with theme: ${theme}, keepTitle: ${keepTitle}`);
- const args = ['-y', 'bun', renderScript, tempMdPath, '--theme', theme];
- if (keepTitle) args.push('--keep-title');
+ const themeDefaults = THEME_STYLE_DEFAULTS[theme] ?? {};
+ const style: StyleConfig = { ...DEFAULT_STYLE, ...themeDefaults };
+ const { baseCss, themeCss } = loadThemeCss(theme);
+ const css = normalizeThemeCss(buildCss(baseCss, themeCss, style));
+ const codeThemeCss = loadCodeThemeCss('github');
- const result = spawnSync('npx', args, {
- stdio: ['inherit', 'pipe', 'pipe'],
- cwd: baseDir,
- });
+ const renderer = initRenderer({});
+ const { html: baseHtml, readingTime } = renderMarkdown(modifiedMarkdown, renderer);
+ let htmlContent = postProcessHtml(baseHtml, readingTime, renderer);
+ if (!keepTitle) htmlContent = removeFirstHeading(htmlContent);
- if (result.status !== 0) {
- const stderr = result.stderr?.toString() || '';
- throw new Error(`Render failed: ${stderr}`);
- }
-
- const tempHtmlPath = tempMdPath.replace(/\.md$/i, '.html');
- if (!fs.existsSync(tempHtmlPath)) {
- throw new Error(`HTML file not generated: ${tempHtmlPath}`);
- }
+ const meta: HtmlDocumentMeta = { title, author, description: summary };
+ const fullHtml = buildHtmlDocument(meta, css, htmlContent, codeThemeCss);
+ const inlinedHtml = normalizeInlineCss(await inlineCss(fullHtml), style);
+ const renderedHtml = modifyHtmlStructure(inlinedHtml);
const finalHtmlPath = markdownPath.replace(/\.md$/i, '.html');
let backupPath: string | undefined;
@@ -224,11 +218,16 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
fs.renameSync(finalHtmlPath, backupPath);
}
- fs.copyFileSync(tempHtmlPath, finalHtmlPath);
+ fs.writeFileSync(finalHtmlPath, renderedHtml, 'utf-8');
const contentImages: ImageInfo[] = [];
+ let tempDir: string | undefined;
for (const img of images) {
- const localPath = await resolveImagePath(img.src, baseDir, tempDir);
+ if (!tempDir && (img.src.startsWith('http://') || img.src.startsWith('https://'))) {
+ const os = await import('node:os');
+ tempDir = fs.mkdtempSync(path.join(os.default.tmpdir(), 'markdown-to-html-'));
+ }
+ const localPath = await resolveImagePath(img.src, baseDir, tempDir ?? baseDir);
contentImages.push({
placeholder: img.placeholder,
localPath,
@@ -236,12 +235,12 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
});
}
- let htmlContent = fs.readFileSync(finalHtmlPath, 'utf-8');
+ let finalContent = fs.readFileSync(finalHtmlPath, 'utf-8');
for (const img of contentImages) {
- const imgTag = `
`;
- htmlContent = htmlContent.replace(img.placeholder, imgTag);
+ const imgTag = `
`;
+ finalContent = finalContent.replace(img.placeholder, imgTag);
}
- fs.writeFileSync(finalHtmlPath, htmlContent, 'utf-8');
+ fs.writeFileSync(finalHtmlPath, finalContent, 'utf-8');
console.error(`[markdown-to-html] HTML saved to: ${finalHtmlPath}`);
diff --git a/skills/baoyu-markdown-to-html/scripts/md/renderer.ts b/skills/baoyu-markdown-to-html/scripts/md/renderer.ts
index 65fb0cb..7a1234c 100644
--- a/skills/baoyu-markdown-to-html/scripts/md/renderer.ts
+++ b/skills/baoyu-markdown-to-html/scripts/md/renderer.ts
@@ -400,10 +400,10 @@ export function renderMarkdown(raw: string, renderer: RendererAPI): {
html: string;
readingTime: ReadTimeResults;
} {
- const preprocessed = preprocessCjkEmphasis(raw);
const { markdownContent, readingTime: readingTimeResult } =
- renderer.parseFrontMatterAndContent(preprocessed);
- const html = marked.parse(markdownContent) as string;
+ renderer.parseFrontMatterAndContent(raw);
+ const preprocessed = preprocessCjkEmphasis(markdownContent);
+ const html = marked.parse(preprocessed) as string;
return { html, readingTime: readingTimeResult };
}
diff --git a/skills/baoyu-markdown-to-html/scripts/md/themes/modern.css b/skills/baoyu-markdown-to-html/scripts/md/themes/modern.css
index d3037bf..ccd2cc0 100644
--- a/skills/baoyu-markdown-to-html/scripts/md/themes/modern.css
+++ b/skills/baoyu-markdown-to-html/scripts/md/themes/modern.css
@@ -1,6 +1,7 @@
/**
* MD 现代主题 (modern)
* 大圆角、药丸形标题、宽松行距、现代感
+ * 如需使用主题色,请使用 var(--md-primary-color) 代替颜色值
*/
/* ==================== 容器样式覆盖 ==================== */
@@ -9,6 +10,8 @@ container {
font-family: var(--md-font-family);
font-size: var(--md-font-size);
line-height: 2;
+ letter-spacing: 0px;
+ font-weight: 400;
background-color: var(--md-container-bg);
border: 1px solid rgba(255, 255, 255, 0.01);
border-radius: 25px;
@@ -44,15 +47,22 @@ h2 {
color: var(--md-primary-color);
font-size: 20px;
font-weight: bold;
+ letter-spacing: 0.578px;
+ line-height: 1.7;
border-bottom: 2px solid var(--md-accent-color);
+ text-align: left;
}
/* ==================== 三级标题 ==================== */
h3 {
+ padding-left: 10px;
+ border-left: 4px solid var(--md-primary-color);
+ border-radius: 2px;
margin: 0 8px 10px;
color: hsl(var(--foreground));
- font-size: 18px;
+ font-size: 20px;
font-weight: bold;
+ line-height: 1.2;
}
/* ==================== 四级标题 ==================== */
@@ -67,7 +77,7 @@ h4 {
h5 {
display: inline-block;
margin: 0 8px 10px;
- padding: 4px 10px;
+ padding: 4px 12px;
color: hsl(var(--foreground));
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgb(189, 224, 254);
@@ -87,86 +97,300 @@ h6 {
/* ==================== 段落 ==================== */
p {
margin: 20px 0;
+ letter-spacing: 0.1em;
color: hsl(var(--foreground));
line-height: 2;
letter-spacing: 0px;
font-size: 15px;
+ font-weight: 400;
+ word-break: break-all;
}
/* ==================== 引用块 ==================== */
blockquote {
font-style: normal;
- padding: 15px 12px;
+ padding: 15px 0;
+ margin: 12px 0;
border-left: 7px solid var(--md-accent-color);
border-radius: 10px;
color: hsl(var(--foreground));
background-color: var(--blockquote-background);
- margin: 12px 0;
}
blockquote > p {
+ display: block;
+ font-size: 1em;
+ letter-spacing: 0.1em;
color: hsl(var(--foreground));
- font-size: 14px;
margin: 0;
}
+/* ==================== GFM 警告块 ==================== */
+.alert-title-note,
+.alert-title-tip,
+.alert-title-info,
+.alert-title-important,
+.alert-title-warning,
+.alert-title-caution,
+.alert-title-abstract,
+.alert-title-summary,
+.alert-title-tldr,
+.alert-title-todo,
+.alert-title-success,
+.alert-title-done,
+.alert-title-question,
+.alert-title-help,
+.alert-title-faq,
+.alert-title-failure,
+.alert-title-fail,
+.alert-title-missing,
+.alert-title-danger,
+.alert-title-error,
+.alert-title-bug,
+.alert-title-example,
+.alert-title-quote,
+.alert-title-cite {
+ display: flex;
+ align-items: center;
+ gap: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.alert-title-note {
+ color: #478be6;
+}
+
+.alert-title-tip {
+ color: #57ab5a;
+}
+
+.alert-title-info {
+ color: #93c5fd;
+}
+
+.alert-title-important {
+ color: #986ee2;
+}
+
+.alert-title-warning {
+ color: #c69026;
+}
+
+.alert-title-caution {
+ color: #e5534b;
+}
+
+.alert-title-abstract,
+.alert-title-summary,
+.alert-title-tldr {
+ color: #00bfff;
+}
+
+.alert-title-todo {
+ color: #478be6;
+}
+
+.alert-title-success,
+.alert-title-done {
+ color: #57ab5a;
+}
+
+.alert-title-question,
+.alert-title-help,
+.alert-title-faq {
+ color: #c69026;
+}
+
+.alert-title-failure,
+.alert-title-fail,
+.alert-title-missing {
+ color: #e5534b;
+}
+
+.alert-title-danger,
+.alert-title-error {
+ color: #e5534b;
+}
+
+.alert-title-bug {
+ color: #e5534b;
+}
+
+.alert-title-example {
+ color: #986ee2;
+}
+
+.alert-title-quote,
+.alert-title-cite {
+ color: #9ca3af;
+}
+
+/* GFM Alert SVG 图标颜色 */
+.alert-icon-note {
+ fill: #478be6;
+}
+
+.alert-icon-tip {
+ fill: #57ab5a;
+}
+
+.alert-icon-info {
+ fill: #93c5fd;
+}
+
+.alert-icon-important {
+ fill: #986ee2;
+}
+
+.alert-icon-warning {
+ fill: #c69026;
+}
+
+.alert-icon-caution {
+ fill: #e5534b;
+}
+
+.alert-icon-abstract,
+.alert-icon-summary,
+.alert-icon-tldr {
+ fill: #00bfff;
+}
+
+.alert-icon-todo {
+ fill: #478be6;
+}
+
+.alert-icon-success,
+.alert-icon-done {
+ fill: #57ab5a;
+}
+
+.alert-icon-question,
+.alert-icon-help,
+.alert-icon-faq {
+ fill: #c69026;
+}
+
+.alert-icon-failure,
+.alert-icon-fail,
+.alert-icon-missing {
+ fill: #e5534b;
+}
+
+.alert-icon-danger,
+.alert-icon-error {
+ fill: #e5534b;
+}
+
+.alert-icon-bug {
+ fill: #e5534b;
+}
+
+.alert-icon-example {
+ fill: #986ee2;
+}
+
+.alert-icon-quote,
+.alert-icon-cite {
+ fill: #9ca3af;
+}
+
/* ==================== 代码块 ==================== */
pre.code__pre,
.hljs.code__pre {
+ font-size: 90%;
+ overflow-x: auto;
border-radius: 10px;
+ padding: 0 !important;
+ line-height: 1.5;
+ margin: 10px 8px;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
}
/* ==================== 图片 ==================== */
img {
+ display: block;
+ max-width: 100%;
+ margin: 0.1em auto 0.5em;
border-radius: 10px;
- margin: 5px auto;
}
/* ==================== 列表 ==================== */
ol {
padding-left: 1em;
- margin: 15px 0;
+ margin-left: 0;
+ color: hsl(var(--foreground));
line-height: 2;
}
ul {
- list-style: none;
- padding-left: 0;
- margin: 15px 0;
+ list-style: circle;
+ padding-left: 1em;
+ margin-left: 0;
+ color: hsl(var(--foreground));
line-height: 2;
}
li {
- margin: 0.2em 0;
+ display: block;
+ margin: 0.2em 8px;
color: hsl(var(--foreground));
- font-size: 15px;
+}
+
+/* ==================== 脚注 ==================== */
+p.footnotes {
+ margin: 0.5em 8px;
+ font-size: 80%;
+ color: hsl(var(--foreground));
+}
+
+/* ==================== 图表 ==================== */
+figure {
+ margin: 1.5em 8px;
+ color: hsl(var(--foreground));
+}
+
+figcaption,
+.md-figcaption {
+ text-align: center;
+ color: #888;
+ font-size: 0.8em;
}
/* ==================== 分隔线 ==================== */
hr {
border-style: solid;
border-width: 1px 0 0;
- border-color: var(--md-primary-color);
+ border-color: var(--md-accent-color);
+ margin: 1.5em 0;
+}
+
+/* ==================== 行内代码 ==================== */
+code {
+ font-size: 90%;
+ color: #d14;
+ background: rgba(27, 31, 35, 0.05);
+ padding: 3px 5px;
+ border-radius: 4px;
+}
+
+/* 代码块内的 code 标签需要特殊处理(覆盖行内 code 样式) */
+pre.code__pre > code,
+.hljs.code__pre > code {
+ display: -webkit-box;
+ padding: 0.5em 1em 1em;
+ overflow-x: auto;
+ text-indent: 0;
+ color: inherit;
+ background: none;
+ white-space: nowrap;
margin: 0;
}
/* ==================== 强调 ==================== */
-strong {
- color: hsl(var(--foreground));
- font-weight: bold;
-}
-
-/* ==================== 标记高亮 ==================== */
-.markup-highlight {
- background-color: hsl(var(--foreground));
- padding: 10px;
- color: var(--md-container-bg);
-}
-
-.markup-underline {
- text-decoration: underline;
- text-decoration-color: var(--md-accent-color);
+em {
+ font-style: italic;
+ font-size: inherit;
}
/* ==================== 链接 ==================== */
@@ -175,7 +399,67 @@ a {
text-decoration: none;
}
+/* ==================== 粗体 ==================== */
+strong {
+ color: var(--md-primary-color);
+ font-weight: bold;
+ font-size: inherit;
+}
+
/* ==================== 表格 ==================== */
+table {
+ color: hsl(var(--foreground));
+}
+
+thead {
+ font-weight: bold;
+ color: hsl(var(--foreground));
+}
+
th {
+ border: 1px solid #dfdfdf;
+ padding: 0.25em 0.5em;
+ color: hsl(var(--foreground));
+ word-break: keep-all;
background: color-mix(in srgb, var(--md-primary-color) 10%, transparent);
}
+
+td {
+ border: 1px solid #dfdfdf;
+ padding: 0.25em 0.5em;
+ color: hsl(var(--foreground));
+ word-break: keep-all;
+}
+
+/* ==================== KaTeX 公式 ==================== */
+.katex-inline {
+ max-width: 100%;
+ overflow-x: auto;
+}
+
+.katex-block {
+ max-width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ padding: 0.5em 0;
+ text-align: center;
+}
+
+/* ==================== 标记高亮 ==================== */
+.markup-highlight {
+ background-color: var(--md-primary-color);
+ padding: 2px 4px;
+ border-radius: 4px;
+ color: #fff;
+}
+
+.markup-underline {
+ text-decoration: underline;
+ text-decoration-color: var(--md-primary-color);
+}
+
+.markup-wavyline {
+ text-decoration: underline wavy;
+ text-decoration-color: var(--md-primary-color);
+ text-decoration-thickness: 2px;
+}