Merge pull request #2848 from riking/whitelist-api
Improve Markdown.whiteListTag, code dialect
This commit is contained in:
commit
564e7a988c
|
@ -3,12 +3,12 @@
|
||||||
**/
|
**/
|
||||||
|
|
||||||
var acceptableCodeClasses =
|
var acceptableCodeClasses =
|
||||||
["lang-auto", "1c", "actionscript", "apache", "applescript", "avrasm", "axapta", "bash", "brainfuck",
|
["auto", "1c", "actionscript", "apache", "applescript", "avrasm", "axapta", "bash", "brainfuck",
|
||||||
"clojure", "cmake", "coffeescript", "cpp", "cs", "css", "d", "delphi", "diff", "xml", "django", "dos",
|
"clojure", "cmake", "coffeescript", "cpp", "cs", "css", "d", "delphi", "diff", "xml", "django", "dos",
|
||||||
"erlang-repl", "erlang", "glsl", "go", "handlebars", "haskell", "http", "ini", "java", "javascript",
|
"erlang-repl", "erlang", "glsl", "go", "handlebars", "haskell", "http", "ini", "java", "javascript",
|
||||||
"json", "lisp", "lua", "markdown", "matlab", "mel", "nginx", "objectivec", "parser3", "perl", "php",
|
"json", "lisp", "lua", "markdown", "matlab", "mel", "nginx", "nohighlight", "objectivec", "parser3",
|
||||||
"profile", "python", "r", "rib", "rsl", "ruby", "rust", "scala", "smalltalk", "sql", "tex", "text",
|
"perl", "php", "profile", "python", "r", "rib", "rsl", "ruby", "rust", "scala", "smalltalk", "sql",
|
||||||
"vala", "vbscript", "vhdl"];
|
"tex", "text", "vala", "vbscript", "vhdl"];
|
||||||
|
|
||||||
var textCodeClasses = ["text", "pre"];
|
var textCodeClasses = ["text", "pre"];
|
||||||
|
|
||||||
|
@ -32,9 +32,9 @@ Discourse.Dialect.replaceBlock({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textCodeClasses.indexOf(matches[1]) !== -1) {
|
if (textCodeClasses.indexOf(matches[1]) !== -1) {
|
||||||
return ['p', ['pre', ['code', flattenBlocks(blockContents) ]]];
|
return ['p', ['pre', ['code', {'class': 'lang-nohighlight'}, flattenBlocks(blockContents) ]]];
|
||||||
} else {
|
} else {
|
||||||
return ['p', ['pre', ['code', {'class': klass}, flattenBlocks(blockContents) ]]];
|
return ['p', ['pre', ['code', {'class': 'lang-' + klass}, flattenBlocks(blockContents) ]]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -69,3 +69,7 @@ Discourse.Dialect.replaceBlock({
|
||||||
return ['p', ['pre', flattenBlocks(blockContents)]];
|
return ['p', ['pre', flattenBlocks(blockContents)]];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Whitelist the language classes
|
||||||
|
var regexpSource = "^lang-(" + acceptableCodeClasses.join('|') + ")$";
|
||||||
|
Discourse.Markdown.whiteListTag('code', 'class', new RegExp(regexpSource, "i"));
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/*global Markdown:true, hljs:true */
|
/*global Markdown:true */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Contains methods to help us with markdown formatting.
|
Contains methods to help us with markdown formatting.
|
||||||
|
@ -7,10 +7,44 @@
|
||||||
@namespace Discourse
|
@namespace Discourse
|
||||||
@module Discourse
|
@module Discourse
|
||||||
**/
|
**/
|
||||||
var _validClasses = {},
|
|
||||||
_validIframes = [],
|
/**
|
||||||
_validTags = {},
|
* An object mapping from HTML tag names to an object mapping the valid
|
||||||
_decoratedCaja = false;
|
* attributes on that tag to an array of permitted values.
|
||||||
|
*
|
||||||
|
* The permitted values can be strings or regexes.
|
||||||
|
*
|
||||||
|
* The pseduo-attribute 'data-*' can be used to validate any data-foo
|
||||||
|
* attributes without any specified validations.
|
||||||
|
*
|
||||||
|
* Code can insert into this map by calling Discourse.Markdown.whiteListTag().
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* <pre><code>
|
||||||
|
* {
|
||||||
|
* a: {
|
||||||
|
* href: ['*'],
|
||||||
|
* data-mention-id: [/^\d+$/],
|
||||||
|
* ...
|
||||||
|
* },
|
||||||
|
* code: {
|
||||||
|
* class: ['ada', 'haskell', 'c', 'cpp', ... ]
|
||||||
|
* },
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* </code></pre>
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
var _validTags = {};
|
||||||
|
/**
|
||||||
|
* Classes valid on all elements. Map from class name to 'true'.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
var _validClasses = {};
|
||||||
|
var _validIframes = [];
|
||||||
|
var _decoratedCaja = false;
|
||||||
|
|
||||||
function validateAttribute(tagName, attribName, value) {
|
function validateAttribute(tagName, attribName, value) {
|
||||||
var tag = _validTags[tagName];
|
var tag = _validTags[tagName];
|
||||||
|
@ -18,18 +52,17 @@ function validateAttribute(tagName, attribName, value) {
|
||||||
// Handle classes
|
// Handle classes
|
||||||
if (attribName === "class") {
|
if (attribName === "class") {
|
||||||
if (_validClasses[value]) { return value; }
|
if (_validClasses[value]) { return value; }
|
||||||
|
}
|
||||||
|
|
||||||
if (tag) {
|
if (attribName.indexOf('data-') === 0) {
|
||||||
var classes = tag['class'];
|
// data-* catch-all validators
|
||||||
if (classes && (classes.indexOf(value) !== -1 || classes.indexOf('*') !== -1)) {
|
if (tag && tag['data-*'] && !tag[attribName]) {
|
||||||
return value;
|
var permitted = tag['data-*'];
|
||||||
}
|
if (permitted && (
|
||||||
}
|
permitted.indexOf(value) !== -1 ||
|
||||||
} else if (attribName.indexOf('data-') === 0) {
|
permitted.indexOf('*') !== -1 ||
|
||||||
// data-* attributes
|
((permitted instanceof RegExp) && permitted.test(value)))
|
||||||
if (tag) {
|
) { return value; }
|
||||||
var allowed = tag[attribName] || tag['data-*'];
|
|
||||||
if (allowed && (allowed === value || allowed.indexOf('*') !== -1)) { return value; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,21 +70,40 @@ function validateAttribute(tagName, attribName, value) {
|
||||||
var attrs = tag[attribName];
|
var attrs = tag[attribName];
|
||||||
if (attrs && (attrs.indexOf(value) !== -1 ||
|
if (attrs && (attrs.indexOf(value) !== -1 ||
|
||||||
attrs.indexOf('*') !== -1) ||
|
attrs.indexOf('*') !== -1) ||
|
||||||
_.any(attrs,function(r){return (r instanceof RegExp) && value.search(r) >= 0;})
|
_.any(attrs, function(r) { return (r instanceof RegExp) && r.test(value); })
|
||||||
) { return value; }
|
) { return value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function anchorRegexp(regex) {
|
||||||
|
if (/^\^.*\$$/.test(regex.source)) {
|
||||||
|
return regex; // already anchored
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags = "";
|
||||||
|
if (regex.global) { throw "Invalid attribute validation regex - cannot be global"; }
|
||||||
|
if (regex.ignoreCase) { flags += "i"; }
|
||||||
|
if (regex.multiline) { flags += "m"; }
|
||||||
|
if (regex.sticky) { throw "Invalid attribute validation regex - cannot be sticky"; }
|
||||||
|
|
||||||
|
return new RegExp("^" + regex.source + "$", flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
Discourse.Markdown = {
|
Discourse.Markdown = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Whitelist class for only a certain tag
|
Add to the attribute whitelist for a certain HTML tag.
|
||||||
|
|
||||||
@param {String} tagName to whitelist
|
@param {String} tagName tag to whitelist the attr for
|
||||||
@param {String} attribName to whitelist
|
@param {String} attribName attr to whitelist for the tag
|
||||||
@param {String} value to whitelist
|
@param {String | RegExp} [value] whitelisted value for the attribute
|
||||||
**/
|
**/
|
||||||
whiteListTag: function(tagName, attribName, value) {
|
whiteListTag: function(tagName, attribName, value) {
|
||||||
|
if (value instanceof RegExp) {
|
||||||
|
value = anchorRegexp(value);
|
||||||
|
}
|
||||||
_validTags[tagName] = _validTags[tagName] || {};
|
_validTags[tagName] = _validTags[tagName] || {};
|
||||||
_validTags[tagName][attribName] = _validTags[tagName][attribName] || [];
|
_validTags[tagName][attribName] = _validTags[tagName][attribName] || [];
|
||||||
_validTags[tagName][attribName].push(value || '*');
|
_validTags[tagName][attribName].push(value || '*');
|
||||||
|
@ -238,26 +290,19 @@ Discourse.Markdown = {
|
||||||
RSVP.EventTarget.mixin(Discourse.Markdown);
|
RSVP.EventTarget.mixin(Discourse.Markdown);
|
||||||
|
|
||||||
Discourse.Markdown.whiteListTag('a', 'class', 'attachment');
|
Discourse.Markdown.whiteListTag('a', 'class', 'attachment');
|
||||||
Discourse.Markdown.whiteListTag('a', 'target', '_blank');
|
|
||||||
Discourse.Markdown.whiteListTag('a', 'class', 'onebox');
|
Discourse.Markdown.whiteListTag('a', 'class', 'onebox');
|
||||||
Discourse.Markdown.whiteListTag('a', 'class', 'mention');
|
Discourse.Markdown.whiteListTag('a', 'class', 'mention');
|
||||||
|
|
||||||
|
Discourse.Markdown.whiteListTag('a', 'target', '_blank');
|
||||||
|
Discourse.Markdown.whiteListTag('a', 'rel', 'nofollow');
|
||||||
Discourse.Markdown.whiteListTag('a', 'data-bbcode');
|
Discourse.Markdown.whiteListTag('a', 'data-bbcode');
|
||||||
Discourse.Markdown.whiteListTag('a', 'name');
|
Discourse.Markdown.whiteListTag('a', 'name');
|
||||||
|
|
||||||
Discourse.Markdown.whiteListTag('img', 'src', /^data:image.*/i);
|
Discourse.Markdown.whiteListTag('img', 'src', /^data:image.*$/i);
|
||||||
|
|
||||||
Discourse.Markdown.whiteListTag('div', 'class', 'title');
|
Discourse.Markdown.whiteListTag('div', 'class', 'title');
|
||||||
Discourse.Markdown.whiteListTag('div', 'class', 'quote-controls');
|
Discourse.Markdown.whiteListTag('div', 'class', 'quote-controls');
|
||||||
|
|
||||||
// explicitly whitelist classes we need allowed through for
|
|
||||||
// syntax highlighting, grabbed from highlight.js
|
|
||||||
hljs.listLanguages().forEach(function (language) {
|
|
||||||
Discourse.Markdown.whiteListTag('code', 'class', language);
|
|
||||||
});
|
|
||||||
Discourse.Markdown.whiteListTag('code', 'class', 'text');
|
|
||||||
Discourse.Markdown.whiteListTag('code', 'class', 'lang-auto');
|
|
||||||
|
|
||||||
Discourse.Markdown.whiteListTag('span', 'class', 'mention');
|
Discourse.Markdown.whiteListTag('span', 'class', 'mention');
|
||||||
Discourse.Markdown.whiteListTag('span', 'class', 'spoiler');
|
Discourse.Markdown.whiteListTag('span', 'class', 'spoiler');
|
||||||
Discourse.Markdown.whiteListTag('div', 'class', 'spoiler');
|
Discourse.Markdown.whiteListTag('div', 'class', 'spoiler');
|
||||||
|
|
|
@ -377,7 +377,7 @@ posting:
|
||||||
default: 10000
|
default: 10000
|
||||||
default_code_lang:
|
default_code_lang:
|
||||||
client: true
|
client: true
|
||||||
default: "lang-auto"
|
default: "auto"
|
||||||
warn_reviving_old_topic_age: 180
|
warn_reviving_old_topic_age: 180
|
||||||
autohighlight_all_code:
|
autohighlight_all_code:
|
||||||
client: true
|
client: true
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
module("Discourse.Markdown", {
|
module("Discourse.Markdown", {
|
||||||
setup: function() {
|
setup: function() {
|
||||||
Discourse.SiteSettings.traditional_markdown_linebreaks = false;
|
Discourse.SiteSettings.traditional_markdown_linebreaks = false;
|
||||||
|
Discourse.SiteSettings.default_code_lang = "auto";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -337,25 +338,25 @@ test("Code Blocks", function() {
|
||||||
"it supports basic code blocks");
|
"it supports basic code blocks");
|
||||||
|
|
||||||
cooked("```json\n{hello: 'world'}\n```\ntrailing",
|
cooked("```json\n{hello: 'world'}\n```\ntrailing",
|
||||||
"<p><pre><code class=\"json\">{hello: 'world'}</code></pre></p>\n\n<p>trailing</p>",
|
"<p><pre><code class=\"lang-json\">{hello: 'world'}</code></pre></p>\n\n<p>trailing</p>",
|
||||||
"It does not truncate text after a code block.");
|
"It does not truncate text after a code block.");
|
||||||
|
|
||||||
cooked("```json\nline 1\n\nline 2\n\n\nline3\n```",
|
cooked("```json\nline 1\n\nline 2\n\n\nline3\n```",
|
||||||
"<p><pre><code class=\"json\">line 1\n\nline 2\n\n\nline3</code></pre></p>",
|
"<p><pre><code class=\"lang-json\">line 1\n\nline 2\n\n\nline3</code></pre></p>",
|
||||||
"it maintains new lines inside a code block.");
|
"it maintains new lines inside a code block.");
|
||||||
|
|
||||||
cooked("hello\nworld\n```json\nline 1\n\nline 2\n\n\nline3\n```",
|
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>",
|
"<p>hello<br/>world<br/></p>\n\n<p><pre><code class=\"lang-json\">line 1\n\nline 2\n\n\nline3</code></pre></p>",
|
||||||
"it maintains new lines inside a code block with leading content.");
|
"it maintains new lines inside a code block with leading content.");
|
||||||
|
|
||||||
cooked("```ruby\n<header>hello</header>\n```",
|
cooked("```ruby\n<header>hello</header>\n```",
|
||||||
"<p><pre><code class=\"ruby\"><header>hello</header></code></pre></p>",
|
"<p><pre><code class=\"lang-ruby\"><header>hello</header></code></pre></p>",
|
||||||
"it escapes code in the code block");
|
"it escapes code in the code block");
|
||||||
|
|
||||||
cooked("```text\ntext\n```", "<p><pre><code>text</code></pre></p>", "handles text without adding class");
|
cooked("```text\ntext\n```", "<p><pre><code class=\"lang-nohighlight\">text</code></pre></p>", "handles text by adding nohighlight");
|
||||||
|
|
||||||
cooked("```ruby\n# cool\n```",
|
cooked("```ruby\n# cool\n```",
|
||||||
"<p><pre><code class=\"ruby\"># cool</code></pre></p>",
|
"<p><pre><code class=\"lang-ruby\"># cool</code></pre></p>",
|
||||||
"it supports changing the language");
|
"it supports changing the language");
|
||||||
|
|
||||||
cooked(" ```\n hello\n ```",
|
cooked(" ```\n hello\n ```",
|
||||||
|
@ -363,11 +364,11 @@ test("Code Blocks", function() {
|
||||||
"only detect ``` at the beginning of lines");
|
"only detect ``` at the beginning of lines");
|
||||||
|
|
||||||
cooked("```ruby\ndef self.parse(text)\n\n text\nend\n```",
|
cooked("```ruby\ndef self.parse(text)\n\n text\nend\n```",
|
||||||
"<p><pre><code class=\"ruby\">def self.parse(text)\n\n text\nend</code></pre></p>",
|
"<p><pre><code class=\"lang-ruby\">def self.parse(text)\n\n text\nend</code></pre></p>",
|
||||||
"it allows leading spaces on lines in a code block.");
|
"it allows leading spaces on lines in a code block.");
|
||||||
|
|
||||||
cooked("```ruby\nhello `eviltrout`\n```",
|
cooked("```ruby\nhello `eviltrout`\n```",
|
||||||
"<p><pre><code class=\"ruby\">hello `eviltrout`</code></pre></p>",
|
"<p><pre><code class=\"lang-ruby\">hello `eviltrout`</code></pre></p>",
|
||||||
"it allows code with backticks in it");
|
"it allows code with backticks in it");
|
||||||
|
|
||||||
cooked("```eviltrout\nhello\n```",
|
cooked("```eviltrout\nhello\n```",
|
||||||
|
|
Loading…
Reference in New Issue