《Lua-in-ConTeXt》11:原始碼彩化

garfileo發表於2023-02-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

這些程式碼僅僅是投機取巧,真正穩健且完善的原始碼彩化程式需要紮實的編譯原理功底。

另附

重新寫了一個更為穩健的版本:

\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

相關文章