使用 PEG.js 以 trim 缩进实现 heredocs

Implement heredocs with trim indent using PEG.js

我正在研究一种类似于 ruby 的语言,称为 gaiman,我正在使用 PEG.js 生成解析器。

您知道是否有一种方法可以通过适当的缩进实现 heredocs?

xxx =  <<<END
       hello
       world
       END

输出应该是:

"hello
world"

我需要这个,因为这段代码看起来不太好:

def foo(arg) {
  if arg == "here" then
     return <<<END
xxx
  xxx
END
  end
end

这是用户想要的功能return:

"xxx
  xxx"

我希望代码看起来像这样:

def foo(arg) {
  if arg == "here" then
     return <<<END
            xxx
              xxx
            END
  end
end

如果我 trim 所有行,用户将无法在需要时使用带前导空格的字符串。有谁知道 PEG.js 是否允许这样做?

我还没有任何 heredocs 的代码,只是想确定我想要的东西是否可行。

编辑:

所以我尝试实现 heredocs,但问题是 PEG 不允许反向引用。

heredoc = "<<<" marker:[\w]+ "\n" text:[\s\S]+ marker {
    return text.join('');
}

它说标记未定义。至于 trimming 我想我可以使用 location() 函数

我不认为这是对解析器生成器的合理期望;几乎没有人能应对挑战。

首先,识别 here-string 语法本质上是上下文相关的,因为结束定界符必须是 <<< 标记后提供的定界符的精确副本。所以你需要一个自定义词法分析器,这意味着你需要一个允许你使用自定义词法分析器的解析器生成器。 (因此假设您需要无扫描器解析器的解析器生成器可能不是最佳选择。)

识别 here-string 标记的结尾应该不会太困难,尽管您不能使用单个正则表达式来完成。我的方法是使用自定义扫描功能,将 here-string 分成一系列行,然后将它们连接起来,直到到达仅包含结束定界符的行。

一旦您识别出文字的文本,您需要按照您想要的方式规范化空格是 <<< 开始的列号。这样,您可以 trim 字符串文字中的每一行。所以你只需要一个词法扫描器来准确报告标记位置。修剪通常不会在生成的词法扫描器中完成;相反,它将是相关的语义动作。 (同样,它可能是语法中的语义动作。但它始终是您编写的代码。)

当您 trim 文字时,您需要处理不可能的情况,因为用户没有遵守缩进要求。你需要用制表符做一些事情;正确处理这些可能意味着您需要一个词法扫描器来计算可见列位置而不是字符偏移量。

我不知道 peg.js 是否符合这些要求,因为我没有使用它。 (我确实查看了文档,但没有看到任何关于如何合并自定义扫描仪功能的指示。但这并不意味着没有办法做到这一点。)我希望至少上面的讨论让您可以查看要使用的解析器生成器的详细文档,否则可以找到适合此用例的不同解析器生成器。

这是不再维护的 PEG.js 的 Peggy 继承者中的 heredocs 的实现。此代码基于 GitHub issue.

heredoc = "<<<" begin:marker "\n" text:($any_char+ "\n")+ _ end:marker (
    &{ return begin === end; }
  / '' { error(`Expected matched marker "${begin}", but marker "${end}" was found`); }
) {
    const loc = location();
    const min = loc.start.column - 1;
    const re = new RegExp(`\s{${min}}`);
    return text.map(line => {
        return line[0].replace(re, '');
    }).join('\n');
}
any_char = (!"\n" .)
marker_char = (!" " !"\n" .)
marker "Marker" = $marker_char+

_ "whitespace"
  = [ \t\n\r]* { return []; }

编辑: 上面的 heredoc 之后的另一段代码不起作用,这里是更好的语法:

{ let heredoc_begin = null; }

heredoc = "<<<" beginMarker "\n" text:content endMarker {
    const loc = location();
    const min = loc.start.column - 1;
    const re = new RegExp(`^\s{${min}}`, 'mg');
    return {
        type: 'Literal',
        value: text.replace(re, '')
    };
}
__ = (!"\n" !" " .)
marker 'Marker' = $__+
beginMarker = m:marker { heredoc_begin = m; }
endMarker = "\n" " "* end:marker &{ return heredoc_begin === end; }
content = $(!endMarker .)*