orez 的故事

2

去年 8 月,我写了「zero 的故事」系列,介绍了自己写的一个很小的文式编程工具 zero。现在郑重宣告,它死了。

事实上,zero 的生死,对这个世界没有任何影响。尽管从理论上说,我是它唯一的用户,但是它的死对我也没有任何影响。因为在它死去之后,orez 就诞生了。不过,orez 的生,对这个世界继续不会有什么影响。

zero 之所以死,是因为它的结构与实现代码太混乱。虽然它能够很好的完成我赋予它的使命,但它还是死了。死于我的不务正业以及对美的追求。

我听见有人在笑。「对美的追求」,写出这么一句的我,也在笑。准确的说,是对更为紧凑的代码的追求。zero 用的代码并不多,大约 1600 行 C 代码,所用的数据结构都是 GLib 所提供的。现在 orez 的代码大概 1100 行。仅仅缩减了 500 行代码,倒也不值得太过于称道,值得骄傲之处在于,整个 orez 的设计与实现的过程是较为严格的贯彻了文式编程的思想——文档与代码是相互结合的,并且它们的撰写是同步进行的。

从 orez 的源文档中可以提取到 orez 的全部代码,见 main.c。由 orez 的源文档转换而成的 PDF 文档,恕我自私,我将它藏匿了起来。

它存在的意义

Linus said:「Talk is cheap, show me the code.」

Orez said:「Code is cheap, show me the doc.」

Orez 的世界观是,程序的文档和程序本身可以放在一起来写。你可以用文本编辑器创建一份文本文件,然后在这份文件中写一段文档,然后再写一段代码。让这两个过程交替进行,像拧麻花一样,像织毛衣一样,像左右互搏一样,像左脚踩右脚右脚又踩左脚的梯云纵一样。orez 认为,这样可以实现码农们男耕女织的美好新生活。

按照 orez 的想法,我创建了一份名为 km.orz 文本文件,然后写了下面这段话:

\type{km_init} 函数实现了 $k$ 均值聚类初始化过程:从样点集合中选取指定数量的样点
作为初始种类中心,然后初始化种类树——对样点集合进行首次分类。

\type{...}$...$ 均为 TeX 排版标记。不懂 TeX,也可以用其他文档排版语言提供的标记来构造相同的内容。

接下来,在 km.orz 中写一段代码:

@ agn_km.c #
static AgnTree * km_init(AgnArray *points, size_t K)
{
        size_t *indices = generate_indices(K, 0, points->n - 1);
        # 初始化种类中心 -> init_centers @
        # 初始化种类树 -> class_tree @
        agn_array_free(init_centers);
        free(indices);
        return class_tree;
}
@

其中,@ ... ## ... @ 以及 @ 这些奇怪的符号,是 orez 的暗语,现在可以不用理睬它们。

接下来,我觉得有必要解释一下 km_init 这个函数的返回值的数据结构,于是继续撰写文档:

\type{km_init} 函数的返回结果是种类树,它是 \type{AgnTree} 的一个实例。种类树
的结构分为三层,根结点,亦即第一层结点,不存放数据;第二层结点表示种类,存放的数据为
种类中心。第三层结点存放分配到相应种类的样点序号。

如果没有上述文档内容的解释,该怎么理解 km_init 返回的那个 class_tree 是什么东西呢,从它的类型定义能看出来吗?

typedef struct AgnTree {
        struct AgnTree *upper;
        struct AgnTree *next;
        struct AgnTree *lower;
        void *data;
} AgnTree;

从它的类型定义,只能看出来它是一棵树。

km_init 函数的全部代码分析吗?此时,km_init 函数还没有完全的写出来,它只是个外壳,它的主体内容目前只是两个占位语句,即:

# 初始化种类中心 -> init_centers @
# 初始化种类树 -> class_tree @

即使 km_init 函数已经完全实现出来了,通过它的源代码能理解 class_tree 吗?

static AgnTree *km_init(AgnArray *points, size_t K)
{
    size_t *indices = generate_indices(K, 0, points->n - 1);
    AgnArray *init_centers = agn_array_alloc(K);
    for (size_t i = 0; i < K; i++) {
        init_centers->body[i] = agn_point_copy(points->body[indices[i]]);
    }
    AgnTree *class_tree = agn_tree_alloc();
    for (size_t i = 0; i < K; i++) {
        agn_tree_prepend(class_tree, init_centers->body[i]);
    }
    assign_points(class_tree, points);
    agn_array_free(init_centers);
    free(indices);
    return class_tree;
}

聪明人也许很快就能看出,class_tree 的根结点存储了一个样点链表的指针;第二层结点存储了 K 个随机样点,而且这些样点似乎扮演的是什么什么中心点的角色。那么 class_tree 的结构就是这样了吗,它还有没有第三层结点?这需要看 assign_points 函数的实现方能知道。此时,assign_points 函数我还没有实现……即使它实现了,通过它能理解 class_tree 的结构吗?

static void assign_points(AgnTree * class_tree, AgnArray * points)
{
    for (size_t i = 0; i < points->n; i++) {
        AgnTree *t = class_tree->lower;
        for (AgnTree * it = t->next; it != NULL; it = it->next) {
            if (agn_point_cmp(it->data, t->data, points->body[i]) <= 0) {
                t = it;
            }
        }
        size_t *id = malloc(sizeof(size_t));
        *id = i;
        agn_tree_prepend(t, id);
    }
}

看懂了 assign_points 函数的实现,如果此时还没有忘掉自己原本的意图——弄懂 class_tree 的结构,那么现在应该能够得出结论:

\type{km_init} 函数的返回结果是种类树,它是 \type{AgnTree} 的一个实例。种类树
的结构分为三层,根结点,亦即第一层结点,不存放数据;第二层结点表示种类,存放的数据为
种类中心。第三层结点存放分配到相应种类的样点序号。

可是,经过逐层分析所所得出的结论,不正是前文中我撰写的那段程序文档内容吗?

我,作为写代码的人,可以在完全实现 km_init 函数以及它的辅助函数 assign_points 之前,就能理解 class_tree 的结构,但是作为阅读代码的人,需要通读所有代码才能弄明白一件在我看来是再简单也不过的事。

有一个问题,为什么不直接将为代码所写的文档内容作为注释嵌入到代码之中?

代码中的注释肯定有助于理解代码,但是这些注释之间的联系极为微弱,它没法像文档那样划分章节,并在这些章节之间建立关联。我们阅读一份文件,总是习惯自上而下的阅读。这种阅读习惯导致我们先看到 assign_points 函数的注释及其实现代码,但是我们却被迫的先跳过它,去看下面的 km_init,如若不然,你就很难搞明白 assign_points 的真正作用。在看了 km_init 的注释及代码之后,再回过头来看 assign_points 的实现。为了阅读一份比较长的源代码,你不得不上下求索那些漫漫且修远的函数调用关系,然后顺着这个关系去阅读代码及其注释……最终,恭喜你,你终于在大脑中构建出了一份文档片段与代码片段的混合物——orez 的源文档。

安装

$ git clone https://github.com/liyanrui/orez.git
$ cd orez
$ make
$ sudo make install

绕出

km.orz 是以 orez 所提倡的那种编程方式的一个示例,可使用以下命令获取它:

$ wget https://github.com/liyanrui/orez/raw/master/example/ConTeXt/km.orz

km.orz 包含了许多 C 代码片段,还有一部分 Bash 代码,其他内容完全由 ConTeXt(一种 TeX 格式)标记文本构成。这些 C 代码片段按照我个人的行文习惯以一种较为容易理解的逻辑顺序嵌于 km.orz 文件之中,如何将它们提取出来交给 C 编译器呢?

从编译器/解释器的角度来看,orez 的行为是非常荒谬的。因为 orez 似乎将编译/解释器所喜欢的代码片段顺序完全的打乱了,像是将一根线弄的乱成一团。事实上并非如此,orez 在打乱代码片段顺序的同时,也为每份代码片段放置了一些标记。通过这些标记,可以随时将这些代码片段重新排列为编译器偏好的那种形式并保存为程序代码文件。

km.orz 文件中,有一份名为「agn_km.c」的代码片段:

@ agn_km.c # ^+
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include "agn_km.h"
@

注: @ ... # 中的 ... 便是代码片段的名称。

