改造

0

现在,我基本熟悉了 pretty-c 模块 [1]。熟悉,也许是为了控制甚至改变,否则没理由要去熟悉。请想一想自己对亲人、爱人和朋友曾经做过的以及正在做的那些事吧!于是,我要对 pretty-c 模块进行一些改造。

过程名

在用伪 C 代码表达一些算法的时候,我会遵循了自己之前养成的文化编程习惯 [2],采用「@ 过程名 #」的形式指代一个算法,采用「# 过程名 @」的形式调用一个算法。例如:

@ K 均值聚类中心初始化 #
输入:点集 points,簇数 K
输出:含聚类初始中心的分类树
{
        size_t *indices = agn_generate_indices(K, 0, points->n - 1);
        # 初始化种类中心 @
        # 初始化种类树 @
        agn_array_free(init_centers);
        free(indices);
        return class_tree;
}

当我在 ConTeXt MkIV 使用 C 代码的形式对上述算法描述进行排版时,我希望它能够通过 pretty-c 模块认出「@ 过程名 #」和「# 过程名 @」,然后,将它们排版为下面这样的形式:

模拟

我的目的是将「@ 过程名 #」和「# 过程名 @」分别排版为「< 过程名 > ≡」和「< 过程名 >」的形式。于是,我要解决的问题是将「@ 过程名 #」和「# 过程名 @」转化为相应的 ConTeXt MkIV 的排版语句。

注:实际上不是 <>,而是 TeX 数学符号 \langle\rangle,现在只是用 <> 作为替代。

< 过程名 > ≡」可以用 ConTeXt 代码 $\langle$ 过程名 $\rangle\equiv$ 来表示。由于 ConTeXt MkIV 会用等宽字体排版代码文本,而我希望能够继续用正文字体来排版「< 过程名 > ≡」,所以需要用 \switchtobody 将字体临时切换为正文字体,即

\switchtobodyfont[rm]{$\langle$ 过程名 $\rangle\equiv$}

这样便完成了对「< 过程名 > ≡」的模拟。将上述语句中的 \equiv 去掉,结果便是对「< 过程名 >」的模拟。

由于我希望模拟结果为深蓝色,所以再增加着色语句:

\color[darkblue]{\switchtobodyfont[rm]{$\langle$ 过程名 $\rangle\equiv$}}

下面是一份完整的 ConTeXt MkIV 源文档,用于查看上述语句的模拟效果:

\usemodule[zhfonts]
\starttext
\starttyping[escape=yes]
/BTEX\color[darkblue]P\switchtobodyfont[rm]{$\langle$过程名$\rangle\equiv$}}/ETEX
\stoptyping
\stoptext

该文档的编译结果如下:

我觉得尖括号的位置有些靠下,有些不协调,那么便将它们抬高一些:

\usemodule[zhfonts]
\starttext
\starttyping[escape=yes]
/BTEX\color[darkblue]P\switchtobodyfont[rm]{\raise0.1em\hbox{$\langle$}过程名\raise0.1em\hbox{$\rangle\equiv$}}}/ETEX
\stoptyping
\stoptext

\raise0.1em\hbox{$\langle$}\raise0.1em\hbox{$\rangle\equiv$} 可将左尖括号抬高 0.1 字符宽度,这是 TeX 家族惯用的版式微调方法。结果为

可以接受。

Handler

现在的问题是,怎样让 pretty-c 模块能够帮我将上面的模拟代码写入 ConTeXt MkIV 源文件。pretty-c 模块的 handler 部分负责此事。handler 是一个函数集:

local handler = visualizers.newhandler {
    ... ... ...,
    boundary     = function(s) CSnippetBoundary(s) end,
    comment      = function(s) CSnippetComment(s)  end,
    string       = function(s) CSnippetString(s) end,
    ... ... ...,
}

这些函数主要用于处理 ConTeXt MkIV 通过 pretty-c 模块从 C 代码中识别出来的语句 s

现在假设 ConTeXt MkIV 能够在 C 代码中识别出「@ 过程名 #」和「# 过程名 @」形式的语句,那么识别结果就是相应的字串 s,亦即对于「@ 过程名 #」而言,s 是以 @ 开头,以 # 结尾;对于「# 过程名 @」而言,s 是以 # 开头,以 @ 结尾。

对于上一节的模拟结果

\color[darkblue]P\switchtobodyfont[rm]{\raise0.1em\hbox{$\langle\,$}过程名\raise0.1em\hbox{$\,\rangle\equiv$}}}

若将「过程名」两侧部分视为 Lua 字串,对于「@ 过程名 #」和「# 过程名 @」这两种形式,可以分离出三个字串:

local langle = "\\color[darkblue]{\\switchtobodyfont[rm]\\raise.1em\\hbox{$\\langle$}"
local rangle = "\\raise.1em\\hbox{$\\rangle$}}"
local rangle_equiv = "\\raise.1em\\hbox{$\\rangle\\equiv$}}"

