require 'rails_helper' require 'pretty_text' require 'html_normalize' describe PrettyText do def n(html) HtmlNormalize.normalize(html) end def cook(*args) n(PrettyText.cook(*args)) end let(:wrapped_image) { "
\nScreen Shot 2014-04-14 at 9.47.10 PM.png966x737 1.47 MB\n
" } let(:wrapped_image_excerpt) { } describe "Cooking" do describe "off topic quoting" do it "can correctly populate topic title" do topic = Fabricate(:topic, title: "this is a test topic :slight_smile:") expected = <
This is a test topic slight_smile

ddd

HTML expect(PrettyText.cook("[quote=\"EvilTrout, post:2, topic:#{topic.id}\"]ddd\n[/quote]", topic_id: 1)).to match_html expected end end describe "with avatar" do let(:default_avatar) { "//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png" } let(:user) { Fabricate(:user) } before do User.stubs(:default_template).returns(default_avatar) end it "produces a quote even with new lines in it" do expect(PrettyText.cook("[quote=\"#{user.username}, post:123, topic:456, full:true\"]ddd\n[/quote]")).to match_html "" end it "should produce a quote" do expect(PrettyText.cook("[quote=\"#{user.username}, post:123, topic:456, full:true\"]ddd[/quote]")).to match_html "" end it "trims spaces on quote params" do expect(PrettyText.cook("[quote=\"#{user.username}, post:555, topic: 666\"]ddd[/quote]")).to match_html "" end end describe "with letter avatar" do let(:user) { Fabricate(:user) } context "subfolder" do before do GlobalSetting.stubs(:relative_url_root).returns("/forum") Discourse.stubs(:base_uri).returns("/forum") end it "should have correct avatar url" do expect(PrettyText.cook("[quote=\"#{user.username}, post:123, topic:456, full:true\"]ddd[/quote]")).to include("/forum/letter_avatar_proxy") end end end it "should handle 3 mentions in a row" do expect(PrettyText.cook('@hello @hello @hello')).to match_html "

@hello @hello @hello

" end it "should handle group mentions with a hyphen and without" do expect(PrettyText.cook('@hello @hello-hello')).to match_html "

@hello @hello-hello

" end it "should sanitize the html" do expect(PrettyText.cook("")).to match_html "

" end it 'should allow for @mentions to have punctuation' do expect(PrettyText.cook("hello @bob's @bob,@bob; @bob\"")).to match_html( "

hello @bob's @bob,@bob; @bob\"

" ) end # see: https://github.com/sparklemotion/nokogiri/issues/1173 skip 'allows html entities correctly' do expect(PrettyText.cook("ℵ£¢")).to eq("

ℵ£¢

") end end describe "rel nofollow" do before do SiteSetting.add_rel_nofollow_to_user_content = true SiteSetting.exclude_rel_nofollow_domains = "foo.com|bar.com" end it "should inject nofollow in all user provided links" do expect(PrettyText.cook('cnn')).to match(/nofollow noopener/) end it "should not inject nofollow in all local links" do expect(PrettyText.cook("cnn") !~ /nofollow/).to eq(true) end it "should not inject nofollow in all subdomain links" do expect(PrettyText.cook("cnn") !~ /nofollow/).to eq(true) end it "should inject nofollow in all non subdomain links" do expect(PrettyText.cook("cnn")).to match(/nofollow/) end it "should not inject nofollow for foo.com" do expect(PrettyText.cook("cnn") !~ /nofollow/).to eq(true) end it "should inject nofollow for afoo.com" do expect(PrettyText.cook("cnn")).to match(/nofollow/) end it "should not inject nofollow for bar.foo.com" do expect(PrettyText.cook("cnn") !~ /nofollow/).to eq(true) end it "should not inject nofollow if omit_nofollow option is given" do expect(PrettyText.cook('cnn', omit_nofollow: true) !~ /nofollow/).to eq(true) end end describe "Excerpt" do it "sanitizes attempts to inject invalid attributes" do spinner = "",100)).to eq("[image]") end context 'alt tags' do it "should keep alt tags" do expect(PrettyText.excerpt("car", 100)).to eq("[car]") end describe 'when alt tag is empty' do it "should not keep alt tags" do expect(PrettyText.excerpt("", 100)).to eq("[#{I18n.t('excerpt_image')}]") end end end context 'title tags' do it "should keep title tags" do expect(PrettyText.excerpt("", 100)).to eq("[car]") end describe 'when title tag is empty' do it "should not keep title tags" do expect(PrettyText.excerpt("", 100)).to eq("[#{I18n.t('excerpt_image')}]") end end end it "should convert images to markdown if the option is set" do expect(PrettyText.excerpt("", 100, markdown_images: true)).to eq("![car](http://cnn.com/a.gif)") end it "should keep spoilers" do expect(PrettyText.excerpt("
", 100)).to match_html "[image]" expect(PrettyText.excerpt("spoiler", 100)).to match_html "spoiler" end it "should remove meta informations" do expect(PrettyText.excerpt(wrapped_image, 100)).to match_html "
[image]" end end it "should have an option to strip links" do expect(PrettyText.excerpt("cnn",100, strip_links: true)).to eq("cnn") end it "should preserve links" do expect(PrettyText.excerpt("cnn",100)).to match_html "cnn" end it "should deal with special keys properly" do expect(PrettyText.excerpt("
",100)).to eq("") end it "should truncate stuff properly" do expect(PrettyText.excerpt("hello world",5)).to eq("hello…") expect(PrettyText.excerpt("

hello

world

",6)).to eq("hello w…") end it "should insert a space between to Ps" do expect(PrettyText.excerpt("

a

b

",5)).to eq("a b") end it "should strip quotes" do expect(PrettyText.excerpt("boom",5)).to eq("boom") end it "should not count the surrounds of a link" do expect(PrettyText.excerpt("cnn",3)).to match_html "cnn" end it "uses an ellipsis instead of html entities if provided with the option" do expect(PrettyText.excerpt("cnn", 2, text_entities: true)).to match_html "cn..." end it "should truncate links" do expect(PrettyText.excerpt("cnn",2)).to match_html "cn…" end it "doesn't extract empty quotes as links" do expect(PrettyText.extract_links("\n").to_a).to be_empty end it "doesn't extract links from elided parts" do expect(PrettyText.extract_links("
cnn
\n").to_a).to be_empty end def extract_urls(text) PrettyText.extract_links(text).map(&:url).to_a end it "should be able to extract links" do expect(extract_urls("http://bla.com")).to eq(["http://cnn.com"]) end it "should extract links to topics" do expect(extract_urls("")).to eq(["/t/topic/321"]) end it "should lazyYT videos" do expect(extract_urls("
")).to eq(["https://www.youtube.com/watch?v=yXEuEUQIP3Q"]) end it "should extract links to posts" do expect(extract_urls("")).to eq(["/t/topic/1234/4567"]) end it "should not extract links to anchors" do expect(extract_urls("TOS")).to eq([]) end it "should not extract links inside quotes" do links = PrettyText.extract_links(" http://useless1.com http://useless2.com ") expect(links.map { |l| [l.url, l.is_quote] }.sort).to eq([ ["http://body_only.com", false], ["http://body_and_quote.com", false], ["/t/topic/1234", true], ].sort) end it "should not preserve tags in code blocks" do expect(PrettyText.excerpt("
<h3>Hours</h3>
",100)).to eq("<h3>Hours</h3>") end it "should handle nil" do expect(PrettyText.excerpt(nil,100)).to eq('') end it "handles span excerpt at the beginning of a post" do expect(PrettyText.excerpt("hi test",100)).to eq('hi') post = Fabricate(:post, raw: "hi test") expect(post.excerpt).to eq("hi") end it "ignores max excerpt length if a span excerpt is specified" do two_hundred = "123456789 " * 20 + "." text = two_hundred + "#{two_hundred}" + two_hundred expect(PrettyText.excerpt(text, 100)).to eq(two_hundred) post = Fabricate(:post, raw: text) expect(post.excerpt).to eq(two_hundred) end it "unescapes html entities when we want text entities" do expect(PrettyText.excerpt("'", 500, text_entities: true)).to eq("'") end it "should have an option to preserve emoji images" do emoji_image = "heart" expect(PrettyText.excerpt(emoji_image, 100, { keep_emoji_images: true })).to match_html(emoji_image) end it "should have an option to remap emoji to code points" do emoji_image = "I :heart: you :unknown: " expect(PrettyText.excerpt(emoji_image, 100, { remap_emoji: true })).to match_html("I ā¤ you :unknown:") end it "should have an option to preserve emoji codes" do emoji_code = ":heart:" expect(PrettyText.excerpt(emoji_code, 100)).to eq(":heart:") end context 'option to preserve onebox source' do it "should return the right excerpt" do onebox = "\n\n\n" expected = "meta.discourse.org" expect(PrettyText.excerpt(onebox, 100, keep_onebox_source: true)) .to eq(expected) expect(PrettyText.excerpt("#{onebox}\n \n \n \n\n\n #{onebox}", 100, keep_onebox_source: true)) .to eq("#{expected}\n\n#{expected}") end it 'should continue to strip quotes' do expect(PrettyText.excerpt( "boom", 100, keep_onebox_source: true )).to eq("boom") end end end describe "strip links" do it "returns blank for blank input" do expect(PrettyText.strip_links("")).to be_blank end it "does nothing to a string without links" do expect(PrettyText.strip_links("I'm the batman")).to eq("I'm the batman") end it "strips links but leaves the text content" do expect(PrettyText.strip_links("I'm the linked batman")).to eq("I'm the linked batman") end it "escapes the text content" do expect(PrettyText.strip_links("I'm the linked <batman>")).to eq("I'm the linked <batman>") end end describe "strip_image_wrapping" do def strip_image_wrapping(html) doc = Nokogiri::HTML.fragment(html) described_class.strip_image_wrapping(doc) doc.to_html end it "doesn't change HTML when there's no wrapped image" do html = "" expect(strip_image_wrapping(html)).to eq(html) end it "strips the metadata" do expect(strip_image_wrapping(wrapped_image)).to match_html "
" end end describe 'format_for_email' do let(:base_url) { "http://baseurl.net" } let(:post) { Fabricate(:post) } before do Discourse.stubs(:base_url).returns(base_url) end it 'does not crash' do PrettyText.format_for_email('test', post) end it "adds base url to relative links" do html = "

@wiseguy, @trollol what do you guys think?

" output = described_class.format_for_email(html, post) expect(output).to eq("

@wiseguy, @trollol what do you guys think?

") end it "doesn't change external absolute links" do html = "

Check out this guy.

" expect(described_class.format_for_email(html, post)).to eq(html) end it "doesn't change internal absolute links" do html = "

Check out this guy.

" expect(described_class.format_for_email(html, post)).to eq(html) end it "can tolerate invalid URLs" do html = "

Check out this guy.

" expect { described_class.format_for_email(html, post) }.to_not raise_error end end it 'can escape *' do expect(PrettyText.cook("***a***a")).to match_html("

aa

") expect(PrettyText.cook("***\\****a")).to match_html("

*a

") end it 'can include code class correctly' do expect(PrettyText.cook("```cpp\ncpp\n```")).to match_html("

cpp
") end it 'indents code correctly' do code = "X\n```\n\n #\n x\n```" cooked = PrettyText.cook(code) expect(cooked).to match_html("

X

\n\n

    #\n    x
") end it 'can substitute s3 cdn correctly' do SiteSetting.enable_s3_uploads = true SiteSetting.s3_access_key_id = "XXX" SiteSetting.s3_secret_access_key = "XXX" SiteSetting.s3_upload_bucket = "test" SiteSetting.s3_cdn_url = "https://awesome.cdn" # add extra img tag to ensure it does not blow up raw = < HTML cooked = <


HTML expect(PrettyText.cook(raw)).to match_html(cooked) end describe "emoji" do it "replaces unicode emoji with our emoji sets if emoji is enabled" do expect(PrettyText.cook("šŸ’£")).to match(/\:bomb\:/) end it "doesn't replace emoji in inline code blocks with our emoji sets if emoji is enabled" do expect(PrettyText.cook("`šŸ’£`")).not_to match(/\:bomb\:/) end it "doesn't replace emoji in code blocks with our emoji sets if emoji is enabled" do expect(PrettyText.cook("```\nšŸ’£`\n```\n")).not_to match(/\:bomb\:/) end it "replaces some glyphs that are not in the emoji range" do expect(PrettyText.cook("ā˜ŗ")).to match(/\:slight_smile\:/) end it "doesn't replace unicode emoji if emoji is disabled" do SiteSetting.enable_emoji = false expect(PrettyText.cook("šŸ’£")).not_to match(/\:bomb\:/) end it "replaces skin toned emoji" do expect(PrettyText.cook("hello šŸ‘±šŸæā€ā™€ļø")).to eq("

hello \":blonde_woman:t6:\"

") expect(PrettyText.cook("hello šŸ‘©ā€šŸŽ¤")).to eq("

hello \":woman_singer:\"

") expect(PrettyText.cook("hello šŸ‘©šŸ¾ā€šŸŽ“")).to eq("

hello \":woman_student:t5:\"

") expect(PrettyText.cook("hello šŸ¤·ā€ā™€ļø")).to eq("

hello \":woman_shrugging:\"

") end end describe "tag and category links" do it "produces tag links" do Fabricate(:topic, {tags: [Fabricate(:tag, name: 'known')]}) cooked = PrettyText.cook(" #unknown::tag #known::tag") html = <<~HTML

#unknown::tag #known

HTML expect(cooked).to match_html(html) end # TODO does it make sense to generate hashtags for tags that are missing in action? end describe "custom emoji" do it "replaces the custom emoji" do CustomEmoji.create!(name: 'trout', upload: Fabricate(:upload)) Emoji.clear_cache expect(PrettyText.cook("hello :trout:")).to match(/]+trout[^>]+>/) end end describe "censored_pattern site setting" do it "can be cleared if it causes cooking to timeout" do SiteSetting.censored_pattern = "evilregex" described_class.stubs(:markdown).raises(MiniRacer::ScriptTerminatedError) PrettyText.cook("Protect against it plz.") rescue nil expect(SiteSetting.censored_pattern).to be_blank end end context "markdown it" do before do SiteSetting.enable_experimental_markdown_it = true end it "replaces skin toned emoji" do expect(PrettyText.cook("hello šŸ‘±šŸæā€ā™€ļø")).to eq("

hello \":blonde_woman:t6:\"

") expect(PrettyText.cook("hello šŸ‘©ā€šŸŽ¤")).to eq("

hello \":woman_singer:\"

") expect(PrettyText.cook("hello šŸ‘©šŸ¾ā€šŸŽ“")).to eq("

hello \":woman_student:t5:\"

") expect(PrettyText.cook("hello šŸ¤·ā€ā™€ļø")).to eq("

hello \":woman_shrugging:\"

") end it "supports href schemes" do SiteSetting.allowed_href_schemes = "macappstore|steam" cooked = cook("[Steam URL Scheme](steam://store/452530)") expected = '

Steam URL Scheme

' expect(cooked).to eq(n expected) end it "supports forbidden schemes" do SiteSetting.allowed_href_schemes = "macappstore|itunes" cooked = cook("[Steam URL Scheme](steam://store/452530)") expected = '

Steam URL Scheme

' expect(cooked).to eq(n expected) end it "produces tag links" do Fabricate(:topic, {tags: [Fabricate(:tag, name: 'known')]}) cooked = PrettyText.cook(" #unknown::tag #known::tag") html = <<~HTML

#unknown::tag #known

HTML expect(cooked).to eq(html.strip) cooked = PrettyText.cook("[`a` #known::tag here](http://somesite.com)") html = <<~HTML

a #known::tag here

HTML expect(cooked).to eq(html.strip) cooked = PrettyText.cook("`a` #known::tag here") expect(cooked).to eq(html.strip) cooked = PrettyText.cook("test #known::tag") html = <<~HTML

test #known

HTML expect(cooked).to eq(html.strip) # ensure it does not fight with the autolinker expect(PrettyText.cook(' http://somewhere.com/#known')).not_to include('hashtag') expect(PrettyText.cook(' http://somewhere.com/?#known')).not_to include('hashtag') expect(PrettyText.cook(' http://somewhere.com/?abc#known')).not_to include('hashtag') end it "can handle mixed lists" do # known bug in old md engine cooked = PrettyText.cook("* a\n\n1. b") expect(cooked).to match_html("
    \n
  1. b
  2. \n
") end it "can handle traditional vs non traditional newlines" do SiteSetting.traditional_markdown_linebreaks = true expect(PrettyText.cook("1\n2")).to match_html "

1 2

" SiteSetting.traditional_markdown_linebreaks = false expect(PrettyText.cook("1\n2")).to match_html "

1
\n2

" end it "can handle mentions" do Fabricate(:user, username: "sam") expect(PrettyText.cook("hi @sam! hi")).to match_html '

hi @sam! hi

' expect(PrettyText.cook("hi\n@sam")).to eq("

hi
\n@sam

") end it "can handle mentions inside a hyperlink" do expect(PrettyText.cook(" @inner ")).to match_html '

@inner

' end it "can handle mentions inside a hyperlink" do expect(PrettyText.cook("[link @inner](http://site.com)")).to match_html '

link @inner

' end it "can handle a list of mentions" do expect(PrettyText.cook("@a,@b")).to match_html('

@a,@b

') end it "can handle emoji by name" do expected = <:smile::sunny:

HTML expect(PrettyText.cook(":smile::sunny:")).to eq(expected.strip) end it "handles emoji boundaries correctly" do cooked = PrettyText.cook("a,:man:t2:,b") expected = '

a,:man:t2:,b

' expect(cooked).to match(expected.strip) end it "can handle emoji by translation" do expected = '

:wink:

' expect(PrettyText.cook(";)")).to eq(expected) end it "can handle multiple emojis by translation" do cooked = PrettyText.cook(":) ;) :)") expect(cooked.split("img").length-1).to eq(3) end it "handles emoji boundries correctly" do expect(PrettyText.cook(",:)")).to include("emoji") expect(PrettyText.cook(":-)\n")).to include("emoji") expect(PrettyText.cook("a :)")).to include("emoji") expect(PrettyText.cook(":),")).not_to include("emoji") expect(PrettyText.cook("abcde ^:;-P")).to include("emoji") end it 'can include code class correctly' do expect(PrettyText.cook("```cpp\ncpp\n```")).to match_html("
cpp\n
") expect(PrettyText.cook("```\ncpp\n```")).to match_html("
cpp\n
") expect(PrettyText.cook("```text\ncpp\n```")).to match_html("
cpp\n
") end it 'indents code correctly' do code = "X\n```\n\n #\n x\n```" cooked = PrettyText.cook(code) expect(cooked).to match_html("

X

\n
\n    #\n    x\n
") end it 'can censor words correctly' do SiteSetting.censored_words = 'apple|banana' expect(PrettyText.cook('yay banana yay')).not_to include('banana') expect(PrettyText.cook('yay `banana` yay')).not_to include('banana') expect(PrettyText.cook("yay \n\n```\nbanana\n````\n yay")).not_to include('banana') expect(PrettyText.cook("# banana")).not_to include('banana') expect(PrettyText.cook("# banana")).to include("\u25a0\u25a0") end it 'supports typographer' do SiteSetting.enable_markdown_typographer = true expect(PrettyText.cook('(tm)')).to eq('

ā„¢

') SiteSetting.enable_markdown_typographer = false expect(PrettyText.cook('(tm)')).to eq('

(tm)

') end it 'handles onebox correctly' do expect(PrettyText.cook("http://a.com\nhttp://b.com").split("onebox").length).to eq(3) expect(PrettyText.cook("http://a.com\n\nhttp://b.com").split("onebox").length).to eq(3) expect(PrettyText.cook("a\nhttp://a.com")).to include('onebox') expect(PrettyText.cook("> http://a.com")).not_to include('onebox') expect(PrettyText.cook("a\nhttp://a.com a")).not_to include('onebox') expect(PrettyText.cook("a\nhttp://a.com\na")).to include('onebox') expect(PrettyText.cook("http://a.com")).to include('onebox') expect(PrettyText.cook("http://a.com ")).to include('onebox') expect(PrettyText.cook("http://a.com a")).not_to include('onebox') expect(PrettyText.cook("- http://a.com")).not_to include('onebox') expect(PrettyText.cook("")).not_to include('onebox') expect(PrettyText.cook(" http://a.com")).not_to include('onebox') expect(PrettyText.cook("a\n http://a.com")).not_to include('onebox') end it "can handle bbcode" do expect(PrettyText.cook("a[b]b[/b]c")).to eq('

abc

') expect(PrettyText.cook("a[i]b[/i]c")).to eq('

abc

') end it "can handle quote edge cases" do expect(PrettyText.cook("a\n[quote]\ntest\n[/quote]\n\n\na")).to include('aside') expect(PrettyText.cook("- a\n[quote]\ntest\n[/quote]\n\n\na")).to include('aside') expect(PrettyText.cook("[quote]\ntest")).not_to include('aside') expect(PrettyText.cook("[quote]abc\ntest\n[/quote]")).not_to include('aside') expect(PrettyText.cook("[quote]\ntest\n[/quote]z")).not_to include('aside') nested = <<~QUOTE [quote] a [quote] b [/quote] c [/quote] QUOTE cooked = PrettyText.cook(nested) expect(cooked.scan('aside').length).to eq(4) expect(cooked.scan('quote]').length).to eq(0) end it "can onebox local topics" do op = Fabricate(:post) reply = Fabricate(:post, topic_id: op.topic_id) url = Discourse.base_url + reply.url quote = create_post(topic_id: op.topic.id, raw: "This is a sample reply with a quote\n\n#{url}") quote.reload expect(quote.cooked).not_to include('[quote') end it "supports tables" do markdown = <<~MD | Tables | Are | Cool | | ------------- |:-------------:| -----:| | col 3 is | right-aligned | $1600 | MD expected = <<~HTML
Tables Are Cool
col 3 is right-aligned $1600
HTML expect(PrettyText.cook(markdown)).to eq(expected.strip) end it "do off topic quoting with emoji unescape" do topic = Fabricate(:topic, title: "this is a test topic :slight_smile:") expected = <<~HTML HTML expect(cook("[quote=\"EvilTrout, post:2, topic:#{topic.id}\"]\nddd\n[/quote]", topic_id: 1)).to eq(n(expected)) end it "supports img bbcode" do cooked = PrettyText.cook "[img]http://www.image/test.png[/img]" html = "

" expect(cooked).to eq(html) end it "provides safety for img bbcode" do cooked = PrettyText.cook "[img]http://aaa.com[/img]" html = '

' expect(cooked).to eq(html) end it "supports email bbcode" do cooked = PrettyText.cook "[email]sam@sam.com[/email]" html = '

sam@sam.com

' expect(cooked).to eq(html) end it "supports url bbcode" do cooked = PrettyText.cook "[url]http://sam.com[/url]" html = '

http://sam.com

' expect(cooked).to eq(html) end it "supports inline code bbcode" do cooked = PrettyText.cook "Testing [code]codified **stuff** and `more` stuff[/code]" html = "

Testing codified **stuff** and `more` stuff

" expect(cooked).to eq(html) end it "supports block code bbcode" do cooked = PrettyText.cook "[code]\ncodified\n\n\n **stuff** and `more` stuff\n[/code]" html = "
codified\n\n\n  **stuff** and `more` stuff
" expect(cooked).to eq(html) end it "support special handling for space in urls" do cooked = PrettyText.cook "http://testing.com?a%20b" html = '

http://testing.com?a%20b

' expect(cooked).to eq(html) end it "supports onebox for decoded urls" do cooked = PrettyText.cook "http://testing.com?a%50b" html = '

http://testing.com?aPb

' expect(cooked).to eq(html) end end end