Sky Watch

对于现在各种文档系统的吐槽

作为强迫症患者,选择合适的文档工具是个特别纠结的问题。过程中涉及到的两种主要需求是

  • 存档需求,要求文档包含足够的结构信息。比如 ConTeXt 里有个宏 \quotation,用来排行间引用,就比直接写引号要包含更多的结构信息。

  • 展示需求,要求工具有足够的设施用来实现各种排版效果。

同时满足这两个需求的工具才是好工具。

HTML + CSS

HTML 作为文档格式最大的问题就是结构上的表达能力太弱,简单地说就是 tag 的种类太少。比如你想写一段引文,只有一个 blockquote 可用,没有特殊的 tag 来表示引文出处之类的结构。原则上你可以写个 <div class="source"> 这样的 tag 来自己定义任何结构,而且在设计网页的时候这是个标准操作,但是用来存档的话就很弱鸡了,不如使用 XML,写起来差不多复杂。

HTML 5 加入了几个重要的结构标签,比如 articlesection,但有个很严重的问题,就是 HTML 5 没有 schema⋯⋯(是的,HTML 4 居然是有 DTD 的⋯⋯)这就是说 HTML 5 不能使用 XML 工具(包括 XSLT)来处理,只能使用专门的解释器。

HTML 的另一个问题是历史包袱太重,标准中充满了诸如 <font>, <i> 这种纯粹用于展示用途的标签。当然你可以选择不用,但是你不能保证别人不用,也不能保证你不需要处理包含这些标签的文档⋯⋯ (眼睛看向 LaTeX。)

EPUB 和其他电子书格式

这些电子书格式一般都是编译结果,也就是单纯用于展示。这些格式问题是它们都是打包的 HTML,一般是由设备内嵌的浏览器渲染,所以几乎一定特别难看⋯⋯ 这里的难看并不是指字体不好,或者行间距太小之类的,而是不使用高级的排版算法(比如断行和各种 micro-typography)。当然表面上来讲这不是电子书的锅,而是 HTML 和浏览器的锅,但是 HTML 是用来写网页的啊,浏览器是用来看网页的啊,好的排版算法只是网页渲染里一个非常微不足道的需求,高效地实现各种交互和动态效果远比排版算法重要的多,也是浏览器厂商关注的重点。然而电子书是本书啊!丫的是书啊!书!一本排版难看的书,不扔难道留着过年⋯⋯?所以电子书使用浏览器渲染,阅读器厂商这种懒惰的行为造就了电子书「难看」的印象。

一般来说我拿到 ePUB 之后会解包转成 TeX 重排一遍⋯⋯

Markdown 和其他简化 markup

我觉得所有这些格式里,Markdown 是最弱鸡但同时也是最无辜的一个。Markdown 的功能特别少,因为发明者 John Gruber 在设计 Markdown 的时候从来就没有想过要用它处理复杂的格式,Gruber 只是为了能方便地写 blog 而已。你去看看他的 blog 就知道 Markdown 为何如此简单了。

然而在懒惰的开源社区开始动歪脑筋以后,一切都改变了⋯⋯ Markdown 有了各种各样的扩展,巨硬甚至有人闲得没事搞了扩展用来写学术文档⋯⋯ 但是 TeX 已经存在了啊,有这个功夫还不如自己搞一套宏包⋯⋯

这就好像你想去赛车,本来你车库里就有辆 Nissian GTR, 调一调悬挂可以直接上,你却非要翻新一辆报废的夏利然后换台发动机⋯⋯

这种事也只有开源社区能干的出来⋯⋯

如果你执意要用 Markdown 处理复杂文档,你最终一定会遇到这些问题:

  • 基本控制符不分左右,无法处理复杂格式。Gruber 最初的 Markdown 处理器是一个基于正则表达式的 Perl 程序,大家都知道标准的正则表达式是不能处理任意层数的嵌套的(比如 HTML 里的嵌套 div),所以 Markdown 里的控制符没有必要分左右(因为反正不能嵌套⋯⋯)。另外由于同样的原因,很多 Markdown 处理器都不能可靠地处理这种情况:_aa*a$2*x_0$b*bb_(假设 $ 是行间公式)。

  • Markdown 标准并不是非常详细,很多行为都是未定义的,所以处理器之间存在兼容问题。比如 some_long_name 这样的表达,有些处理器会认为 “long” 应该加强(一般是斜体),有些会直接把下划线排出来(因为两边没有空格),还有一些有开关可以切换。

  • 处理中文文档不方便,这主要是上一条的特殊情况。由于中文字之间不空格,有些处理器会忽略中间的控制字符。

  • 没有扩展性,官方没有宏语法,你为一种处理器写的宏不能保证在其他支持宏的处理器都能用。

