From 34aacdd184dc2dd63cb129ddfe39afcc5d509b4c Mon Sep 17 00:00:00 2001 From: Marwin Hochfelsner <50826859+hlfan@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:02:56 +0100 Subject: [PATCH] Fix copy-pasting breaking links in linkify --- lib/rich_text.rb | 15 +++++++++++++++ test/lib/rich_text_test.rb | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/lib/rich_text.rb b/lib/rich_text.rb index f1885f03d..c3b1f4b77 100644 --- a/lib/rich_text.rb +++ b/lib/rich_text.rb @@ -3,6 +3,7 @@ module RichText DESCRIPTION_MAX_LENGTH = 500 DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH = 450 + URL_UNSAFE_CHARS = "[^\\w!#$%&'*+,./:;=?@_~^\\-]" def self.new(format, text) case format @@ -88,12 +89,26 @@ module RichText def linkify(text, mode = :urls) ERB::Util.html_escape(text) + .then { |html| expand_host_shorthands(html) } .then { |html| auto_link(html, mode) } .html_safe end private + def expand_host_shorthands(text) + [ + [Settings.linkify_hosts, Settings.linkify_hosts_replacement], + [Settings.linkify_wiki_hosts, Settings.linkify_wiki_hosts_replacement] + ] + .select { |hosts, replacement| replacement && hosts&.any? } + .reduce(text) do |text, (hosts, replacement)| + text.gsub(/(?<=^|#{URL_UNSAFE_CHARS})\b#{Regexp.escape(replacement)}/) do + "#{Settings.server_protocol}://#{hosts[0]}" + end + end + end + def auto_link(text, mode) link_attr = 'rel="nofollow noopener noreferrer" dir="auto"' Rinku.auto_link(text, mode, link_attr) { |url| format_link_text(url) } diff --git a/test/lib/rich_text_test.rb b/test/lib/rich_text_test.rb index febe6fcbf..dd0fa0310 100644 --- a/test/lib/rich_text_test.rb +++ b/test/lib/rich_text_test.rb @@ -237,6 +237,18 @@ class RichTextTest < ActiveSupport::TestCase end end + def test_text_to_html_linkify_recognize + with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => "repl.example.com") do + r = RichText.new("text", "foo repl.example.com/some/path?query=te10#result12 bar") + assert_html r do + assert_dom "a", :count => 1, :text => "repl.example.com/some/path?query=te10#result12" do + assert_dom "> @href", "http://replace-me.example.com/some/path?query=te10#result12" + assert_dom "> @rel", "nofollow noopener noreferrer" + end + end + end + end + def test_text_to_html_linkify_replace_other_scheme with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => "repl.example.com") do r = RichText.new("text", "foo ftp://replace-me.example.com/some/path?query=te10#result12 bar") @@ -313,6 +325,33 @@ class RichTextTest < ActiveSupport::TestCase end end + def test_text_to_html_linkify_recognize_wiki + with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com", + :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do + r = RichText.new("text", "foo wiki.example.com/Tag:surface%3Dmetal bar") + assert_html r do + assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do + assert_dom "> @href", "http://replace-me-wiki.example.com/Tag:surface%3Dmetal" + assert_dom "> @rel", "nofollow noopener noreferrer" + end + end + end + end + + def test_text_to_html_linkify_idempotent + with_settings(:linkify_hosts => ["test.host"], :linkify_hosts_replacement => "test.host") do + t0 = "foo https://test.host/way/123456789 bar" + + r1 = RichText.new("text", t0) + t1 = Nokogiri::HTML.fragment(r1.to_html).text + + r2 = RichText.new("text", t1) + t2 = Nokogiri::HTML.fragment(r2.to_html).text + + assert_equal t1, t2 + end + end + def test_text_to_html_email r = RichText.new("text", "foo example@example.com bar") assert_html r do -- 2.39.5