diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6
index 5ff68ef7959..781a186d843 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6
@@ -227,10 +227,11 @@ export function setup(opts, siteSettings, state) {
opts.markdownIt = true;
opts.setup = true;
- if (!opts.discourse.sanitizer) {
+ if (!opts.discourse.sanitizer || !opts.sanitizer) {
const whiteLister = new WhiteLister(opts.discourse);
opts.sanitizer = opts.discourse.sanitizer = (!!opts.discourse.sanitize) ? a=>sanitize(a, whiteLister) : a=>a;
}
+
}
export function cook(raw, opts) {
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown.js.es6
deleted file mode 100644
index 3303cc34b9d..00000000000
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown.js.es6
+++ /dev/null
@@ -1,597 +0,0 @@
-import guid from 'pretty-text/guid';
-import { default as WhiteLister, whiteListFeature } from 'pretty-text/white-lister';
-import { escape } from 'pretty-text/sanitizer';
-
-var parser = window.BetterMarkdown,
- MD = parser.Markdown,
- DialectHelpers = parser.DialectHelpers,
- hoisted;
-
-let currentOpts;
-
-const emitters = [];
-const preProcessors = [];
-const parseNodes = [];
-
-function findEndPos(text, start, stop, args, offset) {
- let endPos, nextStart;
- do {
- endPos = text.indexOf(stop, offset);
- if (endPos === -1) { return -1; }
- nextStart = text.indexOf(start, offset);
- offset = endPos + stop.length;
- } while (nextStart !== -1 && nextStart < endPos);
- return endPos;
-}
-
-class DialectHelper {
- constructor() {
- this._dialect = MD.dialects.Discourse = DialectHelpers.subclassDialect(MD.dialects.Gruber);
- this._setup = false;
- }
-
- escape(str) {
- return escape(str);
- }
-
- getOptions() {
- return currentOpts;
- }
-
- registerInlineFeature(featureName, start, fn) {
- this._dialect.inline[start] = function() {
- if (!currentOpts.features[featureName]) { return; }
- return fn.apply(this, arguments);
- };
- }
-
- addPreProcessorFeature(featureName, fn) {
- preProcessors.push(raw => {
- if (!currentOpts.features[featureName]) { return raw; }
- return fn(raw, hoister);
- });
- }
-
- /**
- The simplest kind of replacement possible. Replace a stirng token with JsonML.
-
- For example to replace all occurrances of :) with a smile image:
-
- ```javascript
- helper.inlineReplace(':)', text => ['img', {src: '/images/smile.png'}]);
- ```
- **/
- inlineReplaceFeature(featureName, token, emitter) {
- this.registerInline(token, (text, match, prev) => {
- if (!currentOpts.features[featureName]) { return; }
- return [token.length, emitter.call(this, token, match, prev)];
- });
- }
-
- /**
- After the parser has been executed, change the contents of a HTML tag.
-
- Let's say you want to replace the contents of all code tags to prepend
- "EVIL TROUT HACKED YOUR CODE!":
-
- ```javascript
- helper.postProcessTag('code', contents => `EVIL TROUT HACKED YOUR CODE!\n\n${contents}`);
- ```
- **/
- postProcessTagFeature(featureName, tag, emitter) {
- this.onParseNode(event => {
- if (!currentOpts.features[featureName]) { return; }
- const node = event.node;
- if (node[0] === tag) {
- node[node.length-1] = emitter(node[node.length-1]);
- }
- });
- }
-
- /**
- Matches inline using a regular expression. The emitter function is passed
- the matches from the regular expression.
-
- For example, this auto links URLs:
-
- ```javascript
- helper.inlineRegexp({
- matcher: /((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
- spaceBoundary: true,
- start: 'http',
-
- emitter(matches) {
- const url = matches[1];
- return ['a', {href: url}, url];
- }
- });
- ```
- **/
- inlineRegexpFeature(featureName, args) {
- this.registerInline(args.start, function(text, match, prev) {
- if (!currentOpts.features[featureName]) { return; }
- if (invalidBoundary(args, prev)) { return; }
-
- args.matcher.lastIndex = 0;
- const m = args.matcher.exec(text);
- if (m) {
- const result = args.emitter.call(this, m);
- if (result) {
- return [m[0].length, result];
- }
- }
- });
- }
-
- /**
- Handles inline replacements surrounded by tokens.
-
- For example, to handle markdown style bold. Note we use `concat` on the array because
- the contents are JsonML too since we didn't pass `rawContents` as true. This supports
- recursive markup.
-
- ```javascript
- helper.inlineBetween({
- between: '**',
- wordBoundary: true.
- emitter(contents) {
- return ['strong'].concat(contents);
- }
- });
- ```
- **/
- inlineBetweenFeature(featureName, args) {
- const start = args.start || args.between;
- const stop = args.stop || args.between;
- const startLength = start.length;
-
- this.registerInline(start, function(text, match, prev) {
- if (!currentOpts.features[featureName]) { return; }
- if (invalidBoundary(args, prev)) { return; }
-
- const endPos = findEndPos(text, start, stop, args, 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+stop.length, contents];
- }
- });
- }
-
- /**
- Replaces a block of text between a start and stop. As opposed to inline, these
- might span multiple lines.
-
- Here's an example that takes the content between `[code]` ... `[/code]` and
- puts them inside a `pre` tag:
-
- ```javascript
- helper.replaceBlock({
- start: /(\[code\])([\s\S]*)/igm,
- stop: '[/code]',
- rawContents: true,
-
- emitter(blockContents) {
- return ['p', ['pre'].concat(blockContents)];
- }
- });
- ```
- **/
- replaceBlockFeature(featureName, args) {
- function blockFunc(block, next) {
- if (!currentOpts.features[featureName]) { return; }
-
- const linebreaks = currentOpts.traditionalMarkdownLinebreaks;
- if (linebreaks && args.skipIfTradtionalLinebreaks) { return; }
-
- args.start.lastIndex = 0;
- const result = [];
- const match = (args.start).exec(block);
- if (!match) { return; }
-
- const lastChance = () => !next.some(blk => blk.match(args.stop));
-
- // shave off start tag and leading text, if any.
- const pos = args.start.lastIndex - match[0].length;
- const leading = block.slice(0, pos);
- const trailing = match[2] ? match[2].replace(/^\n*/, "") : "";
-
- // The other leading block should be processed first! eg a code block wrapped around a code block.
- if (args.withoutLeading && args.withoutLeading.test(leading)) {
- return;
- }
-
- // just give up if there's no stop tag in this or any next block
- args.stop.lastIndex = block.length - trailing.length;
- if (!args.stop.exec(block) && lastChance()) { return; }
- if (leading.length > 0) {
- var parsedLeading = this.processBlock(MD.mk_block(leading), []);
- if (parsedLeading && parsedLeading[0]) {
- result.push(parsedLeading[0]);
- }
- }
- if (trailing.length > 0) {
- next.unshift(MD.mk_block(trailing, block.trailing,
- block.lineNumber + countLines(leading) + (match[2] ? match[2].length : 0) - trailing.length));
- }
-
- // go through the available blocks to find the matching stop tag.
- const contentBlocks = [];
- let nesting = 0;
- let actualEndPos = -1;
- let currentBlock;
-
- blockloop:
- while (currentBlock = next.shift()) {
-
- // collect all the start and stop tags in the current block
- args.start.lastIndex = 0;
- const startPos = [];
- let m;
- while (m = (args.start).exec(currentBlock)) {
- startPos.push(args.start.lastIndex - m[0].length);
- args.start.lastIndex = args.start.lastIndex - (m[2] ? m[2].length : 0);
- }
- args.stop.lastIndex = 0;
- const endPos = [];
- while (m = (args.stop).exec(currentBlock)) {
- endPos.push(args.stop.lastIndex - m[0].length);
- }
-
- // go through the available end tags:
- let ep = 0;
- let sp = 0;
- while (ep < endPos.length) {
- if (sp < startPos.length && startPos[sp] < endPos[ep]) {
- // there's an end tag, but there's also another start tag first. we need to go deeper.
- sp++; nesting++;
- } else if (nesting > 0) {
- // found an end tag, but we must go up a level first.
- ep++; nesting--;
- } else {
- // found an end tag and we're at the top: done! -- or: start tag and end tag are
- // identical, (i.e. startPos[sp] == endPos[ep]), so we don't do nesting at all.
- actualEndPos = endPos[ep];
- break blockloop;
- }
- }
-
- if (lastChance()) {
- // when lastChance() becomes true the first time, currentBlock contains the last
- // end tag available in the input blocks but it's not on the right nesting level
- // or we would have terminated the loop already. the only thing we can do is to
- // treat the last available end tag as tho it were matched with our start tag
- // and let the emitter figure out how to render the garbage inside.
- actualEndPos = endPos[endPos.length - 1];
- break;
- }
-
- // any left-over start tags still increase the nesting level
- nesting += startPos.length - sp;
- contentBlocks.push(currentBlock);
- }
-
- const stopLen = currentBlock.match(args.stop)[0].length;
- const before = currentBlock.slice(0, actualEndPos).replace(/\n*$/, "");
- const after = currentBlock.slice(actualEndPos + stopLen).replace(/^\n*/, "");
- if (before.length > 0) contentBlocks.push(MD.mk_block(before, "", currentBlock.lineNumber));
- if (after.length > 0) next.unshift(MD.mk_block(after, currentBlock.trailing, currentBlock.lineNumber + countLines(before)));
-
- const emitterResult = args.emitter.call(this, contentBlocks, match);
- if (emitterResult) { result.push(emitterResult); }
- return result;
- };
-
- if (args.priority) {
- blockFunc.priority = args.priority;
- }
-
- this.registerBlock(args.start.toString(), blockFunc);
- }
-
- /**
- After the parser has been executed, post process any text nodes in the HTML document.
- This is useful if you want to apply a transformation to the text.
-
- If you are generating HTML from the text, it is preferable to use the replacer
- functions and do it in the parsing part of the pipeline. This function is best for
- simple transformations or transformations that have to happen after all earlier
- processing is done.
-
- For example, to convert all text to upper case:
-
- ```javascript
- helper.postProcessText(function (text) {
- return text.toUpperCase();
- });
- ```
- **/
- postProcessTextFeature(featureName, fn) {
- emitters.push(function () {
- if (!currentOpts.features[featureName]) { return; }
- return fn.apply(this, arguments);
- });
- }
-
- onParseNodeFeature(featureName, fn) {
- parseNodes.push(function () {
- if (!currentOpts.features[featureName]) { return; }
- return fn.apply(this, arguments);
- });
- }
-
- registerBlockFeature(featureName, name, fn) {
- const blockFunc = function() {
- if (!currentOpts.features[featureName]) { return; }
- return fn.apply(this, arguments);
- };
-
- blockFunc.priority = fn.priority;
- this._dialect.block[name] = blockFunc;
- }
-
- applyFeature(featureName, module) {
- helper.registerInline = (code, fn) => helper.registerInlineFeature(featureName, code, fn);
- helper.replaceBlock = args => helper.replaceBlockFeature(featureName, args);
- helper.addPreProcessor = fn => helper.addPreProcessorFeature(featureName, fn);
- helper.inlineReplace = (token, emitter) => helper.inlineReplaceFeature(featureName, token, emitter);
- helper.postProcessTag = (token, emitter) => helper.postProcessTagFeature(featureName, token, emitter);
- helper.inlineRegexp = args => helper.inlineRegexpFeature(featureName, args);
- helper.inlineBetween = args => helper.inlineBetweenFeature(featureName, args);
- helper.postProcessText = fn => helper.postProcessTextFeature(featureName, fn);
- helper.onParseNode = fn => helper.onParseNodeFeature(featureName, fn);
- helper.registerBlock = (name, fn) => helper.registerBlockFeature(featureName, name, fn);
-
- module.setup(this);
- }
-
- setup() {
- if (this._setup) { return; }
- this._setup = true;
-
- Object.keys(require._eak_seen).forEach(entry => {
- if (entry.indexOf('discourse-markdown') !== -1) {
- const module = requirejs(entry);
- if (module && module.setup) {
- const featureName = entry.split('/').reverse()[0];
- helper.whiteList = info => whiteListFeature(featureName, info);
-
- this.applyFeature(featureName, module);
- helper.whiteList = undefined;
- }
- }
- });
-
- MD.buildBlockOrder(this._dialect.block);
- var index = this._dialect.block.__order__.indexOf("code");
- if (index > -1) {
- this._dialect.block.__order__.splice(index, 1);
- this._dialect.block.__order__.unshift("code");
- }
- MD.buildInlinePatterns(this._dialect.inline);
- }
-};
-
-const helper = new DialectHelper();
-
-export function cook(raw, opts) {
- currentOpts = opts;
-
- hoisted = {};
-
- if (!currentOpts.enableExperimentalMarkdownIt) {
- raw = hoistCodeBlocksAndSpans(raw);
- preProcessors.forEach(p => raw = p(raw));
- }
-
- const whiteLister = new WhiteLister(opts);
-
- let result;
-
- if (currentOpts.enableExperimentalMarkdownIt) {
- result = opts.sanitizer(
- requirejs('pretty-text/engines/markdown-it/instance').default(opts).render(raw),
- whiteLister
- );
- } else {
- const tree = parser.toHTMLTree(raw, 'Discourse');
- result = opts.sanitizer(parser.renderJsonML(parseTree(tree, opts)), whiteLister);
- }
-
- // If we hoisted out anything, put it back
- const keys = Object.keys(hoisted);
- if (keys.length) {
- let found = true;
-
- const unhoist = function(key) {
- result = result.replace(new RegExp(key, "g"), function() {
- found = true;
- return hoisted[key];
- });
- };
-
- while (found) {
- found = false;
- keys.forEach(unhoist);
- }
- }
-
- return result.trim();
-}
-
-export function setup() {
- helper.setup();
-}
-
-function processTextNodes(node, event, emitter) {
- if (node.length < 2) { return; }
-
- if (node[0] === '__RAW') {
- const hash = guid();
- hoisted[hash] = node[1];
- node[1] = hash;
- return;
- }
-
- for (var j=1; j...
code blocks
- text = text.replace(/(\s|^)([\s\S]*?)<\/pre>/ig, function(_, before, content) {
- const hash = guid();
- hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content)));
- return before + "
" + hash + "
";
- });
-
- // code spans (double & single `)
- ["``", "`"].forEach(function(delimiter) {
- var regexp = new RegExp("(^|[^`])" + delimiter + "([^`\\n]+?)" + delimiter + "([^`]|$)", "g");
- text = text.replace(regexp, function(_, before, content, after) {
- const hash = guid();
- hoisted[hash] = escape(showBackslashEscapedCharacters(content.trim()));
- return before + delimiter + hash + delimiter + after;
- });
- });
-
- // replace back all weird character with "\`"
- return showBackslashEscapedCharacters(text);
-}
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-inline.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-inline.js.es6
index 71ce50462af..a2dfa571437 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-inline.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-inline.js.es6
@@ -57,6 +57,7 @@ function tokanizeBBCode(state, silent, ruler) {
let token = state.push('text', '' , 0);
token.content = state.src.slice(pos, pos+tagInfo.length);
+ token.meta = 'bbcode';
state.delimiters.push({
bbInfo: tagInfo,
@@ -105,10 +106,15 @@ function processBBCode(state, silent) {
let tag, className;
if (typeof tagInfo.rule.wrap === 'function') {
- if (!tagInfo.rule.wrap(token, tagInfo)) {
- return false;
+ let content = "";
+ for (let j = startDelim.token+1; j < endDelim.token; j++) {
+ let inner = state.tokens[j];
+ if (inner.type === 'text' && inner.meta !== 'bbcode') {
+ content += inner.content;
+ }
}
- tag = token.tag;
+ tagInfo.rule.wrap(token, state.tokens[endDelim.token], tagInfo, content);
+ continue;
} else {
let split = tagInfo.rule.wrap.split('.');
tag = split[0];
@@ -160,19 +166,35 @@ export function setup(helper) {
}
});
+ const simpleUrlRegex = /^http[s]?:\/\//;
ruler.push('url', {
tag: 'url',
- replace: function(state, tagInfo, content) {
- let token;
+ wrap: function(startToken, endToken, tagInfo, content) {
- token = state.push('link_open', 'a', 1);
- token.attrs = [['href', content], ['data-bbcode', 'true']];
+ const url = (tagInfo.attrs['_default'] || content).trim();
- token = state.push('text', '', 0);
- token.content = content;
+ if (simpleUrlRegex.test(url)) {
+ startToken.type = 'link_open';
+ startToken.tag = 'a';
+ startToken.attrs = [['href', url], ['data-bbcode', 'true']];
+ startToken.content = '';
+ startToken.nesting = 1;
- token = state.push('link_close', 'a', -1);
- return true;
+ endToken.type = 'link_close';
+ endToken.tag = 'a';
+ endToken.content = '';
+ endToken.nesting = -1;
+ } else {
+ // just strip the bbcode tag
+ endToken.content = '';
+ startToken.content = '';
+
+ // edge case, we don't want this detected as a onebox if auto linked
+ // this ensures it is not stripped
+ startToken.type = 'html_inline';
+ }
+
+ return false;
}
});
@@ -180,9 +202,10 @@ export function setup(helper) {
tag: 'email',
replace: function(state, tagInfo, content) {
let token;
+ let email = tagInfo.attrs['_default'] || content;
token = state.push('link_open', 'a', 1);
- token.attrs = [['href', 'mailto:' + content], ['data-bbcode', 'true']];
+ token.attrs = [['href', 'mailto:' + email], ['data-bbcode', 'true']];
token = state.push('text', '', 0);
token.content = content;
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6
index 5f04b72b1d0..788cdaed14b 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6
@@ -22,6 +22,7 @@ function applyOnebox(state, silent) {
if (j === 0 && token.leading_space) {
continue;
} else if (j > 0) {
+
let prevSibling = token.children[j-1];
if (prevSibling.tag !== 'br' || prevSibling.leading_space) {
@@ -45,8 +46,12 @@ function applyOnebox(state, silent) {
continue;
}
- // we already know text matches cause it is an auto link
+ // edge case ... what if this is not http or protocoless?
+ if (!/^http|^\/\//i.test(attrs[0][1])) {
+ continue;
+ }
+ // we already know text matches cause it is an auto link
if (!close || close.type !== "link_close") {
continue;
}
@@ -71,6 +76,7 @@ function applyOnebox(state, silent) {
} else {
// decorate...
attrs.push(["class", "onebox"]);
+ attrs.push(["target", "_blank"]);
}
}
}
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6
index 05afd6bc9ef..ba892ef7067 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6
@@ -26,7 +26,7 @@ const rule = {
continue;
}
- if (split[i].indexOf(/full:\s*true/) === 0) {
+ if (/full:\s*true/.test(split[i])) {
full = true;
continue;
}
diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6
index bfd6eaf904f..32064527aed 100644
--- a/app/assets/javascripts/pretty-text/pretty-text.js.es6
+++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6
@@ -1,12 +1,9 @@
import { cook as cookIt, setup as setupIt } from 'pretty-text/engines/discourse-markdown-it';
-import { sanitize } from 'pretty-text/sanitizer';
-import WhiteLister from 'pretty-text/white-lister';
-const _registerFns = [];
-const identity = value => value;
-
-export function registerOption(fn) {
- _registerFns.push(fn);
+export function registerOption() {
+ if (window.console) {
+ window.console.log("registerOption is deprecated");
+ }
}
export function buildOptions(state) {
@@ -24,7 +21,7 @@ export function buildOptions(state) {
emojiUnicodeReplacer
} = state;
- const features = {
+ let features = {
'bold-italics': true,
'auto-link': true,
'mentions': true,
@@ -36,6 +33,10 @@ export function buildOptions(state) {
'newline': !siteSettings.traditional_markdown_linebreaks
};
+ if (state.features) {
+ features = _.merge(features, state.features);
+ }
+
const options = {
sanitize: true,
getURL,
@@ -54,6 +55,8 @@ export function buildOptions(state) {
markdownIt: true
};
+ // note, this will mutate options due to the way the API is designed
+ // may need a refactor
setupIt(options, siteSettings, state);
return options;
@@ -61,9 +64,14 @@ export function buildOptions(state) {
export default class {
constructor(opts) {
- this.opts = opts || {};
- this.opts.features = this.opts.features || {};
- this.opts.sanitizer = (!!this.opts.sanitize) ? (this.opts.sanitizer || sanitize) : identity;
+ if (!opts) {
+ opts = buildOptions({ siteSettings: {}});
+ }
+ this.opts = opts;
+ }
+
+ disableSanitizer() {
+ this.opts.sanitizer = this.opts.discourse.sanitizer = ident => ident;
}
cook(raw) {
@@ -75,6 +83,6 @@ export default class {
}
sanitize(html) {
- return this.opts.sanitizer(html, new WhiteLister(this.opts));
+ return this.opts.sanitizer(html).trim();
}
};
diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
index 0fefc15b6e3..fd1e73c68a7 100644
--- a/lib/pretty_text.rb
+++ b/lib/pretty_text.rb
@@ -175,12 +175,14 @@ module PrettyText
buffer << "__textOptions = __buildOptions(__optInput);\n"
- # Be careful disabling sanitization. We allow for custom emails
- if opts[:sanitize] == false
- buffer << ('__textOptions.sanitize = false;')
- end
buffer << ("__pt = new __PrettyText(__textOptions);")
+
+ # Be careful disabling sanitization. We allow for custom emails
+ if opts[:sanitize] == false
+ buffer << ('__pt.disableSanitizer();')
+ end
+
opts = context.eval(buffer)
DiscourseEvent.trigger(:markdown_context, context)
diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb
index 49bf5db18ad..b3aa2468c92 100644
--- a/spec/components/pretty_text_spec.rb
+++ b/spec/components/pretty_text_spec.rb
@@ -61,7 +61,7 @@ describe PrettyText do
[/quote]
MD
html = <<~HTML
-
Derpy: http://derp.com?__test=1
', "works with double underscores in urls"); - assert.cooked("Derpy: http://derp.com?_test_=1", - 'Derpy: http://derp.com?_test_=1
', - "works with underscores in urls"); - assert.cooked("Atwood: www.codinghorror.com", 'Atwood: www.codinghorror.com
', "autolinks something that begins with www"); @@ -136,11 +146,11 @@ QUnit.test("Links", assert => { "autolinks a URL with parentheses (like Wikipedia)"); assert.cooked("Here's a tweet:\nhttps://twitter.com/evil_trout/status/345954894420787200", - "Here's a tweet:
https://twitter.com/evil_trout/status/345954894420787200
Here's a tweet:
\nhttps://twitter.com/evil_trout/status/345954894420787200
Link (with an outer \"description\")
", + "Link (with an outer "description")
", "it doesn't consume closing parens as part of the url"); assert.cooked("A link inside parentheses (http://www.example.com)", @@ -188,50 +198,76 @@ QUnit.test("Links", assert => { }); QUnit.test("simple quotes", assert => { - assert.cooked("> nice!", "", "it supports simple quotes"); - assert.cooked(" > nice!", "nice!
", "it allows quotes with preceding spaces"); + assert.cooked("> nice!", "nice!
\n", "it supports simple quotes"); + assert.cooked(" > nice!", "nice!
\n
\n", "it allows quotes with preceding spaces"); assert.cooked("> level 1\n> > level 2", - "nice!
\n
", + "level 1
level 2
\n", "it allows nesting of blockquotes"); assert.cooked("> level 1\n> > level 2", - "level 1
\n\n\nlevel 2
\n
", + "level 1
level 2
\n", "it allows nesting of blockquotes with spaces"); assert.cooked("- hello\n\n > world\n > eviltrout", - "level 1
\n\n\nlevel 2
\n
", +`world
eviltrout
hello
+++world
+
+eviltrout
eviltrout
", - "eviltrout
eviltrout
", "allow multiple spaces to indent"); + assert.cooked(" > indent 1\n > indent 2", "indent 1
indent 2
\n", "allow multiple spaces to indent"); }); QUnit.test("Quotes", assert => { - assert.cookedOptions("[quote=\"eviltrout, post: 1\"]\na quote\n\nsecond line\n\nthird line[/quote]", + assert.cookedOptions("[quote=\"eviltrout, post: 1\"]\na quote\n\nsecond line\n\nthird line\n[/quote]", { topicId: 2 }, - "indent 1
\n
\nindent 2
" + - "a quote
second line
third line
++a quote
+second line
+third line
+
1
\n\nmy quote
2
", - "handles quotes properly"); - assert.cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2", + assert.cookedOptions("[quote=\"bob, post:1\"]\nmy quote\n[/quote]", { topicId: 2, lookupAvatar: function() { } }, - "1
\n\nmy quote
2
", + `++my quote
+
a b
++a
++ +++b
+
hanzo55@yahoo.com
", + "", "won't be affected by email addresses that have a number before the @ symbol"); assert.cooked("@EvilTrout yo", @@ -269,7 +305,7 @@ QUnit.test("Mentions", assert => { "it handles mentions at the beginning of a string"); assert.cooked("yo\n@EvilTrout", - "yo
@EvilTrout
yo
\n@EvilTrout
blocks");
assert.cooked("```\na @test\n```",
- "a @test
",
+ "a @test\n
",
"should not do mentions within a code block.");
assert.cooked("> foo bar baz @eviltrout",
- "foo bar baz @eviltrout
",
+ "\nfoo bar baz @eviltrout
\n
",
"handles mentions in simple quotes");
assert.cooked("> foo bar baz @eviltrout ohmagerd\nlook at this",
- "foo bar baz @eviltrout ohmagerd
look at this
",
+ "\nfoo bar baz @eviltrout ohmagerd
\nlook at this
\n
",
"does mentions properly with trailing text within a simple quote");
assert.cooked("`code` is okay before @mention",
@@ -309,7 +345,7 @@ QUnit.test("Mentions", assert => {
"you can have a mention in an inline code block following a real mention.");
assert.cooked("1. this is a list\n\n2. this is an @eviltrout mention\n",
- "this is a list
this is an @eviltrout mention
",
+ "\n- \n
this is a list
\n \n- \n
this is an @eviltrout mention
\n \n
",
"it mentions properly in a list.");
assert.cooked("Hello @foo/@bar",
@@ -341,11 +377,11 @@ QUnit.test("Category hashtags", assert => {
"it does not translate category hashtag within links");
assert.cooked("```\n# #category-hashtag\n```",
- "# #category-hashtag
",
+ "# #category-hashtag\n
",
"it does not translate category hashtags to links in code blocks");
assert.cooked("># #category-hashtag\n",
- "#category-hashtag
",
+ "\n#category-hashtag
\n
",
"it handles category hashtags in simple quotes");
assert.cooked("# #category-hashtag",
@@ -356,10 +392,6 @@ QUnit.test("Category hashtags", assert => {
"don't #category-hashtag
",
"it does not mention in an inline code block");
- assert.cooked("test #hashtag1/#hashtag2",
- "test #hashtag1/#hashtag2
",
- "it does not convert category hashtag not bounded by spaces");
-
assert.cooked("#category-hashtag",
"#category-hashtag
",
"it works between HTML tags");
@@ -371,14 +403,12 @@ QUnit.test("Heading", assert => {
});
QUnit.test("bold and italics", assert => {
- assert.cooked("a \"**hello**\"", "a \"hello\"
", "bolds in quotes");
+ assert.cooked("a \"**hello**\"", "a "hello"
", "bolds in quotes");
assert.cooked("(**hello**)", "(hello)
", "bolds in parens");
- assert.cooked("**hello**\nworld", "hello
world
", "allows newline after bold");
- assert.cooked("**hello**\n**world**", "hello
world
", "newline between two bolds");
- assert.cooked("**a*_b**", "a*_b
", "allows for characters within bold");
+ assert.cooked("**hello**\nworld", "hello
\nworld
", "allows newline after bold");
+ assert.cooked("**hello**\n**world**", "hello
\nworld
", "newline between two bolds");
assert.cooked("** hello**", "** hello**
", "does not bold on a space boundary");
assert.cooked("**hello **", "**hello **
", "does not bold on a space boundary");
- assert.cooked("你**hello**", "你**hello**
", "does not bold chinese intra word");
assert.cooked("**你hello**", "你hello
", "allows bolded chinese");
});
@@ -388,10 +418,11 @@ QUnit.test("Escaping", assert => {
});
QUnit.test("New Lines", assert => {
- // Note: This behavior was discussed and we determined it does not make sense to do this
- // unless you're using traditional line breaks
- assert.cooked("_abc\ndef_", "_abc
def_
", "it does not allow markup to span new lines");
- assert.cooked("_abc\n\ndef_", "_abc
\n\ndef_
", "it does not allow markup to span new paragraphs");
+ // historically we would not continue inline em or b across lines,
+ // however commonmark gives us no switch to do so and we would be very non compliant.
+ // turning softbreaks into a newline is just a renderer option, not a parser switch.
+ assert.cooked("_abc\ndef_", "abc
\ndef
", "it does allow inlines to span new lines");
+ assert.cooked("_abc\n\ndef_", "_abc
\ndef_
", "it does not allow inlines to span new paragraphs");
});
QUnit.test("Oneboxing", assert => {
@@ -408,9 +439,9 @@ QUnit.test("Oneboxing", assert => {
assert.ok(!matches("http://test.com bob", /onebox/), "doesn't onebox links that have trailing text");
assert.ok(!matches("[Tom Cruise](http://www.tomcruise.com/)", "onebox"), "Markdown links with labels are not oneboxed");
- assert.ok(matches("[http://www.tomcruise.com/](http://www.tomcruise.com/)",
+ assert.ok(!matches("[http://www.tomcruise.com/](http://www.tomcruise.com/)",
"onebox"),
- "Markdown links where the label is the same as the url are oneboxed");
+ "Markdown links where the label is the same as the url but link is explicit");
assert.cooked("http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street",
" {
QUnit.test("Code Blocks", assert => {
assert.cooked("\nhello\n
\n",
- "hello
",
+ "\nhello\n
",
"pre blocks don't include extra lines");
assert.cooked("```\na\nb\nc\n\nd\n```",
- "a\nb\nc\n\nd
",
+ "a\nb\nc\n\nd\n
",
"it treats new lines properly");
assert.cooked("```\ntest\n```",
- "test
",
+ "test\n
",
"it supports basic code blocks");
assert.cooked("```json\n{hello: 'world'}\n```\ntrailing",
- "{hello: 'world'}
\n\ntrailing
",
+ "{hello: 'world'}\n
\ntrailing
",
"It does not truncate text after a code block.");
assert.cooked("```json\nline 1\n\nline 2\n\n\nline3\n```",
- "line 1\n\nline 2\n\n\nline3
",
+ "line 1\n\nline 2\n\n\nline3\n
",
"it maintains new lines inside a code block.");
assert.cooked("hello\nworld\n```json\nline 1\n\nline 2\n\n\nline3\n```",
- "hello
world
\n\nline 1\n\nline 2\n\n\nline3
",
+ "hello
\nworld
\nline 1\n\nline 2\n\n\nline3\n
",
"it maintains new lines inside a code block with leading content.");
assert.cooked("```ruby\nhello \n```",
- "<header>hello</header>
",
+ "<header>hello</header>\n
",
"it escapes code in the code block");
assert.cooked("```text\ntext\n```",
- "text
",
+ "text\n
",
"handles text by adding nohighlight");
assert.cooked("```ruby\n# cool\n```",
- "# cool
",
+ "# cool\n
",
"it supports changing the language");
assert.cooked(" ```\n hello\n ```",
- "```\nhello\n```
",
+ "```\nhello\n```
",
"only detect ``` at the beginning of lines");
assert.cooked("```ruby\ndef self.parse(text)\n\n text\nend\n```",
- "def self.parse(text)\n\n text\nend
",
+ "def self.parse(text)\n\n text\nend\n
",
"it allows leading spaces on lines in a code block.");
assert.cooked("```ruby\nhello `eviltrout`\n```",
- "hello `eviltrout`
",
+ "hello `eviltrout`\n
",
"it allows code with backticks in it");
assert.cooked("```eviltrout\nhello\n```",
- "hello
",
+ "hello\n
",
"it doesn't not whitelist all classes");
assert.cooked("```\n[quote=\"sam, post:1, topic:9441, full:true\"]This is `` a bug.[/quote]\n```",
- "[quote="sam, post:1, topic:9441, full:true"]This is `<not>` a bug.[/quote]
",
+ "[quote="sam, post:1, topic:9441, full:true"]This is `<not>` a bug.[/quote]\n
",
"it allows code with backticks in it");
assert.cooked(" hello\ntest
",
- "hello
\n\ntest
",
+ "hello\n
\ntest
",
"it allows an indented code block to by followed by a ``");
assert.cooked("``` foo bar ```",
@@ -492,7 +523,7 @@ QUnit.test("Code Blocks", assert => {
"it tolerates misuse of code block tags as inline code");
assert.cooked("```\nline1\n```\n```\nline2\n\nline3\n```",
- "line1
\n\nline2\n\nline3
",
+ "line1\n
\nline2\n\nline3\n
",
"it does not consume next block's trailing newlines");
assert.cooked(" test
",
@@ -504,22 +535,22 @@ QUnit.test("Code Blocks", assert => {
"it does not parse other block types in markdown code blocks");
assert.cooked("## a\nb\n```\nc\n```",
- "a
\n\nc
",
+ "a
\nb
\nc\n
",
"it handles headings with code blocks after them.");
});
QUnit.test("URLs in BBCode tags", assert => {
assert.cooked("[img]http://eviltrout.com/eviltrout.png[/img][img]http://samsaffron.com/samsaffron.png[/img]",
- "",
+ "",
"images are properly parsed");
assert.cooked("[url]http://discourse.org[/url]",
- "",
+ "",
"links are properly parsed");
assert.cooked("[url=http://discourse.org]discourse[/url]",
- "",
+ "",
"named links are properly parsed");
});
@@ -530,39 +561,39 @@ QUnit.test("images", assert => {
"It allows images with links around them");
assert.cooked("",
- "",
+ "\n
",
"It allows data images");
});
QUnit.test("censoring", assert => {
assert.cooked("aw shucks, golly gee whiz.",
- "aw ■■■■■■, golly gee ■■■■.
",
+ "aw ■■■■■■, golly gee ■■■■.
",
"it censors words in the Site Settings");
assert.cooked("you are a whizzard! I love cheesewhiz. Whiz.",
- "you are a whizzard! I love cheesewhiz. ■■■■.
",
+ "you are a whizzard! I love cheesewhiz. ■■■■.
",
"it doesn't censor words unless they have boundaries.");
assert.cooked("you are a whizzer! I love cheesewhiz. Whiz.",
- "you are a ■■■■■■■! I love cheesewhiz. ■■■■.
",
+ "you are a ■■■■■■■! I love cheesewhiz. ■■■■.
",
"it censors words even if previous partial matches exist.");
assert.cooked("The link still works. [whiz](http://www.whiz.com)",
- "The link still works. ■■■■
",
+ "The link still works. ■■■■
",
"it won't break links by censoring them.");
assert.cooked("Call techapj the computer whiz at 555-555-1234 for free help.",
- "Call ■■■■■■■ the computer ■■■■ at 555-■■■■■■■■ for free help.
",
+ "Call ■■■■■■■ the computer ■■■■ at 555-■■■■■■■■ for free help.
",
"uses both censored words and patterns from site settings");
assert.cooked("I have a pen, I have an a**le",
- "I have a pen, I have an ■■■■■
",
+ "I have a pen, I have an ■■■■■
",
"it escapes regexp chars");
});
QUnit.test("code blocks/spans hoisting", assert => {
assert.cooked("```\n\n some code\n```",
- " some code
",
+ "\n some code\n
",
"it works when nesting standard markdown code blocks within a fenced code block");
assert.cooked("`$&`",
@@ -575,47 +606,42 @@ QUnit.test('basic bbcode', assert => {
assert.cookedPara("[i]emphasis[/i]", "emphasis", "italics text");
assert.cookedPara("[u]underlined[/u]", "underlined", "underlines text");
assert.cookedPara("[s]strikethrough[/s]", "strikethrough", "strikes-through text");
- assert.cookedPara("[img]http://eviltrout.com/eviltrout.png[/img]", "", "links images");
- assert.cookedPara("[email]eviltrout@mailinator.com[/email]", "eviltrout@mailinator.com", "supports [email] without a title");
+ assert.cookedPara("[img]http://eviltrout.com/eviltrout.png[/img]", "", "links images");
+ assert.cookedPara("[email]eviltrout@mailinator.com[/email]", "eviltrout@mailinator.com", "supports [email] without a title");
assert.cookedPara("[b]evil [i]trout[/i][/b]",
"evil trout",
"allows embedding of tags");
- assert.cookedPara("[EMAIL]eviltrout@mailinator.com[/EMAIL]", "eviltrout@mailinator.com", "supports upper case bbcode");
+ assert.cookedPara("[EMAIL]eviltrout@mailinator.com[/EMAIL]", "eviltrout@mailinator.com", "supports upper case bbcode");
assert.cookedPara("[b]strong [b]stronger[/b][/b]", "strong stronger", "accepts nested bbcode tags");
});
QUnit.test('urls', assert => {
assert.cookedPara("[url]not a url[/url]", "not a url", "supports [url] that isn't a url");
- assert.cookedPara("[url]abc.com[/url]", "abc.com", "no error when a url has no protocol and begins with a");
- assert.cookedPara("[url]http://bettercallsaul.com[/url]", "http://bettercallsaul.com", "supports [url] without parameter");
- assert.cookedPara("[url=http://example.com]example[/url]", "example", "supports [url] with given href");
+ assert.cookedPara("[url]abc.com[/url]", "abc.com", "it magically links using linkify");
+ assert.cookedPara("[url]http://bettercallsaul.com[/url]", "http://bettercallsaul.com", "supports [url] without parameter");
+ assert.cookedPara("[url=http://example.com]example[/url]", "example", "supports [url] with given href");
assert.cookedPara("[url=http://www.example.com][img]http://example.com/logo.png[/img][/url]",
- "",
+ "",
"supports [url] with an embedded [img]");
});
QUnit.test('invalid bbcode', assert => {
- const result = new PrettyText({ lookupAvatar: false }).cook("[code]I am not closed\n\nThis text exists.");
- assert.equal(result, "[code]I am not closed
\n\nThis text exists.
", "does not raise an error with an open bbcode tag.");
+ assert.cooked("[code]I am not closed\n\nThis text exists.",
+ "[code]I am not closed
\nThis text exists.
",
+ "does not raise an error with an open bbcode tag.");
});
QUnit.test('code', assert => {
- assert.cookedPara("[code]\nx++\n[/code]", "x++
", "makes code into pre");
- assert.cookedPara("[code]\nx++\ny++\nz++\n[/code]", "x++\ny++\nz++
", "makes code into pre");
- assert.cookedPara("[code]abc\n#def\n[/code]", 'abc\n#def
', 'it handles headings in a [code] block');
- assert.cookedPara("[code]\n s[/code]",
+ assert.cooked("[code]\nx++\n[/code]", "x++
", "makes code into pre");
+ assert.cooked("[code]\nx++\ny++\nz++\n[/code]", "x++\ny++\nz++
", "makes code into pre");
+ assert.cooked("[code]\nabc\n#def\n[/code]", 'abc\n#def
', 'it handles headings in a [code] block');
+ assert.cooked("[code]\n s\n[/code]",
" s
",
"it doesn't trim leading whitespace");
});
-QUnit.test('lists', assert => {
- assert.cookedPara("[ul][li]option one[/li][/ul]", "- option one
", "creates an ul");
- assert.cookedPara("[ol][li]option one[/li][/ol]", "- option one
", "creates an ol");
- assert.cookedPara("[ul]\n[li]option one[/li]\n[li]option two[/li]\n[/ul]", "- option one
- option two
", "suppresses empty lines in lists");
-});
-
QUnit.test('tags with arguments', assert => {
- assert.cookedPara("[url=http://bettercallsaul.com]better call![/url]", "better call!", "supports [url] with a title");
- assert.cookedPara("[email=eviltrout@mailinator.com]evil trout[/email]", "evil trout", "supports [email] with a title");
+ assert.cookedPara("[url=http://bettercallsaul.com]better call![/url]", "better call!", "supports [url] with a title");
+ assert.cookedPara("[email=eviltrout@mailinator.com]evil trout[/email]", "evil trout", "supports [email] with a title");
assert.cookedPara("[u][i]abc[/i][/u]", "abc", "can nest tags");
assert.cookedPara("[b]first[/b] [b]second[/b]", "first second", "can bold two things on the same line");
});
@@ -655,70 +681,140 @@ QUnit.test("quotes", assert => {
"[quote=\"eviltrout, post:1, topic:2\"]\nthis is <not> a bug\n[/quote]\n\n",
"it escapes the contents of the quote");
- assert.cookedPara("[quote]test[/quote]",
- "test
",
+ assert.cooked("[quote]\ntest\n[/quote]",
+ "\n\ntest
\n
\n ",
"it supports quotes without params");
- assert.cookedPara("[quote]\n*test*\n[/quote]",
- "test
",
+ assert.cooked("[quote]\n*test*\n[/quote]",
+ "\n\ntest
\n
\n ",
"it doesn't insert a new line for italics");
- assert.cookedPara("[quote=,script='a'>", "hello
", "it sanitizes while cooking");
cooked("disney reddit",
"",
"we can embed proper links");
- cooked("hello ", "hello
", "it does not allow centering");
- cooked("hello
\nafter", "after
", "it does not allow tables");
- cooked("a\n
\n", "a\n\n
\n\n
", "it does not double sanitize");
+ cooked("hello ", "hello", "it does not allow centering");
+ cooked("a\n
\n", "a\n
", "it does not double sanitize");
cooked("", "", "it does not allow most iframes");
@@ -38,9 +38,9 @@ QUnit.test("sanitize", assert => {
assert.equal(pt.sanitize("