问题 链接到标题
Markdown 里用 $$ 写好的 LaTeX 数学公式:
$$
\nabla \cdot \mathbf{E} = \frac{\rho}{\varepsilon_0}
$$
期望渲染效果:
$$ \nabla \cdot \mathbf{E} = \frac{\rho}{\varepsilon_0} $$
但转 PDF 时,要么公式显示为原始代码,要么中文字体缺失乱码。本文记录 macOS 下的可行方案。
方案一:Pandoc + XeLaTeX(不推荐) 链接到标题
brew install basictex
pandoc file.md -o file.pdf --pdf-engine=xelatex -V mainfont='PingFang SC'
公式渲染效果没问题,但输出风格偏 TeX 传统——标题字体、表格样式、公式间距都与现代文档有明显差异,不够美观。
推荐方案:Pandoc + MathJax + Puppeteer 链接到标题
核心思路:用 Pandoc 生成带 MathJax CDN 的 HTML,再用 Puppeteer(Headless Chrome)渲染出 PDF。输出样式完全由 CSS 控制。
步骤 链接到标题
1. Markdown 中公式写法 链接到标题
display math 用 $$ ... $$:
$$
\nabla \cdot \mathbf{E} = \frac{\rho}{\varepsilon_0}
$$
$$
\nabla \cdot \mathbf{B} = 0
$$
$$
\nabla \times \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t}
$$
$$
\nabla \times \mathbf{B} = \mu_0 \mathbf{J} + \mu_0 \varepsilon_0 \frac{\partial \mathbf{E}}{\partial t}
$$
渲染效果:
$$ \nabla \cdot \mathbf{E} = \frac{\rho}{\varepsilon_0} $$
$$ \nabla \cdot \mathbf{B} = 0 $$
$$ \nabla \times \mathbf{E} = -\frac{\partial \mathbf{B}}{\partial t} $$
$$ \nabla \times \mathbf{B} = \mu_0 \mathbf{J} + \mu_0 \varepsilon_0 \frac{\partial \mathbf{E}}{\partial t} $$
2. Pandoc 生成 HTML 链接到标题
pandoc -s file.md --mathjax -o output.html
Pandoc 的 --mathjax 会在 HTML <head> 中自动插入 MathJax CDN 链接,并将 $$...$$ 转换为 <span class="math display">\[ ... \]</span>。
3. Puppeteer 渲染 PDF 链接到标题
MathJax 是 JavaScript 渲染引擎,必须在浏览器中执行才能生成公式图形。Puppeteer 启动 Headless Chrome,加载 HTML,等待 MathJax 渲染完成后截图输出 PDF。
关键点:
- 用
file://协议加载:不要用page.setContent(),否则 localStorage 被禁用导致 MathJax 异常 networkidle0:等待所有 CDN 脚本加载完毕 + 500ms 无新请求tex-chtml.js而非tex-svg.js:CommonHTML 输出,公式对齐可控displayAlign属chtml层级:MathJax v3 中不在svg或顶层,而在chtml下
4. 公式居左 链接到标题
如需公式左对齐,在 <head> 中配置:
<script>
window.MathJax = {
chtml: { displayAlign: "left", displayIndent: "0em" }
};
</script>
5. 表格样式 链接到标题
Pandoc 将 Markdown 表格转为 <table>,通过 CSS 自定义样式:
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccc; padding: 8px 12px; }
th { background: #f0f0f0; }
tr:nth-child(even) { background: #f9f9f9; }
完整脚本 链接到标题
以下 md2pdf.js 接受 Markdown 文件路径,输出同名的 PDF:
const puppeteer = require('puppeteer');
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const input = process.argv[2];
if (!input) { console.error('Usage: node md2pdf.js file.md'); process.exit(1); }
const htmlFile = `/tmp/${path.basename(input)}.html`;
const pdfFile = input.replace(/\.md$/, '.pdf');
// Step 1: pandoc to HTML
execSync(`pandoc -s "${input}" --mathjax -o "${htmlFile}"`, { stdio: 'inherit' });
// Step 2: extract body content
const html = fs.readFileSync(htmlFile, 'utf-8');
const body = html.slice(html.indexOf('<body>') + 6, html.indexOf('</body>'));
// Step 3: wrap with custom template
const template = `<!DOCTYPE html>
<html><head><meta charset="utf-8">
<script>window.MathJax = { chtml: { displayAlign: "left", displayIndent: "0em" } };</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
<style>
body { font-family: system-ui; padding: 2em; line-height: 2; font-size: 12pt; }
h1, h2 { font-family: system-ui, sans-serif; }
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
th, td { border: 1px solid #ccc; padding: 8px 12px; text-align: left; }
th { background: #f0f0f0; font-weight: bold; }
tr:nth-child(even) { background: #f9f9f9; }
</style></head><body>${body}</body></html>`;
fs.writeFileSync(htmlFile, template);
// Step 4: Puppeteer render to PDF
(async () => {
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.goto('file://' + htmlFile, { waitUntil: 'networkidle0', timeout: 120000 });
await page.pdf({ path: pdfFile, format: 'A4', printBackground: true });
await browser.close();
console.log('PDF:', pdfFile);
})();
用法:
node md2pdf.js 麦克斯韦方程组.md
前提:安装了 pandoc,且 puppeteer 可访问。如 npx 安装的 md-to-pdf,需指定路径:
NODE_PATH=$(npm root -g)/md-to-pdf/node_modules node md2pdf.js file.md
或全局安装 puppeteer:
npm install -g puppeteer
踩坑总结 链接到标题
| 尝试过的方案 | 问题 |
|---|---|
md-to-pdf + KaTeX |
marked 把 \[ 当转义吃掉,raw HTML script 标签被丢弃 |
pandoc --katex + weasyprint |
weasyprint 不执行 JS,公式不渲染 |
pandoc + XeLaTeX |
需装 basictex(~100MB),输出风格偏 TeX 传统 |
| 推荐:Pandoc + MathJax + Puppeteer | 样式完全 CSS 可控,公式对齐、表格美观 |
关键细节 链接到标题
- 定界符一定要用
$$ ... $$:Pandoc 默认 markdown 解析器识别$$为 display math,\[ ... \]在marked和 Pandoc 中都会被当作转义处理 - MathJax
displayAlign在chtml层级:MathJax v3 中不在顶层options或svg,CommonHTML 输出下的配置路径是chtml.displayAlign tex-chtml.js而非tex-svg.js:CommonHTML 输出样式可控,SVG 输出的对齐配置在某些环境下不生效file://协议而非setContent():setContent创建的无源文档禁止 localStorage,MathJax 缓存异常