summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIsrael Madueme <purelogiq@gmail.com>2018-06-15 23:42:19 +0200
committerDylan William Hardison <dylan@hardison.net>2018-06-15 23:42:19 +0200
commit170ec08234e29050c5d78d52e4100207625897d2 (patch)
tree14e8abc9e746dc30f42527b024d85b64f474e001
parent404dc5496967203c5f99755340f43d712420446a (diff)
downloadbugzilla-170ec08234e29050c5d78d52e4100207625897d2.tar.gz
bugzilla-170ec08234e29050c5d78d52e4100207625897d2.tar.xz
Bug 1456877 - Add a wrapper around libcmark_gfm to Bugzilla
-rw-r--r--Bugzilla.pm12
-rw-r--r--Bugzilla/Markdown/GFM.pm92
-rw-r--r--Bugzilla/Markdown/GFM/Node.pm33
-rw-r--r--Bugzilla/Markdown/GFM/Parser.pm109
-rw-r--r--Bugzilla/Markdown/GFM/SyntaxExtension.pm31
-rw-r--r--Bugzilla/Markdown/GFM/SyntaxExtensionList.pm23
-rw-r--r--t/markdown.t73
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>'),
+ "&lt;script>hijack()&lt;/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;