问题 链接到标题

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 输出,公式对齐可控
  • displayAlignchtml 层级: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 displayAlignchtml 层级:MathJax v3 中不在顶层 optionssvg,CommonHTML 输出下的配置路径是 chtml.displayAlign
  • tex-chtml.js 而非 tex-svg.js:CommonHTML 输出样式可控,SVG 输出的对齐配置在某些环境下不生效
  • file:// 协议而非 setContent()setContent 创建的无源文档禁止 localStorage,MathJax 缓存异常

下一篇:给 Hugo 博客加上数学公式渲染