五光十色

在看个人 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...

相关文章
相关标签/搜索