]> git.openstreetmap.org Git - rails.git/commitdiff
Update bundle master
authorTom Hughes <tom@compton.nu>
Tue, 1 Apr 2025 12:25:25 +0000 (13:25 +0100)
committerTom Hughes <tom@compton.nu>
Tue, 1 Apr 2025 12:25:25 +0000 (13:25 +0100)
19 files changed:
Gemfile.lock
app/assets/javascripts/diary_entry.js
app/assets/javascripts/id.js
app/assets/javascripts/index/directions-endpoint.js
app/assets/javascripts/index/directions.js
app/assets/javascripts/index/home.js
app/assets/javascripts/index/layers/notes.js
app/assets/javascripts/index/new_note.js
app/assets/javascripts/index/note.js
app/assets/javascripts/index/search.js
app/assets/javascripts/leaflet.map.js
app/assets/javascripts/osm.js.erb
app/assets/javascripts/user.js
app/views/dashboards/_contact.html.erb
app/views/dashboards/show.html.erb
app/views/site/id.html.erb
config/settings.yml
lib/rich_text.rb
test/lib/rich_text_test.rb

index 925eebe3c68b72f76aac88eb618a413ed30bb811..eee8346e97cfe59a8f2b6118a03d027421caa546 100644 (file)
@@ -96,8 +96,8 @@ GEM
     autoprefixer-rails (10.4.19.0)
       execjs (~> 2)
     aws-eventstream (1.3.2)
-    aws-partitions (1.1073.0)
-    aws-sdk-core (3.221.0)
+    aws-partitions (1.1078.0)
+    aws-sdk-core (3.222.1)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
@@ -107,7 +107,7 @@ GEM
     aws-sdk-kms (1.99.0)
       aws-sdk-core (~> 3, >= 3.216.0)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.182.0)
