使用 Perl 6 解析 Perl 5 pod

我刚刚完成了一个Perl 5 pod 解析器的开发,该解析器是用Perl 6编写的。开发语法出乎意料地简单,这证明了Perl 6的强大,因为我并不是天才程序员。在#perl6社区的帮助下,我学到了一些有趣的东西,并想与大家分享。还有代码!

顺便说一句,如果你还没有阅读我的Perl 6语法介绍,请先阅读,这样后面的文章应该更容易理解。

开发语法

在Perl 6中,语法是一个用于解析文本的特殊类型的类。基本思想是使用token方法声明一系列正则表达式,然后使用它们来解析输入。对于Pod::Perl5::Grammar,我实际上是通过阅读perlpod,即Perl 5 pod规范,来编写token的。

有几个挑战。首先,考虑如何定义一个列表的正则表达式?在pod中,列表可以包含列表,那么定义可以包含自身吗?答案是肯定的,递归定义是可行的,只要它不匹配空字符串,这会导致无限循环。下面是定义

token over_back { <over>
                    [
                      <_item> | <paragraph> | <verbatim_paragraph> | <blank_line> |
                      <_for> | <begin_end> | <pod> | <encoding> | <over_back>
                    ]*
                    <back>
                  }

token over      { ^^\=over [\h+ <[0..9]>+ ]? \n }
token _item     { ^^\=item \h+ <name>
                    [
                        [ \h+ <paragraph>  ]
                      | [ \h* \n <blank_line> <paragraph>? ]
                    ]
                  }
token back      { ^^\=back \h* \n }

token over_back 描述了从开始到结束的整个列表。它基本上说,列表必须以=over开始,以=back结束,并且可以在其中包含很多东西,包括另一个over_back

为了简单起见,我尽量将token的命名与pod中的写法一致。在某些情况下,这并不可能,例如item与Grammar类继承的另一个方法命名空间冲突。所以请注意这些情况,你会得到奇怪的错误(这是一个bug)。

这是我在语法中反复使用的一个我很喜欢的模式

[ <pod_section> | <?!before <pod_section> > .]*

该模式在有模式要捕获时很有用,但如果没有匹配的模式则忽略其他所有内容。在这种情况下,pod_section是一个定义pod部分的token,但pod通常与Perl代码一起编写,语法应该忽略这些内容。因此,定义的第二部分使用负向前瞻?!before来检查下一个字符不是pod_section,并使用点.来匹配其他所有内容(包括换行符)。两个条件都放在方括号中,星号放在组外,以便逐个字符检查。

该语法可用于解析独立和内联pod。它将找到的每个pod部分提取到匹配对象中(基本上是Perl数据结构),以便进行后续处理。它很容易使用

use Pod::Perl5::Grammar;

my $match = Pod::Perl5::Grammar.parse($pod);

# or

my $match = Pod::Perl5::Grammar.parsefile("/path/to/some.pod");

动作类

到目前为止一切都很酷,但我们还可以做得更多。动作类是常规的Perl 6类,可以在解析时传递给语法。它们为token匹配事件提供行为(动作)。只需将动作类中的方法命名为应执行的动作的token即可。我编写了一个从pod到HTML的动作。以下是将=head1转换为HTML的方法

method head1 ($/)
{
  self.add_to_html('body', "<h1>{$/<singleline_text>.Str}</h1>\n");
}

每次语法匹配到head1 token时,此方法都会执行。它传递正则表达式捕获变量$/,其中包含head1正则表达式捕获,从中提取文本字符串。

这里有一个有趣的事实:动作类甚至比语法更容易编写。使用Pod::Perl5::Grammar编写一个pod到markdown转换器将是微不足道的,除非有人比我先做(提示,提示)。话虽如此,我在开发过程中确实遇到了一些挑战。

本质上,对于HTML转换,每个动作类方法只需从匹配的标记中提取文本,按需重新格式化,然后打印出来。这种方法一直很好用,直到我遇到了嵌套标记,比如位于文本段落中的格式代码。你不想从这样

There are different ways to emphasize text, I<this is in italics> and  B<this is in bold>

变成这样

<i>this is in italics</i>
<b>this is in bold</b>
<p>There are different ways to emphasize text, I<this is in italics> and  B<this is in bold></p>

这种情况可能发生,因为斜体和粗体标记正则表达式会首先匹配。为了解决这个问题,我使用了一个缓冲区来存储转换后的子标记的HTML,然后在匹配到段落标记时,用缓冲区的内容替换其自身的文本。这个动作类的代码如下所示

method paragraph ($/ is copy)
{
  my $original_text = $/<text>.Str.chomp;
  my $para_text = $/<text>.Str.chomp;

  for self.get_buffer('paragraph').reverse -> $pair # reverse as we're working outside in
  {
    $para_text = $para_text.subst($pair.key, {$pair.value});
  }
  self.add_to_html('body', "<p>{$para_text}</p>\n");
  self.clear_buffer('paragraph');
  }

method italic ($/)
{
  self.add_to_buffer('paragraph', $/.Str => "<i>{$/<multiline_text>.Str}</i>");
}

method bold ($/)
{
  self.add_to_buffer('paragraph', $/.Str => "<b>{$/<multiline_text>.Str}</b>");
}

注意动作类的一个问题是正则表达式处理。我所见到的每个动作类示例都在方法签名中使用了$/。这是一个错误,因为你知道这会做什么

