From 16fa1095275da32ee86718a2673595d3c94b62f8 Mon Sep 17 00:00:00 2001 From: Matijs van Zuijlen Date: Sun, 4 Jan 2026 15:38:53 +0100 Subject: [PATCH 1/3] Filter in-reply-to text for note through full filter pipeline --- app/models/note.rb | 9 +++++++++ app/views/notes/show_in_reply.html.erb | 2 +- spec/controllers/notes_controller_spec.rb | 13 ++++++++++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index b640ce62..aeb5a5e2 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -43,6 +43,15 @@ def tags [] end + def generate_html(field, text = nil) + if field == :in_reply_to + html = TextFilter.make_filter("none").filter_text(text) + html_postprocess(field, html).to_s + else + super + end + end + def html_postprocess(field, html) super(field, PublifyCore::TextFilter::Twitterfilter.filtertext(html)) end diff --git a/app/views/notes/show_in_reply.html.erb b/app/views/notes/show_in_reply.html.erb index 84585258..d6176e2a 100644 --- a/app/views/notes/show_in_reply.html.erb +++ b/app/views/notes/show_in_reply.html.erb @@ -5,7 +5,7 @@ <%= image_tag(@reply['user']['profile_image_url'], class: 'alignleft', alt: @reply['user']['name']) %> <% end %> <%= get_reply_context_url(@reply) %> -

<%= nofollowify_links(PublifyCore::TextFilter::Twitterfilter.filtertext(@reply['text'])) %>

+

<%= nofollowify_links(@note.generate_html(:in_reply_to, @reply['text'])) %>