+    aws-sdk-s3 (1.183.0)
       aws-sdk-core (~> 3, >= 3.216.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
@@ -365,7 +365,7 @@ GEM
     listen (3.9.0)
       rb-fsevent (~> 0.10, >= 0.10.3)
       rb-inotify (~> 0.9, >= 0.9.10)
-    logger (1.6.6)
+    logger (1.7.0)
     logstasher (2.1.5)
       activesupport (>= 5.2)
       request_store
@@ -407,7 +407,7 @@ GEM
     net-smtp (0.5.1)
       net-protocol
     nio4r (2.7.4)
-    nokogiri (1.18.6)
+    nokogiri (1.18.7)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
     oauth (1.1.0)
@@ -469,7 +469,7 @@ GEM
       iniparse (~> 1.4)
       rexml (>= 3.3.9)
     parallel (1.26.3)
-    parser (3.3.7.2)
+    parser (3.3.7.4)
       ast (~> 2.4.1)
       racc
     pg (1.5.9)
@@ -550,7 +550,7 @@ GEM
     rb-inotify (0.11.1)
       ffi (~> 1.0)
     rchardet (1.9.0)
-    rdoc (6.13.0)
+    rdoc (6.13.1)
       psych (>= 4.0.0)
     regexp_parser (2.10.0)
     reline (0.6.0)
@@ -563,7 +563,7 @@ GEM
     rouge (4.5.1)
     rtlcss (0.2.1)
       mini_racer (>= 0.6.3)
-    rubocop (1.74.0)
+    rubocop (1.75.1)
       json (~> 2.3)
       language_server-protocol (~> 3.17.0.2)
       lint_roller (~> 1.1.0)
@@ -571,7 +571,7 @@ GEM
       parser (>= 3.3.0.2)
       rainbow (>= 2.2.2, < 4.0)
       regexp_parser (>= 2.9.3, < 3.0)
-      rubocop-ast (>= 1.38.0, < 2.0)
+      rubocop-ast (>= 1.43.0, < 2.0)
       ruby-progressbar (~> 1.7)
       unicode-display_width (>= 2.4.0, < 4.0)
     rubocop-ast (1.43.0)
@@ -583,19 +583,19 @@ GEM
     rubocop-factory_bot (2.27.1)
       lint_roller (~> 1.1)
       rubocop (~> 1.72, >= 1.72.1)
-    rubocop-minitest (0.37.1)
+    rubocop-minitest (0.38.0)
       lint_roller (~> 1.1)
-      rubocop (>= 1.72.1, < 2.0)
+      rubocop (>= 1.75.0, < 2.0)
       rubocop-ast (>= 1.38.0, < 2.0)
-    rubocop-performance (1.24.0)
+    rubocop-performance (1.25.0)
       lint_roller (~> 1.1)
-      rubocop (>= 1.72.1, < 2.0)
+      rubocop (>= 1.75.0, < 2.0)
       rubocop-ast (>= 1.38.0, < 2.0)
-    rubocop-rails (2.30.3)
+    rubocop-rails (2.31.0)
       activesupport (>= 4.2.0)
       lint_roller (~> 1.1)
       rack (>= 1.1)
-      rubocop (>= 1.72.1, < 2.0)
+      rubocop (>= 1.75.0, < 2.0)
       rubocop-ast (>= 1.38.0, < 2.0)
     rubocop-rake (0.7.1)
       lint_roller (~> 1.1)
index bfc3fc0128a70470324f68117f7789d1316d8584..e434460941529f39642e29af7658fe8d0bcc235d 100644 (file)
@@ -11,7 +11,7 @@ $(function () {
       map.removeLayer(marker);
     }
 
-    marker = L.marker(e.latlng, { icon: OSM.getUserIcon() }).addTo(map)
+    marker = L.marker(e.latlng, { icon: OSM.getMarker({}) }).addTo(map)
       .bindPopup(OSM.i18n.t("diary_entries.edit.marker_text"));
   }
 
@@ -36,7 +36,7 @@ $(function () {
     map.setView(centre, params.zoom);
 
     if ($("#latitude").val() && $("#longitude").val()) {
-      marker = L.marker(centre, { icon: OSM.getUserIcon() }).addTo(map)
+      marker = L.marker(centre, { icon: OSM.getMarker({}) }).addTo(map)
         .bindPopup(OSM.i18n.t("diary_entries.edit.marker_text"));
     }
 
index d60d4b82d0d0b709335d77aed1d32e7cca19ea10..b5c8a41539559122fdb3454bb1b3d34822c6e363 100644 (file)
@@ -76,5 +76,10 @@ document.addEventListener("DOMContentLoaded", function () {
       const data = parent.OSM.mapParams();
       goToLocation(data);
     });
+
+    const projectTitle = parent.document.title;
+    new MutationObserver(() =>
+      parent.document.title = [document.title, projectTitle].filter(t => t).join(" | ")
+    ).observe(document.querySelector("title"), { childList: true, subtree: true, characterData: true });
   }
 });