基于 Lua 的字串连接语法,「@ 过程名 #」可表示为「langle .. x .. rangle_equiv」,而「# 过程名 @」则可表示为「langle .. x .. rangle」,其中 x 是去掉了两端的 @# 符号以及空白字符的 s。合成所得的字串,可以通过调用 ConTeXt MkIV 提供的 context 函数,将其写入源文档,例如:

context(langle .. x .. rangle_equiv)

对于 s,要去掉两段的 @# 符号及空白字符,可以使用 Lua 字串模块提供的 gsub 函数。例如,若 s 为「@ 过程名 #」形式,可像下面这样处理:

string.gsub(s, "string.gsub(s, "^@%s*(.-)%s*#$", "%1")

s 为「# 过程名 @」形式,可像下面这样处理:

string.gsub(s, "string.gsub(s, "^#%s*(.-)%s*@$", "%1")

string.gsub 处理之后的 s,便是上述的 x

有了这些知识,我便可以在 handler 集合中增加两个 handler:

local handler = visualizers.newhandler {
    ... ... ...,
    procname     = function(s) context(langle .. string.gsub(s, "^@%s*(.-)%s*#$", "%1") .. rangle_equiv) end,
    procnameref  = function(s) context(langle .. string.gsub(s, "^#%s*(.-)%s*@$", "%1") .. rangle) end,
    ... ... ...,
}

这样,只要 ConTeXt MkIV 能够通过 pretty-c 模块从 C 代码中识别出「@ 过程名 #」和「# 过程名 @」形式的语句,再把它们分别传递给上述的两个 handler 便可以完成相应的排版工作。

Grammar

若让 ConTeXt MkIV 能够通过 pretty-c 模块从 C 代码中识别出「@ 过程名 #」和「# 过程名 @」形式的语句,这需要在 pretty-c 模块增加两条语法规则。

对于「@ 过程名 #」形式的语句,它们对应的语法规则若用 LPEG 代码来描述,最简单的描述为「P("@") * (1 - P("#"))^0 * P("#")」,意思是,以 @ 为开始,以 # 为结尾,且中间出现 # 的任意字串。同理,对于形如「# 过程名 @」形式的语句,它们对应的语法规则可以描述为「P("#") * (1 - P("@"))^0 * P("@")」。

我对 Lua 的 LPEG 库很陌生。上述代码里出现的 1,我的浅薄理解是,它表示任意字符,应当也可以写为 P(1)

但是,上述语法规则,过于简单,容易导致 ConTeXt MkIV 在识别一些语句时会出现在我看来是错误的判断。例如,

#include <stdio.h>
int main(void)
{
        # 打印 "hello world" @
        return 0;
}

若 ConTeXt MkIV 按照「P("#") * (1 - P("@"))^0 * P("@")」这条规则去识别上述代码,它会将其「读」为:

#include <stdio.h>
int main(void)
{
        # 打印 "hello world" @

因为这部分代码,是以 # 开头,以 @ 结尾,并且中间未出现 @

如何解决这样的问题呢?有三种方法。第一种方法是不解决。第二种方法是换用别的符号来作为「过程名」的起止符(定界符)。第三种方法是对语法规则进一步给出限定,从而避免混淆。我用第三种方法来解决这个问题。

我对「@ 过程名 #」和「# 过程名 @」形式的语句给出的限定是,「过程名」中不能出现换行符。于是,它们对应的语法规则就变成:「P("@") * (1 - P("#") - newline)^0 * P("#")」和「P("#") * (1 - P("@") - newline)^0 * P("@")」。如此,当 ConTeXt MkIV 遇到 #include <stdio.h> 语句时,它很容易就可以根据「P("#") * (1 - P("@") - newline)^0 * P("@")」判断这条语句并不符合这一语法,于是就消除了错误。

下面将这两条语法规则添加到 t-pretty-c.lua 的 grammar 集合:

local grammar = visualizers.newgrammar(
   "default",
   {
      "visualizer",
      ... ... ... ,
      procname    = makepattern(handler, "procname", P("@") * (1 - P("#") - newline)^0 * P("#")),
      procnameref = makepattern(handler, "procnameref", P("#") * (1 - P("@") - newline)^0 * P("@")),
      pattern =
          V("procname")
          + V("procnameref")
          + ... ... ... ,

      ... ... ... ,
   }
}

这样,ConTeXt MkIV 一旦加载了 pretty-c 模块,它就具备从 C 代码中识别「@ 过程名 #」和「# 过程名 @」形式的语句了。

总结

现在,我差不多可以为 ConTeXt MkIV 编写任何一种我熟悉的编程语言的代码高亮模块了。


[1] 五颜六色
[2] orez 的故事


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

你可能感兴趣的

载入中...