烦恼一般都是想太多了。

0%

使用pandoc转换pdf加上书签目录

在文章 几种绘图语法的比较 中,我说了我想要的一个 markdown 编辑器, typora 实际上非常优秀,但不开源。 Macdown 非常不错,开源。通过将其 mermaid 进行升级,viz.js 进行升级后,感觉非常不错了,唯一还有一点,就是导出 pdf 没有 Toc,而只能在页内设置 Toc,所以来研究一下用 pandoc 来进行转换看看效果如何。

pandoc

pandoc 网站 一句话介绍:

如果你需要在一个标记文件格式到另外一种标记文件格式间进行转换,那么 pandoc 就是你的瑞士军刀。

其可以在很多种文件间相互转换。

我最在意的是从 md 到 pdf 或者 md 到 word 的转换。

Typora 据说其转换是会将我们的代码转换成一自己专有的中间格式,进行导出。当然,其除了 pdf 和 html 外的导出是通过 pandoc 来实现的,其导出为 pdf 的效果实在是太棒了。

抽象语法树

我们可以用命令来生成一个抽象语法树的 JSON 表示:

pandoc -t json <input file>

如我的测试文件

# header
你好
```mermaid
graph TB;
a --> b;
\`\`\`

输出:

{
"pandoc-api-version" : [
1,
20
],
"meta" : {},
"blocks" : [
{
"c" : [
1,
[
"header",
[],
[]
],
[
{
"c" : "header",
"t" : "Str"
}
]
],
"t" : "Header"
},
{
"c" : [
{
"t" : "Str",
"c" : "你好"
}
],
"t" : "Para"
},
{
"c" : [
[
"",
[
"mermaid"
],
[]
],
"graph TB;\na --> b;"
],
"t" : "CodeBlock"
}
]
}

一个 pandoc 的 AST 包含一个 meta 块(包含如标题,作者,日期)等的元数据及一个 Block 元素组成的列表。

在我们的例子中,有三个 Block 元素:Header, Str, CodeBlock。每个都有一个内容列表(由 Inline 元素组成)。

简单看一下 CodeBlock 的在 AST 内的组成,其包括两部分:

  • Attr 包括三个参数:(identifier, [classes],[(key,value)]) 分别是标识符,类列表,k-v键值对列表
  • Text 就是代码本身。事实pandoc 是进行了封装的 Unicode Text 字节

在我们的 md 文件中,将代码的类型,标注成了 classes 。

参考用 python 的一个 filter ,调用外部的 mermaid -cli 来进行渲染:

pandoc-mermaid-filter

基本语法

pandoc -s -f gfm -t pdf -o outputfile
  • -f FORMAT, -r FORMAT, —from=FORMAT, —read=FORMAT 输入文件格式
  • -t FORMAT, -w FORMAT, —to=FORMAT, —write=FORMAT 输出文件格式
  • -o 输出文件
  • -s, —standalone 增加页眉和页脚。pdf, epub, epub3, fb2, docx, 格式会自动设置此选项。

建立 PDF

最简单的代码就是:

pandoc test.txt -o test.pdf

pandoc 默认使用 LaTeX 来建立 PDF 文件,这就要求我们首先安装 latex 引擎。当然,其也可以使用 ConText, roff ms, HTML 来作为中间格式。需要中间格式的时候,我们需要为输出文件设置一个 .pdf 扩展,然后添加 —pdf—engine 选项或者 -t context, -t html 或者 -t ms。用来生成中间文件的工具通过 --pdf-engine 来进行指定。

pandoc -V 'CJKmainfont=Songti TC' -V mainfont=Menlo --from gfm --listings --pdf-engine=xelatex

可以通过变量控制 PDF 的风格,这依赖于我们使用的中间文件格式:查看 variables for LaTeX, variables for ConTeXt, variables for wkhtmltopdf, variables for ms. 。当我们使用 HTML 作为中间格式的时候,其输出可以用 --css 来控制风格

如果要调试 PDF 的生成,我们可以通过查看其中间表示:不使用 -o test.pdf 我们使用 -s -o test.tex 来生成 LaTex。然后用 pdflatex test.tex来进行测试。

当使用 LaTex 的时候,下面这些包必须可用(这些基本都包含在活跃的 Tex 版本中): amsfonts, amsmath, lm, unicode-math, ifxetex, ifluatex, listings (如果使用 —listings 选项), fancyvrb, longtable, booktabs, graphicx (如果文档包含图片), hyperref, xcolor, ulem, geometry (geometry 变量已设置), setspace (与 linestretch 一起), and babel (with lang).

xelatex or lualatex 引擎需要 fontspec. xelatex 使用 polyglossia (with lang), xecjk, and bidi (with the dir variable set).

如果设置了 mathspec 变量,xelatex 会使用 mathspec 而不是 unicode-math

upquotemicrotype 包可用的话就会被使用,当 csquotes 被设置为 true 或者元数据字段被设置为 true 时,csquotes 会因为 typography 而使用。

下面这些包在存在的时候会用来提高输出的质量,但 pandoc 并不要求他们一定要存在: upquote (在逐字环境中使用直接引号), microtype (更好的间隔控制), parskip (更好的段间距控制), xurl (为了更好的URLs换行), bookmark (为更好的 PDF 书签), and footnotehyper or footnote (为了允许表中的脚注).

—pdf-engine

有多个 pdf 引擎:

pdflatex, lualatex, xelatex, latexmk, tectonic, wkhtmltopdf, weasyprint, prince, context, and pdfroff

如果引擎不在我们的路径变量中,那么就需要指定完整路径。如果没有指定这个选项, pandoc 会根据输出来决定使用哪一个默认的引擎:

  • -t latex or none: pdflatex (other options: xelatex, lualatex, tectonic, latexmk)
  • -t context: context
  • -t html: wkhtmltopdf (other options: prince, weasyprint)
  • -t ms: pdfroff

—toc, —table-of-contents

包含自动生成的 Toc(或者,latex, context, docx, odt, opendocument, rst, or ms, 情况下有指令需要生成)。这个选项必须配合 -s/--standalone 使用才有效,其在 man, docbook4, docbook5, jats 输出中无效。

如果我们使用 ms 来生成 PDF,TOC 会出现在文档标题的前面,我们可以用 --pdf-engine-opt==--no-toc-relocation 来让其在文档后面。

—toc-depth=NUMBER

指定要包含在 TOC 中的节等级。默认是3.

mactex

用 brew 已经找不到包了。所以我们可以安装 macTex,不过这玩意比较大。所以 pandoc 官方给了一个建议:

默认情况下 pandoc 使用LaTeX 来生成 PDF 。 因为完整的 MacTeX 会使用 4GB 的磁盘空间,我们建议使用 BasicTeX or TinyTeX 同时使用 tlmgr 来根据需要安装其他包. 如果我们收到警告说字体不存在,我们可以:

> tlmgr install collection-fontsrecommended
>

>

BasicTex

直接 brew 安装:

brew cask install basictex

安装后的目录在

/usr/local/texlive/2019basic

之后我们很多命令到能用了比如:pdflatex, xelatex, luatex 我们来试试。

先装两个依赖:

tlmgr  install titling lastpage

中文字体

使用命令 fc-list :lang=zh(fontconfig 包) 来查看有哪些中文字体:

System/Library/Assets/com_apple_MobileAsset_Font5/b2d7b382c0fbaa5777103242eb048983c40fb807.asset/AssetData/Kaiti.ttc: Kaiti TC,楷體\-繁,楷体\-繁:style=Bold,粗體,粗体
/System/Library/Assets/com_apple_MobileAsset_Font5/1183acef85eb1efe456a14378a2eb985c09768c9.asset/AssetData/Lantinghei.ttc: Lantinghei TC,蘭亭黑\-繁,兰亭黑\-繁:style=Extralight,纖黑,纤黑
/System/Library/Assets/com_apple_MobileAsset_Font5/940db29a0ab220999d9a1dbe3eb0819a718057b5.asset/AssetData/Libian.ttc: Libian SC,隸變\-簡,隶变\-简:style=Regular,標準體,常规体
/System/Library/Fonts/STHeiti Medium.ttc: Heiti SC,黑體\-簡,黒体\-簡,Heiti\-간체,黑体\-简:style=中黑,Medium,Halbfett,Normaali,Moyen,Medio,ミディアム,중간체,Médio,Средний,Normal,中等,Media
/System/Library/Assets/com_apple_MobileAsset_Font5/db09870736c6892b6a56035428f2b1b6d0a954fd.asset/AssetData/WawaTC-Regular.otf: Wawati TC,娃娃體\-繁,娃娃体\-繁:style=Regular,標準體,常规体
/System/Library/Assets/com_apple_MobileAsset_Font5/ce85149bd68e9f8b

使用示例

pandoc  年终总结.md -o srs.pdf --pdf-engine=xelatex -V CJKmainfont='Heiti SC'

我看网上大多的示例都是使用的是 mainfont 结果出错,非得用 CJKmainfont 才行,真是很坑

这是因为,网上使用的模板,与默认的模板不同,默认的模板位于 pandoc 目录下,比如我用 brew 安装的 pandoc 其模板位于:

/usr/local/Cellar/pandoc/2.8.1/share/x86_64-osx-ghc-8.8.1/pandoc-2.8.1/data/templates

下面,其中使用的就是 CJKmainfont 这个变量来设置字体的。

至此,如何将 md 转换为 pdf 就已经是完成了。但遗留的问题就是:

对于我 md 里面使用的 graphviz , mermaid 图表,如何才能给我在 PDF 中转换出来呢?

模板

当使用 -s/--standalone 选项的时候,pandoc 会在自表示的文档在中,在需要时使用一个模板来添加页眉和页脚。如果要查看默认的模板,键入:

pandoc -D *FORMAT*
  • FORMAT 输出文档的格式。

例如

pandoc -D latex

我们可以使用 --template 来指定一个自定义的模板,或者,我们可以在系统的目录中对默认模板进行替换(将文件 templates/default.*FORMAT*放在用户的数据目录(通过命令 pandoc --version来查看)。(关于系统默认模板的目录位置,我使用 brew 安装的话是位于:/usr/local/Cellar/pandoc/2.8.1/share/x86_64-osx-ghc-8.8.1/pandoc-2.8.1/data/templates 下面:)

但是有几个例外:

  • odt 自定义 default.opendocument 模板
  • pdf 自定义 defaut.latex 模板(或 在使用 -t context 时修改 default.context ,使用 ms 的时候自定义 default.ms,或在使用 -t html 的时候定义 -t html )
  • docx pptx 没有模板。

模板会包含变量,我们可以通过命令行的 -V/--variable 来进行设置。

latex 模板语法

过滤器 Filter

Pandoc 提供了一个接口,用户可以用这个接口来编写程序(叫做过滤器)来在 pandoc 上的 AST (抽象语法树)进行操作。

Pandoc 由一系列的 读入器(Reader) 和写出器(Writer)组成。当我们将一个文档从一种格式转换为另外一种格式的时候,首先会由 pandoc 将输入文档转换为解析为 pandoc 的中间格式——abstract syntax tree(抽象语法树),然后由 Writer 来进行输出。AST 定义在 Text.Pandoc.Definition in the pandoc-typespackage. 模块中。

一个 Filter 就是一个修改 AST 的程序:

INPUT --reader--> AST --filter--> AST --writer--> OUTPUT

Filter 被看成是一个管道,其从标准输入读入,然后输出到标准输出。其会消耗,然后产生一个 pandoc 的 AST JSON 表示。Filter 可以用任何的程序写成。我们只需要在命令行中指定过滤器就行:

pandoc -s input.txt --filter pandoc-citeproc -o output.htl

有 一些第三方的过滤器: list of third party filters on the wiki.

   source format

(pandoc)

JSON-formatted AST

(filter)

JSON-formatted AST

(pandoc)

target format

如果我们要用 python 来编写 Filter 的话,可以使用 pandocfilters 这个包:

pip install pandocfilters

在最开头的例子中,我们可以来写一个过滤器:

#!/usr/bin/env python

"""
Pandoc filter to convert all level 2+ headers to paragraphs with
emphasized text.
"""

from pandocfilters import toJSONFilter, Emph, Para

def behead(key, value, format, meta):
if key == 'CodeBlock':
value[1] = 'code'
return CodeBlock(value[0],value[1])

if __name__ == "__main__":
toJSONFilter(behead)

toJSONFilter(behead) 会遍历 AST,然后对每个元素应用 behead action。如果 behead 没有返回值,那么这个节点就不会被改变;如果其返回一个对象,那么这个节点就会被替换;如果其返回一个列表,新的列表就会被拼接在一起。

在这个过滤器中 format, meta 没有被使用,但 format 提供了一个对目标格式进行访问的途径,meta 提供了对文档元数据的访问。

在我们的过滤器中,我们将 CodeBlock 中的内容就直接改成了 code。需要记住的是:

  • 我们的过滤器读取的是 JSON AST 表示,也就是说 key,value 都是常规的 JSON 类型数据

  • 我们的过滤器返回的一定要是一个 pandoc 类型,需要用 JSON 类型的数据来构造相应的对象

更多的办法,就需要我们去研究 AST 的 JSON 表示了。

Lua filters

传统的 pandoc filters 操作的是 JSON 表示的 AST,可以用任何语言来编写 filter 。

尽管我们可以使用任何语言来编写 filter ,但其拥有很多不好的地方。首先,读入 JSON 和写出 JSON 都是有开销的(每个 filter 一对,两次)。其次,一个 filter 是否工作依赖用户的环境是否安装了 filter 的依赖。

因此,从2.0 开始,就开始支持用 lua 来编写 filter 了,这样不需要任何额外的依赖。pandoc 内置了一个 5.3 版本的 lua 解释器。pandoc 的所有数据类型都已经注入到这个 Lua 环境中了

图表

并不是非常好和统一的方式,准备自己研究一个比较好的办法来实现他。

mermaid

需要通过装过滤器来完成,当前能看到有两个过滤器:

安装过滤器

pip install pandoc-mermaid-filter

安装 mermaid cli

yarn add mermaid.cli

或者

npm install mermaid.cli

不推荐全局安装。

开始转换

MERMAID_BIN=./node_modules/.bin/mmdc pandoc  年终总结.md -o srs.pdf --pdf-engine=xelatex -V CJKmainfont='Songti SC' --filter pandoc-mermaid-filter

但事实上,用过滤器的形式还是有点麻烦了,所以考虑一下如何用一个比较简单的方式来完成这个工作。

当然,为了概念上的统一,一条路走到最后,还是值得一试的。

graphviz

有一个预处理器可以让我们完成在 md 内插入 graphviz plantUML 图表。其本身就是为了 pandoc 而设计的。

Generic preprocessor (with pandoc in mind)

暂时不细究,考虑还最终是不是要这样做再进行仔细的考虑。

plantUML

同上

我的解决方案

我日常是使用 macdown 来写文档,也会使用 typora 来进行一些导出工作,因为其导出为 pdf 的效果非常棒。

在 macdown ,其导出的 PDF 不支持 书签式的 TOC,也就是 docx 中的大纲,所以才萌生了用 pandoc 来实现的想法。

其支持 mermaid, graphviz ,其具体的实现是:

  • graphviz 使用了 viz-js 来实现
  • mermaid 使用的是 mermaidjs 来实现。

就此看来,我如果使用 js 来做过滤器,同时将 viz-js 和 meraidjs 引入到过滤器中的话,就能达到我在编辑器中所查看到的效果,和转换出来的效果一致。将 graphviz ,mermaid 的图表都渲染成 svg 格式,这样的效果就非常的OK了。

pandoc github wiki filter 一节中,列出了用 nodejs 来编写过滤器的办法。有一个模块 pandoc-filter-node 来进行支持 js 的 filter 编写。

pdf 模板

markdown 解析器

我一般使用的是 Github Flavor markdown ,我们指定的时候加上 -f gfm 就可以了。

graphviz mermaid filter

这样实际上是可以达到与 我在 macdown 内进行编辑的时候的效果是一致的。

字体

  • 中文字体 使用 adobe 开源字体 adobe-fonts
  • 英文字体

字体的区别:

  • Serif 有衬线体 在字的笔划开始及结束的地方有额外的装饰,而且笔划的粗细会因直横的不同而有不同。相反的,Sans Serif 则没有这些额外的装饰,笔划粗细大致差不多。通常文章的内文、正文使用的是易读性较佳的 Serif 字体,这可增加易读性,而且长时间阅读下因为会以 word 为单位来阅读,较不容易疲倦。
  • Sans Serif 无衬线体 而标题、表格内用字则採用较醒目的 Sans Serif 字体,它需要显着、醒目,但不必长时间盯着这些字来阅读。
  • Monospace 所谓的等宽字体,是指每个字符宽度都一致的字体。一个著名的例子就是 Courier New 字体。因为字符宽度一致,所以特别容易对齐,能快速精确的定位到某行某列,因此经常用来显示代码。