]> git.openstreetmap.org Git - rails.git/blob - test/lib/rich_text_test.rb
Make linkify detect shortened paths
[rails.git] / test / lib / rich_text_test.rb
1 # frozen_string_literal: true
2
3 require "test_helper"
4
5 class RichTextTest < ActiveSupport::TestCase
6   include Rails::Dom::Testing::Assertions::SelectorAssertions
7
8   def test_html_to_html
9     r = RichText.new("html", "foo http://example.com/ bar")
10     assert_html r do
11       assert_select "a", 1
12       assert_select "a[href='http://example.com/']", 1
13       assert_select "a[rel='nofollow noopener noreferrer']", 1
14     end
15
16     r = RichText.new("html", "foo <a href='http://example.com/'>bar</a> baz")
17     assert_html r do
18       assert_select "a", 1
19       assert_select "a[href='http://example.com/']", 1
20       assert_select "a[rel='nofollow noopener noreferrer']", 1
21     end
22
23     r = RichText.new("html", "foo <a rel='junk me trash' href='http://example.com/'>bar</a> baz")
24     assert_html r do
25       assert_select "a", 1
26       assert_select "a[href='http://example.com/']", 1
27       assert_select "a[rel='me nofollow noopener noreferrer']", 1
28     end
29
30     r = RichText.new("html", "foo example@example.com bar")
31     assert_html r do
32       assert_select "a", 0
33     end
34
35     r = RichText.new("html", "foo <a href='mailto:example@example.com'>bar</a> baz")
36     assert_html r do
37       assert_select "a", 1
38       assert_select "a[href='mailto:example@example.com']", 1
39       assert_select "a[rel='nofollow noopener noreferrer']", 1
40     end
41
42     r = RichText.new("html", "foo <div>bar</div> baz")
43     assert_html r do
44       assert_select "div", false
45       assert_select "p", /^foo *bar *baz$/
46     end
47
48     r = RichText.new("html", "foo <script>bar = 1;</script> baz")
49     assert_html r do
50       assert_select "script", false
51       assert_select "p", /^foo *baz$/
52     end
53
54     r = RichText.new("html", "foo <style>div { display: none; }</style> baz")
55     assert_html r do
56       assert_select "style", false
57       assert_select "p", /^foo *baz$/
58     end
59
60     r = RichText.new("html", "<table><tr><td>column</td></tr></table>")
61     assert_html r do
62       assert_select "table[class='table table-sm w-auto']"
63     end
64
65     r = RichText.new("html", "<p class='btn btn-warning'>Click Me</p>")
66     assert_html r do
67       assert_select "p[class='btn btn-warning']", false
68       assert_select "p", /^Click Me$/
69     end
70
71     r = RichText.new("html", "<p style='color:red'>Danger</p>")
72     assert_html r do
73       assert_select "p[style='color:red']", false
74       assert_select "p", /^Danger$/
75     end
76   end
77
78   def test_html_to_text
79     r = RichText.new("html", "foo <a href='http://example.com/'>bar</a> baz")
80     assert_equal "foo <a href='http://example.com/'>bar</a> baz", r.to_text
81   end
82
83   def test_markdown_to_html
84     r = RichText.new("markdown", "foo http://example.com/ bar")
85     assert_html r do
86       assert_select "a", 1
87       assert_select "a[href='http://example.com/']", 1
88       assert_select "a[rel='nofollow noopener noreferrer']", 1
89     end
90
91     r = RichText.new("markdown", "foo [bar](http://example.com/) baz")
92     assert_html r do
93       assert_select "a", 1
94       assert_select "a[href='http://example.com/']", 1
95       assert_select "a[rel='nofollow noopener noreferrer']", 1
96     end
97
98     r = RichText.new("markdown", "foo <a rel='junk me trash' href='http://example.com/'>bar</a>) baz")
99     assert_html r do
100       assert_select "a", 1
101       assert_select "a[href='http://example.com/']", 1
102       assert_select "a[rel='me nofollow noopener noreferrer']", 1
103     end
104
105     r = RichText.new("markdown", "foo example@example.com bar")
106     assert_html r do
107       assert_select "a", 1
108       assert_select "a[href='mailto:example@example.com']", 1
109       assert_select "a[rel='nofollow noopener noreferrer']", 1
110     end
111
112     r = RichText.new("markdown", "foo [bar](mailto:example@example.com) bar")
113     assert_html r do
114       assert_select "a", 1
115       assert_select "a[href='mailto:example@example.com']", 1
116       assert_select "a[rel='nofollow noopener noreferrer']", 1
117     end
118
119     r = RichText.new("markdown", "foo ![bar](http://example.com/example.png) bar")
120     assert_html r do
121       assert_select "img", 1
122       assert_select "img[alt='bar']", 1
123       assert_select "img[src='http://example.com/example.png']", 1
124     end
125
126     r = RichText.new("markdown", "# foo bar baz")
127     assert_html r do
128       assert_select "h1", "foo bar baz"
129     end
130
131     r = RichText.new("markdown", "## foo bar baz")
132     assert_html r do
133       assert_select "h2", "foo bar baz"
134     end
135
136     r = RichText.new("markdown", "### foo bar baz")
137     assert_html r do
138       assert_select "h3", "foo bar baz"
139     end
140
141     r = RichText.new("markdown", "* foo bar baz")
142     assert_html r do
143       assert_select "ul" do
144         assert_select "li", "foo bar baz"
145       end
146     end
147
148     r = RichText.new("markdown", "1. foo bar baz")
149     assert_html r do
150       assert_select "ol" do
151         assert_select "li", "foo bar baz"
152       end
153     end
154
155     r = RichText.new("markdown", "foo *bar* _baz_ qux")
156     assert_html r do
157       assert_select "em", "bar"
158       assert_select "em", "baz"
159     end
160
161     r = RichText.new("markdown", "foo **bar** __baz__ qux")
162     assert_html r do
163       assert_select "strong", "bar"
164       assert_select "strong", "baz"
165     end
166
167     r = RichText.new("markdown", "foo `bar` baz")
168     assert_html r do
169       assert_select "code", "bar"
170     end
171
172     r = RichText.new("markdown", "    foo bar baz")
173     assert_html r do
174       assert_select "pre", /^\s*foo bar baz\s*$/
175     end
176
177     r = RichText.new("markdown", "|column|column")
178     assert_html r do
179       assert_select "table[class='table table-sm w-auto']"
180     end
181
182     r = RichText.new("markdown", "Click Me\n{:.btn.btn-warning}")
183     assert_html r do
184       assert_select "p[class='btn btn-warning']", false
185       assert_select "p", /^Click Me$/
186     end
187
188     r = RichText.new("markdown", "<p style='color:red'>Danger</p>")
189     assert_html r do
190       assert_select "p[style='color:red']", false
191       assert_select "p", /^Danger$/
192     end
193   end
194
195   def test_markdown_table_alignment
196     # Ensure that kramdown table alignment styles are converted to bootstrap classes
197     markdown_table = <<~MARKDOWN
198       | foo  | bar |
199       |:----:|----:|
200       |center|right|
201     MARKDOWN
202     r = RichText.new("markdown", markdown_table)
203     assert_html r do
204       assert_select "td[style='text-align:center']", false
205       assert_select "td[class='text-center']", true
206       assert_select "td[style='text-align:right']", false
207       assert_select "td[class='text-end']", true
208     end
209   end
210
211   def test_markdown_to_text
212     r = RichText.new("markdown", "foo [bar](http://example.com/) baz")
213     assert_equal "foo [bar](http://example.com/) baz", r.to_text
214   end
215
216   def test_text_to_html_linkify
217     with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => "repl.example.com") do
218       r = RichText.new("text", "foo http://example.com/ bar")
219       assert_html r do
220         assert_dom "a", :count => 1, :text => "http://example.com/" do
221           assert_dom "> @href", "http://example.com/"
222           assert_dom "> @rel", "nofollow noopener noreferrer"
223         end
224       end
225     end
226   end
227
228   def test_text_to_html_linkify_replace
229     with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => "repl.example.com") do
230       r = RichText.new("text", "foo https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12 bar")
231       assert_html r do
232         assert_dom "a", :count => 1, :text => "repl.example.com/some/path?query=te<st&limit=20>10#result12" do
233           assert_dom "> @href", "https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
234           assert_dom "> @rel", "nofollow noopener noreferrer"
235         end
236       end
237     end
238   end
239
240   def test_text_to_html_linkify_recognize
241     with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => "repl.example.com") do
242       r = RichText.new("text", "foo repl.example.com/some/path?query=te<st&limit=20>10#result12 bar")
243       assert_html r do
244         assert_dom "a", :count => 1, :text => "repl.example.com/some/path?query=te<st&limit=20>10#result12" do
245           assert_dom "> @href", "http://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
246           assert_dom "> @rel", "nofollow noopener noreferrer"
247         end
248       end
249     end
250   end
251
252   def test_text_to_html_linkify_replace_other_scheme
253     with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => "repl.example.com") do
254       r = RichText.new("text", "foo ftp://replace-me.example.com/some/path?query=te<st&limit=20>10#result12 bar")
255       assert_html r do
256         assert_dom "a", :count => 1, :text => "ftp://replace-me.example.com/some/path?query=te<st&limit=20>10#result12" do
257           assert_dom "> @href", "ftp://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
258           assert_dom "> @rel", "nofollow noopener noreferrer"
259         end
260       end
261     end
262   end
263
264   def test_text_to_html_linkify_replace_undefined
265     with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => nil) do
266       r = RichText.new("text", "foo https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12 bar")
267       assert_html r do
268         assert_dom "a", :count => 1, :text => "https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12" do
269           assert_dom "> @href", "https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
270           assert_dom "> @rel", "nofollow noopener noreferrer"
271         end
272       end
273     end
274   end
275
276   def test_text_to_html_linkify_wiki_replace_prefix
277     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
278                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
279       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
280       assert_html r do
281         assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do
282           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
283           assert_dom "> @rel", "nofollow noopener noreferrer"
284         end
285       end
286     end
287   end
288
289   def test_text_to_html_linkify_wiki_replace_prefix_undefined
290     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
291                   :linkify_wiki_optional_path_prefix => nil) do
292       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
293       assert_html r do
294         assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/Tag:surface%3Dmetal" do
295           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
296           assert_dom "> @rel", "nofollow noopener noreferrer"
297         end
298       end
299     end
300   end
301
302   def test_text_to_html_linkify_wiki_replace_undefined_prefix
303     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => nil,
304                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
305       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
306       assert_html r do
307         assert_dom "a", :count => 1, :text => "https://replace-me-wiki.example.com/Tag:surface%3Dmetal" do
308           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
309           assert_dom "> @rel", "nofollow noopener noreferrer"
310         end
311       end
312     end
313   end
314
315   def test_text_to_html_linkify_wiki_replace_prefix_no_match
316     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
317                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
318       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/w bar")
319       assert_html r do
320         assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/w" do
321           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/w"
322           assert_dom "> @rel", "nofollow noopener noreferrer"
323         end
324       end
325     end
326   end
327
328   def test_text_to_html_linkify_recognize_wiki
329     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
330                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
331       r = RichText.new("text", "foo wiki.example.com/Tag:surface%3Dmetal bar")
332       assert_html r do
333         assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do
334           assert_dom "> @href", "http://replace-me-wiki.example.com/Tag:surface%3Dmetal"
335           assert_dom "> @rel", "nofollow noopener noreferrer"
336         end
337       end
338     end
339   end
340
341   def test_text_to_html_linkify_idempotent
342     with_settings(:linkify_hosts => ["test.host"], :linkify_hosts_replacement => "test.host") do
343       t0 = "foo https://test.host/way/123456789 bar"
344
345       r1 = RichText.new("text", t0)
346       t1 = Nokogiri::HTML.fragment(r1.to_html).text
347
348       r2 = RichText.new("text", t1)
349       t2 = Nokogiri::HTML.fragment(r2.to_html).text
350
351       assert_equal t1, t2
352     end
353   end
354
355   def test_text_to_html_linkify_recognize_path
356     with_settings(:linkify => { :detection_rules => [{ :patterns => ["@(?<username>\\w+)"], :path_template => "user/\\k<username>" }] }) do
357       r = RichText.new("text", "foo @example bar")
358       assert_html r do
359         assert_dom "a", :count => 1, :text => "http://test.host/user/example" do
360           assert_dom "> @href", "http://test.host/user/example"
361           assert_dom "> @rel", "nofollow noopener noreferrer"
362         end
363       end
364     end
365   end
366
367   def test_text_to_html_linkify_recognize_path_no_partial_match
368     with_settings(:linkify => { :detection_rules => [{ :patterns => ["@(?<username>\\w+)"], :path_template => "user/\\k<username>" }] }) do
369       r = RichText.new("text", "foo example@example.com bar")
370       assert_html r do
371         assert_select "a", 0
372       end
373     end
374   end
375
376   def test_text_to_html_linkify_recognize_wiki_path
377     with_settings(:linkify => { :detection_rules => [{ :patterns => ["(?<key>[^\"?#<>/\\s]+)=(?<value>[^\"?#<>\\s]+)"], :path_template => "Tag:\\k<key>=\\k<value>", :host => "http://example.wiki" }] }) do
378       r = RichText.new("text", "foo surface=metal bar")
379       assert_html r do
380         assert_dom "a", :count => 1, :text => "http://example.wiki/Tag:surface=metal" do
381           assert_dom "> @href", "http://example.wiki/Tag:surface=metal"
382           assert_dom "> @rel", "nofollow noopener noreferrer"
383         end
384       end
385     end
386     with_settings(:linkify => { :detection_rules => [{ :patterns => ["(?<key>[^\"?#<>/\\s]+)=\\*?"], :path_template => "Key:\\k<key>", :host => "http://example.wiki" }] }) do
387       r = RichText.new("text", "foo surface=* bar")
388       assert_html r do
389         assert_dom "a", :count => 1, :text => "http://example.wiki/Key:surface" do
390           assert_dom "> @href", "http://example.wiki/Key:surface"
391           assert_dom "> @rel", "nofollow noopener noreferrer"
392         end
393       end
394     end
395   end
396
397   def test_text_to_html_linkify_no_year_misinterpretation
398     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.")
399     assert_html r do
400       assert_select "a", 0
401     end
402   end
403
404   def test_text_to_html_email
405     r = RichText.new("text", "foo example@example.com bar")
406     assert_html r do
407       assert_select "a", 0
408     end
409   end
410
411   def test_text_to_html_escape
412     r = RichText.new("text", "foo < bar & baz > qux")
413     assert_html r do
414       assert_select "p", "foo < bar & baz > qux"
415     end
416   end
417
418   def test_text_to_text
419     r = RichText.new("text", "foo http://example.com/ bar")
420     assert_equal "foo http://example.com/ bar", r.to_text
421   end
422
423   def test_text_no_opengraph_properties
424     r = RichText.new("text", "foo https://example.com/ bar")
425     assert_nil r.image
426     assert_nil r.image_alt
427     assert_nil r.description
428   end
429
430   def test_html_no_opengraph_properties
431     r = RichText.new("html", "foo <a href='https://example.com/'>bar</a> baz")
432     assert_nil r.image
433     assert_nil r.image_alt
434     assert_nil r.description
435   end
436
437   def test_markdown_no_image
438     r = RichText.new("markdown", "foo [bar](https://example.com/) baz")
439     assert_nil r.image
440     assert_nil r.image_alt
441   end
442
443   def test_markdown_image
444     r = RichText.new("markdown", "foo ![bar](https://example.com/image.jpg) baz")
445     assert_equal "https://example.com/image.jpg", r.image
446     assert_equal "bar", r.image_alt
447   end
448
449   def test_markdown_first_image
450     r = RichText.new("markdown", "foo ![bar1](https://example.com/image1.jpg) baz\nfoo ![bar2](https://example.com/image2.jpg) baz")
451     assert_equal "https://example.com/image1.jpg", r.image
452     assert_equal "bar1", r.image_alt
453   end
454
455   def test_markdown_image_with_empty_src
456     r = RichText.new("markdown", "![invalid]()")
457     assert_nil r.image
458     assert_nil r.image_alt
459   end
460
461   def test_markdown_skip_image_with_empty_src
462     r = RichText.new("markdown", "![invalid]() ![valid](https://example.com/valid.gif)")
463     assert_equal "https://example.com/valid.gif", r.image
464     assert_equal "valid", r.image_alt
465   end
466
467   def test_markdown_html_image
468     r = RichText.new("markdown", "<img src='https://example.com/img_element.png' alt='alt text here'>")
469     assert_equal "https://example.com/img_element.png", r.image
470     assert_equal "alt text here", r.image_alt
471   end
472
473   def test_markdown_html_image_without_alt
474     r = RichText.new("markdown", "<img src='https://example.com/img_element.png'>")
475     assert_equal "https://example.com/img_element.png", r.image
476     assert_nil r.image_alt
477   end
478
479   def test_markdown_html_image_with_empty_src
480     r = RichText.new("markdown", "<img src='' alt='forgot src'>")
481     assert_nil r.image
482     assert_nil r.image_alt
483   end
484
485   def test_markdown_skip_html_image_with_empty_src
486     r = RichText.new("markdown", "<img src='' alt='forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
487     assert_equal "https://example.com/next_img_element.png", r.image
488     assert_equal "have src", r.image_alt
489   end
490
491   def test_markdown_html_image_without_src
492     r = RichText.new("markdown", "<img alt='totally forgot src'>")
493     assert_nil r.image
494     assert_nil r.image_alt
495   end
496
497   def test_markdown_skip_html_image_without_src
498     r = RichText.new("markdown", "<img alt='totally forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
499     assert_equal "https://example.com/next_img_element.png", r.image
500     assert_equal "have src", r.image_alt
501   end
502
503   def test_markdown_no_description
504     r = RichText.new("markdown", "#Nope")
505     assert_nil r.description
506   end
507
508   def test_markdown_description
509     r = RichText.new("markdown", "This is an article about something.")
510     assert_equal "This is an article about something.", r.description
511   end
512
513   def test_markdown_description_after_heading
514     r = RichText.new("markdown", "#Heading\n\nHere starts the text.")
515     assert_equal "Here starts the text.", r.description
516   end
517
518   def test_markdown_description_after_image
519     r = RichText.new("markdown", "![bar](https://example.com/image.jpg)\n\nThis is below the image.")
520     assert_equal "This is below the image.", r.description
521   end
522
523   def test_markdown_description_only_first_paragraph
524     r = RichText.new("markdown", "This thing.\n\nMaybe also that thing.")
525     assert_equal "This thing.", r.description
526   end
527
528   def test_markdown_description_elements
529     r = RichText.new("markdown", "*Something* **important** [here](https://example.com/).")
530     assert_equal "Something important here.", r.description
531   end
532
533   def test_markdown_html_description
534     r = RichText.new("markdown", "<p>Can use HTML tags.</p>")
535     assert_equal "Can use HTML tags.", r.description
536   end
537
538   def test_markdown_description_max_length
539     m = RichText::DESCRIPTION_MAX_LENGTH
540     o = 3 # "...".length
541
542     r = RichText.new("markdown", "x" * m)
543     assert_equal "x" * m, r.description
544
545     r = RichText.new("markdown", "y" * (m + 1))
546     assert_equal "#{'y' * (m - o)}...", r.description
547
548     r = RichText.new("markdown", "*zzzzzzzzz*z" * ((m + 1) / 10.0).ceil)
549     assert_equal "#{'z' * (m - o)}...", r.description
550   end
551
552   def test_markdown_description_word_break_threshold_length
553     m = RichText::DESCRIPTION_MAX_LENGTH
554     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
555     o = 3 # "...".length
556
557     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}")
558     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}", r.description
559
560     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1))}")
561     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 4)}...", r.description
562
563     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}")
564     assert_equal "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}", r.description
565
566     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o))}")
567     assert_equal "#{'x' * (t - o)}...", r.description
568   end
569
570   def test_markdown_description_word_break_multiple_spaces
571     m = RichText::DESCRIPTION_MAX_LENGTH
572     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
573     o = 3 # "...".length
574
575     r = RichText.new("markdown", "#{'x' * (t - o)}  #{'y' * (m - (t - o - 1))}")
576     assert_equal "#{'x' * (t - o)}...", r.description
577   end
578
579   private
580
581   def assert_html(richtext, &block)
582     html = richtext.to_html
583     assert_predicate html, :html_safe?
584     root = Nokogiri::HTML::DocumentFragment.parse(html)
585     assert_select root, "*" do
586       yield block
587     end
588   end
589 end