五光十色

在看個人 github 主頁(聽說有識之士正紛紛將本身的項目遷往 gitlab)的時候,發現了之前 fork 的一個項目 pretty-c。這個項目爲 ConTeXt MkIV 實現了一個模塊,用於解決 C 代碼的高亮(Highlighting)問題。git

ConTeXt MkIV 是我這十多年來用來排版一些含有數學公式、代碼、表格等元素的文檔最重要的工具,如今則是我拿來寫學位論文的工具。若它不支持 C 代碼高亮,會以爲它對不起我這份遲遲未能完成的論文,由於論文裏全部算法皆以僞 C 代碼的形式描述。github

pretty-c 模塊爲 ConTeXt MkIV 解決了 C 代碼高亮問題,那彷佛看起來就沒問題了。七年前,我是這樣認爲的,當時寫了一份測試文檔 foo.tex:算法

\usemodule[pretty-c]
 
\starttext
\starttyping[option=c]
#include <stdio.h>
int main(void)
{
        printf("Hello!\n");
        return 0;
}
\stoptyping
\stoptext

使用 ConTeXt MkIV,對其進行「編譯」,segmentfault

$ context foo

獲得的排版結果(PDF 文件 foo.pdf)以下:函數

可是當我將上述 C 代碼中的「Hello!」字串換成「你好啊!」時,ConTeXt MkIV 在將 foo.tex 編譯爲 foo.pdf 的過程當中便會報錯:工具

tex error       > tex error on line 98 in file /tmp/foo.tex: ! 
String contains an invalid utf-8 sequence

l.98 
   �st

這顯然是 pretty-c 沒法正確識別 C 代碼中的中文字串所致使的問題。無獨有偶,當 C 代碼中出現含有中文字符的註釋時,也會像上面報錯。gitlab

C 語言惟二容許我寫中文的地方,在 pretty-c 裏不被支持(韓文、日文、越文等東亞文字天然也不被支持),這讓我沒法容忍。測試

簡化

pretty-c 的實現代碼 [1] 分爲兩部分,亦即兩份文件,t-pretty-c.mkiv 與 t-pretty-c.lua。編碼

我打開 t-pretty-c.mkiv 看了一下,lua

\registerctxluafile{t-pretty-c.lua}{1.501}
\unprotect
\setupcolor[ema]

\definestartstop
    [CSnippet]
    [DefaultSnippet]

\definestartstop
    [CSnippetName]
    [\c!color=darkgoldenrod,
     \c!style=]

\definestartstop
    [CSnippetKeyword]
    [\c!color=purple,
     \c!style=]

\definestartstop
    [CSnippetType]
    [\c!color=forestgreen,
     \c!style=]

\definestartstop
    [CSnippetPreproc]
    [\c!color=orchid,
     \c!style=]

\definestartstop
    [CSnippetBoundary]
    [\c!color=steelblue,
     \c!style=]

\definestartstop
    [CSnippetComment]
    [\c!color=darkred,
     \c!style=]

\definestartstop
    [CSnippetString]
    [\c!color=mediumblue,
     \c!style=]

\definetyping[C][\c!option=c]

\protect \endinput

雖然不知這些代碼的具體用意,可是我能看出來,它們主要是爲了C 代碼裏的類型、變量名、字符串、預處理、起止括號、註釋等元素分別定義顏色,這樣在作代碼高亮的時候,將這些元素渲染成對應的顏色。之因此可以肯定這一點,是由於我修改了幾個顏色值,觀察到了它們對 foo.tex 編譯結果的影響。

因爲我肯定 pretty-c 在處理字符串與註釋文本的高亮時不支持中文,爲了更快的找出問題所在,我決定從觀察 pretty-c 如何處理註釋文本入手。因而,我嘗試對 t-pretty-c.mkiv 進行簡化,僅保留與註釋文本相關的顏色設定以及我不肯定其功能的一部分代碼:

\registerctxluafile{t-pretty-c.lua}{1.501}
\unprotect
\setupcolor[ema]

