Только что закончил разработку парсера pod (plain old documentation) для Perl 5, написанного на Perl 6. Грамматику сделать получилось на удивление легко – спасибо Perl 6, ведь я сам-то не особенно какой гений программирования. С помощью ребят из #perl6 я узнал много всего интересного по ходу дела, и хочу поделиться этим со всеми. Ну и код, конечно, тоже прилагается.
Кстати, рекомендую прочесть сначала моё введение в грамматики в Perl 6, и многое в этой статье станет более ясным.
В Perl 6 грамматика – особый тип классов для разбора текстов. Идея в том, чтобы объявить последовательность регулярок и назначить им токены, которые затем можно использовать для разбора ввода. Для Pod::Perl5::Grammar я подробно проработал спецификацию perlpod, добавляя по мере исследования стандартов нужные токены.
Конечно, есть несколько проблем. Во-первых, как определить регулярку для списков? В pod списки могут содержать списки – может ли определение включать себя? Оказывается, что рекурсивные определения возможны, если только они не совпадают со строкой нулевой длины, что приведёт к бесконечному циклу. Вот определение:
Токен over_back описывает весь список с начала до конца. Проще говоря, там написано, что лист должен начинаться с =over и заканчиваться с =back, а посередине может быть много всякого, включая ещё один over_back!
Для простоты я обычно называл токены так, как они пишутся в pod, хотя иногда это не получалось из-за пересечений пространств имён.
Следующий шаблон мне особенно нравится, поэтому я часто к нему обращался:
Он полезен, если вам надо найти шаблон, но при этом игнорировать всё остальное, если он не найден. В нашем случае, pod_section – это токен, определяющий секцию в pod, но pod часто пишут прямо в коде Perl, и тогда всё лишнее должно быть проигнорировано. Поэтому, во второй части определения используется негативный lookahead ?!before для проверки того, что следующий отрывок текста не равен pod_section, и используется точка, чтобы зацепить «всё остальное», включая переводы строк. Оба условия сгруппированы в квадратных скобках со звёздочкой снаружи, чтобы проверять текст посимвольно.
Грамматику можно использовать для разбора pod как оформленной отдельно, так и включённой в код. Она вырезает все секции pod и помещает их в объект match, с которым затем можно работать. Использовать её легко:
Классы действий – это обычные классы Perl 6, которые можно передавать в грамматику во время разбора. Они позволяют назначать поведение (действия) токенам для работы в момент совпадения шаблона. Надо просто назвать методы в классе так же, как токен, над которым его необходимо выполнить. Я написал класс действий pod-to-HTML. Вот метод для преобразования =head1 в HTML:
Каждый раз, когда грамматика использует токен head1, выполняется этот метод. Ему передаётся переменная $/, содержащая найденную последовательность head1, из который и извлекается текстовая строка.
Для преобразования в HTML каждый класс действий просто извлекает текст из нужного токена, переформатирует его и выводит. Всё работало прекрасно, пока я не встретил вложенные токены вроде кодов форматирования, находящихся внутри параграфа текста. Вместо:
Получалось:
Это происходит оттого, что italics и bold находятся регулярками в первую очередь. Пришлось использовать буфер для хранения HTML из токенов второго уровня. Когда находится токен параграфа, парсер подставляет вместо текста содержимое этого буфера. Класс выглядит так:
Особое внимание нужно обратить на работу с регулярками. Каждый пример класса действий использует $/. Это ошибка – догадайтесь, что случится в результате:
Присвоение переменной только для чтения или значению.
Ядерный взрыв. Когда $/ передаётся в head1, оно служит только для чтения. Выполнение любой регулярки в той же лексической области видимости попытается перезаписать $/. На это я пару раз напарывался, и с помощью канала #perl6 я остановился на таком варианте:
Добавив is copy к параметрам, я делаю копию значения вместо указания на $/. Затем я копирую переменную match в $match, и тогда следующая регулярка может спокойно работать с $/. Думаю, что лучше вообще сделать так:
Просто не называть параметр $/, и всё сработает. Но всесторонне я это пока не проверял.
Для использования класса действий мы просто передаём его в грамматику:
В первом примере используется позиционный аргумент :$actions. Он обязательно должен называться actions. Во втором примере я назвал аргумент :actions($actions), и в этом случае объект класса действий может называться как угодно.
Статьи на PerlTricks.com написаны в HTML, со своими именами классов и тегами span. Его сложно редактировать и сложно писать. Я хотел бы использовать для редактирования pod – он был бы проще для писателей и для редактора. Поэтому мне хочется расширить pod, добавив в него всякие полезные функции для блогов. Например, форматирование делается через B<...> и тому подобные функции. Почему бы не добавить @<… > для ссылок на Twitter, или M<… > для ссылок на MetaCPAN?
Поскольку грамматики в Perl 6 – это классы, их можно наследовать и переопределять. Поэтому я могу добавить свои собственные коды так:
Также нужно переопределить токен format_codes, чтобы включить в него новые:
Вот так всё просто. Новая грамматика сможет парсить pod и работать с моими новыми кодами форматирования. Конечно, класс Pod::Perl5::Pod тоже можно расширять и переопределять, и в результате получится что-то вроде:
Существует более наглядный способ работы с группами токенов, multi-dispatch. Вместо определения format_codes как списка альтернативных токенов, мы объявляем метод-прототип, и объявляем каждый метод форматирования как вариант multi прототипа.
При наследовании грамматики нет необходимости переопределять format_codes. Можно добавить новые через multi:
Такой подход также упрощает работу с объектом match в плане пути для извлечения данных. К примеру, следующий код выбирает секцию ссылок с третьего параграфа блока pod:
В первом примере требуется ссылка на имя формата токена. Но с помощью multi-dispatch этого можно избежать, как показано во втором примере.
В целом, написание парсера pod на Perl 6 было довольно простым и прямолинейным занятием. Если у вас возникают вопросы при программировании на Perl 6, крайне рекомендую irc канал #perl6 на сервере freenode, люди там собрались достаточно доброжелательные и отзывчивые.
Кстати, рекомендую прочесть сначала моё введение в грамматики в 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, люди там собрались достаточно доброжелательные и отзывчивые.