diff options
author | Israel Madueme <purelogiq@gmail.com> | 2018-06-15 23:42:19 +0200 |
---|---|---|
committer | Dylan William Hardison <dylan@hardison.net> | 2018-06-15 23:42:19 +0200 |
commit | 170ec08234e29050c5d78d52e4100207625897d2 (patch) | |
tree | 14e8abc9e746dc30f42527b024d85b64f474e001 | |
parent | 404dc5496967203c5f99755340f43d712420446a (diff) | |
download | bugzilla-170ec08234e29050c5d78d52e4100207625897d2.tar.gz bugzilla-170ec08234e29050c5d78d52e4100207625897d2.tar.xz |
Bug 1456877 - Add a wrapper around libcmark_gfm to Bugzilla
-rw-r--r-- | Bugzilla.pm | 12 | ||||
-rw-r--r-- | Bugzilla/Markdown/GFM.pm | 92 | ||||
-rw-r--r-- | Bugzilla/Markdown/GFM/Node.pm | 33 | ||||
-rw-r--r-- | Bugzilla/Markdown/GFM/Parser.pm | 109 | ||||
-rw-r--r-- | Bugzilla/Markdown/GFM/SyntaxExtension.pm | 31 | ||||
-rw-r--r-- | Bugzilla/Markdown/GFM/SyntaxExtensionList.pm | 23 | ||||
-rw-r--r-- | t/markdown.t | 73 |
7 files changed, 373 insertions, 0 deletions
diff --git a/Bugzilla.pm b/Bugzilla.pm index 7ab7031e7..9df38138d 100644 --- a/Bugzilla.pm +++ b/Bugzilla.pm @@ -38,6 +38,8 @@ use Bugzilla::Flag; use Bugzilla::Hook; use Bugzilla::Install::Localconfig qw(read_localconfig); use Bugzilla::Install::Util qw(init_console include_languages); +use Bugzilla::Markdown::GFM; +use Bugzilla::Markdown::GFM::Parser; use Bugzilla::Memcached; use Bugzilla::Template; use Bugzilla::Token; @@ -865,6 +867,11 @@ sub check_rate_limit { } } +sub markdown_parser { + return request_cache->{markdown_parser} + ||= Bugzilla::Markdown::GFM::Parser->new( {extensions => [qw( autolink tagfilter table strikethrough)] } ); +} + # Private methods # Per-process cleanup. Note that this is a plain subroutine, not a method, @@ -1190,6 +1197,11 @@ of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>. Feeds the provided message into our centralised auditing system. +=item C<markdown_parser> + +Returns a L<Bugzilla::Markdown::GFM::Parser> with the default extensions +loaded (autolink, tagfilter, table, and strikethrough). + =back =head1 B<CACHING> diff --git a/Bugzilla/Markdown/GFM.pm b/Bugzilla/Markdown/GFM.pm new file mode 100644 index 000000000..f3f24fc6a --- /dev/null +++ b/Bugzilla/Markdown/GFM.pm @@ -0,0 +1,92 @@ +package Bugzilla::Markdown::GFM; + +use 5.10.1; +use strict; +use warnings; + +use Alien::libcmark_gfm; +use FFI::Platypus; +use FFI::Platypus::Buffer qw( scalar_to_buffer buffer_to_scalar ); +use Exporter qw(import); + +use Bugzilla::Markdown::GFM::SyntaxExtension; +use Bugzilla::Markdown::GFM::SyntaxExtensionList; +use Bugzilla::Markdown::GFM::Parser; +use Bugzilla::Markdown::GFM::Node; + +our @EXPORT_OK = qw(cmark_markdown_to_html); + +my %OPTIONS = ( + default => 0, + sourcepos => ( 1 << 1 ), + hardbreaks => ( 1 << 2 ), + safe => ( 1 << 3 ), + nobreaks => ( 1 << 4 ), + normalize => ( 1 << 8 ), + validate_utf8 => ( 1 << 9 ), + smart => ( 1 << 10 ), + github_pre_lang => ( 1 << 11 ), + liberal_html_tag => ( 1 << 12 ), + footnotes => ( 1 << 13 ), + strikethrough_double_tilde => ( 1 << 14 ), + table_prefer_style_attributes => ( 1 << 15 ), +); + +my $FFI = FFI::Platypus->new( + lib => [grep { not -l $_ } Alien::libcmark_gfm->dynamic_libs], +); + +$FFI->custom_type( + markdown_options_t => { + native_type => 'int', + native_to_perl => sub { + my ($options) = @_; + my $result = {}; + foreach my $key (keys %OPTIONS) { + $result->{$key} = ($options & $OPTIONS{$key}) != 0; + } + return $result; + }, + perl_to_native => sub { + my ($options) = @_; + my $result = 0; + foreach my $key (keys %OPTIONS) { + if ($options->{$key}) { + $result |= $OPTIONS{$key}; + } + } + return $result; + } + } +); + +$FFI->attach(cmark_markdown_to_html => ['opaque', 'int', 'markdown_options_t'] => 'string', + sub { + my $c_func = shift; + my($markdown, $markdown_length) = scalar_to_buffer $_[0]; + return $c_func->($markdown, $markdown_length, $_[1]); + } +); + +# This has to happen after something from the main lib is loaded +$FFI->attach('core_extensions_ensure_registered' => [] => 'void'); + +core_extensions_ensure_registered(); + +Bugzilla::Markdown::GFM::SyntaxExtension->SETUP($FFI); +Bugzilla::Markdown::GFM::SyntaxExtensionList->SETUP($FFI); +Bugzilla::Markdown::GFM::Node->SETUP($FFI); +Bugzilla::Markdown::GFM::Parser->SETUP($FFI); + +1; + +__END__ + +=head1 NAME + +Bugzilla::Markdown::GFM - Sets up the FFI to libcmark_gfm. + +=head1 DESCRIPTION + +This modules mainly just does setup work. See L<Bugzilla::Markdown::GFM::Parser> +to actually render markdown to html. diff --git a/Bugzilla/Markdown/GFM/Node.pm b/Bugzilla/Markdown/GFM/Node.pm new file mode 100644 index 000000000..da5af1a68 --- /dev/null +++ b/Bugzilla/Markdown/GFM/Node.pm @@ -0,0 +1,33 @@ +package Bugzilla::Markdown::GFM::Node; + +use 5.10.1; +use strict; +use warnings; + +sub SETUP { + my ($class, $FFI) = @_; + + $FFI->custom_type( + markdown_node_t => { + native_type => 'opaque', + native_to_perl => sub { + bless \$_[0], $class if $_[0]; + }, + perl_to_native => sub { ${ $_[0] } }, + } + ); + + $FFI->attach( + [ cmark_node_free => 'DESTROY' ], + [ 'markdown_node_t' ] => 'void' + ); + + $FFI->attach( + [ cmark_render_html => 'render_html' ], + [ 'markdown_node_t', 'markdown_options_t', 'markdown_syntax_extension_list_t'] => 'string', + ); +} + +1; + +__END__ diff --git a/Bugzilla/Markdown/GFM/Parser.pm b/Bugzilla/Markdown/GFM/Parser.pm new file mode 100644 index 000000000..5307b49c1 --- /dev/null +++ b/Bugzilla/Markdown/GFM/Parser.pm @@ -0,0 +1,109 @@ +package Bugzilla::Markdown::GFM::Parser; + +use 5.10.1; +use strict; +use warnings; + +use FFI::Platypus::Buffer qw( scalar_to_buffer buffer_to_scalar ); + +sub new { + my ($class, $options) = @_; + my $extensions = delete $options->{extensions} // []; + my $parser = $class->_new($options); + $parser->{_options} = $options; + + eval { + foreach my $name (@$extensions) { + my $extension = Bugzilla::Markdown::GFM::SyntaxExtension->find($name) + or die "unknown extension: $name"; + $parser->attach_syntax_extension($extension); + } + }; + + return $parser; +} + +sub render_html { + my ($self, $markdown) = @_; + $self->feed($markdown); + my $node = $self->finish; + return $node->render_html($self->{_options}, $self->get_syntax_extensions); +} + +sub SETUP { + my ($class, $FFI) = @_; + + $FFI->custom_type( + markdown_parser_t => { + native_type => 'opaque', + native_to_perl => sub { + bless { _pointer => $_[0] }, $class; + }, + perl_to_native => sub { $_[0]->{_pointer} }, + } + ); + + $FFI->attach( + [ cmark_parser_new => '_new' ], + [ 'markdown_options_t' ] => 'markdown_parser_t', + sub { + my $c_func = shift; + return $c_func->($_[1]); + } + ); + + $FFI->attach( + [ cmark_parser_free => 'DESTROY' ], + [ 'markdown_parser_t' ] => 'void' + ); + + $FFI->attach( + [ cmark_parser_feed => 'feed'], + ['markdown_parser_t', 'opaque', 'int'] => 'void', + sub { + my $c_func = shift; + $c_func->($_[0], scalar_to_buffer $_[1]); + } + ); + + $FFI->attach( + [ cmark_parser_finish => 'finish' ], + [ 'markdown_parser_t' ] => 'markdown_node_t', + ); + + $FFI->attach( + [ cmark_parser_attach_syntax_extension => 'attach_syntax_extension' ], + [ 'markdown_parser_t', 'markdown_syntax_extension_t' ] => 'void', + ); + + $FFI->attach( + [ cmark_parser_get_syntax_extensions => 'get_syntax_extensions' ], + [ 'markdown_parser_t' ] => 'markdown_syntax_extension_list_t', + ); +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Markdown::GFM::Parser - Transforms markdown into HTML via libcmark_gfm. + +=head1 SYNOPSIS + + use Bugzilla::Markdown::GFM; + use Bugzilla::Markdown::GFM::Parser; + + my $parser = Bugzilla::Markdown::GFM::Parser->new({ + extensions => [qw( autolink tagfilter table strikethrough )] + }); + + say $parser->render_html(<<'MARKDOWN'); + # My header + + This is **markdown**! + + - list item 1 + - list item 2 + MARKDOWN diff --git a/Bugzilla/Markdown/GFM/SyntaxExtension.pm b/Bugzilla/Markdown/GFM/SyntaxExtension.pm new file mode 100644 index 000000000..56efa177a --- /dev/null +++ b/Bugzilla/Markdown/GFM/SyntaxExtension.pm @@ -0,0 +1,31 @@ +package Bugzilla::Markdown::GFM::SyntaxExtension; + +use 5.10.1; +use strict; +use warnings; + +sub SETUP { + my ($class, $FFI) = @_; + + $FFI->custom_type( + markdown_syntax_extension_t => { + native_type => 'opaque', + native_to_perl => sub { + bless \$_[0], $class if $_[0]; + }, + perl_to_native => sub { $_[0] ? ${ $_[0] } : 0 }, + } + ); + $FFI->attach( + [ cmark_find_syntax_extension => 'find' ], + [ 'string' ] => 'markdown_syntax_extension_t', + sub { + my $c_func = shift; + return $c_func->($_[1]); + } + ); +} + +1; + +__END__ diff --git a/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm b/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm new file mode 100644 index 000000000..06a9798c2 --- /dev/null +++ b/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm @@ -0,0 +1,23 @@ +package Bugzilla::Markdown::GFM::SyntaxExtensionList; + +use 5.10.1; +use strict; +use warnings; + +sub SETUP { + my ($class, $FFI) = @_; + + $FFI->custom_type( + markdown_syntax_extension_list_t => { + native_type => 'opaque', + native_to_perl => sub { + bless \$_[0], $class if $_[0]; + }, + perl_to_native => sub { $_[0] ? ${ $_[0] } : 0 }, + } + ); +} + +1; + +__END__ diff --git a/t/markdown.t b/t/markdown.t new file mode 100644 index 000000000..0344706c9 --- /dev/null +++ b/t/markdown.t @@ -0,0 +1,73 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. +use 5.10.1; +use strict; +use warnings; +use lib qw( . lib local/lib/perl5 ); +use Bugzilla; +use Test::More; + +my $parser = Bugzilla->markdown_parser; + +is( + $parser->render_html('# header'), + "<h1>header</h1>\n", + 'Simple header' +); + +is( + $parser->render_html('`code snippet`'), + "<p><code>code snippet</code></p>\n", + 'Simple code snippet' +); + +is( + $parser->render_html('http://bmo-web.vm'), + "<p><a href=\"http://bmo-web.vm\">http://bmo-web.vm</a></p>\n", + 'Autolink extension' +); + +is( + $parser->render_html('<script>hijack()</script>'), + "<script>hijack()</script>\n", + 'Tagfilter extension' +); + +is( + $parser->render_html('~~strikethrough~~'), + "<p><del>strikethrough</del></p>\n", + 'Strikethrough extension' +); + +my $table_markdown = <<'MARKDOWN'; +| Col1 | Col2 | +| ---- |:----:| +| val1 | val2 | +MARKDOWN + +my $table_html = <<'HTML'; +<table> +<thead> +<tr> +<th>Col1</th> +<th align="center">Col2</th> +</tr> +</thead> +<tbody> +<tr> +<td>val1</td> +<td align="center">val2</td> +</tr></tbody></table> +HTML + +is( + $parser->render_html($table_markdown), + $table_html, + 'Table extension' +); + +done_testing; |