我正在努力了解 LPEG。我已经设法产生了一种符合我想要的语法,但是我一直在反对这个语法并且没有走远。这个想法是解析一个简化形式的 TeX 的文档。我想将文档拆分为:

  • 环境,即 \begin{cmd}\end{cmd} 对。
  • 命令可以采用像这样的参数:\foo{bar} 或可以是裸露的:\foo
  • 环境和命令都可以有这样的参数: \command[color=green,background=blue]{content}
  • 其他东西。

  • 我还想跟踪行号信息以进行错误处理。这是我到目前为止所拥有的:
    lpeg = require("lpeg")
    lpeg.locale(lpeg)
    -- Assume a lot of "X = lpeg.X" here.
    
    -- Line number handling from http://lua-users.org/lists/lua-l/2011-05/msg00607.html
    -- with additional print statements to check they are working.
    local newline = P"\r"^-1 * "\n" / function (a) print("New"); end
    local incrementline = Cg( Cb"linenum" )/ function ( a ) print("NL");  return a + 1 end , "linenum"
    local setup = Cg ( Cc ( 1) , "linenum" )
    nl = newline * incrementline
    space = nl + lpeg.space
    
    -- Taken from "Name-value lists" in http://www.inf.puc-rio.br/~roberto/lpeg/
    local identifier = (R("AZ") + R("az") + P("_") + R("09"))^1
    local sep = lpeg.S(",;") * space^0
    local value = (1-lpeg.S(",;]"))^1
    local pair = lpeg.Cg(C(identifier) * space ^0 * "=" * space ^0 * C(value)) * sep^-1
    local list = lpeg.Cf(lpeg.Ct("") * pair^0, rawset)
    local parameters = (P("[") * list * P("]")) ^-1
    
    -- And the rest is mine
    
    anything = C( (space^1 + (1-lpeg.S("\\{}")) )^1) * Cb("linenum") / function (a,b) return { text = a, line = b } end
    
    begin_environment = P("\\begin") * Ct(parameters) * P("{") * Cg(identifier, "environment") * Cb("environment") * P("}") / function (a,b) return { params = a[1], environment = b } end
    end_environment = P("\\end{") * Cg(identifier) * P("}")
    
    texlike = lpeg.P{
      "document";
      document = setup * V("stuff") * -1,
      stuff = Cg(V"environment" + anything + V"bracketed_stuff" + V"command_with" + V"command_without")^0,
      bracketed_stuff = P"{" * V"stuff" * P"}" / function (a) return a end,
      command_with =((P("\\") * Cg(identifier) * Ct(parameters) * Ct(V"bracketed_stuff"))-P("\\end{")) / function (i,p,n) return { command = i, parameters = p, nodes = n } end,
      command_without = (( P("\\") * Cg(identifier) * Ct(parameters) )-P("\\end{")) / function (i,p) return { command = i, parameters = p } end,
      environment = Cg(begin_environment * Ct(V("stuff")) * end_environment) / function (b,stuff, e) return { b = b, stuff = stuff, e = e} end
    }
    

    它几乎有效!
    > texlike:match("\\foo[one=two]thing\\bar")
    {
      command = "foo",
      parameters = {
        {
          one = "two",
        },
      },
    }
    {
      line = 1,
      text = "thing",
    }
    {
      command = "bar",
      parameters = {
      },
    }
    

    但!首先,我根本无法让行号处理部分工作。 incrementline 中的函数永远不会被触发。

    我也不太清楚如何将嵌套捕获信息传递给处理函数(这就是为什么我在语法上半随机地散布 CgCCt 的原因)。这意味着从 command_with 中只返回一项:
    > texlike:match("\\foo{text \\command moretext}")
    {
      command = "foo",
      nodes = {
        {
          line = 1,
          text = "text ",
        },
      },
      parameters = {
      },
    }
    

    我也希望能够检查环境开始和结束是否匹配,但是当我尝试这样做时,到我到达“结束”时,我从“开始”的反向引用不在范围内。我不知道从这里去哪里。

    最佳答案

    迟到的答案,但希望如果您仍在寻找解决方案或想知道问题出在哪里,它会提供一些见解。

    你的语法有几个问题,其中一些可能很难发现。

    此处的行增量看起来不正确:

    local incrementline = Cg( Cb"linenum" ) /
                          function ( a ) print("NL");  return a + 1 end,
                          "linenum"
    

    看起来您打算创建一个命名的捕获组而不是匿名组。 backcapture linenum 本质上就像一个变量一样使用。问题是因为这是在匿名捕获中,linenum 不会正确更新 - function(a) 在调用时总是会收到 1。您需要将关闭的 ) 移到最后,以便包含 "linenum":
    local incrementline = Cg( Cb"linenum" /
                          function ( a ) print("NL");  return a + 1 end,
                          "linenum")
    

    用于 Cg 捕获的相关 LPeg documentation

    第二个问题与您的 anything 非终端规则有关:
    anything = C( (space^1 + (1-lpeg.S("\\{}")) )^1) * Cb("linenum") ...
    

    这里有几件事要小心。首先,一个命名的 Cg 捕获(来自 incrementline 规则一旦它被修复)不会产生任何东西,除非它在一个表中或者你反向引用它。第二个主要的事情是它有一个像变量一样的临时作用域。更准确地说,一旦你在外部捕获中关闭它,它的范围就会结束——就像你在这里做的一样:
    C( (space^1 + (...) )^1)
    

    这意味着当你用 * Cb("linenum") 引用它的 backcapture 时,已经太晚了——你真正想要的 linenum 已经关闭了它的作用域。

    我总是发现 LPeg 的 re 语法更容易理解,所以我用它重写了语法:
    local grammar_cb =
    {
      fold = pairfold,
      resetlinenum = resetlinenum,
      incrementlinenum = incrementlinenum, getlinenum = getlinenum,
      error = error
    }
    
    local texlike_grammar = re.compile(
    [[
      document    <- '' -> resetlinenum {| docpiece* |} !.
      docpiece    <- {| envcmd |} / {| cmd |} / multiline
      beginslash  <- cmdslash 'begin'
      endslash    <- cmdslash 'end'
      envcmd      <- beginslash paramblock? {:beginenv: envblock :} (!endslash docpiece)*
                     endslash openbrace {:endenv: =beginenv :} closebrace / &beginslash {} -> error .
      envblock    <- openbrace key closebrace
      cmd         <- cmdslash {:command: identifier :} (paramblock? cmdblock)?
      cmdblock    <- openbrace {:nodes: {| docpiece* |} :} closebrace
      paramblock  <- opensq ( {:parameters: {| parampairs |} -> fold :} / whitesp) closesq
      parampairs  <- parampair (sep parampair)*
      parampair   <- key assign value
      key         <- whitesp { identifier }
      value       <- whitesp { [^],;%s]+ }
      multiline   <- (nl? text)+
      text        <- {| {:text: (!cmd !closebrace !%nl [_%w%p%s])+ :} {:line: '' -> getlinenum :} |}
      identifier  <- [_%w]+
      cmdslash    <- whitesp '\'
      assign      <- whitesp '='
      sep         <- whitesp ','
      openbrace   <- whitesp '{'
      closebrace  <- whitesp '}'
      opensq      <- whitesp '['
      closesq     <- whitesp ']'
      nl          <- {%nl+} -> incrementlinenum
      whitesp     <- (nl / %s)*
    ]], grammar_cb)
    

    回调函数直接定义为:
    local function pairfold(...)
      local t, kv = {}, ...
      if #kv % 2 == 1 then return ... end
      for i = #kv, 2, -2 do
        t[ kv[i - 1] ] = kv[i]
      end
      return t
    end
    
    local incrementlinenum, getlinenum, resetlinenum do
      local line = 1
      function incrementlinenum(nl)
        assert(not nl:match "%S")
        line = line + #nl
      end
    
      function getlinenum() return line end
      function resetlinenum() line = 1 end
    end
    

    使用多行的类似 tex 的非平凡 str 测试语法:
      local test1 = [[\foo{text \bar[color = red, background =   black]{
      moretext \baz{
    even
    more text} }
    
    
    this time skipping multiple
    
    lines even, such wow!}]]
    

    以 lua-table 格式生成以下 AST:
    {
      command = "foo",
      nodes = {
        {
          text = "text",
          line = 1
        },
        {
          parameters = {
            color = "red",
            background = "black"
          },
          command = "bar",
          nodes = {
            {
              text = "  moretext",
              line = 2
            },
            {
              command = "baz",
              nodes = {
                {
                  text = "even ",
                  line = 3
                },
                {
                  text = "more text",
                  line = 4
                }
              }
            }
          }
        },
        {
          text = "this time skipping multiple",
          line = 7
        },
        {
          text = "lines even, such wow!",
          line = 9
        }
      }
    }
    

    开始/结束环境的第二个测试:
      local test2 = [[\begin[p1
    =apple,
    p2=blue]{scope} scope foobar
    \end{scope} global foobar]]
    

    这似乎给出了您正在寻找的大致内容:
    {
      {
        {
          text = " scope foobar",
          line = 3
        },
        parameters = {
          p1 = "apple",
          p2 = "blue"
        },
        beginenv = "scope",
        endenv = "scope"
      },
      {
        text = " global foobar",
        line = 4
      }
    }
    

    关于lua - 使用 lpeg 解析类似 TeX 的语言,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/21622316/

    10-12 03:56