ferno/loco

此包已被废弃且不再维护。未建议替代包。

维护者

详细信息

github.com/qntm/loco

主页

源码

问题

dev-main 2022-05-15 20:04 UTC

This package is auto-updated.

Last update: 2022-05-15 20:04:04 UTC


README

Loco 是一个 PHP 的解析库。

Loco 使用单值解析器,称为 MonoParser。传统的、“热情”的解析器返回一组可能的结果,如果解析不可能,则结果集为空。一个“懒惰”的解析器在第一次调用时返回一个可能的结果,然后在随后的每次调用中返回进一步的结果,直到没有更多可能的结果。相比之下,MonoParser 只返回单个结果或失败。这反过来使得回溯变得不可能,从而有两个效果:

  • 它防止了解析时间成为指数级的,
  • 并且将表达能力降低到只有某些特定的 无歧义 自由文法。

Loco 直接解析字符串,不需要中间词法分析步骤。

Loco 在创建文法时检测无限循环(例如 (|a)*)和 左递归(例如 S -> Sa)。

API

Loco 导出以下解析器类,所有类都在 Ferno\Loco 命名空间中。

Ferno\Loco\MonoParser

所有解析器继承的抽象基类。其中“Mono”表示解析器返回一个结果,或者失败。

Ferno\Loco\MonoParser 有一个重要的方法,match($string, $i = 0),它要么以 array("j" => 9, "value" => "something") 的形式返回成功的匹配,要么抛出 Ferno\Loco\ParseFailureException

还有一个更有用的方法 parse($string),它要么返回解析值 "something",要么在匹配失败或没有占用提供的整个字符串长度时抛出 Ferno\Loco\ParseFailureException

Ferno\Loco\EmptyParser

找到空字符串(并且总是成功)。回调没有传递任何参数。默认回调返回 null

new Ferno\Loco\EmptyParser();
// returns null

new Ferno\Loco\EmptyParser(
    function() { return array(); }
);
// return an empty array instead

Ferno\Loco\StringParser

找到静态字符串。回调传递一个参数,即匹配的字符串。是的,每次调用实际上都是相同的函数调用。默认回调返回第一个参数,即字符串。

new Ferno\Loco\StringParser("name");
// returns "name"

new Ferno\Loco\StringParser(
    "name",
    function($string) { return strrev($string); }
);
// returns "eman"

Ferno\Loco\RegexParser

匹配正则表达式。正则表达式必须使用 ^ 在要匹配的子字符串的开始处锚定。否则,就没有办法阻止 PHP 在表达式中的其他地方完全匹配,这是非常糟糕的。注意:像 /^a|b/ 这样的结构只将 "a" 锚定在字符串的开始处;"b" 可能在任何地方匹配!你应该使用 /^(a|b)//^a|^b/

回调为每个子匹配传递一个参数。例如,如果正则表达式是 /^ab(cd(ef)gh)ij/,则第一个参数是整个匹配 "abcdefghij",第二个参数是 "cdefgh",第三个参数是 "ef"。默认回调仅返回第一个参数,即整个匹配。

new Ferno\Loco\RegexParser("/^'([a-zA-Z_][a-zA-Z_0-9]*)'/");
// returns the full match including the single quotes

new Ferno\Loco\RegexParser(
    "/^'([a-zA-Z_][a-zA-Z_0-9]*)'/",
    function($match0, $match1) { return $match1; }
);
// discard the single quotes and returns only the inner string

Ferno\Loco\Utf8Parser

匹配单个UTF-8字符。您可以提供一组排除列表,其中包含不匹配的字符。

new Ferno\Loco\Utf8Parser(array("<", ">", "&"));
// any UTF-8 character except the three listed

回调函数传递一个参数,即匹配的字符串。默认回调函数返回第一个参数,即字符串。

为了获得最佳效果,可以交替使用Ferno\Loco\LazyAltParserFerno\Loco\StringParsers,例如匹配HTML字符实体“<”、“>”、“&”等。

Ferno\Loco\LazyAltParser

此封装器通过在多个内部解析器之间交替来封装“选择”解析器组合器。关键词是“延迟”。一旦其中一个匹配,就返回该结果,故事结束。没有合并多个内部解析器结果的能力,也没有返回(回溯)到该解析器并尝试获取其他结果的能力,如果第一个结果被证明是错误的。

