关于tex:LuainConTeXt11源码彩化

4次阅读

共计 4621 个字符,预计需要花费 12 分钟才能阅读完成。

上一篇:缓冲区魔法

以下示例,可能应用 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

这些代码仅仅是投机取巧,真正持重且欠缺的源码彩化程序须要扎实的编译原理功底。

正文完
 0