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

от автора

Только что закончил разработку парсера 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, люди там собрались достаточно доброжелательные и отзывчивые.

ссылка на оригинал статьи http://habrahabr.ru/post/264225/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *