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; +}