diff --git a/Gemfile.lock b/Gemfile.lock index a2b7197d260..ac8f70d4497 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -156,7 +156,7 @@ GEM mime-types (2.99.3) mini_mime (0.1.3) mini_portile2 (2.2.0) - mini_racer (0.1.9) + mini_racer (0.1.10) libv8 (~> 5.3) minitest (5.10.2) mocha (1.2.1) diff --git a/app/assets/javascripts/markdown-it-bundle.js b/app/assets/javascripts/markdown-it-bundle.js index 3460c3a6027..dcacaf1748d 100644 --- a/app/assets/javascripts/markdown-it-bundle.js +++ b/app/assets/javascripts/markdown-it-bundle.js @@ -1,15 +1,15 @@ //= require markdown-it.js -//= require ./pretty-text/engines/markdown-it/helpers -//= require ./pretty-text/engines/markdown-it/mentions -//= require ./pretty-text/engines/markdown-it/quotes -//= require ./pretty-text/engines/markdown-it/emoji -//= require ./pretty-text/engines/markdown-it/onebox -//= require ./pretty-text/engines/markdown-it/bbcode-block -//= require ./pretty-text/engines/markdown-it/bbcode-inline -//= require ./pretty-text/engines/markdown-it/code -//= require ./pretty-text/engines/markdown-it/category-hashtag -//= require ./pretty-text/engines/markdown-it/censored -//= require ./pretty-text/engines/markdown-it/table -//= require ./pretty-text/engines/markdown-it/paragraph -//= require ./pretty-text/engines/markdown-it/newline -//= require ./pretty-text/engines/markdown-it/html_img +//= require ./pretty-text/engines/discourse-markdown/helpers +//= require ./pretty-text/engines/discourse-markdown/mentions +//= require ./pretty-text/engines/discourse-markdown/quotes +//= require ./pretty-text/engines/discourse-markdown/emoji +//= require ./pretty-text/engines/discourse-markdown/onebox +//= require ./pretty-text/engines/discourse-markdown/bbcode-block +//= require ./pretty-text/engines/discourse-markdown/bbcode-inline +//= require ./pretty-text/engines/discourse-markdown/code +//= require ./pretty-text/engines/discourse-markdown/category-hashtag +//= require ./pretty-text/engines/discourse-markdown/censored +//= require ./pretty-text/engines/discourse-markdown/table +//= require ./pretty-text/engines/discourse-markdown/paragraph +//= require ./pretty-text/engines/discourse-markdown/newline +//= require ./pretty-text/engines/discourse-markdown/html_img diff --git a/app/assets/javascripts/pretty-text-bundle.js b/app/assets/javascripts/pretty-text-bundle.js index 691fad8e9a5..869631eda19 100644 --- a/app/assets/javascripts/pretty-text-bundle.js +++ b/app/assets/javascripts/pretty-text-bundle.js @@ -3,11 +3,8 @@ //= require ./pretty-text/censored-words //= require ./pretty-text/emoji/data //= require ./pretty-text/emoji -//= require ./pretty-text/engines/discourse-markdown //= require ./pretty-text/engines/discourse-markdown-it -//= require_tree ./pretty-text/engines/discourse-markdown //= require xss.min -//= require better_markdown.js //= require ./pretty-text/xss //= require ./pretty-text/white-lister //= require ./pretty-text/sanitizer 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 ac80029cfdb..0011709fe18 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 @@ -1,4 +1,4 @@ -import { default as WhiteLister, whiteListFeature } from 'pretty-text/white-lister'; +import { default as WhiteLister } from 'pretty-text/white-lister'; import { sanitize } from 'pretty-text/sanitizer'; import guid from 'pretty-text/guid'; @@ -10,10 +10,10 @@ function deprecate(feature, name){ }; } -function createHelper(featureName, opts, optionCallbacks, pluginCallbacks, getOptions) { +function createHelper(featureName, opts, optionCallbacks, pluginCallbacks, getOptions, whiteListed) { let helper = {}; helper.markdownIt = true; - helper.whiteList = info => whiteListFeature(featureName, info); + helper.whiteList = info => whiteListed.push([featureName, info]); helper.registerInline = deprecate(featureName,'registerInline'); helper.replaceBlock = deprecate(featureName,'replaceBlock'); helper.addPreProcessor = deprecate(featureName,'addPreProcessor'); @@ -151,7 +151,7 @@ export function setup(opts, siteSettings, state) { } // we got to require this late cause bundle is not loaded in pretty-text - Helpers = Helpers || requirejs('pretty-text/engines/markdown-it/helpers'); + Helpers = Helpers || requirejs('pretty-text/engines/discourse-markdown/helpers'); opts.markdownIt = true; @@ -165,6 +165,7 @@ export function setup(opts, siteSettings, state) { const check = /discourse-markdown\/|markdown-it\//; let features = []; + let whiteListed = []; Object.keys(require._eak_seen).forEach(entry => { if (check.test(entry)) { @@ -173,7 +174,7 @@ export function setup(opts, siteSettings, state) { const featureName = entry.split('/').reverse()[0]; features.push(featureName); - module.setup(createHelper(featureName, opts, optionCallbacks, pluginCallbacks, getOptions)); + module.setup(createHelper(featureName, opts, optionCallbacks, pluginCallbacks, getOptions, whiteListed)); } } }); @@ -227,10 +228,16 @@ 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); + + whiteListed.forEach(([feature, info]) => { + whiteLister.whiteListFeature(feature, info); + }); + 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/auto-link.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/auto-link.js.es6 deleted file mode 100644 index 78ebe441fd6..00000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/auto-link.js.es6 +++ /dev/null @@ -1,27 +0,0 @@ -// This addition handles auto linking of text. When included, it will parse out links and create -// ``s for them. - -const urlReplacerArgs = { - matcher: /^((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/, - spaceOrTagBoundary: true, - - emitter(matches) { - const url = matches[1]; - let href = url; - - // Don't autolink a markdown link to something - if (url.match(/\]\[\d$/)) { return; } - - // If we improperly caught a markdown link abort - if (url.match(/\(http/)) { return; } - - if (url.match(/^www/)) { href = "http://" + url; } - return ['a', { href }, url]; - } -}; - -export function setup(helper) { - if (helper.markdownIt) { return; } - helper.inlineRegexp(_.merge({start: 'http'}, urlReplacerArgs)); - helper.inlineRegexp(_.merge({start: 'www'}, urlReplacerArgs)); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-block.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 similarity index 100% rename from app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-block.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-inline.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-inline.js.es6 similarity index 75% rename from app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-inline.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-inline.js.es6 index 2184adb7ffd..a2dfa571437 100644 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/bbcode-inline.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-inline.js.es6 @@ -1,4 +1,4 @@ -import { parseBBCodeTag } from 'pretty-text/engines/markdown-it/bbcode-block'; +import { parseBBCodeTag } from 'pretty-text/engines/discourse-markdown/bbcode-block'; function tokanizeBBCode(state, silent, ruler) { @@ -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/bbcode.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode.js.es6 deleted file mode 100644 index d0118497523..00000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode.js.es6 +++ /dev/null @@ -1,170 +0,0 @@ -export function register(helper, codeName, args, emitter) { - // Optional second param for args - if (typeof args === "function") { - emitter = args; - args = {}; - } - - helper.replaceBlock({ - start: new RegExp("\\[" + codeName + "(=[^\\[\\]]+)?\\]([\\s\\S]*)", "igm"), - stop: new RegExp("\\[\\/" + codeName + "\\]", "igm"), - emitter(blockContents, matches) { - - - const options = helper.getOptions(); - while (blockContents.length && (typeof blockContents[0] === "string" || blockContents[0] instanceof String)) { - blockContents[0] = String(blockContents[0]).replace(/^\s+/, ''); - if (!blockContents[0].length) { - blockContents.shift(); - } else { - break; - } - } - - let contents = []; - if (blockContents.length) { - const nextContents = blockContents.slice(1); - blockContents = this.processBlock(blockContents[0], nextContents); - - nextContents.forEach(nc => { - blockContents = blockContents.concat(this.processBlock(nc, [])); - }); - - blockContents.forEach(bc => { - if (typeof bc === "string" || bc instanceof String) { - var processed = this.processInline(String(bc)); - if (processed.length) { - contents.push(['p'].concat(processed)); - } - } else { - contents.push(bc); - } - }); - } - if (!args.singlePara && contents.length === 1 && contents[0] instanceof Array && contents[0][0] === "para") { - contents[0].shift(); - contents = contents[0]; - } - const result = emitter(contents, matches[1] ? matches[1].replace(/^=|\"/g, '') : null, options); - return args.noWrap ? result : ['p', result]; - } - }); -}; - -export function builders(helper) { - function replaceBBCode(tag, emitter, opts) { - const start = `[${tag}]`; - const stop = `[/${tag}]`; - - opts = opts || {}; - opts = _.merge(opts, { start, stop, emitter }); - helper.inlineBetween(opts); - - opts = _.merge(opts, { start: start.toUpperCase(), stop: stop.toUpperCase(), emitter }); - helper.inlineBetween(opts); - } - - return { - replaceBBCode, - - register(codeName, args, emitter) { - register(helper, codeName, args, emitter); - }, - - rawBBCode(tag, emitter) { - replaceBBCode(tag, emitter, { rawContents: true }); - }, - - removeEmptyLines(contents) { - const result = []; - for (let i=0; i < contents.length; i++) { - if (contents[i] !== "\n") { result.push(contents[i]); } - } - return result; - }, - - replaceBBCodeParamsRaw(tag, emitter) { - var opts = { - rawContents: true, - emitter(contents) { - const m = /^([^\]]+)\]([\S\s]*)$/.exec(contents); - if (m) { return emitter.call(this, m[1], m[2]); } - } - }; - - helper.inlineBetween(_.merge(opts, { start: "[" + tag + "=", stop: "[/" + tag + "]" })); - - tag = tag.toUpperCase(); - helper.inlineBetween(_.merge(opts, { start: "[" + tag + "=", stop: "[/" + tag + "]" })); - } - }; -} - -export function setup(helper) { - - if (helper.markdownIt) { return; } - - helper.whiteList(['span.bbcode-b', 'span.bbcode-i', 'span.bbcode-u', 'span.bbcode-s']); - - const { replaceBBCode, rawBBCode, removeEmptyLines, replaceBBCodeParamsRaw } = builders(helper); - - replaceBBCode('b', contents => ['span', {'class': 'bbcode-b'}].concat(contents)); - replaceBBCode('i', contents => ['span', {'class': 'bbcode-i'}].concat(contents)); - replaceBBCode('u', contents => ['span', {'class': 'bbcode-u'}].concat(contents)); - replaceBBCode('s', contents => ['span', {'class': 'bbcode-s'}].concat(contents)); - - replaceBBCode('ul', contents => ['ul'].concat(removeEmptyLines(contents))); - replaceBBCode('ol', contents => ['ol'].concat(removeEmptyLines(contents))); - replaceBBCode('li', contents => ['li'].concat(removeEmptyLines(contents))); - - rawBBCode('img', href => ['img', {href}]); - rawBBCode('email', contents => ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]); - - replaceBBCode('url', contents => { - if (!Array.isArray(contents)) { return; } - - const first = contents[0]; - if (contents.length === 1 && Array.isArray(first) && first[0] === 'a') { - // single-line bbcode links shouldn't be oneboxed, so we mark this as a bbcode link. - if (typeof first[1] !== 'object') { first.splice(1, 0, {}); } - first[1]['data-bbcode'] = true; - } - return ['concat'].concat(contents); - }); - - replaceBBCodeParamsRaw('url', function(param, contents) { - const url = param.replace(/(^")|("$)/g, ''); - return ['a', {'href': url}].concat(this.processInline(contents)); - }); - - replaceBBCodeParamsRaw("email", function(param, contents) { - return ['a', {href: "mailto:" + param, 'data-bbcode': true}].concat(contents); - }); - - helper.onParseNode(event => { - if (!Array.isArray(event.node)) { return; } - const result = [event.node[0]]; - const nodes = event.node.slice(1); - for (let i = 0; i < nodes.length; i++) { - if (Array.isArray(nodes[i]) && nodes[i][0] === 'concat') { - for (let j = 1; j < nodes[i].length; j++) { result.push(nodes[i][j]); } - } else { - result.push(nodes[i]); - } - } - for (let i = 0; i < result.length; i++) { event.node[i] = result[i]; } - }); - - helper.replaceBlock({ - start: /(\[code\])([\s\S]*)/igm, - stop: /\[\/code\]/igm, - rawContents: true, - - emitter(blockContents) { - const options = helper.getOptions(); - const inner = blockContents.join("\n"); - const defaultCodeLang = options.defaultCodeLang; - return ['p', ['pre', ['code', {'class': `lang-${defaultCodeLang}`}, inner]]]; - } - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bold-italics.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bold-italics.js.es6 deleted file mode 100644 index a79a983b08b..00000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bold-italics.js.es6 +++ /dev/null @@ -1,73 +0,0 @@ -import guid from 'pretty-text/guid'; - -/** - markdown-js doesn't ensure that em/strong codes are present on word boundaries. - So we create our own handlers here. -**/ - -// From PageDown -const aLetter = /[a-zA-Z0-9\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376-\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0523\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0621-\u064a\u0660-\u0669\u066e-\u066f\u0671-\u06d3\u06d5\u06e5-\u06e6\u06ee-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07c0-\u07ea\u07f4-\u07f5\u07fa\u0904-\u0939\u093d\u0950\u0958-\u0961\u0966-\u096f\u0971-\u0972\u097b-\u097f\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc-\u09dd\u09df-\u09e1\u09e6-\u09f1\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a66-\u0a6f\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0-\u0ae1\u0ae6-\u0aef\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b35-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b66-\u0b6f\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0be6-\u0bef\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58-\u0c59\u0c60-\u0c61\u0c66-\u0c6f\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0-\u0ce1\u0ce6-\u0cef\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d3d\u0d60-\u0d61\u0d66-\u0d6f\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32-\u0e33\u0e40-\u0e46\u0e50-\u0e59\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0ed0-\u0ed9\u0edc-\u0edd\u0f00\u0f20-\u0f29\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8b\u1000-\u102a\u103f-\u1049\u1050-\u1055\u105a-\u105d\u1061\u1065-\u1066\u106e-\u1070\u1075-\u1081\u108e\u1090-\u1099\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1159\u115f-\u11a2\u11a8-\u11f9\u1200-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u1676\u1681-\u169a\u16a0-\u16ea\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u17e0-\u17e9\u1810-\u1819\u1820-\u1877\u1880-\u18a8\u18aa\u1900-\u191c\u1946-\u196d\u1970-\u1974\u1980-\u19a9\u19c1-\u19c7\u19d0-\u19d9\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b50-\u1b59\u1b83-\u1ba0\u1bae-\u1bb9\u1c00-\u1c23\u1c40-\u1c49\u1c4d-\u1c7d\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u203f-\u2040\u2054\u2071\u207f\u2090-\u2094\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2183-\u2184\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2c6f\u2c71-\u2c7d\u2c80-\u2ce4\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3006\u3031-\u3035\u303b-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31b7\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fc3\ua000-\ua48c\ua500-\ua60c\ua610-\ua62b\ua640-\ua65f\ua662-\ua66e\ua67f-\ua697\ua717-\ua71f\ua722-\ua788\ua78b-\ua78c\ua7fb-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8d0-\ua8d9\ua900-\ua925\ua930-\ua946\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa50-\uaa59\uac00-\ud7a3\uf900-\ufa2d\ufa30-\ufa6a\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe33-\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]/; - - -function unhoist(obj,from,to){ - let unhoisted = 0; - const regex = new RegExp(from, "g"); - - if(_.isArray(obj)){ - for (let i=0; i= 0) { - const newText = this.processInline(text.substring(match.length, finish+1)); - const unhoisted_length = unhoist(newText,hash,match[0]); - const array = typeof tag === "string" ? [tag].concat(newText) : [tag[0], [tag[1]].concat(newText)]; - return [(finish + match.length + 1) - unhoisted_length, array]; - } - }); - } - - replaceMarkdown('***', ['strong','em']); - replaceMarkdown('___', ['strong','em']); - replaceMarkdown('**', 'strong'); - replaceMarkdown('__', 'strong'); - replaceMarkdown('*', 'em'); - replaceMarkdown('_', 'em'); -}; diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js.es6 index c33eeff65f6..79d57002a68 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js.es6 @@ -1,20 +1,104 @@ +function addHashtag(buffer, matches, state) { + const options = state.md.options.discourse; + const [hashtag, slug] = matches; + const categoryHashtagLookup = options.categoryHashtagLookup; + const result = categoryHashtagLookup && categoryHashtagLookup(slug); + + let token; + + if (result) { + token = new state.Token('link_open', 'a', 1); + token.attrs = [['class', 'hashtag'], ['href', result[0]]]; + token.block = false; + buffer.push(token); + + token = new state.Token('text', '', 0); + token.content = '#'; + buffer.push(token); + + token = new state.Token('span_open', 'span', 1); + token.block = false; + buffer.push(token); + + token = new state.Token('text', '', 0); + token.content = result[1]; + buffer.push(token); + + buffer.push(new state.Token('span_close', 'span', -1)); + + buffer.push(new state.Token('link_close', 'a', -1)); + } else { + + token = new state.Token('span_open', 'span', 1); + token.attrs = [['class', 'hashtag']]; + buffer.push(token); + + token = new state.Token('text', '', 0); + token.content = hashtag; + buffer.push(token); + + token = new state.Token('span_close', 'span', -1); + buffer.push(token); + } +} + +const REGEX = /#([\w-:]{1,101})/gi; + +function allowedBoundary(content, index, utils) { + let code = content.charCodeAt(index); + return (utils.isWhiteSpace(code) || utils.isPunctChar(String.fromCharCode(code))); +} + +function applyHashtag(content, state) { + let result = null, + match, + pos = 0; + + while (match = REGEX.exec(content)) { + // check boundary + if (match.index > 0) { + if (!allowedBoundary(content, match.index-1, state.md.utils)) { + continue; + } + } + + // check forward boundary as well + if (match.index + match[0].length < content.length) { + if (!allowedBoundary(content, match.index + match[0].length, state.md.utils)) { + continue; + } + } + + if (match.index > pos) { + result = result || []; + let token = new state.Token('text', '', 0); + token.content = content.slice(pos, match.index); + result.push(token); + } + + result = result || []; + addHashtag(result, match, state); + + pos = match.index + match[0].length; + } + + if (result && pos < content.length) { + let token = new state.Token('text', '', 0); + token.content = content.slice(pos); + result.push(token); + } + + return result; +} + export function setup(helper) { - if (helper.markdownIt) { return; } + if (!helper.markdownIt) { return; } - helper.inlineRegexp({ - start: '#', - matcher: /^#([\w-:]{1,101})/i, - spaceOrTagBoundary: true, + helper.registerPlugin(md=>{ - emitter(matches) { - const options = helper.getOptions(); - const [hashtag, slug] = matches; - const categoryHashtagLookup = options.categoryHashtagLookup; - const result = categoryHashtagLookup && categoryHashtagLookup(slug); - - return result ? ['a', { class: 'hashtag', href: result[0] }, '#', ["span", {}, result[1]]] - : ['span', { class: 'hashtag' }, hashtag]; - } + md.core.ruler.push('category-hashtag', state => md.options.discourse.helpers.textReplace( + state, applyHashtag, true /* skip all links */ + )); }); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 index d3ed549fe09..3d5cb8d9313 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 @@ -1,18 +1,44 @@ -import { censor } from 'pretty-text/censored-words'; -import { registerOption } from 'pretty-text/pretty-text'; +import { censorFn } from 'pretty-text/censored-words'; -registerOption((siteSettings, opts) => { - opts.features.censored = true; - opts.censoredWords = siteSettings.censored_words; - opts.censoredPattern = siteSettings.censored_pattern; -}); +function recurse(tokens, apply) { + let i; + for(i=0;i { + if (token.content) { + token.content = censor(token.content); + } + }); +} export function setup(helper) { - if (helper.markdownIt) { return; } + if (!helper.markdownIt) { return; } - helper.addPreProcessor(text => { - const options = helper.getOptions(); - return censor(text, options.censoredWords, options.censoredPattern); + helper.registerOptions((opts, siteSettings) => { + opts.censoredWords = siteSettings.censored_words; + opts.censoredPattern = siteSettings.censored_pattern; + }); + + helper.registerPlugin(md => { + const words = md.options.discourse.censoredWords; + const patterns = md.options.discourse.censoredPattern; + + if ((words && words.length > 0) || (patterns && patterns.length > 0)) { + const replacement = String.fromCharCode(9632); + const censor = censorFn(words, patterns, replacement); + md.core.ruler.push('censored', state => censorTree(state, censor)); + } }); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js.es6 index 048b27a36d4..c8d94967a1e 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js.es6 @@ -1,27 +1,38 @@ -import { escape } from 'pretty-text/sanitizer'; -import { registerOption } from 'pretty-text/pretty-text'; +// we need a custom renderer for code blocks cause we have a slightly non compliant +// format with special handling for text and so on -// Support for various code blocks const TEXT_CODE_CLASSES = ["text", "pre", "plain"]; -function codeFlattenBlocks(blocks) { - let result = ""; - blocks.forEach(function(b) { - result += b; - if (b.trailing) { result += b.trailing; } - }); - return result; + +function render(tokens, idx, options, env, slf, md) { + let token = tokens[idx], + info = token.info ? md.utils.unescapeAll(token.info) : '', + langName = md.options.discourse.defaultCodeLang, + className, + escapedContent = md.utils.escapeHtml(token.content); + + if (info) { + // strip off any additional languages + info = info.split(/\s+/g)[0]; + } + + const acceptableCodeClasses = md.options.discourse.acceptableCodeClasses; + if (acceptableCodeClasses && info && acceptableCodeClasses.indexOf(info) !== -1) { + langName = info; + } + + className = TEXT_CODE_CLASSES.indexOf(info) !== -1 ? 'lang-nohighlight' : 'lang-' + langName; + + return `
${escapedContent}
\n`; } -registerOption((siteSettings, opts) => { - opts.features.code = true; - opts.defaultCodeLang = siteSettings.default_code_lang; - opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']); -}); - export function setup(helper) { + if (!helper.markdownIt) { return; } - if (helper.markdownIt) { return; } + helper.registerOptions((opts, siteSettings) => { + opts.defaultCodeLang = siteSettings.default_code_lang; + opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']); + }); helper.whiteList({ custom(tag, name, value) { @@ -34,50 +45,7 @@ export function setup(helper) { } }); - helper.replaceBlock({ - start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm, - stop: /^```$/gm, - withoutLeading: /\[quote/gm, //if leading text contains a quote this should not match - emitter(blockContents, matches) { - const opts = helper.getOptions(); - - let codeLang = opts.defaultCodeLang; - const acceptableCodeClasses = opts.acceptableCodeClasses; - if (acceptableCodeClasses && matches[1] && acceptableCodeClasses.indexOf(matches[1]) !== -1) { - codeLang = matches[1]; - } - - if (TEXT_CODE_CLASSES.indexOf(matches[1]) !== -1) { - return ['p', ['pre', ['code', {'class': 'lang-nohighlight'}, codeFlattenBlocks(blockContents) ]]]; - } else { - return ['p', ['pre', ['code', {'class': 'lang-' + codeLang}, codeFlattenBlocks(blockContents) ]]]; - } - } - }); - - helper.replaceBlock({ - start: /(]*\>)([\s\S]*)/igm, - stop: /<\/pre>/igm, - rawContents: true, - skipIfTradtionalLinebreaks: true, - - emitter(blockContents) { - return ['p', ['pre', codeFlattenBlocks(blockContents)]]; - } - }); - - // 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. - helper.onParseNode(function(event) { - const node = event.node, - path = event.path; - - if (node[0] === 'code') { - const regexp = (path && path[path.length-1] && path[path.length-1][0] && path[path.length-1][0] === "pre") ? - / +$/g : /^ +| +$/g; - - const contents = node[node.length-1]; - node[node.length-1] = escape(contents.replace(regexp,'')); - } + helper.registerPlugin(md=>{ + md.renderer.rules.fence = (tokens,idx,options,env,slf)=>render(tokens,idx,options,env,slf,md); }); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 index 60864a2263b..0e4eed203bf 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 @@ -1,117 +1,246 @@ -import { registerOption } from 'pretty-text/pretty-text'; import { buildEmojiUrl, isCustomEmoji } from 'pretty-text/emoji'; import { translations } from 'pretty-text/emoji/data'; -let _unicodeReplacements; -let _unicodeRegexp; -export function setUnicodeReplacements(replacements) { - _unicodeReplacements = replacements; - if (replacements) { - // We sort and reverse to match longer emoji sequences first - _unicodeRegexp = new RegExp(Object.keys(replacements).sort().reverse().join("|"), "g"); - } -}; +const MAX_NAME_LENGTH = 60; -function escapeRegExp(s) { - return s.replace(/[-/\\^$*+?.()|[\]{}]/gi, '\\$&'); +let translationTree = null; + +// This allows us to efficiently search for aliases +// We build a data structure that allows us to quickly +// search through our N next chars to see if any match +// one of our alias emojis. +// +function buildTranslationTree() { + let tree = []; + let lastNode; + + Object.keys(translations).forEach(function(key){ + let i; + let node = tree; + + for(i=0;i 0) { + let prev = content.charCodeAt(pos-1); + if (!state.md.utils.isSpace(prev) && !state.md.utils.isPunctChar(String.fromCharCode(prev))) { + return; } } - return true; + + pos++; + if (content.charCodeAt(pos) === 58) { + return; + } + + let length = 0; + while(length < MAX_NAME_LENGTH) { + length++; + + if (content.charCodeAt(pos+length) === 58) { + // check for t2-t6 + if (content.substr(pos+length+1, 3).match(/t[2-6]:/)) { + length += 3; + } + break; + } + + if (pos+length > content.length) { + return; + } + } + + if (length === MAX_NAME_LENGTH) { + return; + } + + return content.substr(pos, length); } -registerOption((siteSettings, opts, state) => { - opts.features.emoji = !!siteSettings.enable_emoji; - opts.emojiSet = siteSettings.emoji_set || ""; - opts.customEmoji = state.customEmoji; -}); +// straight forward :smile: to emoji image +function getEmojiTokenByName(name, state) { + + let info; + if (info = imageFor(name, state.md.options.discourse)) { + let token = new state.Token('emoji', 'img', 0); + token.attrs = [['src', info.url], + ['title', info.title], + ['class', info.classes], + ['alt', info.title]]; + + return token; + } +} + +function getEmojiTokenByTranslation(content, pos, state) { + + translationTree = translationTree || buildTranslationTree(); + + let currentTree = translationTree; + + let i; + let search = true; + let found = false; + let start = pos; + + while(search) { + + search = false; + let code = content.charCodeAt(pos); + + for (i=0;i 0) { + let leading = content.charAt(start-1); + if (!state.md.utils.isSpace(leading.charCodeAt(0)) && !state.md.utils.isPunctChar(leading)) { + return; + } + } + + // check trailing for punct or space + if (pos < content.length) { + let trailing = content.charCodeAt(pos); + if (!state.md.utils.isSpace(trailing)){ + return; + } + } + + let token = getEmojiTokenByName(found, state); + if (token) { + return { pos, token }; + } +} + +function applyEmoji(content, state, emojiUnicodeReplacer) { + let i; + let result = null; + let contentToken = null; + + let start = 0; + + if (emojiUnicodeReplacer) { + content = emojiUnicodeReplacer(content); + } + + let endToken = content.length; + + for (i=0; i0) { + contentToken = new state.Token('text', '', 0); + contentToken.content = content.slice(start,i); + result.push(contentToken); + } + + result.push(token); + endToken = start = i + offset; + } + } + + if (endToken < content.length) { + contentToken = new state.Token('text', '', 0); + contentToken.content = content.slice(endToken); + result.push(contentToken); + } + + return result; +} export function setup(helper) { - if (helper.markdownIt) { return; } + if (!helper.markdownIt) { return; } - helper.whiteList('img.emoji'); - - function imageFor(code) { - code = code.toLowerCase(); - const opts = helper.getOptions(); - const url = buildEmojiUrl(code, opts); - if (url) { - const title = `:${code}:`; - const classes = isCustomEmoji(code, opts) ? "emoji emoji-custom" : "emoji"; - return ['img', { href: url, title, 'class': classes, alt: title }]; - } - } - - const translationsWithColon = {}; - Object.keys(translations).forEach(t => { - if (t[0] === ':') { - translationsWithColon[t] = translations[t]; - } else { - const replacement = translations[t]; - helper.inlineReplace(t, (token, match, prev) => { - return checkPrev(prev) ? imageFor(replacement) : token; - }); - } - }); - const translationColonRegexp = new RegExp(Object.keys(translationsWithColon).map(t => `(${escapeRegExp(t)})`).join("|")); - - helper.registerInline(':', (text, match, prev) => { - const endPos = text.indexOf(':', 1); - const firstSpace = text.search(/\s/); - if (!checkPrev(prev)) { return; } - - // If there is no trailing colon, check our translations that begin with colons - if (endPos === -1 || (firstSpace !== -1 && endPos > firstSpace)) { - translationColonRegexp.lastIndex = 0; - const m = translationColonRegexp.exec(text); - if (m && m[0] && text.indexOf(m[0]) === 0) { - // Check outer edge - const lastChar = text.charAt(m[0].length); - if (lastChar && !/\s/.test(lastChar)) return; - const contents = imageFor(translationsWithColon[m[0]]); - if (contents) { - return [m[0].length, contents]; - } - } - return; - } - - let between; - const emojiNameMatch = text.match(/(?:.*?)(:(?!:).?[\w-]*(?::t\d)?:)/); - if (emojiNameMatch) { - between = emojiNameMatch[0].slice(1, -1); - } else { - between = text.slice(1, -1); - } - - const contents = imageFor(between); - if (contents) { - return [text.indexOf(between, 1) + between.length + 1, contents]; - } + helper.registerOptions((opts, siteSettings, state)=>{ + opts.features.emoji = !!siteSettings.enable_emoji; + opts.emojiSet = siteSettings.emoji_set || ""; + opts.customEmoji = state.customEmoji; }); - helper.addPreProcessor(text => { - if (_unicodeReplacements) { - _unicodeRegexp.lastIndex = 0; - - let m; - while ((m = _unicodeRegexp.exec(text)) !== null) { - let replacement = ":" + _unicodeReplacements[m[0]] + ":"; - const before = text.charAt(m.index-1); - if (!/\B/.test(before)) { - replacement = "\u200b" + replacement; - } - text = text.replace(m[0], replacement); - } - } - return text; + helper.registerPlugin((md)=>{ + md.core.ruler.push('emoji', state => md.options.discourse.helpers.textReplace( + state, (c,s)=>applyEmoji(c,s,md.options.discourse.emojiUnicodeReplacer)) + ); }); } diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/helpers.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js.es6 similarity index 100% rename from app/assets/javascripts/pretty-text/engines/markdown-it/helpers.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js.es6 diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 deleted file mode 100644 index 1d8f21a205b..00000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 +++ /dev/null @@ -1,52 +0,0 @@ -const BLOCK_TAGS = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'details', - 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', - 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'iframe', 'noscript', 'ol', 'output', - 'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video', 'summary']; - -function splitAtLast(tag, block, next, first) { - const endTag = ``; - let endTagIndex = first ? block.indexOf(endTag) : block.lastIndexOf(endTag); - - if (endTagIndex !== -1) { - endTagIndex += endTag.length; - - const trailing = block.substr(endTagIndex).replace(/^\s+/, ''); - if (trailing.length) { - next.unshift(trailing); - } - - return [ block.substr(0, endTagIndex) ]; - } -}; - -export function setup(helper) { - - if (helper.markdownIt) { return; } - - // If a row begins with HTML tags, don't parse it. - helper.registerBlock('html', function(block, next) { - let split, pos; - - // Fix manual blockquote paragraphing even though it's not strictly correct - // PERF NOTE: /\S+
= 0) { - if(block.substring(0, pos).search(/\s/) === -1) { - split = splitAtLast('blockquote', block, next, true); - if (split) { return this.processInline(split[0]); } - } - } - - const m = /^\s*<\/?([^>]+)\>/.exec(block); - if (m && m[1]) { - const tag = m[1].split(/\s/); - if (tag && tag[0] && BLOCK_TAGS.indexOf(tag[0]) !== -1) { - split = splitAtLast(tag[0], block, next); - if (split) { - if (split.length === 1 && split[0] === block) { return; } - return split; - } - return [ block.toString() ]; - } - } - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/html_img.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html_img.js.es6 similarity index 100% rename from app/assets/javascripts/pretty-text/engines/markdown-it/html_img.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/html_img.js.es6 diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 index 84be9e5f32b..602af3c15ae 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 @@ -1,51 +1,88 @@ -/** - Supports our 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. -**/ +const regex = /^(\w[\w.-]{0,59})\b/i; + +function applyMentions(state, silent, isWhiteSpace, isPunctChar, mentionLookup, getURL) { + + let pos = state.pos; + + // 64 = @ + if (silent || state.src.charCodeAt(pos) !== 64) { + return false; + } + + if (pos > 0) { + let prev = state.src.charCodeAt(pos-1); + if (!isWhiteSpace(prev) && !isPunctChar(String.fromCharCode(prev))) { + return false; + } + } + + // skip if in a link + if (state.tokens) { + let last = state.tokens[state.tokens.length-1]; + if (last) { + if (last.type === 'link_open') { + return false; + } + if (last.type === 'html_inline' && last.content.substr(0,2) === " { - const node = event.node, - path = event.path; - - if (node[1] && node[1]["class"] === 'mention') { - const parent = path[path.length - 1]; - - // If the parent is an 'a', remove it - if (parent && parent[0] === 'a') { - const name = node[2]; - node.length = 0; - node[0] = "__RAW"; - node[1] = name; - } - } - }); - - helper.inlineRegexp({ - start: '@', - // NOTE: since we can't use SiteSettings here (they loads later in process) - // we are being less strict to account for more cases than allowed - matcher: /^@(\w[\w.-]{0,59})\b/i, - wordBoundary: true, - - emitter(matches) { - const mention = matches[0].trim(); - const name = matches[1]; - const opts = helper.getOptions(); - const mentionLookup = opts.mentionLookup; - - const type = mentionLookup && mentionLookup(name); - if (type === "user") { - return ['a', {'class': 'mention', href: opts.getURL("/u/") + name.toLowerCase()}, mention]; - } else if (type === "group") { - return ['a', {'class': 'mention-group', href: opts.getURL("/groups/") + name}, mention]; - } else { - return ['span', {'class': 'mention'}, mention]; - } - } + helper.registerPlugin(md => { + md.inline.ruler.push('mentions', (state,silent)=> applyMentions( + state, + silent, + md.utils.isWhiteSpace, + md.utils.isPunctChar, + md.options.discourse.mentionLookup, + md.options.discourse.getURL + )); }); } + diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6 index a453445a2c0..f1eb2ba759d 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6 @@ -1,30 +1,53 @@ -// Support for the newline behavior in markdown that most expect. Look through all text nodes -// in the tree, replace any new lines with `br`s. +// see: https://github.com/markdown-it/markdown-it/issues/375 +// +// we use a custom paragraph rule cause we have to signal when a +// link starts with a space, so we can bypass a onebox +// this is a freedom patch, so careful, may break on updates + + +function newline(state, silent) { + var token, pmax, max, pos = state.pos; + + if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; } + + pmax = state.pending.length - 1; + max = state.posMax; + + // ' \n' -> hardbreak + // Lookup in pending chars is bad practice! Don't copy to other rules! + // Pending string is stored in concat mode, indexed lookups will cause + // convertion to flat mode. + if (!silent) { + if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) { + if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) { + state.pending = state.pending.replace(/ +$/, ''); + token = state.push('hardbreak', 'br', 0); + } else { + state.pending = state.pending.slice(0, -1); + token = state.push('softbreak', 'br', 0); + } + + } else { + token = state.push('softbreak', 'br', 0); + } + } + + pos++; + + // skip heading spaces for next line + while (pos < max && state.md.utils.isSpace(state.src.charCodeAt(pos))) { + if (token) { + token.leading_space = true; + } + pos++; + } + + state.pos = pos; + return true; +}; export function setup(helper) { - - if (helper.markdownIt) { return; } - - helper.postProcessText((text, event) => { - const { options, insideCounts } = event; - if (options.traditionalMarkdownLinebreaks || (insideCounts.pre > 0)) { return; } - - if (text === "\n") { - // If the tag is just a new line, replace it with a `
` - return [['br']]; - } else { - // If the text node contains new lines, perhaps with text between them, insert the - // `
` tags. - const split = text.split(/\n+/); - if (split.length) { - const replacement = []; - for (var i=0; i 0) { replacement.push(split[i]); } - if (i !== split.length-1) { replacement.push(['br']); } - } - - return replacement; - } - } + helper.registerPlugin(md => { + md.inline.ruler.at('newline', newline); }); } 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 875321911f8..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 @@ -1,71 +1,95 @@ import { lookupCache } from 'pretty-text/oneboxer'; -// Given a node in the document and its parent, determine whether it is on its own line or not. -function isOnOneLine(link, parent) { - if (!parent) { return false; } - - const siblings = parent.slice(1); - if ((!siblings) || (siblings.length < 1)) { return false; } - - const idx = siblings.indexOf(link); - if (idx === -1) { return false; } - - if (idx > 0) { - const prev = siblings[idx-1]; - if (prev[0] !== 'br') { return false; } +function applyOnebox(state, silent) { + if (silent || !state.tokens || state.tokens.length < 3) { + return; } - if (idx < siblings.length) { - const next = siblings[idx+1]; - if (next && (!((next[0] === 'br') || (typeof next === 'string' && next.trim() === "")))) { return false; } - } + let i; + for(i=1;i { - const node = event.node, - path = event.path; + if (j === 0 && token.leading_space) { + continue; + } else if (j > 0) { - // We only care about links - if (node[0] !== 'a') { return; } + let prevSibling = token.children[j-1]; - const parent = path[path.length - 1]; + if (prevSibling.tag !== 'br' || prevSibling.leading_space) { + continue; + } + } - // We don't onebox bbcode - if (node[1]['data-bbcode']) { - delete node[1]['data-bbcode']; - return; - } + // look ahead for soft or hard break + let text = token.children[j+1]; + let close = token.children[j+2]; + let lookahead = token.children[j+3]; - // We don't onebox mentions - if (node[1]['class'] === 'mention') { return; } + if (lookahead && lookahead.tag !== 'br') { + continue; + } - // Don't onebox links within a list - for (var i=0; i { + md.core.ruler.after('linkify', 'onebox', applyOnebox); }); } diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/paragraph.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/paragraph.js.es6 similarity index 100% rename from app/assets/javascripts/pretty-text/engines/markdown-it/paragraph.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/paragraph.js.es6 diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6 deleted file mode 100644 index e8a4a9a1314..00000000000 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6 +++ /dev/null @@ -1,83 +0,0 @@ -import { register } from 'pretty-text/engines/discourse-markdown/bbcode'; -import { registerOption } from 'pretty-text/pretty-text'; -import { performEmojiUnescape } from 'pretty-text/emoji'; - -registerOption((siteSettings, opts) => { - opts.enableEmoji = siteSettings.enable_emoji; - opts.emojiSet = siteSettings.emoji_set; -}); - - -export function setup(helper) { - - if (helper.markdownIt) { return; } - - register(helper, 'quote', {noWrap: true, singlePara: true}, (contents, bbParams, options) => { - - const params = {'class': 'quote'}; - let username = null; - const opts = helper.getOptions(); - - if (bbParams) { - const paramsSplit = bbParams.split(/\,\s*/); - username = paramsSplit[0]; - - paramsSplit.forEach(function(p,i) { - if (i > 0) { - var assignment = p.split(':'); - if (assignment[0] && assignment[1]) { - const escaped = helper.escape(assignment[0]); - // don't escape attributes, makes no sense - if (escaped === assignment[0]) { - params['data-' + assignment[0]] = helper.escape(assignment[1].trim()); - } - } - } - }); - } - - let avatarImg; - const postNumber = parseInt(params['data-post'], 10); - const topicId = parseInt(params['data-topic'], 10); - - if (options.lookupAvatarByPostNumber) { - // client-side, we can retrieve the avatar from the post - avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId); - } else if (options.lookupAvatar) { - // server-side, we need to lookup the avatar from the username - avatarImg = options.lookupAvatar(username); - } - - // If there's no username just return a simple quote - if (!username) { - return ['p', ['aside', params, ['blockquote'].concat(contents)]]; - } - - const header = ['div', {'class': 'title'}, - ['div', {'class': 'quote-controls'}], - avatarImg ? ['__RAW', avatarImg] : "", - username ? `${username}:` : "" ]; - - if (options.topicId && postNumber && options.getTopicInfo && topicId !== options.topicId) { - const topicInfo = options.getTopicInfo(topicId); - if (topicInfo) { - var href = topicInfo.href; - if (postNumber > 0) { href += "/" + postNumber; } - // get rid of username said stuff - header.pop(); - - let title = topicInfo.title; - - if (opts.enableEmoji) { - title = performEmojiUnescape(topicInfo.title, { - getURL: opts.getURL, emojiSet: opts.emojiSet - }); - } - - header.push(['a', {'href': href}, title]); - } - } - - return ['aside', params, header, ['blockquote'].concat(contents)]; - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/quotes.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 similarity index 98% rename from app/assets/javascripts/pretty-text/engines/markdown-it/quotes.js.es6 rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 index 05afd6bc9ef..ba892ef7067 100644 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/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/engines/discourse-markdown/table.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 index 1b148e68434..4bb5ef92d62 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 @@ -1,35 +1,31 @@ -import { registerOption } from 'pretty-text/pretty-text'; - -function tableFlattenBlocks(blocks) { - let result = ""; - - blocks.forEach(b => { - result += b; - if (b.trailing) { result += b.trailing; } - }); - - // bypass newline insertion - return result.replace(/[\n\r]/g, " "); -}; - -registerOption((siteSettings, opts) => { - opts.features.table = !!siteSettings.allow_html_tables; -}); - export function setup(helper) { - if (helper.markdownIt) { return; } + if (!helper.markdownIt) { return; } - helper.whiteList(['table', 'table.md-table', 'tbody', 'thead', 'tr', 'th', 'td']); + // this is built in now + // TODO: sanitizer needs fixing, does not properly support this yet - helper.replaceBlock({ - start: /(]*>)([\S\s]*)/igm, - stop: /<\/table>/igm, - rawContents: true, - priority: 1, + // we need a custom callback for style handling + helper.whiteList({ + custom: function(tag,attr,val) { + if (tag !== 'th' && tag !== 'td') { + return false; + } - emitter(contents) { - return ['table', {"class": "md-table"}, tableFlattenBlocks.apply(this, [contents])]; + if (attr !== 'style') { + return false; + } + + return (val === 'text-align:right' || val === 'text-align:left' || val === 'text-align:center'); } }); + + helper.whiteList([ + 'table', + 'tbody', + 'thead', + 'tr', + 'th', + 'td', + ]); } diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/category-hashtag.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/category-hashtag.js.es6 deleted file mode 100644 index 79d57002a68..00000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/category-hashtag.js.es6 +++ /dev/null @@ -1,104 +0,0 @@ -function addHashtag(buffer, matches, state) { - const options = state.md.options.discourse; - const [hashtag, slug] = matches; - const categoryHashtagLookup = options.categoryHashtagLookup; - const result = categoryHashtagLookup && categoryHashtagLookup(slug); - - let token; - - if (result) { - token = new state.Token('link_open', 'a', 1); - token.attrs = [['class', 'hashtag'], ['href', result[0]]]; - token.block = false; - buffer.push(token); - - token = new state.Token('text', '', 0); - token.content = '#'; - buffer.push(token); - - token = new state.Token('span_open', 'span', 1); - token.block = false; - buffer.push(token); - - token = new state.Token('text', '', 0); - token.content = result[1]; - buffer.push(token); - - buffer.push(new state.Token('span_close', 'span', -1)); - - buffer.push(new state.Token('link_close', 'a', -1)); - } else { - - token = new state.Token('span_open', 'span', 1); - token.attrs = [['class', 'hashtag']]; - buffer.push(token); - - token = new state.Token('text', '', 0); - token.content = hashtag; - buffer.push(token); - - token = new state.Token('span_close', 'span', -1); - buffer.push(token); - } -} - -const REGEX = /#([\w-:]{1,101})/gi; - -function allowedBoundary(content, index, utils) { - let code = content.charCodeAt(index); - return (utils.isWhiteSpace(code) || utils.isPunctChar(String.fromCharCode(code))); -} - -function applyHashtag(content, state) { - let result = null, - match, - pos = 0; - - while (match = REGEX.exec(content)) { - // check boundary - if (match.index > 0) { - if (!allowedBoundary(content, match.index-1, state.md.utils)) { - continue; - } - } - - // check forward boundary as well - if (match.index + match[0].length < content.length) { - if (!allowedBoundary(content, match.index + match[0].length, state.md.utils)) { - continue; - } - } - - if (match.index > pos) { - result = result || []; - let token = new state.Token('text', '', 0); - token.content = content.slice(pos, match.index); - result.push(token); - } - - result = result || []; - addHashtag(result, match, state); - - pos = match.index + match[0].length; - } - - if (result && pos < content.length) { - let token = new state.Token('text', '', 0); - token.content = content.slice(pos); - result.push(token); - } - - return result; -} - -export function setup(helper) { - - if (!helper.markdownIt) { return; } - - helper.registerPlugin(md=>{ - - md.core.ruler.push('category-hashtag', state => md.options.discourse.helpers.textReplace( - state, applyHashtag, true /* skip all links */ - )); - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/censored.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/censored.js.es6 deleted file mode 100644 index 3d5cb8d9313..00000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/censored.js.es6 +++ /dev/null @@ -1,44 +0,0 @@ -import { censorFn } from 'pretty-text/censored-words'; - -function recurse(tokens, apply) { - let i; - for(i=0;i { - if (token.content) { - token.content = censor(token.content); - } - }); -} - -export function setup(helper) { - - if (!helper.markdownIt) { return; } - - helper.registerOptions((opts, siteSettings) => { - opts.censoredWords = siteSettings.censored_words; - opts.censoredPattern = siteSettings.censored_pattern; - }); - - helper.registerPlugin(md => { - const words = md.options.discourse.censoredWords; - const patterns = md.options.discourse.censoredPattern; - - if ((words && words.length > 0) || (patterns && patterns.length > 0)) { - const replacement = String.fromCharCode(9632); - const censor = censorFn(words, patterns, replacement); - md.core.ruler.push('censored', state => censorTree(state, censor)); - } - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/code.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/code.js.es6 deleted file mode 100644 index c8d94967a1e..00000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/code.js.es6 +++ /dev/null @@ -1,51 +0,0 @@ -// we need a custom renderer for code blocks cause we have a slightly non compliant -// format with special handling for text and so on - -const TEXT_CODE_CLASSES = ["text", "pre", "plain"]; - - -function render(tokens, idx, options, env, slf, md) { - let token = tokens[idx], - info = token.info ? md.utils.unescapeAll(token.info) : '', - langName = md.options.discourse.defaultCodeLang, - className, - escapedContent = md.utils.escapeHtml(token.content); - - if (info) { - // strip off any additional languages - info = info.split(/\s+/g)[0]; - } - - const acceptableCodeClasses = md.options.discourse.acceptableCodeClasses; - if (acceptableCodeClasses && info && acceptableCodeClasses.indexOf(info) !== -1) { - langName = info; - } - - className = TEXT_CODE_CLASSES.indexOf(info) !== -1 ? 'lang-nohighlight' : 'lang-' + langName; - - return `
${escapedContent}
\n`; -} - -export function setup(helper) { - if (!helper.markdownIt) { return; } - - helper.registerOptions((opts, siteSettings) => { - opts.defaultCodeLang = siteSettings.default_code_lang; - opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']); - }); - - helper.whiteList({ - custom(tag, name, value) { - if (tag === 'code' && name === 'class') { - const m = /^lang\-(.+)$/.exec(value); - if (m) { - return helper.getOptions().acceptableCodeClasses.indexOf(m[1]) !== -1; - } - } - } - }); - - helper.registerPlugin(md=>{ - md.renderer.rules.fence = (tokens,idx,options,env,slf)=>render(tokens,idx,options,env,slf,md); - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/emoji.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/emoji.js.es6 deleted file mode 100644 index 0e4eed203bf..00000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/emoji.js.es6 +++ /dev/null @@ -1,246 +0,0 @@ -import { buildEmojiUrl, isCustomEmoji } from 'pretty-text/emoji'; -import { translations } from 'pretty-text/emoji/data'; - -const MAX_NAME_LENGTH = 60; - -let translationTree = null; - -// This allows us to efficiently search for aliases -// We build a data structure that allows us to quickly -// search through our N next chars to see if any match -// one of our alias emojis. -// -function buildTranslationTree() { - let tree = []; - let lastNode; - - Object.keys(translations).forEach(function(key){ - let i; - let node = tree; - - for(i=0;i 0) { - let prev = content.charCodeAt(pos-1); - if (!state.md.utils.isSpace(prev) && !state.md.utils.isPunctChar(String.fromCharCode(prev))) { - return; - } - } - - pos++; - if (content.charCodeAt(pos) === 58) { - return; - } - - let length = 0; - while(length < MAX_NAME_LENGTH) { - length++; - - if (content.charCodeAt(pos+length) === 58) { - // check for t2-t6 - if (content.substr(pos+length+1, 3).match(/t[2-6]:/)) { - length += 3; - } - break; - } - - if (pos+length > content.length) { - return; - } - } - - if (length === MAX_NAME_LENGTH) { - return; - } - - return content.substr(pos, length); -} - -// straight forward :smile: to emoji image -function getEmojiTokenByName(name, state) { - - let info; - if (info = imageFor(name, state.md.options.discourse)) { - let token = new state.Token('emoji', 'img', 0); - token.attrs = [['src', info.url], - ['title', info.title], - ['class', info.classes], - ['alt', info.title]]; - - return token; - } -} - -function getEmojiTokenByTranslation(content, pos, state) { - - translationTree = translationTree || buildTranslationTree(); - - let currentTree = translationTree; - - let i; - let search = true; - let found = false; - let start = pos; - - while(search) { - - search = false; - let code = content.charCodeAt(pos); - - for (i=0;i 0) { - let leading = content.charAt(start-1); - if (!state.md.utils.isSpace(leading.charCodeAt(0)) && !state.md.utils.isPunctChar(leading)) { - return; - } - } - - // check trailing for punct or space - if (pos < content.length) { - let trailing = content.charCodeAt(pos); - if (!state.md.utils.isSpace(trailing)){ - return; - } - } - - let token = getEmojiTokenByName(found, state); - if (token) { - return { pos, token }; - } -} - -function applyEmoji(content, state, emojiUnicodeReplacer) { - let i; - let result = null; - let contentToken = null; - - let start = 0; - - if (emojiUnicodeReplacer) { - content = emojiUnicodeReplacer(content); - } - - let endToken = content.length; - - for (i=0; i0) { - contentToken = new state.Token('text', '', 0); - contentToken.content = content.slice(start,i); - result.push(contentToken); - } - - result.push(token); - endToken = start = i + offset; - } - } - - if (endToken < content.length) { - contentToken = new state.Token('text', '', 0); - contentToken.content = content.slice(endToken); - result.push(contentToken); - } - - return result; -} - -export function setup(helper) { - - if (!helper.markdownIt) { return; } - - helper.registerOptions((opts, siteSettings, state)=>{ - opts.features.emoji = !!siteSettings.enable_emoji; - opts.emojiSet = siteSettings.emoji_set || ""; - opts.customEmoji = state.customEmoji; - }); - - helper.registerPlugin((md)=>{ - md.core.ruler.push('emoji', state => md.options.discourse.helpers.textReplace( - state, (c,s)=>applyEmoji(c,s,md.options.discourse.emojiUnicodeReplacer)) - ); - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/mentions.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/mentions.js.es6 deleted file mode 100644 index 602af3c15ae..00000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/mentions.js.es6 +++ /dev/null @@ -1,88 +0,0 @@ -const regex = /^(\w[\w.-]{0,59})\b/i; - -function applyMentions(state, silent, isWhiteSpace, isPunctChar, mentionLookup, getURL) { - - let pos = state.pos; - - // 64 = @ - if (silent || state.src.charCodeAt(pos) !== 64) { - return false; - } - - if (pos > 0) { - let prev = state.src.charCodeAt(pos-1); - if (!isWhiteSpace(prev) && !isPunctChar(String.fromCharCode(prev))) { - return false; - } - } - - // skip if in a link - if (state.tokens) { - let last = state.tokens[state.tokens.length-1]; - if (last) { - if (last.type === 'link_open') { - return false; - } - if (last.type === 'html_inline' && last.content.substr(0,2) === " { - md.inline.ruler.push('mentions', (state,silent)=> applyMentions( - state, - silent, - md.utils.isWhiteSpace, - md.utils.isPunctChar, - md.options.discourse.mentionLookup, - md.options.discourse.getURL - )); - }); -} - diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/newline.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/newline.js.es6 deleted file mode 100644 index f1eb2ba759d..00000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/newline.js.es6 +++ /dev/null @@ -1,53 +0,0 @@ -// see: https://github.com/markdown-it/markdown-it/issues/375 -// -// we use a custom paragraph rule cause we have to signal when a -// link starts with a space, so we can bypass a onebox -// this is a freedom patch, so careful, may break on updates - - -function newline(state, silent) { - var token, pmax, max, pos = state.pos; - - if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; } - - pmax = state.pending.length - 1; - max = state.posMax; - - // ' \n' -> hardbreak - // Lookup in pending chars is bad practice! Don't copy to other rules! - // Pending string is stored in concat mode, indexed lookups will cause - // convertion to flat mode. - if (!silent) { - if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) { - if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) { - state.pending = state.pending.replace(/ +$/, ''); - token = state.push('hardbreak', 'br', 0); - } else { - state.pending = state.pending.slice(0, -1); - token = state.push('softbreak', 'br', 0); - } - - } else { - token = state.push('softbreak', 'br', 0); - } - } - - pos++; - - // skip heading spaces for next line - while (pos < max && state.md.utils.isSpace(state.src.charCodeAt(pos))) { - if (token) { - token.leading_space = true; - } - pos++; - } - - state.pos = pos; - return true; -}; - -export function setup(helper) { - helper.registerPlugin(md => { - md.inline.ruler.at('newline', newline); - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/onebox.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/onebox.js.es6 deleted file mode 100644 index 5f04b72b1d0..00000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/onebox.js.es6 +++ /dev/null @@ -1,89 +0,0 @@ -import { lookupCache } from 'pretty-text/oneboxer'; - -function applyOnebox(state, silent) { - if (silent || !state.tokens || state.tokens.length < 3) { - return; - } - - let i; - for(i=1;i 0) { - let prevSibling = token.children[j-1]; - - if (prevSibling.tag !== 'br' || prevSibling.leading_space) { - continue; - } - } - - // look ahead for soft or hard break - let text = token.children[j+1]; - let close = token.children[j+2]; - let lookahead = token.children[j+3]; - - if (lookahead && lookahead.tag !== 'br') { - continue; - } - - // check attrs only include a href - let attrs = child["attrs"]; - - if (!attrs || attrs.length !== 1 || attrs[0][0] !== "href") { - continue; - } - - // we already know text matches cause it is an auto link - - if (!close || close.type !== "link_close") { - continue; - } - - // we already determined earlier that 0 0 was href - let cached = lookupCache(attrs[0][1]); - - if (cached) { - // replace link with 2 blank text nodes and inline html for onebox - child.type = 'html_raw'; - child.content = cached; - child.inline = true; - - text.type = 'html_raw'; - text.content = ''; - text.inline = true; - - close.type = 'html_raw'; - close.content = ''; - close.inline = true; - - } else { - // decorate... - attrs.push(["class", "onebox"]); - } - } - } - } - } -} - - -export function setup(helper) { - - if (!helper.markdownIt) { return; } - - helper.registerPlugin(md => { - md.core.ruler.after('linkify', 'onebox', applyOnebox); - }); -} diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/table.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/table.js.es6 deleted file mode 100644 index 4bb5ef92d62..00000000000 --- a/app/assets/javascripts/pretty-text/engines/markdown-it/table.js.es6 +++ /dev/null @@ -1,31 +0,0 @@ -export function setup(helper) { - - if (!helper.markdownIt) { return; } - - // this is built in now - // TODO: sanitizer needs fixing, does not properly support this yet - - // we need a custom callback for style handling - helper.whiteList({ - custom: function(tag,attr,val) { - if (tag !== 'th' && tag !== 'td') { - return false; - } - - if (attr !== 'style') { - return false; - } - - return (val === 'text-align:right' || val === 'text-align:left' || val === 'text-align:center'); - } - }); - - helper.whiteList([ - 'table', - 'tbody', - 'thead', - 'tr', - 'th', - 'td', - ]); -} diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index f2c4633d20c..f341e7c4c7c 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -1,13 +1,10 @@ -import { cook, setup } from 'pretty-text/engines/discourse-markdown'; 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() { + // TODO next major version deprecate this + // if (window.console) { + // window.console.log("registerOption is deprecated"); + // } } export function buildOptions(state) { @@ -25,11 +22,7 @@ export function buildOptions(state) { emojiUnicodeReplacer } = state; - if (!siteSettings.enable_experimental_markdown_it) { - setup(); - } - - const features = { + let features = { 'bold-italics': true, 'auto-link': true, 'mentions': true, @@ -41,6 +34,10 @@ export function buildOptions(state) { 'newline': !siteSettings.traditional_markdown_linebreaks }; + if (state.features) { + features = _.merge(features, state.features); + } + const options = { sanitize: true, getURL, @@ -56,44 +53,37 @@ export function buildOptions(state) { mentionLookup: state.mentionLookup, emojiUnicodeReplacer, allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null, - markdownIt: siteSettings.enable_experimental_markdown_it + markdownIt: true }; - if (siteSettings.enable_experimental_markdown_it) { - setupIt(options, siteSettings, state); - } else { - // TODO deprecate this - _registerFns.forEach(fn => fn(siteSettings, options, state)); - } + // note, this will mutate options due to the way the API is designed + // may need a refactor + setupIt(options, siteSettings, state); return options; } 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; - // We used to do a failsafe call to setup here - // under new engine we always expect setup to be called by buildOptions. - // setup(); + if (!opts) { + opts = buildOptions({ siteSettings: {}}); + } + this.opts = opts; + } + + disableSanitizer() { + this.opts.sanitizer = this.opts.discourse.sanitizer = ident => ident; } cook(raw) { if (!raw || raw.length === 0) { return ""; } let result; - - if (this.opts.markdownIt) { - result = cookIt(raw, this.opts); - } else { - result = cook(raw, this.opts); - } - + result = cookIt(raw, this.opts); return result ? result : ""; } sanitize(html) { - return this.opts.sanitizer(html, new WhiteLister(this.opts)); + return this.opts.sanitizer(html).trim(); } }; diff --git a/app/assets/javascripts/pretty-text/white-lister.js.es6 b/app/assets/javascripts/pretty-text/white-lister.js.es6 index e1c1d56a4ab..04578037f8c 100644 --- a/app/assets/javascripts/pretty-text/white-lister.js.es6 +++ b/app/assets/javascripts/pretty-text/white-lister.js.es6 @@ -1,115 +1,112 @@ -const masterList = {}; -const masterCallbacks = {}; - -const _whiteLists = {}; -const _callbacks = {}; - -function concatUniq(src, elems) { - src = src || []; - if (!Array.isArray(elems)) { - elems = [elems]; - } - return src.concat(elems.filter(e => src.indexOf(e) === -1)); -} +// to match: +// abcd +// abcd[test] +// abcd[test=bob] +const WHITELIST_REGEX = /([^\[]+)(\[([^=]+)(=(.*))?\])?/; export default class WhiteLister { constructor(options) { - options = options || { - features: { - default: true - } - }; - options.features.default = true; + this._enabled = { "default": true }; + this._allowedHrefSchemes = (options && options.allowedHrefSchemes) || []; + this._rawFeatures = [["default", DEFAULT_LIST]]; - this._featureKeys = Object.keys(options.features).filter(f => options.features[f]); - this._key = this._featureKeys.join(':'); - this._features = options.features; - this._options = options; + this._cache = null; + + if (options && options.features) { + Object.keys(options.features).forEach(f => { + if (options.features[f]) { + this._enabled[f] = true; + } + }); + } } - getCustom() { - if (!_callbacks[this._key]) { - const callbacks = []; - this._featureKeys.forEach(f => { - (masterCallbacks[f] || []).forEach(cb => callbacks.push(cb)); - }); - _callbacks[this._key] = callbacks; - } + whiteListFeature(feature, info) { + this._rawFeatures.push([feature, info]); + } - return _callbacks[this._key]; + disable(feature) { + this._enabled[feature] = false; + this._cache = null; + } + + enable(feature) { + this._enabled[feature] = true; + this._cache = null; + } + + _buildCache() { + const tagList = {}; + const attrList = {}; + const custom = []; + + this._rawFeatures.forEach(([name, info]) => { + if (!this._enabled[name]) return; + + if (info.custom) { + custom.push(info.custom); + return; + } + + if (typeof info === "string") { + info = [info]; + } + + (info || []).forEach(tag => { + const classes = tag.split('.'); + const tagWithAttr = classes.shift(); + + const m = WHITELIST_REGEX.exec(tagWithAttr); + if (m) { + + const [,tagname,,attr,,val] = m; + tagList[tagname] = []; + + let attrs = attrList[tagname] = attrList[tagname] || {}; + if (classes.length > 0) { + attrs["class"] = (attrs["class"] || []).concat(classes); + } + + if (attr) { + let attrInfo = attrs[attr] = attrs[attr] || []; + + if (val) { + attrInfo.push(val); + } else { + attrs[attr] = ["*"]; + } + } + } + }); + }); + + + this._cache = {custom, whiteList: {tagList, attrList}}; + } + + _ensureCache() { + if (!this._cache) { this._buildCache(); } } getWhiteList() { - if (!_whiteLists[this._key]) { - const tagList = {}; - let attrList = {}; + this._ensureCache(); + return this._cache.whiteList; + } - // merge whitelists for these features - this._featureKeys.forEach(f => { - const info = masterList[f] || {}; - Object.keys(info).forEach(t => { - tagList[t] = []; - attrList[t] = attrList[t] || {}; - - const attrs = info[t]; - Object.keys(attrs).forEach(a => attrList[t][a] = concatUniq(attrList[t][a], attrs[a])); - }); - }); - - _whiteLists[this._key] = { tagList, attrList }; - } - return _whiteLists[this._key]; + getCustom() { + this._ensureCache(); + return this._cache.custom; } getAllowedHrefSchemes() { - return this._options.allowedHrefSchemes || []; + return this._allowedHrefSchemes; } } -// Builds our object that represents whether something is sanitized for a particular feature. -export function whiteListFeature(feature, info) { - const featureInfo = {}; - - // we can supply a callback instead - if (info.custom) { - masterCallbacks[feature] = masterCallbacks[feature] || []; - masterCallbacks[feature].push(info.custom); - return; - } - - if (typeof info === "string") { info = [info]; } - - (info || []).forEach(tag => { - const classes = tag.split('.'); - const tagName = classes.shift(); - const m = /\[([^\]]+)]/.exec(tagName); - if (m) { - const [full, inside] = m; - const stripped = tagName.replace(full, ''); - const vals = inside.split('='); - - featureInfo[stripped] = featureInfo[stripped] || {}; - if (vals.length === 2) { - const [name, value] = vals; - featureInfo[stripped][name] = value; - } else { - featureInfo[stripped][inside] = '*'; - } - } - - featureInfo[tagName] = featureInfo[tagName] || {}; - if (classes.length) { - featureInfo[tagName]['class'] = concatUniq(featureInfo[tagName]['class'], classes); - } - }); - - masterList[feature] = featureInfo; -} - // Only add to `default` when you always want your whitelist to occur. In other words, // don't change this for a plugin or a feature that can be disabled -whiteListFeature('default', [ +const DEFAULT_LIST = [ 'a.attachment', 'a.hashtag', 'a.mention', @@ -174,4 +171,4 @@ whiteListFeature('default', [ 'sub', 'sup', 'ul', -]); +]; diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index 1d30e8e2965..f9b01467938 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -42,9 +42,7 @@ Discourse.Environment = '<%= Rails.env %>'; Discourse.SiteSettings = ps.get('siteSettings'); Discourse.LetterAvatarVersion = '<%= LetterAvatar.version %>'; - <%- if SiteSetting.enable_experimental_markdown_it %> Discourse.MarkdownItURL = '<%= asset_url('markdown-it-bundle.js') %>'; - <%- end %> I18n.defaultLocale = '<%= SiteSetting.default_locale %>'; Discourse.start(); Discourse.set('assetVersion','<%= Discourse.assets_digest %>'); diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index bff1fc2adfa..ab722fe3f9d 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -946,7 +946,6 @@ ar: notify_mods_when_user_blocked: "إذا تم حظر المستخدم تلقائيا، وإرسال رسالة الى جميع المشرفين." flag_sockpuppets: "إذا رد أحد المستخدمين جديد إلى موضوع من عنوان IP نفسه باسم المستخدم الجديد الذي بدأ هذا الموضوع، علم كل من مناصبهم كدعاية المحتملين." traditional_markdown_linebreaks: "استعمل السطور التالفه التقليديه في Markdown, التي تتطلب مساحتين بيضاوين للسطور التالفه" - allow_html_tables: "كل الجداول يجب ان تدخل ب لغة ال HTML مثال TABLE , THEAD , TD , TR , TH سوف يأوذن لهم ( تتطلب مراجعة لكل المقالات القديمة )" post_undo_action_window_mins: "عدد الدقائق التي يسمح فيها للأعضاء بالتراجع عن آخر إجراءاتهم على المنشور (إعجاب، اشارة، إلخ...)" must_approve_users: "يجب أن الموظفين يوافق على جميع حسابات المستخدم الجديدة قبل أن يتم السماح لهم للوصول إلى الموقع. تحذير: تمكين هذا لموقع الحية إلغاء وصول المستخدمين الحاليين غير الموظفين!" pending_users_reminder_delay: "نبه المشرفين إذا وجد اعضاء ينتظرون الموافقة لمدة اطول من الساعات ، قم بوضع الخيار -1 لايقاف التنبيهات ." diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index b8244744d02..ea748fa7e04 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -903,7 +903,6 @@ da: notify_mods_when_user_blocked: "Send en besked til alle moderatorer hvis en bruger blokeres automatisk" flag_sockpuppets: "Hvis en ny bruger svarer på et emne fra den samme IP adresse som den der startede emnet, så rapporter begge at deres indlæg potentielt er spam." traditional_markdown_linebreaks: "Brug traditionelle linjeskift i Markdown, som kræver 2 mellemrum i slutningen af sætningen." - allow_html_tables: "Tillad tabeller at blive oprettet i Markdown med brug af HTML tags. TABLE, THEAD, TD, TR, TH vil blive tilladt (kræver en fuld re-indeksering af gamle indlæg som benytter tabeller) " post_undo_action_window_mins: "Antal minutter som brugere er tilladt at fortryde handlinger på et indlæg (like, flag, etc)." must_approve_users: "Personale skal godkende alle nye bruger konti inden de kan tilgå sitet. ADVARSEL: aktivering af dette for et live site vil medføre en ophævning af adgang for eksisterende ikke-personale brugere." pending_users_reminder_delay: "Underret moderatorer hvis nye brugere har ventet på godkendelse i længere end så mange timer. Skriv -1 for at deaktivere notifikationer." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 024f52ccf2c..5792ad524cd 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -921,7 +921,6 @@ de: notify_mods_when_user_blocked: "Wenn ein Benutzer automatisch gesperrt wird, sende eine Nachricht an alle Moderatoren." flag_sockpuppets: "Wenn ein neuer Benutzer auf ein Thema antwortet, das von einem anderen neuen Benutzer aber mit der gleichen IP-Adresse begonnen wurde, markiere beide Beiträge als potenziellen Spam." traditional_markdown_linebreaks: "Traditionelle Zeilenumbrüche in Markdown, die zwei nachfolgende Leerzeichen für einen Zeilenumbruch benötigen." - allow_html_tables: "Erlaube es, Tabellen in Markdown mit HTML-Tags einzugeben. TABLE, THEAD, TD, TR, TH werden erlaubt (alle Beiträge mit Tabellen müssen ihr HTML erneuern)" post_undo_action_window_mins: "Minuten, die ein Benutzer hat, um Aktionen auf einen Beitrag rückgängig zu machen (Gefällt mir, Meldung, usw.)." must_approve_users: "Team-Mitglieder müssen alle neuen Benutzerkonten freischalten, bevor diese Zugriff auf die Website erhalten. ACHTUNG: Das Aktivieren dieser Option für eine Live-Site entfernt den Zugriff auch für alle existierenden Benutzer außer für Team-Mitglieder!" pending_users_reminder_delay: "Benachrichtige die Moderatoren, falls neue Benutzer mehr als so viele Stunden auf ihre Genehmigung gewartet haben. Stelle -1 ein, um diese Benachrichtigungen zu deaktivieren." diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index ed8e3e75542..3ad65a40043 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -890,7 +890,6 @@ el: notify_mods_when_user_blocked: "Εάν ένας χρήστης αυτόματα μπλοκαριστει, στείλε μήνυμα σε όλους τους συντονιστές." flag_sockpuppets: "Εάν ένας νέος χρήστης απαντήσει σε ένα νήμα από την ίδια διεύθυνση ΙP όπως ο νέος χρήστης, ο οποίος ξεκίνησε το νήμα, και οι δυο δημοσιεύσεις τους θα επισημανθούν ως δυνητικά ανεπιθύμητες." traditional_markdown_linebreaks: "Χρήση παραδοσιακών αλλαγών γραμμών στη Markdown, η οποία απαιτεί δύο κενά διαστήματα για μια αλλαγή γραμμής." - allow_html_tables: "Αποδοχή εισδοχής πινάκων στη Markdown με τη χρήση ετικετών HTML. TABLE, THEAD, TD, TR, TH θα μπαίνουν στη λίστα επιτρεπόμενων (απαιτείται πληρής αντιγραφή σε όλες τις αναρτήσεις που περιέχουν πίνακες)" post_undo_action_window_mins: "Αριθμός των λεπτών όπου οι χρήστες δικαιούνται να αναιρέσουν πρόσφατες ενέργειες πάνω σε ένα θέμα (μου αρέσει, επισήμανση, κτλ) " must_approve_users: "Το προσωπικό πρέπει να εγκρίνει όλους τους λογαριασμούς των νέων χρηστών προτού τους επιτραπεί να έχουν πρόσβαση στην ιστοσελίδα. Προειδοποίηση: ενεργοποιώντας το για μια ζωντανή ιστοσελίδα θα έχει ως αποτέλεσμα την ανάκληση για τους υπάρχοντες χρήστες που δεν ανήκουν στο προσωπικό!" pending_users_reminder_delay: "Ειδοποίηση συντονιστών αν καινούργιοι χρήστες περιμένουν για αποδοχή για μεγαλύτερο απο αυτό το χρονικό διάστημα. Όρισέ το στο -1 για να απενεργοποιηθούν οι ειδοποιήσεις." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2335cfeb109..2fc2c220cc8 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1026,9 +1026,7 @@ en: flag_sockpuppets: "If a new user replies to a topic from the same IP address as the new user who started the topic, flag both of their posts as potential spam." traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak." - enable_experimental_markdown_it: "Enable the experimental markdown.it CommonMark engine, WARNING: some plugins may not work correctly" enable_markdown_typographer: "Use basic typography rules to improve text readability of paragraphs of text, replaces (c) (tm) etc, with symbols, reduces number of question marks and so on" - allow_html_tables: "Allow tables to be entered in Markdown using HTML tags. TABLE, THEAD, TD, TR, TH will be whitelisted (requires full rebake on all old posts containing tables)" post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)." must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site. WARNING: enabling this for a live site will revoke access for existing non-staff users!" pending_users_reminder_delay: "Notify moderators if new users have been waiting for approval for longer than this many hours. Set to -1 to disable notifications." diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 1dbe6baf1f9..775899a0f78 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -932,9 +932,7 @@ es: notify_mods_when_user_blocked: "Si un usuario es bloqueado automáticamente, enviar un mensaje a todos los moderadores." flag_sockpuppets: "Si un nuevo usuario responde a un tema desde la misma dirección de IP que el nuevo usuario que inició el tema, reportar los posts de los dos como spam en potencia." traditional_markdown_linebreaks: "Utiliza saltos de línea tradicionales en Markdown, que requieren dos espacios al final para un salto de línea." - enable_experimental_markdown_it: "Habilitar el motor experimental CommonMark markdown.it, ADVERTENCIA: algunos plugins podrían no funcionar correctamente." enable_markdown_typographer: "Utilice reglas básicas de tipografía para mejorar la legibilidad de texto de los párrafos de texto, reemplaza (c) (tm) etc, con símbolos, reduce el número de signos de interrogación y así sucesivamente" - allow_html_tables: "Permitir la inserción de tablas en Markdown usando etiquetas HTML. Se permitirá usar TABLE, THEAD, TD, TR o TH (requiere un rebake completo para los posts antiguos que contengan tablas)" post_undo_action_window_mins: "Número de minutos durante los cuales los usuarios pueden deshacer sus acciones recientes en un post (me gusta, reportes, etc)." must_approve_users: "Los miembros administración deben aprobar todas las nuevas cuentas antes de que se les permita el acceso al sitio. AVISO: ¡habilitar esta opción en un sitio activo revocará el acceso a los usuarios que no sean moderadores o admin!" pending_users_reminder_delay: "Notificar a los moderadores si hay nuevos usuarios que hayan estado esperando aprbación durante más estas horas. Usa -1 para desactivar estas notificaciones." diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 209ab34571a..a1dc35576d4 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -884,7 +884,6 @@ fa_IR: notify_mods_when_user_blocked: "اگر کاربر به‌طور خودکار مسدود شد، به تمام مدیران پیام بفرست." flag_sockpuppets: "اگر کاربری به موضوع با ای پی برابر با کاربری که نوشته را شروع کرده ٬ آنها را به عنوان هرزنامه پرچم گزاری کن." traditional_markdown_linebreaks: "در مدل‌های نشانه گزاری از خط جدید سنتی استفاده کن،‌ که برای linebreak نیاز به دو فضای انتهایی دارد ." - allow_html_tables: "اجازه ارسال جدول به صورت markdown با تگ های HTML. TABLE, THEAD, TD, TR, TH قابل استفاده هستند. (نیازمند ایجا دوباره در نوشته‌های قدیمی که شامل جدول هستند)" post_undo_action_window_mins: "تعداد دقایقی که کاربران اجازه دارند اقدامی را که در نوشته انجام داده اند باز گردانند. (پسند، پرچم گذاری،‌ چیزهای دیگر)." must_approve_users: "همکاران باید تمامی حساب‌های کاربری را قبل از اجازه دسترسی به سایت تایید کنند. اخطار: فعال‌سازی این گزینه ممکن است باعث جلوگیری از دسترسی کاربرانی که قبلا عضو شده‌اند نیز بشود!" pending_users_reminder_delay: "اگر کاربر‌ها بیشتر از این مقدار ساعت منتظر تایید بودند به مدیران اعلام کن. مقدار -1 برای غیرفعال‌سازی." diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 9c699ac9ec1..d2aad62daa6 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -931,9 +931,7 @@ fi: notify_mods_when_user_blocked: "Jos käyttäjä estetään automaattisesti, lähetä viesti kaikille valvojille." flag_sockpuppets: "Jos uuden käyttäjän luomaan ketjuun vastaa toinen uusi käyttäjä samasta IP-osoitteesta, liputa molemmat viestit mahdolliseksi roskapostiksi." traditional_markdown_linebreaks: "Käytä perinteisiä rivinvaihtoja Markdownissa, joka vaatii kaksi perättäistä välilyöntiä rivin vaihtoon." - enable_experimental_markdown_it: "Ota käyttöön kokeellinen markdown.it Commonmark ohjelmistomoottori. VAROITUS: jotkut lisäosat voivat lakata toimimasta oikein" enable_markdown_typographer: "Käytetään tavanomaisia typografisia sääntöjä parantamaan tekstikappaleiden luettavuutta, (c), (tm) ym. korvataan symboleilla, kysymysmerkkien määrää vähennetään jne." - allow_html_tables: "Salli taulukoiden syöttäminen Markdowniin käyttäen HTML tageja. TABLE, THEAD, TD, TR, TH valkolistataan (edellyttää kaikkien taulukoita sisältävien vanhojen viestien uudelleen rakentamisen)" post_undo_action_window_mins: "Kuinka monta minuuttia käyttäjällä on aikaa perua viestiin kohdistuva toimi (tykkäys, liputus, etc)." must_approve_users: "Henkilökunnan täytyy hyväksyä kaikki uudet tilit, ennen uusien käyttäjien päästämistä sivustolle. VAROITUS: tämän asetuksen valitseminen poistaa pääsyn kaikilta jo olemassa olevilta henkilökuntaan kuulumattomilta käyttäjiltä." pending_users_reminder_delay: "Ilmoita valvojille, jos uusi käyttäjä on odottanut hyväksyntää kauemmin kuin näin monta tuntia. Aseta -1, jos haluat kytkeä ilmoitukset pois päältä." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 2b85e006886..cd44c0a4586 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -931,7 +931,6 @@ fr: notify_mods_when_user_blocked: "Si un utilisateur est bloqué automatiquement, envoyer un message à tous les modérateurs." flag_sockpuppets: "Si un nouvel utilisateur répond à un sujet avec la même adresse IP que le nouvel utilisateur qui a commencé le sujet, alors leurs messages seront automatiquement marqués comme spam." traditional_markdown_linebreaks: "Utiliser le retour à la ligne traditionnel dans Markdown, qui nécessite deux espaces pour un saut de ligne." - allow_html_tables: "Autoriser la saisie des tableaux dans le Markdown en utilisant les tags HTML : TABLE, THEAD, TD, TR, TH sont autorisés (nécessite un rebake de tous les anciens messages contenant des tableaux)" post_undo_action_window_mins: "Nombre de minutes pendant lesquelles un utilisateur peut annuler une action sur un message (J'aime, signaler, etc.)" must_approve_users: "Les responsables doivent approuver les nouveaux utilisateurs afin qu'ils puissent accéder au site. ATTENTION : activer cette option sur un site en production suspendra l'accès des utilisateurs existants qui ne sont pas des responsables !" pending_users_reminder_delay: "Avertir les modérateurs si des nouveaux utilisateurs sont en attente d'approbation depuis x heures. Mettre -1 pour désactiver les notifications." diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 5795be1d51c..d809362bdc1 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -921,7 +921,6 @@ he: notify_mods_when_user_blocked: "אם משתמש נחסם אוטומטית, שילחו הודעה לכל המנחים." flag_sockpuppets: "אם משתמשים חדשים מגיבים לנושא מכתובת IP זהה לזו של מי שהחל את הנושא, סמנו את הפוסטים של שניהם כספאם פוטנציאלי." traditional_markdown_linebreaks: "שימוש בשבירת שורות מסורתית בסימון, מה שדורש שני רווחים עוקבים למעבר שורה." - allow_html_tables: "אפשרו הכנסת טבלאות ב Markdown באמצעות תגיות HTML. התגיות TABLE, THEAD, TD, TR, TH יהיו ברשימה לבנה (מצריך אפייה מחדש של כל הפוסטים הישנים שכוללים טבלאות)" post_undo_action_window_mins: "מספר הדקות בהן מתאפשר למשתמשים לבטל פעולות אחרות בפוסט (לייק, סימון, וכו')." must_approve_users: "על הצוות לאשר את כל המשתמשים החדשים לפני שהם מקבלים גישה לאתר. אזהרה: בחירה זו עבור אתר קיים תשלול גישה ממשתמשים קיימים שאינם מנהלים." pending_users_reminder_delay: "הודיעו למנחים אם משתמשים חדשים ממתינים לאישור למעלה מכמות זו של שעות. קבעו ל -1 כדי לנטרל התראות." diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 722c723d096..ad31c6d81b5 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -934,7 +934,6 @@ it: notify_mods_when_user_blocked: "Se un utente è bloccato automaticamente, manda un messaggio ai moderatori." flag_sockpuppets: "Se un nuovo utente risponde ad un argomento dallo stesso indirizzo IP dell'utente che ha aperto l'argomento stesso, segnala entrambi i messaggi come potenziale spam." traditional_markdown_linebreaks: "Usa l'accapo tradizionale in Markdown, cioè due spazi a fine riga per andare a capo." - allow_html_tables: "Consenti di inserire tabelle in Markdown usando tag HTML. I tag TABLE, THEAD, TD, TR, TH saranno consentiti (richiede un full rebake di tutti i vecchi messaggi contenenti tabelle)" post_undo_action_window_mins: "Numero di minuti durante i quali gli utenti possono annullare le loro azioni recenti su un messaggio (segnalazioni, Mi piace, ecc.)." must_approve_users: "Lo staff deve approvare tutti i nuovi account utente prima che essi possano accedere al sito. ATTENZIONE: abilitare l'opzione per un sito live revocherà l'accesso per tutti gli utenti non-staff esistenti!" pending_users_reminder_delay: "Notifica i moderatori se nuovi utenti sono in attesa di approvazione per più di queste ore. Imposta a -1 per disabilitare le notifiche." diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 30f6b772cda..2bffa0478e7 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -876,9 +876,7 @@ ko: notify_mods_when_user_blocked: "만약 사용자가 자동 블락되면 중간 운영자에게 메시지 보내기" flag_sockpuppets: "어떤 신규 사용자(예:24이내 가입자)가 글타래를 생성하고 같은 IP주소의 또 다른 신규 사용자가 댓글을 쓰면 자동 스팸 신고" traditional_markdown_linebreaks: "Markdown에서 전통적인 linebreak를 사용, linebreak시 두개의 trailing space를 사용하는 것." - enable_experimental_markdown_it: "(실험) CommonMark를 지원하는 markdown.it 엔진을 사용합니다. 경고: 올바르게 작동하지 않는 플러그인이 있을 수 있습니다." enable_markdown_typographer: "문단의 가독성을 높이기 위해서 기본 타이포그라피 룰을 사용합니다. (c) (tm), 기타 기호를 교체하고 연달아 나오는 물음표의 갯수를 줄입니다." - allow_html_tables: "마크다운 문서에 HTML 테이블을 허용합니다. TABLE, THEAD, TD, TR, TH 태그를 사용할 수 있습니다.(테이블이 포함된 이전 게시물에 적용하려면 rebake 해야 합니다.)" post_undo_action_window_mins: "사용자가 어떤 글에 대해서 수행한 작업(신고 등)을 취소하는 것이 허용되는 시간(초)" must_approve_users: "스태프는 반드시 사이트 엑세스권한을 허용하기 전에 모든 신규가입계정을 승인해야 합니다. 경고: 이것을 활성화하면 기존 스태프 아닌 회원들의 엑세스권한이 회수됩니다." pending_users_reminder_delay: "새로운 사용자가 승인을 기다리는 시간이 여기에 지정된 시간횟수보다 더 길어길경우 운영자에게 알려줍니다. 알림을 해제하려면 -1로 설정하세요." diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index c28ee211c71..828a8c074ba 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -895,7 +895,6 @@ nl: notify_mods_when_user_blocked: "Als een gebruiker automatisch geblokkeerd is, stuur dan een bericht naar alle moderatoren." flag_sockpuppets: "Als een nieuwe gebruiker antwoord op een topic vanaf hetzelfde ip-adres als de nieuwe gebruiker die het topic opende, markeer dan beide berichten als potentiële spam." traditional_markdown_linebreaks: "Gebruik traditionele regeleinden in Markdown, welke 2 spaties aan het einde van een regel nodig heeft voor een regeleinde." - allow_html_tables: "Sta toe dat tabellen in Markdown mogen worden ingevoerd met behulp van HTML-tags. TABLE, TD, TR, TH zullen aan de whitelist worden toegevoegd (vereist volledig herbouwen van alle oude berichten met tabellen)" post_undo_action_window_mins: "Het aantal minuten waarin gebruikers hun recente acties op een bericht nog terug kunnen draaien (liken, markeren, etc)." must_approve_users: "Stafleden moeten alle nieuwe gebruikersaccounts goedkeuren voordat ze de site mogen bezoeken. OPGELET: als dit wordt aangezet voor een actieve site wordt alle toegang voor bestaande niet stafleden ingetrokken." pending_users_reminder_delay: "Moderators informeren als nieuwe gebruikers al langer dan dit aantal uren op goedkeuring wachten. Stel dit in op -1 om meldingen uit te schakelen." diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 67ddc8bdf67..bdf16364395 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -956,7 +956,6 @@ pl_PL: notify_mods_when_user_blocked: "If a user is automatically blocked, send a message to all moderators." flag_sockpuppets: "Jeśli nowy użytkownik odpowiada na dany temat z tego samego adresu IP co nowy użytkownik, który założył temat, oznacz ich posty jako potencjalny spam." traditional_markdown_linebreaks: "Używaj tradycyjnych znaków końca linii w Markdown, to znaczy dwóch spacji na końcu linii." - allow_html_tables: "Pozwalaj tabelom być zamieszczanym w Markdown przy użyciu tagów HTML. TABLE, THEAD, TD, TR, TH będą dozwolone (wymaga pełnego rebake na wszystkich starych postach zawierających tabele)." post_undo_action_window_mins: "Przez tyle minut użytkownicy mogą cofnąć swoje ostatnie działania przy danym poście (lajki, flagowanie, itd.)." must_approve_users: "Zespół musi zaakceptować wszystkie nowe konta zanim uzyskają dostęp do serwisu. UWAGA: włączenie tego dla już udostępnionej strony sprawi, że zostanie odebrany dostęp wszystkim istniejącym użytkownikom spoza zespołu." pending_users_reminder_delay: "Powiadomić moderatorów jeżeli nowi użytkownicy czekali na zatwierdzenie dłużej niż his mamy godzin. Ustaw -1 aby wyłączyć powiadomienia. " diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index a89dda5a2da..3f859154d67 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -802,7 +802,6 @@ pt: notify_mods_when_user_blocked: "Se um utilizador for bloqueado de forma automática, enviar uma mensagem a todos os moderadores." flag_sockpuppets: "Se um novo utilizador responde a um tópico a partir do mesmo endereço IP do novo utilizador que iniciou o tópico, sinalizar ambas as mensagens como potencial spam." traditional_markdown_linebreaks: "Utilize tradicionais quebras de linha no Markdown, que requer dois espaços no final para uma quebra de linha." - allow_html_tables: "Permitir inserção de tabelas em Markdown utilizando tags HTML. TABLE,THEAD, TD, TR,TH fazem parte da lista branca (requer que todas as mensagens antigas que contém tabelas sejam refeitas)" post_undo_action_window_mins: "Número de minutos durante o qual os utilizadores têm permissão para desfazer ações numa mensagem (gostos, sinalizações, etc)." must_approve_users: "O pessoal deve aprovar todas as novas contas de utilizador antes destas terem permissão para aceder ao sítio. AVISO: ativar isto para um sítio ativo irá revogar o acesso aos utilizadores existentes que não fazem parte do pessoal!" pending_users_reminder_delay: "Notificar moderadores se novos utilizadores estiverem à espera de aprovação por mais que esta quantidade de horas. Configurar com -1 para desativar notificações." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 8082e409bf1..e4413b92537 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -878,7 +878,6 @@ ro: notify_mods_when_user_blocked: "Dacă un utilizator este blocat automat, trimite un mesaj tuturor moderatorilor." flag_sockpuppets: "Dacă un utilizator nou răspunde unui subiect de la același IP ca utilizatorul ce a pornit subiectul, marchează ambele postări ca potențial spam." traditional_markdown_linebreaks: "Folosește întreruperi de rând tradiționale în Markdown, ceea ce necesită două spații pentru un capăt de rând. " - allow_html_tables: "Permite introducerea de tabele în Markdown prin folosirea de etichete HTML. HEAD, TD, TR, TH vor fi autorizate (necesită un rebake pe toate postările vechi ce conțin tabele)" post_undo_action_window_mins: "Numărul de minute în care utilizatorii pot anula acțiunile recente asupra unei postări (aprecieri, marcări cu marcaje de avertizare, etc)." must_approve_users: "Membrii echipei trebuie să aprobe toate conturile noilor utilizatori înainte ca aceștia să poată accesa site-ul. ATENȚIE: activarea acestei opțiuni pentru un site în producție va revoca accesul tuturor utilizatorilor care nu sunt membri ai echipei!" pending_users_reminder_delay: "Notifică moderatorii dacă noii utilizatori sunt în așteptarea aprobării de mai mult de atâtea ore. Setează la -1 pentru a dezactiva notificările." diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 71cd2e89a39..912d3d5da07 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -761,7 +761,6 @@ sk: notify_mods_when_user_blocked: "Ak je používateľ automaticky zablokovaný, pošli správu všetkým moderátorom." flag_sockpuppets: "Ak nový používateľ odpovedá na tému z rovnakej IP adresy, ako nový používateľ, ktorý danú tému vytvoril, označ oba ich príspevky ako potencionálny spam." traditional_markdown_linebreaks: "V Markdown použiť tradičné oddeľovače riadkov, čo vyžaduje dve koncové medzery ako oddeľovač riadku." - allow_html_tables: "V Markdown umožniť použitie tabuliek pomocou HTML značiek. TABLE, THEAD, TD, TR, TH budú umožnené (vyžaduje \"full rebake\" na všetkých starých príspevkoch ktoré obsahujú tabuľky)" post_undo_action_window_mins: "Počet minút počas ktorých môžu používatelia zrušiť poslednú akciu na príspevku (\"Páči sa\", označenie, atď..)." must_approve_users: "Obsluha musí povoliť účty všetkým novým používateľom skôr než im bude povolený prístup na stránku. UPOZORNENIE: zapnutie na živej stránke spôsobí zrušenie prístupu pre existujúcich používateľov, okrem obsluhy!" pending_users_reminder_delay: "Upozorni moderátora ak nový používateľ čaká na schválenie dlhšie ako tento počet hodín. Nastavte -1 pre vypnutie upozornenia." diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 6ac8c593d3e..e6242939a3d 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -818,7 +818,6 @@ sv: notify_mods_when_user_blocked: "Om en användare blockeras automatiskt, skicka ett meddelande till alla moderatorer." flag_sockpuppets: "Flagga båda användarnas inlägg som potentiell skräppost om en ny användare svarar på ett ämne från samma IP-adress som den andra nya användaren som skapade ämnet." traditional_markdown_linebreaks: "Använd vanliga radmatningar i Markdown, vilka kräver 2 avslutande mellanslag för en radmatning." - allow_html_tables: "Tillåt tabeller att läggas in i Markdown genom användning av HTML-taggar. TABLE, THEAD, TD, TR, TH kommer att vitlistas (kräver full uppdatering/rebake av alla gamla inlägg som innehåller tabeller)" post_undo_action_window_mins: "Antal minuter som en användare tillåts att upphäva handlingar på ett inlägg som gjorts nyligen (gillning, flaggning osv)." must_approve_users: "Personal måste godkänna alla nya användarkonton innan de tillåts använda webbplatsen. VARNING: om det tillåts när webbplatsen är live så kommer det att upphäva tillgång för alla existerande användare som inte är personal!" pending_users_reminder_delay: "Notifiera moderatorer om nya användare har väntat på godkännande längre än så här många timmar. Ange -1 för att inaktivera notifikationer. " diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 0430d47a47d..1e9ec205cb9 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -705,7 +705,6 @@ tr_TR: notify_mods_when_user_blocked: "Eğer bir kullanıcı otomatik olarak engellendiyse, tüm moderatörlere ileti yolla." flag_sockpuppets: "Eğer, yeni kullanıcı konuya, konuyu başlatan yeni kullanıcı ile aynı IP adresinden cevap yazarsa, her iki gönderiyi de potansiyel istenmeyen olarak bildir. " traditional_markdown_linebreaks: "Markdown'da, satır sonundan önce yazının sağında iki tane boşluk gerektiren, geleneksel satır sonu metodunu kullan." - allow_html_tables: "Tabloların HTML etiketleri kullanılarak Markdown ile oluşturulmasına izin verin. TABLE, THEAD, TD, TR, TH kabul edilir (tablo içeren tüm eski gönderilerin yenilenmesini gerektirir) " post_undo_action_window_mins: "Bir gönderide yapılan yeni eylemlerin (beğenme, bildirme vb) geri alınabileceği zaman, dakika olarak" must_approve_users: "Siteye erişimlerine izin verilmeden önce tüm yeni kullanıcı hesaplarının görevliler tarafından onaylanması gerekir. UYARI: yayındaki bir site için bunu etkinleştirmek görevli olmayan hesapların erişimini iptal edecek." pending_users_reminder_delay: "Belirtilen saatten daha uzun bir süredir onay bekleyen yeni kullanıcılar mevcutsa moderatörleri bilgilendir. Bilgilendirmeyi devre dışı bırakmak için -1 girin." diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index 62f13094af1..346f3e56281 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -681,7 +681,6 @@ vi: notify_mods_when_user_blocked: "Nếu một thành viên được khóa tự động, gửi tin nhắn đến tất cả các điều hành viên." flag_sockpuppets: "Nếu thành viên mới trả lời chủ đề có cùng địa chỉ IP với thành viên mới tạo chủ đề, đánh dấu các bài viết của họ là spam tiềm năng." traditional_markdown_linebreaks: "Sử dụng ngắt dòng truyền thống trong Markdown, đòi hỏi hai khoảng trống kế tiếp cho một ngắt dòng." - allow_html_tables: "Cho phép nhập bảng trong Markdown sử dụng các thẻ HTML. TABLE, THEAD, TD, TR, TH sẽ được sử dụng (đòi hỏi thực hiện lại cho các bài viết cũ có chứa bảng)" post_undo_action_window_mins: "Số phút thành viên được phép làm lại các hành động gần đây với bài viết (like, đánh dấu...)." must_approve_users: "Quản trị viên phải duyệt tất cả các tài khoản thành viên mới trước khi họ có quyền truy cập website. LƯU Ý: bật tính năng này trên site đang hoạt động sẽ hủy bỏ quyền truy cập đối với các tài khoản thành viên hiện tại!" pending_users_reminder_delay: "Thông báo cho quản trị viên nếu thành viên mới đã chờ duyệt lâu hơn số giờ được thiết lập ở đây, đặt là -1 để tắt thông báo." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 297f1c3e334..b81aabde716 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -881,7 +881,6 @@ zh_CN: notify_mods_when_user_blocked: "如果一个用户被自动封禁了,发送一个私信给所有管理员。" flag_sockpuppets: "如果一个新用户开始了一个主题,并且同时另一个新用户以同一个 IP 在该主题回复,他们所有的帖子都将被自动标记为垃圾。" traditional_markdown_linebreaks: "在 Markdown 中使用传统换行符,即用两个尾随空格来换行" - allow_html_tables: "允许在输入 Markdown 文本时使用表格 HTML 标签。标签 TABLE、THEAD、TD、TR、TH 将被允许使用,即白名单这些标签(需要重置所有包含表格的老帖子的 HTML)" post_undo_action_window_mins: "允许用户在帖子上进行撤销操作(赞、标记等)所需等待的间隔分钟数" must_approve_users: "新用户在被允许访问站点前需要由管理人员批准。警告:在运行的站点中启用将解除所有非管理人员用户的访问权限!" pending_users_reminder_delay: "如果新用户等待批准时间超过此小时设置则通知版主。设置 -1 关闭通知。" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index ebd13f4af56..16962647e74 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -832,7 +832,6 @@ zh_TW: notify_mods_when_user_blocked: "若有用戶被自動封鎖,將發送訊息給所有板主。" flag_sockpuppets: "如果一個新用戶開始了一個主題,並且同時另一個新用戶以同一個 IP 在該主題回復,他們所有的帖子都將被自動標記為垃圾。" traditional_markdown_linebreaks: "在 Markdown 中使用傳統的換行符號,即用兩個行末空格來換行" - allow_html_tables: "允許在輸入 Markdown 文本時使用表格 HTML 標籤。標籤 TABLE、THEAD、TD、TR、TH 將被允許使用,即白名單這些標籤(需要重置所有包含表格的老帖子的 HTML)" post_undo_action_window_mins: "允許用戶在帖子上進行撤銷操作(讚、標記等)所需等待的時間分隔(分鐘)" must_approve_users: "新用戶在被允許訪問站點前需要由管理人員批准。警告:在運行的站點中啟用將解除所有非管理人員用戶的訪問權限!" pending_users_reminder_delay: "如果新用戶等待批准時間超過此小時設置則通知版主。設置 -1 關閉通知。" diff --git a/config/site_settings.yml b/config/site_settings.yml index 67f8503b91c..7bef397bdda 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -495,19 +495,12 @@ posting: delete_removed_posts_after: client: true default: 24 - enable_experimental_markdown_it: - client: true - default: false - shadowed_by_global: true traditional_markdown_linebreaks: client: true default: false enable_markdown_typographer: client: true - default: false - allow_html_tables: - client: true - default: false + default: true suppress_reply_directly_below: client: true default: true diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 2b9b3489b78..fd1e73c68a7 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -80,11 +80,7 @@ module PrettyText ctx_load(ctx, "#{Rails.root}/app/assets/javascripts/discourse-loader.js") ctx_load(ctx, "vendor/assets/javascripts/lodash.js") ctx_load_manifest(ctx, "pretty-text-bundle.js") - - if SiteSetting.enable_experimental_markdown_it - ctx_load_manifest(ctx, "markdown-it-bundle.js") - end - + ctx_load_manifest(ctx, "markdown-it-bundle.js") root_path = "#{Rails.root}/app/assets/javascripts/" apply_es6_file(ctx, root_path, "discourse/lib/utilities") @@ -152,13 +148,6 @@ module PrettyText paths[:S3BaseUrl] = Discourse.store.absolute_base_url end - if SiteSetting.enable_experimental_markdown_it - # defer load markdown it - unless context.eval("window.markdownit") - ctx_load_manifest(context, "markdown-it-bundle.js") - end - end - custom_emoji = {} Emoji.custom.map { |e| custom_emoji[e.name] = e.url } @@ -186,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/lib/pretty_text/shims.js b/lib/pretty_text/shims.js index b3efe6cdf54..93a1ef592fd 100644 --- a/lib/pretty_text/shims.js +++ b/lib/pretty_text/shims.js @@ -7,8 +7,6 @@ __utils = require('discourse/lib/utilities'); __emojiUnicodeReplacer = null; __setUnicode = function(replacements) { - require('pretty-text/engines/discourse-markdown/emoji').setUnicodeReplacements(replacements); - let unicodeRegexp = new RegExp(Object.keys(replacements).sort().reverse().join("|"), "g"); __emojiUnicodeReplacer = function(text) { diff --git a/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 b/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 index 4fb9b3d0f3b..602846062e2 100644 --- a/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 +++ b/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 @@ -1,25 +1,3 @@ -import { registerOption } from 'pretty-text/pretty-text'; - -function insertDetails(_, summary, details) { - return `
${summary}${details}
`; -} - -// replace all [details] BBCode with HTML 5.1 equivalent -function replaceDetails(text) { - text = text || ""; - - while (text !== (text = text.replace(/\[details=([^\]]+)\]((?:(?!\[details=[^\]]+\]|\[\/details\])[\S\s])*)\[\/details\]/ig, insertDetails))); - - // add new lines to make sure we *always* have a

element after and around - // otherwise we can't hide the content since we can't target text nodes via CSS - return text.replace(/<\/summary>/ig, "\n\n") - .replace(/<\/details>/ig, "\n\n\n\n"); -} - -registerOption((siteSettings, opts) => { - opts.features.details = true; -}); - const rule = { tag: 'details', before: function(state, attrs) { @@ -46,11 +24,7 @@ export function setup(helper) { 'details.elided' ]); - if (helper.markdownIt) { - helper.registerPlugin(md => { - md.block.bbcode_ruler.push('details', rule); - }); - } else { - helper.addPreProcessor(text => replaceDetails(text)); - } + helper.registerPlugin(md => { + md.block.bbcode_ruler.push('details', rule); + }); } diff --git a/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 b/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 index a1bd26c43d6..4087ea22b21 100644 --- a/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 +++ b/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 @@ -8,7 +8,7 @@ const defaultOpts = buildOptions({ emoji_set: 'emoji_one', highlighted_languages: 'json|ruby|javascript', default_code_lang: 'auto', - censored_words: 'shucks|whiz|whizzer' + censored_words: '' }, getURL: url => url }); @@ -19,17 +19,13 @@ test("details", assert => { assert.equal(new PrettyText(defaultOpts).cook(input), expected.replace(/\/>/g, ">"), text); }; cooked(`

Infocoucou
`, - `
Info\n\n

coucou

\n\n
`, + `
Infocoucou
`, "manual HTML for details"); - cooked(`
Infocoucou
`, - `
Info\n\n

coucou

\n\n
`, - "manual HTML for details with a space"); - cooked(`
Infocoucou
`, - `
Info\n\n

coucou

\n\n
`, - "open attribute"); - - cooked(`
Infocoucou
`, - `
Info\n\n

coucou

\n\n
`, - "open attribute"); + cooked("[details=testing]\ntest\n[/details]", +`
+ +testing +

test

+
`); }); diff --git a/script/.gitignore b/script/.gitignore new file mode 100644 index 00000000000..1fb9ef57453 --- /dev/null +++ b/script/.gitignore @@ -0,0 +1 @@ +tmp/* diff --git a/script/import_scripts/lithium.rb b/script/import_scripts/lithium.rb index bd0a6f00553..54cd27e1d92 100644 --- a/script/import_scripts/lithium.rb +++ b/script/import_scripts/lithium.rb @@ -53,7 +53,6 @@ class ImportScripts::Lithium < ImportScripts::Base def execute - SiteSetting.allow_html_tables = true @max_start_id = Post.maximum(:id) import_categories diff --git a/script/test_pretty_text.rb b/script/test_pretty_text.rb new file mode 100644 index 00000000000..b9456788e36 --- /dev/null +++ b/script/test_pretty_text.rb @@ -0,0 +1,24 @@ +require File.expand_path("../../config/environment", __FILE__) + + +puts PrettyText.cook "test" +1000.times do |i| + # PrettyText.v8.eval <<~JS + # window.markdownit().render('test'); + # JS + PrettyText.cook "test" + + PrettyText.v8.eval('gc()') + + # if i % 500 == 0 + #p PrettyText.v8.heap_stats + # end +end + +# sam@ubuntu script % ruby test_pretty_text.rb +# {:total_physical_size=>10556240, :total_heap_size_executable=>5242880, :total_heap_size=>16732160, :used_heap_size=>7483336, :heap_size_limit=>1501560832} +# {:total_physical_size=>288670880, :total_heap_size_executable=>6291456, :total_heap_size=>292507648, :used_heap_size=>252365360, :heap_size_limit=>1501560832} +# {:total_physical_size=>543060056, :total_heap_size_executable=>6291456, :total_heap_size=>548360192, :used_heap_size=>503699768, :heap_size_limit=>1501560832} +# {:total_physical_size=>793401560, :total_heap_size_executable=>6291456, :total_heap_size=>801067008, :used_heap_size=>739517840, :heap_size_limit=>1501560832} +# {:total_physical_size=>1045932696, :total_heap_size_executable=>6291456, :total_heap_size=>1055870976, :used_heap_size=>992549688, :heap_size_limit=>1501560832} +# {:total_physical_size=>1298442008, :total_heap_size_executable=>6291456, :total_heap_size=>1309626368, :used_heap_size=>1224681072, :heap_size_limit=>1501560832} diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 9767681376d..775ed145865 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -164,7 +164,7 @@ describe CookedPostProcessor do it "generates overlay information" do cpp.post_process_images - expect(cpp.html).to match_html "

+ expect(cpp.html).to match_html "

\n

" expect(cpp).to be_dirty @@ -197,7 +197,7 @@ describe CookedPostProcessor do it "generates overlay information" do cpp.post_process_images - expect(cpp.html).to match_html "

+ expect(cpp.html).to match_html "

\n

" expect(cpp).to be_dirty @@ -206,7 +206,7 @@ describe CookedPostProcessor do it "should escape the filename" do upload.update_attributes!(original_filename: ">.png") cpp.post_process_images - expect(cpp.html).to match_html "

+ expect(cpp.html).to match_html "

\n

" end @@ -233,7 +233,7 @@ describe CookedPostProcessor do it "generates overlay information" do cpp.post_process_images - expect(cpp.html).to match_html "

+ expect(cpp.html).to match_html "

\n

" expect(cpp).to be_dirty @@ -652,7 +652,7 @@ describe CookedPostProcessor do let(:cpp) { CookedPostProcessor.new(post) } context "emoji inside a quote" do - let(:post) { Fabricate(:post, raw: "time to eat some sweet [quote]:candy:[/quote] mmmm") } + let(:post) { Fabricate(:post, raw: "time to eat some sweet \n[quote]\n:candy:\n[/quote]\n mmmm") } it "doesn't award a badge when the emoji is in a quote" do cpp.grant_badges diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index ea6c749d5d6..b3aa2468c92 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -5,7 +5,7 @@ require 'html_normalize' describe PrettyText do before do - SiteSetting.enable_experimental_markdown_it = true + SiteSetting.enable_markdown_typographer = false end def n(html) @@ -61,7 +61,7 @@ describe PrettyText do [/quote] MD html = <<~HTML -