回调函数传递一个参数,即唯一的成功内部匹配。默认回调函数直接返回第一个参数。

new Ferno\Loco\LazyAltParser(
    array(
        new Ferno\Loco\StringParser("foo"),
        new Ferno\Loco\StringParser("bar")
    )
);
// returns either "foo" or "bar"

Ferno\Loco\ConcParser

此封装器通过连接一系列内部解析器来封装“连接”解析器组合器。如果序列为空,则相当于上面的Ferno\Loco\EmptyParser

为每个内部解析器传递一个参数,每个参数包含该解析器的结果。例如,new Ferno\Loco\ConcParser(array($a, $b, $c), $callback)将向其回调函数传递三个参数。第一个包含解析器$a的结果,第二个包含解析器$b的结果,第三个包含解析器$c的结果。默认回调函数以数组的形式返回参数:return func_get_args();

new Ferno\Loco\ConcParser(
    array(
        new Ferno\Loco\RegexParser("/^<([a-zA-Z_][a-zA-Z_0-9]*)>/", function($match0, $match1) { return $match1; }),
        new Ferno\Loco\StringParser(", "),
        new Ferno\Loco\RegexParser("/^<(\d\d\d\d-\d\d-\d\d)>/",     function($match0, $match1) { return $match1; }),
        new Ferno\Loco\StringParser(", "),
        new Ferno\Loco\RegexParser("/^<([A-Z]{2}[0-9]{7})>/",       function($match0, $match1) { return $match1; }),
    ),
    function($name, $comma1, $opendate, $comma2, $ref) { return new Account($accountname, $opendate, $ref); }
);
// match something like "<Williams>, <2011-06-30>, <GH7784939>"
// return new Account("Williams", "2011-06-30", "GH7784939")

Ferno\Loco\GreedyMultiParser

此封装器封装了“Kleene星号闭包”解析器组合器,以匹配单个内部解析器多次(有限或无限多次)。有有限的上限,这大致相当于上面的Ferno\Loco\ConcParser。有无穷的上限,这更有趣。如名称所示,Ferno\Loco\GreedyMultiParser将在返回之前尽可能多次地匹配。没有同时返回多个匹配的选项;只返回最大的匹配。也没有回溯和尝试消费更多或更少的实例的选项。

为每个匹配传递一个参数。例如,new Ferno\Loco\GreedyMultiParser($a, 2, 4, $callback)可以向其回调函数传递2、3或4个参数。`new GreedyMultiParser($a, 0, null, $callback)`有无限的上限,可以向其回调函数传递无限数量的参数。(PHP似乎没有问题。)默认回调函数以数组的形式返回所有参数:return func_get_args();

记住,PHP函数可以定义为function(){...},同时接受任意数量的参数。

new Ferno\Loco\GreedyMultiParser(
    new Ferno\Loco\LazyAltParser(
        array(
            new Ferno\Loco\Utf8Parser(array("<", ">", "&")),                         // match any UTF-8 character except <, > or &
            new Ferno\Loco\StringParser("&lt;",  function($string) { return "<"; }), // ...or an escaped < (unescape it)
            new Ferno\Loco\StringParser("&gt;",  function($string) { return ">"; }), // ...or an escaped > (unescape it)
            new Ferno\Loco\StringParser("&amp;", function($string) { return "&"; })  // ...or an escaped & (unescape it)
        )
    ),
    0,                                                  // at least 0 times
    null,                                               // at most infinitely many times
    function() { return implode("", func_get_args()); } // concatenate all of the matched characters together
);
// matches a continuous string of valid, UTF-8 encoded HTML text
// returns the unescaped string

Ferno\Loco\Grammar

上述内容都很好,但并不完整。首先,当它们过度嵌套时,我们的解析器会变得很大且难以阅读。其次,它使递归变得非常困难;例如,解析器不能轻易地放在自己内部。没有递归,我们只能解析正则语言,而不能解析上下文无关语言。

