diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index e668828ccbf..e0b745a47f8 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..4ab8eb7ccb2 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -410,5 +410,44 @@ export function rescueThemeError(name, error, api) { document.body.prepend(alertDiv); } +const CODE_BLOCKS_RULES = [ + { rule: /`(?:[^`\n]+?\n?)+?`/gm, end: "`" }, + { rule: /^```[^]*?^```/gm, end: "\n```" }, + { rule: /\[code\][^]*?\[\/code\]/gm, end: "\n[/code]" } +]; + +export function getCodeBlocks(value) { + const blocks = []; + + CODE_BLOCKS_RULES.forEach(entry => { + const { rule, end } = entry; + + let match; + while ((match = rule.exec(value)) != null) { + blocks.push([match.index, match.index + match[0].length]); + } + + // Try to end block and see if other code blocks are found + if (end) { + while ((match = rule.exec(value + end)) != null) { + // Save only positions that were not found before (which end past the + // end of the original value). + if ( + match.index < value.length && + match.index + match[0].length > value.length + ) { + blocks.push([match.index, value.length]); + } + } + } + }); + + return blocks; +} + +export function inCodeBlock(value, pos) { + return getCodeBlocks(value).any(([start, end]) => start <= pos && pos <= end); +} + // 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..46c00735bf3 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -9,7 +9,9 @@ import { setDefaultHomepage, caretRowCol, setCaretPosition, - fillMissingDates + fillMissingDates, + getCodeBlocks, + inCodeBlock } from "discourse/lib/utilities"; QUnit.module("lib:utilities"); @@ -186,3 +188,50 @@ QUnit.test("fillMissingDates", assert => { "it returns a JSON array with 31 dates" ); }); + +QUnit.test("getCodeBlocks - works with [code]", assert => { + assert.deepEqual( + getCodeBlocks("[code]\nfoo\n[/code]\n\nbar\n\n[code]\nbaz"), + [ + [0, 18], + [25, 35] + ] + ); +}); + +QUnit.test("getCodeBlocks - works with backticks", assert => { + assert.deepEqual(getCodeBlocks("foo `bar\nbar`! `baz"), [ + [4, 13], + [15, 19] + ]); +}); + +QUnit.test("getCodeBlocks - works with triple backticks", assert => { + assert.deepEqual(getCodeBlocks("```\nfoo\n```\n\nbar\n\n```\nbaz"), [ + [0, 11], + [18, 25] + ]); +}); + +QUnit.test("inCodeBlock", assert => { + const raw = + "bar\n\n```\nfoo\n```\n\nbar\n\n`foo\nfoo`\n\nbar\n\n[code]\nfoo\n[/code]\n\nbar`foo"; + + assert.notOk(inCodeBlock(raw, 4)); + assert.ok(inCodeBlock(raw, 5)); + assert.ok(inCodeBlock(raw, 16)); + assert.notOk(inCodeBlock(raw, 17)); + + assert.notOk(inCodeBlock(raw, 22)); + assert.ok(inCodeBlock(raw, 23)); + assert.ok(inCodeBlock(raw, 32)); + assert.notOk(inCodeBlock(raw, 33)); + + assert.notOk(inCodeBlock(raw, 38)); + assert.ok(inCodeBlock(raw, 39)); + assert.ok(inCodeBlock(raw, 57)); + assert.notOk(inCodeBlock(raw, 58)); + + assert.notOk(inCodeBlock(raw, 61)); + assert.ok(inCodeBlock(raw, 62)); +});