index a4b3a928bbbfa85a68eb5a7647bcaaaa8490efa8..97b841f1443c6dceae67daeb7845a167708159e2 100644 (file)
@@ -1,15 +1,8 @@
-OSM.DirectionsEndpoint = function Endpoint(map, input, iconUrl, dragCallback, changeCallback) {
+OSM.DirectionsEndpoint = function Endpoint(map, input, marker, dragCallback, changeCallback) {
   const endpoint = {};
 
   endpoint.marker = L.marker([0, 0], {
-    icon: L.icon({
-      iconUrl: iconUrl,
-      iconSize: [25, 41],
-      iconAnchor: [12, 41],
-      popupAnchor: [1, -34],
-      shadowUrl: OSM.MARKER_SHADOW,
-      shadowSize: [41, 41]
-    }),
+    icon: OSM.getMarker(marker),
     draggable: true,
     autoPan: true
   });
index 05793345327d42714b6d23f308135f83cff528b2..57def249ac68d1d58800a0a8798e6a986b7e3c56 100644 (file)
@@ -33,8 +33,8 @@ OSM.Directions = function (map) {
   };
 
   const endpoints = [
-    OSM.DirectionsEndpoint(map, $("input[name='route_from']"), OSM.MARKER_GREEN, endpointDragCallback, endpointChangeCallback),
-    OSM.DirectionsEndpoint(map, $("input[name='route_to']"), OSM.MARKER_RED, endpointDragCallback, endpointChangeCallback)
+    OSM.DirectionsEndpoint(map, $("input[name='route_from']"), { icon: "MARKER_GREEN" }, endpointDragCallback, endpointChangeCallback),
+    OSM.DirectionsEndpoint(map, $("input[name='route_to']"), { icon: "MARKER_RED" }, endpointDragCallback, endpointChangeCallback)
   ];
 
   let downloadURL = null;
index 597b68eff4a7216e81cb1c39eacf101cbb1cb8bd..1cc644cf2bc64b32ad514e1e66036ba94db6f2b6 100644 (file)
@@ -17,7 +17,7 @@ OSM.Home = function (map) {
         map.setView(OSM.home, 15, { reset: true });
       });
       marker = L.marker(OSM.home, {
-        icon: OSM.getUserIcon(),
+        icon: OSM.getMarker({}),
         title: OSM.i18n.t("javascripts.home.marker_title")
       }).addTo(map);
     } else {
index 104f6f2f22cbf40d226bdfe1c411cb108c6a354f..00f1fb4e93be9c561edc1b7e25566b68590551ac 100644 (file)
@@ -3,24 +3,6 @@ OSM.initializeNotesLayer = function (map) {
   const noteLayer = map.noteLayer;
   let notes = {};
 
-  const noteIcons = {
-    "new": L.icon({
-      iconUrl: OSM.NEW_NOTE_MARKER,
-      iconSize: [25, 40],
-      iconAnchor: [12, 40]
-    }),
-    "open": L.icon({
-      iconUrl: OSM.OPEN_NOTE_MARKER,
-      iconSize: [25, 40],
-      iconAnchor: [12, 40]
-    }),
-    "closed": L.icon({
-      iconUrl: OSM.CLOSED_NOTE_MARKER,
-      iconSize: [25, 40],
-      iconAnchor: [12, 40]
-    })
-  };
-
   noteLayer.on("add", () => {
     loadNotes();
     map.on("moveend", loadNotes);
@@ -41,7 +23,7 @@ OSM.initializeNotesLayer = function (map) {
   function updateMarker(old_marker, feature) {
     let marker = old_marker;
     if (marker) {
-      marker.setIcon(noteIcons[feature.properties.status]);
+      marker.setIcon(OSM.getMarker({ icon: `${feature.properties.status}_NOTE_MARKER`, shadow: false, height: 40 }));
     } else {
       let title;
       const description = feature.properties.comments[0];
@@ -51,7 +33,7 @@ OSM.initializeNotesLayer = function (map) {
       }
 
       marker = L.marker(feature.geometry.coordinates.reverse(), {
-        icon: noteIcons[feature.properties.status],
+        icon: OSM.getMarker({ icon: `${feature.properties.status}_NOTE_MARKER`, shadow: false, height: 40 }),
         title,
         opacity: 0.8,
         interactive: true
index b1457d10b0233a745b5e9c4a2736637bcfe63834..1b409846f870848fb48d9871061d71e53ce6b99f 100644 (file)
@@ -6,24 +6,6 @@ OSM.NewNote = function (map) {
   let newNoteMarker,
       halo;
 
-  const noteIcons = {
-    "new": L.icon({
-      iconUrl: OSM.NEW_NOTE_MARKER,
-      iconSize: [25, 40],
-      iconAnchor: [12, 40]
-    }),
-    "open": L.icon({
-      iconUrl: OSM.OPEN_NOTE_MARKER,
-      iconSize: [25, 40],
-      iconAnchor: [12, 40]
-    }),
-    "closed": L.icon({
-      iconUrl: OSM.CLOSED_NOTE_MARKER,
-      iconSize: [25, 40],
-      iconAnchor: [12, 40]
-    })
-  };
-
   addNoteButton.on("click", function (e) {
     e.preventDefault();
     e.stopPropagation();
@@ -49,7 +31,7 @@ OSM.NewNote = function (map) {
 
   function addCreatedNoteMarker(feature) {
     const marker = L.marker(feature.geometry.coordinates.reverse(), {
-      icon: noteIcons[feature.properties.status],
+      icon: OSM.getMarker({ icon: `${feature.properties.status}_NOTE_MARKER`, shadow: false, height: 40 }),
       opacity: 0.9,
       interactive: true
     });
@@ -79,7 +61,7 @@ OSM.NewNote = function (map) {
     if (newNoteMarker) map.removeLayer(newNoteMarker);
 
     newNoteMarker = L.marker(latlng, {
-      icon: noteIcons.new,
+      icon: OSM.getMarker({ icon: "NEW_NOTE_MARKER", shadow: false, height: 40 }),
       opacity: 0.9,
       draggable: true
     });
index f0b7dae273d9b02a0ac812d36125b4dd2bbe5fd5..a77735c95966890a66eab7c0b3e52ff8995d04bf 100644 (file)
@@ -2,24 +2,6 @@ OSM.Note = function (map) {
   const content = $("#sidebar_content"),
         page = {};
 
-  const noteIcons = {
-    "new": L.icon({
-      iconUrl: OSM.NEW_NOTE_MARKER,
-      iconSize: [25, 40],
-      iconAnchor: [12, 40]
-    }),
-    "open": L.icon({
-      iconUrl: OSM.OPEN_NOTE_MARKER,
-      iconSize: [25, 40],
-      iconAnchor: [12, 40]
-    }),
-    "closed": L.icon({
-      iconUrl: OSM.CLOSED_NOTE_MARKER,
-      iconSize: [25, 40],
-      iconAnchor: [12, 40]
-    })
-  };
-
   page.pushstate = page.popstate = function (path, id) {
     OSM.loadSidebarContent(path, function () {
       const data = $(".details").data();
@@ -87,7 +69,7 @@ OSM.Note = function (map) {
         type: "note",
         id: parseInt(id, 10),
         latLng: L.latLng(data.coordinates.split(",")),
-        icon: noteIcons[data.status]
+        icon: OSM.getMarker({ icon: `${data.status}_NOTE_MARKER`, shadow: false, height: 40 })
       }, function () {
         if (!hashParams.center && !skipMoveToNote) {
           const latLng = L.latLng(data.coordinates.split(","));
index b3ef3ceb3c13de3d4a7dbad7b4d8f7a313719000..4c1213320a70672bbe7f2c72e44ecee242ddf857 100644 (file)
@@ -63,7 +63,7 @@ OSM.Search = function (map) {
     if (!marker) {
       const data = $(this).find("a.set_position").data();
 
-      marker = L.marker([data.lat, data.lon], { icon: OSM.getUserIcon() });
+      marker = L.marker([data.lat, data.lon], { icon: OSM.getMarker({}) });
 
       $(this).data("marker", marker);
     }
index ac31f79416034e1f894e7cefbedcfdd678606b0c..b301711a2679758deea332603d34acf13acc363d 100644 (file)
@@ -391,13 +391,17 @@ OSM.isDarkMap = function () {
   return window.matchMedia("(prefers-color-scheme: dark)").matches;
 };
 
-OSM.getUserIcon = function (url) {
-  return L.icon({
-    iconUrl: url || OSM.MARKER_RED,
-    iconSize: [25, 41],
-    iconAnchor: [12, 41],
-    popupAnchor: [1, -34],
-    shadowUrl: OSM.MARKER_SHADOW,
-    shadowSize: [41, 41]
-  });
+OSM.getMarker = function ({ icon = "MARKER_RED", shadow = true, height = 41 }) {
+  const options = {
+    iconUrl: OSM[icon.toUpperCase()] || OSM.MARKER_RED,
+    iconSize: [25, height],
+    iconAnchor: [12, height],
+    popupAnchor: [1, -34]
+  };
+  if (shadow) {
+    options.shadowUrl = OSM.MARKER_SHADOW;
+    options.shadowSize = [41, 41];
+    options.shadowAnchor = [12, 41];
+  }
+  return L.icon(options);
 };
index 4f0bd8f777c608986d65c4e1d7ab1589dad8d07c..6762ce6b08abc9d4ea2e4194fad9b12e0c2fdd91 100644 (file)
@@ -30,6 +30,7 @@ OSM = {
   LAYER_DEFINITIONS: <%= MapLayers::full_definitions("config/layers.yml").to_json %>,
   LAYERS_WITH_MAP_KEY: <%= YAML.load_file(Rails.root.join("config/key.yml")).keys.to_json %>,
 
+  MARKER_BLUE: <%= image_path("marker-blue.png").to_json %>,
   MARKER_GREEN: <%= image_path("marker-green.png").to_json %>,
   MARKER_RED: <%= image_path("marker-red.png").to_json %>,
 
index 77a71097b1d41d899372c043e5341ba8f1ce2e27..a2516984e0a84da9ccab4ca92c525f403f9623ce 100644 (file)
@@ -51,7 +51,7 @@ $(function () {
 
     if ($("#map").hasClass("set_location")) {
       marker = L.marker([0, 0], {
-        icon: OSM.getUserIcon(),
+        icon: OSM.getMarker({}),
         keyboard: false,
         interactive: false
       });
@@ -124,7 +124,7 @@ $(function () {
       $("[data-user]").each(function () {
         const user = $(this).data("user");
         if (user.lon && user.lat) {
-          L.marker([user.lat, user.lon], { icon: OSM.getUserIcon(user.icon) }).addTo(map)
+          L.marker([user.lat, user.lon], { icon: OSM.getMarker({ icon: user.icon }) }).addTo(map)
             .bindPopup(user.description, { minWidth: 200 });
         }
       });
index 0dda0b35481f98d146f563974105a6120224c86e..fc99ada79b9aaa6050e7465294f5d969d0b71fe6 100644 (file)
@@ -1,7 +1,7 @@
 <% user_data = {
      :lon => contact.home_lon,
      :lat => contact.home_lat,
-     :icon => image_path(type == "following" ? "marker-blue.png" : "marker-green.png"),
+     :icon => type == "following" ? "MARKER_BLUE" : "MARKER_GREEN",
      :description => render(:partial => "popup", :object => contact, :locals => { :type => type })
    } %>
 <%= tag.div :class => "clearfix row", :data => { :user => user_data } do %>
index e110ad531f1f11ad66b4464e24a39a3e52f7ead9..894edd2404217c37374737594a9bf957e76b84df 100644 (file)
@@ -15,7 +15,7 @@
       <% user_data = {
            :lon => current_user.home_lon,
            :lat => current_user.home_lat,
-           :icon => image_path("marker-red.png"),
+           :icon => "MARKER_RED",
            :description => render(:partial => "popup", :object => current_user, :locals => { :type => "your location" })
          } %>
       <%= tag.div "", :id => "map", :class => "content_map border border-secondary-subtle rounded z-0", :data => { :user => user_data } %>
index 64cb4fd58e84be24042079846fb0364899454b72..52f2de937cff4e4d0d71f2d843d9b3ef6eaef289 100644 (file)
@@ -6,6 +6,7 @@
   <!--[if !IE || gte IE 9]><!-->
   <%= javascript_include_tag "id" %>
   <!-- <![endif]-->
+  <title></title>
 </head>
 <body>
 <% data = {}
index 775df2c115da4e5d0a94055919d6f5f490dd7614..416fb1931f1b354c31f8392bbe553581c4494758 100644 (file)
@@ -144,6 +144,8 @@ linkify_hosts_replacement: "osm.org"
 linkify_wiki_hosts: ["wiki.openstreetmap.org", "wiki.osm.org", "wiki.openstreetmap.com", "wiki.openstreetmaps.org", "osm.wiki", "www.osm.wiki", "wiki.osm.wiki"]
 # Shorter host to replace wiki hosts
 linkify_wiki_hosts_replacement: "osm.wiki"
+# Regexp for wiki prefix that can be removed
+linkify_wiki_optional_path_prefix: "^/wiki(?=/[A-Z])"
 # External authentication credentials
 #google_auth_id: ""
 #google_auth_secret: ""
index 1147cbc6031041beaa88ac85b6636c3259abafb6..79249730707cbfa75dfda488c614dba2a01d0b1b 100644 (file)
@@ -77,15 +77,25 @@ module RichText
       link_attr = 'rel="nofollow noopener noreferrer"'
       Rinku.auto_link(ERB::Util.html_escape(text), mode, link_attr) do |url|
         url = shorten_host(url, Settings.linkify_hosts, Settings.linkify_hosts_replacement)
-        shorten_host(url, Settings.linkify_wiki_hosts, Settings.linkify_wiki_hosts_replacement)
+        shorten_host(url, Settings.linkify_wiki_hosts, Settings.linkify_wiki_hosts_replacement) do |path|
+          path.sub(Regexp.new(Settings.linkify_wiki_optional_path_prefix || ""), "")
+        end
       end.html_safe
     end
 
     private
 
     def shorten_host(url, hosts, hosts_replacement)
-      %r{^https?://([^/]*)(.*)$}.match(url) do |m|
-        "#{hosts_replacement}#{m[2]}" if hosts_replacement && hosts&.include?(m[1])
+      %r{^(https?://([^/]*))(.*)$}.match(url) do |m|
+        scheme_host, host, path = m.captures
+        if hosts&.include?(host)
+          path = yield(path) if block_given?
+          if hosts_replacement
+            "#{hosts_replacement}#{path}"
+          else
+            "#{scheme_host}#{path}"
+          end
+        end || url
       end || url
     end
   end
index c04a4b3ae5e12c78c856db30f42b21d54a6f2458..4ec85bcf9b991968a72893634b8c61637f94a29b 100644 (file)
@@ -269,8 +269,22 @@ class RichTextTest < ActiveSupport::TestCase
     end
   end
 
-  def test_text_to_html_linkify_wiki_replace
-    with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com") do
+  def test_text_to_html_linkify_wiki_replace_prefix
+    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 https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
+      assert_html r do
+        assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do
+          assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
+          assert_dom "> @rel", "nofollow noopener noreferrer"
+        end
+      end
+    end
+  end
+
+  def test_text_to_html_linkify_wiki_replace_prefix_undefined
+    with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
+                  :linkify_wiki_optional_path_prefix => nil) do
       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
       assert_html r do
         assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/Tag:surface%3Dmetal" do
@@ -281,6 +295,32 @@ class RichTextTest < ActiveSupport::TestCase
     end
   end
 
+  def test_text_to_html_linkify_wiki_replace_undefined_prefix
+    with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => nil,
+                  :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
+      r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
+      assert_html r do
+        assert_dom "a", :count => 1, :text => "https://replace-me-wiki.example.com/Tag:surface%3Dmetal" do
+          assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
+          assert_dom "> @rel", "nofollow noopener noreferrer"
+        end
+      end
+    end
+  end
+
+  def test_text_to_html_linkify_wiki_replace_prefix_no_match
+    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 https://replace-me-wiki.example.com/wiki/w bar")
+      assert_html r do
+        assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/w" do
+          assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/w"
+          assert_dom "> @rel", "nofollow noopener noreferrer"
+        end
+      end
+    end
+  end
+
   def test_text_to_html_email
     r = RichText.new("text", "foo example@example.com bar")
     assert_html r do