1

在看我的 github 主页(据说有识之士正纷纷将自己的项目迁往 gitlab)的时候,发现了以前 fork 的一个项目 pretty-c。这个项目为 ConTeXt MkIV 实现了一个模块,用于解决 C 代码的高亮(Highlighting)问题。

ConTeXt MkIV 是我这十多年来用来排版一些含有数学公式、代码、表格等元素的文档最重要的工具,现在则是我拿来写学位论文的工具。若它不支持 C 代码高亮,会觉得它对不起我这份迟迟未能完成的论文,因为论文里所有算法皆以伪 C 代码的形式描述。

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,对其进行「编译」,

$ 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 代码中出现含有中文字符的注释时,也会像上面报错。

C 语言唯二允许我写中文的地方,在 pretty-c 里不被支持(韩文、日文、越文等东亚文字自然也不被支持),这让我无法容忍。

简化

pretty-c 的实现代码 [1] 分为两部分,亦即两份文件,t-pretty-c.mkiv 与 t-pretty-c.lua。

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

\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...


garfileo
6k 声望1.9k 粉丝

这里可能不会再更新了。


引用和评论

2 篇内容引用
0 条评论