总之使用 Markdown 写复杂文档是件特别蛋疼的事。

AsciiDoc

表面上看起来,AsciiDoc 是一套和 Markdown 长得差不多的简单语法,实际上它的设计目标和 Markdown 完全不同。AsciiDoc 使用 DocBook 作为中间格式(而不像 Markdown 和其他语法是为输出 HTML 优化),从一开始就是为了给复杂文档提供一套简单且完备的语法。举几个高级功能的例子:

  • 区分 article 和 book 类型。在 book 文档类里有 frontmatter 和 backmatter,附记、摘要、引用、索引应有尽有。

  • 所有的元素都可以附加属性,AsciiDoc 自己不用的属性会带到输出文档里。

  • 基本控制符有两套,用来区分周围有没有空格的情况。

  • 所有的 block 元素都可以加标题,都可以带 id 用来交叉引用。

  • 记得我之前说 HTML tag 不够丰富么?AsciiDoc 里的引文有特殊的属性用来标记出处。

  • 解决 block 元素之间从属关系的歧义。比如你在一个列表后面写了一个段落,你如何告诉处理器这个段落是列表的一部分,而不在列表之外?AsciiDoc 有特殊语法解决这个问题。随之而来的另一个问题如果你有一个一级列表,里面有个二级列表,后面有个段落,你怎么告诉处理器这个段落属于一级列表而不属于二级列表?AsciiDoc 把这个问题也解决了。

AsciiDoc 是我非常常用的语法,它有足够丰富的设施用来实现一般文档可能碰到的所有需求,同时源码写出来简单易读。我以前的 blog 引擎使用 Flask-FlatPages 渲染文章,默认使用 Markdown 语法。我为了用 AsciiDoc,最近重写了 blog 引擎,加入了自定义后端支持。这篇文章就是用 AsciiDoctor(一个 Ruby 写的 AsciiDoc 处理器)渲染的。

*TeX

在这些所有的工具里,TeX 有特殊地位。首先,TeX 是一个真正的排版工具,它自己决定每一个字、每一个标点符号的位置(而不依靠像浏览器这样的前端),相比之下像 Markdown 和 AsciiDoc 只能叫「文档工具」。更重要的是,Knuth 是现代极其少见的对数学、计算机和排版都有深刻理解的文艺复兴男。而 TeX 也是这些文档工具里唯一一个表达能力、排版效果都很优秀,同时支持数学公式的。所以尽管现在看来 TeX 有各种各样的缺点(大多是历史包袱),但在需要排高质量学术文档的时候,TeX 仍然是第一选择,也是事实上的标准。在民用领域,尽管有些工具可以在某些方面超越 TeX,但没有任何一个系统在所有方面都有优势。同时,TeX 应该说是计算机排版领域的一个开创性工作,其中的断行和公式布局等算法到今天依然在行业中广泛使用(可能是唯一可用的)。在像 InDesign 等现代排版系统中,断行算法基本都是 TeX 断行算法的某种改进版。而 Office 里的新版公式编辑器(民用领域除 TeX 以外唯一可用的公式排版工具)也是用了改进后的 TeX 公式布局。

当然我们现在说的 TeX 系统,一般是指 TeX 编译器加上一大堆宏包,其中最常用的两套宏包是 LaTeXConTeXt。这两套宏包从设计理念上基本是两个极端,所以必须分别讨论。

LaTeX 面向的主要用户是学术文档的作者。对于这些人来说,LaTeX 与其说是个排版工具,不如说是个像 Word 一样的字处理程序。LaTeX 自带一套还算说得过去的默认风格,你只需要告诉它文章的标题、作者、摘要和正文,它就会自动排出一个看上去还行的文档,但是如果你想完全自己设计版面,或者使用 LaTeX 本身没有的高级功能,就有点麻烦了,需要使用第三方宏包。比如如果你需要排一本正经的书,那就需要 Memoir 宏包。这个宏包提供了许多专业排版必须的工具,比如它有个专门的宏用来排 epigraph,并且允许分别设置 stock size 和 paper size。由于 LaTeX 的用户众多,它有海量的第三方宏包可供选择。但是这些宏包有两个特点,导致了 LaTeX 的几乎所有不足:

  • 这些宏包是很多不同的人写的。

  • 这些宏包的作者很多都是科研人员,他们有些不懂编程,有些不懂排版,大部分人两者都不懂。