可以将它想象为一根线的线头。若系统中已经安装了 orez,使用以下命令,可将这根线——km.orz 中所有这个代码片段相关联的代码片段抽取出来:

$ orez --tangle --entrance="agn_km.c" km.orz --output=agn_km.c

抽取结果是可编译的 agn_km.c 文件。

orez 支持短命令选项,上述命令可简写为:

$ orez -t -e "agn_km.c" km.orz -o agn_km.c 

km.orz 中不止一根线。例如还有一根线,其线头分别是「ang_km.h」。orez 可将多根线一并绕出:

$ orez -t -s "," -e "agn_km.h, agn_km.c" km.orz -o "agn_km.h, agn_km.c"

-e-o 选项的值相同,这只是偶然,并非必然。前者是各个「线头」的名称,后者是绕出的各条「线」的名称,它们必须逐一对应,不能乱了次序。

其实,km.orz 还有一根线头「gen-km-src.sh」,通过它可以绕出一份 Bash 脚本:

$ orez -e gen-km-src.sh -o gen-km-src.sh km.orz

执行这份 Bash 脚本可从一组 .orz 文件中绕出一个 $k$ 均值聚类的测试程序的全部源码文件。

注:由于不便将全部源码文件都放出,所以在上述示例中绕出的源码文件或 Bash 脚本均不可用。

若线头的名称与绕出的源码文件名称相同,可以省略 -o 选项。例如:

$ orez -t -s "," -e "agn_km.h, agn_km.c" km.orz

上文所用的「线头」与「线」的比喻,不是我创造的。在很久很久很久以前,Donald Knuth 将这个过程称为 tangle。可能是因为这个过程,类似于从一团混乱的线团中找到一根线的线头,缠在绕线器上,将它抽取出来。Knuth 所写的文学编程工具 WEB,用于从 WEB 文件中抽取可编译的 PASCAL 代码的工具就叫 tangle,「orez -t」与之类似。

编程语言是形式化的,遵循严格的机器规则——图灵机,或遵循某种数学演算规则——Lambda 演算。orez 试图将自然语言与编程语言结合到一起,有人对此持否定态度。不过,他们否定的角度完全是错的。他们非常错误的将文式编程理解为让自然语言凌驾于编程语言之上,或者让编程语言更像自然语言。事实上,文式编程强调的是代码片段出现的顺序符合人类的阅读习惯,并且对代码进行良好的注解,并非在强调自然语言可以替代程序语言。不过,认为直接通过阅读源码就可以弄懂源码的人,在使用「orez -t」从 orez 源文件中绕出可编译的代码之后,可以将 orez 源文件删除,以此表示对 orez 世界观的蔑视。

秩序

虽然在 orez 源文件中,代码片段并非是按照编译器的逻辑顺序分布的,但是 orez 提供了一些特殊的标记,通过这些标记可以随时将代码片段按照编译器或解释器的逻辑顺序提取出来。这是上一节「绕出」部分所讲述的内容。

我们在 orez 源文件中实际上维护了代码片段的两种顺序,一种是人序,另一种是机序。人序是可见的,当我们从上而下逐行阅读 orez 源文件或它经由排版软件产生的文档时,所看到代码片段的排列顺序便是人序。机序,是指代码片段按照程序代码编译器或解释器的运作规律所建立的顺序。在 orez 源文件中,代码片段的机序由几个特殊符号来维护。

代码片段的定义与引用

最基本的机序是代码片段的定义与引用。定义一个代码片段,基本格式如下:

@ 代码片段的名称 #
[... 代码片段 ...]
@

@ 是代码片段名称的起始符。# 是代码片段名称的终结符。

下面是一份来自 km.orz 的代码片段的定义:

@ 样点重新分类 #
empty_classes(class_tree);
assign_points(class_tree, points);
@

代码片段的名称可以是除 @# 之外的任意字符。代码片段必须以 @ 作为结尾。

每个代码都可以作为一个起点,orez 可从它开始绕出一份代码。只不过有的代码片段可以绕出一份很长的代码,有的只是绕出个线头。譬如上面的「样点重新分类」这个代码片段,从它开始,绕出的代码是这个代码片段本身:

$ orez -t -e "样点重新分类" -o foo.c km.orz

