Pull to refresh

Парсим pod от Perl 5 при помощи Perl 6

Reading time7 min
Views1.9K
Original author: David Farrell
Только что закончил разработку парсера pod (plain old documentation) для Perl 5, написанного на Perl 6. Грамматику сделать получилось на удивление легко – спасибо Perl 6, ведь я сам-то не особенно какой гений программирования. С помощью ребят из #perl6 я узнал много всего интересного по ходу дела, и хочу поделиться этим со всеми. Ну и код, конечно, тоже прилагается.

Кстати, рекомендую прочесть сначала моё введение в грамматики в Perl 6, и многое в этой статье станет более ясным.

Разработка грамматики


В Perl 6 грамматика – особый тип классов для разбора текстов. Идея в том, чтобы объявить последовательность регулярок и назначить им токены, которые затем можно использовать для разбора ввода. Для Pod::Perl5::Grammar я подробно проработал спецификацию perlpod, добавляя по мере исследования стандартов нужные токены.

Конечно, есть несколько проблем. Во-первых, как определить регулярку для списков? В 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 }


Токен over_back описывает весь список с начала до конца. Проще говоря, там написано, что лист должен начинаться с =over и заканчиваться с =back, а посередине может быть много всякого, включая ещё один over_back!

Для простоты я обычно называл токены так, как они пишутся в pod, хотя иногда это не получалось из-за пересечений пространств имён.

Следующий шаблон мне особенно нравится, поэтому я часто к нему обращался:

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


Он полезен, если вам надо найти шаблон, но при этом игнорировать всё остальное, если он не найден. В нашем случае, pod_section – это токен, определяющий секцию в pod, но pod часто пишут прямо в коде Perl, и тогда всё лишнее должно быть проигнорировано. Поэтому, во второй части определения используется негативный lookahead ?!before для проверки того, что следующий отрывок текста не равен pod_section, и используется точка, чтобы зацепить «всё остальное», включая переводы строк. Оба условия сгруппированы в квадратных скобках со звёздочкой снаружи, чтобы проверять текст посимвольно.

Грамматику можно использовать для разбора pod как оформленной отдельно, так и включённой в код. Она вырезает все секции pod и помещает их в объект match, с которым затем можно работать. Использовать её легко:

use Pod::Perl5::Grammar;

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

# или

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


Классы действий


Классы действий – это обычные классы Perl 6, которые можно передавать в грамматику во время разбора. Они позволяют назначать поведение (действия) токенам для работы в момент совпадения шаблона. Надо просто назвать методы в классе так же, как токен, над которым его необходимо выполнить. Я написал класс действий pod-to-HTML. Вот метод для преобразования =head1 в HTML:

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


Каждый раз, когда грамматика использует токен head1, выполняется этот метод. Ему передаётся переменная $/, содержащая найденную последовательность head1, из который и извлекается текстовая строка.

Для преобразования в 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>


Это происходит оттого, что italics и bold находятся регулярками в первую очередь. Пришлось использовать буфер для хранения 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, поскольку мы работаем снаружи внутрь
  {
    $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/ # тупой пример
  {
    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 в $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);

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


В первом примере используется позиционный аргумент :$actions. Он обязательно должен называться actions. Во втором примере я назвал аргумент :actions($actions), и в этом случае объект класса действий может называться как угодно.

Улучшаем pod


Статьи на PerlTricks.com написаны в HTML, со своими именами классов и тегами span. Его сложно редактировать и сложно писать. Я хотел бы использовать для редактирования pod – он был бы проще для писателей и для редактора. Поэтому мне хочется расширить pod, добавив в него всякие полезные функции для блогов. Например, форматирование делается через B<...> и тому подобные функции. Почему бы не добавить @<… > для ссылок на Twitter, или M<… > для ссылок на MetaCPAN?

Поскольку грамматики в Perl 6 – это классы, их можно наследовать и переопределять. Поэтому я могу добавить свои собственные коды так:

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="https://metacpan.org/pod//{$match<name>.Str}">{$match<name>.Str}</a>");
  }
}


Это ещё не всё


Существует более наглядный способ работы с группами токенов, multi-dispatch. Вместо определения 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. Можно добавить новые через multi:

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


Такой подход также упрощает работу с объектом match в плане пути для извлечения данных. К примеру, следующий код выбирает секцию ссылок с третьего параграфа блока pod:

is $match<pod_section>[0]<paragraph>[2]<text><format_codes>[0]<link><section>.Str # обычный вариант
is $match<pod_section>[0]<paragraph>[2]<text><format_codes>[0]<section>.Str # эквивалент через multi dispatch 


В первом примере требуется ссылка на имя формата токена. Но с помощью multi-dispatch этого можно избежать, как показано во втором примере.

Заключение


В целом, написание парсера pod на Perl 6 было довольно простым и прямолинейным занятием. Если у вас возникают вопросы при программировании на Perl 6, крайне рекомендую irc канал #perl6 на сервере freenode, люди там собрались достаточно доброжелательные и отзывчивые.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+9
Comments0

Articles

Change theme settings