如今,我基本熟悉了 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 模块认出「@ 过程名 #
」和「# 过程名 @
」,而后,将它们排版为下面这样的形式:segmentfault
个人目的是将「@ 过程名 #
」和「# 过程名 @
」分别排版为「< 过程名 > ≡
」和「< 过程名 >
」的形式。因而,我要解决的问题是将「@ 过程名 #
」和「# 过程名 @
」转化为相应的 ConTeXt MkIV 的排版语句。编程语言
注:实际上不是<
和>
,而是 TeX 数学符号\langle
和\rangle
,如今只是用<
和>
做为替代。
「< 过程名 > ≡
」能够用 ConTeXt 代码 $\langle$ 过程名 $\rangle\equiv$
来表示。因为 ConTeXt MkIV 会用等宽字体排版代码文本,而我但愿可以继续用正文字体来排版「< 过程名 > ≡
」,因此须要用 \switchtobody
将字体临时切换为正文字体,即函数
\switchtobodyfont[rm]{$\langle$ 过程名 $\rangle\equiv$}
这样便完成了对「< 过程名 > ≡
」的模拟。将上述语句中的 \equiv
去掉,结果即是对「< 过程名 >
」的模拟。字体
因为我但愿模拟结果为深蓝色,因此再增长着色语句:ui
\color[darkblue]{\switchtobodyfont[rm]{$\langle$ 过程名 $\rangle\equiv$}}
下面是一份完整的 ConTeXt MkIV 源文档,用于查看上述语句的模拟效果:lua
\usemodule[zhfonts] \starttext \starttyping[escape=yes] /BTEX\color[darkblue]P\switchtobodyfont[rm]{$\langle$过程名$\rangle\equiv$}}/ETEX \stoptyping \stoptext
该文档的编译结果以下:spa
我以为尖括号的位置有些靠下,有些不协调,那么便将它们抬高一些:3d
\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 家族惯用的版式微调方法。结果为
能够接受。
如今的问题是,怎样让 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 即可以完成相应的排版工做。
若让 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 编写任何一种我熟悉的编程语言的代码高亮模块了。