若某个代码片段引用了其他代码片段,那么以它为起点绕出一份比它更长一些的代码。代码片段的引用语法如下:

# [... 被引用的代码片段的名称 ...] @

与代码片段名称的格式相反,此时 # 变成了代码片段引用标记的起始符,而 @ 变成了代码片段引用标记的终结符。

例如,在 km.orz 中,上述的「样点重新分类」这个代码片段被另一个代码片段「agn_km.c」引用了,后者的定义如下:

@ agn_km.c # +
<agn-km-classify>
AgnTree * agn_km_classify(AgnArray *points, size_t K)
{
        AgnTree *class_tree = km_init(points, K);
        while (!class_tree_stablized(class_tree, points)) {
                # 样点重新分类 @
        }
        return class_tree;
}
@

若以「agn_km.c」为起点,在绕出其代码的过程中,orez 会遇到代码片段「样点重新分类」的引用。此时,orez 会先绕出代码片段「样点重新分类」的内容,然后再继续绕出「agn_km.c」的内容。代码片段的引用链越长,orez 绕出的代码也就越长。

代码片段的累加

除代码片段的引用可以延长绕出的代码之外,还有一种同名代码片段的机制可以实现这种效果。所谓同名的代码片段,就是先定义一个代码片段,例如:

@ agn_point.c #
AgnPoint *agn_point_copy(AgnPoint *x)
{
        AgnPoint *y = agn_point_alloc(x->n);
        y->n = x->n;
        for (size_t i = 0; i < y->n; i++) y->body[i] = x->body[i];
        return y;
}
@

接下来,还可以定义同样名称的代码片段,但是代码片段名称终结符 # 之后必须跟随 +^+ 运算符。例如上文中已经出现的那份代码片段:

@ agn_point.c # +
void agn_point_free(AgnPoint *x)
{
        free(x->body);
        free(x);
}
@

可将同名的代码片段视为一个数组中的元素。+ 表示将当前的代码片段追加到数组的尾部,而 ^+ 表示将当前的代码片段追加到数组的首部,它们只能出现在代码片段名称终结符 # 之后,且与 # 处于同一行。当 orez 对同名的代码进行绕出时,它会从这个数组中的首个元素开始,逐一进行代码绕出。

同名的代码片段,很有效缓解了我的命名恐惧症。在撰写 km.orz 的过程中,大部分代码片段都是同名的代码片段,主要借助 +^+控制它们的机序。

标签

当我实现了 orez 对基于 +^+ 的同名代码片段的支持之后,不禁暗自佩服自己,竟然也能想出一些好主意。不过,很快就发现了问题,单凭 +^+ 来控制同名代码片段数组元素的次序,无法实现在数组的指定位置插入一个同名代码片段。于是,我又想出一个好主意,为代码片段附加一个标签。例如,上文出现的代码片段「agn_km.c」便是一份带标签的代码片段:

@ agn_km.c # +
<agn-km-classify>
AgnTree * agn_km_classify(AgnArray *points, size_t K)
{
        AgnTree *class_tree = km_init(points, K);
        while (!class_tree_stablized(class_tree, points)) {
                # 样点重新分类 @
        }
        return class_tree;
}
@

<agn-km-classify> 便是标签,它只能出现于代码片段名称终结符的下一行。我们将这份代码片段称为代码片段「agn_km.c <agn-km-classify>」。

现在,若想将一个代码片段插入到代码片段「agn_km.c <agn-km-classify>」在同名代码片段数组中的位置之前,亦即前者在同名代码片段数组中的下标值比后者小 1,此时便可借助代码标签来实现这一目的,即:

@ agn_km.c # <agn-km-classify> ^+
static bool class_tree_stablized(AgnTree *class_tree, AgnArray *points)
{
        # 生成新旧种类中心集合 -> centers 与 new_centers @
        # 判断 centers 与 new_centers 是否相同 -> stablized @
        for (size_t i = 0; i < centers->n; i++) {
                agn_point_free(centers->body[i]);
        }
        agn_array_free(centers);
        agn_array_free(new_centers);
        return stablized;
}
@

代码片段标签在参与 +^+ 运算时,它们只能处于当前代码片段名称的终结符 # 之后,运算符之前。

