上一篇:缓冲区魔法
以下示例,可能应用 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
这些代码仅仅是投机取巧,真正持重且欠缺的源码彩化程序须要扎实的编译原理功底。