首先,每个宏包作者都有自己的命名风格和宏调用风格,所以在同一个文档里调用多个宏包的宏就会导致代码可读性很差。同时,不同的宏包之间很可能互相冲突,导致莫名其妙的 bug。另一个很坑的因素就是很多宏包都不能保证向前兼容,所以说不好哪天你的文章就不能编译了。这就导致了 LaTeX 文档实际使用时的一个重要缺陷:在空间和时间上都不能很好的移植。如果你拿到了别人的文档源代码,大多数情况下你是不太可能完全看懂的,而如果这是个很老的文档,可能连编译都不行,因为其中用到的某些宏已经改了名字,或者压根就不存在了。想改?看都看不懂,怎么改⋯⋯

LaTeX 社区宏包的另一个重要缺陷就是大部分作者并不懂如何设计一套好的宏,导致在写文档时很难分离结构和表示。这和前面的几个缺陷共同就导致了 LaTeX 文档并不适合作为存档格式。

在意识到 LaTeX 的这些缺陷后,我转向了 ConTeXt。这个系统在设计上与 LaTeX 有两点巨大的区别。ConTeXt 的作者是个专业排版工作者,所以它是一个正统的排版工具,默认样式惨不忍睹,用户需要从头定义好自己的样式才能使用;而 ConTeXt 不依赖第三方宏,几乎所有的功能都可以用内置宏完成,而且设置起来极其方便并且风格统一。如果你需要改变某种现有元素的风格,只需要写 \setupxxxx[a,b,c][d=e, f=g];如果你需要新定义一种元素,只需要写 \definexxxx[a,b,c][d=e, f=g]

比如我在排作业的时候,定义了一套新的列表叫 problem,大题用数字编号,小题用字母编号,大题之间空 1cm,只需要如下几行:

\defineitemgroup[problem][levels=2]
\setupitemgroup[problem][1][n, inmargin]
\setupitemgroup[problem][1][style=\tfb, inbetween={\blank[1cm]}]
\setupitemgroup[problem][2][a, standard]
\setupitemgroup[problem][2][left=(, right=), stopper=]

在写题的时候使用

\startproblem
  \item ...
  \item
  \startproblem
    \item ...
  \stopproblem
\stopproblem

这样就很好地做到了结构与样式分离。当然 ConTeXt 也不是完美的,它在时间上的不可移植性比 LaTeX 更严重,在我使用的这六年里就已经遇到了一些以前的文档现在不能编译,或者编译以后乱掉的情况,所以 ConTeXt 文件也不适合存档。

关于 ConTeXt 的最后一个槽点,就是它实在是太强大了。裸的 TeX 也很强大,但是 TeX 的强大是因为它只包含了很少量的基础能力,复杂性来源于这些基础能力的组织形式。ConTeXt 正好相反,它包含的宏数目众多,每个宏都有大量的选项,有些宏还有多种用法,这些所有的信息很难完整的整理到一套文档里。ConTeXt MkIV 的手册至少七年前就开始动工,到现在还没写完,其中光是如何定义和使用字体的部分就长达 70 页(而且版面还挺宽的⋯⋯)。ConTeXt 中包含一个扩展的 Metapost 实现,这部分的手册单独成书,也长达 376 页。除此以外官方还有大量的手册和教程,专门介绍系统的某些方面(比如如何使用 visual debugging)。即便如此,我在使用过程中还是遇到了一些需要使用 undocumented feature 的情况,我在邮件列表里问了以后才知道。

XML-based applications & Docbook

据说现在出版业有转向使用 XML 的大趋势。工业上使用的 XML 工具,我们草民是用不了的。如果你想搞家用 XML 流程,目前大概就只有一个选择,就是 Docbook。使用 XML 显而易见的好处就是不用担心可移植性。就算以后 XML 格式淘汰了,你也能猜出每个 tag 的意思,然后自己写个程序解析。