织出

orez 可将 orez 源文件转换为 YAML 文件。Donald Knuth 将类似的过程称为 weave(织出)。因为程序文档像一张网,不仅文档的内容中存在着许多交叉引用,而且代码片段之间也存在着许多交叉引用——代码片段之间的引用关系以及带标签的代码片段的 +^+ 运算均产生交叉引用。事实上,将 orez 源文件转换为 YAML 文件的过程中,orez 程序的主要工作是生成代码片段之间的交叉引用信息。至于文档内容中的交叉引用,直接交于可与 orez 相结合的文档排版工具来处理。

基于 YAML 文件,orez 能够与许多排版工具取得结合。我们的时代已经进步到了不需要再为获得一份排版美观的程序文档而像 Donald Knuth 写 TeX 那样拼的程度了。

对于网页形式的程序文档,有 Markdown 这种的轻量级的标记语言,也有 reStructuredText 这种重量级的标记语言,实在不济,还有 HTML 5 + CSS 3 + JavaScript。它们都有助于生成美观的网页形式的程序文档。对于由 orez 源文件转换而来的 YAML 文件,主流的脚本语言均能借助一些 YAML 解析库将其转换为 Markdown,reStructuredText,或 HTML 5 + CSS 3 + JavaScript 的文档形式。如果文档中需要数学公式,可以学点 TeX 数学标记,将显示数学公式的任务交给 MathJax 来做。

对于面向纸本书籍排版的程序文档,有 TeX,LaTeX 或 ConTeXt。过去,这些排版工具对中文支持的不够好,排版字体的配置极为繁琐。现在有 XeTeX 与 LuaTeX 引擎了。

总之,过去很难解决的文档排版问题,现在都不是问题。现在的问题是掌握这些工具。

在广泛意义(不限于印刷意义)的文档排版方面,我较为熟悉 Nikola 与 ConTeXt。Nikola 可以将 Markdown 或 reStructuredText 之类的标记语言转化为静态网页。由于 Nikola 支持代码高亮显示,能嵌入 MathJax 实现 TeX 数学公式的显示,能够以静态站点的形式发布文档,因此我将它作为 orez 战略合作伙伴,写了一份名为 orez-md 的 Python 脚本实现二者的沟通交流。

获取 hello-world.orz:

$ wget https://github.com/liyanrui/orez/raw/master/example/markdown/hello-world.orz

从 hello-world.orz 中织出 hello-world.md 的命令为:

$ orez -w hello-world.orz -o hello-world.yml
$ orez-md hello-world.yml > hello-world.md

$ orez -w hello-world.orz | orez-md > hello-world.md

然后将 Markdown 文件交给 Nikola 来处理即可。

注:Nikola 的安装与配置指南,见:https://getnikola.com/getting...

对于面向纸本书籍排版的程序文档方面,要从很久很久之前说起。早年,受王垠的蛊惑,喜欢上了 ConTeXt,还为它写了一个支持中文排版的插件。为了继续维护这种喜欢,我将 ConTeXt 也作为 orez 的另一个战略合作伙伴,写了一份名为 orez-ctx 的 Python 脚本实现二者的沟通交流。可以通过以下命令,可将前几节用作示例的 km.orz 转换为 YAML 文件,进而由 orez-ctx 产生 ConTeXt 文件,最后由 ConTeXt MkIV 生成 PDF 格式的程序文档:

# 假设已经 `git clone https://github.com/liyanrui/orez.git`
$ cd orez/example
$ orez -w km.orz | orez-ctx > km.tex
$ context km.tex

注:orez-style.tex 相对于 km.tex,类似于 CSS 文件相对于 HTML 文件

注:有关 ConTeXt MkIV 的安装以及中文环境的配置,可参考我写的一些文档

如果希望 orez 能够支持其他的文档排版工具,这只能自力更生,自己编写一些脚本将 YAML 文档转换为指定的文档排版语言。结合 orez 的源文档,看懂 orez 生成的 YAML 文档应该并不困难。

借 Nikola 的力

