上一篇:缓冲区魔法

以下示例,能够使用 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 模块备忘录

garfileo
6k 声望1.9k 粉丝

这里可能不会再更新了。