mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 08:04:03 +08:00
Replace Markdown parser.
This commit is contained in:
parent
0d2b4aeb61
commit
7f69a58439
|
@ -1,297 +0,0 @@
|
||||||
/*global HANDLEBARS_TEMPLATES:true md5:true*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
Support for BBCode rendering
|
|
||||||
|
|
||||||
@class BBCode
|
|
||||||
@namespace Discourse
|
|
||||||
@module Discourse
|
|
||||||
**/
|
|
||||||
Discourse.BBCode = {
|
|
||||||
|
|
||||||
QUOTE_REGEXP: /\[quote=([^\]]*)\]((?:[\s\S](?!\[quote=[^\]]*\]))*?)\[\/quote\]/im,
|
|
||||||
IMG_REGEXP: /\[img\]([\s\S]*?)\[\/img\]/i,
|
|
||||||
URL_REGEXP: /\[url\]([\s\S]*?)\[\/url\]/i,
|
|
||||||
URL_WITH_TITLE_REGEXP: /\[url=(.+?)\]([\s\S]*?)\[\/url\]/i,
|
|
||||||
|
|
||||||
// Define our replacers
|
|
||||||
replacers: {
|
|
||||||
base: {
|
|
||||||
withoutArgs: {
|
|
||||||
"ol": function(_, content) { return "<ol>" + content + "</ol>"; },
|
|
||||||
"li": function(_, content) { return "<li>" + content + "</li>"; },
|
|
||||||
"ul": function(_, content) { return "<ul>" + content + "</ul>"; },
|
|
||||||
"code": function(_, content) { return "<pre>" + content + "</pre>"; },
|
|
||||||
"url": function(_, url) { return "<a href=\"" + url + "\">" + url + "</a>"; },
|
|
||||||
"email": function(_, address) { return "<a href=\"mailto:" + address + "\">" + address + "</a>"; },
|
|
||||||
"img": function(_, src) { return "<img src=\"" + src + "\">"; }
|
|
||||||
},
|
|
||||||
withArgs: {
|
|
||||||
"url": function(_, href, title) { return "<a href=\"" + href + "\">" + title + "</a>"; },
|
|
||||||
"email": function(_, address, title) { return "<a href=\"mailto:" + address + "\">" + title + "</a>"; },
|
|
||||||
"color": function(_, color, content) {
|
|
||||||
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(color)) {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
return "<span style=\"color: " + color + "\">" + content + "</span>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// For HTML emails
|
|
||||||
email: {
|
|
||||||
withoutArgs: {
|
|
||||||
"b": function(_, content) { return "<b>" + content + "</b>"; },
|
|
||||||
"i": function(_, content) { return "<i>" + content + "</i>"; },
|
|
||||||
"u": function(_, content) { return "<u>" + content + "</u>"; },
|
|
||||||
"s": function(_, content) { return "<s>" + content + "</s>"; },
|
|
||||||
"spoiler": function(_, content) { return "<span style='background-color: #000'>" + content + "</span>"; }
|
|
||||||
},
|
|
||||||
withArgs: {
|
|
||||||
"size": function(_, size, content) { return "<span style=\"font-size: " + size + "px\">" + content + "</span>"; }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// For sane environments that support CSS
|
|
||||||
"default": {
|
|
||||||
withoutArgs: {
|
|
||||||
"b": function(_, content) { return "<span class='bbcode-b'>" + content + "</span>"; },
|
|
||||||
"i": function(_, content) { return "<span class='bbcode-i'>" + content + "</span>"; },
|
|
||||||
"u": function(_, content) { return "<span class='bbcode-u'>" + content + "</span>"; },
|
|
||||||
"s": function(_, content) { return "<span class='bbcode-s'>" + content + "</span>"; },
|
|
||||||
"spoiler": function(_, content) { return "<span class=\"spoiler\">" + content + "</span>";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
withArgs: {
|
|
||||||
"size": function(_, size, content) { return "<span class=\"bbcode-size-" + size + "\">" + content + "</span>"; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Apply a particular set of replacers
|
|
||||||
|
|
||||||
@method apply
|
|
||||||
@param {String} text The text we want to format
|
|
||||||
@param {String} environment The environment in which this
|
|
||||||
**/
|
|
||||||
apply: function(text, environment) {
|
|
||||||
var replacer = Discourse.BBCode.parsedReplacers()[environment];
|
|
||||||
// apply all available replacers
|
|
||||||
replacer.forEach(function(r) {
|
|
||||||
text = text.replace(r.regexp, r.fn);
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Lazy parse replacers
|
|
||||||
|
|
||||||
@property parsedReplacers
|
|
||||||
**/
|
|
||||||
parsedReplacers: function() {
|
|
||||||
if (this.parsed) return this.parsed;
|
|
||||||
|
|
||||||
var result = {};
|
|
||||||
|
|
||||||
_.each(Discourse.BBCode.replacers, function(rules, name) {
|
|
||||||
|
|
||||||
var parsed = result[name] = [];
|
|
||||||
|
|
||||||
_.each(_.extend(Discourse.BBCode.replacers.base.withoutArgs, rules.withoutArgs), function(val, tag) {
|
|
||||||
parsed.push({ regexp: new RegExp("\\[" + tag + "\\]([\\s\\S]*?)\\[\\/" + tag + "\\]", "igm"), fn: val });
|
|
||||||
});
|
|
||||||
|
|
||||||
_.each(_.extend(Discourse.BBCode.replacers.base.withArgs, rules.withArgs), function(val, tag) {
|
|
||||||
parsed.push({ regexp: new RegExp("\\[" + tag + "=?(.+?)\\]([\\s\\S]*?)\\[\\/" + tag + "\\]", "igm"), fn: val });
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
this.parsed = result;
|
|
||||||
return this.parsed;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Build the BBCode quote around the selected text
|
|
||||||
|
|
||||||
@method buildQuoteBBCode
|
|
||||||
@param {Discourse.Post} post The post we are quoting
|
|
||||||
@param {String} contents The text selected
|
|
||||||
**/
|
|
||||||
buildQuoteBBCode: function(post, contents) {
|
|
||||||
var contents_hashed, result, sansQuotes, stripped, stripped_hashed, tmp;
|
|
||||||
if (!contents) contents = "";
|
|
||||||
|
|
||||||
sansQuotes = contents.replace(this.QUOTE_REGEXP, '').trim();
|
|
||||||
if (sansQuotes.length === 0) return "";
|
|
||||||
|
|
||||||
result = "[quote=\"" + (post.get('username')) + ", post:" + (post.get('post_number')) + ", topic:" + (post.get('topic_id'));
|
|
||||||
|
|
||||||
/* Strip the HTML from cooked */
|
|
||||||
tmp = document.createElement('div');
|
|
||||||
tmp.innerHTML = post.get('cooked');
|
|
||||||
stripped = tmp.textContent || tmp.innerText;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Let's remove any non alphanumeric characters as a kind of hash. Yes it's
|
|
||||||
not accurate but it should work almost every time we need it to. It would be unlikely
|
|
||||||
that the user would quote another post that matches in exactly this way.
|
|
||||||
*/
|
|
||||||
stripped_hashed = stripped.replace(/[^a-zA-Z0-9]/g, '');
|
|
||||||
contents_hashed = contents.replace(/[^a-zA-Z0-9]/g, '');
|
|
||||||
|
|
||||||
/* If the quote is the full message, attribute it as such */
|
|
||||||
if (stripped_hashed === contents_hashed) result += ", full:true";
|
|
||||||
result += "\"]\n" + sansQuotes + "\n[/quote]\n\n";
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
We want to remove urls in BBCode tags from a string before applying markdown
|
|
||||||
to prevent them from being modified by markdown.
|
|
||||||
This will return an object that contains:
|
|
||||||
- a new version of the text with the urls replaced with unique ids
|
|
||||||
- a `template()` function for reapplying them later.
|
|
||||||
|
|
||||||
@method extractUrls
|
|
||||||
@param {String} text The text inside which we want to replace urls
|
|
||||||
@returns {Object} object containing the new string and template function
|
|
||||||
**/
|
|
||||||
extractUrls: function(text) {
|
|
||||||
var result = { text: "" + text, replacements: [] };
|
|
||||||
var replacements = [];
|
|
||||||
var matches, key;
|
|
||||||
|
|
||||||
_.each([Discourse.BBCode.IMG_REGEXP, Discourse.BBCode.URL_REGEXP, Discourse.BBCode.URL_WITH_TITLE_REGEXP], function(r) {
|
|
||||||
while (matches = r.exec(result.text)) {
|
|
||||||
key = md5(matches[0]);
|
|
||||||
replacements.push({ key: key, value: matches[0] });
|
|
||||||
result.text = result.text.replace(matches[0], key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
result.template = function(input) {
|
|
||||||
_.each(replacements, function(r) {
|
|
||||||
input = input.replace(r.key, r.value);
|
|
||||||
});
|
|
||||||
return input;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (result);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
We want to remove quotes from a string before applying markdown to avoid
|
|
||||||
weird stuff with newlines and such. This will return an object that
|
|
||||||
contains a new version of the text with the quotes replaced with
|
|
||||||
unique ids and `template()` function for reapplying them later.
|
|
||||||
|
|
||||||
@method extractQuotes
|
|
||||||
@param {String} text The text inside which we want to replace quotes
|
|
||||||
@returns {Object} object containing the new string and template function
|
|
||||||
**/
|
|
||||||
extractQuotes: function(text) {
|
|
||||||
var result = { text: "" + text, replacements: [] };
|
|
||||||
var replacements = [];
|
|
||||||
var matches, key;
|
|
||||||
|
|
||||||
while (matches = Discourse.BBCode.QUOTE_REGEXP.exec(result.text)) {
|
|
||||||
key = md5(matches[0]);
|
|
||||||
replacements.push({
|
|
||||||
key: key,
|
|
||||||
value: matches[0],
|
|
||||||
content: matches[2].trim()
|
|
||||||
});
|
|
||||||
result.text = result.text.replace(matches[0], key + "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
result.template = function(input) {
|
|
||||||
_.each(replacements,function(r) {
|
|
||||||
var val = r.value.trim();
|
|
||||||
val = val.replace(r.content, r.content.replace(/\n/g, '<br>'));
|
|
||||||
input = input.replace(r.key, val);
|
|
||||||
});
|
|
||||||
return input;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (result);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Replace quotes with appropriate markup
|
|
||||||
|
|
||||||
@method formatQuote
|
|
||||||
@param {String} text The text inside which we want to replace quotes
|
|
||||||
@param {Object} opts Rendering options
|
|
||||||
**/
|
|
||||||
formatQuote: function(text, opts) {
|
|
||||||
var args, matches, params, paramsSplit, paramsString, templateName, username;
|
|
||||||
|
|
||||||
var splitter = function(p,i) {
|
|
||||||
if (i > 0) {
|
|
||||||
var assignment = p.split(':');
|
|
||||||
if (assignment[0] && assignment[1]) {
|
|
||||||
return params.push({
|
|
||||||
key: assignment[0],
|
|
||||||
value: assignment[1].trim()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
while (matches = this.QUOTE_REGEXP.exec(text)) {
|
|
||||||
paramsString = matches[1].replace(/\"/g, '');
|
|
||||||
paramsSplit = paramsString.split(/\, */);
|
|
||||||
params = [];
|
|
||||||
_.each(paramsSplit, splitter);
|
|
||||||
username = paramsSplit[0];
|
|
||||||
|
|
||||||
// remove leading <br>s
|
|
||||||
var content = matches[2].trim();
|
|
||||||
|
|
||||||
var avatarImg;
|
|
||||||
if (opts.lookupAvatarByPostNumber) {
|
|
||||||
// client-side, we can retrieve the avatar from the post
|
|
||||||
var postNumber = parseInt(_.find(params, { 'key' : 'post' }).value, 10);
|
|
||||||
avatarImg = opts.lookupAvatarByPostNumber(postNumber);
|
|
||||||
} else if (opts.lookupAvatar) {
|
|
||||||
// server-side, we need to lookup the avatar from the username
|
|
||||||
avatarImg = opts.lookupAvatar(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arguments for formatting
|
|
||||||
args = {
|
|
||||||
username: I18n.t('user.said', {username: username}),
|
|
||||||
params: params,
|
|
||||||
quote: content,
|
|
||||||
avatarImg: avatarImg
|
|
||||||
};
|
|
||||||
|
|
||||||
// Name of the template
|
|
||||||
templateName = 'quote';
|
|
||||||
if (opts && opts.environment) templateName = "quote_" + opts.environment;
|
|
||||||
// Apply the template
|
|
||||||
text = text.replace(matches[0], "</p>" + HANDLEBARS_TEMPLATES[templateName](args) + "<p>");
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
Format a text string using BBCode
|
|
||||||
|
|
||||||
@method format
|
|
||||||
@param {String} text The text we want to format
|
|
||||||
@param {Object} opts Rendering options
|
|
||||||
**/
|
|
||||||
format: function(text, opts) {
|
|
||||||
var environment = opts && opts.environment ? opts.environment : 'default';
|
|
||||||
// Apply replacers for basic tags
|
|
||||||
text = Discourse.BBCode.apply(text, environment);
|
|
||||||
// Format
|
|
||||||
text = Discourse.BBCode.formatQuote(text, opts);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,4 +1,4 @@
|
||||||
/*global Markdown:true */
|
/*global Markdown:true BetterMarkdown:true */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Contains methods to help us with markdown formatting.
|
Contains methods to help us with markdown formatting.
|
||||||
|
@ -94,116 +94,30 @@ Discourse.Markdown = {
|
||||||
markdownConverter: function(opts) {
|
markdownConverter: function(opts) {
|
||||||
if (!opts) opts = {};
|
if (!opts) opts = {};
|
||||||
|
|
||||||
var converter = new Markdown.Converter();
|
return {
|
||||||
var mentionLookup = opts.mentionLookup || Discourse.Mention.lookupCache;
|
makeHtml: function(text) {
|
||||||
|
|
||||||
var quoteTemplate = null, urlsTemplate = null;
|
// Linebreaks
|
||||||
|
var linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks;
|
||||||
// Before cooking callbacks
|
if (!linebreaks) {
|
||||||
converter.hooks.chain("preConversion", function(text) {
|
text = text.replace(/(^[\w<][^\n]*\n+)/gim, function(t) {
|
||||||
// If a user puts text right up against a quote, make sure the spacing is equivalnt to a new line
|
if (t.match(/\n{2}/gim)) return t;
|
||||||
return text.replace(/\[\/quote\]/, "[/quote]\n");
|
return t.replace("\n", " \n");
|
||||||
});
|
});
|
||||||
|
|
||||||
converter.hooks.chain("preConversion", function(text) {
|
|
||||||
Discourse.Markdown.textResult = null;
|
|
||||||
Discourse.Markdown.trigger('beforeCook', { detail: text, opts: opts });
|
|
||||||
return Discourse.Markdown.textResult || text;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract quotes so their contents are not passed through markdown.
|
|
||||||
converter.hooks.chain("preConversion", function(text) {
|
|
||||||
var extracted = Discourse.BBCode.extractQuotes(text);
|
|
||||||
quoteTemplate = extracted.template;
|
|
||||||
return extracted.text;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extract urls in BBCode tags so they are not passed through markdown.
|
|
||||||
converter.hooks.chain("preConversion", function(text) {
|
|
||||||
var extracted = Discourse.BBCode.extractUrls(text);
|
|
||||||
urlsTemplate = extracted.template;
|
|
||||||
return extracted.text;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Support autolinking of www.something.com
|
|
||||||
converter.hooks.chain("preConversion", function(text) {
|
|
||||||
return text.replace(/(^|[\s\n])(www\.[a-z\.\-\_\(\)\/\?\=\%0-9]+)/gim, function(full, _, rest) {
|
|
||||||
return " <a href=\"http://" + rest + "\">" + rest + "</a>";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// newline prediction in trivial cases
|
|
||||||
var linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks;
|
|
||||||
if (!linebreaks) {
|
|
||||||
converter.hooks.chain("preConversion", function(text) {
|
|
||||||
return text.replace(/(^[\w<][^\n]*\n+)/gim, function(t) {
|
|
||||||
if (t.match(/\n{2}/gim)) return t;
|
|
||||||
return t.replace("\n", " \n");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// github style fenced code
|
|
||||||
converter.hooks.chain("preConversion", function(text) {
|
|
||||||
return text.replace(/^`{3}(?:(.*$)\n)?([\s\S]*?)^`{3}/gm, function(wholeMatch, m1, m2) {
|
|
||||||
var escaped = Handlebars.Utils.escapeExpression(m2);
|
|
||||||
return "<pre><code class='" + (m1 || 'lang-auto') + "'>" + escaped + "</code></pre>";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
converter.hooks.chain("postConversion", function(text) {
|
|
||||||
if (!text) return "";
|
|
||||||
|
|
||||||
// don't do @username mentions inside <pre> or <code> blocks
|
|
||||||
text = text.replace(/<(pre|code)>([\s\S](?!<(pre|code)>))*?@([\s\S](?!<(pre|code)>))*?<\/(pre|code)>/gi, function(m) {
|
|
||||||
return m.replace(/@/g, '@');
|
|
||||||
});
|
|
||||||
|
|
||||||
// add @username mentions, if valid; must be bounded on left and right by non-word characters
|
|
||||||
text = text.replace(/(\W)(@[A-Za-z0-9][A-Za-z0-9_]{2,14})(?=\W)/g, function(x, pre, name) {
|
|
||||||
if (mentionLookup(name.substr(1))) {
|
|
||||||
return pre + "<a href='" + Discourse.getURL("/users/") + (name.substr(1).toLowerCase()) + "' class='mention'>" + name + "</a>";
|
|
||||||
} else {
|
|
||||||
return pre + "<span class='mention'>" + name + "</span>";
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// a primitive attempt at oneboxing, this regex gives me much eye sores
|
text = Discourse.Dialect.cook(text, opts);
|
||||||
text = text.replace(/(<li>)?((<p>|<br>)[\s\n\r]*)(<a href=["]([^"]+)[^>]*)>([^<]+<\/a>[\s\n\r]*(?=<\/p>|<br>))/gi, function() {
|
|
||||||
// We don't onebox items in a list
|
|
||||||
if (arguments[1]) return arguments[0];
|
|
||||||
var url = arguments[5];
|
|
||||||
var onebox;
|
|
||||||
|
|
||||||
if (Discourse && Discourse.Onebox) {
|
if (!text) return "";
|
||||||
onebox = Discourse.Onebox.lookupCache(url);
|
|
||||||
|
if (opts.sanitize) {
|
||||||
|
if (!window.sanitizeHtml) return "";
|
||||||
|
text = window.sanitizeHtml(text);
|
||||||
}
|
}
|
||||||
if (onebox && onebox.trim().length > 0) {
|
|
||||||
return arguments[2] + onebox;
|
|
||||||
} else {
|
|
||||||
return arguments[2] + arguments[4] + " class=\"onebox\" target=\"_blank\">" + arguments[6];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return(text);
|
return text;
|
||||||
});
|
}
|
||||||
|
};
|
||||||
converter.hooks.chain("postConversion", function(text) {
|
|
||||||
// reapply quotes
|
|
||||||
if (quoteTemplate) { text = quoteTemplate(text); }
|
|
||||||
// reapply urls
|
|
||||||
if (urlsTemplate) { text = urlsTemplate(text); }
|
|
||||||
// format with BBCode
|
|
||||||
return Discourse.BBCode.format(text, opts);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (opts.sanitize) {
|
|
||||||
converter.hooks.chain("postConversion", function(text) {
|
|
||||||
if (!window.sanitizeHtml) return "";
|
|
||||||
return window.sanitizeHtml(text);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return converter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
48
app/assets/javascripts/discourse/components/quote.js
Normal file
48
app/assets/javascripts/discourse/components/quote.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
Build the BBCode for a Quote
|
||||||
|
|
||||||
|
@class BBCode
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.Quote = {
|
||||||
|
|
||||||
|
REGEXP: /\[quote=([^\]]*)\]((?:[\s\S](?!\[quote=[^\]]*\]))*?)\[\/quote\]/im,
|
||||||
|
|
||||||
|
/**
|
||||||
|
Build the BBCode quote around the selected text
|
||||||
|
|
||||||
|
@method buildQuote
|
||||||
|
@param {Discourse.Post} post The post we are quoting
|
||||||
|
@param {String} contents The text selected
|
||||||
|
**/
|
||||||
|
build: function(post, contents) {
|
||||||
|
var contents_hashed, result, sansQuotes, stripped, stripped_hashed, tmp;
|
||||||
|
if (!contents) contents = "";
|
||||||
|
|
||||||
|
sansQuotes = contents.replace(this.REGEXP, '').trim();
|
||||||
|
if (sansQuotes.length === 0) return "";
|
||||||
|
|
||||||
|
result = "[quote=\"" + post.get('username') + ", post:" + post.get('post_number') + ", topic:" + post.get('topic_id');
|
||||||
|
|
||||||
|
/* Strip the HTML from cooked */
|
||||||
|
tmp = document.createElement('div');
|
||||||
|
tmp.innerHTML = post.get('cooked');
|
||||||
|
stripped = tmp.textContent || tmp.innerText;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Let's remove any non alphanumeric characters as a kind of hash. Yes it's
|
||||||
|
not accurate but it should work almost every time we need it to. It would be unlikely
|
||||||
|
that the user would quote another post that matches in exactly this way.
|
||||||
|
*/
|
||||||
|
stripped_hashed = stripped.replace(/[^a-zA-Z0-9]/g, '');
|
||||||
|
contents_hashed = contents.replace(/[^a-zA-Z0-9]/g, '');
|
||||||
|
|
||||||
|
/* If the quote is the full message, attribute it as such */
|
||||||
|
if (stripped_hashed === contents_hashed) result += ", full:true";
|
||||||
|
result += "\"]\n" + sansQuotes + "\n[/quote]\n\n";
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -117,7 +117,7 @@ Discourse.QuoteButtonController = Discourse.Controller.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
var buffer = this.get('buffer');
|
var buffer = this.get('buffer');
|
||||||
var quotedText = Discourse.BBCode.buildQuoteBBCode(post, buffer);
|
var quotedText = Discourse.Quote.build(post, buffer);
|
||||||
if (composerController.get('content.replyDirty')) {
|
if (composerController.get('content.replyDirty')) {
|
||||||
composerController.appendText(quotedText);
|
composerController.appendText(quotedText);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -323,7 +323,7 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
||||||
replyToPost: function(post) {
|
replyToPost: function(post) {
|
||||||
var composerController = this.get('controllers.composer');
|
var composerController = this.get('controllers.composer');
|
||||||
var quoteController = this.get('controllers.quoteButton');
|
var quoteController = this.get('controllers.quoteButton');
|
||||||
var quotedText = Discourse.BBCode.buildQuoteBBCode(quoteController.get('post'), quoteController.get('buffer'));
|
var quotedText = Discourse.Quote.build(quoteController.get('post'), quoteController.get('buffer'));
|
||||||
|
|
||||||
var topic = post ? post.get('topic') : this.get('model');
|
var topic = post ? post.get('topic') : this.get('model');
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
This addition handles auto linking of text. When included, it will parse out links and create
|
||||||
|
a hrefs for them.
|
||||||
|
|
||||||
|
@event register
|
||||||
|
@namespace Discourse.Dialect
|
||||||
|
**/
|
||||||
|
Discourse.Dialect.on("register", function(event) {
|
||||||
|
|
||||||
|
var dialect = event.dialect,
|
||||||
|
MD = event.MD;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Parses out links from HTML.
|
||||||
|
|
||||||
|
@method autoLink
|
||||||
|
@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['autolink'] = function autoLink(block, next) {
|
||||||
|
var pattern = /(^|\s)((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
|
||||||
|
result,
|
||||||
|
remaining = block,
|
||||||
|
m;
|
||||||
|
|
||||||
|
var pushIt = function(p) { result.push(p) };
|
||||||
|
|
||||||
|
while (m = pattern.exec(remaining)) {
|
||||||
|
result = result || ['p'];
|
||||||
|
|
||||||
|
var url = m[2],
|
||||||
|
urlIndex = remaining.indexOf(url),
|
||||||
|
before = remaining.slice(0, urlIndex);
|
||||||
|
|
||||||
|
if (before.match(/\[\d+\]/)) { return; }
|
||||||
|
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
remaining = remaining.slice(urlIndex + url.length);
|
||||||
|
|
||||||
|
if (before) {
|
||||||
|
this.processInline(before).forEach(pushIt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayUrl = url;
|
||||||
|
if (url.match(/^www/)) { url = "http://" + url; }
|
||||||
|
result.push(['a', {href: url}, displayUrl]);
|
||||||
|
|
||||||
|
if (remaining && remaining.match(/\n/)) {
|
||||||
|
next.unshift(MD.mk_block(remaining));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
if (remaining.length) {
|
||||||
|
this.processInline(remaining).forEach(pushIt);
|
||||||
|
}
|
||||||
|
return [result];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
171
app/assets/javascripts/discourse/dialects/bbcode_dialect.js
Normal file
171
app/assets/javascripts/discourse/dialects/bbcode_dialect.js
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
/**
|
||||||
|
Regsiter all functionality for supporting BBCode in Discourse.
|
||||||
|
|
||||||
|
@event register
|
||||||
|
@namespace Discourse.Dialect
|
||||||
|
**/
|
||||||
|
Discourse.Dialect.on("register", function(event) {
|
||||||
|
|
||||||
|
var dialect = event.dialect,
|
||||||
|
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
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@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.inline["[quote="] = function bbcodeQuote(text, orig_match) {
|
||||||
|
var bbcodePattern = new RegExp("\\[quote=?([^\\[\\]]+)?\\]([\\s\\S]*?)\\[\\/quote\\]", "igm"),
|
||||||
|
m = bbcodePattern.exec(text);
|
||||||
|
|
||||||
|
if (!m) { return; }
|
||||||
|
var paramsString = m[1].replace(/\"/g, ''),
|
||||||
|
params = {'class': 'quote'},
|
||||||
|
paramsSplit = paramsString.split(/\, */),
|
||||||
|
username = paramsSplit[0],
|
||||||
|
opts = dialect.options;
|
||||||
|
|
||||||
|
paramsSplit.forEach(function(p,i) {
|
||||||
|
if (i > 0) {
|
||||||
|
var assignment = p.split(':');
|
||||||
|
if (assignment[0] && assignment[1]) {
|
||||||
|
params['data-' + assignment[0]] = assignment[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var avatarImg;
|
||||||
|
if (opts.lookupAvatarByPostNumber) {
|
||||||
|
// client-side, we can retrieve the avatar from the post
|
||||||
|
var postNumber = parseInt(params['data-post'], 10);
|
||||||
|
avatarImg = opts.lookupAvatarByPostNumber(postNumber);
|
||||||
|
} else if (opts.lookupAvatar) {
|
||||||
|
// server-side, we need to lookup the avatar from the username
|
||||||
|
avatarImg = opts.lookupAvatar(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
var quote = ['aside', params,
|
||||||
|
['div', {'class': 'title'},
|
||||||
|
['div', {'class': 'quote-controls'}],
|
||||||
|
avatarImg ? avatarImg + "\n" : "",
|
||||||
|
I18n.t('user.said',{username: username})
|
||||||
|
],
|
||||||
|
['blockquote'].concat(this.processInline(m[2]))
|
||||||
|
];
|
||||||
|
|
||||||
|
return [m[0].length, quote];
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Discourse.Dialect.on("parseNode", function(event) {
|
||||||
|
|
||||||
|
var node = event.node,
|
||||||
|
path = event.path;
|
||||||
|
|
||||||
|
// Make sure any quotes are followed by a <br>. The formatting looks weird otherwise.
|
||||||
|
if (node[0] === 'aside' && node[1] && node[1]['class'] === 'quote') {
|
||||||
|
var parent = path[path.length - 1],
|
||||||
|
location = parent.indexOf(node)+1,
|
||||||
|
trailing = parent.slice(location);
|
||||||
|
|
||||||
|
if (trailing.length) {
|
||||||
|
parent.splice(location, 0, ['br']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
110
app/assets/javascripts/discourse/dialects/dialect.js
Normal file
110
app/assets/javascripts/discourse/dialects/dialect.js
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
/**
|
||||||
|
|
||||||
|
Discourse uses the Markdown.js as its main parser. `Discourse.Dialect` is the framework
|
||||||
|
for extending it with additional formatting.
|
||||||
|
|
||||||
|
To extend the dialect, you can register a handler, and you will receive an `event` object
|
||||||
|
with a handle to the markdown `Dialect` from Markdown.js that we are defining. Here's
|
||||||
|
a sample dialect that replaces all occurances of "evil trout" with a link that says
|
||||||
|
"EVIL TROUT IS AWESOME":
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
|
||||||
|
Discourse.Dialect.on("register", function(event) {
|
||||||
|
var dialect = event.dialect;
|
||||||
|
|
||||||
|
// To see how this works, review one of our samples or the Markdown.js code:
|
||||||
|
dialect.inline["evil trout"] = function(text) {
|
||||||
|
return ["evil trout".length, ['a', {href: "http://eviltrout.com"}, "EVIL TROUT IS AWESOME"] ];
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also manipulate the JsonML tree that is produced by the parser before it converted to HTML.
|
||||||
|
This is useful if the markup you want needs a certain structure of HTML elements. Rather than
|
||||||
|
writing regular expressions to match HTML, consider parsing the tree instead! We use this for
|
||||||
|
making sure a onebox is on one line, as an example.
|
||||||
|
|
||||||
|
This example changes the content of any `<code>` tags.
|
||||||
|
|
||||||
|
The `event.path` attribute contains the current path to the node.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
Discourse.Dialect.on("parseNode", function(event) {
|
||||||
|
var node = event.node,
|
||||||
|
path = event.path;
|
||||||
|
|
||||||
|
if (node[0] === 'code') {
|
||||||
|
node[node.length-1] = "EVIL TROUT HACKED YOUR CODE";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**/
|
||||||
|
var parser = window.BetterMarkdown,
|
||||||
|
MD = parser.Markdown,
|
||||||
|
|
||||||
|
// Our dialect
|
||||||
|
dialect = MD.dialects.Discourse = MD.subclassDialect( MD.dialects.Gruber ),
|
||||||
|
|
||||||
|
initialized = false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
Initialize our dialects for processing.
|
||||||
|
|
||||||
|
@method initializeDialects
|
||||||
|
**/
|
||||||
|
initializeDialects = function() {
|
||||||
|
Discourse.Dialect.trigger('register', {dialect: dialect, MD: MD});
|
||||||
|
MD.buildBlockOrder(dialect.block);
|
||||||
|
MD.buildInlinePatterns(dialect.inline);
|
||||||
|
initialized = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Parse a JSON ML tree, using registered handlers to adjust it if necessary.
|
||||||
|
|
||||||
|
@method parseTree
|
||||||
|
@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.
|
||||||
|
@returns {Array} the parsed tree
|
||||||
|
**/
|
||||||
|
parseTree = function parseTree(tree, path) {
|
||||||
|
if (tree instanceof Array) {
|
||||||
|
Discourse.Dialect.trigger('parseNode', {node: tree, path: path});
|
||||||
|
|
||||||
|
path = path || [];
|
||||||
|
path.push(tree);
|
||||||
|
tree.slice(1).forEach(function (n) {
|
||||||
|
parseTree(n, path);
|
||||||
|
});
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
return tree;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
An object used for rendering our dialects.
|
||||||
|
|
||||||
|
@class Dialect
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.Dialect = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Cook text using the dialects.
|
||||||
|
|
||||||
|
@method cook
|
||||||
|
@param {String} text the raw text to cook
|
||||||
|
@returns {String} the cooked text
|
||||||
|
**/
|
||||||
|
cook: function(text, opts) {
|
||||||
|
if (!initialized) { initializeDialects(); }
|
||||||
|
dialect.options = opts;
|
||||||
|
return parser.renderJsonML(parseTree(parser.toHTMLTree(text, 'Discourse')));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RSVP.EventTarget.mixin(Discourse.Dialect);
|
|
@ -0,0 +1,87 @@
|
||||||
|
/**
|
||||||
|
Support for github style code blocks, here you begin with three backticks and supply a language,
|
||||||
|
The language is made into a class on the resulting `<code>` element.
|
||||||
|
|
||||||
|
@event register
|
||||||
|
@namespace Discourse.Dialect
|
||||||
|
**/
|
||||||
|
Discourse.Dialect.on("register", function(event) {
|
||||||
|
var dialect = event.dialect,
|
||||||
|
MD = event.MD;
|
||||||
|
|
||||||
|
/**
|
||||||
|
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)); }
|
||||||
|
|
||||||
|
while (next.length > 0) {
|
||||||
|
var b = next.shift(),
|
||||||
|
n = b.match(/([^`]*)```([^`]*)/m),
|
||||||
|
diff = ((typeof b.lineNumber === "undefined") ? lineNumber : b.lineNumber) - lineNumber;
|
||||||
|
|
||||||
|
lineNumber = b.lineNumber;
|
||||||
|
for (var i=1; i<diff; i++) {
|
||||||
|
codeContents.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n) {
|
||||||
|
if (n[2]) {
|
||||||
|
next.unshift(MD.mk_block(n[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
codeContents.push(n[1].trim());
|
||||||
|
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
|
||||||
|
and we can use HTML and Javascript examples.
|
||||||
|
|
||||||
|
@event parseNode
|
||||||
|
@namespace Discourse.Dialect
|
||||||
|
**/
|
||||||
|
Discourse.Dialect.on("parseNode", function(event) {
|
||||||
|
var node = event.node,
|
||||||
|
path = event.path;
|
||||||
|
|
||||||
|
if (node[0] === 'code') {
|
||||||
|
node[node.length-1] = Handlebars.Utils.escapeExpression(node[node.length-1]);
|
||||||
|
}
|
||||||
|
});
|
68
app/assets/javascripts/discourse/dialects/mention_dialect.js
Normal file
68
app/assets/javascripts/discourse/dialects/mention_dialect.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
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
|
||||||
|
local map.
|
||||||
|
|
||||||
|
@event register
|
||||||
|
@namespace Discourse.Dialect
|
||||||
|
**/
|
||||||
|
Discourse.Dialect.on("register", function(event) {
|
||||||
|
|
||||||
|
var dialect = event.dialect,
|
||||||
|
MD = event.MD;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Support for github style code blocks
|
||||||
|
|
||||||
|
@method mentionSupport
|
||||||
|
@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['mentions'] = function mentionSupport(block, next) {
|
||||||
|
var pattern = /(\W|^)(@[A-Za-z0-9][A-Za-z0-9_]{2,14})(?=(\W|$))/gm,
|
||||||
|
result,
|
||||||
|
remaining = block,
|
||||||
|
m,
|
||||||
|
mentionLookup = dialect.options.mentionLookup || Discourse.Mention.lookupCache;
|
||||||
|
|
||||||
|
if (block.match(/^ {3}/)) { return; }
|
||||||
|
|
||||||
|
var pushIt = function(p) { result.push(p) };
|
||||||
|
|
||||||
|
while (m = pattern.exec(remaining)) {
|
||||||
|
result = result || ['p'];
|
||||||
|
|
||||||
|
var username = m[2],
|
||||||
|
usernameIndex = remaining.indexOf(username),
|
||||||
|
before = remaining.slice(0, usernameIndex);
|
||||||
|
|
||||||
|
pattern.lastIndex = 0;
|
||||||
|
remaining = remaining.slice(usernameIndex + username.length);
|
||||||
|
|
||||||
|
if (before) {
|
||||||
|
this.processInline(before).forEach(pushIt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mentionLookup(username.substr(1))) {
|
||||||
|
result.push(['a', {'class': 'mention', href: Discourse.getURL("/users/") + username.substr(1).toLowerCase()}, username]);
|
||||||
|
} else {
|
||||||
|
result.push(['span', {'class': 'mention'}, username]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remaining && remaining.match(/\n/)) {
|
||||||
|
next.unshift(MD.mk_block(remaining));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
if (remaining.length) {
|
||||||
|
this.processInline(remaining).forEach(pushIt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [result];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
70
app/assets/javascripts/discourse/dialects/onebox_dialect.js
Normal file
70
app/assets/javascripts/discourse/dialects/onebox_dialect.js
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
Given a node in the document and its parent, determine whether it is on its
|
||||||
|
own line or not.
|
||||||
|
|
||||||
|
@method isOnOneLine
|
||||||
|
@namespace Discourse.Dialect
|
||||||
|
**/
|
||||||
|
var isOnOneLine = function(link, parent) {
|
||||||
|
if (!parent) { return false; }
|
||||||
|
|
||||||
|
var siblings = parent.slice(1);
|
||||||
|
if ((!siblings) || (siblings.length < 1)) { return false; }
|
||||||
|
|
||||||
|
var idx = siblings.indexOf(link);
|
||||||
|
if (idx === -1) { return false; }
|
||||||
|
|
||||||
|
if (idx > 0) {
|
||||||
|
var prev = siblings[idx-1];
|
||||||
|
if (prev[0] !== 'br') { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx < siblings.length) {
|
||||||
|
var next = siblings[idx+1];
|
||||||
|
if (next && (!((next[0] === 'br') || (typeof next === 'string' && next.trim() === "")))) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
We only onebox stuff that is on its own line. This navigates the JsonML tree and
|
||||||
|
correctly inserts the oneboxes.
|
||||||
|
|
||||||
|
@event parseNode
|
||||||
|
@namespace Discourse.Dialect
|
||||||
|
**/
|
||||||
|
Discourse.Dialect.on("parseNode", function(event) {
|
||||||
|
var node = event.node,
|
||||||
|
path = event.path;
|
||||||
|
|
||||||
|
// We only care about links
|
||||||
|
if (node[0] !== 'a') { return; }
|
||||||
|
|
||||||
|
var parent = path[path.length - 1];
|
||||||
|
|
||||||
|
// We don't onebox bbcode
|
||||||
|
if (node[1]['data-bbcode']) {
|
||||||
|
delete node[1]['data-bbcode'];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't onebox links within a list
|
||||||
|
for (var i=0; i<path.length; i++) {
|
||||||
|
if (path[i][0] === 'li') { return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOnOneLine(node, parent)) {
|
||||||
|
node[1]['class'] = 'onebox';
|
||||||
|
node[1].target = '_blank';
|
||||||
|
|
||||||
|
if (Discourse && Discourse.Onebox) {
|
||||||
|
var contents = Discourse.Onebox.lookupCache(node[1].href);
|
||||||
|
if (contents) {
|
||||||
|
node[0] = 'raw';
|
||||||
|
node[1] = contents;
|
||||||
|
node.length = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -221,7 +221,7 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
**/
|
**/
|
||||||
replyLength: function() {
|
replyLength: function() {
|
||||||
var reply = this.get('reply') || "";
|
var reply = this.get('reply') || "";
|
||||||
while (Discourse.BBCode.QUOTE_REGEXP.test(reply)) { reply = reply.replace(Discourse.BBCode.QUOTE_REGEXP, ""); }
|
while (Discourse.Quote.REGEXP.test(reply)) { reply = reply.replace(Discourse.Quote.REGEXP, ""); }
|
||||||
return reply.replace(/\s+/img, " ").trim().length;
|
return reply.replace(/\s+/img, " ").trim().length;
|
||||||
}.property('reply'),
|
}.property('reply'),
|
||||||
|
|
||||||
|
@ -279,7 +279,7 @@ Discourse.Composer = Discourse.Model.extend({
|
||||||
this.set('loading', true);
|
this.set('loading', true);
|
||||||
var composer = this;
|
var composer = this;
|
||||||
return Discourse.Post.load(postId).then(function(post) {
|
return Discourse.Post.load(postId).then(function(post) {
|
||||||
composer.appendText(Discourse.BBCode.buildQuoteBBCode(post, post.get('raw')));
|
composer.appendText(Discourse.Quote.build(post, post.get('raw')));
|
||||||
composer.set('loading', false);
|
composer.set('loading', false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -394,7 +394,7 @@ Discourse.Post.reopenClass({
|
||||||
loadQuote: function(postId) {
|
loadQuote: function(postId) {
|
||||||
return Discourse.ajax("/posts/" + postId + ".json").then(function(result) {
|
return Discourse.ajax("/posts/" + postId + ".json").then(function(result) {
|
||||||
var post = Discourse.Post.create(result);
|
var post = Discourse.Post.create(result);
|
||||||
return Discourse.BBCode.buildQuoteBBCode(post, post.get('raw'));
|
return Discourse.Quote.build(post, post.get('raw'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
1343
app/assets/javascripts/external/Markdown.Converter.js
vendored
1343
app/assets/javascripts/external/Markdown.Converter.js
vendored
File diff suppressed because it is too large
Load Diff
1436
app/assets/javascripts/external/markdown.js
vendored
Normal file
1436
app/assets/javascripts/external/markdown.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1294
app/assets/javascripts/external/twitter-text-1.5.0.js
vendored
1294
app/assets/javascripts/external/twitter-text-1.5.0.js
vendored
File diff suppressed because it is too large
Load Diff
|
@ -28,6 +28,8 @@
|
||||||
//= require ./discourse/routes/discourse_route
|
//= require ./discourse/routes/discourse_route
|
||||||
//= require ./discourse/routes/discourse_restricted_user_route
|
//= require ./discourse/routes/discourse_restricted_user_route
|
||||||
|
|
||||||
|
//= require ./discourse/dialects/dialect
|
||||||
|
//= require_tree ./discourse/dialects
|
||||||
//= require_tree ./discourse/controllers
|
//= require_tree ./discourse/controllers
|
||||||
//= require_tree ./discourse/components
|
//= require_tree ./discourse/components
|
||||||
//= require_tree ./discourse/models
|
//= require_tree ./discourse/models
|
||||||
|
|
|
@ -88,6 +88,7 @@ class PostAnalyzer
|
||||||
|
|
||||||
# Returns an array of all links in a post excluding mentions
|
# Returns an array of all links in a post excluding mentions
|
||||||
def raw_links
|
def raw_links
|
||||||
|
|
||||||
return [] unless @raw.present?
|
return [] unless @raw.present?
|
||||||
|
|
||||||
return @raw_links if @raw_links.present?
|
return @raw_links if @raw_links.present?
|
||||||
|
|
|
@ -96,7 +96,6 @@ module PrettyText
|
||||||
"app/assets/javascripts/external/md5.js",
|
"app/assets/javascripts/external/md5.js",
|
||||||
"app/assets/javascripts/external/lodash.js",
|
"app/assets/javascripts/external/lodash.js",
|
||||||
"app/assets/javascripts/external/Markdown.Converter.js",
|
"app/assets/javascripts/external/Markdown.Converter.js",
|
||||||
"app/assets/javascripts/external/twitter-text-1.5.0.js",
|
|
||||||
"lib/headless-ember.js",
|
"lib/headless-ember.js",
|
||||||
"app/assets/javascripts/external/rsvp.js",
|
"app/assets/javascripts/external/rsvp.js",
|
||||||
Rails.configuration.ember.handlebars_location)
|
Rails.configuration.ember.handlebars_location)
|
||||||
|
@ -106,10 +105,17 @@ module PrettyText
|
||||||
ctx.eval("var I18n = {}; I18n.t = function(a,b){ return helpers.t(a,b); }");
|
ctx.eval("var I18n = {}; I18n.t = function(a,b){ return helpers.t(a,b); }");
|
||||||
|
|
||||||
ctx_load(ctx,
|
ctx_load(ctx,
|
||||||
"app/assets/javascripts/discourse/components/bbcode.js",
|
"app/assets/javascripts/external/markdown.js",
|
||||||
|
"app/assets/javascripts/discourse/dialects/dialect.js",
|
||||||
"app/assets/javascripts/discourse/components/utilities.js",
|
"app/assets/javascripts/discourse/components/utilities.js",
|
||||||
"app/assets/javascripts/discourse/components/markdown.js")
|
"app/assets/javascripts/discourse/components/markdown.js")
|
||||||
|
|
||||||
|
Dir["#{Rails.root}/app/assets/javascripts/discourse/dialects/**.js"].each do |dialect|
|
||||||
|
unless dialect =~ /\/dialect\.js$/
|
||||||
|
ctx.load(dialect)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Load server side javascripts
|
# Load server side javascripts
|
||||||
if DiscoursePluginRegistry.server_side_javascripts.present?
|
if DiscoursePluginRegistry.server_side_javascripts.present?
|
||||||
DiscoursePluginRegistry.server_side_javascripts.each do |ssjs|
|
DiscoursePluginRegistry.server_side_javascripts.each do |ssjs|
|
||||||
|
|
|
@ -4,15 +4,6 @@ require 'pretty_text'
|
||||||
describe PrettyText do
|
describe PrettyText do
|
||||||
|
|
||||||
describe "Cooking" do
|
describe "Cooking" do
|
||||||
it "should support github style code blocks" do
|
|
||||||
PrettyText.cook("```
|
|
||||||
test
|
|
||||||
```").should match_html "<pre><code class=\"lang-auto\">test \n</code></pre>"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should support quoting [] " do
|
|
||||||
PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"][sam][/quote]").should =~ /\[sam\]/
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "with avatar" do
|
describe "with avatar" do
|
||||||
|
|
||||||
|
@ -23,15 +14,15 @@ test
|
||||||
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></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\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></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\">\nEvilTrout said:</div>\n<blockquote>ddd\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></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\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></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\">\nEvilTrout 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></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\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></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\">\nEvilTrout said:</div>\n<blockquote>ddd</blockquote></aside></p>"
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -40,36 +31,10 @@ test
|
||||||
PrettyText.cook('@hello @hello @hello').should match_html "<p><span class=\"mention\">@hello</span> <span class=\"mention\">@hello</span> <span class=\"mention\">@hello</span></p>"
|
PrettyText.cook('@hello @hello @hello').should match_html "<p><span class=\"mention\">@hello</span> <span class=\"mention\">@hello</span> <span class=\"mention\">@hello</span></p>"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should not do weird @ mention stuff inside a pre block" do
|
|
||||||
|
|
||||||
PrettyText.cook("```
|
|
||||||
a @test
|
|
||||||
```").should match_html "<pre><code class=\"lang-auto\">a @test \n</code></pre>"
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should sanitize the html" do
|
it "should sanitize the html" do
|
||||||
PrettyText.cook("<script>alert(42)</script>").should match_html "alert(42)"
|
PrettyText.cook("<script>alert(42)</script>").should match_html "alert(42)"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should escape html within the code block" do
|
|
||||||
|
|
||||||
PrettyText.cook("```text
|
|
||||||
<header>hello</header>
|
|
||||||
```").should match_html "<pre><code class=\"text\"><header>hello</header> \n</code></pre>"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "should support language choices" do
|
|
||||||
|
|
||||||
PrettyText.cook("```ruby
|
|
||||||
test
|
|
||||||
```").should match_html "<pre><code class=\"ruby\">test \n</code></pre>"
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should decorate @mentions' do
|
|
||||||
PrettyText.cook("Hello @eviltrout").should match_html "<p>Hello <span class=\"mention\">@eviltrout</span></p>"
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should allow for @mentions to have punctuation' do
|
it 'should allow for @mentions to have punctuation' do
|
||||||
PrettyText.cook("hello @bob's @bob,@bob; @bob\"").should
|
PrettyText.cook("hello @bob's @bob,@bob; @bob\"").should
|
||||||
match_html "<p>hello <span class=\"mention\">@bob</span>'s <span class=\"mention\">@bob</span>,<span class=\"mention\">@bob</span>; <span class=\"mention\">@bob</span>\"</p>"
|
match_html "<p>hello <span class=\"mention\">@bob</span>'s <span class=\"mention\">@bob</span>,<span class=\"mention\">@bob</span>; <span class=\"mention\">@bob</span>\"</p>"
|
||||||
|
@ -78,11 +43,6 @@ test
|
||||||
it 'should add spoiler tags' do
|
it 'should add spoiler tags' do
|
||||||
PrettyText.cook("[spoiler]hello[/spoiler]").should match_html "<p><span class=\"spoiler\">hello</span></p>"
|
PrettyText.cook("[spoiler]hello[/spoiler]").should match_html "<p><span class=\"spoiler\">hello</span></p>"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should only detect ``` at the begining of lines" do
|
|
||||||
PrettyText.cook(" ```\n hello\n ```")
|
|
||||||
.should match_html "<pre><code>```\nhello\n```\n</code></pre>"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "rel nofollow" do
|
describe "rel nofollow" do
|
||||||
|
|
|
@ -57,6 +57,7 @@ describe TopicLink do
|
||||||
@topic.posts.create(user: @user, raw: 'initial post')
|
@topic.posts.create(user: @user, raw: 'initial post')
|
||||||
@post = @topic.posts.create(user: @user, raw: "Link to another topic:\n\n#{@url}\n\n")
|
@post = @topic.posts.create(user: @user, raw: "Link to another topic:\n\n#{@url}\n\n")
|
||||||
@post.reload
|
@post.reload
|
||||||
|
|
||||||
TopicLink.extract_from(@post)
|
TopicLink.extract_from(@post)
|
||||||
|
|
||||||
@link = @topic.topic_links.first
|
@link = @topic.topic_links.first
|
||||||
|
|
|
@ -7,13 +7,14 @@ var format = function(input, expected, text) {
|
||||||
};
|
};
|
||||||
|
|
||||||
test('basic bbcode', function() {
|
test('basic bbcode', function() {
|
||||||
format("[b]strong[/b]", "<span class='bbcode-b'>strong</span>", "bolds text");
|
format("[b]strong[/b]", "<span class=\"bbcode-b\">strong</span>", "bolds text");
|
||||||
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++<br/>\n</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("[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");
|
||||||
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");
|
||||||
});
|
});
|
||||||
|
@ -31,11 +32,11 @@ test('color', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tags with arguments', function() {
|
test('tags with arguments', function() {
|
||||||
format("[size=35]BIG[/size]", "<span class=\"bbcode-size-35\">BIG</span>", "supports [size=]");
|
format("[size=35]BIG [b]whoop[/b][/size]", "<span class=\"bbcode-size-35\">BIG <span class=\"bbcode-b\">whoop</span></span>", "supports [size=]");
|
||||||
format("[url=http://bettercallsaul.com]better call![/url]", "<a href=\"http://bettercallsaul.com\">better call!</a>", "supports [url] with a title");
|
format("[url=http://bettercallsaul.com]better call![/url]", "<a href=\"http://bettercallsaul.com\">better call!</a>", "supports [url] with a title");
|
||||||
format("[email=eviltrout@mailinator.com]evil trout[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">evil trout</a>", "supports [email] with a title");
|
format("[email=eviltrout@mailinator.com]evil trout[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">evil trout</a>", "supports [email] with a title");
|
||||||
format("[u][i]abc[/i][/u]", "<span class='bbcode-u'><span class='bbcode-i'>abc</span></span>", "can nest tags");
|
format("[u][i]abc[/i][/u]", "<span class=\"bbcode-u\"><span class=\"bbcode-i\">abc</span></span>", "can nest tags");
|
||||||
format("[b]first[/b] [b]second[/b]", "<span class='bbcode-b'>first</span> <span class='bbcode-b'>second</span>", "can bold two things on the same line");
|
format("[b]first[/b] [b]second[/b]", "<span class=\"bbcode-b\">first</span> <span class=\"bbcode-b\">second</span>", "can bold two things on the same line");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,7 +50,7 @@ test("quotes", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
var formatQuote = function(val, expected, text) {
|
var formatQuote = function(val, expected, text) {
|
||||||
equal(Discourse.BBCode.buildQuoteBBCode(post, val), expected, text);
|
equal(Discourse.Quote.build(post, val), expected, text);
|
||||||
};
|
};
|
||||||
|
|
||||||
formatQuote(undefined, "", "empty string for undefined content");
|
formatQuote(undefined, "", "empty string for undefined content");
|
||||||
|
@ -58,6 +59,7 @@ test("quotes", function() {
|
||||||
|
|
||||||
formatQuote("lorem", "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n", "correctly formats quotes");
|
formatQuote("lorem", "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n", "correctly formats quotes");
|
||||||
|
|
||||||
|
|
||||||
formatQuote(" lorem \t ",
|
formatQuote(" lorem \t ",
|
||||||
"[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n",
|
"[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n",
|
||||||
"trims white spaces before & after the quoted contents");
|
"trims white spaces before & after the quoted contents");
|
||||||
|
@ -74,34 +76,27 @@ test("quotes", function() {
|
||||||
|
|
||||||
test("quote formatting", function() {
|
test("quote formatting", function() {
|
||||||
|
|
||||||
// TODO: This HTML matching is quite ugly.
|
format("[quote=\"EvilTrout, post:123, topic:456, full:true\"][sam][/quote]",
|
||||||
|
"<aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">" +
|
||||||
|
"<div class=\"quote-controls\"></div>EvilTrout said:</div><blockquote>[sam]</blockquote></aside>",
|
||||||
|
"it allows quotes with [] inside");
|
||||||
|
|
||||||
format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]",
|
format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]",
|
||||||
"</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n " +
|
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:" +
|
||||||
"<div class='quote-controls'></div>\n \n eviltrout said:\n </div>\n <blockquote>abc</blockquote>\n</aside>\n<p>",
|
"</div><blockquote>abc</blockquote></aside>",
|
||||||
"renders quotes properly");
|
"renders quotes properly");
|
||||||
|
|
||||||
format("[quote=\"eviltrout, post:1, topic:1\"]abc[quote=\"eviltrout, post:2, topic:2\"]nested[/quote][/quote]",
|
format("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]\nhello",
|
||||||
"</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n <div class='quote-controls'></div>" +
|
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:" +
|
||||||
"\n \n eviltrout said:\n </div>\n <blockquote>abc1fe072ca2fadbb4f3dfca9ee8bedef19</blockquote>\n</aside>\n<p> ",
|
"</div><blockquote>abc</blockquote></aside><br/>\nhello",
|
||||||
"can nest quotes");
|
"handles new lines properly");
|
||||||
|
|
||||||
format("before[quote=\"eviltrout, post:1, topic:1\"]first[/quote]middle[quote=\"eviltrout, post:2, topic:2\"]second[/quote]after",
|
format("before[quote=\"eviltrout, post:1, topic:1\"]first[/quote]middle[quote=\"eviltrout, post:2, topic:2\"]second[/quote]after",
|
||||||
"before</p><aside class='quote' data-post=\"1\" data-topic=\"1\" >\n <div class='title'>\n <div class='quote-controls'>" +
|
"before<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:</div><blockquote>" +
|
||||||
"</div>\n \n eviltrout said:\n </div>\n <blockquote>first</blockquote>\n</aside>\n<p></p>\n\n<p>middle</p><aside class='quote'" +
|
"first</blockquote></aside><br/>middle<aside class=\"quote\" data-post=\"2\" data-topic=\"2\"><div class=\"title\"><div class=\"quote-controls\"></div>" +
|
||||||
" data-post=\"2\" data-topic=\"2\" >\n <div class='title'>\n <div class='quote-controls'></div>\n \n eviltrout said:\n " +
|
"eviltrout said:</div><blockquote>second</blockquote></aside><br/>after",
|
||||||
"</div>\n <blockquote>second</blockquote>\n</aside>\n<p> <br>\nafter",
|
|
||||||
"can handle more than one quote");
|
"can handle more than one quote");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test("extract quotes", function() {
|
|
||||||
|
|
||||||
var q = "[quote=\"eviltrout, post:1, topic:2\"]hello[/quote]";
|
|
||||||
var result = Discourse.BBCode.extractQuotes(q + " world");
|
|
||||||
|
|
||||||
equal(result.text, md5(q) + "\n world");
|
|
||||||
present(result.template);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ module("Discourse.Markdown", {
|
||||||
});
|
});
|
||||||
|
|
||||||
var cooked = function(input, expected, text) {
|
var cooked = function(input, expected, text) {
|
||||||
equal(Discourse.Markdown.cook(input, {mentionLookup: false }), expected, text);
|
var result = Discourse.Markdown.cook(input, {mentionLookup: false, sanitize: true});
|
||||||
|
equal(result, expected, text);
|
||||||
};
|
};
|
||||||
|
|
||||||
var cookedOptions = function(input, opts, expected, text) {
|
var cookedOptions = function(input, opts, expected, text) {
|
||||||
|
@ -21,7 +22,7 @@ test("basic cooking", function() {
|
||||||
test("Line Breaks", function() {
|
test("Line Breaks", function() {
|
||||||
|
|
||||||
var input = "1\n2\n3";
|
var input = "1\n2\n3";
|
||||||
cooked(input, "<p>1 <br>\n2 <br>\n3</p>", "automatically handles trivial newlines");
|
cooked(input, "<p>1<br>2<br>3</p>", "automatically handles trivial newlines");
|
||||||
|
|
||||||
var traditionalOutput = "<p>1\n2\n3</p>";
|
var traditionalOutput = "<p>1\n2\n3</p>";
|
||||||
|
|
||||||
|
@ -36,13 +37,18 @@ test("Line Breaks", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Links", function() {
|
test("Links", function() {
|
||||||
|
|
||||||
cooked("Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A",
|
cooked("Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A",
|
||||||
'<p>Youtube: <a href="http://www.youtube.com/watch?v=1MrpeBRkM5A">http://www.youtube.com/watch?v=1MrpeBRkM5A</a></p>',
|
'<p>Youtube: <a href="http://www.youtube.com/watch?v=1MrpeBRkM5A">http://www.youtube.com/watch?v=1MrpeBRkM5A</a></p>',
|
||||||
"allows links to contain query params");
|
"allows links to contain query params");
|
||||||
|
|
||||||
cooked("Derpy: http://derp.com?__test=1",
|
cooked("Derpy: http://derp.com?__test=1",
|
||||||
'<p>Derpy: <a href="http://derp.com?%5F%5Ftest=1">http://derp.com?__test=1</a></p>',
|
'<p>Derpy: <a href="http://derp.com?__test=1">http://derp.com?__test=1</a></p>',
|
||||||
"escapes double underscores in URLs");
|
"works with double underscores in urls");
|
||||||
|
|
||||||
|
cooked("Derpy: http://derp.com?_test_=1",
|
||||||
|
'<p>Derpy: <a href="http://derp.com?_test_=1">http://derp.com?_test_=1</a></p>',
|
||||||
|
"works with underscores in urls");
|
||||||
|
|
||||||
cooked("Atwood: www.codinghorror.com",
|
cooked("Atwood: www.codinghorror.com",
|
||||||
'<p>Atwood: <a href="http://www.codinghorror.com">www.codinghorror.com</a></p>',
|
'<p>Atwood: <a href="http://www.codinghorror.com">www.codinghorror.com</a></p>',
|
||||||
|
@ -63,34 +69,48 @@ test("Links", function() {
|
||||||
cooked("Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)",
|
cooked("Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)",
|
||||||
'<p>Batman: <a href="http://en.wikipedia.org/wiki/The_Dark_Knight_(film)">http://en.wikipedia.org/wiki/The_Dark_Knight_(film)</a></p>',
|
'<p>Batman: <a href="http://en.wikipedia.org/wiki/The_Dark_Knight_(film)">http://en.wikipedia.org/wiki/The_Dark_Knight_(film)</a></p>',
|
||||||
"autolinks a URL with parentheses (like Wikipedia)");
|
"autolinks a URL with parentheses (like Wikipedia)");
|
||||||
|
|
||||||
|
cooked("Here's a tweet:\nhttps://twitter.com/evil_trout/status/345954894420787200",
|
||||||
|
"<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.");
|
||||||
|
|
||||||
|
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",
|
||||||
|
"<p><a href=\"http://discourse.org\">http://discourse.org</a> and " +
|
||||||
|
"<a href=\"http://discourse.org/another_url\">http://discourse.org/another_url</a> and " +
|
||||||
|
"<a href=\"http://www.imdb.com/name/nm2225369\">http://www.imdb.com/name/nm2225369</a></p>",
|
||||||
|
'allows multiple links on one line');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Quotes", function() {
|
test("Quotes", function() {
|
||||||
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><aside class='quote' data-post=\"1\" >\n <div class='title'>\n <div class='quote-controls'></div>\n" +
|
"<p>1<aside class=\"quote\" data-post=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>bob\n" +
|
||||||
" bob\n bob said:\n </div>\n <blockquote>my quote</blockquote>\n</aside>\n<p></p>\n\n<p>2</p>",
|
"bob said:</div><blockquote>my quote</blockquote></aside><br/>2</p>",
|
||||||
"handles quotes properly");
|
"handles quotes properly");
|
||||||
|
|
||||||
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) { } },
|
{ topicId: 2, lookupAvatar: function(name) { } },
|
||||||
"<p>1</p><aside class='quote' data-post=\"1\" >\n <div class='title'>\n <div class='quote-controls'></div>\n" +
|
"<p>1<aside class=\"quote\" data-post=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>bob said:</div><blockquote>my quote</blockquote></aside><br/>2</p>",
|
||||||
" \n bob said:\n </div>\n <blockquote>my quote</blockquote>\n</aside>\n<p></p>\n\n<p>2</p>",
|
|
||||||
"includes no avatar if none is found");
|
"includes no avatar if none is found");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Mentions", function() {
|
test("Mentions", function() {
|
||||||
cookedOptions("Hello @sam", { mentionLookup: (function() { return true; }) },
|
cookedOptions("Hello @sam", { mentionLookup: (function() { return true; }) },
|
||||||
"<p>Hello <a href='/users/sam' class='mention'>@sam</a></p>",
|
"<p>Hello <a class=\"mention\" href=\"/users/sam\">@sam</a></p>",
|
||||||
"translates mentions to links");
|
"translates mentions to links");
|
||||||
|
|
||||||
cooked("Hello @EvilTrout", "<p>Hello <span class='mention'>@EvilTrout</span></p>", "adds a mention class");
|
cooked("Hello @EvilTrout", "<p>Hello <span class=\"mention\">@EvilTrout</span></p>", "adds a mention class");
|
||||||
cooked("robin@email.host", "<p>robin@email.host</p>", "won't add mention class to an email address");
|
cooked("robin@email.host", "<p>robin@email.host</p>", "won't add mention class to an email address");
|
||||||
cooked("hanzo55@yahoo.com", "<p>hanzo55@yahoo.com</p>", "won't be affected by email addresses that have a number before the @ symbol");
|
cooked("hanzo55@yahoo.com", "<p>hanzo55@yahoo.com</p>", "won't be affected by email addresses that have a number before the @ symbol");
|
||||||
cooked("@EvilTrout yo", "<p><span class='mention'>@EvilTrout</span> yo</p>", "doesn't do @username mentions inside <pre> or <code> blocks");
|
cooked("@EvilTrout yo", "<p><span class=\"mention\">@EvilTrout</span> yo</p>", "it handles mentions at the beginning of a string");
|
||||||
|
cooked("yo\n@EvilTrout", "<p>yo<br><span class=\"mention\">@EvilTrout</span></p>", "it handles mentions at the beginning of a new line");
|
||||||
cooked("`evil` @EvilTrout `trout`",
|
cooked("`evil` @EvilTrout `trout`",
|
||||||
"<p><code>evil</code> <span class='mention'>@EvilTrout</span> <code>trout</code></p>",
|
"<p><code>evil</code> <span class=\"mention\">@EvilTrout</span> <code>trout</code></p>",
|
||||||
"deals correctly with multiple <code> blocks");
|
"deals correctly with multiple <code> blocks");
|
||||||
|
cooked("```\na @test\n```", "<p><pre><code class=\"lang-auto\">a @test</code></pre></p>", "should not do mentions within a code block.");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -101,22 +121,59 @@ test("Oneboxing", function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
ok(!matches("- http://www.textfiles.com/bbs/MINDVOX/FORUMS/ethics\n\n- http://drupal.org", /onebox/),
|
ok(!matches("- http://www.textfiles.com/bbs/MINDVOX/FORUMS/ethics\n\n- http://drupal.org", /onebox/),
|
||||||
"doesn't onebox a link within a list");
|
"doesn't onebox a link within a list");
|
||||||
|
|
||||||
ok(matches("http://test.com", /onebox/), "adds a onebox class to a link on its own line");
|
ok(matches("http://test.com", /onebox/), "adds a onebox class to a link on its own line");
|
||||||
ok(matches("http://test.com\nhttp://test2.com", /onebox[\s\S]+onebox/m), "supports multiple links");
|
ok(matches("http://test.com\nhttp://test2.com", /onebox[\s\S]+onebox/m), "supports multiple links");
|
||||||
ok(!matches("http://test.com bob", /onebox/), "doesn't onebox links that have trailing text");
|
ok(!matches("http://test.com bob", /onebox/), "doesn't onebox links that have trailing text");
|
||||||
|
|
||||||
cooked("http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street",
|
cooked("http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street",
|
||||||
"<p><a href=\"http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street\" class=\"onebox\" target=\"_blank\"" +
|
"<p><a href=\"http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street\" class=\"onebox\"" +
|
||||||
">http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street</a></p>",
|
">http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street</a></p>",
|
||||||
"works with links that have underscores in them");
|
"works with links that have underscores in them");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Code Blocks", function() {
|
||||||
|
|
||||||
|
cooked("```\ntest\n```",
|
||||||
|
"<p><pre><code class=\"lang-auto\">test</code></pre></p>",
|
||||||
|
"it supports basic code blocks");
|
||||||
|
|
||||||
|
cooked("```json\n{hello: 'world'}\n```\ntrailing",
|
||||||
|
"<p><pre><code class=\"json\">{hello: 'world'}</code></pre></p>\n\n<p>\ntrailing</p>",
|
||||||
|
"It does not truncate text after a code block.");
|
||||||
|
|
||||||
|
cooked("```json\nline 1\n\nline 2\n\n\nline3\n```",
|
||||||
|
"<p><pre><code class=\"json\">line 1\n\nline 2\n\n\nline3</code></pre></p>",
|
||||||
|
"it maintains new lines inside a code block.");
|
||||||
|
|
||||||
|
cooked("hello\nworld\n```json\nline 1\n\nline 2\n\n\nline3\n```",
|
||||||
|
"<p>hello<br>world<br></p>\n\n<p><pre><code class=\"json\">line 1\n\nline 2\n\n\nline3</code></pre></p>",
|
||||||
|
"it maintains new lines inside a code block with leading content.");
|
||||||
|
|
||||||
|
cooked("```text\n<header>hello</header>\n```",
|
||||||
|
"<p><pre><code class=\"text\"><header>hello</header></code></pre></p>",
|
||||||
|
"it escapes code in the code block");
|
||||||
|
|
||||||
|
cooked("```ruby\n# cool\n```",
|
||||||
|
"<p><pre><code class=\"ruby\"># cool</code></pre></p>",
|
||||||
|
"it supports changing the language");
|
||||||
|
|
||||||
|
cooked(" ```\n hello\n ```",
|
||||||
|
"<pre><code>```\nhello\n```</code></pre>",
|
||||||
|
"only detect ``` at the begining of lines");
|
||||||
|
});
|
||||||
|
|
||||||
test("SanitizeHTML", function() {
|
test("SanitizeHTML", function() {
|
||||||
|
|
||||||
equal(sanitizeHtml("<div><script>alert('hi');</script></div>"), "<div></div>");
|
equal(sanitizeHtml("<div><script>alert('hi');</script></div>"), "<div></div>");
|
||||||
equal(sanitizeHtml("<div><p class=\"funky\" wrong='1'>hello</p></div>"), "<div><p class=\"funky\">hello</p></div>");
|
equal(sanitizeHtml("<div><p class=\"funky\" wrong='1'>hello</p></div>"), "<div><p class=\"funky\">hello</p></div>");
|
||||||
|
cooked("hello<script>alert(42)</script>", "<p>hello</p>", "it sanitizes while cooking");
|
||||||
|
|
||||||
|
cooked("<a href='http://disneyland.disney.go.com/'>disney</a> <a href='http://reddit.com'>reddit</a>",
|
||||||
|
"<p><a href=\"http://disneyland.disney.go.com/\">disney</a> <a href=\"http://reddit.com\">reddit</a></p>",
|
||||||
|
"we can embed proper links");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user