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_html_spam_score
 
  84     r = RichText.new("html", "foo <a href='http://example.com/'>bar</a> baz")
 
  85     assert_equal 55, r.spam_score.round
 
  88   def test_markdown_to_html
 
  89     r = RichText.new("markdown", "foo http://example.com/ bar")
 
  92       assert_select "a[href='http://example.com/']", 1
 
  93       assert_select "a[rel='nofollow noopener noreferrer']", 1
 
  96     r = RichText.new("markdown", "foo [bar](http://example.com/) baz")
 
  99       assert_select "a[href='http://example.com/']", 1
 
 100       assert_select "a[rel='nofollow noopener noreferrer']", 1
 
 103     r = RichText.new("markdown", "foo <a rel='junk me trash' href='http://example.com/'>bar</a>) baz")
 
 106       assert_select "a[href='http://example.com/']", 1
 
 107       assert_select "a[rel='me nofollow noopener noreferrer']", 1
 
 110     r = RichText.new("markdown", "foo example@example.com bar")
 
 113       assert_select "a[href='mailto:example@example.com']", 1
 
 114       assert_select "a[rel='nofollow noopener noreferrer']", 1
 
 117     r = RichText.new("markdown", "foo [bar](mailto:example@example.com) bar")
 
 120       assert_select "a[href='mailto:example@example.com']", 1
 
 121       assert_select "a[rel='nofollow noopener noreferrer']", 1
 
 124     r = RichText.new("markdown", "foo  bar")
 
 126       assert_select "img", 1
 
 127       assert_select "img[alt='bar']", 1
 
 128       assert_select "img[src='http://example.com/example.png']", 1
 
 131     r = RichText.new("markdown", "# foo bar baz")
 
 133       assert_select "h1", "foo bar baz"
 
 136     r = RichText.new("markdown", "## foo bar baz")
 
 138       assert_select "h2", "foo bar baz"
 
 141     r = RichText.new("markdown", "### foo bar baz")
 
 143       assert_select "h3", "foo bar baz"
 
 146     r = RichText.new("markdown", "* foo bar baz")
 
 148       assert_select "ul" do
 
 149         assert_select "li", "foo bar baz"
 
 153     r = RichText.new("markdown", "1. foo bar baz")
 
 155       assert_select "ol" do
 
 156         assert_select "li", "foo bar baz"
 
 160     r = RichText.new("markdown", "foo *bar* _baz_ qux")
 
 162       assert_select "em", "bar"
 
 163       assert_select "em", "baz"
 
 166     r = RichText.new("markdown", "foo **bar** __baz__ qux")
 
 168       assert_select "strong", "bar"
 
 169       assert_select "strong", "baz"
 
 172     r = RichText.new("markdown", "foo `bar` baz")
 
 174       assert_select "code", "bar"
 
 177     r = RichText.new("markdown", "    foo bar baz")
 
 179       assert_select "pre", /^\s*foo bar baz\s*$/
 
 182     r = RichText.new("markdown", "|column|column")
 
 184       assert_select "table[class='table table-sm w-auto']"
 
 187     r = RichText.new("markdown", "Click Me\n{:.btn.btn-warning}")
 
 189       assert_select "p[class='btn btn-warning']", false
 
 190       assert_select "p", /^Click Me$/
 
 193     r = RichText.new("markdown", "<p style='color:red'>Danger</p>")
 
 195       assert_select "p[style='color:red']", false
 
 196       assert_select "p", /^Danger$/
 
 200   def test_markdown_table_alignment
 
 201     # Ensure that kramdown table alignment styles are converted to bootstrap classes
 
 202     markdown_table = <<~MARKDOWN
 
 207     r = RichText.new("markdown", markdown_table)
 
 209       assert_select "td[style='text-align:center']", false
 
 210       assert_select "td[class='text-center']", true
 
 211       assert_select "td[style='text-align:right']", false
 
 212       assert_select "td[class='text-end']", true
 
 216   def test_markdown_to_text
 
 217     r = RichText.new("markdown", "foo [bar](http://example.com/) baz")
 
 218     assert_equal "foo [bar](http://example.com/) baz", r.to_text
 
 221   def test_markdown_spam_score
 
 222     r = RichText.new("markdown", "foo [bar](http://example.com/) baz")
 
 223     assert_equal 50, r.spam_score.round
 
 226   def test_text_to_html_linkify
 
 227     with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => "repl.example.com") do
 
 228       r = RichText.new("text", "foo http://example.com/ bar")
 
 230         assert_dom "a", :count => 1, :text => "http://example.com/" do
 
 231           assert_dom "> @href", "http://example.com/"
 
 232           assert_dom "> @rel", "nofollow noopener noreferrer"
 
 238   def test_text_to_html_linkify_replace
 
 239     with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => "repl.example.com") do
 
 240       r = RichText.new("text", "foo https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12 bar")
 
 242         assert_dom "a", :count => 1, :text => "repl.example.com/some/path?query=te<st&limit=20>10#result12" do
 
 243           assert_dom "> @href", "https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
 
 244           assert_dom "> @rel", "nofollow noopener noreferrer"
 
 250   def test_text_to_html_linkify_replace_other_scheme
 
 251     with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => "repl.example.com") do
 
 252       r = RichText.new("text", "foo ftp://replace-me.example.com/some/path?query=te<st&limit=20>10#result12 bar")
 
 254         assert_dom "a", :count => 1, :text => "ftp://replace-me.example.com/some/path?query=te<st&limit=20>10#result12" do
 
 255           assert_dom "> @href", "ftp://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
 
 256           assert_dom "> @rel", "nofollow noopener noreferrer"
 
 262   def test_text_to_html_linkify_replace_undefined
 
 263     with_settings(:linkify_hosts => ["replace-me.example.com"], :linkify_hosts_replacement => nil) do
 
 264       r = RichText.new("text", "foo https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12 bar")
 
 266         assert_dom "a", :count => 1, :text => "https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12" do
 
 267           assert_dom "> @href", "https://replace-me.example.com/some/path?query=te<st&limit=20>10#result12"
 
 268           assert_dom "> @rel", "nofollow noopener noreferrer"
 
 274   def test_text_to_html_linkify_wiki_replace_prefix
 
 275     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
 
 276                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
 
 277       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
 
 279         assert_dom "a", :count => 1, :text => "wiki.example.com/Tag:surface%3Dmetal" do
 
 280           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
 
 281           assert_dom "> @rel", "nofollow noopener noreferrer"
 
 287   def test_text_to_html_linkify_wiki_replace_prefix_undefined
 
 288     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
 
 289                   :linkify_wiki_optional_path_prefix => nil) do
 
 290       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
 
 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"
 
 300   def test_text_to_html_linkify_wiki_replace_undefined_prefix
 
 301     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => nil,
 
 302                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
 
 303       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal bar")
 
 305         assert_dom "a", :count => 1, :text => "https://replace-me-wiki.example.com/Tag:surface%3Dmetal" do
 
 306           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/Tag:surface%3Dmetal"
 
 307           assert_dom "> @rel", "nofollow noopener noreferrer"
 
 313   def test_text_to_html_linkify_wiki_replace_prefix_no_match
 
 314     with_settings(:linkify_wiki_hosts => ["replace-me-wiki.example.com"], :linkify_wiki_hosts_replacement => "wiki.example.com",
 
 315                   :linkify_wiki_optional_path_prefix => "^/wiki(?=/[A-Z])") do
 
 316       r = RichText.new("text", "foo https://replace-me-wiki.example.com/wiki/w bar")
 
 318         assert_dom "a", :count => 1, :text => "wiki.example.com/wiki/w" do
 
 319           assert_dom "> @href", "https://replace-me-wiki.example.com/wiki/w"
 
 320           assert_dom "> @rel", "nofollow noopener noreferrer"
 
 326   def test_text_to_html_email
 
 327     r = RichText.new("text", "foo example@example.com bar")
 
 333   def test_text_to_html_escape
 
 334     r = RichText.new("text", "foo < bar & baz > qux")
 
 336       assert_select "p", "foo < bar & baz > qux"
 
 340   def test_text_to_text
 
 341     r = RichText.new("text", "foo http://example.com/ bar")
 
 342     assert_equal "foo http://example.com/ bar", r.to_text
 
 345   def test_text_spam_score
 
 346     r = RichText.new("text", "foo http://example.com/ bar")
 
 347     assert_equal 141, r.spam_score.round
 
 350   def test_text_no_opengraph_properties
 
 351     r = RichText.new("text", "foo https://example.com/ bar")
 
 353     assert_nil r.image_alt
 
 354     assert_nil r.description
 
 357   def test_html_no_opengraph_properties
 
 358     r = RichText.new("html", "foo <a href='https://example.com/'>bar</a> baz")
 
 360     assert_nil r.image_alt
 
 361     assert_nil r.description
 
 364   def test_markdown_no_image
 
 365     r = RichText.new("markdown", "foo [bar](https://example.com/) baz")
 
 367     assert_nil r.image_alt
 
 370   def test_markdown_image
 
 371     r = RichText.new("markdown", "foo  baz")
 
 372     assert_equal "https://example.com/image.jpg", r.image
 
 373     assert_equal "bar", r.image_alt
 
 376   def test_markdown_first_image
 
 377     r = RichText.new("markdown", "foo  baz\nfoo  baz")
 
 378     assert_equal "https://example.com/image1.jpg", r.image
 
 379     assert_equal "bar1", r.image_alt
 
 382   def test_markdown_image_with_empty_src
 
 383     r = RichText.new("markdown", "![invalid]()")
 
 385     assert_nil r.image_alt
 
 388   def test_markdown_skip_image_with_empty_src
 
 389     r = RichText.new("markdown", "![invalid]() ")
 
 390     assert_equal "https://example.com/valid.gif", r.image
 
 391     assert_equal "valid", r.image_alt
 
 394   def test_markdown_html_image
 
 395     r = RichText.new("markdown", "<img src='https://example.com/img_element.png' alt='alt text here'>")
 
 396     assert_equal "https://example.com/img_element.png", r.image
 
 397     assert_equal "alt text here", r.image_alt
 
 400   def test_markdown_html_image_without_alt
 
 401     r = RichText.new("markdown", "<img src='https://example.com/img_element.png'>")
 
 402     assert_equal "https://example.com/img_element.png", r.image
 
 403     assert_nil r.image_alt
 
 406   def test_markdown_html_image_with_empty_src
 
 407     r = RichText.new("markdown", "<img src='' alt='forgot src'>")
 
 409     assert_nil r.image_alt
 
 412   def test_markdown_skip_html_image_with_empty_src
 
 413     r = RichText.new("markdown", "<img src='' alt='forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
 
 414     assert_equal "https://example.com/next_img_element.png", r.image
 
 415     assert_equal "have src", r.image_alt
 
 418   def test_markdown_html_image_without_src
 
 419     r = RichText.new("markdown", "<img alt='totally forgot src'>")
 
 421     assert_nil r.image_alt
 
 424   def test_markdown_skip_html_image_without_src
 
 425     r = RichText.new("markdown", "<img alt='totally forgot src'> <img src='https://example.com/next_img_element.png' alt='have src'>")
 
 426     assert_equal "https://example.com/next_img_element.png", r.image
 
 427     assert_equal "have src", r.image_alt
 
 430   def test_markdown_no_description
 
 431     r = RichText.new("markdown", "#Nope")
 
 432     assert_nil r.description
 
 435   def test_markdown_description
 
 436     r = RichText.new("markdown", "This is an article about something.")
 
 437     assert_equal "This is an article about something.", r.description
 
 440   def test_markdown_description_after_heading
 
 441     r = RichText.new("markdown", "#Heading\n\nHere starts the text.")
 
 442     assert_equal "Here starts the text.", r.description
 
 445   def test_markdown_description_after_image
 
 446     r = RichText.new("markdown", "\n\nThis is below the image.")
 
 447     assert_equal "This is below the image.", r.description
 
 450   def test_markdown_description_only_first_paragraph
 
 451     r = RichText.new("markdown", "This thing.\n\nMaybe also that thing.")
 
 452     assert_equal "This thing.", r.description
 
 455   def test_markdown_description_elements
 
 456     r = RichText.new("markdown", "*Something* **important** [here](https://example.com/).")
 
 457     assert_equal "Something important here.", r.description
 
 460   def test_markdown_html_description
 
 461     r = RichText.new("markdown", "<p>Can use HTML tags.</p>")
 
 462     assert_equal "Can use HTML tags.", r.description
 
 465   def test_markdown_description_max_length
 
 466     m = RichText::DESCRIPTION_MAX_LENGTH
 
 469     r = RichText.new("markdown", "x" * m)
 
 470     assert_equal "x" * m, r.description
 
 472     r = RichText.new("markdown", "y" * (m + 1))
 
 473     assert_equal "#{'y' * (m - o)}...", r.description
 
 475     r = RichText.new("markdown", "*zzzzzzzzz*z" * ((m + 1) / 10.0).ceil)
 
 476     assert_equal "#{'z' * (m - o)}...", r.description
 
 479   def test_markdown_description_word_break_threshold_length
 
 480     m = RichText::DESCRIPTION_MAX_LENGTH
 
 481     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
 
 484     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}")
 
 485     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 1)}", r.description
 
 487     r = RichText.new("markdown", "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1))}")
 
 488     assert_equal "#{'x' * (t - o - 1)} #{'y' * (m - (t - o - 1) - 4)}...", r.description
 
 490     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}")
 
 491     assert_equal "#{'x' * (t - o)} #{'y' * (m - (t - o) - 1)}", r.description
 
 493     r = RichText.new("markdown", "#{'x' * (t - o)} #{'y' * (m - (t - o))}")
 
 494     assert_equal "#{'x' * (t - o)}...", r.description
 
 497   def test_markdown_description_word_break_multiple_spaces
 
 498     m = RichText::DESCRIPTION_MAX_LENGTH
 
 499     t = RichText::DESCRIPTION_WORD_BREAK_THRESHOLD_LENGTH
 
 502     r = RichText.new("markdown", "#{'x' * (t - o)}  #{'y' * (m - (t - o - 1))}")
 
 503     assert_equal "#{'x' * (t - o)}...", r.description
 
 508   def assert_html(richtext, &block)
 
 509     html = richtext.to_html
 
 510     assert_predicate html, :html_safe?
 
 511     root = Nokogiri::HTML::DocumentFragment.parse(html)
 
 512     assert_select root, "*" do