\definestartstop
    [CSnippet]
    [DefaultSnippet]

\definestartstop
    [CSnippetComment]
    [\c!color=darkred,
     \c!style=]

\definetyping[C][\c!option=c]

\protect \endinput

以後再看 t-pretty-c.lua:

if not modules then modules = { } end modules ['t-pretty-c'] = {
    version   = 1.501,
    comment   = "Companion to t-pretty-c.mkiv",
    author    = "Renaud Aubin",
    copyright = "2010 Renaud Aubin",
    license   = "GNU General Public License version 3"
}

local tohash = table.tohash
local P, S, V, patterns = lpeg.P, lpeg.S, lpeg.V, lpeg.patterns


local keyword = tohash {
   "auto", "break", "case", "const", "continue", "default", "do",
   "else", "enum", "extern", "for", "goto", "if", "register", "return",
   "sizeof", "static", "struct", "switch", "typedef", "union", "volatile",
   "while",
}

local type = tohash {
   "char", "double", "float", "int", "long", "short", "signed", "unsigned",
   "void",
}

local preproc = tohash {
   "define", "include", "pragma", "if", "ifdef", "ifndef", "elif", "endif",
   "defined",
}

local context               = context
local verbatim              = context.verbatim
local makepattern           = visualizers.makepattern

local CSnippet              = context.CSnippet
local startCSnippet         = context.startCSnippet
local stopCSnippet          = context.stopCSnippet

local CSnippetBoundary      = verbatim.CSnippetBoundary
local CSnippetSpecial       = verbatim.CSnippetSpecial
local CSnippetComment       = verbatim.CSnippetComment
local CSnippetKeyword       = verbatim.CSnippetKeyword
local CSnippetType          = verbatim.CSnippetType
local CSnippetPreproc       = verbatim.CSnippetPreproc
local CSnippetName          = verbatim.CSnippetName
local CSnippetString        = verbatim.CSnippetString

local typedecl = false

local function visualizename_a(s)
   if keyword[s] then
      CSnippetKeyword(s)
      typedecl=false
   elseif type[s] then
      CSnippetType(s)
      typedecl=true
   elseif preproc[s] then
      CSnippetPreproc(s)
      typedecl=false
   else 
      verbatim(s)
      typedecl=false
   end
end

local function visualizename_b(s)
   if(typedecl) then
      CSnippetName(s)
      typedecl=false
   else
      visualizename_a(s)
   end
end

local function visualizename_c(s)
   if(typedecl) then
      CSnippetBoundary(s)
      typedecl=false
   else
      visualizename_a(s)
   end
end

local handler = visualizers.newhandler {
    startinline  = function() CSnippet(false,"{") end,
    stopinline   = function() context("}") end,
    startdisplay = function() startCSnippet() end,
    stopdisplay  = function() stopCSnippet() end ,

    boundary     = function(s) CSnippetBoundary(s) end,
    comment      = function(s) CSnippetComment(s) end,
    string       = function(s) CSnippetString(s) end,
    name         = function(s) CSnippetName(s) end,
    type         = function(s) CSnippetType(s) end,
    preproc      = function(s) CSnippetPreproc(s) end,
    varname      = function(s) CSnippetVarName(s) end,

    name_a       = visualizename_a,
    name_b       = visualizename_b,
    name_c       = visualizename_c,
}

local space       = patterns.space
local anything    = patterns.anything
local newline     = patterns.newline
local emptyline   = patterns.emptyline
local beginline   = patterns.beginline
local somecontent = patterns.somecontent

local comment     = P("//") * patterns.space^0 * (1 - patterns.newline)^0
local incomment_open = P("/*")
local incomment_close = P("*/")

local name        = (patterns.letter + patterns.underscore)
                  * (patterns.letter + patterns.underscore + patterns.digit)^0
local boundary    = S('{}')