Nikola 是使用 Python 开发的一个静态网站生成工具。它接受使用 Markdown 或 reStructuredText 之类的标记语言撰写的文档,输出 HTML 页面。Nikola 的安装以及基本用法,可参考它的「Getting Started」文档。下面我要讲述 orez 如何与 Nikola 取得结合,以实现 orez 源文件向 HTML 页面的逐步转换。

创建文档站点

现在,假设你已经具备了一个可用的 Nikola 环境。可使用以下命令建立名为 orez 站点(可以根据自己喜好取其他名字):

$ nikola init --demo --quiet orez
$ tree -L 1 orez
orez
├── conf.py
├── files
├── galleries
├── images
├── listings
├── posts
├── README.txt
├── stories
└── templates

其中,conf.py 是 orez 站点的配置文件,output 目录是 orez 站点的根目录——该目录内存放 Nikola 生成的网页文件。

让 Nikola 支持 Markdown

Nikola 默认支持的文档标记语言是 reStructuredText,若使之支持 Markdown,需要修改 conf.py,将其中的变量 POSTSPAGES 的值修改为:

POSTS = (
    ("posts/*.md", "posts", "post.tmpl"),
    ("posts/*.rst", "posts", "post.tmpl"),
    ("posts/*.txt", "posts", "post.tmpl"),
    ("posts/*.html", "posts", "post.tmpl"),
)
PAGES = (
    ("stories/*.md", "stories", "story.tmpl"),
    ("stories/*.rst", "stories", "story.tmpl"),
    ("stories/*.txt", "stories", "story.tmpl"),
    ("stories/*.html", "stories", "story.tmpl"),
)

注:若希望自己定制站点首页,可将 PAGES 设为:

PAGES = (
    ("stories/*.md", "", "story.tmpl"),
    ("stories/*.rst", "", "story.tmpl"),
    ("stories/*.txt", "", "story.tmpl"),
    ("stories/*.html", "", "story.tmpl"),
)

这样,在使用 nikola new_postnikola build 命令时,这些命令会将后缀名为 .md 的文件作为 Markdown 文档进行处理。

创建 orez 源文件

首先在 orez 站点创建一个目录 orez-meta,专用于存放 orez 源文件:

$ mkdir orez-meta

面向 Nikola 的 orez 源文件,本质上也是一份 Markdown 文件,只是其中的代码片段是 orez 的特有格式。因此,可以先让 Nikola 为我们生成 Markdown 文档模板,然后将其作为 orez 源文件使用。

作为示例,在 orez 目录下,执行以下命令:

$ nikola new_post -f markdown --title="hello-world"

便可创建一份 Markdown 文件 hello-orez.md。

所创建的 hello-world.md 位于 posts 目录,nikola new_post 命令会在这份文件中写入一些模板信息:

<!-- 
.. title: hello-orez
.. slug: hello-orez
.. date: 2016-08-07 07:47:40 UTC
.. tags: 
.. category: 
.. link: 
.. description: 
.. type: text
-->

Write your post here.

这些模板信息可根据自行酌情修改。

然后将 hello-world.md 文件复制到 orez-meta 目录,并将后缀名改为 .orz,这样便有了一份 orez 源文件。接下来可以向这份文件中按照 orez 的理想撰写程序文档,编写程序代码……若仅仅是为了体验一下,可复制 orez 项目的 example/markdown 目录中的 hello-world.orz 文件。

orez-md

orez 源文件编辑完毕之后,便可将其再转换为 Markdown 文件。对于上一节创建的 hello-world.orz 文件,可使用以下命令将其再次转换为 hello-world.md 文件,并将这份文件覆盖 posts 目录中由 nikola new_post 命令创建的那份 hello-world.md 文件:

$ orez -w hello-world.orz | orez-md > ../posts/hello-world.md

文档生成

将 orez 源文件转换为 Markdown 文件之后,便可在 orez 目录下执行:

$ nikola build

nikola build」 命令可将 posts 目录中的所有 Markdown 文件转换为 HTML 文件,并将这些 HTML 文件存放于 output/posts 目录。output 目录便是 Nikola 静态站点所有网页的的根目录。

然后执行:

$ nikola serve --browser

便可运行 Nikola 内置的 HTTP 服务器,并开启浏览器定位到 orez 站点的首页。如果上述过程没有出错,应该能够在 orez 站点首页上看到标题为「 Orez 的 Hello World 示例」的文档。

