1. 核心思想:概念与文档树

在写代码之前,这一章先把 CallioText 看待文档的方式讲清楚:什么是概念,概念为什么分成两层, 文档树上有哪几种节点,编辑器和印刷器又各自负责什么。这一章没有代码, 但后面的每一章都建立在这一章的基础上,值得慢慢读完。

从 LaTeX 说起

用 Word 写过论文的人都记得那种滋味:定理的样式要统一,编号要连续,而这一切全靠手来维持。 加粗是自己加的,「定理 3.2」是自己编的;在第三个定理前插入一个新定理, 后面的编号便全部作废,只能挨个改过去;哪天想统一换掉定理的样式,就只好从头到尾把文章翻一遍。

LaTeX 用户走的是另一条路。在 LaTeX 里,一个定理写作:

\begin{theorem}
    ...
\end{theorem}

作者只说「从这里到这里是一个定理」,字体、编号、间距都交给样式系统去操心; 想换样式,改一处定义就够了。这背后是一个值得记住的想法:

文档的内容与结构,应当和它的外观分开。作者负责声明结构,外观交给渲染规则统一处理。

不过 LaTeX 毕竟是互联网时代之前的产物:它的产出是为打印准备的页面, 它的输入是随着文章一起膨胀的代码。CallioText 做的事情,是把同一个想法带进图形化编辑器: 交互接近 Word,文档的内部结构像 LaTeX 一样明确,外观由渲染规则决定, 而输出端是自由的,可以是网页,也可以走向 PDF。

概念

上面那个 theorem,在 CallioText 里叫做一个概念(concept)。 概念就是你为文档中某一类成分定义的类型:定理、证明、脚注、引用块、分隔线,都可以是概念。 每个概念规定两件事:

  1. 这类成分有哪些参数。比如「定理」可以有标题参数(显示为定理、引理还是命题)和编号方式参数。
  2. 这类成分如何渲染。编辑时的外观和输出成品时的外观是分开定义的两套,互不影响。

概念分两层

CallioText 的概念系统有一个关键设计:概念分成两层。 用 OOP 来类比,一级概念相当于类,二级概念相当于这个类的对象。

一级概念(FirstClassConcept)二级概念(SecondClassConcept)
谁来定义引擎,用代码任何人,可以在运行时创建(比如从 JSON 加载)
定义了什么参数原型(有哪些参数、默认值)、渲染逻辑继承某个一级概念,重写部分参数的默认值,或把某些参数固定住

举例:一级概念 primary 表示「一段需要突出展示的话」, 定义了 titleprefixordering 三个参数和渲染规则(标题和编号在前,正文在后)。 二级概念「命题」继承 primary,把标题默认值设为「命题」; 二级概念「项目」也继承 primary,但标题显示为项目名。 两个二级概念共享同一份渲染代码,只是参数不同。

这样设计有两个动机:

命名惯例:一级概念用小写英文单词命名,如 primarystrongquote; 二级概念用写作者语言里的实际词汇,如「命题」「强调」「引用」。

文档树:七种节点

一篇文档是一棵树。树上的节点共有七种,其中五种是概念节点(需要指定自己使用的概念), 两种是纯内容节点:

节点type角色允许的子节点
文本text叶子节点,实际的文字
段落paragraph一段文字的容器文本、行内、支撑
行内inline行内语义:强调、链接、行内公式文本、行内
group块级语义:定理、证明、引用块段落、组、结构、支撑(不能直接放文本或行内)
结构structure有固定内部布局的语义,比如一行分多列只能是组
支撑support无内容的占位元素:分隔线、图片无(占位空文本)
抽象abstract附着在节点上的独立子文档:脚注、批注;文档的根也是抽象节点与组相同

表格之外,有几条规则值得单独说明:

概念节点的字段

每个概念节点携带一组固定字段。以组节点为例:

interface GroupNode {
    type: "group"
    idx: string
    concept: string
    parameters: ParameterList
    children: NonLeafNode[]
    abstract: AbstractNode[]
    relation: "chaining" | "separating"
}

relation 需要解释一下。两个相邻的「列表」块,可以是两个独立的列表(分离), 也可以是同一个列表被中间内容打断后的延续(相连)。渲染和编号都能感知这个区别: 比如自动编号可以配置成「遇到分离的块重新从 1 开始,相连的块接着编」。 写作者在编辑器里可以随时切换。

编辑器与印刷器

处理这棵树的部件有两个:编辑器和印刷器。它们的关系用编译器来比喻最贴切。 文档树是一种中间表示;编辑器相当于编译器的前端,把高级语言(写作者的图形化输入)编码成中间表示; 印刷器相当于编译器的后端,把中间表示翻译成目标语言(网页,或者进一步的 PDF 输出)。 前端和后端只通过中间表示打交道,所以同一棵树可以有完全独立的两副外观。

印刷器有一个重要特性:正式渲染之前,它会先把整棵树按文档顺序完整遍历一遍(这一步叫预处理)。 原因很直接:像定理编号这样的信息,单看一个节点是算不出来的,第 5 个定理的编号是 5, 必须数过前面 4 个才知道。预处理就是把这类全文级的信息先算好,渲染时每个节点直接取用。 自动编号和交叉引用都建立在这一步上,第 6 章会具体使用它。

本章要点