上一篇:缓冲区魔法
以下示例,能够使用 ConTeXt 默认的等宽字体排版一段 C 程序源码:
\environment card-env
\starttext
\starttyping
#include <stdio.h>
int main(void)
{
printf("Hello world!\n");
return 0;
}
\stoptyping
\stoptext
这段 C 程序源码在我的 Emacs 编辑器里,变量类型、宏、关键字、函数名等元素,颜色不一,可读性显然优于 ConTeXt 默认的排版结果,证据是,反对者的家里早已没有黑白电视了。
下面基于 Lua 的 Lpeg 库以及 ConTeXt LMTX 的 Pretty Printing 功能,实现 C 程序源码的彩化。
框架
以下 Lua 代码可以构造一个 ConTeXt 默认的解析器的复本 c_parser
:
local P, V = lpeg.P, lpeg.V
local new_grammar = visualizers.newgrammar
local g = {
pattern = V"default:pattern",
visualizer = V"pattern"^1
}
local c_parser = P(new_grammar("default", g))
然后给解析器取个名字 foo
,并将其注入 ConTeXt 的源码彩化机制:
visualizers.register("foo", { parser = c_parser })
解析器的名字是在 \starttyping ... \stoptyping
命令中使用的,即
\starttyping[option=foo]
#include <stdio.h>
int main(void)
{
printf("Hello world!\n");
return 0;
}
\stoptyping
TeX 编译器处理上述 ConTeXt 源码时,会根据 option
的值调用上述 Lua 定义的解析器 c_parser
。
完整的 ConTeXt 源文件内容如下:
\environment card-env
\startluacode
local P, V = lpeg.P, lpeg.V
local new_grammar = visualizers.newgrammar
local g = {
pattern = V"default:pattern",
visualizer = V"pattern"^1
}
local c_parser = P(new_grammar("default", g))
visualizers.register("foo", { parser = c_parser })
\stopluacode
\starttext
\starttyping[option=foo]
#include <stdio.h>
int main(void)
{
printf("Hello world!\n");
return 0;
}
\stoptyping
\stoptext
排版结果依然是黑白的,因为它是 ConTeXt 默认解析器的输出结果。若想实现 C 程序源码的彩化,需要基于上述代码,逐步向 c_parser
增加规则。
数据类型解析
以下代码可以解析代码中的 int
类型并将其色彩设为 blue
:
local function type_color(s)
context.color{"blue"}
visualizers.writeargument(s)
end
local c_type = P"int"
local g = {
type = c_type / type_color,
pattern = V"type" + V"default:pattern",
... ... ...
}
其中,visualizers.writeargument
的作用是将 s
作为宏的参数。在上述语境里,当 s
变为宏参数,它前面的宏是 context.color{"blue"}
,亦即 \color[blue]
,因此
context.color{"blue"}
visualizers.writeargument(s)
等价于 \color[blue]{Lua 变量 s 的值}
。
s
是什么呢?先看语法表 g
的第一条
type = c_type / type_color
等号右边表达式的含义是,将与 c_type
相匹配的字符串转发给 type_color
函数。s
即 \starttyping ...\stoptyping
所囊括的字符串中与 c_type
匹配的部分。
仅能实现 int
类型的解析和处理自然是远远不够,但是对于其他类型的解析,只需要做 Lpeg 的加法运算,例如
local c_type = P"int" + P"char" + P"float" + P"double" + ...
假设
local c_type = P"int" + P"void"
直觉上,以下 C 程序源码
int main(void)
{
... ... ...
}
中的 void
能够被解析且着色,但结果并非如此,因为我实现的解析器并不能真正理解 C 语言的语法。c_parser
在解析完 int
后,由 ConTeXt 定义的默认规则 V"default:pattern"
忽略空白字符,然后得到的字符串是 main(void)
,它无法与 P"void"
匹配。
我不懂编译原理,故而无力用 Lpeg 实现真正的 C 语法解析器,我能做的是使用一点暴力手段,令解析器能够以忽略 main(
这样的字符串的方式触及 void
,即
local function default(s)
-- 直接将解析结果发送给 ConTeXt
visualizers.write(s)
end
local function type_color(s)
context.color{"blue"}
visualizers.writeargument(s)
end
local c_type = P"int" + P"void"
local g = {
type = c_type / type_color,
other = ((R"AZ" + R"az" + P"_" + P".")^1 * S"()") / default,
pattern = V"type" + V"other" + V"default:pattern",
visualizer = V"pattern"^1
}
C 字符串解析
C 语言的字符串语法可描述为
local qt = P'"'
local c_string = qt * (1 - qt)^0 * qt
即「字符串 = 引号 + 非引号字符(或空字符) + 引号」。基于该规则,便可实现 C 语言字符串的解析和着色:
local g = {
... ... ...
string = c_string / string_color,
... ... ...
pattern = V"type" + V"string" + V"other" + V"default:pattern",
... ... ...
}
不幸的是,C 语言字符串还有引号转义形式,例如
"Hello \"world\"!"
上述 Lua 规则遇到转义的引号便乱了套:
补救方法是
local qt_esc = P'\\"'
local c_string = qt * (qt_esc + (1 - qt))^0 * qt
上述代码若写成
local qt_esc = P'\\"'
local c_string = qt * ((1 - qt) + qt_esc)^0 * qt
则无效。
解析 C 预处理指令
以下模式可匹配 C 预处理指令:
local c_preproc =
P"#" * (R"az" + R"AZ" + P"_")^1
* space^1
* (S"<>." + R"az" + R"AZ" + P"_")^1
将其加入语法规则集:
local g = {
... ... ...
preproc = c_preproc / string_color,
... ... ...
pattern = V"type" + V"string" + V"preproc" + V"other" + V"default:pattern",
... ... ...
}
关键字
下面代码仅对 return
的解析和着色:
local function keyword_color(s)
context.color{"middlegreen"}
visualizers.writeargument(s)
end
... ... ...
local c_keyword = P"return"
local g = {
... ... ...
keyword = c_keyword / keyword_color,
... ... ...
pattern = V"type" + V"string" + V"preproc"
+ V"keyword" + V"other" + V"default:pattern",
... ... ...
}
要添加对其他 C 关键字的支持,只需
local c_keyword = P"return" + P"for" + P"break" + ...
结语
完整的代码如下:
\environment card-env
\startluacode
local P, V, S, R = lpeg.P, lpeg.V, lpeg.S, lpeg.R
local new_grammar = visualizers.newgrammar
local function default(s)
-- 直接将解析结果发送给 ConTeXt
visualizers.write(s)
end
local function type_color(s)
context.color{"blue"}
visualizers.writeargument(s)
end
local function string_color(s)
context.color{"middlemagenta"}
visualizers.writeargument(s)
end
local function keyword_color(s)
context.color{"middlegreen"}
visualizers.writeargument(s)
end
local c_type = P"int" + P"void"
local qt = P'"'
local qt_esc = P'\\"'
local c_string = qt * (qt_esc + (1 - qt))^0 * qt
local space = S" \t"
local c_preproc =
P"#" * (R"az" + R"AZ" + P"_")^1
* space^1
* (S"<>." + R"az" + R"AZ" + P"_")^1
local c_keyword = P"return"
local g = {
type = c_type / type_color,
string = c_string / string_color,
preproc = c_preproc / string_color,
keyword = c_keyword / keyword_color,
other = ((R"AZ" + R"az" + P"_" + P".")^1 * S"()") / default,
pattern = V"type" + V"string" + V"preproc"
+ V"keyword" + V"other" + V"default:pattern",
visualizer = V"pattern"^1
}
local c_parser = P(new_grammar("default", g))
visualizers.register("foo", { parser = c_parser })
\stopluacode
\starttext
\starttyping[option=foo]
#include <stdio.h>
int main(void)
{
printf("Hello \"world\"!\n");
return 0;
}
\stoptyping
\stoptext
这些代码仅仅是投机取巧,真正稳健且完善的源码彩化程序需要扎实的编译原理功底。
另附
重新写了一个更为稳健的版本:
\environment card-env
\usecolors[crayola]
\startluacode
local function default(s)
visualizers.write(s)
end
local function type_color(s)
context.color{"GreenBlue"}
visualizers.writeargument(s)
end
local function name_color(s)
context.color{"MadderLake"}
visualizers.writeargument(s)
end
local function keyword_color(s)
context.color{"PurplePizzazz"}
visualizers.writeargument(s)
end
local function string_color(s)
context.color{"middlemagenta"}
visualizers.writeargument(s)
end
local function comment_color(s)
context.color{"QuickSilver"}
visualizers.writeargument(s)
end
local P, V, S, R = lpeg.P, lpeg.V, lpeg.S, lpeg.R
local space = S" \t"
local type = P"int" + P"void" + P"char"
local name = (R"az" + R"AZ" + P"_" + R"09")^1
local lp, rp = P"(", P")"
local star = space^0 * P"*"^0 * space^0
local comma = space^0 * P"," * space^0
local keyword = P"return" + P"const"
local preproc = P"#" * name * space^0 * (S"<>." + name)^1
local qt, qt_esc = P'"', P'\\"'
local str = qt * (qt_esc + (1 - qt))^0 * qt
local g = {
comment = (P"/*" * (1 - P"*/")^0 * P"*/") / comment_color,
keyword = keyword / keyword_color,
preproc = preproc / string_color,
string = str / string_color,
type = (V"keyword"* (space^1 / default) * (type / type_color))
+ (type / type_color),
name = name / default,
value = lpeg.patterns.digits + V"string" + V"name",
var_decl = (V"type" * (star / default) * V"name") + V"type",
param = V"var_decl" * ((comma / default) * V"var_decl")^0,
arg = V"value" * ((comma / default) * V"value")^0,
func = V"type"
* (space^1 / default)
* (name / name_color)
* (space^0 / default)
* (lp / default)
* V"param"
* (rp / default),
func_call = (name / name_color)
* (space^0 /default)
* (lp / default)
* V"arg"
* (rp / default),
pattern = V"comment" + V"preproc" + V"keyword" + V"string"
+ V"func" + V"func_call" + V"default:pattern",
visualizer = V"pattern"^1
}
local new_grammar = visualizers.newgrammar
local c_parser = P(new_grammar("default", g))
visualizers.register("foo", { parser = c_parser })
\stopluacode
\starttext
\starttyping[option=foo]
#include <stdio.h>
const int main(const int argc, const char **argv, int abc)
{
/* comment */
printf("Hello \"world\"!\n");
foo(a, b, c);
return 0;
}
\stoptyping
\stoptext
下一篇:zhfonts 模块备忘录
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。