method head1 ($/)
{
  if $/.Str ~~ m/foobar/ # silly example
  {
    self.add_to_html('body', "<h1>{$/<singleline_text>.Str}\n");
  }
}
Cannot assign to a readonly variable or a value

蘑菇云风格的爆炸。当将$/传递给head1时,它只读。在相同的词法作用域中执行任何正则表达式都会尝试覆盖$/。这让我吃过几次亏,在#perl6的帮助下,我最终使用了这个模式

method head1 ($/ is copy)
{
  my $match = $/;
  if $match.Str ~~ m/foobar/
  {
    self.add_to_html('body', "<h1>{$match<singleline_text>.Str}</h1>\n");
  }
}

is copy添加到签名中会创建一个$/的副本,而不是引用。然后我将匹配变量复制到$match中,这样接下来的正则表达式就可以覆盖$/了。我认为更好的解决方案是这样做

method head1 ($match)
{
  if $match.Str ~~ m/foobar/
  {
    self.add_to_html('body', "<h1>{$match<singleline_text>.Str}</h1>\n");
  }
}

我认为就这么简单,只要不要将签名参数命名为$/,所有的麻烦就会消失。我没有对这个方法进行过彻底的测试...

要使用动作类,只需将其传递给语法

use Pod::Perl5::Grammar;
use Pod::Perl5::ToHTML;

my $actions = Pod::Perl5::ToHTML.new;
my $match = Pod::Perl5::Grammar.parse($pod, :$actions);

# or
my $match = Pod::Perl5::Grammar.parse($pod, :actions($actions));

在第一个例子中,我使用了命名位置参数:$actions。这必须命名为actions才能工作。在第二个例子中,我这样命名参数::actions($actions),在这种情况下,动作类对象可以命名为任何你想要的名字。

改进pod

PerlTricks.com文章是用HTML编写的。这种特殊的雪花式HTML带有类名和span标签。这对于作者来说既麻烦又难以编辑。我希望能使用pod作为源文件——这样作者使用起来会更方便,我编辑起来也会更快。话虽如此,我想扩展pod以包含一些有用的博客功能。例如,你可能熟悉像B<...>这样的格式代码用于加粗,等等。那么,对于Twitter引用,使用@< ... >如何?或者对于MetaCPAN链接使用M< ... >呢?

由于Perl 6语法是类,因此可以继承和重写。因此,我可以像这样将我的Twitter和Metacpan格式代码添加到语法中

grammar Pod::Perl5::Grammar::PerlTricks is Pod::Perl5::Grammar
{
  token twitter  { @\< <name> \> }
  token metacpan { M\< <name> \> }
}

我还需要重写format_codes标记,以便包括新的标记

token format_codes  {
  [
    <italic>|<bold>|<code>|<link>
    |<escape>|<filename>|<singleline>
    |<index>|<zeroeffect>|<twitter|<metacpan>
  ]
}

就这么简单。新的语法将解析所有的pod,以及我的两个新格式代码。当然,动作类Pod::Perl5::Pod也可以扩展和重写,看起来可能像这样

Pod::Perl5::ToHTML::PerlTricks is Pod::Perl5::ToHTML
{
  method twitter ($match)
  {
    self.add_to_buffer('paragraph',
      $match.Str =>
"<a href="http://twitter.com/{$match<name>.Str}">{$match<name>.Str}</a>");
  }
  method metacpan ($match)
  {
    self.add_to_buffer('paragraph',
      $match.Str =>
"<a href="{$match<name>.Str}">{$match<name>.Str}</a>"" >);
  }
}

等等,还有更多

有一种更干净的方式来管理标记组,它被称为多分派。我们不是将format_codes定义为一个可以匹配的替代标记列表,而是声明一个原型方法,并将每个格式方法声明为原型的multi。看看这个

proto token format_codes  { * }
multi token format_codes:italic { I\< <multiline_text>  \>  }
multi token format_codes:bold   { B\< <multiline_text>  \>  }
multi token format_codes:code   { C\< <multiline_text>  \>  }
...

现在当这个语法被继承时,没有必要重写format_codes。相反,我可以声明新的标记为多分派

grammar Pod::Perl5::Grammar::PerlTricks is Pod::Perl5::Grammar
{
  token format_codes:twitter  { @\< <name> \> }
  token format_codes:metacpan { M\< <name> \> }
}

使用多分派还有一点微小的优点,那就是在处理匹配对象时简化了数据提取路径。例如,这些代码从pod块的第三段中提取链接部分

is $match<pod_section>[0]<paragraph>[2]<text><format_codes>[0]<link><section>.Str # regular version
is $match<pod_section>[0]<paragraph>[2]<text><format_codes>[0]<section>.Str # multi dispatch equivalent

在第一个例子中,需要格式标记名link。但是,使用多分派,我们可以移除它,如第二个例子所示。

结论

这就是我学到的;总的来说,用Perl 6编写pod解析器很简单。如果你在用Perl 6编程并且有问题,我强烈推荐访问freenode上的#perl6 irc频道,那里的人都很友好,反应迅速。

更新: 添加了多分派示例。感谢Jonathan Scott Duff提供多分派解释和代码。2015-05-01


这篇文章最初发布在PerlTricks.com上。

标签

David Farrell

David是一位职业程序员,他经常在推特博客上分享代码和编程艺术。

浏览他们的文章

反馈

这篇文章有什么问题吗?请通过在GitHub上创建问题或拉取请求来帮助我们。