Replace Markdown Linebreak Regexp with node parser.

This commit is contained in:
Robin Ward 2013-08-26 15:21:23 -04:00
parent 7c07079ed9
commit 2d45c56ba5
8 changed files with 111 additions and 32 deletions

View File

@ -97,17 +97,7 @@ Discourse.Markdown = {
return { return {
makeHtml: function(text) { makeHtml: function(text) {
// Linebreaks
var linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks;
if (!linebreaks) {
text = text.replace(/(^[\w<][^\n]*\n+)/gim, function(t) {
if (t.match(/\n{2}/gim)) return t;
return t.replace("\n", " \n");
});
}
text = Discourse.Dialect.cook(text, opts); text = Discourse.Dialect.cook(text, opts);
if (!text) return ""; if (!text) return "";
if (opts.sanitize) { if (opts.sanitize) {

View File

@ -174,7 +174,7 @@ Discourse.Dialect.on("register", function(event) {
result.push(['p', ['aside', params, result.push(['p', ['aside', params,
['div', {'class': 'title'}, ['div', {'class': 'title'},
['div', {'class': 'quote-controls'}], ['div', {'class': 'quote-controls'}],
avatarImg ? avatarImg + "\n" : "", avatarImg ? avatarImg : "",
I18n.t('user.said',{username: username}) I18n.t('user.said',{username: username})
], ],
contents contents

View File

@ -32,8 +32,7 @@
```javascript ```javascript
Discourse.Dialect.on("parseNode", function(event) { Discourse.Dialect.on("parseNode", function(event) {
var node = event.node, var node = event.node;
path = event.path;
if (node[0] === 'code') { if (node[0] === 'code') {
node[node.length-1] = "EVIL TROUT HACKED YOUR CODE"; node[node.length-1] = "EVIL TROUT HACKED YOUR CODE";
@ -68,16 +67,22 @@ var parser = window.BetterMarkdown,
@method parseTree @method parseTree
@param {Array} tree the JsonML tree to parse @param {Array} tree the JsonML tree to parse
@param {Array} path the path of ancestors to the current node in the tree. Can be used for matching. @param {Array} path the path of ancestors to the current node in the tree. Can be used for matching.
@param {Object} insideCounts counts what tags we're inside
@returns {Array} the parsed tree @returns {Array} the parsed tree
**/ **/
parseTree = function parseTree(tree, path) { parseTree = function parseTree(tree, path, insideCounts) {
if (tree instanceof Array) { if (tree instanceof Array) {
Discourse.Dialect.trigger('parseNode', {node: tree, path: path}); Discourse.Dialect.trigger('parseNode', {node: tree, path: path, dialect: dialect, insideCounts: insideCounts || {}});
path = path || []; path = path || [];
insideCounts = insideCounts || {};
path.push(tree); path.push(tree);
tree.slice(1).forEach(function (n) { tree.slice(1).forEach(function (n) {
parseTree(n, path); var tagName = n[0];
insideCounts[tagName] = (insideCounts[tagName] || 0) + 1;
parseTree(n, path, insideCounts);
insideCounts[tagName] = insideCounts[tagName] - 1;
}); });
path.pop(); path.pop();
} }
@ -103,7 +108,8 @@ Discourse.Dialect = {
cook: function(text, opts) { cook: function(text, opts) {
if (!initialized) { initializeDialects(); } if (!initialized) { initializeDialects(); }
dialect.options = opts; dialect.options = opts;
return parser.renderJsonML(parseTree(parser.toHTMLTree(text, 'Discourse'))); var tree = parser.toHTMLTree(text, 'Discourse');
return parser.renderJsonML(parseTree(tree));
} }
}; };

View File

@ -84,10 +84,48 @@ Discourse.Dialect.on("register", function(event) {
@namespace Discourse.Dialect @namespace Discourse.Dialect
**/ **/
Discourse.Dialect.on("parseNode", function(event) { Discourse.Dialect.on("parseNode", function(event) {
var node = event.node, var node = event.node;
path = event.path;
if (node[0] === 'code') { if (node[0] === 'code') {
node[node.length-1] = Handlebars.Utils.escapeExpression(node[node.length-1]); node[node.length-1] = Handlebars.Utils.escapeExpression(node[node.length-1]);
} }
}); });
Discourse.Dialect.on("parseNode", function(event) {
var node = event.node,
opts = event.dialect.options,
insideCounts = event.insideCounts,
linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks;
if (!linebreaks) {
// We don't add line breaks inside a pre
if (insideCounts.pre > 0) { return; }
if (node.length > 1) {
for (var j=1; j<node.length; j++) {
var textContent = node[j];
if (typeof textContent === "string") {
if (textContent === "\n") {
node[j] = ['br'];
} else {
var split = textContent.split(/\n+/);
if (split.length) {
var spliceInstructions = [j, 1];
for (var i=0; i<split.length; i++) {
if (split[i].length > 0) {
spliceInstructions.push(split[i]);
if (i !== split.length-1) { spliceInstructions.push(['br']); }
}
}
node.splice.apply(node, spliceInstructions);
}
}
}
}
}
}
});

View File

@ -0,0 +1,42 @@
/**
Support for the newline behavior in markdown that most expect.
@event parseNode
@namespace Discourse.Dialect
**/
Discourse.Dialect.on("parseNode", function(event) {
var node = event.node,
opts = event.dialect.options,
insideCounts = event.insideCounts,
linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks;
if (!linebreaks) {
// We don't add line breaks inside a pre
if (insideCounts.pre > 0) { return; }
if (node.length > 1) {
for (var j=1; j<node.length; j++) {
var textContent = node[j];
if (typeof textContent === "string") {
if (textContent === "\n") {
node[j] = ['br'];
} else {
var split = textContent.split(/\n+/);
if (split.length) {
var spliceInstructions = [j, 1];
for (var i=0; i<split.length; i++) {
if (split[i].length > 0) {
spliceInstructions.push(split[i]);
if (i !== split.length-1) { spliceInstructions.push(['br']); }
}
}
node.splice.apply(node, spliceInstructions);
}
}
}
}
}
}
});

View File

@ -14,15 +14,15 @@ describe PrettyText do
end end
it "produces a quote even with new lines in it" do it "produces a quote even with new lines in it" do
PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]").should match_html "<p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">\nEvilTrout said:</div>\n<blockquote>ddd\n</blockquote></aside></p>" PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]").should match_html "<p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">EvilTrout said:</div>\n<blockquote>ddd<br>\n</blockquote></aside></p>"
end end
it "should produce a quote" do it "should produce a quote" do
PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]").should match_html "<p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">\nEvilTrout said:</div>\n<blockquote>ddd</blockquote></aside></p>" PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]").should match_html "<p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">EvilTrout said:</div>\n<blockquote>ddd</blockquote></aside></p>"
end end
it "trims spaces on quote params" do it "trims spaces on quote params" do
PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]").should match_html "<p><aside class=\"quote\" data-post=\"555\" data-topic=\"666\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">\nEvilTrout said:</div>\n<blockquote>ddd</blockquote></aside></p>" PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]").should match_html "<p><aside class=\"quote\" data-post=\"555\" data-topic=\"666\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">EvilTrout said:</div>\n<blockquote>ddd</blockquote></aside></p>"
end end
end end

View File

@ -88,7 +88,7 @@ test("quote formatting", function() {
format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]\nhello", format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]\nhello",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:" + "<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:" +
"</div><blockquote>abc</blockquote></aside></p>\n\n<p>\nhello", "</div><blockquote>abc</blockquote></aside></p>\n\n<p>hello",
"handles new lines properly"); "handles new lines properly");
}); });

View File

@ -20,8 +20,7 @@ test("basic cooking", function() {
cooked("***hello***", "<p><strong><em>hello</em></strong></p>", "it can do bold and italics at once."); cooked("***hello***", "<p><strong><em>hello</em></strong></p>", "it can do bold and italics at once.");
}); });
test("Line Breaks", function() { test("Traditional Line Breaks", function() {
var input = "1\n2\n3"; var input = "1\n2\n3";
cooked(input, "<p>1<br>2<br>3</p>", "automatically handles trivial newlines"); cooked(input, "<p>1<br>2<br>3</p>", "automatically handles trivial newlines");
@ -34,7 +33,12 @@ test("Line Breaks", function() {
Discourse.SiteSettings.traditional_markdown_linebreaks = true; Discourse.SiteSettings.traditional_markdown_linebreaks = true;
cooked(input, traditionalOutput, "It supports traditional markdown via a Site Setting"); cooked(input, traditionalOutput, "It supports traditional markdown via a Site Setting");
});
test("Line Breaks", function() {
cooked("[] first choice\n[] second choice",
"<p>[] first choice<br>[] second choice</p>",
"it handles new lines correctly with [] options");
}); });
test("Links", function() { test("Links", function() {
@ -79,11 +83,10 @@ test("Links", function() {
"<p>Here's a tweet:<br><a href=\"https://twitter.com/evil_trout/status/345954894420787200\" class=\"onebox\">https://twitter.com/evil_trout/status/345954894420787200</a></p>", "<p>Here's a tweet:<br><a href=\"https://twitter.com/evil_trout/status/345954894420787200\" class=\"onebox\">https://twitter.com/evil_trout/status/345954894420787200</a></p>",
"It doesn't strip the new line."); "It doesn't strip the new line.");
cooked("1. View @eviltrout's profile here: http://meta.discourse.org/users/eviltrout/activity\nnext line.", cooked("1. View @eviltrout's profile here: http://meta.discourse.org/users/eviltrout/activity<br>next line.",
"<ol><li>View <span class=\"mention\">@eviltrout</span>'s profile here: <a href=\"http://meta.discourse.org/users/eviltrout/activity\">http://meta.discourse.org/users/eviltrout/activity</a><br>next line.</li></ol>", "<ol><li>View <span class=\"mention\">@eviltrout</span>'s profile here: <a href=\"http://meta.discourse.org/users/eviltrout/activity\">http://meta.discourse.org/users/eviltrout/activity</a><br>next line.</li></ol>",
"allows autolinking within a list without inserting a paragraph."); "allows autolinking within a list without inserting a paragraph.");
cooked("[3]: http://eviltrout.com", "", "It doesn't autolink markdown link references"); cooked("[3]: http://eviltrout.com", "", "It doesn't autolink markdown link references");
cooked("http://discourse.org and http://discourse.org/another_url and http://www.imdb.com/name/nm2225369", cooked("http://discourse.org and http://discourse.org/another_url and http://www.imdb.com/name/nm2225369",
@ -98,13 +101,13 @@ test("Quotes", function() {
cookedOptions("[quote=\"eviltrout, post: 1\"]\na quote\n\nsecond line\n[/quote]", cookedOptions("[quote=\"eviltrout, post: 1\"]\na quote\n\nsecond line\n[/quote]",
{ topicId: 2 }, { topicId: 2 },
"<p><aside class=\"quote\" data-post=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:</div><blockquote>\n" + "<p><aside class=\"quote\" data-post=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:</div><blockquote>" +
"a quote<br/><br/>second line<br/></blockquote></aside></p>", "a quote<br/><br/>second line<br/></blockquote></aside></p>",
"works with multiple lines"); "works with multiple lines");
cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2", cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2",
{ topicId: 2, lookupAvatar: function(name) { return "" + name; } }, { topicId: 2, lookupAvatar: function(name) { return "" + name; } },
"<p>1</p>\n\n<p><aside class=\"quote\" data-post=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>bob\n" + "<p>1</p>\n\n<p><aside class=\"quote\" data-post=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>bob" +
"bob said:</div><blockquote>my quote</blockquote></aside></p>\n\n<p>2</p>", "bob said:</div><blockquote>my quote</blockquote></aside></p>\n\n<p>2</p>",
"handles quotes properly"); "handles quotes properly");
@ -138,7 +141,7 @@ test("Mentions", function() {
"handles mentions in simple quotes"); "handles mentions in simple quotes");
cooked("> foo bar baz @eviltrout ohmagerd\nlook at this", cooked("> foo bar baz @eviltrout ohmagerd\nlook at this",
"<blockquote><p>foo bar baz <span class=\"mention\">@eviltrout</span> ohmagerd\nlook at this</p></blockquote>", "<blockquote><p>foo bar baz <span class=\"mention\">@eviltrout</span> ohmagerd<br>look at this</p></blockquote>",
"does mentions properly with trailing text within a simple quote"); "does mentions properly with trailing text within a simple quote");
cooked("`code` is okay before @mention", cooked("`code` is okay before @mention",
@ -162,7 +165,7 @@ test("Mentions", function() {
"you can have a mention in an inline code block following a real mention."); "you can have a mention in an inline code block following a real mention.");
cooked("1. this is a list\n\n2. this is an @eviltrout mention\n", cooked("1. this is a list\n\n2. this is an @eviltrout mention\n",
"<ol><li><p>this is a list</p></li><li><p>this is an <span class=\"mention\">@eviltrout</span> mention </p></li></ol>", "<ol><li><p>this is a list</p></li><li><p>this is an <span class=\"mention\">@eviltrout</span> mention</p></li></ol>",
"it mentions properly in a list."); "it mentions properly in a list.");
cookedOptions("@eviltrout", alwaysTrue, cookedOptions("@eviltrout", alwaysTrue,
@ -202,7 +205,7 @@ test("Code Blocks", function() {
"it supports basic code blocks"); "it supports basic code blocks");
cooked("```json\n{hello: 'world'}\n```\ntrailing", cooked("```json\n{hello: 'world'}\n```\ntrailing",
"<p><pre><code class=\"json\">{hello: &#x27;world&#x27;}</code></pre></p>\n\n<p>\ntrailing</p>", "<p><pre><code class=\"json\">{hello: &#x27;world&#x27;}</code></pre></p>\n\n<p>trailing</p>",
"It does not truncate text after a code block."); "It does not truncate text after a code block.");
cooked("```json\nline 1\n\nline 2\n\n\nline3\n```", cooked("```json\nline 1\n\nline 2\n\n\nline3\n```",