Docbook 用两种用法,普通的一种是你随便写个程序,或者使用 Pandoc 这样的转换器,把 Docbook 文档转成其他格式(比如 TeX),然后用相应的处理器处理。而比较文艺的一种用法是使用 XSLT 处理器把 XML 渲染成最终格式,这就是所谓的「纯 XML 流程」,据说在工业界很时髦。我还是比较倾向于第一种方法,原因在后面会说到。使用 XSLT 就需要有相应的 XSLT 样式表。Docbook 有一套免费的样式表,支持输出多种格式,包括 HTML,PDF,和 ePUB。比较神奇的是,输出 PDF 的流程也是纯 XML 的,需要先使用样式表把 Docbook 转成 XSL-FO,再使用 FO 处理器(比如免费的 Apache FOP)渲染成 PDF。

在我使用 Docbook 的经验中,数学公式的处理是最大的坑。Docbook 的 schema 并没有规定数学公式应该如何写,所以这里就有三种可能:

  1. 由于 Docbook 本身是 XML,一种自然的写法是使用 MathML。但并不是所有前端(最终渲染出文档的东西)都支持。现在的浏览器一般都支持 Presentational MathML,所以对于 HTML 和 ePUB 输出来说,MathML 是可用的。但是 PDF 输出就很麻烦了,FOP 压根就不支持渲染公式。所以使用纯 XML 流程只能输出到 HTML。

    MathML in ePUB
    Figure 1. iBook 渲染包含 MathML 的 ePUB。文本断行难看到爆。
  2. 写 TeX 公式。输出 HTML 的话依然可以使用 MathJax 做到纯 XML 流程。需要 PDF 的话可以先转成 TeX 再编译;我还没有用过这种方法,因为我一直在试图搞定下面这种方法。

  3. 写 MathML,输出 PDF 的时候先转成 TeX。这就需要一套把 MathML 转成 TeX 格式的程序,理论上 XSLT 可以方便地实现这个功能。我想到这个方法以后就开始着手写一套 XSLT 2 实现,但是在写的过程中发现一个惊天神坑:居然没有好用的免费 XSLT 2 处理器⋯⋯ 如果你搜索 XSLT 处理器,出来的结果一般都是 XSLT 1 的(其实连浏览器都支持 XSLT 1),支持 XSLT 2 的免费处理器好像就只有 Saxon 一个。但是 Saxon 的尾递归优化有 bug(好像是吧,我记不清了⋯⋯),所以我没法用它替换 MathML 中的众多 entities。我觉得做这件事最好还是找个有 XML 库的通用语言,XSLT 有自己的局限性,但是这样就不文艺了⋯⋯

总的来说,Docbook 是个适合存档的格式,但是目前并没有足够好用的免费工具链。

Scribble

最后说说 Racket 的官方文档系统 Scribble。Racket 是一个实现各种奇葩小语种的平台,而 Scribble 正是这个平台上的小语种之一。Scribble 的语法看起来和 TeX 差不多,但实际上它是个正经的函数式语言,语法可以和 Racket 的 s 表达式无缝转换。比如下面的代码

This is @italic{important}.

就等价于

"This is " (italic "important") "."

并且还可以内嵌 s 表达式,所以原则上你可以在文档中嵌入任何形式的逻辑,包括 Racket 的宏,这就又比 TeX 宏强大许多了。

Scribble 自带了很多写 Racket 文档有用的宏,和一些生成计算机期刊论文的文档类,但除此以外并没有其他丰富的格式。我觉得它是个很有潜力的系统,如果你有足够的意愿来设计一套自己的宏(甚至可以搞一套输出 XSL-FO 的前端),Scribble 大概会非常好用。但是如果你像我一样懒的话,Scribble 只能用来写写程序文档。

不爽的总结

总的来说,个人排版/出版和其他多媒体领域(比如视频编辑)一样。如果你看看网上的普世价值,都说现在的免费程序可以让你足不出户就排版并发表自己的书和文章,如果你要求不高的话也的确如此。但是如果你对产品的要求和工业上一样严格,就会发现免费的工具要不然就远远达不到要求,要不然就是年代过于久远,已经开始不能适应现在的新技术和流程。像我这样的弱鸡此时就只能在家打打游戏,坐等下一位大牛的出现。