local grammar = visualizers.newgrammar(
   "default",
   {
      "visualizer",

      ltgtstring = makepattern(handler,"string",P("<")) * V("space")^0
      * (makepattern(handler,"string",1-patterns.newline-P(">")))^0
   * makepattern(handler,"string",P(">")+patterns.newline),


      sstring = makepattern(handler,"string",patterns.dquote)
      * ( V("whitespace") + makepattern(handler,"string",(P("\\")*P(1))+1-patterns.dquote) )^0
      * makepattern(handler,"string",patterns.dquote),

      dstring = makepattern(handler,"string",patterns.squote)
      * ( V("whitespace") + makepattern(handler,"string",(P("\\")*P(1))+1-patterns.squote) )^0
      * makepattern(handler,"string",patterns.squote),

      comment = makepattern(handler,"comment",comment),
      --       * (V("space") + V("content"))^0,

      incomment = makepattern(handler,"comment",incomment_open)
      * ( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0
      * makepattern(handler,"comment",incomment_close),
   
      argsep = V("optionalwhitespace") * makepattern(handler,"default",P(",")) * V("optionalwhitespace"),
      argumentslist = V("optionalwhitespace") * (makepattern(handler,"name",name) + V("argsep"))^0,

      preproc = makepattern(handler,"preproc", P("#")) * V("optionalwhitespace") * makepattern(handler,"preproc", name) * V("whitespace") 
      * (
         (makepattern(handler,"boundary", name) * makepattern(handler,"default",P("(")) * V("argumentslist") * makepattern(handler,"default",P(")")))
         + ((makepattern(handler,"name", name) * (V("space")-V("newline"))^1 ))
        )^-1,

      name = (makepattern(handler,"name_c", name) * V("optionalwhitespace") * makepattern(handler,"default",P("(")))
      + (makepattern(handler,"name_b", name) * V("optionalwhitespace") * makepattern(handler,"default",P("=") + P(";") + P(")") + P(",") ))
      + makepattern(handler,"name_a",name),

    pattern =
      V("incomment")
      + V("comment")
      + V("ltgtstring")
      + V("dstring")
      + V("sstring")
      + V("preproc")
      + V("name")
      + makepattern(handler,"boundary",boundary)
      + V("space")
      + V("line")
      + V("default"),

    visualizer =
        V("pattern")^1
   }
)

local parser = P(grammar)

visualizers.register("c", { parser = parser, handler = handler, grammar = grammar } )

因爲我已決定只探查註釋文本的處理,因此儘管 t-pretty-c.lua 代碼不少,可是值得我關心的卻不多,只有下面這些(包含我認爲必須存在只是我拿不許的那部分代碼):

local P, S, V, patterns = lpeg.P, lpeg.S, lpeg.V, lpeg.patterns

local context               = context
local verbatim              = context.verbatim
local makepattern           = visualizers.makepattern

local CSnippet              = context.CSnippet
local startCSnippet         = context.startCSnippet
local stopCSnippet          = context.stopCSnippet

local CSnippetComment       = verbatim.CSnippetComment

local handler = visualizers.newhandler {
    startinline  = function() CSnippet(false,"{") end,
    stopinline   = function() context("}") end,
    startdisplay = function() startCSnippet() end,
    stopdisplay  = function() stopCSnippet() end ,
    
    comment      = function(s) CSnippetComment(s) end,
}

local comment     = P("//") * patterns.space^0 * (1 - patterns.newline)^0
local incomment_open = P("/*")
local incomment_close = P("*/")

local grammar = visualizers.newgrammar(
   "default",
   {
      "visualizer",
      comment = makepattern(handler,"comment",comment),
      incomment = makepattern(handler,"comment",incomment_open)
                  * ( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0
                  * makepattern(handler,"comment",incomment_close),
      pattern = V("incomment") + V("comment"),
      visualizer = V("pattern")^1
   }
)

local parser = P(grammar)
visualizers.register("c", { parser = parser, handler = handler, grammar = grammar } )

對 t-pretty-c.mkiv 和 t-pretty-c.lua 簡化以後,我獲得的就是一個只能對 C 代碼中的註釋文本進行高亮處理的 ConTeXt MkIV 模塊。假設這個模塊名爲 simple-pretty-c,將上述簡化的代碼分別保存爲 t-simple-pretty-c.mkiv 和 t-simple-pretty-c.lua,並將 t-simple-pretty-c.mkiv 中的 \registerctxluafile{t-pretty-c.lua}{1.501} 修改成 \registerctxluafile{t-simple-pretty-c.lua}{1.501}

如今須要試驗一下簡化後的模塊,可否工做。若不能工做,那麼也就沒法進一步對這個模塊做更深刻的刺探。爲了完成這個試驗,我新建了一個 foo 目錄,將 t-simple-pretty-c.mkiv 和 t-simple-pretty-c.lua 放到這個目錄內,而後在該目錄內再建立一份 foo.tex 文件,內容以下:

\usemodule[simple-pretty-c] % 簡化的 pretty-c 模塊

\starttext
\starttyping[option=c]
// test 1
/* test 2 */
int i = 10; /* test 3 */
\stoptyping
\stoptext

其中,「// test 1」,「/* test 2 */」以及「/* test 3 */」皆爲 C 代碼中的註釋文本。用 ConTeXt MkIV 編譯 foo.tex,結果在生成的 foo.pdf 文件中只有「// test 1」被顯示且被高亮,其餘文本連被顯示的機會都沒有。我試着在「// test 1」以前增長一個空格,結果連「// test 1」也不會被顯示。這個結果說明,簡化後的 pretty-c 模塊能夠工做,但功能不完善,只要在代碼中遇到它不能處理的元素,就會自動罷工,以至在該元素以後儘管有註釋文本,它也不會理睬。

從新考察 t-pretty-c.lua 中的代碼,發如今

pattern =
      V("incomment")
      + V("comment")
      + V("ltgtstring")
      + V("dstring")
      + V("sstring")
      + V("preproc")
      + V("name")
      + makepattern(handler,"boundary",boundary)
      + V("space")
      + V("line")
      + V("default"),

裏面,V("incomment")V("comment") 確定與註釋有關,其餘的,除了 V("default") 以外,皆與 C 代碼中其餘我可以肯定的具體元素有關。因而便試着 V("comment") 增長到 t-simple-pretty-c.lua 中,即:

pattern = V("incomment") + V("comment") + V("default"),

再從新編譯 foo.tex,即可以獲得正確的結果:

如此看來, V("default") 一定對應着 C 代碼中全部非註釋文本元素的一套默認處理規則。

刺探

如今,我有了一個足夠簡化的 pretty-c 模塊。因爲這個模塊功能簡單,而且可以正常工做,所以我能夠對代碼進行一些試探性的修改,根據輸出結果來逐步熟悉這個模塊是如何工做的。

首先,我可以肯定 t-simple-pretty-c.mkiv 中的

\definestartstop
    [CSnippetComment]
    [\c!color=darkred,
     \c!style=]

用於設定註釋文本的顏色。這一點在上文中已經說過了。

其次,我可以肯定 t-simple-pretty-c.lua 文件中的

comment      = function(s) CSnippetComment(s) end

一定與

\definestartstop
    [CSnippetComment]
    [\c!color=darkred,
     \c!style=]

有着密切的聯繫。不只僅是由於這兩塊代碼中都出現了 CSnippetComment

在 t-simple-pretty-c.lua 文件中, CSnippetComment 顯然是個函數,然而我卻沒有定義過這個函數,它就這樣莫名其妙的存在了。這在人間,即是見鬼,但在程序裏,一切必須是肯定的。所以,我判定這個函數是 ConTeXt MkIV 自動生成的。假若我將上述 Lua 代碼中的 CSnippetComment 替換爲 context,即

comment      = function(s) context(s) end

再從新編譯 foo.tex,結果會致使代碼註釋文本的顏色再也不是暗紅色,而是黑色。context 函數的做用很簡單,它直接將所接受的字串 s 輸出到 PDF 文件中。所以,這個結果意味着 CSnippetComment 會對字串 s 的內容進行着色處理,可是它所用的顏色一定是從 t-simple-pretty-c.mkiv 文件裏對 CSnippetComment 的設定中得來。

這樣就明白了 t-simple-pretty-c.lua 文件中這段代碼

local handler = visualizers.newhandler {
    startinline  = function() CSnippet(false,"{") end,
    stopinline   = function() context("}") end,
    startdisplay = function() startCSnippet() end,
    stopdisplay  = function() stopCSnippet() end ,
    
    comment      = function(s) CSnippetComment(s) end,
}

的用途。這是一個函數集合,其中每一個函數會在最終將代碼的渲染結果輸出到 PDF 文件的時候被 ConTeXt MkIV(確切地說是 LuaTeX 引擎)調用。若將 ConTeXt MkIV 生成的 PDF 文件喻做顯示器,那麼這個函數集所扮演的角色即是顯卡。

既然 CSnippetComment(s) 的做用是將字串 s 的內容「染成」暗紅色,那麼字串 s 一定包含了 C 代碼中的註釋文本信息。這個信息是從哪裏得來的呢?天然是 ConTeXt MkIV 從 foo.tex 文檔中「發現」的。它之因此可以確認哪些代碼是註釋文本,一定與 t-simple-pretty-c.lua 裏的這段代碼有密切關係:

local comment         = P("//") * patterns.space^0 * (1 - patterns.newline)^0
local incomment_open  = P("/*")
local incomment_close = P("*/")

local grammar = visualizers.newgrammar(
   "default",
   {
      "visualizer",
      comment = makepattern(handler,"comment",comment),
      incomment = makepattern(handler,"comment",incomment_open)
                  * ( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0
                  * makepattern(handler,"comment",incomment_close),
      pattern = V("incomment") + V("comment") + V("default"),
      visualizer = V("pattern")^1
   }
)

要看明白這部分代碼,前提是須要對 Lua 語言的 LPEG 庫 [2] 有一些瞭解。不瞭解也沒有關係,我能夠根據 C 代碼中註釋文本的形式去猜想便可。

例如,下面這行代碼

local comment         = P("//") * patterns.space^0 * (1 - patterns.newline)^0

它的做用應該是從 C 代碼中尋找以 // 開頭的文本,而且這行文本不能包含換行符。這顯然是在搜索相似「// test 1」這樣的註釋文本,而 comment 變量存儲的即是搜索到的文本。

grammarvisualizers.newgrammar 函數(或方法)的返回值,這個函數所接受的第 2 個參數是一個表,其中下面這幾行代碼

comment = makepattern(handler,"comment",comment),
      incomment = makepattern(handler,"comment",incomment_open)
                  * ( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0
                  * makepattern(handler,"comment",incomment_close),
      pattern = V("incomment") + V("comment") + V("default"),
      visualizer = V("pattern")^1

應該註釋文本的搜索過程有關。我能夠經過去除代碼來驗證這一猜想。例如,將上述代碼消減爲

incomment = makepattern(handler,"comment",incomment_open)
                  * ( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0
                  * makepattern(handler,"comment",incomment_close),
      pattern = V("incomment") + V("default"),
      visualizer = V("pattern")^1

而後,我斷言註釋文本「// test 1」會沒法被 ConTeXt MkIV 識別出來,於是它沒有機會被渲染爲暗紅色。從新編譯 foo.tex,果不其然:

上述的代碼消減還讓我發現了 V("comment")

comment = makepattern(handler,"comment",comment)

之間密切的聯繫,具體的細節我不清楚,可是我可以肯定

pattern = V("incomment") + V("comment") + V("default")

中所出現的「V("..."),除了「V("default")」,一定與上面經過 makepattern 函數構造的變量相對應。

爲何中文不行?(1)

如今我已經準確地找到了 simple-pretty-c 模塊中對註釋文本的識別代碼。simple-pretty-c 模塊沒法處理含有中文字符的註釋文本,一定是識別註釋文本的代碼有問題。爲了對此加以驗證,我將 foo.tex 的內容修改成:

\usemodule[zhfonts]
\usemodule[simple-pretty-c]

\starttext
\starttyping[option=c]
// 測試 1
/* test 2 */
int i = 10; /* test 3 */
\stoptyping
\stoptext
注:zhfonts 是我爲 ConTeXt MkIV 寫的中文支持模塊 [3]。

亦即,我先驗證「// ...」形式的註釋文本是否可以被 simple-pretty-c 模塊識別。再度編譯 foo.tex,結果出乎個人意料,「// ...」形式的註釋文本雖然含有中文字符,可是卻被正確的識別和高亮處理了。

我以前認爲 pretty-c 模塊不能處理含中文字符的註釋文本,是由於我在「/* ... */」形式的註釋文本中遇到了這種狀況。如今,再次驗證一下,將 foo.tex 改成:

\usemodule[zhfonts]
\usemodule[simple-pretty-c]

\starttext
\starttyping[option=c]
// 測試 1
/* 測試 2 */
int i = 10; /* test 3 */
\stoptyping
\stoptext

結果,在編譯 foo.tex 的過程當中 ConTeXt MkIV 開始報錯:

tex error       > tex error on line 19 in file /home/garfileo/var/tmp/foo/foo.tex: ! 
String contains an invalid utf-8 sequence

l.19 
   �st

如今我可以肯定,問題一定出在

incomment = makepattern(handler,"comment",incomment_open)
                  * ( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0
                  * makepattern(handler,"comment",incomment_close)

裏。由於 t-simple-pretty-c.lua 中只有這部分代碼是用來識別 /* ... */」形式的註釋文本的。

解決方法

知道了本身面對的問題具體是什麼,問題就解決了一半。此外,我如今也掌握了一條對我有利的信息,即「// ...」形式的註釋文本,即便含有中文字符,它也能被正確識別與高亮處理。

t-simple-pretty-c.lua 中,與識別「// ...」形式的註釋文本有關聯的代碼以下:

local comment     = P("//") * patterns.space^0 * (1 - patterns.newline)^0
comment = makepattern(handler, "comment", comment)

這裏,變量名稱有點亂。第一個 comment 變量,顯然是做爲 makepattern 的第三個參數來用的,而第二個 comment 變量其實是一個表(做爲參數傳給 visualizers.newgrammar 函數)中的一個元素的名稱;亦即這兩個 comment 各有所指。所以,上述這兩行代碼能夠合爲一行:

comment = makepattern(handler, "comment", P("//") * patterns.space^0 * (1 - patterns.newline)^0)

再來看 t-simple-pretty-c.lua 中與識別「/* ... */」形式的註釋文本有關聯的代碼:

incomment = makepattern(handler,"comment",incomment_open)
            * ( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0
            * makepattern(handler,"comment",incomment_close)

這兩處代碼的區別是什麼?用於構造 comment 的代碼只用了一次 makepattern 函數,而用於構造 incomment 的代碼卻用了三次 makepattern 函數,還用了一次 V("whitespace)

一樣是識別註釋文本,用於識別「/* ... */」形式的註釋文本的代碼彷佛很羅嗦。結合「// 測試 1」這種形式的註釋文本,我能猜想出「P("//") * patterns.space^0 * (1 - patterns.newline)^0」的做用,它在描述一種文本模式,而這種形式一定與 C 代碼註釋形式相符,即:

  • // 開頭的的文本,這與 「P("//") 」對應;
  • // 以後不能出現換行符,這與「 (1 - patterns.newline)^0」對應。

除此以外,沒有更好的解釋了,對我而言。

那麼「patterns.space^0」對應什麼呢?我認爲它是多餘的。因此,我就試驗了一下,從代碼中將其刪除,結果代表,並不影響 ConTeXt MkIV 對「// ...」形式的註釋文本的識別。至此,我可以肯定,pretty-c 模塊的做者有些犯糊塗。「patterns.space^0」,顧名思義,我猜它的意思是「可能有空格,也可能沒有」。同理,「 (1 - patterns.newline)^0」的意思是「可能有字符,也可能沒有,可是不能出現換行符(newline)」。

*」是一個運算符,它將「P("//") 」、「patterns.space^0」以及「 (1 - patterns.newline)^0」這三者鏈接起來,就造成了 ConTeXt MkIV 對「// ...」形式的註釋文本的識別規則,而這個規則由 makepattern 函數生成。

充分利用這些信息,我就差很少能夠看懂用於識別「/* ... */」形式的註釋文本的代碼了。我能夠將

makepattern(handler,"comment",incomment_open)
* ( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0
* makepattern(handler,"comment",incomment_close)

肢解爲:

  • makepattern(handler,"comment",incomment_open)」用於生成識別「/*」的規則;
  • makepattern(handler,"comment",incomment_close)」用於生成識別「*/」的規則;
  • ( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0」用於生成識別位於「/*」和「*/」之間的字符(這些字符不能夠含有「*/」)的規則。

問題繼續明確,simple-pretty-c 模塊,之因此不能識別含有中文字符的「/* ... */」註釋文本,是由於「( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0」有錯誤。另外,我也可以肯定一個事實,即 makepattern 所生成的規則也能夠用「*」運算符鏈接起來,從而合成一條規則。

我如今掌握的信息已經足夠多了,甚至能夠判定,我只須要用「 (1 - incomment_close)^0」去替代「( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0」,即可以完成識別位於「/*」和「*/」之間的字符(這些字符不能夠含有「*/」)這一任務,此外,我也不須要對用三次 makepattern,徹底能夠像對待「// ...」註釋文本那樣,只用一次 makepattern 便可。因而,我將 incomment 部分的代碼修改成:

incomment = makepattern(handler, "comment", incomment_open * (1-incomment_close)^0 * incomment_close)

再編譯 foo.tex,結果便正確了。

如今,可以正確處理含有中文的註釋文本的 t-simple-pretty-c.lua 文件,其內容以下:

local P, S, V, patterns = lpeg.P, lpeg.S, lpeg.V, lpeg.patterns

local context               = context
local verbatim              = context.verbatim
local makepattern           = visualizers.makepattern

local CSnippet              = context.CSnippet
local startCSnippet         = context.startCSnippet
local stopCSnippet          = context.stopCSnippet

local CSnippetComment       = verbatim.CSnippetComment

local handler = visualizers.newhandler {
    startinline  = function() CSnippet(false,"{") end,
    stopinline   = function() context("}") end,
    startdisplay = function() startCSnippet() end,
    stopdisplay  = function() stopCSnippet() end ,
    
    comment      = function(s) CSnippetComment(s) end,
}

local comment     = P("//") * (1 - patterns.newline)^0
local incomment_open = P("/*")
local incomment_close = P("*/")

local grammar = visualizers.newgrammar(
   "default",
   {
      "visualizer",
      comment = makepattern(handler,"comment",comment),
      incomment = makepattern(handler,"comment",incomment_open * (1-incomment_close)^0 * incomment_close),
      pattern = V("incomment") + V("comment") + V("default"),
      visualizer = V("pattern")^1
   }
)

local parser = P(grammar)
visualizers.register("c", { parser = parser, handler = handler, grammar = grammar } )

含中文字符的字串

對於 pretty-c 模塊中有關含中文字符的字串的處理,能夠採用如上述類似的方式進行修正。

例如,對於雙引號形式的字符串,其識別代碼應當以下:

dstring = makepattern(handler,"string",patterns.dquote * ((P("\\")*P(1))+1-patterns.dquote)^0 * patterns.dquote)
注:pretty-c 模塊裏, sstringdstring 的名字有點小混亂。

爲何中文不行?(2)

如今,回過頭來分析一下 pretty-c 模塊爲什麼沒法識別註釋文本以及字串中所包含的中文字符。爲了便於分析,須要將 incomment 規則改回去:

incomment = makepattern(handler,"comment",incomment_open)
                  * ( V("whitespace") + makepattern(handler,"comment",1-incomment_close) )^0
                  * makepattern(handler,"comment",incomment_close)

我已經可以肯定,這條規則所能識別的註釋文本,會發送給函數:

comment      = function(s) CSnippetComment(s) end

所以,我能夠在上面這個「方程」的右部增長可以將 s 的內容輸出到終端的代碼:

comment      = function(s) print(s) print("--------") CSnippetComment(s) end

而後編譯下面這份 foo.tex:

\usemodule[pretty-c]
 
\starttext
\starttyping[option=c]
/* test */
\stoptyping
\stoptext

在終端裏觀察 ConTeXt MkIV 編譯文檔的過程的輸出信息,能夠發現會出現如下信息:

/*
----
t
----
e
----
s
----
t
----
*/

這說明 incomment 規則識別註釋文本時,除起止符「/*」和「*/」以外,對註釋文本是逐個字符進行識別的。

我將 foo.tex 改成

\usemodule[pretty-c]
 
\starttext
\starttyping[option=c]
/* 測 */
\stoptyping
\stoptext

對其進行編譯,會輸出:

/*
----
�
----
�
----
�
----
*/

這個結果說明,「測」字被 incomment 規則當成了三個字符。因爲我知道 foo.tex 中的文本是 UTF-8 編碼,而「測」字的 UTF-8 編碼爲 0xE6 0xB5 0x8B,長度爲三個字節。如今能夠肯定 incomment 是按字節來識別除起止符「/*」和「*/」以外的註釋文本。因爲只有 ASCII 編碼是對字符按字節進行編碼,所以能夠判定,incomment 規則是錯誤地以 ASCII 編碼來解讀 UTF-8 編碼的字符。這就是 ConTeXt MkIV 在終端裏報出 String contains an invalid utf-8 sequence 這一錯誤的緣由。

在上述的終端輸出信息中能夠發現,註釋文本的起止符可以被正確識別,所以出現編碼識別錯誤之處在於

( V("whitespace") + makepattern(handler,"comment",1 - incomment_close) )^0

V("whitespace") 應該是識別空白字符的規則,它應該是多餘的。由於在上文已經肯定 (1 - incomment_close) 這樣的規則容許字串包含空白字符。所以,上述出錯的代碼可簡化爲

makepattern(handler, "comment", 1 - incomment_close)^0

是這行代碼出現了 UTF-8 編碼識別錯誤。它的含義是

  • makepattern(handler, "comment", 1 - incomment_close) 構造一條文本識別規則 X;
  • ^0 來 X 所能識別的文本可能出現屢次,也可能不出現。

根據上面的終端信息輸出,能夠肯定這條規則所實現的效果是逐個 ASCII 字符去識別文本,亦即 makepattern(handler, "comment", 1 - incomment_close) 只能起到識別一個 ASCII 字符的效果。因而,這條規則遇到表示爲多個字節的單個 UTF-8 編碼的字符,便會將其肢解,從而引起錯誤。

實際上,將 ^0 移入 makepattern 函數以內,即

makepattern(handler, "comment", (1 - incomment_close)^0)

這時,再編譯 foo.tex,就不會出錯,並且終端裏會顯示如下信息:

/*
----
 測 
----
*/

總結

解決問題要有耐心。有耐心未必會花費不少時間,反而大多數時候能夠節省時間。這個問題,我以前沒耐心,已經花費了我好幾年的「記憶」,直至今天得以解決,而解決這個問題只用了 1 天。

將 pretty-c 模塊簡化爲 simple-pretty-c 模塊的過程,有些相似電路分析中常常要用到的戴維南定理。總的原則是,構造一個能夠工做而且足夠簡單的小模塊,這樣便於對問題做細緻的分析。

在對 LPEG 近乎無知的狀況下,充分利用本身所掌握的信息,能夠對 LPEG 的基本用法進行反推,而且推斷出來的結果也是能夠利用的。固然,最好的辦法是閱讀 LPEG 的文檔。可是我認爲,這樣反推,會更有助於理解 LPEG。有時間的話,我會看看 LPEG 的論文。


[1] https://github.com/nibua-r/pr...
[2] http://www.inf.puc-rio.br/~ro...
[3] https://segmentfault.com/a/11...

相關文章
相關標籤/搜索