]> git.openstreetmap.org Git - rails.git/blob - test/lib/rich_text_test.rb
Replace marker shadow image with vector graphic
[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 => { :normalisation_rules => [{ :hosts => ["replace-me.example.com"], :host_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 => { :normalisation_rules => [{ :hosts => ["replace-me.example.com"], :host_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 => { :normalisation_rules => [{ :hosts => ["replace-me.example.com"], :host_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 => { :normalisation_rules => [{ :hosts => ["replace-me.example.com"], :host_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 => { :normalisation_rules => [{ :hosts => ["replace-me.example.com"] }] }) 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 => { :normalisation_rules => [{ :hosts => ["replace-me-wiki.example.com"], :host_replacement => "wiki.example.com", :optional_path_prefix => "^/wiki(?=/[A-Z])" }] }) do
278       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
279       assert_html r do
280         assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do
281           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
282           assert_dom "> @rel", "nofollow noopener noreferrer"
283         end
284       end
285     end
286   end
287
288   def test_text_to_html_linkify_wiki_replace_prefix_undefined
289     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me-wiki.example.com"], :host_replacement => "wiki.example.com" }] }) do
290       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
291       assert_html r do
292         assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/Tag:surface%3Dmetal" do
293           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
294           assert_dom "> @rel", "nofollow noopener noreferrer"
295         end
296       end
297     end
298   end
299
300   def test_text_to_html_linkify_wiki_replace_undefined_prefix
301     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me-wiki.example.com"], :optional_path_prefix => "^/wiki(?=/[A-Z])" }] }) do
302       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
303       assert_html r do
304         assert_dom "a", :count => 1, :text => "https://replace-me-wiki.example.com/Tag:surface%3Dmetal" do
305           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
306           assert_dom "> @rel", "nofollow noopener noreferrer"
307         end
308       end
309     end
310   end
311
312   def test_text_to_html_linkify_wiki_replace_prefix_no_match
313     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me-wiki.example.com"], :host_replacement => "wiki.example.com", :optional_path_prefix => "^/wiki(?=/[A-Z])" }] }) do
314       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/w bar")
315       assert_html r do
316         assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/w" do
317           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/w"
318           assert_dom "> @rel", "nofollow noopener noreferrer"
319         end
320       end
321     end
322   end
323
324   def test_text_to_html_linkify_recognize_wiki
325     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["replace-me-wiki.example.com"], :host_replacement => "wiki.example.com", :optional_path_prefix => "^/wiki(?=/[A-Z])" }] }) do
326       r = RichText.new("text", "foo wiki.example.com/Tag:surface%3Dmetal bar")
327       assert_html r do
328         assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do
329           assert_dom "> @href", "http://replace-me-wiki.example.com/Tag:surface%3Dmetal"
330           assert_dom "> @rel", "nofollow noopener noreferrer"
331         end
332       end
333     end
334   end
335
336   def test_text_to_html_linkify_idempotent
337     with_settings(:linkify => { :normalisation_rules => [{ :hosts => ["test.host"], :host_replacement => "test.host" }] }) do
338       t0 = "foo https://test.host/way/123456789 bar"
339
340       r1 = RichText.new("text", t0)
341       t1 = Nokogiri::HTML.fragment(r1.to_html).text
342
343       r2 = RichText.new("text", t1)
344       t2 = Nokogiri::HTML.fragment(r2.to_html).text
345
346       assert_equal t1, t2
347     end
348   end
349
350   def test_text_to_html_linkify_recognize_path
351     with_settings(:linkify => { :detection_rules => [{ :patterns => ["@(?<username>\\w+)"], :path_template => "user/\\k<username>" }] }) do
352       r = RichText.new("text", "foo @example bar")
353       assert_html r do
354         assert_dom "a", :count => 1, :text => "http://test.host/user/example" do
355           assert_dom "> @href", "http://test.host/user/example"
356           assert_dom "> @rel", "nofollow noopener noreferrer"
357         end
358       end
359     end
360   end
361
362   def test_text_to_html_linkify_recognize_path_no_partial_match
363     with_settings(:linkify => { :detection_rules => [{ :patterns => ["@(?<username>\\w+)"], :path_template => "user/\\k<username>" }] }) do
364       r = RichText.new("text", "foo example@example.com bar")
365       assert_html r do
366         assert_select "a", 0
367       end
368     end
369   end
370
371   def test_text_to_html_linkify_recognize_wiki_path
372     with_settings(:linkify => { :detection_rules => [{ :patterns => ["(?<key>[^\"?#<>/\\s]+)=(?<value>[^\"?#<>\\s]+)"], :path_template => "Tag:\\k<key>=\\k<value>", :host => "http://example.wiki" }] }) do
373       r = RichText.new("text", "foo surface=metal bar")
374       assert_html r do
375         assert_dom "a", :count => 1, :text => "http://example.wiki/Tag:surface=metal" do
376           assert_dom "> @href", "http://example.wiki/Tag:surface=metal"
377           assert_dom "> @rel", "nofollow noopener noreferrer"
378         end
379       end
380     end
381     with_settings(:linkify => { :detection_rules => [{ :patterns => ["(?<key>[^\"?#<>/\\s]+)=\\*?"], :path_template => "Key:\\k<key>", :host => "http://example.wiki" }] }) do
382       r = RichText.new("text", "foo surface=* bar")
383       assert_html r do
384         assert_dom "a", :count => 1, :text => "http://example.wiki/Key:surface" do
385           assert_dom "> @href", "http://example.wiki/Key:surface"
386           assert_dom "> @rel", "nofollow noopener noreferrer"
387         end
388       end
389     end
390   end
391
392   def test_text_to_html_linkify_openstreetmap_links
393     with_settings(:server_url => "www.openstreetmap.org", :server_protocol => "https") do
394       cases = {
395         "https://www.openstreetmap.org/note/4655490" =>
396           ["note/4655490", "https://www.openstreetmap.org/note/4655490"],
397
398         "https://www.openstreetmap.org/changeset/163353772" =>
399           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
400
401         "https://www.openstreetmap.org/way/1249366504" =>
402           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
403
404         "https://www.openstreetmap.org/way/1249366504/history" =>
405           ["way/1249366504/history", "https://www.openstreetmap.org/way/1249366504/history"],
406
407         "https://www.openstreetmap.org/way/1249366504/history/2" =>
408           ["way/1249366504/history/2", "https://www.openstreetmap.org/way/1249366504/history/2"],
409
410         "https://www.openstreetmap.org/node/12639964186" =>
411           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
412
413         "https://www.openstreetmap.org/relation/7876483" =>
414           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
415
416         "https://www.openstreetmap.org/user/aharvey" =>
417           ["@aharvey", "https://www.openstreetmap.org/user/aharvey"],
418
419         "https://wiki.openstreetmap.org/wiki/Key:boundary" =>
420           ["boundary=*", "https://wiki.openstreetmap.org/wiki/Key:boundary"],
421
422         "https://wiki.openstreetmap.org/wiki/Tag:boundary=place" =>
423           ["boundary=place", "https://wiki.openstreetmap.org/wiki/Tag:boundary=place"],
424
425         "boundary=*" =>
426           ["boundary=*", "https://wiki.openstreetmap.org/wiki/Key:boundary"],
427
428         "boundary=place" =>
429           ["boundary=place", "https://wiki.openstreetmap.org/wiki/Tag:boundary=place"],
430
431         "@aharvey" =>
432           ["@aharvey", "https://www.openstreetmap.org/user/aharvey"],
433
434         "node/12639964186" =>
435           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
436
437         "node 12639964186" =>
438           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
439
440         "n12639964186" =>
441           ["node/12639964186", "https://www.openstreetmap.org/node/12639964186"],
442
443         "way/1249366504" =>
444           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
445
446         "way 1249366504" =>
447           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
448
449         "w1249366504" =>
450           ["way/1249366504", "https://www.openstreetmap.org/way/1249366504"],
451
452         "relation/7876483" =>
453           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
454
455         "relation 7876483" =>
456           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
457
458         "r7876483" =>
459           ["relation/7876483", "https://www.openstreetmap.org/relation/7876483"],
460
461         "changeset/163353772" =>
462           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
463
464         "changeset 163353772" =>
465           ["changeset/163353772", "https://www.openstreetmap.org/changeset/163353772"],
466
467         "note/4655490" =>
468           ["note/4655490", "https://www.openstreetmap.org/note/4655490"],
469
470         "note 4655490" =>
471           ["note/4655490", "https://www.openstreetmap.org/note/4655490"]
472       }
473
474       cases.each do |input, (expected_text, expected_href)|
475         r = RichText.new("text", input)
476         assert_html r do
477           assert_dom "a[href='#{expected_href}']", :count => 1, :text => expected_text
478         end
479       end
480     end
481   end
482
483   def test_text_to_html_linkify_no_year_misinterpretation
484     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.")
485     assert_html r do
486       assert_select "a", 0
487     end
488   end
489
490   def test_text_to_html_dont_linkify_too_easily
491     # We used to allow this, but removed it because it
492     # caused issues in some languages.
493     # Eg: example below is Polish for "Reverted in 12345"
494     r = RichText.new("text", "Wycofane w 12345")
495
496     assert_html r do
497       assert_select "a", 0
498     end
499   end
500
501   def test_deactivated_linkify_expansion_in_markdown
502     t0 = "foo `surface=metal` bar"
503
504     r1 = RichText.new("markdown", t0)
505     t1 = Nokogiri::HTML.fragment(r1.to_html).text
506
507     assert_equal t0.delete("`"), t1.strip
508   end
509
510   def test_text_to_html_linkify_trims_punctuation
511     r = RichText.new("text", "foo `surface=metal) bar inscription=🙂 baz")
512     assert_html r do
513       assert_dom "a", :count => 1
514       assert_dom "a[href$='Tag:surface=metal']", :text => "surface=metal"
515     end
516   end
517
518   def test_text_to_html_linkify_recognizes_non_standard_wiki_pages
519     r_paren = RichText.new("text", "foo source=Isle_of_Man_Government_1:25000_map_(2007) bar")
520     assert_html r_paren do
521       assert_dom "a[href*='Tag:source']", :text => "source=Isle_of_Man_Government_1:25000_map_(2007)"
522     end
523     r_latin_ext = RichText.new("text", "foo cuisine=açaí bar")
524     assert_html r_latin_ext do
525       assert_dom "a[href*='Tag:cuisine']", :text => "cuisine=açaí"
526     end
527     r_cyrillic = RichText.new("text", "foo name=Продукты bar")
528     assert_html r_cyrillic do
529       assert_dom "a[href*='Tag:name']", :text => "name=Продукты"
530     end
531     r_cjk = RichText.new("text", "foo shop=園芸店 bar")
532     assert_html r_cjk do
533       assert_dom "a[href*='Tag:shop']", :text => "shop=園芸店"
534     end
535   end
536
537   def test_text_to_html_email
538     r = RichText.new("text", "foo example@example.com bar")
539     assert_html r do
540       assert_select "a", 0
541     end
542   end
543
544   def test_text_to_html_escape
545     r = RichText.new("text", "foo < bar & baz > qux")
546     assert_html r do
547       assert_select "p", "foo < bar & baz > qux"
548     end
549   end
550
551   def test_text_to_text
552     r = RichText.new("text", "foo http://example.com/ bar")
553     assert_equal "foo http://example.com/ bar", r.to_text
554   end
555
556   def test_text_no_opengraph_properties
557     r = RichText.new("text", "foo https://example.com/ bar")
558     assert_nil r.image
559     assert_nil r.image_alt
560     assert_nil r.description
561   end
562
563   def test_html_no_opengraph_properties
564     r = RichText.new("html", "foo <a href='https://example.com/'>bar</a> baz")
565     assert_nil r.image
566     assert_nil r.image_alt
567     assert_nil r.description
568   end
569
570   def test_markdown_no_image
571     r = RichText.new("markdown", "foo [bar](https://example.com/) baz")
572     assert_nil r.image
573     assert_nil r.image_alt
574   end
575
576   def test_markdown_image
577     r = RichText.new("markdown", "foo ![bar](https://example.com/image.jpg) baz")
578     assert_equal "https://example.com/image.jpg", r.image
579     assert_equal "bar", r.image_alt
580   end
581
582   def test_markdown_first_image
583     r = RichText.new("markdown", "foo ![bar1](https://example.com/image1.jpg) baz\nfoo ![bar2](https://example.com/image2.jpg) baz")
584     assert_equal "https://example.com/image1.jpg", r.image
585     assert_equal "bar1", r.image_alt
586   end
587
588   def test_markdown_image_with_empty_src
589     r = RichText.new("markdown", "![invalid]()")
590     assert_nil r.image
591     assert_nil r.image_alt
592   end
593
594   def test_markdown_skip_image_with_empty_src
595     r = RichText.new("markdown", "![invalid]() ![valid](https://example.com/valid.gif)")
596     assert_equal "https://example.com/valid.gif", r.image
597     assert_equal "valid", r.image_alt
598   end
599
600   def test_markdown_html_image
601     r = RichText.new("markdown", "<img src='https://example.com/img_element.png' alt='alt text here'>")
602     assert_equal "https://example.com/img_element.png", r.image
603     assert_equal "alt text here", r.image_alt
604   end
605
606   def test_markdown_html_image_without_alt
607     r = RichText.new("markdown", "<img src='https://example.com/img_element.png'>")
608     assert_equal "https://example.com/img_element.png", r.image
609     assert_nil r.image_alt
610   end
611
612   def test_markdown_html_image_with_empty_src
613     r = RichText.new("markdown", "<img src='' alt='forgot src'>")
614     assert_nil r.image
615     assert_nil r.image_alt
616   end
617
618   def test_markdown_skip_html_image_with_empty_src
619     r = RichText.new("markdown", "<img src='' alt='forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
620     assert_equal "https://example.com/next_img_element.png", r.image
621     assert_equal "have src", r.image_alt
622   end
623
624   def test_markdown_html_image_without_src
625     r = RichText.new("markdown", "<img alt='totally forgot src'>")
626     assert_nil r.image
627     assert_nil r.image_alt
628   end
629
630   def test_markdown_skip_html_image_without_src
631     r = RichText.new("markdown", "<img alt='totally forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
632     assert_equal "https://example.com/next_img_element.png", r.image
633     assert_equal "have src", r.image_alt
634   end
635
636   def test_markdown_no_description
637     r = RichText.new("markdown", "#Nope")
638     assert_nil r.description
639   end
640
641   def test_markdown_description
642     r = RichText.new("markdown", "This is an article about something.")
643     assert_equal "This is an article about something.", r.description
644   end
645
646   def test_markdown_description_after_heading
647     r = RichText.new("markdown", "#Heading\n\nHere starts the text.")
648     assert_equal "Here starts the text.", r.description
649   end
650
651   def test_markdown_description_after_image
652     r = RichText.new("markdown", "![bar](https://example.com/image.jpg)\n\nThis is below the image.")
653     assert_equal "This is below the image.", r.description
654   end
655
656   def test_markdown_description_only_first_paragraph
657     r = RichText.new("markdown", "This thing.\n\nMaybe also that thing.")
658     assert_equal "This thing.", r.description
659   end
660
661   def test_markdown_description_elements
662     r = RichText.new("markdown", "*Something* **important** [here](https://example.com/).")
663     assert_equal "Something important here.", r.description
664   end
665
666   def test_markdown_html_description
667     r = RichText.new("markdown", "<p>Can use HTML tags.</p>")
668     assert_equal "Can use HTML tags.", r.description
669   end
670
671   def test_markdown_description_max_length
672     m = RichText::DESCRIPTION_MAX_LENGTH
673     o = 3 # "...".length
674
675     r = RichText.new("markdown", "x" * m)
676     assert_equal "x" * m, r.description
677
678     r = RichText.new("markdown", "y" * (m + 1))
679     assert_equal "#{'y' * (m - o)}...", r.description
680
681     r = RichText.new("markdown", "*zzzzzzzzz*z" * ((m + 1) / 10.0).ceil)
682     assert_equal "#{'z' * (m - o)}...", r.description
683   end
684
685   def test_markdown_description_word_break_threshold_length
686     m = RichText::DESCRIPTION_MAX_LENGTH
687     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
688     o = 3 # "...".length
689
690     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}")
691     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}", r.description
692
693     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1))}")
694     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 4)}...", r.description
695
696     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}")
697     assert_equal "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}", r.description
698
699     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o))}")
700     assert_equal "#{'x' * (t - o)}...", r.description
701   end
702
703   def test_markdown_description_word_break_multiple_spaces
704     m = RichText::DESCRIPTION_MAX_LENGTH
705     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
706     o = 3 # "...".length
707
708     r = RichText.new("markdown", "#{'x' * (t - o)}  #{'y' * (m - (t - o - 1))}")
709     assert_equal "#{'x' * (t - o)}...", r.description
710   end
711
712   private
713
714   def assert_html(richtext, &block)
715     html = richtext.to_html
716     assert_predicate html, :html_safe?
717     root = Nokogiri::HTML::DocumentFragment.parse(html)
718     assert_select root, "*" do
719       yield block
720     end
721   end
722 end