From 0f43be0bf4053f3e211758328005902402c2f9cf Mon Sep 17 00:00:00 2001 From: Marwin Hochfelsner <50826859+hlfan@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:12:03 +0100 Subject: [PATCH] Make linkify detect shortened paths --- config/settings.yml | 38 +++++++++++++++++++++++++++++ lib/rich_text.rb | 18 ++++++++++++++ test/lib/rich_text_test.rb | 49 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/config/settings.yml b/config/settings.yml index 625aeb007..bee4027ff 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -139,6 +139,44 @@ fossgis_valhalla_url: "https://valhalla1.openstreetmap.de/route" # Endpoints for Wikimedia integration wikidata_api_url: "https://www.wikidata.org/w/api.php" wikimedia_commons_url: "https://commons.wikimedia.org/wiki/" +linkify: + detection_rules: + - patterns: + - "node/(?\\d+)" + - "node (?\\d{5,})" + - "n ?(?\\d{5,})" + path_template: "node/\\k" + - patterns: + - "way/(?\\d+)" + - "way (?\\d{5,})" + - "w ?(?\\d{5,})" + path_template: "way/\\k" + - patterns: + - "relation/(?\\d+)" + - "relation (?\\d{5,})" + - "r ?(?\\d{5,})" + path_template: "relation/\\k" + - patterns: + - "changeset/(?\\d+)" + - "changeset (?\\d{5,})" + - "cs ?(?\\d{5,})" + path_template: "changeset/\\k" + - patterns: + - "note/(?\\d+)" + - "note (?\\d{5,})" + path_template: "note/\\k" + - patterns: + - "user/(?\\S+)" + - "@(?\\S+)" + path_template: "user/\\k" + - patterns: + - "(?[^\"?#<>/\\s]+)=\\*" + path_template: "wiki/Key:\\k" + host: "https://wiki.openstreetmap.org" + - patterns: + - "(?[^\"?#<>/\\s]+)=(?[^\"?#<>\\s]+)" + path_template: "wiki/Tag:\\k=\\k" + host: "https://wiki.openstreetmap.org" # Main website hosts to match in linkify linkify_hosts: ["www.openstreetmap.org", "www.osm.org", "www.openstreetmap.com", "openstreetmap.org", "osm.org", "openstreetmap.com"] # Shorter host to replace main hosts diff --git a/lib/rich_text.rb b/lib/rich_text.rb index c3b1f4b77..f7be24dd4 100644 --- a/lib/rich_text.rb +++ b/lib/rich_text.rb @@ -89,6 +89,7 @@ module RichText def linkify(text, mode = :urls) ERB::Util.html_escape(text) + .then { |html| expand_link_shorthands(html) } .then { |html| expand_host_shorthands(html) } .then { |html| auto_link(html, mode) } .html_safe @@ -96,6 +97,23 @@ module RichText private + def gsub_pairs_for_linkify_detection + Array + .wrap(Settings.linkify&.detection_rules) + .select { |rule| rule.path_template && rule.patterns.is_a?(Array) } + .flat_map do |rule| + expanded_path = "#{rule.host || "#{Settings.server_protocol}://#{Settings.server_url}"}/#{rule.path_template}" + rule.patterns + .select { |pattern| pattern.is_a?(String) } + .map { |pattern| [Regexp.new("(?<=^|#{URL_UNSAFE_CHARS})#{pattern}", Regexp::IGNORECASE), expanded_path] } + end + end + + def expand_link_shorthands(text) + gsub_pairs_for_linkify_detection + .reduce(text) { |text, (pattern, replacement)| text.gsub(pattern, replacement) } + end + def expand_host_shorthands(text) [ [Settings.linkify_hosts, Settings.linkify_hosts_replacement], diff --git a/test/lib/rich_text_test.rb b/test/lib/rich_text_test.rb index dd0fa0310..02ff67c9a 100644 --- a/test/lib/rich_text_test.rb +++ b/test/lib/rich_text_test.rb @@ -352,6 +352,55 @@ class RichTextTest < ActiveSupport::TestCase end end + def test_text_to_html_linkify_recognize_path + with_settings(:linkify => { :detection_rules => [{ :patterns => ["@(?\\w+)"], :path_template => "user/\\k" }] }) do + r = RichText.new("text", "foo @example bar") + assert_html r do + assert_dom "a", :count => 1, :text => "http://test.host/user/example" do + assert_dom "> @href", "http://test.host/user/example" + assert_dom "> @rel", "nofollow noopener noreferrer" + end + end + end + end + + def test_text_to_html_linkify_recognize_path_no_partial_match + with_settings(:linkify => { :detection_rules => [{ :patterns => ["@(?\\w+)"], :path_template => "user/\\k" }] }) do + r = RichText.new("text", "foo example@example.com bar") + assert_html r do + assert_select "a", 0 + end + end + end + + def test_text_to_html_linkify_recognize_wiki_path + with_settings(:linkify => { :detection_rules => [{ :patterns => ["(?[^\"?#<>/\\s]+)=(?[^\"?#<>\\s]+)"], :path_template => "Tag:\\k=\\k", :host => "http://example.wiki" }] }) do + r = RichText.new("text", "foo surface=metal bar") + assert_html r do + assert_dom "a", :count => 1, :text => "http://example.wiki/Tag:surface=metal" do + assert_dom "> @href", "http://example.wiki/Tag:surface=metal" + assert_dom "> @rel", "nofollow noopener noreferrer" + end + end + end + with_settings(:linkify => { :detection_rules => [{ :patterns => ["(?[^\"?#<>/\\s]+)=\\*?"], :path_template => "Key:\\k", :host => "http://example.wiki" }] }) do + r = RichText.new("text", "foo surface=* bar") + assert_html r do + assert_dom "a", :count => 1, :text => "http://example.wiki/Key:surface" do + assert_dom "> @href", "http://example.wiki/Key:surface" + assert_dom "> @rel", "nofollow noopener noreferrer" + end + end + end + end + + def test_text_to_html_linkify_no_year_misinterpretation + r = RichText.new("text", "We thought there was no way 2020 could be worse than 2019. We were wrong. Please note 2025 is the first square year since OSM started. In that year, some osmlab repos switched from node 22 to bun 1.3.") + assert_html r do + assert_select "a", 0 + end + end + def test_text_to_html_email r = RichText.new("text", "foo example@example.com bar") assert_html r do -- 2.39.5