Ferno\Loco\Grammar 类使得这变得非常简单。在核心上,Ferno\Loco\Grammar 只是一个 Ferno\Loco\MonoParser。但是,Ferno\Loco\Grammar 接受一个解析器关联数组作为输入——这意味着每个解析器都附有一个名称。同时,它内部的解析器可以通过名称而不是直接包含来引用其他解析器。在实例化时,Ferno\Loco\Grammar 会解决这些引用,并检测诸如左递归、引用不存在解析器的名称、危险的结构如 Ferno\Loco\GreedyMultiParser(new Ferno\Loco\EmptyParser(), 0, null) 等异常。

这是一个简单的 Ferno\Loco\Grammar,它可以识别(一些)有效的 HTML 段落并返回这些段落的文本内容

$p = new Ferno\Loco\Grammar(
    "paragraph",
    array(
        "paragraph" => new Ferno\Loco\ConcParser(
            array(
                "OPEN_P",
                "CONTENT",
                "CLOSE_P"
            ),
            function($open_p, $content, $close_p) {
                return $content;
            }
        ),

        "OPEN_P" => new Ferno\Loco\StringParser("<p>"),

        "CONTENT" => new Ferno\Loco\GreedyMultiParser(
            "UTF-8 CHAR",
            0,
            null,
            function() { return implode("", func_get_args()); }
        ),

        "CLOSE_P" => new Ferno\Loco\StringParser("</p>"),

        "UTF-8 CHAR" => new Ferno\Loco\LazyAltParser(
            array(
                new Ferno\Loco\Utf8Parser(array("<", ">", "&")),                         // match any UTF-8 character except <, > or &
                new Ferno\Loco\StringParser("&lt;",  function($string) { return "<"; }), // ...or an escaped < (unescape it)
                new Ferno\Loco\StringParser("&gt;",  function($string) { return ">"; }), // ...or an escaped > (unescape it)
                new Ferno\Loco\StringParser("&amp;", function($string) { return "&"; })  // ...or an escaped & (unescape it)
            )
        ),
    )
);

$p->parse("<p>Your text here &amp; here &amp; &lt;here&gt;</p>");
// returns "Your text here & here & <here>"

示例

Loco 还附带了一个公共领域示例集合

开发

假设您已安装 PHP 7.4 或更高版本,并安装了 Composer,运行

composer install

以安装所有依赖项,然后

composer run-script test

运行测试脚本,包括 linting。

关于 Loco

我创建了 Loco,因为我想要让人们在我的网站上使用 XHTML 注释,并且我希望能够以灵活的方式验证这些 XHTML,从窄小的 XHTML 子集开始,并随着时间的推移添加对更多标签的支持。我相信编写解析库会比手写(然后不断重写)解析器更有效和有教育意义。

Loco 与 Loco 语法示例 examples/simpleComment.php 实现了第一个目标。这些示例已经成功使用了几年的时间。后来,我开发了 examples/locoNotation.php,这使我处理的事情变得更加简单。然而,也存在一些缺点

  • 每次运行评论提交 PHP 脚本时,都必须实例化语法,这既费力又不优雅。PHP 不允许检索回调的文本内容,因此将 Loco 从解析库转换成真正的解析生成器的过程陷入了停滞。
  • 缺乏回溯意味着我必须非常小心地描述我的 CFG,以确保其无歧义且能正确工作。这种额外努力某种程度上抵消了意义。
  • 我现在意识到,在解析用户输入时,生成有意义的错误信息是考虑的重要因素之一。Loco 在这方面做得不太好,用户发现创建正确的 HTML 来使其满意很困难。
  • 我非常讨厌使用 PHP。

在开始项目之前,我还观察到 PHP 没有解析器组合库,并决定填补这个空白。然而,我又遇到了一些问题

  • 当时,我实际上并不知道“解析器组合”这个术语的含义。它不是“由其他解析器组合而成的解析器”。它是一个“接受一个或多个解析器作为输入并返回一个新解析器作为输出的函数或运算符”。您可以在上面看到术语被多次误用。据我所知,PHP 仍然没有解析器组合库。
  • 我知道,并且仍然知道,我对解析的一般知识知之甚少。
  • 我非常讨厌使用 PHP。

总的来说,我会说这个项目满足了当时的我的需求,如果你现在使用它并满足你的需求,那可能只是巧合。在使用 Loco 或检查其代码时,我会保持谨慎。在撰写本文时,我的网站上的评论只允许严格的纯文本,不允许 HTML 或任何其他类型的格式。