2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2025-09-06 10:50:21 +08:00

Better API for adding on to our Dialect

This commit is contained in:
Robin Ward 2013-08-27 12:52:00 -04:00
parent 92d7953dd0
commit 8f94760cd4
7 changed files with 265 additions and 242 deletions

View file

@ -1,45 +1,19 @@
/** /**
This addition handles auto linking of text. When included, it will parse out links and create This addition handles auto linking of text. When included, it will parse out links and create
a hrefs for them. a hrefs for them.
@event register
@namespace Discourse.Dialect
**/ **/
Discourse.Dialect.on("register", function(event) { var urlReplacerArgs = {
matcher: /(^|\s)((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
spaceBoundary: true,
var dialect = event.dialect, emitter: function(matches) {
MD = event.MD; var url = matches[2],
displayUrl = url;
/**
Parses out links from HTML.
@method autoLink
@param {String} text the text match
@param {Array} match the match found
@param {Array} prev the previous jsonML
@return {Array} an array containing how many chars we've replaced and the jsonML content for it.
@namespace Discourse.Dialect
**/
dialect.inline['http'] = dialect.inline['www'] = function autoLink(text, match, prev) {
// We only care about links on boundaries
if (prev && (prev.length > 0)) {
var last = prev[prev.length - 1];
if (typeof last === "string" && (!last.match(/\s$/))) { return; }
}
var pattern = /(^|\s)((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
m = pattern.exec(text);
if (m) {
var url = m[2],
displayUrl = m[2];
if (url.match(/^www/)) { url = "http://" + url; } if (url.match(/^www/)) { url = "http://" + url; }
return [m[0].length, ['a', {href: url}, displayUrl]]; return ['a', {href: url}, displayUrl];
} }
}; };
Discourse.Dialect.inlineRegexp(_.merge({start: 'http'}, urlReplacerArgs));
}); Discourse.Dialect.inlineRegexp(_.merge({start: 'www'}, urlReplacerArgs));

View file

@ -1,76 +1,112 @@
/** /**
Regsiter all functionality for supporting BBCode in Discourse. Create a simple BBCode tag handler
@event register @method replaceBBCode
@namespace Discourse.Dialect @param {tag} tag the tag we want to match
@param {function} emitter the function that creates JsonML for the tag
**/ **/
function replaceBBCode(tag, emitter) {
Discourse.Dialect.inlineReplace({
start: "[" + tag + "]",
stop: "[/" + tag + "]",
emitter: emitter
});
}
/**
Creates a BBCode handler that accepts parameters. Passes them to the emitter.
@method replaceBBCodeParamsRaw
@param {tag} tag the tag we want to match
@param {function} emitter the function that creates JsonML for the tag
**/
function replaceBBCodeParamsRaw(tag, emitter) {
Discourse.Dialect.inlineReplace({
start: "[" + tag + "=",
stop: "[/" + tag + "]",
rawContents: true,
emitter: function(contents) {
var regexp = /^([^\]]+)\](.*)$/,
m = regexp.exec(contents);
if (m) { return emitter.call(this, m[1], m[2]); }
}
});
}
/**
Creates a BBCode handler that accepts parameters. Passes them to the emitter.
Processes the inside recursively so it can be nested.
@method replaceBBCodeParams
@param {tag} tag the tag we want to match
@param {function} emitter the function that creates JsonML for the tag
**/
function replaceBBCodeParams(tag, emitter) {
replaceBBCodeParamsRaw(tag, function (param, contents) {
return emitter(param, this.processInline(contents));
});
}
replaceBBCode('b', function(contents) { return ['span', {'class': 'bbcode-b'}].concat(contents); });
replaceBBCode('i', function(contents) { return ['span', {'class': 'bbcode-i'}].concat(contents); });
replaceBBCode('u', function(contents) { return ['span', {'class': 'bbcode-u'}].concat(contents); });
replaceBBCode('s', function(contents) { return ['span', {'class': 'bbcode-s'}].concat(contents); });
replaceBBCode('ul', function(contents) { return ['ul'].concat(contents); });
replaceBBCode('ol', function(contents) { return ['ol'].concat(contents); });
replaceBBCode('li', function(contents) { return ['li'].concat(contents); });
replaceBBCode('spoiler', function(contents) { return ['span', {'class': 'spoiler'}].concat(contents); });
Discourse.Dialect.inlineReplace({
start: '[img]',
stop: '[/img]',
rawContents: true,
emitter: function(contents) { return ['img', {href: contents}]; }
});
Discourse.Dialect.inlineReplace({
start: '[email]',
stop: '[/email]',
rawContents: true,
emitter: function(contents) { return ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]; }
});
Discourse.Dialect.inlineReplace({
start: '[url]',
stop: '[/url]',
rawContents: true,
emitter: function(contents) { return ['a', {href: contents, 'data-bbcode': true}, contents]; }
});
replaceBBCodeParamsRaw("url", function(param, contents) {
return ['a', {href: param, 'data-bbcode': true}, contents];
});
replaceBBCodeParamsRaw("email", function(param, contents) {
return ['a', {href: "mailto:" + param, 'data-bbcode': true}, contents];
});
replaceBBCodeParams("size", function(param, contents) {
return ['span', {'class': "bbcode-size-" + param}].concat(contents);
});
replaceBBCodeParams("color", function(param, contents) {
// Only allow valid HTML colors.
if (/^(\#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)|(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)$/.test(param)) {
return ['span', {style: "color: " + param}].concat(contents);
} else {
return ['span'].concat(contents);
}
});
Discourse.Dialect.on("register", function(event) { Discourse.Dialect.on("register", function(event) {
var dialect = event.dialect, var dialect = event.dialect,
MD = event.MD; MD = event.MD;
var createBBCode = function(tag, builder, hasArgs) {
return function(text, orig_match) {
var bbcodePattern = new RegExp("\\[" + tag + "=?([^\\[\\]]+)?\\]([\\s\\S]*?)\\[\\/" + tag + "\\]", "igm");
var m = bbcodePattern.exec(text);
if (m && m[0]) {
return [m[0].length, builder(m, this)];
}
};
};
var bbcodes = {'b': ['span', {'class': 'bbcode-b'}],
'i': ['span', {'class': 'bbcode-i'}],
'u': ['span', {'class': 'bbcode-u'}],
's': ['span', {'class': 'bbcode-s'}],
'spoiler': ['span', {'class': 'spoiler'}],
'li': ['li'],
'ul': ['ul'],
'ol': ['ol']};
Object.keys(bbcodes).forEach(function(tag) {
var element = bbcodes[tag];
dialect.inline["[" + tag + "]"] = createBBCode(tag, function(m, self) {
return element.concat(self.processInline(m[2]));
});
});
dialect.inline["[img]"] = createBBCode('img', function(m) {
return ['img', {href: m[2]}];
});
dialect.inline["[email]"] = createBBCode('email', function(m) {
return ['a', {href: "mailto:" + m[2], 'data-bbcode': true}, m[2]];
});
dialect.inline["[url]"] = createBBCode('url', function(m) {
return ['a', {href: m[2], 'data-bbcode': true}, m[2]];
});
dialect.inline["[url="] = createBBCode('url', function(m, self) {
return ['a', {href: m[1], 'data-bbcode': true}].concat(self.processInline(m[2]));
});
dialect.inline["[email="] = createBBCode('email', function(m, self) {
return ['a', {href: "mailto:" + m[1], 'data-bbcode': true}].concat(self.processInline(m[2]));
});
dialect.inline["[size="] = createBBCode('size', function(m, self) {
return ['span', {'class': "bbcode-size-" + m[1]}].concat(self.processInline(m[2]));
});
dialect.inline["[color="] = function(text, orig_match) {
var bbcodePattern = new RegExp("\\[color=?([^\\[\\]]+)?\\]([\\s\\S]*?)\\[\\/color\\]", "igm"),
m = bbcodePattern.exec(text);
if (m && m[0]) {
if (!/^(\#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)|(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)$/.test(m[1])) {
return [m[0].length].concat(this.processInline(m[2]));
}
return [m[0].length, ['span', {style: "color: " + m[1]}].concat(this.processInline(m[2]))];
}
};
/** /**
Support BBCode [code] blocks Support BBCode [code] blocks

View file

@ -1,43 +1,25 @@
/** /**
Markdown.js doesn't seem to do bold and italics at the same time if you surround code with markdown-js doesn't ensure that em/strong codes are present on word boundaries.
three asterisks. This adds that support. So we create our own handlers here.
@event register
@namespace Discourse.Dialect
**/ **/
Discourse.Dialect.on("register", function(event) {
var dialect = event.dialect,
MD = event.MD;
var inlineBuilder = function(symbol, tag, surround) {
return function(text, match, prev) {
if (prev && (prev.length > 0)) {
var last = prev[prev.length - 1];
if (typeof last === "string" && (!last.match(/\W$/))) { return; }
}
var regExp = new RegExp("^\\" + symbol + "([^\\" + symbol + "]+)" + "\\" + symbol, "igm"),
m = regExp.exec(text);
if (m) {
var contents = [tag].concat(this.processInline(m[1]));
if (surround) {
contents = [surround, contents];
}
return [m[0].length, contents];
}
};
};
dialect.inline['***'] = inlineBuilder('**', 'em', 'strong');
dialect.inline['**'] = inlineBuilder('**', 'strong');
dialect.inline['*'] = inlineBuilder('*', 'em');
dialect.inline['_'] = inlineBuilder('_', 'em');
// Support for simultaneous bold and italics
Discourse.Dialect.inlineReplace({
between: '***',
wordBoundary: true,
emitter: function(contents) { return ['strong', ['em'].concat(contents)]; }
}); });
// Builds a common markdown replacer
var replaceMarkdown = function(match, tag) {
Discourse.Dialect.inlineReplace({
between: match,
wordBoundary: true,
emitter: function(contents) { return [tag].concat(contents) }
});
};
replaceMarkdown('**', 'strong');
replaceMarkdown('*', 'em');
replaceMarkdown('_', 'em');

View file

@ -43,23 +43,20 @@
**/ **/
var parser = window.BetterMarkdown, var parser = window.BetterMarkdown,
MD = parser.Markdown, MD = parser.Markdown,
// Our dialect
dialect = MD.dialects.Discourse = MD.subclassDialect( MD.dialects.Gruber ), dialect = MD.dialects.Discourse = MD.subclassDialect( MD.dialects.Gruber ),
initialized = false;
initialized = false,
/** /**
Initialize our dialects for processing. Initialize our dialects for processing.
@method initializeDialects @method initializeDialects
**/ **/
initializeDialects = function() { function initializeDialects() {
Discourse.Dialect.trigger('register', {dialect: dialect, MD: MD}); Discourse.Dialect.trigger('register', {dialect: dialect, MD: MD});
MD.buildBlockOrder(dialect.block); MD.buildBlockOrder(dialect.block);
MD.buildInlinePatterns(dialect.inline); MD.buildInlinePatterns(dialect.inline);
initialized = true; initialized = true;
}, }
/** /**
Parse a JSON ML tree, using registered handlers to adjust it if necessary. Parse a JSON ML tree, using registered handlers to adjust it if necessary.
@ -70,7 +67,7 @@ var parser = window.BetterMarkdown,
@param {Object} insideCounts counts what tags we're inside @param {Object} insideCounts counts what tags we're inside
@returns {Array} the parsed tree @returns {Array} the parsed tree
**/ **/
parseTree = function parseTree(tree, path, insideCounts) { function parseTree(tree, path, insideCounts) {
if (tree instanceof Array) { if (tree instanceof Array) {
Discourse.Dialect.trigger('parseNode', {node: tree, path: path, dialect: dialect, insideCounts: insideCounts || {}}); Discourse.Dialect.trigger('parseNode', {node: tree, path: path, dialect: dialect, insideCounts: insideCounts || {}});
@ -87,7 +84,26 @@ var parser = window.BetterMarkdown,
path.pop(); path.pop();
} }
return tree; return tree;
}; }
/**
Returns true if there's an invalid word boundary for a match.
@method invalidBoundary
@param {Object} args our arguments, including whether we care about boundaries
@param {Array} prev the previous content, if exists
@returns {Boolean} whether there is an invalid word boundary
**/
function invalidBoundary(args, prev) {
if (!args.wordBoundary && !args.spaceBoundary) { return; }
var last = prev[prev.length - 1];
if (typeof last !== "string") { return; }
if (args.wordBoundary && (!last.match(/\W$/))) { return true; }
if (args.spaceBoundary && (!last.match(/\s$/))) { return true; }
}
/** /**
An object used for rendering our dialects. An object used for rendering our dialects.
@ -110,7 +126,51 @@ Discourse.Dialect = {
dialect.options = opts; dialect.options = opts;
var tree = parser.toHTMLTree(text, 'Discourse'); var tree = parser.toHTMLTree(text, 'Discourse');
return parser.renderJsonML(parseTree(tree)); return parser.renderJsonML(parseTree(tree));
},
inlineRegexp: function(args) {
dialect.inline[args.start] = function(text, match, prev) {
if (invalidBoundary(args, prev)) { return; }
args.matcher.lastIndex = 0;
var m = args.matcher.exec(text);
if (m) {
var result = args.emitter.call(this, m);
if (result) {
return [m[0].length, result];
}
}
};
},
inlineReplace: function(args) {
var start = args.start || args.between,
stop = args.stop || args.between,
startLength = start.length;
dialect.inline[start] = function(text, match, prev) {
if (invalidBoundary(args, prev)) { return; }
var endPos = text.indexOf(stop, startLength);
if (endPos === -1) { return; }
var between = text.slice(startLength, endPos);
// If rawcontents is set, don't process inline
if (!args.rawContents) {
between = this.processInline(between);
}
var contents = args.emitter.call(this, between);
if (contents) {
return [endPos + startLength + 1, contents];
} }
}; };
}
};
RSVP.EventTarget.mixin(Discourse.Dialect); RSVP.EventTarget.mixin(Discourse.Dialect);

View file

@ -2,47 +2,20 @@
Supports Discourse's custom @mention syntax for calling out a user in a post. Supports Discourse's custom @mention syntax for calling out a user in a post.
It will add a special class to them, and create a link if the user is found in a It will add a special class to them, and create a link if the user is found in a
local map. local map.
@event register
@namespace Discourse.Dialect
**/ **/
Discourse.Dialect.on("register", function(event) { Discourse.Dialect.inlineRegexp({
start: '@',
matcher: /^(@[A-Za-z0-9][A-Za-z0-9_]{2,14})/m,
wordBoundary: true,
var dialect = event.dialect, emitter: function(matches) {
MD = event.MD; var username = matches[1],
mentionLookup = this.dialect.options.mentionLookup || Discourse.Mention.lookupCache;
/**
Parses out @username mentions.
@method parseMentions
@param {String} text the text match
@param {Array} match the match found
@param {Array} prev the previous jsonML
@return {Array} an array containing how many chars we've replaced and the jsonML content for it.
@namespace Discourse.Dialect
**/
dialect.inline['@'] = function parseMentions(text, match, prev) {
// We only care about mentions on word boundaries
if (prev && (prev.length > 0)) {
var last = prev[prev.length - 1];
if (typeof last === "string" && (!last.match(/\W$/))) { return; }
}
var pattern = /^(@[A-Za-z0-9][A-Za-z0-9_]{2,14})(?=(\W|$))/m,
m = pattern.exec(text);
if (m) {
var username = m[1],
mentionLookup = dialect.options.mentionLookup || Discourse.Mention.lookupCache;
if (mentionLookup(username.substr(1))) { if (mentionLookup(username.substr(1))) {
return [username.length, ['a', {'class': 'mention', href: Discourse.getURL("/users/") + username.substr(1).toLowerCase()}, username]]; return ['a', {'class': 'mention', href: Discourse.getURL("/users/") + username.substr(1).toLowerCase()}, username];
} else { } else {
return [username.length, ['span', {'class': 'mention'}, username]]; return ['span', {'class': 'mention'}, username];
} }
} }
};
}); });

View file

@ -10,16 +10,12 @@ Discourse.Dialect.on("parseNode", function(event) {
insideCounts = event.insideCounts, insideCounts = event.insideCounts,
linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks; linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks;
if (!linebreaks) { if (linebreaks || (insideCounts.pre > 0) || (node.length < 1)) { return; }
// 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++) { for (var j=1; j<node.length; j++) {
var textContent = node[j]; var textContent = node[j];
if (typeof textContent === "string") { if (typeof textContent === "string") {
if (textContent === "\n") { if (textContent === "\n") {
node[j] = ['br']; node[j] = ['br'];
} else { } else {
@ -37,6 +33,5 @@ Discourse.Dialect.on("parseNode", function(event) {
} }
} }
} }
}
}
}); });

View file

@ -17,6 +17,9 @@ test('basic bbcode', function() {
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");
format("[email]eviltrout@mailinator.com[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">eviltrout@mailinator.com</a>", "supports [email] without a title"); format("[email]eviltrout@mailinator.com[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">eviltrout@mailinator.com</a>", "supports [email] without a title");
format("[b]evil [i]trout[/i][/b]",
"<span class=\"bbcode-b\">evil <span class=\"bbcode-i\">trout</span></span>",
"allows embedding of tags");
}); });
test('lists', function() { test('lists', function() {
@ -28,7 +31,7 @@ test('color', function() {
format("[color=#00f]blue[/color]", "<span style=\"color: #00f\">blue</span>", "supports [color=] with a short hex value"); format("[color=#00f]blue[/color]", "<span style=\"color: #00f\">blue</span>", "supports [color=] with a short hex value");
format("[color=#ffff00]yellow[/color]", "<span style=\"color: #ffff00\">yellow</span>", "supports [color=] with a long hex value"); format("[color=#ffff00]yellow[/color]", "<span style=\"color: #ffff00\">yellow</span>", "supports [color=] with a long hex value");
format("[color=red]red[/color]", "<span style=\"color: red\">red</span>", "supports [color=] with an html color"); format("[color=red]red[/color]", "<span style=\"color: red\">red</span>", "supports [color=] with an html color");
format("[color=javascript:alert('wat')]noop[/color]", "noop", "it performs a noop on invalid input"); format("[color=javascript:alert('wat')]noop[/color]", "<span>noop</span>", "it performs a noop on invalid input");
}); });
test('tags with arguments', function() { test('tags with arguments', function() {