]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1456877 - Add a wrapper around libcmark_gfm to Bugzilla
authorIsrael Madueme <purelogiq@gmail.com>
Fri, 15 Jun 2018 21:42:19 +0000 (14:42 -0700)
committerDylan William Hardison <dylan@hardison.net>
Fri, 15 Jun 2018 21:42:19 +0000 (14:42 -0700)
Bugzilla.pm
Bugzilla/Markdown/GFM.pm [new file with mode: 0644]
Bugzilla/Markdown/GFM/Node.pm [new file with mode: 0644]
Bugzilla/Markdown/GFM/Parser.pm [new file with mode: 0644]
Bugzilla/Markdown/GFM/SyntaxExtension.pm [new file with mode: 0644]
Bugzilla/Markdown/GFM/SyntaxExtensionList.pm [new file with mode: 0644]
t/markdown.t [new file with mode: 0644]

index 7ab7031e713d4c2377376ab5e9733b3919687e9c..9df38138dd6388eec6a9578640a3b889d21365c5 100644 (file)
@@ -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 (file)
index 0000000..f3f24fc
--- /dev/null
@@ -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 (file)
index 0000000..da5af1a
--- /dev/null
@@ -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 (file)
index 0000000..5307b49
--- /dev/null
@@ -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 (file)
index 0000000..56efa17
--- /dev/null
@@ -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 (file)
index 0000000..06a9798
--- /dev/null
@@ -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 (file)
index 0000000..0344706
--- /dev/null
@@ -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;