]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1456878 - Support markdown comments
authorIsrael Madueme <purelogiq@gmail.com>
Tue, 11 Dec 2018 19:58:30 +0000 (14:58 -0500)
committerDylan William Hardison <dylan@hardison.net>
Tue, 11 Dec 2018 19:58:30 +0000 (14:58 -0500)
Adds support for markdown comments, notable features:
- Markdown rendering with the GFM library
- All new comments are treated as markdown
- new lines treated as new lines (hardbreaks extension on); this makes it so that you don't need 2 spaces a the end of a line to make a new line and is what github does too.
- BMO link previews for bug shorthand ~~as well as custom markdown links~~ (link previews on markdown links left for another day).
  - quoteUrls is used on the actual text nodes after markdown html is rendered.
- Works with comment editing
- Quoting a markdown comment will fetch the raw markdown
- Does *not* attempt to update the non-Bug Modal ui with markdown rendering, as requested in https://bugzilla.mozilla.org/show_bug.cgi?id=1456878#c9
- Adds a 'use_markdown' feature flag to the Advanced parameters. Things work as expected when on vs off.

21 files changed:
Bugzilla.pm
Bugzilla/Bug.pm
Bugzilla/Comment.pm
Bugzilla/Config/Advanced.pm
Bugzilla/Markdown.pm [new file with mode: 0644]
Bugzilla/Template.pm
Bugzilla/WebService/Bug.pm
attachment.cgi
docs/en/rst/api/core/v1/comment.rst
extensions/BMO/template/en/default/email/bugmail.html.tmpl
extensions/BugModal/template/en/default/bug_modal/activity_stream.html.tmpl
extensions/BugModal/template/en/default/bug_modal/new_comment.html.tmpl
extensions/BugModal/web/bug_modal.css
extensions/BugModal/web/bug_modal.js
extensions/EditComments/lib/WebService.pm
process_bug.cgi
skins/standard/global.css
t/008filter.t
t/markdown.t
template/en/default/admin/params/advanced.html.tmpl
template/en/default/bug/comment.html.tmpl

index bf82df668f8ad1ad1c8e967b6a6d9f6e1cec2b3d..c21b450b2a2254c145db2bbb52751de0e63aacda 100644 (file)
@@ -795,12 +795,10 @@ sub check_rate_limit {
   }
 }
 
