diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 index 27873269ee1..fd59b7328b1 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 @@ -129,7 +129,7 @@ const rule = { token = state.push('quote_header_close', 'div', -1); } - token = state.push('bbcode_open', 'blockquote', 1); + token = state.push('bbcode_open', 'blockquote', 1); }, after: function(state) { @@ -145,8 +145,8 @@ export function setup(helper) { opts.emojiSet = siteSettings.emoji_set; }); - helper.registerPlugin(md=>{ - md.block.bbcode.ruler.push('quotes', rule); + helper.registerPlugin(md => { + md.block.bbcode.ruler.push('quotes', rule); }); helper.whiteList(['img[class=avatar]']); diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 263d139e729..3796ce79d77 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -3,6 +3,7 @@ require_dependency 'url_helper' require_dependency 'pretty_text' +require_dependency 'quote_comparer' class CookedPostProcessor include ActionView::Helpers::NumberHelper @@ -33,6 +34,7 @@ class CookedPostProcessor DiscourseEvent.trigger(:before_post_process_cooked, @doc, @post) post_process_oneboxes post_process_images + post_process_quotes keep_reverse_index_up_to_date optimize_urls update_post_image @@ -90,6 +92,24 @@ class CookedPostProcessor end end + def post_process_quotes + @doc.css("aside.quote").each do |q| + post_number = q['data-post'] + topic_id = q['data-topic'] + if topic_id && post_number + comparer = QuoteComparer.new( + topic_id.to_i, + post_number.to_i, + q.css('blockquote').text + ) + + if comparer.modified? + q['class'] = ((q['class'] || '') + " quote-modified").strip + end + end + end + end + def add_large_image_placeholder!(img) url = img["src"] diff --git a/lib/pretty_text/shims.js b/lib/pretty_text/shims.js index aa95290e6cf..46ce2595873 100644 --- a/lib/pretty_text/shims.js +++ b/lib/pretty_text/shims.js @@ -73,12 +73,12 @@ function __lookupAvatar(p) { return __utils.avatarImg({size: "tiny", avatarTemplate: __helpers.avatar_template(p) }, __getURL); } -function __formatUsername(u) { - return __helpers.format_username(u); +function __formatUsername(username) { + return __helpers.format_username(username); } -function __lookupPrimaryUserGroup(p) { - return __helpers.lookup_primary_user_group(p); +function __lookupPrimaryUserGroup(username) { + return __helpers.lookup_primary_user_group(username); } function __getCurrentUser(userId) { diff --git a/lib/quote_comparer.rb b/lib/quote_comparer.rb new file mode 100644 index 00000000000..c466d718c65 --- /dev/null +++ b/lib/quote_comparer.rb @@ -0,0 +1,24 @@ +class QuoteComparer + def self.whitespace + " \t\r\n".freeze + end + + def initialize(topic_id, post_number, text) + @topic_id = topic_id + @post_number = post_number + @text = text + @parent_post = Post.where(topic_id: @topic_id, post_number: @post_number).first + end + + # This algorithm is far from perfect, but it follows the Discourse + # philosophy of "catch the obvious cases, leave moderation for the + # complicated ones" + def modified? + return true if @text.blank? || @parent_post.blank? + + parent_text = Nokogiri::HTML::fragment(@parent_post.cooked).text.delete(QuoteComparer.whitespace) + text = @text.delete(QuoteComparer.whitespace) + + !parent_text.include?(text) + end +end diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 74c2ba7f8d5..2f60a6e46f8 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -778,4 +778,38 @@ describe CookedPostProcessor do end + context "quote processing" do + let(:cpp) { CookedPostProcessor.new(cp) } + let(:pp) { Fabricate(:post, raw: "This post is ripe for quoting!") } + + context "with an unmodified quote" do + let(:cp) do + Fabricate( + :post, + raw: "[quote=\"#{pp.user.username}, post: #{pp.post_number}, topic:#{pp.topic_id}]\nripe for quoting\n[/quote]\ntest" + ) + end + + it "should not be marked as modified" do + cpp.post_process_quotes + expect(cpp.doc.css('aside.quote.quote-modified')).to be_blank + end + end + + context "with a modified quote" do + let(:cp) do + Fabricate( + :post, + raw: "[quote=\"#{pp.user.username}, post: #{pp.post_number}, topic:#{pp.topic_id}]\nmodified\n[/quote]\ntest" + ) + end + + it "should be marked as modified" do + cpp.post_process_quotes + expect(cpp.doc.css('aside.quote.quote-modified')).to be_present + end + end + + end + end diff --git a/spec/components/quote_comparer_spec.rb b/spec/components/quote_comparer_spec.rb new file mode 100644 index 00000000000..e0be873dd38 --- /dev/null +++ b/spec/components/quote_comparer_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' +require 'quote_comparer' + +describe QuoteComparer do + + describe "#modified?" do + let(:post) { Fabricate(:post, raw: "This has **text** we _are_ matching") } + + def qc(text) + QuoteComparer.new(post.topic_id, post.post_number, text) + end + + it "returns true for no post" do + expect(QuoteComparer.new(nil, nil, "test")).to be_modified + end + + it "returns true for nil text" do + expect(qc(nil)).to be_modified + end + + it "returns true for empty text" do + expect(qc("")).to be_modified + end + + it "returns true for modified text" do + expect(qc("text is modified")).to be_modified + end + + it "return false when the text matches exactly" do + expect(qc("This has text we are matching")).not_to be_modified + end + + it "return false when there's a substring" do + expect(qc("text we are")).not_to be_modified + end + + it "return false when there's extra space" do + expect(qc("\n\ntext we are \t")).not_to be_modified + end + end +end