FIX: smarter newline handling for <img> tags on line alone

Run all of pretty text spec on new engine
This commit is contained in:
Sam 2017-07-10 12:20:50 -04:00
parent 890a99232d
commit ba9898c5a9
3 changed files with 515 additions and 366 deletions

View File

@ -12,3 +12,4 @@
//= require ./pretty-text/engines/markdown-it/table
//= require ./pretty-text/engines/markdown-it/paragraph
//= require ./pretty-text/engines/markdown-it/newline
//= require ./pretty-text/engines/markdown-it/html_img

View File

@ -0,0 +1,74 @@
// special handling for IMG tags on a line by themeselves
// we always have to handle it as so it is an inline
// see: https://talk.commonmark.org/t/newline-and-img-tags/2511
const REGEX = /^<img.*\\?>\s*$/i;
function rule(state, startLine, endLine) {
var nextLine, token, lineText,
pos = state.bMarks[startLine] + state.tShift[startLine],
max = state.eMarks[startLine];
// if it's indented more than 3 spaces, it should be a code block
if (state.sCount[startLine] - state.blkIndent >= 4) { return false; }
if (!state.md.options.html) { return false; }
if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; }
let pos1 = state.src.charCodeAt(pos+1);
if (pos1 !== 73 /* I */ && pos1 !== 105 /* i */) { return false; }
lineText = state.src.slice(pos, max);
if (!REGEX.test(lineText)) {
return false;
}
let lines = [];
lines.push(lineText);
nextLine = startLine + 1;
for (; nextLine < endLine; nextLine++) {
pos = state.bMarks[nextLine] + state.tShift[nextLine];
max = state.eMarks[nextLine];
lineText = state.src.slice(pos, max);
if (lineText.trim() === "") {
break;
}
if (!REGEX.test(lineText)) {
break;
}
lines.push(lineText);
}
state.line = nextLine;
let oldParentType = state.parentType;
state.parentType = 'paragraph';
token = state.push('paragraph_open', 'p', 0);
token.map = [startLine, state.line];
token = state.push('inline', '', 0);
token.content = lines.join('\n');
token.map = [startLine, state.line];
token.children = [];
token = state.push('paragraph_close', 'p', -1);
state.parentType = oldParentType;
return true;
}
export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md=>{
md.block.ruler.before('html_block', 'html_img', rule, {alt: ['fence']});
});
}

View File

