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) { "
" } 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 = <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 "ddd
@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("", 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("<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 = ""
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 you "
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 = ""
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 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 = < hello
") expect(PrettyText.cook("hello š©āš¤")).to eq("hello
") expect(PrettyText.cook("hello š©š¾āš")).to eq("hello
") expect(PrettyText.cook("hello š¤·āāļø")).to eq("hello
") 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
") expect(PrettyText.cook("hello š©āš¤")).to eq("hello
") expect(PrettyText.cook("hello š©š¾āš")).to eq("hello
") expect(PrettyText.cook("hello š¤·āāļø")).to eq("hello
") end it "supports href schemes" do SiteSetting.allowed_href_schemes = "macappstore|steam" cooked = cook("[Steam URL Scheme](steam://store/452530)") expected = '' 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 = '' 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 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 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("1 2
" SiteSetting.traditional_markdown_linebreaks = false expect(PrettyText.cook("1\n2")).to match_html "1
\n2
hi @sam! hi
' expect(PrettyText.cook("hi\n@sam")).to eq("hi
\n@sam
@a,@b
') end it "can handle emoji by name" do expected = < 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,,b
' expect(cooked).to match(expected.strip) end it "can handle emoji by translation" do expected = '' 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("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 = <<~HTMLTables | Are | Cool |
---|---|---|
col 3 is | right-aligned | $1600 |
Testing codified **stuff** and `more` stuff
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 = ''
expect(cooked).to eq(html)
end
it "supports onebox for decoded urls" do
cooked = PrettyText.cook "http://testing.com?a%50b"
html = ''
expect(cooked).to eq(html)
end
end
end