1 # frozen_string_literal: true
5 class RichTextTest < ActiveSupport::TestCase
6 include Rails::Dom::Testing::Assertions::SelectorAssertions
9 r = RichText.new("html", "foo http://example.com/ bar")
12 assert_select "a[href='http://example.com/']", 1
13 assert_select "a[rel='nofollow noopener noreferrer']", 1
16 r = RichText.new("html", "foo <a href='http://example.com/'>bar</a> baz")
19 assert_select "a[href='http://example.com/']", 1
20 assert_select "a[rel='nofollow noopener noreferrer']", 1
23 r = RichText.new("html", "foo <a rel='junk me trash' href='http://example.com/'>bar</a> baz")
26 assert_select "a[href='http://example.com/']", 1
27 assert_select "a[rel='me nofollow noopener noreferrer']", 1
30 r = RichText.new("html", "foo example@example.com bar")
35 r = RichText.new("html", "foo <a href='mailto:example@example.com'>bar</a> baz")
38 assert_select "a[href='mailto:example@example.com']", 1
39 assert_select "a[rel='nofollow noopener noreferrer']", 1
42 r = RichText.new("html", "foo <div>bar</div> baz")
44 assert_select "div", false
45 assert_select "p", /^foo *bar *baz$/
48 r = RichText.new("html", "foo <script>bar = 1;</script> baz")
50 assert_select "script", false
51 assert_select "p", /^foo *baz$/
54 r = RichText.new("html", "foo <style>div { display: none; }</style> baz")
56 assert_select "style", false
57 assert_select "p", /^foo *baz$/
60 r = RichText.new("html", "<table><tr><td>column</td></tr></table>")
62 assert_select "table[class='table table-sm w-auto']"
65 r = RichText.new("html", "<p class='btn btn-warning'>Click Me</p>")
67 assert_select "p[class='btn btn-warning']", false
68 assert_select "p", /^Click Me$/
71 r = RichText.new("html", "<p style='color:red'>Danger</p>")
73 assert_select "p[style='color:red']", false
74 assert_select "p", /^Danger$/
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
83 def test_markdown_to_html
84 r = RichText.new("markdown", "foo http://example.com/ bar")
87 assert_select "a[href='http://example.com/']", 1
88 assert_select "a[rel='nofollow noopener noreferrer']", 1
91 r = RichText.new("markdown", "foo [bar](http://example.com/) baz")
94 assert_select "a[href='http://example.com/']", 1
95 assert_select "a[rel='nofollow noopener noreferrer']", 1
98 r = RichText.new("markdown", "foo <a rel='junk me trash' href='http://example.com/'>bar</a>) baz")
101 assert_select "a[href='http://example.com/']", 1
102 assert_select "a[rel='me nofollow noopener noreferrer']", 1
105 r = RichText.new("markdown", "foo example@example.com bar")
108 assert_select "a[href='mailto:example@example.com']", 1
109 assert_select "a[rel='nofollow noopener noreferrer']", 1
112 r = RichText.new("markdown", "foo [bar](mailto:example@example.com) bar")
115 assert_select "a[href='mailto:example@example.com']", 1
116 assert_select "a[rel='nofollow noopener noreferrer']", 1
119 r = RichText.new("markdown", "foo  bar")
121 assert_select "img", 1
122 assert_select "img[alt='bar']", 1
123 assert_select "img[src='http://example.com/example.png']", 1
126 r = RichText.new("markdown", "# foo bar baz")
128 assert_select "h1", "foo bar baz"
131 r = RichText.new("markdown", "## foo bar baz")
133 assert_select "h2", "foo bar baz"
136 r = RichText.new("markdown", "### foo bar baz")
138 assert_select "h3", "foo bar baz"
141 r = RichText.new("markdown", "* foo bar baz")
143 assert_select "ul" do
144 assert_select "li", "foo bar baz"
148 r = RichText.new("markdown", "1. foo bar baz")
150 assert_select "ol" do
151 assert_select "li", "foo bar baz"
155 r = RichText.new("markdown", "foo *bar* _baz_ qux")
157 assert_select "em", "bar"
158 assert_select "em", "baz"
161 r = RichText.new("markdown", "foo **bar** __baz__ qux")
163 assert_select "strong", "bar"
164 assert_select "strong", "baz"
167 r = RichText.new("markdown", "foo `bar` baz")
169 assert_select "code", "bar"
172 r = RichText.new("markdown", " foo bar baz")
174 assert_select "pre", /^\s*foo bar baz\s*$/
177 r = RichText.new("markdown", "|column|column")
179 assert_select "table[class='table table-sm w-auto']"
182 r = RichText.new("markdown", "Click Me\n{:.btn.btn-warning}")
184 assert_select "p[class='btn btn-warning']", false
185 assert_select "p", /^Click Me$/
188 r = RichText.new("markdown", "<p style='color:red'>Danger</p>")
190 assert_select "p[style='color:red']", false
191 assert_select "p", /^Danger$/
195 def test_markdown_table_alignment
196 # Ensure that kramdown table alignment styles are converted to bootstrap classes
197 markdown_table = <<~MARKDOWN
202 r = RichText.new("markdown", markdown_table)
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
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
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")
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"
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")
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"
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")
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"
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")
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"
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")
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"
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")
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"
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")
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"
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")
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"
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")
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"
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")
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"
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"
345 r1 = RichText.new("text", t0)
346 t1 = Nokogiri::HTML.fragment(r1.to_html).text
348 r2 = RichText.new("text", t1)
349 t2 = Nokogiri::HTML.fragment(r2.to_html).text
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")
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"
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")
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")
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"
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")
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"
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.")
404 def test_text_to_html_email
405 r = RichText.new("text", "foo example@example.com bar")
411 def test_text_to_html_escape
412 r = RichText.new("text", "foo < bar & baz > qux")
414 assert_select "p", "foo < bar & baz > qux"
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
423 def test_text_no_opengraph_properties
424 r = RichText.new("text", "foo https://example.com/ bar")
426 assert_nil r.image_alt
427 assert_nil r.description
430 def test_html_no_opengraph_properties
431 r = RichText.new("html", "foo <a href='https://example.com/'>bar</a> baz")
433 assert_nil r.image_alt
434 assert_nil r.description
437 def test_markdown_no_image
438 r = RichText.new("markdown", "foo [bar](https://example.com/) baz")
440 assert_nil r.image_alt
443 def test_markdown_image
444 r = RichText.new("markdown", "foo  baz")
445 assert_equal "https://example.com/image.jpg", r.image
446 assert_equal "bar", r.image_alt
449 def test_markdown_first_image
450 r = RichText.new("markdown", "foo  baz\nfoo  baz")
451 assert_equal "https://example.com/image1.jpg", r.image
452 assert_equal "bar1", r.image_alt
455 def test_markdown_image_with_empty_src
456 r = RichText.new("markdown", "![invalid]()")
458 assert_nil r.image_alt
461 def test_markdown_skip_image_with_empty_src
462 r = RichText.new("markdown", "![invalid]() ")
463 assert_equal "https://example.com/valid.gif", r.image
464 assert_equal "valid", r.image_alt
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
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
479 def test_markdown_html_image_with_empty_src
480 r = RichText.new("markdown", "<img src='' alt='forgot src'>")
482 assert_nil r.image_alt
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
491 def test_markdown_html_image_without_src
492 r = RichText.new("markdown", "<img alt='totally forgot src'>")
494 assert_nil r.image_alt
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
503 def test_markdown_no_description
504 r = RichText.new("markdown", "#Nope")
505 assert_nil r.description
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
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
518 def test_markdown_description_after_image
519 r = RichText.new("markdown", "\n\nThis is below the image.")
520 assert_equal "This is below the image.", r.description
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
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
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
538 def test_markdown_description_max_length
539 m = RichText::DESCRIPTION_MAX_LENGTH
542 r = RichText.new("markdown", "x" * m)
543 assert_equal "x" * m, r.description
545 r = RichText.new("markdown", "y" * (m + 1))
546 assert_equal "#{'y' * (m - o)}...", r.description
548 r = RichText.new("markdown", "*zzzzzzzzz*z" * ((m + 1) / 10.0).ceil)
549 assert_equal "#{'z' * (m - o)}...", r.description
552 def test_markdown_description_word_break_threshold_length
553 m = RichText::DESCRIPTION_MAX_LENGTH
554 t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
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
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
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
566 r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o))}")
567 assert_equal "#{'x' * (t - o)}...", r.description
570 def test_markdown_description_word_break_multiple_spaces
571 m = RichText::DESCRIPTION_MAX_LENGTH
572 t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
575 r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o - 1))}")
576 assert_equal "#{'x' * (t - o)}...", r.description
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