Better API for parsing out blocks in the parser.

This commit is contained in:
Robin Ward 2013-08-29 11:38:51 -04:00
parent bbd79aafd1
commit 3cec95a2c3
6 changed files with 107 additions and 181 deletions

View file

@ -102,70 +102,26 @@ replaceBBCodeParams("color", function(param, contents) {
} }
}); });
Discourse.Dialect.on("register", function(event) { // Handles `[code] ... [/code]` blocks
Discourse.Dialect.replaceBlock({
start: /(\[code\])([\s\S]*)/igm,
stop: '[/code]',
var dialect = event.dialect, emitter: function(blockContents) {
MD = event.MD; return ['p', ['pre'].concat(blockContents)];
}
/**
Support BBCode [code] blocks
@method bbcodeCode
@param {Markdown.Block} block the block to examine
@param {Array} next the next blocks in the sequence
@return {Array} the JsonML containing the markup or undefined if nothing changed.
@namespace Discourse.Dialect
**/
dialect.inline["[code]"] = function bbcodeCode(text, orig_match) {
var bbcodePattern = new RegExp("\\[code\\]([\\s\\S]*?)\\[\\/code\\]", "igm"),
m = bbcodePattern.exec(text);
if (m) {
var contents = m[1].trim().split("\n");
var html = ['pre', "\n"];
contents.forEach(function (n) {
html.push(n.trim());
html.push(["br"]);
html.push("\n");
}); });
return [m[0].length, html]; // Support BBCode [quote] blocks
} Discourse.Dialect.replaceBlock({
}; start: new RegExp("\\[quote=?([^\\[\\]]+)?\\]([\\s\\S]*)", "igm"),
stop: '[/quote]',
emitter: function(blockContents, matches, options) {
/** var paramsString = matches[1].replace(/\"/g, ''),
Support BBCode [quote] blocks
@method bbcodeQuote
@param {Markdown.Block} block the block to examine
@param {Array} next the next blocks in the sequence
@return {Array} the JsonML containing the markup or undefined if nothing changed.
@namespace Discourse.Dialect
**/
dialect.block['quote'] = function bbcodeQuote(block, next) {
var m = new RegExp("\\[quote=?([^\\[\\]]+)?\\]([\\s\\S]*)", "igm").exec(block);
if (m) {
var paramsString = m[1].replace(/\"/g, ''),
params = {'class': 'quote'}, params = {'class': 'quote'},
paramsSplit = paramsString.split(/\, */), paramsSplit = paramsString.split(/\, */),
username = paramsSplit[0], username = paramsSplit[0];
opts = dialect.options,
startPos = block.indexOf(m[0]),
leading,
quoteContents = [],
result = [];
if (startPos > 0) {
leading = block.slice(0, startPos);
var para = ['p'];
this.processInline(leading).forEach(function (l) {
para.push(l);
});
result.push(para);
}
paramsSplit.forEach(function(p,i) { paramsSplit.forEach(function(p,i) {
if (i > 0) { if (i > 0) {
@ -177,53 +133,30 @@ Discourse.Dialect.on("register", function(event) {
}); });
var avatarImg; var avatarImg;
if (opts.lookupAvatarByPostNumber) { if (options.lookupAvatarByPostNumber) {
// client-side, we can retrieve the avatar from the post // client-side, we can retrieve the avatar from the post
var postNumber = parseInt(params['data-post'], 10); var postNumber = parseInt(params['data-post'], 10);
avatarImg = opts.lookupAvatarByPostNumber(postNumber); avatarImg = options.lookupAvatarByPostNumber(postNumber);
} else if (opts.lookupAvatar) { } else if (options.lookupAvatar) {
// server-side, we need to lookup the avatar from the username // server-side, we need to lookup the avatar from the username
avatarImg = opts.lookupAvatar(username); avatarImg = options.lookupAvatar(username);
} }
if (m[2]) { next.unshift(MD.mk_block(m[2])); } var contents = this.processInline(blockContents.join(" \n \n"));
while (next.length > 0) {
var b = next.shift(),
n = b.match(/([\s\S]*)\[\/quote\]([\s\S]*)/m);
if (n) {
if (n[2]) {
next.unshift(MD.mk_block(n[2]));
}
quoteContents.push(n[1]);
break;
} else {
quoteContents.push(b);
}
}
var contents = this.processInline(quoteContents.join(" \n \n"));
contents.unshift('blockquote'); contents.unshift('blockquote');
return ['p', ['aside', params,
result.push(['p', ['aside', params,
['div', {'class': 'title'}, ['div', {'class': 'title'},
['div', {'class': 'quote-controls'}], ['div', {'class': 'quote-controls'}],
avatarImg ? avatarImg : "", avatarImg ? avatarImg : "",
I18n.t('user.said', {username: username}) I18n.t('user.said', {username: username})
], ],
contents contents
]]); ]];
return result;
} }
};
}); });
Discourse.Dialect.on("parseNode", function(event) { Discourse.Dialect.on("parseNode", function(event) {
var node = event.node, var node = event.node,
path = event.path; path = event.path;

View file

@ -204,6 +204,64 @@ Discourse.Dialect = {
}; };
}, },
replaceBlock: function(args) {
dialect.block[args.start.toString()] = function(block, next) {
args.start.lastIndex = 0;
var m = (args.start).exec(block);
if (!m) { return; }
var startPos = block.indexOf(m[0]),
leading,
blockContents = [],
result = [],
lineNumber = block.lineNumber;
if (startPos > 0) {
leading = block.slice(0, startPos);
lineNumber += (leading.split("\n").length - 1);
var para = ['p'];
this.processInline(leading).forEach(function (l) {
para.push(l);
});
result.push(para);
}
if (m[2]) { next.unshift(MD.mk_block(m[2], null, lineNumber + 1)); }
lineNumber++;
while (next.length > 0) {
var b = next.shift(),
blockLine = b.lineNumber,
diff = ((typeof blockLine === "undefined") ? lineNumber : blockLine) - lineNumber;
var endFound = b.indexOf(args.stop),
leadingContents = b.slice(0, endFound),
trailingContents = b.slice(endFound+args.stop.length);
for (var i=1; i<diff; i++) {
blockContents.push("");
}
lineNumber = blockLine + b.split("\n").length - 1;
if (endFound !== -1) {
if (trailingContents) {
next.unshift(MD.mk_block(trailingContents));
}
blockContents.push(leadingContents.replace(/\s+$/, ""));
break;
} else {
blockContents.push(b);
}
}
var test = args.emitter.call(this, blockContents, m, dialect.options);
result.push(test);
return result;
};
},
/** /**
After the parser has been executed, post process any text nodes in the HTML document. After the parser has been executed, post process any text nodes in the HTML document.
This is useful if you want to apply a transformation to the text. This is useful if you want to apply a transformation to the text.

View file

@ -5,77 +5,12 @@
@event register @event register
@namespace Discourse.Dialect @namespace Discourse.Dialect
**/ **/
Discourse.Dialect.on("register", function(event) { Discourse.Dialect.replaceBlock({
var dialect = event.dialect, start: /^`{3}([^\n]+)?\n?([\s\S]*)?/gm,
MD = event.MD; stop: '```',
emitter: function(blockContents, matches) {
/** return ['p', ['pre', ['code', {'class': matches[1] || 'lang-auto'}, blockContents.join("\n") ]]];
Support for github style code blocks
@method githubCode
@param {Markdown.Block} block the block to examine
@param {Array} next the next blocks in the sequence
@return {Array} the JsonML containing the markup or undefined if nothing changed.
@namespace Discourse.Dialect
**/
dialect.block.github_code = function githubCode(block, next) {
var m = /^`{3}([^\n]+)?\n?([\s\S]*)?/gm.exec(block);
if (m) {
var startPos = block.indexOf(m[0]),
leading,
codeContents = [],
result = [],
lineNumber = block.lineNumber;
if (startPos > 0) {
leading = block.slice(0, startPos);
lineNumber += (leading.split("\n").length - 1);
var para = ['p'];
this.processInline(leading).forEach(function (l) {
para.push(l);
});
result.push(para);
} }
if (m[2]) { next.unshift(MD.mk_block(m[2], null, lineNumber + 1)); }
lineNumber++;
while (next.length > 0) {
var b = next.shift(),
blockLine = b.lineNumber,
diff = ((typeof blockLine === "undefined") ? lineNumber : blockLine) - lineNumber;
var endFound = b.indexOf('```'),
leadingCode = b.slice(0, endFound),
trailingCode = b.slice(endFound+3);
for (var i=1; i<diff; i++) {
codeContents.push("");
}
lineNumber = blockLine + b.split("\n").length - 1;
if (endFound !== -1) {
if (trailingCode) {
next.unshift(MD.mk_block(trailingCode));
}
codeContents.push(leadingCode.replace(/\s+$/, ""));
break;
} else {
codeContents.push(b);
}
}
result.push(['p', ['pre', ['code', {'class': m[1] || 'lang-auto'}, codeContents.join("\n") ]]]);
return result;
}
};
}); });
// Ensure that content in a code block is fully escaped. This way it's not white listed // Ensure that content in a code block is fully escaped. This way it's not white listed

View file

@ -14,7 +14,7 @@ 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\">EvilTrout said:</div>\n<blockquote>ddd<br>\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</blockquote></aside></p>"
end end
it "should produce a quote" do it "should produce a quote" do

View file

@ -11,8 +11,8 @@ test('basic bbcode', function() {
format("[i]emphasis[/i]", "<span class=\"bbcode-i\">emphasis</span>", "italics text"); format("[i]emphasis[/i]", "<span class=\"bbcode-i\">emphasis</span>", "italics text");
format("[u]underlined[/u]", "<span class=\"bbcode-u\">underlined</span>", "underlines text"); format("[u]underlined[/u]", "<span class=\"bbcode-u\">underlined</span>", "underlines text");
format("[s]strikethrough[/s]", "<span class=\"bbcode-s\">strikethrough</span>", "strikes-through text"); format("[s]strikethrough[/s]", "<span class=\"bbcode-s\">strikethrough</span>", "strikes-through text");
format("[code]\nx++\n[/code]", "<pre>\nx++<br/>\n</pre>", "makes code into pre"); format("[code]\nx++\n[/code]", "<pre>\nx++</pre>", "makes code into pre");
format("[code]\nx++\ny++\nz++\n[/code]", "<pre>\nx++<br/>\ny++<br/>\nz++<br/>\n</pre>", "makes code into pre"); format("[code]\nx++\ny++\nz++\n[/code]", "<pre>\nx++\ny++\nz++</pre>", "makes code into pre");
format("[spoiler]it's a sled[/spoiler]", "<span class=\"spoiler\">it's a sled</span>", "supports spoiler tags"); format("[spoiler]it's a sled[/spoiler]", "<span class=\"spoiler\">it's a sled</span>", "supports spoiler tags");
format("[img]http://eviltrout.com/eviltrout.png[/img]", "<img src=\"http://eviltrout.com/eviltrout.png\"/>", "links images"); format("[img]http://eviltrout.com/eviltrout.png[/img]", "<img src=\"http://eviltrout.com/eviltrout.png\"/>", "links images");
format("[url]http://bettercallsaul.com[/url]", "<a href=\"http://bettercallsaul.com\">http://bettercallsaul.com</a>", "supports [url] without a title"); format("[url]http://bettercallsaul.com[/url]", "<a href=\"http://bettercallsaul.com\">http://bettercallsaul.com</a>", "supports [url] without a title");

View file

@ -114,7 +114,7 @@ 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>" + "<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</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",