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_email
356 r = RichText.new("text", "foo example@example.com bar")
362 def test_text_to_html_escape
363 r = RichText.new("text", "foo < bar & baz > qux")
365 assert_select "p", "foo < bar & baz > qux"
369 def test_text_to_text
370 r = RichText.new("text", "foo http://example.com/ bar")
371 assert_equal "foo http://example.com/ bar", r.to_text
374 def test_text_no_opengraph_properties
375 r = RichText.new("text", "foo https://example.com/ bar")
377 assert_nil r.image_alt
378 assert_nil r.description
381 def test_html_no_opengraph_properties
382 r = RichText.new("html", "foo <a href='https://example.com/'>bar</a> baz")
384 assert_nil r.image_alt
385 assert_nil r.description
388 def test_markdown_no_image
389 r = RichText.new("markdown", "foo [bar](https://example.com/) baz")
391 assert_nil r.image_alt
394 def test_markdown_image
395 r = RichText.new("markdown", "foo  baz")
396 assert_equal "https://example.com/image.jpg", r.image
397 assert_equal "bar", r.image_alt
400 def test_markdown_first_image
401 r = RichText.new("markdown", "foo  baz\nfoo  baz")
402 assert_equal "https://example.com/image1.jpg", r.image
403 assert_equal "bar1", r.image_alt
406 def test_markdown_image_with_empty_src
407 r = RichText.new("markdown", "![invalid]()")
409 assert_nil r.image_alt
412 def test_markdown_skip_image_with_empty_src
413 r = RichText.new("markdown", "![invalid]() ")
414 assert_equal "https://example.com/valid.gif", r.image
415 assert_equal "valid", r.image_alt
418 def test_markdown_html_image
419 r = RichText.new("markdown", "<img src='https://example.com/img_element.png' alt='alt text here'>")
420 assert_equal "https://example.com/img_element.png", r.image
421 assert_equal "alt text here", r.image_alt
424 def test_markdown_html_image_without_alt
425 r = RichText.new("markdown", "<img src='https://example.com/img_element.png'>")
426 assert_equal "https://example.com/img_element.png", r.image
427 assert_nil r.image_alt
430 def test_markdown_html_image_with_empty_src
431 r = RichText.new("markdown", "<img src='' alt='forgot src'>")
433 assert_nil r.image_alt
436 def test_markdown_skip_html_image_with_empty_src
437 r = RichText.new("markdown", "<img src='' alt='forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
438 assert_equal "https://example.com/next_img_element.png", r.image
439 assert_equal "have src", r.image_alt
442 def test_markdown_html_image_without_src
443 r = RichText.new("markdown", "<img alt='totally forgot src'>")
445 assert_nil r.image_alt
448 def test_markdown_skip_html_image_without_src
449 r = RichText.new("markdown", "<img alt='totally forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
450 assert_equal "https://example.com/next_img_element.png", r.image
451 assert_equal "have src", r.image_alt
454 def test_markdown_no_description
455 r = RichText.new("markdown", "#Nope")
456 assert_nil r.description
459 def test_markdown_description
460 r = RichText.new("markdown", "This is an article about something.")
461 assert_equal "This is an article about something.", r.description
464 def test_markdown_description_after_heading
465 r = RichText.new("markdown", "#Heading\n\nHere starts the text.")
466 assert_equal "Here starts the text.", r.description
469 def test_markdown_description_after_image
470 r = RichText.new("markdown", "\n\nThis is below the image.")
471 assert_equal "This is below the image.", r.description
474 def test_markdown_description_only_first_paragraph
475 r = RichText.new("markdown", "This thing.\n\nMaybe also that thing.")
476 assert_equal "This thing.", r.description
479 def test_markdown_description_elements
480 r = RichText.new("markdown", "*Something* **important** [here](https://example.com/).")
481 assert_equal "Something important here.", r.description
484 def test_markdown_html_description
485 r = RichText.new("markdown", "<p>Can use HTML tags.</p>")
486 assert_equal "Can use HTML tags.", r.description
489 def test_markdown_description_max_length
490 m = RichText::DESCRIPTION_MAX_LENGTH
493 r = RichText.new("markdown", "x" * m)
494 assert_equal "x" * m, r.description
496 r = RichText.new("markdown", "y" * (m + 1))
497 assert_equal "#{'y' * (m - o)}...", r.description
499 r = RichText.new("markdown", "*zzzzzzzzz*z" * ((m + 1) / 10.0).ceil)
500 assert_equal "#{'z' * (m - o)}...", r.description
503 def test_markdown_description_word_break_threshold_length
504 m = RichText::DESCRIPTION_MAX_LENGTH
505 t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
508 r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}")
509 assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}", r.description
511 r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1))}")
512 assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 4)}...", r.description
514 r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}")
515 assert_equal "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}", r.description
517 r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o))}")
518 assert_equal "#{'x' * (t - o)}...", r.description
521 def test_markdown_description_word_break_multiple_spaces
522 m = RichText::DESCRIPTION_MAX_LENGTH
523 t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
526 r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o - 1))}")
527 assert_equal "#{'x' * (t - o)}...", r.description
532 def assert_html(richtext, &block)
533 html = richtext.to_html
534 assert_predicate html, :html_safe?
535 root = Nokogiri::HTML::DocumentFragment.parse(html)
536 assert_select root, "*" do