-sub markdown_parser {
-  require Bugzilla::Markdown::GFM;
-  require Bugzilla::Markdown::GFM::Parser;
-  return request_cache->{markdown_parser}
-    ||= Bugzilla::Markdown::GFM::Parser->new(
-    {extensions => [qw( autolink tagfilter table strikethrough)]});
+sub markdown {
+  require Bugzilla::Markdown;
+  state $markdown = Bugzilla::Markdown->new;
+  return $markdown;
 }
 
 # Private methods
@@ -1121,10 +1119,9 @@ of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>.
 
 Feeds the provided message into our centralised auditing system.
 
-=item C<markdown_parser>
+=item C<markdown>
 
-Returns a L<Bugzilla::Markdown::GFM::Parser> with the default extensions
-loaded (autolink, tagfilter, table, and strikethrough).
+Returns a L<Bugzilla::Markdown> object.
 
 =back
 
index 5673ab6e32ac82f4e80f618ed66480630bc4dfe8..102ea78e748e4c654701fd60a1b2fe99b4d74bea 100644 (file)
@@ -995,6 +995,9 @@ sub create {
 
   # We now have a bug id so we can fill this out
   $creation_comment->{'bug_id'} = $bug->id;
+  if (Bugzilla->params->{use_markdown}) {
+    $creation_comment->{'is_markdown'} = 1;
+  }
 
   # Insert the comment. We always insert a comment on bug creation,
   # but sometimes it's blank.
@@ -2726,8 +2729,9 @@ sub set_all {
     $self->add_comment(
       $params->{'comment'}->{'body'},
       {
-        isprivate => $params->{'comment'}->{'is_private'},
-        work_time => $params->{'work_time'}
+        isprivate   => $params->{'comment'}->{'is_private'},
+        work_time   => $params->{'work_time'},
+        is_markdown => Bugzilla->params->{use_markdown} ? 1 : 0
       }
     );
   }
@@ -3248,7 +3252,7 @@ sub remove_cc {
   @$cc_users = grep { $_->id != $user->id } @$cc_users;
 }
 
-# $bug->add_comment("comment", {isprivate => 1, work_time => 10.5,
+# $bug->add_comment("comment", {isprivate => 1, work_time => 10.5, is_markdown => 1,
 #                               type => CMT_NORMAL, extra_data => $data});
 sub add_comment {
   my ($self, $comment, $params) = @_;
index 50bab3fec785a726276710f21871d71f80378940..5604175aed54038df3c33eb82ca8d4c0b720239d 100644 (file)
@@ -45,6 +45,7 @@ use constant DB_COLUMNS => qw(
   already_wrapped
   type
   extra_data
+  is_markdown
 );
 
 use constant UPDATE_COLUMNS => qw(
@@ -62,14 +63,15 @@ use constant ID_FIELD => 'comment_id';
 use constant LIST_ORDER => 'bug_when, comment_id';
 
 use constant VALIDATORS => {
-  bug_id     => \&_check_bug_id,
-  who        => \&_check_who,
-  bug_when   => \&_check_bug_when,
-  work_time  => \&_check_work_time,
-  thetext    => \&_check_thetext,
-  isprivate  => \&_check_isprivate,
-  extra_data => \&_check_extra_data,
-  type       => \&_check_type,
+  bug_id      => \&_check_bug_id,
+  who         => \&_check_who,
+  bug_when    => \&_check_bug_when,
+  work_time   => \&_check_work_time,
+  thetext     => \&_check_thetext,
+  isprivate   => \&_check_isprivate,
+  is_markdown => \&Bugzilla::Object::check_boolean,
+  extra_data  => \&_check_extra_data,
+  type        => \&_check_type,
 };
 
 use constant VALIDATOR_DEPENDENCIES => {
@@ -234,6 +236,7 @@ sub body            { return $_[0]->{'thetext'}; }
 sub bug_id          { return $_[0]->{'bug_id'}; }
 sub creation_ts     { return $_[0]->{'bug_when'}; }
 sub is_private      { return $_[0]->{'isprivate'}; }
+sub is_markdown     { return $_[0]->{'is_markdown'}; }
 
 sub work_time {
 
@@ -579,6 +582,10 @@ C<string> Time spent as related to this comment.
 
 C<boolean> Comment is marked as private.
 
+=item C<is_markdown>
+
+C<boolean> Whether this comment needs Markdown rendering to be applied.
+
 =item C<already_wrapped>
 
 If this comment is stored in the database word-wrapped, this will be C<1>.
index 7b76944e14ea22dc3d667e1d27965bf9bc98483d..26d5d12460be7eec724835ccd8e03071e0efe6a2 100644 (file)
@@ -28,6 +28,8 @@ use constant get_param_list => (
   },
 
   {name => 'disable_bug_updates', type => 'b', default => 0},
+
+  {name => 'use_markdown', type => 'b', default => 0},
 );
 
 1;
diff --git a/Bugzilla/Markdown.pm b/Bugzilla/Markdown.pm
new file mode 100644 (file)
index 0000000..6a69694
--- /dev/null
@@ -0,0 +1,74 @@
+# 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.
+
+package Bugzilla::Markdown;
+use 5.10.1;
+use Moo;
+
+use Encode;
+use Mojo::DOM;
+use HTML::Escape qw(escape_html);
+use List::MoreUtils qw(any);
+
+has 'markdown_parser' => (is => 'lazy');
+has 'bugzilla_shorthand' => (
+  is      => 'ro',
+  default => sub {
+    require Bugzilla::Template;
+    \&Bugzilla::Template::quoteUrls;
+  }
+);
+
+sub _build_markdown_parser {
+  if (Bugzilla->has_feature('alien_cmark')) {
+    require Bugzilla::Markdown::GFM;
+    require Bugzilla::Markdown::GFM::Parser;
+    return Bugzilla::Markdown::GFM::Parser->new({
+      hardbreaks    => 1,
+      validate_utf8 => 1,
+      safe          => 1,
+      extensions    => [qw( autolink tagfilter table strikethrough )],
+    });
+  }
+  else {
+    return undef;
+  }
+}
+
+sub render_html {
+  my ($self, $markdown, $bug, $comment, $user) = @_;
+  my $parser = $self->markdown_parser;
+  return escape_html($markdown) unless $parser;
+
+  my @valid_text_parent_tags = ('p', 'li', 'td');
+  my @bad_tags               = qw( img );
+  my $bugzilla_shorthand     = $self->bugzilla_shorthand;
+  my $html                   = decode('UTF-8', $parser->render_html($markdown));
+  my $dom                    = Mojo::DOM->new($html);
+
+  $dom->find(join(', ', @bad_tags))->map('remove');
+  $dom->find(join ', ', @valid_text_parent_tags)->map(sub {
+    my $node = shift;
+    $node->descendant_nodes->map(sub {
+      my $child = shift;
+      if ( $child->type eq 'text'
+        && $child->children->size == 0
+        && any { $child->parent->tag eq $_ } @valid_text_parent_tags)
+      {
+        my $text = $child->content;
+        $child->replace(Mojo::DOM->new($bugzilla_shorthand->($text)));
+      }
+      return $child;
+    });
+    return $node;
+  });
+  return $dom->to_string;
+
+}
+
+
+1;
index fbc8690641886ce109f7a23a034de10f165226c5..23ef28a4d1c271f0e5fb8ddfc4c2b98e2e848a42 100644 (file)
@@ -739,6 +739,22 @@ sub create {
         1
       ],
 
+      renderMarkdown => [
+        sub {
+          my ($context, $bug, $comment, $user) = @_;
+          return sub {
+            my $text = shift;
+            if ($comment && $comment->is_markdown && Bugzilla->params->{use_markdown}) {
+              return Bugzilla->markdown->render_html($text, $bug, $comment, $user);
+            }
+            else {
+              return quoteUrls($text, $bug, $comment, $user);
+            }
+          };
+        },
+        1
+      ],
+
       bug_link => [
         sub {
           my ($context, $bug, $options) = @_;
index c04d1bc283afb65d614c30b829c7dfd3141849aa..e4727ed56a4cd95e8c47e8afa275804fe7bc2c2d 100644 (file)
@@ -370,7 +370,10 @@ sub render_comment {
   Bugzilla->switch_to_shadow_db();
   my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef;
 
-  my $html = Bugzilla::Template::quoteUrls($params->{text}, $bug);
+  my $html
+    = Bugzilla->params->{use_markdown}
+    ? Bugzilla->markdown->render_html($params->{text}, $bug)
+    : Bugzilla::Template::quoteUrls($params->{text}, $bug);
 
   return {html => $html};
 }
index 01ee9a9e2c12fbbd81dd3e8da83dddfcf7851f65..51c0f012d1554d6f0a00c5997efb6eb78e3aa1ef 100755 (executable)
@@ -621,7 +621,8 @@ sub insert {
     {
       isprivate  => $attachment->isprivate,
       type       => CMT_ATTACHMENT_CREATED,
-      extra_data => $attachment->id
+      extra_data => $attachment->id,
+      is_markdown => Bugzilla->params->{use_markdown} ? 1 : 0
     }
   );
 
@@ -780,7 +781,8 @@ sub update {
       {
         isprivate  => $attachment->isprivate,
         type       => CMT_ATTACHMENT_UPDATED,
-        extra_data => $attachment->id
+        extra_data => $attachment->id,
+        is_markdown => Bugzilla->params->{use_markdown} ? 1 : 0
       }
     );
   }
index 2e6ca1e29ce2e5f31cb5be74a8f4fb1850ac474c..a1ef7abb3038b34d6bda2fc07be6a3b6e4aaf4a0 100644 (file)
@@ -102,6 +102,8 @@ creation_time  datetime  This is exactly same as the ``time`` key. Use this
 is_private     boolean   ``true`` if this comment is private (only visible to a
                          certain group called the "insidergroup"), ``false``
                          otherwise.
+is_markdown    boolean   ``true`` if this comment is markdown. ``false`` if this
+                         comment is plaintext.
 =============  ========  ========================================================
 
 **Errors**
@@ -123,7 +125,8 @@ it can also throw the following errors:
 Create Comments
 ---------------
 
-This allows you to add a comment to a bug in Bugzilla.
+This allows you to add a comment to a bug in Bugzilla. All comments created via the
+API will be considered Markdown (specifically GitHub Flavored Markdown).
 
 **Request**
 
index 7f2754fdcecf28e2e037f6515d75cb3f8c8164c5..0e6620bf82b014a6949d2e363dccfac9a8000053 100644 (file)
               at [% comment.creation_ts FILTER time(undef, to_user.timezone) %]
             </b>
           [% END %]
-          <pre class="comment" style="font-size: initial">[% comment.body_full({ wrap => 1 }) FILTER quoteUrls(bug, comment) %]</pre>
+          [% IF comment.is_markdown AND Param('use_markdown') %]
+            [% comment_tag = 'div' %]
+          [% ELSE %]
+            [% comment_tag = 'pre' %]
+          [% END %]
+          <[% comment_tag FILTER none %] class="comment" style="font-size: initial">[% comment.body_full({ wrap => 1 }) FILTER renderMarkdown(bug, comment) %]</[% comment_tag FILTER none %]>
         </div>
       [% END %]
     </div>
index 03aa9163904e0a6e53bedac948afb5b0694c795a..4aac9524d959c365ac62621ae0a7caedb782f11b 100644 (file)
 [% END %]
 
 [% BLOCK comment_body %]
-  <pre class="comment-text [%= "bz_private" IF comment.is_private %]" id="ct-[% comment.count FILTER none %]"
-    [% IF comment.collapsed +%] style="display:none"[% END ~%]
+  [% IF comment.is_markdown AND Param('use_markdown') %]
+    [% comment_tag = 'div' %]
+  [% ELSE %]
+    [% comment_tag = 'pre' %]
+  [% END %]
+
+  <[% comment_tag FILTER none %] class="comment-text [%= "bz_private" IF comment.is_private %]"
+                     id="ct-[% comment.count FILTER none %]"
+                     data-comment-id="[% comment.id FILTER none %]"
+                     [% IF comment.is_markdown +%] data-ismarkdown="true" [% END ~%]
+                     [% IF comment.collapsed +%] style="display:none"[% END ~%]
   >[% FILTER collapse %]
     [% IF comment.is_about_attachment && comment.attachment.is_image ~%]
-      <a href="[% basepath FILTER none %]attachment.cgi?id=[% comment.attachment.id FILTER none %]"
+      <a href="attachment.cgi?id=[% comment.attachment.id FILTER none %]"
         title="[% comment.attachment.description FILTER html %]"
-        class="lightbox"><img src="[% basepath FILTER none %]extensions/BugModal/web/image.png" width="16" height="16"></a>
+        class="lightbox"><img src="extensions/BugModal/web/image.png" width="16" height="16"></a>
     [% END %]
   [% END %]
-  [%~ comment.body_full FILTER quoteUrls(bug, comment) ~%]</pre>
+  [%~ comment.body_full FILTER renderMarkdown(bug, comment) ~%]</[% comment_tag FILTER none %]>
 [% END %]
 
 [%
index bb633788f08b4ab4d7068a409821b3be5f5b19c6..83502ed96ab7ccaa746fd7a9809896dfc9d5424d 100644 (file)
     <textarea rows="5" cols="80" name="comment" id="comment" aria-labelledby="comment-edit-tab"></textarea>
   </div>
   <div id="comment-preview-tabpanel" class="comment-tabpanel" role="tabpanel" aria-labelledby="comment-preview-tab" style="display:none">
-    <pre id="comment-preview" class="comment-text"></pre>
+    [% IF Param('use_markdown') %]
+      [% comment_tag = 'div' %]
+    [% ELSE %]
+      [% comment_tag = 'pre' %]
+    [% END %]
+    <[% comment_tag FILTER none %] id="comment-preview" class="comment-text"></[% comment_tag FILTER none %]>
   </div>
 
-  <div id="bugzilla-etiquette">
-    <a href="[% basepath FILTER none %]page.cgi?id=etiquette.html" target="_blank" tabindex="-1">
-      Comments Subject to Etiquette and Contributor Guidelines</a>
+  <div id="add-comment-tips">
+    [% IF Param('use_markdown') %]
+      <div id="comment-markdown-tip">
+        <img src="extensions/BMO/web/images/notice.png" width="16" height="16">
+        <a href="https://guides.github.com/features/mastering-markdown/" target="_blank">Markdown styling now supported</a>
+      </div>
+    [% END %]
+
+    <div id="bugzilla-etiquette">
+      <a href="page.cgi?id=etiquette.html" target="_blank" tabindex="-1">
+        Comments Subject to Etiquette and Contributor Guidelines</a>
+    </div>
   </div>
 
   <div id="after-comment-commit-button">
index ad3fd481d85c156fb5f211284d9c99e6213284ce..cb85e3faf7d68107b419ca95b1353d985dd1b765 100644 (file)
@@ -680,8 +680,8 @@ h3.change-name {
 body.platform-Win32 .comment-text, body.platform-Win64 .comment-text {
     font-family: "Fira Mono", monospace;
 }
-
-.comment-text span.quote, .comment-text span.quote_wrapped {
+.comment-text span.quote, .comment-text span.quote_wrapped,
+div.comment-text pre {
     background: #eee !important;
     color: #444 !important;
     display: block !important;
@@ -695,6 +695,40 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text {
     border: 1px dashed darkred;
 }
 
+/* Markdown comments */
+div.comment-text {
+    white-space: normal;
+    padding: 0 8px 0 8px;
+    font-family: inherit !important;
+}
+
+div.comment-text code {
+    color: #444;
+    background-color: #eee;
+    font-size: 13px;
+    font-family: "Fira Mono","Droid Sans Mono",Menlo,Monaco,"Courier New",monospace;
+}
+
+div.comment-text table {
+    border-collapse: collapse;
+}
+
+div.comment-text th, div.comment-text td {
+    padding: 5px 10px;
+    border: 1px solid #ccc;
+}
+
+div.comment-text hr {
+    display: block !important;
+}
+
+div.comment-text blockquote {
+    background: #fcfcfc;
+    border-left: 5px solid #ccc;
+    margin: 1.5em 10px;
+    padding: 0.5em 10px;
+}
+
 .comment-tags {
     padding: 0 8px !important;
 }
@@ -768,11 +802,16 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text {
     margin-top: 20px;
 }
 
-#add-comment-private,
-#bugzilla-etiquette {
+#add-comment-private {
     float: right;
 }
 
+#add-comment-tips {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 1em;
+}
+
 #comment {
     border: 1px solid #ccc;
 }
@@ -781,7 +820,7 @@ body.platform-Win32 .comment-text, body.platform-Win64 .comment-text {
     clear: both;
     width: 100%;
     box-sizing: border-box !important;
-    margin: 0 0 1em;
+    margin: 0 0 0.5em;
     max-width: 1024px;
 }
 
index 59d7b2ce541183aaaf85486b5193ff0c64b14e49..3e973fe1e1968cc77216781f6877637d52550445 100644 (file)
@@ -858,31 +858,54 @@ $(function() {
 
             var prefix = "(In reply to " + comment_author + " from comment #" + comment_id + ")\n";
             var reply_text = "";
-            if (BUGZILLA.user.settings.quote_replies == 'quoted_reply') {
-                var text = $('#ct-' + comment_id).text();
-                reply_text = prefix + wrapReplyText(text);
-            } else if (BUGZILLA.user.settings.quote_replies == 'simply_reply') {
-                reply_text = prefix;
+
+            var quoteMarkdown = function($comment) {
+                const uid = $comment.data('comment-id');
+                bugzilla_ajax(
+                    {
+                        url: `rest/bug/comment/${uid}`,
+                    },
+                    (data) => {
+                        const quoted = data['comments'][uid]['text'].replace(/\n/g, "\n> ");
+                        reply_text = `${prefix}\n> ${quoted}`;
+                        populateNewComment();
+                    }
+                );
             }
 
-            // quoting a private comment, check the 'private' cb
-            $('#add-comment-private-cb').prop('checked',
-                $('#add-comment-private-cb:checked').length || $('#is-private-' + comment_id + ':checked').length);
+            var populateNewComment = function() {
+                // quoting a private comment, check the 'private' cb
+                $('#add-comment-private-cb').prop('checked',
+                    $('#add-comment-private-cb:checked').length || $('#is-private-' + comment_id + ':checked').length);
 
-            // remove embedded links to attachment details
-            reply_text = reply_text.replace(/(attachment\s+\d+)(\s+\[[^\[\n]+\])+/gi, '$1');
+                // remove embedded links to attachment details
+                reply_text = reply_text.replace(/(attachment\s+\d+)(\s+\[[^\[\n]+\])+/gi, '$1');
 
-            $.scrollTo($('#comment'), function() {
-                if ($('#comment').val() != reply_text) {
-                    $('#comment').val($('#comment').val() + reply_text);
-                }
+                $.scrollTo($('#comment'), function() {
+                    if ($('#comment').val() != reply_text) {
+                        $('#comment').val($('#comment').val() + reply_text);
+                    }
 
-                if (BUGZILLA.user.settings.autosize_comments) {
-                    autosize.update($('#comment'));
-                }
+                    if (BUGZILLA.user.settings.autosize_comments) {
+                        autosize.update($('#comment'));
+                    }
 
-                $('#comment').focus();
-            });
+                    $('#comment').trigger('input').focus();
+                });
+            }
+
+            if (BUGZILLA.user.settings.quote_replies == 'quoted_reply') {
+                var $comment = $('#ct-' + comment_id);
+                if ($comment.attr('data-ismarkdown')) {
+                    quoteMarkdown($comment);
+                } else {
+                    reply_text = prefix + wrapReplyText($comment.text());
+                    populateNewComment();
+                }
+            } else if (BUGZILLA.user.settings.quote_replies == 'simply_reply') {
+                reply_text = prefix;
+                populateNewComment();
+            }
         });
 
     if (BUGZILLA.user.settings.autosize_comments) {
index ebb93d678bf4ce642d54c041a1e73f76d2c2afdc..338cc3907fcbd65ce343f70517579ea5f9ecf95f 100644 (file)
@@ -141,11 +141,15 @@ sub update_comment {
   $comment->{thetext} = $new_comment;
   $bug->_sync_fulltext(update_comments => 1);
 
+  my $html
+    = $comment->is_markdown && Bugzilla->params->{use_markdown}
+    ? Bugzilla->markdown->render_html($new_comment, $bug)
+    : Bugzilla::Template::quoteUrls($new_comment, $bug);
+
   # Respond with the updated comment and number of revisions
   return {
-    text => $self->type('string', $new_comment),
-    html =>
-      $self->type('string', Bugzilla::Template::quoteUrls($new_comment, $bug)),
+    text  => $self->type('string', $new_comment),
+    html  => $self->type('string', $html),
     count => $self->type(
       'int',
       $dbh->selectrow_array(
index c72d72d63e14c49ab9e948b946f6e3dbc1de9228..bae5b17195db6096cdc02bf2e627f9904badedf6 100755 (executable)
@@ -275,6 +275,7 @@ if (should_set('comment')) {
   $set_all_fields{comment} = {
     body       => scalar $cgi->param('comment'),
     is_private => scalar $cgi->param('comment_is_private'),
+    is_markdown => Bugzilla->params->{use_markdown} ? 1 : 0,
   };
 }
 if (should_set('see_also')) {
index 76f6aa250f6a1477716ae0a485f9f8c4c7c0aa75..2ff5529631c3e1db1372489c5bc6a87689fae557 100644 (file)
@@ -963,7 +963,7 @@ input.required, select.required, span.required_explanation {
 }
 
 #comment {
-    margin: 0px 0px 1em 0px;
+    margin: 0px 0px 0.5em 0px;
 }
 
 /*******************/
@@ -1518,7 +1518,8 @@ table.edit_form hr {
     left: 16px;
 }
 
-.bz_comment_text span.quote, .bz_comment_text span.quote_wrapped {
+.bz_comment_text span.quote, .bz_comment_text span.quote_wrapped,
+div.bz_comment_text pre {
     background: #eee !important;
     color: #444 !important;
     display: block !important;
@@ -1528,6 +1529,40 @@ table.edit_form hr {
     padding: 5px !important;
 }
 
+/* Markdown comments */
+div.bz_comment_text {
+    white-space: normal;
+    padding: 0 8px 0 8px;
+    font-family: inherit !important;
+}
+
+div.bz_comment_text code {
+    color: #444;
+    background-color: #eee;
+    font-size: 13px;
+    font-family: "Fira Mono","Droid Sans Mono",Menlo,Monaco,"Courier New",monospace;
+}
+
+div.bz_comment_text table {
+    border-collapse: collapse;
+}
+
+div.bz_comment_text th, div.bz_comment_text td {
+    padding: 5px 10px;
+    border: 1px solid #ccc;
+}
+
+div.bz_comment_text hr {
+    display: block !important;
+}
+
+div.bz_comment_text blockquote {
+    background: #fcfcfc;
+    border-left: 5px solid #ccc;
+    margin: 1.5em 10px;
+    padding: 0.5em 10px;
+}
+
 .bz_comment_tags {
     background: #eee;
     box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
index 6c9924020d04151bb2374813db917add14da4437..671b8ff813f1673a439cd0cadb898f90330a0142 100644 (file)
@@ -221,7 +221,7 @@ sub directive_ok {
                                         quoteUrls|time|uri|xml|lower|html_light|
                                         obsolete|inactive|closed|unitconvert|
                                         txt|html_linebreak|none|json|null|id|
-                                        markdown)\b/x;
+                                        markdown|renderMarkdown)\b/x;
   return 0;
 }
 
index ca31fcdc02695c249841b576c676e9e3945bde81..23f6692d137a147cd5f7419189c53f984b29b9b3 100644 (file)
@@ -8,10 +8,20 @@ use 5.10.1;
 use strict;
 use warnings;
 use lib qw( . lib local/lib/perl5 );
+
+use Bugzilla::Test::MockDB;
+use Bugzilla::Test::MockParams (password_complexity => 'no_constraints');
 use Bugzilla;
 use Test2::V0;
 
-my $parser = Bugzilla->markdown_parser;
+my $have_cmark_gfm = eval {
+    require Alien::libcmark_gfm;
+    require Bugzilla::Markdown::GFM;
+};
+
+plan skip_all => "these tests require Alien::libcmark_gfm" unless $have_cmark_gfm;
+
+my $parser = Bugzilla->markdown;
 
 is($parser->render_html('# header'), "<h1>header</h1>\n", 'Simple header');
 
@@ -27,11 +37,14 @@ is(
   'Autolink extension'
 );
 
-is(
-  $parser->render_html('<script>hijack()</script>'),
-  "&lt;script>hijack()&lt;/script>\n",
-  'Tagfilter extension'
-);
+SKIP: {
+  skip("currently no raw html is allowed via the safe option", 1);
+  is(
+    $parser->render_html('<script>hijack()</script>'),
+    "&lt;script&gt;hijack()&lt;/script&gt;\n",
+    'Tagfilter extension'
+  );
+}
 
 is(
   $parser->render_html('~~strikethrough~~'),
index 2fe59c53318e66e190d02ff7f825a2f68d915f95..ff63f62b542d4c698f9149b4da367499053d7bfa 100644 (file)
@@ -67,4 +67,9 @@
 
   disable_bug_updates =>
     "When enabled, all updates to $terms.bugs will be blocked.",
+
+  use_markdown =>
+    "When enabled, existing markdown comments will be rendered as markdown"
+    _ " and new comments will be treated as markdown. When disabled ALL comments,"
+    _ " will be rendered as plaintext and new comments will be plaintext.",
 } %]
index bbb1d9e8445920b4a48ac76a765dd82c313f430b..77c393366b81797cfe83f16c6851c0d4b3a8f85e 100644 (file)
   <div id="comment_preview" class="bz_default_hidden bz_comment">
     <div id="comment_preview_loading" class="bz_default_hidden">Generating Preview...</div>
     <div id="comment_preview_error" class="bz_default_hidden"></div>
-    <pre id="comment_preview_text" class="bz_comment_text"></pre>
+    <div id="comment_preview_text" class="bz_comment_text"></div>
+  </div>
+[% END %]
+
+[% IF Param('use_markdown') %]
+  <div id="comment-markdown-tip">
+    <img src="extensions/BMO/web/images/notice.png" width="16" height="16">
+    <a href="https://guides.github.com/features/mastering-markdown/" target="_blank">Markdown styling now supported</a>
   </div>
 [% END %]