自定义 css 文件

如果希望 orez 源文件转换而成的网页能够美观一些,并且希望代码片段能够高亮显示,那么需要对 orez 站点的 css 文件进行一些定制。在 orez 目录中创建 mycss 目录,将我制作的一份 css 文件 tweak.css 放入该目录:

$ mkdir mycss
$ cd mycss
$ wget https://github.com/liyanrui/orez/raw/master/example/markdown/tweak.css

然后修改 orez 站点的配置文件 conf.py,将其中的 FILES_FOLDERSEXTRA_HEAD_DATA 的值修改为:

FILES_FOLDERS = {'mycss': 'mycss'}

EXTRA_HEAD_DATA = """
<link href="/mycss/tweak.css" rel="stylesheet" type="text/css">
<base target="_parent" />
"""

注:FILES_FOLDERS 的值是驱使 nikola build 命令将 mycss 目录复制到 output 目录——orez 站点根目录。如果直接在 output 目录中创建 mycss 目录并置入 css 文件,它们可能会被 nikola build 命令清除。

然后再次执行:

$ nikola build

便可将 tweak.css 中定义的样式作用于站点的所有页面。

hello-world-page

答疑

Q:你真的在用它?

A:Right. 除了让 orez 能够「自举」之外,我们在用它来写一个小型的 C 库:

agn-doc

Q:使用「orez -t」命令从 orez 源文件中提取的代码,若编译时出错,如何将错误信息定位到 orez 源文件?

A:使用 orez 的 --line 选项(简写为 -l)可以在提取的代码中增加一些 #line 语句。例如:

$ orez -t -l -e agn_km.c km.orz

提取的代码,其中某个片段如下:

#line 170 "./km.orz"
AgnTree *
agn_km_classify(AgnList *points, size_t K, AgnArrayEnv *env) {
        AgnTree *class_tree = km_init(points, K, env);
        while (!class_tree_stablized(class_tree, env)) {
                #line 237 "./km.orz"
                empty_classes(class_tree);
                assign_points(class_tree, points, env);
                #line 175 "./km.orz"
        }
        return class_tree;
}

对于 C/C++ 代码而言,这些 #line 语句可以让编译器将代码定位到 orez 源文件。对于非 C/C++ 语言,可想法将 #line 语句修改为代码注释,然后对代码进行人肉定位……

Q:在使用「orez -w」命令将 orez 源文件转换为 YAML 文件时,为何有时会遇到类似下面这样的警告信息?这种情况该如何处理?

WARNING **: Line 324: Programing Language of <@ agn_km.c #> is unknown!

A:之所以会出现这样的警告信息,是因为 orez 希望排版工具能够对代码片段进行高亮处理。排版工具在对代码进行高亮处理时,需要知道代码片段所用的编程语言。要消除这些警告信息,只需在作为起点的代码片段中添加编程语言标记。例如:

@ agn_km.c # [C]
<km-init>
static AgnTree *
km_init(AgnList *points, size_t K, AgnArrayEnv *env) {
        size_t n = agn_list_size(points);
        size_t *indices = generate_indices(K, 0, n - 1);
        # 初始化种类中心 -> init_centers @
        # 初始化种类树 -> class_tree @
        agn_array_free(init_centers, NULL);
        free(indices);
        return class_tree;
}
@

其中,[C] 便是编程语言标记。编程语言标记只能位于代码片段名称终结符 # 之后,且位于 +^+ 运算符(包括带代码片段标记的运算符)之前。

不需要为每个代码片段添加语言标记。orez 会将某个代码片段的语言标记传播到与该代码片段有直接或间接联系的所有代码片段。

Q:现在有适合编辑 orez 源文件的文本编辑器吗?

A:应该是没有。这个世界上,有哪种文本编辑器能够在编辑一份文本文件的过程中,同时支持某种排版标记或语言,某种或多种编程语言?

不过,有一些迂回的办法。譬如,如果用 Emacs,如下图所示,可以开启两个窗口,一个用于撰写文档,一个用于编写代码。代码写好后,复制到文档中。如果我有更多的时间,会考虑基于这种模式写一些便民的 Emacs 命令。

orez and emacs


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

载入中...