上一篇:緩衝區魔法
以下示例,能夠使用 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