十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VSCode Markdown 导出带书签 PDF 的完整方案:Pandoc + XeLaTeX 实践

VSCode Markdown 导出带书签 PDF 的完整方案:Pandoc + XeLaTeX 实践 你是不是也遇到过这种情况在 VSCode 里辛辛苦苦写了一篇很长的 Markdown 笔记逻辑清晰、配图齐全准备导出一份带目录、能直接发给别人的 PDF结果插件一键导出后PDF 打开一看左侧没有任何书签几十页的文档全靠滚动条往下翻。这个问题看起来只是“VSCode、Markdown、PDF、书签”几个词的事但真折腾起来能绕出各种坑。我在这个需求上试过很多方案从常见的 markdown-pdf 插件、md-to-pdf、Chrome 打印、PrinceXML到最后换到 Pandoc XeLaTeX才算把“带书签”这件事彻底稳定下来。这篇文章就是把我踩过的坑和最终这套可复制的流程完整写下来从原理、环境搭建到命令参数再到常见报错排查一次性讲透。适合正在被这个问题卡住的人不管你是写技术文档、课程笔记还是整理知识库照着操作基本都能顺利导出带书签的 PDF。1. 先搞清楚PDF 书签到底是什么为什么插件导出总是不带1.1 PDF 书签的本质是“大纲”不是页面上的目录文字很多朋友分不清“PDF 里的目录页”和“PDF 书签”的区别这是理解整个问题最关键的一步。PDF 书签在格式规范里叫Outline大纲是存在 PDF 文件元数据里的一套结构化数据不直接显示在页面上。PDF 阅读器比如 Edge、Adobe Acrobat、PDF Expert打开文件后会在侧边栏把它渲染成可折叠、可点击跳转的树状列表。这跟 Markdown 文档里的#标题其实是一个道理理论上 Markdown 的标题层级可以直接映射成 PDF 大纲层级#对应一级书签##对应二级###对应三级。但问题是很多转换工具在渲染 PDF 时只把标题当成“页面上的文字”处理并不会写入 PDF 的 Outline 元数据。打个比方纸质书的目录页只是印在纸上的索引而 PDF 书签更像是电子阅读器里那个能点击跳转的侧边栏。你要的是后者但很多免费转换工具给到你的只是前者甚至连前者都没有。1.2 为什么 markdown-pdf 这类插件的书签时有时无VSCode 里最常用的 Markdown PDF 导出插件是 markdown-pdf我用过相当长一段时间。它的优点是安装简单、开箱即用但它导出 PDF 是否带书签在不同版本、不同系统下表现非常不稳定。有时候有有时候没有换个电脑又变一种结果。原因在于这类插件本质上是调用 Electron 或者 Chromium 的打印机制把 Markdown 先渲染成 HTML 页面再打印成 PDF。浏览器打印引擎生成 PDF 时对 PDF 大纲书签的支持受版本影响很大很多时候它只输出页面内容不输出可访问性大纲结构。所以你会发现同一份 Markdown在同事电脑导出来有书签在自己电脑导出就没有。我搜这个问题的时候还看到有人回答说“需要在 VSCode 里导出 PDF就要先下载 PrinceXML”其实这是旧版本 markdown-pdf 插件的做法用 HTML 转 PDF 的渲染器。PrinceXML 可以直接处理 HTML 和 CSS但它的书签支持也需要额外配置而且商业使用有授权限制。不是说它不行只是对“写 Markdown 顺便导个 PDF”这个场景来说它并不是最优解。最稳的路径还是下面要讲的 Pandoc LaTeX 引擎。1.3 这个问题到底影响哪些人如果你只是偶尔拿 Markdown 导出一页两页的速查表书签有没有问题不大。但如果你常写长文档比如技术方案、毕业论文、操作手册、项目周报合订本或者需要把一堆 Markdown 笔记合并成一本完整的知识库 PDF那书签就是刚需。没有书签的长 PDF阅读体验极差查找章节基本靠猜页码发给别人后对方大概率会在微信里问你“第几章在哪一页”。我最初触发这个需求的场景是要把团队内部的一整套运维文档从 Markdown 导出成 PDF 发给新同事。文档总共一百多页用 markdown-pdf 导出来没有书签新同事找“环境搭建”那一章找了半天。后来我才下决心把工具链换成 Pandoc XeLaTeX从此导出 PDF 稳定带书签再没在这个问题上浪费过时间。2. 选对工具链Pandoc XeLaTeX目前带书签最稳的组合2.1 各方案书签支持真实对比先把我在这个需求上实测过的几个方案放在一起对比方便你根据自己情况选型方案书签支持中文排版字体配置适合场景markdown-pdf 插件不稳定时有时无一般无需额外配置随手快速导出单页Chrome / Edge 打印 PDF视浏览器版本而定一般无需额外配置临时分享简单文档md-to-pdf (Node.js)需要额外配置一般依赖系统字体已用 Node 生态的人PrinceXML需额外配置 HTML 大纲一般需要 CSS 调复杂 HTML 排版场景Pandoc XeLaTeX稳定自动生成好需要设置中文字体长文档、正式文档表格里最显眼的就是最后一行。Pandoc 负责把 Markdown 转换成各种格式它生成 PDF 时可以选择不同的 PDF 引擎其中 XeLaTeX 对 Unicode 字体的支持极好中文排版质量也远高于浏览器打印。最重要的是LaTeX 的 hyperref 宏包会自动把章节结构写入 PDF 大纲只要你的 Markdown 标题层级正确生成出来的 PDF 天然就带书签不需要任何额外配置。2.2 为什么不用 PrinceXML聊到这个问题时很多人会看到网上旧教程提到 PrinceXML我也专门试过。结论是作为通用 HTML 转 PDF 的工具PrinceXML 本身能力很强但它不是给“Markdown 转 PDF”这个场景准备的。你需要先把 Markdown 转成 HTML自己写一份复杂的 CSS 去控制分页、页眉页脚、标题层级同时还要在 HTML 里构建合理的nav结构PrinceXML 才会根据它生成书签。换句话说用 PrinceXML 确实能达到目标但你要做的额外工作非常多而且它是商业软件授权费不便宜。除非你本来就是在做网页电子书、复杂版式排版这类项目否则为了“给 Markdown 加书签”去引入它性价比太低。我自己折腾完一圈后的体会是普通文档输出别碰 PrinceXML。2.3 Pandoc LaTeX 能达到的效果最终方案的效果可以简单概括成三点第一PDF 导出后自动生成可折叠的书签树章节层级清晰第二中文排版质量高字体可以自由选择宋体、黑体、苹方等第三支持代码高亮、表格、公式、图片覆盖基本上 Markdown 能表达的内容它都能排版出来。这套方案的核心思路是Pandoc 把 Markdown 解析成 LaTeX 代码XeLaTeX 再把 LaTeX 编译成 PDF。Pandoc 负责“理解 Markdown”LaTeX 负责“排版”两者各干各擅长的活。可能你会觉得多了一层就复杂但实际使用中你只需要维护一条 Pandoc 命令LaTeX 编译细节完全不需要操心。3. 环境准备VSCode、Pandoc、LaTeX 一步到位3.1 安装 PandocPandoc 的安装非常简单不同系统下的方式如下# Windows用 winget winget install --id JohnMacFarlane.Pandoc # macOS用 Homebrew brew install pandoc # Linux / WSL用 apt sudo apt update sudo apt install pandoc安装完成后在终端里执行pandoc --version能看到版本号就说明装好了。如果你不喜欢命令行从 Pandoc 官网下载对应系统的安装包双击安装也行原理一样。3.2 安装 LaTeX 引擎Windows 推荐 MiKTeXLinux 推荐 texliveLaTeX 引擎是整套流程里唯一一个体积比较大、安装耗时比较长的环节。但这也是值得的地方毕竟书签就是靠它生成的。Windows 上我推荐装MiKTeX它最大的好处是支持“按需自动安装宏包”。也就是说你第一次编译的时候如果遇到文档里用了某个功能需要额外的 LaTeX 宏包MiKTeX 会提示自动下载安装不用你自己去手动装。缺点是这个首次编译过程通常很慢可能要好几分钟需要耐心等。而且前提是你的网络能正常访问它的宏包仓库否则会卡在下载环节。安装时注意把“自动安装缺失宏包”的选项设成 Yes。Linux/WSL 上推荐直接装 TeX Live 的完整套件。因为 Pandoc 转 PDF 时用到的宏包比较多如果只装最小集经常会遇到“找不到某宏包”的报错补装非常烦。直接一条命令装全# Debian / Ubuntu / WSL sudo apt install texlive-xetex texlive-lang-chinese texlive-fonts-recommended fonts-noto-cjk这套命令里texlive-xetex提供 XeLaTeX 编译引擎texlive-lang-chinese包含中文排版支持xeCJK 宏包等fonts-noto-cjk是 Noto CJK 中文字体后面排版中文时要用到。macOS 上如果不想装完整的 MacTeX体积巨大可以装BasicTeXbrew install --cask basictex但 BasicTeX 宏包比较少后面编译时补装宏包会比较折腾所以如果你主要用 macOS 做文档输出我还是建议直接装完整版 MacTeX省得后面各种缺宏包。3.3 在 VSCode 里把编译命令封装成 TaskPandoc 命令虽然不复杂但参数一多每次手动敲也容易错。我的做法是把它写进项目的.vscode/tasks.json实现 VSCode 里一键编译。{ version: 2.0.0, tasks: [ { label: pandoc pdf, type: shell, command: pandoc, args: [ ${workspaceFolder}/docs/input.md, -o, ${workspaceFolder}/dist/output.pdf, --pdf-enginexelatex, -V, CJKmainfontNoto Serif CJK SC, --toc, --toc-depth3, -V, geometry:margin2.5cm, --highlight-styletango ], group: build, problemMatcher: [] } ] }这里把输入文件固定成了docs/input.md输出目录是dist/你需要提前手动创建dist文件夹否则命令会报“目录不存在”。配置好之后在 VSCode 里按CtrlShiftB就能调出构建任务直接执行编译非常方便。3.4 如果你在 Windows 上用 WSL很多 Windows 用户习惯用 WSL 跑 Linux 环境VSCode 通过 Remote-WSL 插件直接编辑 WSL 里的文件。这种情况下Pandoc 和 TeX Live 都装在 WSL 里VSCode 打开的也是 WSL 里的工作区Tasks 配置方式不变它会默认在 WSL 的 shell 里执行命令路径也按 Linux 规则来。有一点要注意WSL 里生成 PDF 用到的中文字体必须装到 WSL 系统里而不是 Windows 的字体。所以在 WSL 环境下sudo apt install fonts-noto-cjk这一步不能省。如果你发现生成的 PDF 中文全部是方块大概率就是 WSL 里缺中文字体。4. 核心实操一条命令导出带书签的中文 PDF4.1 最小命令与参数逐个拆解环境准备好之后真正导出 PDF 的命令其实只有一条。下面这个是我日常最常用的一条命令先看完整版pandoc input.md -o output.pdf \ --pdf-enginexelatex \ -V CJKmainfontNoto Serif CJK SC \ --toc \ --toc-depth3 \ -V geometry:margin2.5cm \ --highlight-styletango逐个参数解释一下理解这些参数后你就可以根据自己的需求自由调整--pdf-enginexelatex指定 PDF 引擎。Pandoc 本身不是排版引擎它只是个转换器生成 PDF 时必须选一个后端。这里选xelatex是因为它对 Unicode 和系统字体的支持最好中文场景绕不开它。-V CJKmainfontNoto Serif CJK SC指定中文主字体。英文和中文在 LaTeX 里是两套字体体系这里专门给中文设定字体。Windows 上可以用Microsoft YaHei或SimSunmacOS 上可以用PingFang SCLinux 推荐Noto Serif CJK SC或Noto Sans CJK SC。--toc在 PDF 正文内生成一个目录页。注意这个目录页只是文档中的一页跟独立书签是两个东西但通常我们两个都要。这个参数是生成目录页用的。--toc-depth3目录页展示到三级标题也就是#、##、###三层。如果你只想展示一级、二级改成2即可。-V geometry:margin2.5cm设置页面边距为 2.5 厘米这会影响整个 PDF 的排版范围。--highlight-styletango代码块配色方案让代码在 PDF 里更好看。Pandoc 支持tango、breezedark、kate等多种配色。书签本身不需要额外参数因为 XeLaTeX 编译时hy自perref 宏包会自动把你的章节结构写入 PDF 大纲。换句话说只要你用--pdf-enginexelatex书签就是白送的真正复杂的是让中文字体和目录配置正确。4.2 Markdown 标题层级对书签的影响书签好不好用很大程度上取决于你的 Markdown 标题层级是否规范。我见过很多人写 Markdown 把标题层级当列表用想到什么层级就随手写几个#这会导致导出的书签树非常混乱。建议长期保持一个习惯每一份文档只使用一个有意义的顶层标题并且层级统一递减。比如# 第一章 环境准备 ## 1.1 安装 Pandoc ### 1.1.1 Windows 安装 ... ## 1.2 安装 LaTeX ...这样生成 PDF 后书签就会是一棵清晰的树第一层级是章第二层是节第三层是具体小节。如果你的 Markdown 里出现跳级比如直接#下面接###书签层级也会跟着跳折叠起来会很突兀。这个小细节比工具选型更能决定最终 PDF 的体验。4.3 图片、表格、代码块的处理Markdown 里的图片路径在 Pandoc 转换时默认以“当前工作目录”为基准。假设你的input.md文件在docs/目录图片在docs/images/目录那么你必须在docs/目录下执行 Pandoc 命令图片才能被正确引用。如果你非要在项目根目录执行命令可以加一个--resource-pathdocs参数告诉 Pandoc 去docs目录找资源pandoc docs/input.md -o output.pdf --pdf-enginexelatex --resource-pathdocs ...表格和代码块不用太担心Pandoc 会自动把 Markdown 表格转换成 LaTeX 的 table 环境代码块则会根据--highlight-style参数进行语法高亮。唯一要注意的是代码块里的中文注释偶尔会出现字体缺失的问题解决办法是给 mainfont 也指定一个包含中文的字体最简单的做法是加参数-V mainfontNoto Sans CJK SC这样英文和中文字体都统一到 Noto Sans 系列里问题就消失了。4.4 多 Markdown 合并成一本带书签的 PDF写知识库或者项目手册时经常需要把多篇 Markdown 合并成一个 PDF。这个需求 Pandoc 天然支持直接把多个文件按顺序丢进去就行pandoc 01-前言.md 02-环境准备.md 03-快速开始.md 04-常见问题.md \ -o manual.pdf \ --pdf-enginexelatex \ -V CJKmainfontNoto Serif CJK SC \ --toc --toc-depth3合并后需要注意书签的层级逻辑如果每个文件的顶层标题都是#那它们合并后会是平级的一级书签这是合理的。如果想让整本书有一个总标题可以在最前面加一个00-封面.md里面只放一行# 项目用户手册这样它就是整本书的一级书签后面的各章变成它下面的二级书签结构会很漂亮。4.5 用 YAML 元数据自动生成标题页如果你想给 PDF 加一个正式的标题、作者、日期信息不用自己手工排版直接在 Markdown 文件最开头写一段 YAML 前置元数据即可--- title: 前端团队编码规范手册 author: 技术文档组 date: 2025-03-01 ---Pandoc 会读取这些字段在 PDF 开头自动生成一个居中的标题块。LaTeX 的 article 文档类默认会把标题放在第一页上部看起来简洁正式。如果你需要单独的封面页就需要自定义 LaTeX 模板了这个后面讲进阶时再说。5. 进阶配置让 PDF 更像一份正式文档5.1 自定义中文字体中文字体对 PDF 的观感影响非常大。LaTeX 里设置中文字体的核心变量是CJKmainfont这个前面已经介绍过。但很多人在 Windows 上会踩坑在终端里输入微软雅黑中文名结果编译报错找不到字体。正确做法是使用字体的英文名称。查看 Windows 系统中文字体英文名最省事的方法是打开“字体设置”面板或者直接在命令行里执行fc-list :langzhLinux/WSLWindows 下可以在安装完 Git Bash 后执行同样的命令。几个常用中文字体名参考字体显示名英文名LaTeX 里填这个思源宋体Noto Serif CJK SC思源黑体Noto Sans CJK SC微软雅黑Microsoft YaHei宋体SimSun黑体SimHei苹方macOSPingFang SC排版正式一点的手册、论文建议用宋体或者思源宋体做 PPT 配套文档、技术方案用黑体或者思源黑体更现代。5.2 页眉、页脚、页码LaTeX 默认的页码在页面底部居中如果你想要类似“公司内部资料”这种页眉或者想要左侧页码、右侧章节名的效果可以通过一个额外的 LaTeX 头文件实现然后让 Pandoc 引用它。先建一个header.tex文件内容如下\usepackage{fancyhdr} \pagestyle{fancy} \fancyhead[L]{\leftmark} \fancyhead[R]{内部资料请勿外传} \cfoot{\thepage} \renewcommand{\headrulewidth}{0.4pt}然后在 Pandoc 命令里加一个-H header.tex参数pandoc input.md -o output.pdf \ --pdf-enginexelatex \ -V CJKmainfontNoto Serif CJK SC \ --toc --toc-depth3 \ -H header.tex\leftmark会自动显示当前章节的标题\thepage是当前页码\headrulewidth控制页眉横线的粗细。这套配置对正式文档来说非常实用而且不复杂改一次可以长期复用。5.3 代码高亮风格调整Pandoc 默认代码高亮风格是pygments如果你觉得不好看可以换成别的。常用的几个方案在命令里直接换参数即可# 暖色系 --highlight-styletango # 深色风格 --highlight-stylebreezedark # 类似 GitHub 的简洁风格 --highlight-stylegithub如果这些内置风格都不满意你可以用pandoc --print-highlight-stylebreezedark my.style导出一个风格文件然后手动修改里面的颜色值再用--highlight-stylemy.style引用自由度非常高。5.4 公式支持与特殊符号Pandoc 天然支持 Markdown 里的 LaTeX 公式语法。比如$Emc^2$行内公式或者用$$...$$包裹的独立公式。XeLaTeX 引擎对公式的支持是完备的不需要额外配置。但有一点要提醒如果你的文档里出现 emoji 表情或者特殊 Unicode 符号XeLaTeX 默认字体可能不含这些字形编译时会提示 “Missing character”。最省事的处理方式是文档中不要用 emoji或者在字体配置里额外加一个 fallback 字体但后一种做法配置成本较高不推荐普通用户折腾。6. 踩坑实录VSCode Markdown PDF 常见问题排查6.1 中文全是方块或者直接消失这是最常见的坑。通常原因不是 Pandoc 的问题而是系统里缺少中文字体或者字体名写得不对。排查思路先确定系统有哪些中文字体。Linux/WSL 执行fc-list :langzhWindows 可以打开“设置 - 个性化 - 字体”在里面找到字体的英文名称。然后回头检查 Pandoc 命令里的CJKmainfont是否填的英文名。如果字体确实存在但仍然方块试试在命令前面加一段-v mainfontNoto Sans CJK SC有的版本里主字体缺失会导致中文字符全部跑到 CJK 字体逻辑里但 LaTeX 渲染时找不到指定了主字体后问题会自然解除。6.2 MiKTeX 首次编译太慢宏包下载失败Windows 用户装 MiKTeX 后第一次编译通常要下载几百 MB 的宏包速度取决于网络。我的建议是编译前先打开 MiKTeX Console在“Settings - Packages”里把 “Install missing packages on the fly” 设为 Yes避免中途弹窗卡住进程。如果下载反复失败可以把宏包仓库切换到国内镜像源。MiKTeX Console 的 “Packages - Change package source” 里可以选择镜像仓库。TeX Live 用户如果遇到宏包缺失多数情况是因为没装完整包建议直接检查texlive-lang-chinese是否安装。6.3 图片不显示或者图片路径报错Pandoc 对图片路径比较严格。相对路径必须在执行命令的目录下能解析到否则编译报错或者 PDF 里出现空白占位。解决思路尽量保证执行 Pandoc 的目录和 Markdown 文件所在目录一致。如果文件在docs/子目录图片在docs/assets/可以加--resource-pathdocsPandoc 会去docs目录下寻找。文件路径避免中文和空格LaTeX 对某些字符处理很敏感长期用下来全英文路径最省心。6.4 代码块中文注释变空白或乱码如果你在代码里写了中文注释但 PDF 里这些注释显示成空白通常是mainfont没有设置成中文字体。英文代码块的内容由 mainfont 控制而 mainfont 默认可能是不支持中文的字体。加上-V mainfontNoto Sans CJK SC或者-V mainfontMicrosoft YaHei后这个问题基本就解决了。6.5 生成的 PDF 没有书签如果你用的是我前面介绍的 Pandoc XeLaTeX 流程几乎不会出现这个问题。但如果你是从 markdown-pdf 插件切过来发现 PDF 在 Edge 里打开左侧没有书签分两步排查第一步确认 PDF 打开时左侧栏有没有“大纲”按钮。Edge、Chrome 的 PDF 阅读器通常会在左侧显示一个“目录”图标点开才是书签内容有些 PDF 大纲存在但默认不显示需要手动点开。第二步确认 PDF 真是用 XeLaTeX 生成的。如果只是用了某个插件把 Markdown 转成 HTML再打印成 PDF那没有书签很正常属于工具选型问题不是设置问题。6.6 常见问题速查表症状大概率原因解决办法中文方块/空白缺中文字体或字体名错误安装 Noto CJK 字体检查CJKmainfont英文名MiKTeX 编译极慢首次下载宏包开启自动安装宏包换镜像源图片不显示图片路径解析不到加--resource-pathdocs或在 md 所在目录执行代码块中文缺失mainfont 不支持中文指定-V mainfontNoto Sans CJK SC没有书签用错了转换工具换 Pandoc XeLaTeX公式显示为源码缺少 MathJax 或 LaTeX 包确认使用--pdf-enginexelatex7. 补救方案已经导出的 PDF 怎么手动补书签7.1 在专业 PDF 阅读器里手动加书签如果你手上已经有一份没有书签的 PDF而且不打算重新生成还有一个手工补救的办法用支持编辑书签的 PDF 阅读器添加。Adobe Acrobat Pro 和 PDF-XChange Editor 都支持这个功能具体操作大同小异打开 PDF调出书签面板在要加书签的页面位置点击然后在书签面板里“新建书签”输入标题即可。但说实话如果文档有几十页手动加书签的效率极低而且很容易加错位置。这个方法只对几页的文档适用长文档还是老老实实回到源头重新导出。7.2 用 pypdf 脚本批量补书签如果你的文档有明确的章节结构和页码可以用 Python 的 pypdf 库批量生成书签。先安装依赖pip install pypdf然后写一个简单的脚本文档from pypdf import PdfReader, PdfWriter reader PdfReader(no_bookmark.pdf) writer PdfWriter() writer.clone_document_from_reader(reader) # (标题, 页码)页码从 1 开始脚本内部会转成 0 起始索引 toc [ (第1章 引言, 1), (第2章 环境准备, 3), (2.1 安装 Pandoc, 4), (2.2 安装 LaTeX, 6), (第3章 导出 PDF, 9), ] parent_chapter None for title, page in toc: outline_item writer.add_outline_item(title, page - 1) # 如果是以第x章开头当成一级书签否则挂到最近的一级书签下面 if title.startswith(第): parent_chapter outline_item elif parent_chapter is not None: # 把二级书签挂到一级书签下 pass with open(with_bookmark.pdf, wb) as f: writer.write(f)这个脚本的细节其实还有很多可玩的地方比如通过parent参数实现书签嵌套。pypdf 的add_outline_item支持传入parent来指定当前书签挂在哪个父级书签下面如果要做多级嵌套可以先把父级 outline item 对象存下来再传入子级。这个方案适合有少量固定文档需要临时处理的场景但对于每日产出的文档来说我还是更推荐从源头用 Pandoc 解决问题因为一劳永逸。写在最后的个人体会这套 Pandoc XeLaTeX 的流程我前后用了一年多可以很负责任地说它是目前我找到的“VSCode 里写 Markdown 导出带书签 PDF”最稳的方案。虽然初次安装 LaTeX 环境要花一点时间但配置完之后所有长文档导出都是一条命令的事再也不用担心书签时有时无的问题。最后分享一个小技巧如果你经常要导 PDF建议把常用的 Pandoc 参数写成一个脚本文件比如export-pdf.sh放进项目根目录甚至绑定到 VSCode 的 Task 里。以后写文档只需要关注内容本身不需要每次回忆那串参数。另外写 Markdown 时维护好标题层级是比任何工具都重要的一件事这一点无论用不用这套流程都成立。
返回列表