@ -4,6 +4,10 @@ require 'html_normalize'
describe PrettyText do
before do
SiteSetting.enable_experimental_markdown_it = true
end
def n(html)
HtmlNormalize.normalize(html)
end
@ -12,23 +16,15 @@ describe PrettyText do
n(PrettyText.cook(*args))
end
# see: https://github.com/sparklemotion/nokogiri/issues/1173
skip 'allows html entities correctly' do
expect(PrettyText.cook("&aleph;&pound;&#162;")).to eq("<p>&aleph;&pound;&#162;</p>")
end
let(:wrapped_image) { "<div class=\"lightbox-wrapper\"><a href=\"//localhost:3000/uploads/default/4399/33691397e78b4d75.png\" class=\"lightbox\" title=\"Screen Shot 2014-04-14 at 9.47.10 PM.png\"><img src=\"//localhost:3000/uploads/default/_optimized/bd9/b20/bbbcd6a0c0_655x500.png\" width=\"655\" height=\"500\"><div class=\"meta\">\n<span class=\"filename\">Screen Shot 2014-04-14 at 9.47.10 PM.png</span><span class=\"informations\">966x737 1.47 MB</span><span class=\"expand\"></span>\n</div></a></div>" }
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
<aside class="quote" data-post="2" data-topic="#{topic.id}"><div class="title">
<div class="quote-controls"></div><a href="http://test.localhost/t/this-is-a-test-topic-slight-smile/#{topic.id}/2">This is a test topic <img src="/images/emoji/twitter/slight_smile.png?v=5" title="slight_smile" alt="slight_smile" class="emoji"></a>
</div>
<blockquote><p>ddd</p></blockquote></aside>
HTML
expect(PrettyText.cook("[quote=\"EvilTrout, post:2, topic:#{topic.id}\"]ddd\n[/quote]", topic_id: 1)).to match_html expected
end
end
describe "Quoting" do
describe "with avatar" do
let(:default_avatar) { "//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png" }
@ -38,19 +34,91 @@ HTML
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 "<aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img alt='' width=\"20\" height=\"20\" src=\"//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">#{user.username}:</div>\n<blockquote><p>ddd</p></blockquote></aside>"
it "do off topic quoting with emoji unescape" do
topic = Fabricate(:topic, title: "this is a test topic :slight_smile:")
expected = <<~HTML
<aside class="quote" data-topic="#{topic.id}" data-post="2">
<div class="title">
<div class="quote-controls"></div>
<a href="http://test.localhost/t/this-is-a-test-topic-slight-smile/#{topic.id}/2">This is a test topic <img src="/images/emoji/twitter/slight_smile.png?v=5" title="slight_smile" alt="slight_smile" class="emoji"></a>
</div>
<blockquote>
<p>ddd</p>
</blockquote>
</aside>
HTML
expect(cook("[quote=\"EvilTrout, post:2, topic:#{topic.id}\"]\nddd\n[/quote]", topic_id: 1)).to eq(n(expected))
end
it "should produce a quote" do
expect(PrettyText.cook("[quote=\"#{user.username}, post:123, topic:456, full:true\"]ddd[/quote]")).to match_html "<aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img alt='' width=\"20\" height=\"20\" src=\"//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">#{user.username}:</div>\n<blockquote><p>ddd</p></blockquote></aside>"
it "produces a quote even with new lines in it" do
md = <<~MD
[quote="#{user.username}, post:123, topic:456, full:true"]
ddd
[/quote]
MD
html = <<~HTML
<aside class="quote" data-post="123" data-topic="456">
<div class="title">
<div class="quote-controls"></div>
<img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div>
<blockquote>
<p>ddd</p>
</blockquote>
</aside>
HTML
expect(PrettyText.cook(md)).to eq(html.strip)
end
it "trims spaces on quote params" do
expect(PrettyText.cook("[quote=\"#{user.username}, post:555, topic: 666\"]ddd[/quote]")).to match_html "<aside class=\"quote\" data-post=\"555\" data-topic=\"666\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img alt='' width=\"20\" height=\"20\" src=\"//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">#{user.username}:</div>\n<blockquote><p>ddd</p></blockquote></aside>"
md = <<~MD
[quote="#{user.username}, post:555, topic: 666"]
ddd
[/quote]
MD
html = <<~HTML
<aside class="quote" data-post="555" data-topic="666">
<div class="title">
<div class="quote-controls"></div>
<img alt width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div>
<blockquote>
<p>ddd</p>
</blockquote>
</aside>
HTML
expect(PrettyText.cook(md)).to eq(html.strip)
end
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
describe "with letter avatar" do
let(:user) { Fabricate(:user) }
@ -61,22 +129,44 @@ HTML
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")
md = <<~MD
[quote="#{user.username}, post:123, topic:456, full:true"]
ddd
[/quote]
MD
expect(PrettyText.cook(md)).to include("/forum/letter_avatar_proxy")
end
end
end
end
describe "Mentions" do
it "should handle 3 mentions in a row" do
expect(PrettyText.cook('@hello @hello @hello')).to match_html "<p><span class=\"mention\">@hello</span> <span class=\"mention\">@hello</span> <span class=\"mention\">@hello</span></p>"
end
it "should handle group mentions with a hyphen and without" do
expect(PrettyText.cook('@hello @hello-hello')).to match_html "<p><span class=\"mention\">@hello</span> <span class=\"mention\">@hello-hello</span></p>"
it "can handle mentions" do
Fabricate(:user, username: "sam")
expect(PrettyText.cook("hi @sam! hi")).to match_html '<p>hi <a class="mention" href="/u/sam">@sam</a>! hi</p>'
expect(PrettyText.cook("hi\n@sam")).to eq("<p>hi<br>\n<a class=\"mention\" href=\"/u/sam\">@sam</a></p>")
end
it "can handle mentions inside a hyperlink" do
expect(PrettyText.cook("<a> @inner</a> ")).to match_html '<p><a> @inner</a></p>'
end
it "should sanitize the html" do
expect(PrettyText.cook("<script>alert(42)</script>")).to match_html "<p></p>"
it "can handle mentions inside a hyperlink" do
expect(PrettyText.cook("[link @inner](http://site.com)")).to match_html '<p><a href="http://site.com" rel="nofollow noopener">link @inner</a></p>'
end
it "can handle a list of mentions" do
expect(PrettyText.cook("@a,@b")).to match_html('<p><span class="mention">@a</span>,<span class="mention">@b</span></p>')
end
it "should handle group mentions with a hyphen and without" do
expect(PrettyText.cook('@hello @hello-hello')).to match_html "<p><span class=\"mention\">@hello</span> <span class=\"mention\">@hello-hello</span></p>"
end
it 'should allow for @mentions to have punctuation' do
@ -85,13 +175,52 @@ HTML
)
end
# see: https://github.com/sparklemotion/nokogiri/issues/1173
skip 'allows html entities correctly' do
expect(PrettyText.cook("&aleph;&pound;&#162;")).to eq("<p>&aleph;&pound;&#162;</p>")
end
describe "code fences" do
it 'indents code correctly' do
code = <<~MD
X
```
#
x
```
MD
cooked = PrettyText.cook(code)
html = <<~HTML
<p>X</p>
<pre><code class="lang-auto"> #
x
</code></pre>
HTML
expect(cooked).to eq(html.strip)
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 'can include code class correctly' do
expect(PrettyText.cook("```cpp\ncpp\n```")).to match_html("<pre><code class='lang-cpp'>cpp\n</code></pre>")
expect(PrettyText.cook("```\ncpp\n```")).to match_html("<pre><code class='lang-auto'>cpp\n</code></pre>")
expect(PrettyText.cook("```text\ncpp\n```")).to match_html("<pre><code class='lang-nohighlight'>cpp\n</code></pre>")
end
it 'indents code correctly' do
code = "X\n```\n\n #\n x\n```"
cooked = PrettyText.cook(code)
expect(cooked).to match_html("<p>X</p>\n<pre><code class=\"lang-auto\">\n #\n x\n</code></pre>")
end
it 'does censor code fences' do
SiteSetting.censored_words = 'apple|banana'
expect(PrettyText.cook("# banana")).not_to include('banana')
end
end
describe "rel nofollow" do
before do
SiteSetting.add_rel_nofollow_to_user_content = true
@ -330,7 +459,6 @@ HTML
)).to eq("boom")
end
end
end
describe "strip links" do
@ -402,20 +530,55 @@ HTML
end
end
it 'can escape *' do
expect(PrettyText.cook("***a***a")).to match_html("<p><strong><em>a</em></strong>a</p>")
expect(PrettyText.cook("***\\****a")).to match_html("<p><strong><em>*</em></strong>a</p>")
it 'Is smart about linebreaks and IMG tags' do
raw = <<~MD
a <img>
<img>
<img>
<img>
<img>
a
<img>
- li
<img>
```
test
```
```
test
```
MD
html = <<~HTML
<p>a <img><br>
<img></p>
<p>
<img><br>
<img></p>
<p>
<img></p>
<p>a</p>
<p>
<img></p>
<ul>
<li>li</li>
</ul>
<p>
<img></p>
<pre><code class="lang-auto">test
</code></pre>
<pre><code class="lang-auto">test
</code></pre>
HTML
expect(PrettyText.cook(raw)).to eq(html.strip)
end
it 'can include code class correctly' do
expect(PrettyText.cook("```cpp\ncpp\n```")).to match_html("<p></p><pre><code class='lang-cpp'>cpp</code></pre>")
end
it 'indents code correctly' do
code = "X\n```\n\n #\n x\n```"
cooked = PrettyText.cook(code)
expect(cooked).to match_html("<p>X<br></p>\n\n<p></p><pre><code class=\"lang-auto\"> #\n x</code></pre>")
end
it 'can substitute s3 cdn correctly' do
SiteSetting.enable_s3_uploads = true
@ -425,19 +588,22 @@ HTML
SiteSetting.s3_cdn_url = "https://awesome.cdn"
# add extra img tag to ensure it does not blow up
raw = <<HTML
raw = <<~HTML
<img>
<img src='https:#{Discourse.store.absolute_base_url}/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg'>
<img src='http:#{Discourse.store.absolute_base_url}/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg'>
<img src='#{Discourse.store.absolute_base_url}/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg'>
HTML
cooked = <<HTML
<p> <img><br> <img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"><br> <img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"><br> <img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"></p>
html = <<~HTML
<p>
<img><br>
<img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"><br>
<img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"><br>
<img src="https://awesome.cdn/original/9/9/99c9384b8b6d87f8509f8395571bc7512ca3cad1.jpg"></p>
HTML
expect(PrettyText.cook(raw)).to match_html(cooked)
expect(PrettyText.cook(raw)).to eq(html.strip)
end
describe "emoji" do
@ -449,10 +615,6 @@ HTML
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
@ -470,22 +632,6 @@ HTML
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
<p> <span class=\"hashtag\">#unknown::tag</span> <a class=\"hashtag\" href=\"http://test.localhost/tags/known\">#<span>known</span></a></p>
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))
@ -504,12 +650,6 @@ HTML
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("<p>hello <img src=\"/images/emoji/twitter/blonde_woman/6.png?v=5\" title=\":blonde_woman:t6:\" class=\"emoji\" alt=\":blonde_woman:t6:\"></p>")
expect(PrettyText.cook("hello 👩‍🎤")).to eq("<p>hello <img src=\"/images/emoji/twitter/woman_singer.png?v=5\" title=\":woman_singer:\" class=\"emoji\" alt=\":woman_singer:\"></p>")
@ -583,24 +723,6 @@ HTML
expect(PrettyText.cook("1\n2")).to match_html "<p>1<br>\n2</p>"
end
it "can handle mentions" do
Fabricate(:user, username: "sam")
expect(PrettyText.cook("hi @sam! hi")).to match_html '<p>hi <a class="mention" href="/u/sam">@sam</a>! hi</p>'
expect(PrettyText.cook("hi\n@sam")).to eq("<p>hi<br>\n<a class=\"mention\" href=\"/u/sam\">@sam</a></p>")
end
it "can handle mentions inside a hyperlink" do
expect(PrettyText.cook("<a> @inner</a> ")).to match_html '<p><a> @inner</a></p>'
end
it "can handle mentions inside a hyperlink" do
expect(PrettyText.cook("[link @inner](http://site.com)")).to match_html '<p><a href="http://site.com" rel="nofollow noopener">link @inner</a></p>'
end
it "can handle a list of mentions" do
expect(PrettyText.cook("@a,@b")).to match_html('<p><span class="mention">@a</span>,<span class="mention">@b</span></p>')
end
it "can handle emoji by name" do
@ -634,24 +756,10 @@ HTML
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("<pre><code class='lang-cpp'>cpp\n</code></pre>")
expect(PrettyText.cook("```\ncpp\n```")).to match_html("<pre><code class='lang-auto'>cpp\n</code></pre>")
expect(PrettyText.cook("```text\ncpp\n```")).to match_html("<pre><code class='lang-nohighlight'>cpp\n</code></pre>")
end
it 'indents code correctly' do
code = "X\n```\n\n #\n x\n```"
cooked = PrettyText.cook(code)
expect(cooked).to match_html("<p>X</p>\n<pre><code class=\"lang-auto\">\n #\n x\n</code></pre>")
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
@ -685,27 +793,6 @@ HTML
expect(PrettyText.cook("a[i]b[/i]c")).to eq('<p>a<span class="bbcode-i">b</span>c</p>')
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)
@ -749,23 +836,6 @@ 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
<aside class="quote" data-topic="#{topic.id}" data-post="2">
<div class="title">
<div class="quote-controls"></div>
<a href="http://test.localhost/t/this-is-a-test-topic-slight-smile/#{topic.id}/2">This is a test topic <img src="/images/emoji/twitter/slight_smile.png?v=5" title="slight_smile" alt="slight_smile" class="emoji"></a>
</div>
<blockquote>
<p>ddd</p>
</blockquote>
</aside>
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]"
@ -815,6 +885,10 @@ HTML
expect(cooked).to eq(html)
end
it "should sanitize the html" do
expect(PrettyText.cook("<script>alert(42)</script>")).to eq ""
end
end