使用 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上。
标签
反馈
这篇文章有什么问题吗?请通过在GitHub上创建问题或拉取请求来帮助我们。