From f1b38ba4fb778c08a45dd001300fc651c92718ba Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 12 Jul 2017 16:59:58 -0400 Subject: [PATCH] Integrate new engine, correct old specs corrects edge cases with - full quotes - [url] with nested tags - engine overrides - onebox applying to non http srcs --- .../engines/discourse-markdown-it.js.es6 | 3 +- .../engines/discourse-markdown.js.es6 | 597 ------------------ .../discourse-markdown/bbcode-inline.js.es6 | 47 +- .../engines/discourse-markdown/onebox.js.es6 | 8 +- .../engines/discourse-markdown/quotes.js.es6 | 2 +- .../pretty-text/pretty-text.js.es6 | 32 +- lib/pretty_text.rb | 10 +- spec/components/pretty_text_spec.rb | 30 +- test/javascripts/lib/pretty-text-test.js.es6 | 382 ++++++----- test/javascripts/lib/sanitizer-test.js.es6 | 10 +- 10 files changed, 341 insertions(+), 780 deletions(-) delete mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown.js.es6 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 fn(event)); - - for (var j=0; j$/.exec(n[1])) { - // Remove paragraphs around comment-only nodes. - tree[i] = n[1]; - } else { - parseTree(n, options, path, insideCounts); - } - - insideCounts[tagName] = insideCounts[tagName] - 1; - } - - // If raw nodes are in paragraphs, pull them up - if (tree.length === 2 && tree[0] === 'p' && tree[1] instanceof Array && tree[1][0] === "__RAW") { - var text = tree[1][1]; - tree[0] = "__RAW"; - tree[1] = text; - } - - path.pop(); - } - return tree; -} - -// Returns true if there's an invalid word boundary for a match. -function invalidBoundary(args, prev) { - if (!(args.wordBoundary || args.spaceBoundary || args.spaceOrTagBoundary)) { return false; } - - var last = prev[prev.length - 1]; - if (typeof last !== "string") { return false; } - - if (args.wordBoundary && (!last.match(/\W$/))) { return true; } - if (args.spaceBoundary && (!last.match(/\s$/))) { return true; } - if (args.spaceOrTagBoundary && (!last.match(/(\s|\>|\()$/))) { return true; } -} - -function countLines(str) { - let index = -1, count = 0; - while ((index = str.indexOf("\n", index + 1)) !== -1) { count++; } - return count; -} - -function hoister(t, target, replacement) { - const regexp = new RegExp(target.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), "g"); - if (t.match(regexp)) { - const hash = guid(); - t = t.replace(regexp, hash); - hoisted[hash] = replacement; - } - return t; -} - -function outdent(t) { - return t.replace(/^([ ]{4}|\t)/gm, ""); -} - -function removeEmptyLines(t) { - return t.replace(/^\n+/, "").replace(/\s+$/, ""); -} - -function hideBackslashEscapedCharacters(t) { - return t.replace(/\\\\/g, "\u1E800").replace(/\\`/g, "\u1E8001"); -} - -function showBackslashEscapedCharacters(t) { - return t.replace(/\u1E8001/g, "\\`").replace(/\u1E800/g, "\\\\"); -} - -function hoistCodeBlocksAndSpans(text) { - // replace all "\`" with a single character - text = hideBackslashEscapedCharacters(text); - - // /!\ the order is important /!\ - - // fenced code blocks (AKA GitHub code blocks) - text = text.replace(/(^\n*|\n)```([a-z0-9\-]*)\n([\s\S]*?)\n```/g, function(_, before, language, content) { - const hash = guid(); - hoisted[hash] = escape(showBackslashEscapedCharacters(removeEmptyLines(content))); - return before + "```" + language + "\n" + hash + "\n```"; - }); - - // markdown code blocks - text = text.replace(/(^\n*|\n\n)((?:(?:[ ]{4}|\t).*\n*)+)/g, function(match, before, content, index) { - // make sure we aren't in a list - var previousLine = text.slice(0, index).trim().match(/.*$/); - if (previousLine && previousLine[0].length) { - previousLine = previousLine[0].trim(); - if (/^(?:\*|\+|-|\d+\.)\s+/.test(previousLine)) { - return match; - } - } - // we can safely hoist the code block - const hash = guid(); - hoisted[hash] = escape(outdent(showBackslashEscapedCharacters(removeEmptyLines(content)))); - return before + " " + hash + "\n"; - }); - - //
...
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 -