<%= get_reply_context_twitter_link(@reply) %> diff --git a/spec/controllers/notes_controller_spec.rb b/spec/controllers/notes_controller_spec.rb index bc37d187..303d8d77 100644 --- a/spec/controllers/notes_controller_spec.rb +++ b/spec/controllers/notes_controller_spec.rb @@ -36,10 +36,13 @@ end context "in reply" do + render_views + let(:reply) do { "id_str" => "123456789", "created_at" => DateTime.new(2014, 1, 23, 13, 47).in_time_zone, + "text" => "**original** #foo", "user" => { "screen_name" => "a screen name", "entities" => { @@ -57,9 +60,13 @@ before { get :show, params: { permalink: permalink } } - it { expect(response).to be_successful } - it { expect(response).to render_template("show_in_reply") } - it { expect(assigns[:page_title]).to eq("Notes | test blog ") } + it "successfully renders the tweet the note replies to without extra filters" do + aggregate_failures do + expect(response).to be_successful + expect(response.body).to have_text "**original** #foo" + expect(response.body).to have_link "#foo" + end + end end context "note not found" do From 7af4206a0dd40351f332a062b790d98d2efeb4e0 Mon Sep 17 00:00:00 2001 From: Matijs van Zuijlen Date: Sun, 4 Jan 2026 12:50:30 +0100 Subject: [PATCH 2/3] Merge notes show templates --- Manifest.txt | 1 - app/controllers/notes_controller.rb | 5 +---- app/views/notes/show.html.erb | 15 +++++++++++++++ app/views/notes/show_in_reply.html.erb | 18 ------------------ 4 files changed, 16 insertions(+), 23 deletions(-) delete mode 100644 app/views/notes/show_in_reply.html.erb diff --git a/Manifest.txt b/Manifest.txt index bc0dad84..decaacb0 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -247,7 +247,6 @@ app/views/notes/_note.html.erb app/views/notes/error.html.erb app/views/notes/index.html.erb app/views/notes/show.html.erb -app/views/notes/show_in_reply.html.erb app/views/notification_mailer/_mail_footer.html.erb app/views/notification_mailer/_mail_header.html.erb app/views/notification_mailer/article.html.erb diff --git a/app/controllers/notes_controller.rb b/app/controllers/notes_controller.rb index 604589ac..ed6223d0 100644 --- a/app/controllers/notes_controller.rb +++ b/app/controllers/notes_controller.rb @@ -19,10 +19,7 @@ def index def show @note = Note.published.find_by! permalink: CGI.escape(params[:permalink]) - if @note.in_reply_to_message.present? - @reply = JSON.parse(@note.in_reply_to_message) - render :show_in_reply - end + @reply = JSON.parse(@note.in_reply_to_message) if @note.in_reply_to_message.present? end private diff --git a/app/views/notes/show.html.erb b/app/views/notes/show.html.erb index 2cabdcd9..c5aa99fd 100644 --- a/app/views/notes/show.html.erb +++ b/app/views/notes/show.html.erb @@ -1,5 +1,20 @@

+ <% if @reply %> +
+ <% if @reply['user']['profile_image_url'] %> + <%= image_tag(@reply['user']['profile_image_url'], class: 'alignleft', alt: @reply['user']['name']) %> + <% end %> + <%= get_reply_context_url(@reply) %> +

<%= nofollowify_links(@note.generate_html(:in_reply_to, @reply['text'])) %>

+

+ + <%= get_reply_context_twitter_link(@reply) %> + +

+
+ <% end %> + <%= render partial: 'note', object: @note %>
diff --git a/app/views/notes/show_in_reply.html.erb b/app/views/notes/show_in_reply.html.erb deleted file mode 100644 index d6176e2a..00000000 --- a/app/views/notes/show_in_reply.html.erb +++ /dev/null @@ -1,18 +0,0 @@ -
-
-
- <% if @reply['user']['profile_image_url'] %> - <%= image_tag(@reply['user']['profile_image_url'], class: 'alignleft', alt: @reply['user']['name']) %> - <% end %> - <%= get_reply_context_url(@reply) %> -

<%= nofollowify_links(@note.generate_html(:in_reply_to, @reply['text'])) %>

-

- - <%= get_reply_context_twitter_link(@reply) %> - -

-
- - <%= render partial: 'note', object: @note %> -
-
From 0c2f2b489731b7ea8ed06bd3a3cd59d37d19b6f7 Mon Sep 17 00:00:00 2001 From: Matijs van Zuijlen Date: Sun, 4 Jan 2026 15:54:14 +0100 Subject: [PATCH 3/3] Pull Twitterfilter filter into Note model Only notes should use this filter. --- Manifest.txt | 1 - app/models/note.rb | 50 +++++++++++++++-- lib/publify_core.rb | 1 - lib/publify_core/text_filter/twitterfilter.rb | 55 ------------------- spec/lib/text_filter_plugin_spec.rb | 3 +- spec/models/note_spec.rb | 9 +++ .../text_filter/twitterfilter_spec.rb | 39 ------------- 7 files changed, 56 insertions(+), 102 deletions(-) delete mode 100644 lib/publify_core/text_filter/twitterfilter.rb delete mode 100644 spec/publify_core/text_filter/twitterfilter_spec.rb diff --git a/Manifest.txt b/Manifest.txt index decaacb0..6ad13463 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -385,7 +385,6 @@ lib/publify_core/text_filter/markdown.rb lib/publify_core/text_filter/markdown_smartquotes.rb lib/publify_core/text_filter/none.rb lib/publify_core/text_filter/smartypants.rb -lib/publify_core/text_filter/twitterfilter.rb lib/publify_core/version.rb lib/publify_guid.rb lib/publify_plugins.rb diff --git a/app/models/note.rb b/app/models/note.rb index aeb5a5e2..669799ef 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true +require "twitter" +require "json" +require "uri" +require "html/pipeline" +require "html/pipeline/hashtag/hashtag_filter" + class Note < Content - require "twitter" - require "json" - require "uri" include PublifyGuid include ConfigManager @@ -30,6 +33,40 @@ class Note < Content TWITTER_HTTPS_URL_LENGTH = 21 TWITTER_LINK_LENGTH = 22 + class TwitterHashtagFilter < HTML::Pipeline::HashtagFilter + def initialize(text) + super(text, + tag_url: "https://twitter.com/search?q=%%23%s&src=tren&mode=realtime", + tag_link_attr: "") + end + end + + class TwitterMentionFilter < HTML::Pipeline::MentionFilter + def initialize(text) + super(text, base_url: "https://twitter.com") + end + + # Override base mentions finder, treating @mention just like any other @foo. + def self.mentioned_logins_in(text, username_pattern = UsernamePattern) + text.gsub MentionPatterns[username_pattern] do |match| + login = Regexp.last_match(1) + yield match, login, false + end + end + + # Override base link creator, removing the class + def link_to_mentioned_user(login) + result[:mentioned_usernames] |= [login] + + url = base_url.dup + url << "/" unless %r{[/~]\z}.match?(url) + + "" \ + "@#{login}" \ + "" + end + end + def set_permalink self.permalink = "#{id}-#{body.to_permalink[0..79]}" if permalink.blank? save @@ -53,7 +90,12 @@ def generate_html(field, text = nil) end def html_postprocess(field, html) - super(field, PublifyCore::TextFilter::Twitterfilter.filtertext(html)) + helper = PublifyCore::ContentTextHelpers.new + html = helper.auto_link(html) + + html = TwitterHashtagFilter.new(html).call + html = TwitterMentionFilter.new(html).call.to_s + super end def truncate(message, length) diff --git a/lib/publify_core.rb b/lib/publify_core.rb index ebcc4b94..c0c36541 100644 --- a/lib/publify_core.rb +++ b/lib/publify_core.rb @@ -12,7 +12,6 @@ require "publify_core/text_filter/markdown" require "publify_core/text_filter/markdown_smartquotes" require "publify_core/text_filter/smartypants" -require "publify_core/text_filter/twitterfilter" require "publify_core/string_ext" require "bootstrap" diff --git a/lib/publify_core/text_filter/twitterfilter.rb b/lib/publify_core/text_filter/twitterfilter.rb deleted file mode 100644 index fe91fedb..00000000 --- a/lib/publify_core/text_filter/twitterfilter.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require "text_filter_plugin" -require "html/pipeline" -require "html/pipeline/hashtag/hashtag_filter" - -module PublifyCore::TextFilter - class Twitterfilter < TextFilterPlugin::PostProcess - plugin_display_name "Twitter Filter" - plugin_description "Convert hashtags and mentions to links" - - class TwitterHashtagFilter < HTML::Pipeline::HashtagFilter - def initialize(text) - super(text, - tag_url: "https://twitter.com/search?q=%%23%s&src=tren&mode=realtime", - tag_link_attr: "") - end - end - - class TwitterMentionFilter < HTML::Pipeline::MentionFilter - def initialize(text) - super(text, base_url: "https://twitter.com") - end - - # Override base mentions finder, treating @mention just like any other @foo. - def self.mentioned_logins_in(text, username_pattern = UsernamePattern) - text.gsub MentionPatterns[username_pattern] do |match| - login = Regexp.last_match(1) - yield match, login, false - end - end - - # Override base link creator, removing the class - def link_to_mentioned_user(login) - result[:mentioned_usernames] |= [login] - - url = base_url.dup - url << "/" unless %r{[/~]\z}.match?(url) - - "" \ - "@#{login}" \ - "" - end - end - - def self.filtertext(text) - # First, autolink - helper = PublifyCore::ContentTextHelpers.new - text = helper.auto_link(text) - - text = TwitterHashtagFilter.new(text).call - TwitterMentionFilter.new(text).call.to_s - end - end -end diff --git a/spec/lib/text_filter_plugin_spec.rb b/spec/lib/text_filter_plugin_spec.rb index 17ecdf81..83db2034 100644 --- a/spec/lib/text_filter_plugin_spec.rb +++ b/spec/lib/text_filter_plugin_spec.rb @@ -9,8 +9,7 @@ PublifyCore::TextFilter::None, PublifyCore::TextFilter::Markdown, PublifyCore::TextFilter::Smartypants, - PublifyCore::TextFilter::MarkdownSmartquotes, - PublifyCore::TextFilter::Twitterfilter) + PublifyCore::TextFilter::MarkdownSmartquotes) end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index d6aa8920..4673b1b4 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -294,6 +294,15 @@ expect(note.html).to eq(expected) end + it "works with a hashtag and a mention" do + note = create(:note, body: "A test tweet with a #hashtag and a @mention") + expected = + "

A test tweet with a #hashtag and a" \ + " @mention

" + expect(note.html).to eq expected + end + it "links markdown links only once" do note = create(:note, body: "A test tweet with [a markdown link](https://link.com)") expect(note.html) diff --git a/spec/publify_core/text_filter/twitterfilter_spec.rb b/spec/publify_core/text_filter/twitterfilter_spec.rb deleted file mode 100644 index 6ff64d5e..00000000 --- a/spec/publify_core/text_filter/twitterfilter_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -RSpec.describe PublifyCore::TextFilter::Twitterfilter do - describe ".filtertext" do - it "replaces a hashtag with a proper URL to Twitter search" do - text = described_class.filtertext("A test tweet with a #hashtag") - expect(text) - .to eq "A test tweet with a #hashtag" - end - - it "replaces a @mention by a proper URL to the twitter account" do - text = described_class.filtertext("A test tweet with a @mention") - expect(text) - .to eq("A test tweet with a @mention") - end - - it "replaces a http URL by a proper link" do - text = described_class.filtertext("A test tweet with a http://link.com") - expect(text).to eq("A test tweet with a http://link.com") - end - - it "replaces a https URL with a proper link" do - text = described_class.filtertext("A test tweet with a https://link.com") - expect(text) - .to eq("A test tweet with a https://link.com") - end - - it "works with a hashtag and a mention" do - text = described_class.filtertext("A test tweet with a #hashtag and a @mention") - expect(text) - .to eq("A test tweet with a #hashtag and a" \ - " @mention") - end - end -end