From f62b8990acb9ce4af8bfc67fc80dc0847177e458 Mon Sep 17 00:00:00 2001 From: Dan Ungureanu Date: Mon, 9 Dec 2019 15:07:15 +0200 Subject: [PATCH] FIX: Do not autocomplete categories or emojis in code blocks (#8459) This reapplies commit b643526d9a407b8abb826dba78a954cdfe6d6133 after being reverted in commit f65c4535556eeff24944369d6f262ef6be147eec. Unlike the original commit, this does a single pass and does not take into account unfinished code blocks. --- .../components/composer-editor.js.es6 | 8 +++-- .../discourse/components/d-editor.js.es6 | 13 ++++++-- .../discourse/lib/category-hashtags.js.es6 | 20 ++++++++---- .../discourse/lib/utilities.js.es6 | 25 +++++++++++++++ test/javascripts/lib/utilities-test.js.es6 | 32 ++++++++++++++++++- 5 files changed, 87 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index ce8634bfcf7..8e58a0de04e 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -33,7 +33,9 @@ import { tinyAvatar, formatUsername, clipboardData, - safariHacksDisabled + safariHacksDisabled, + caretPosition, + inCodeBlock } from "discourse/lib/utilities"; import { validateUploadedFiles, @@ -192,7 +194,9 @@ export default Component.extend({ afterComplete() { // ensures textarea scroll position is correct scheduleOnce("afterRender", () => $input.blur().focus()); - } + }, + triggerRule: textarea => + !inCodeBlock(textarea.value, caretPosition(textarea)) }); } diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index f223071d733..7a26db929d4 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -20,7 +20,9 @@ import { siteDir } from "discourse/lib/text-direction"; import { determinePostReplaceSelection, clipboardData, - safariHacksDisabled + safariHacksDisabled, + caretPosition, + inCodeBlock } from "discourse/lib/utilities"; import toMarkdown from "discourse/lib/to-markdown"; import deprecated from "discourse-common/lib/deprecated"; @@ -420,6 +422,10 @@ export default Component.extend({ }, onKeyUp: (text, cp) => { + if (inCodeBlock(text, cp)) { + return false; + } + const matches = /(?:^|[^a-z])(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/gi.exec( text.substring(0, cp) ); @@ -511,7 +517,10 @@ export default Component.extend({ } return list; }); - } + }, + + triggerRule: textarea => + !inCodeBlock(textarea.value, caretPosition(textarea)) }); }, diff --git a/app/assets/javascripts/discourse/lib/category-hashtags.js.es6 b/app/assets/javascripts/discourse/lib/category-hashtags.js.es6 index cac265f259e..28042fff160 100644 --- a/app/assets/javascripts/discourse/lib/category-hashtags.js.es6 +++ b/app/assets/javascripts/discourse/lib/category-hashtags.js.es6 @@ -1,5 +1,9 @@ export const SEPARATOR = ":"; -import { caretRowCol } from "discourse/lib/utilities"; +import { + caretRowCol, + caretPosition, + inCodeBlock +} from "discourse/lib/utilities"; export function replaceSpan($elem, categorySlug, categoryLink) { $elem.replaceWith( @@ -21,10 +25,14 @@ export function categoryHashtagTriggerRule(textarea, opts) { if (/^#{1}\w+/.test(line)) return false; } - if (col < 6) { - // Don't trigger autocomplete when ATX-style headers are used - return line.slice(0, col) !== "#".repeat(col); - } else { - return true; + // Don't trigger autocomplete when ATX-style headers are used + if (col < 6 && line.slice(0, col) === "#".repeat(col)) { + return false; } + + if (inCodeBlock(textarea.value, caretPosition(textarea))) { + return false; + } + + return true; } diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index 145674f3a57..7bda9ca4df5 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -410,5 +410,30 @@ export function rescueThemeError(name, error, api) { document.body.prepend(alertDiv); } +const CODE_BLOCKS_REGEX = /^( |\t).*|`[^`]+`|^```[^]*?^```|\[code\][^]*?\[\/code\]/gm; +// | ^ | ^ | ^ | ^ | +// | | | | +// | | | code blocks between [code] +// | | | +// | | +--- code blocks between three backquote +// | | +// | +----- inline code between backquotes +// | +// +------- paragraphs starting with 4 spaces or tab + +export function inCodeBlock(text, pos) { + const matches = text.matchAll(CODE_BLOCKS_REGEX); + + for (const match of matches) { + const begin = match.index; + const end = match.index + match[0].length; + if (begin <= pos && pos <= end) { + return true; + } + } + + return false; +} + // This prevents a mini racer crash export default {}; diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index 39c9b0635a5..dd34d8a204c 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -9,7 +9,8 @@ import { setDefaultHomepage, caretRowCol, setCaretPosition, - fillMissingDates + fillMissingDates, + inCodeBlock } from "discourse/lib/utilities"; QUnit.module("lib:utilities"); @@ -186,3 +187,32 @@ QUnit.test("fillMissingDates", assert => { "it returns a JSON array with 31 dates" ); }); + +QUnit.test("inCodeBlock", assert => { + const text = + "000\n\n```\n111\n```\n\n000\n\n`111 111`\n\n000\n\n[code]\n111\n[/code]\n\n 111\n\t111\n\n000`000"; + for (let i = 0; i < text.length; ++i) { + if (text[i] === "0") { + assert.notOk(inCodeBlock(text, i), `position ${i} is not in code block`); + } else if (text[i] === "1") { + assert.ok(inCodeBlock(text, i), `position ${i} is in code block`); + } + } +}); + +QUnit.skip("inCodeBlock - runs fast", assert => { + const phrase = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; + const text = `${phrase}\n\n\`\`\`\n${phrase}\n\`\`\`\n\n${phrase}\n\n\`${phrase}\n${phrase}\n\n${phrase}\n\n[code]\n${phrase}\n[/code]\n\n${phrase}\n\n ${phrase}\n\n\`${phrase}\`\n\n${phrase}`; + + let time = Number.MAX_VALUE; + for (let i = 0; i < 10; ++i) { + const start = performance.now(); + inCodeBlock(text, text.length); + const end = performance.now(); + time = Math.min(time, end - start); + } + + // This runs in 'keyUp' event handler so it should run as fast as + // possible. It should take less than 1ms for the test text. + assert.ok(time < 10); +});