From 837f916cb8de1e84b1e96242b5b67ff1b29bd3a7 Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Fri, 18 Mar 2022 16:32:39 -0700 Subject: [PATCH 01/11] Adds a test to demonstrate the issue with emoji autocomplete reported in https://github.com/vector-im/element-web/issues/19302. Signed-off-by: Ryan Browne --- test/autocomplete/EmojiProvider-test.js | 64 +++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/autocomplete/EmojiProvider-test.js diff --git a/test/autocomplete/EmojiProvider-test.js b/test/autocomplete/EmojiProvider-test.js new file mode 100644 index 00000000000..319a81961a2 --- /dev/null +++ b/test/autocomplete/EmojiProvider-test.js @@ -0,0 +1,64 @@ +/* +Copyright 2022 Ryan Browne + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import EmojiProvider from '../../src/autocomplete/EmojiProvider'; + +const EMOJI_SHORTNAMES = [ + ':+1', + ':heart', + ':grinning', + ':hand', + ':man', + ':sweat', + ':monkey', + ':boat', + ':mailbox', + ':cop', + ':bow', + ':kiss', + ':golf', +]; + +// Some emoji shortcodes are too short and do not actually trigger autocompletion until the ending `:`. +// This means that we cannot compare their autocompletion before and after the ending `:` and have +// to simply assert that the final completion with the colon is the exact emoji. +const TOO_SHORT_EMOJI_SHORTNAME = [ + { emojiShortcode: ':o', expectedEmoji: '⭕️' }, +]; + +describe('EmojiProvider', function() { + it.each(EMOJI_SHORTNAMES)('Returns consistent results after final colon %s', async function(emojiShortcode) { + const ep = new EmojiProvider('test-room'); + const range = { "beginning": true, "start": 0, "end": 3 }; + const completionsBeforeColon = await ep.getCompletions(emojiShortcode, range); + const completionsAfterColon = await ep.getCompletions(emojiShortcode + ':', range); + + const firstCompletionWithoutColon = completionsBeforeColon[0].completion; + const firstCompletionWithColon = completionsAfterColon[0].completion; + + expect(firstCompletionWithoutColon).toEqual(firstCompletionWithColon); + }); + + it.each( + TOO_SHORT_EMOJI_SHORTNAME, + )("Returns correct results after final colon %s", async ({ emojiShortcode, expectedEmoji }) => { + const ep = new EmojiProvider('test-room'); + const range = { "beginning": true, "start": 0, "end": 3 }; + const completions = await ep.getCompletions(emojiShortcode + ':', range); + + expect(completions[0].completion).toEqual(expectedEmoji); + }); +}); From 02566d66b487ee2d2ff5bd5ad634e9fb06a5861b Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Fri, 18 Mar 2022 16:33:03 -0700 Subject: [PATCH 02/11] Trim trailing `:` when checking for autocompletes for emoji. Closes https://github.com/vector-im/element-web/issues/19302 Signed-off-by: Ryan Browne --- src/autocomplete/EmojiProvider.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 55d58a89536..b5865807fb8 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -3,6 +3,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd Copyright 2017, 2018 New Vector Ltd Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2022 Ryan Browne Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -62,6 +63,19 @@ function score(query, space) { } } +function colonsTrimmed(string: string): string { + // Trim off leading and potentially trailing `:` to correctly + // match the emoji data as they exist in emojibase. + let returned = string; + if (string[0] === ':') { + returned = returned.substring(1); + } + if (returned[returned.length - 1] === ':') { + returned = returned.slice(0, -1); + } + return returned; +} + export default class EmojiProvider extends AutocompleteProvider { matcher: QueryMatcher; nameMatcher: QueryMatcher; @@ -109,7 +123,7 @@ export default class EmojiProvider extends AutocompleteProvider { sorters.push(c => score(matchedString, c.emoji.shortcodes[0])); // then sort by max score of all shortcodes, trim off the `:` sorters.push(c => Math.min( - ...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)), + ...c.emoji.shortcodes.map(s => score(colonsTrimmed(matchedString), s)), )); // If the matchedString is not empty, sort by length of shortcode. Example: // matchedString = ":bookmark" From ac09e71e4c6151e35d21f612c9b329ead2a381f1 Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Fri, 18 Mar 2022 17:03:14 -0700 Subject: [PATCH 03/11] Move all references to the emoji delimiter character to reference a constant. Signed-off-by: Ryan Browne --- src/autocomplete/EmojiProvider.tsx | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index b5865807fb8..00c05fe567f 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -34,9 +34,13 @@ import { TimelineRenderingType } from '../contexts/RoomContext'; const LIMIT = 20; +// The delimiter used to start and end emoji shortcodes. +const EMOJI_DELIMITER = ':'; + // Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase // anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs -const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g'); +const EMOJI_SHORTNAME_REGEX = `(?${EMOJI_DELIMITER}^|\\s)${EMOJI_DELIMITER}[+-\\w]*${EMOJI_DELIMITER}?`; +const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|' + EMOJI_SHORTNAME_REGEX + ')$', 'g'); interface ISortedEmoji { emoji: IEmoji; @@ -63,14 +67,14 @@ function score(query, space) { } } -function colonsTrimmed(string: string): string { +function delimiterTrimmed(string: string): string { // Trim off leading and potentially trailing `:` to correctly // match the emoji data as they exist in emojibase. let returned = string; - if (string[0] === ':') { + if (string[0] === EMOJI_DELIMITER) { returned = returned.substring(1); } - if (returned[returned.length - 1] === ':') { + if (returned[returned.length - 1] === EMOJI_DELIMITER) { returned = returned.slice(0, -1); } return returned; @@ -84,7 +88,7 @@ export default class EmojiProvider extends AutocompleteProvider { super({ commandRegex: EMOJI_REGEX, renderingType }); this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], - funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)], + funcs: [o => o.emoji.shortcodes.map(s => `${EMOJI_DELIMITER}${s}${EMOJI_DELIMITER}`)], // For matching against ascii equivalents shouldMatchWordsOnly: false, }); @@ -121,9 +125,9 @@ export default class EmojiProvider extends AutocompleteProvider { // then sort by score (Infinity if matchedString not in shortcode) sorters.push(c => score(matchedString, c.emoji.shortcodes[0])); - // then sort by max score of all shortcodes, trim off the `:` + // then sort by max score of all shortcodes, trim off the `EMOJI_DELIMITER` sorters.push(c => Math.min( - ...c.emoji.shortcodes.map(s => score(colonsTrimmed(matchedString), s)), + ...c.emoji.shortcodes.map(s => score(delimiterTrimmed(matchedString), s)), )); // If the matchedString is not empty, sort by length of shortcode. Example: // matchedString = ":bookmark" @@ -138,7 +142,7 @@ export default class EmojiProvider extends AutocompleteProvider { completions = completions.map(c => ({ completion: c.emoji.unicode, component: ( - + { c.emoji.unicode } ), From 7cd2a97a6e03fe873e60e5ed03a392d741d862a9 Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Mon, 4 Apr 2022 01:38:00 -0700 Subject: [PATCH 04/11] Revert "Move all references to the emoji delimiter character to reference a constant." This reverts commit ac09e71e4c6151e35d21f612c9b329ead2a381f1. Signed-off-by: Ryan Browne --- src/autocomplete/EmojiProvider.tsx | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 00c05fe567f..b5865807fb8 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -34,13 +34,9 @@ import { TimelineRenderingType } from '../contexts/RoomContext'; const LIMIT = 20; -// The delimiter used to start and end emoji shortcodes. -const EMOJI_DELIMITER = ':'; - // Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase // anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs -const EMOJI_SHORTNAME_REGEX = `(?${EMOJI_DELIMITER}^|\\s)${EMOJI_DELIMITER}[+-\\w]*${EMOJI_DELIMITER}?`; -const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|' + EMOJI_SHORTNAME_REGEX + ')$', 'g'); +const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g'); interface ISortedEmoji { emoji: IEmoji; @@ -67,14 +63,14 @@ function score(query, space) { } } -function delimiterTrimmed(string: string): string { +function colonsTrimmed(string: string): string { // Trim off leading and potentially trailing `:` to correctly // match the emoji data as they exist in emojibase. let returned = string; - if (string[0] === EMOJI_DELIMITER) { + if (string[0] === ':') { returned = returned.substring(1); } - if (returned[returned.length - 1] === EMOJI_DELIMITER) { + if (returned[returned.length - 1] === ':') { returned = returned.slice(0, -1); } return returned; @@ -88,7 +84,7 @@ export default class EmojiProvider extends AutocompleteProvider { super({ commandRegex: EMOJI_REGEX, renderingType }); this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], - funcs: [o => o.emoji.shortcodes.map(s => `${EMOJI_DELIMITER}${s}${EMOJI_DELIMITER}`)], + funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)], // For matching against ascii equivalents shouldMatchWordsOnly: false, }); @@ -125,9 +121,9 @@ export default class EmojiProvider extends AutocompleteProvider { // then sort by score (Infinity if matchedString not in shortcode) sorters.push(c => score(matchedString, c.emoji.shortcodes[0])); - // then sort by max score of all shortcodes, trim off the `EMOJI_DELIMITER` + // then sort by max score of all shortcodes, trim off the `:` sorters.push(c => Math.min( - ...c.emoji.shortcodes.map(s => score(delimiterTrimmed(matchedString), s)), + ...c.emoji.shortcodes.map(s => score(colonsTrimmed(matchedString), s)), )); // If the matchedString is not empty, sort by length of shortcode. Example: // matchedString = ":bookmark" @@ -142,7 +138,7 @@ export default class EmojiProvider extends AutocompleteProvider { completions = completions.map(c => ({ completion: c.emoji.unicode, component: ( - + { c.emoji.unicode } ), From da10ca3da0091a7517cac2dbcb043b9afb43ccc9 Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Mon, 4 Apr 2022 01:40:12 -0700 Subject: [PATCH 05/11] Rename variable. Signed-off-by: Ryan Browne --- src/autocomplete/EmojiProvider.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index b5865807fb8..b83f97d36c1 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -63,11 +63,11 @@ function score(query, space) { } } -function colonsTrimmed(string: string): string { +function colonsTrimmed(str: string): string { // Trim off leading and potentially trailing `:` to correctly // match the emoji data as they exist in emojibase. - let returned = string; - if (string[0] === ':') { + let returned = str; + if (str[0] === ':') { returned = returned.substring(1); } if (returned[returned.length - 1] === ':') { @@ -122,8 +122,9 @@ export default class EmojiProvider extends AutocompleteProvider { // then sort by score (Infinity if matchedString not in shortcode) sorters.push(c => score(matchedString, c.emoji.shortcodes[0])); // then sort by max score of all shortcodes, trim off the `:` + const trimmedMatch = colonsTrimmed(matchedString); sorters.push(c => Math.min( - ...c.emoji.shortcodes.map(s => score(colonsTrimmed(matchedString), s)), + ...c.emoji.shortcodes.map(s => score(trimmedMatch, s)), )); // If the matchedString is not empty, sort by length of shortcode. Example: // matchedString = ":bookmark" From 3f8a4fe2ff765e15bb668da83b8e6818ce246f30 Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Mon, 4 Apr 2022 01:43:07 -0700 Subject: [PATCH 06/11] Make the test file a .js file. Signed-off-by: Ryan Browne --- .../autocomplete/{EmojiProvider-test.js => EmojiProvider-test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/autocomplete/{EmojiProvider-test.js => EmojiProvider-test.ts} (100%) diff --git a/test/autocomplete/EmojiProvider-test.js b/test/autocomplete/EmojiProvider-test.ts similarity index 100% rename from test/autocomplete/EmojiProvider-test.js rename to test/autocomplete/EmojiProvider-test.ts From 997a5bf0acb4677de2f19d3a21b4d0b68cc74025 Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Mon, 4 Apr 2022 01:44:29 -0700 Subject: [PATCH 07/11] Update quotes to match style and make a valid stubbed room. Signed-off-by: Ryan Browne --- test/autocomplete/EmojiProvider-test.ts | 37 +++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/test/autocomplete/EmojiProvider-test.ts b/test/autocomplete/EmojiProvider-test.ts index 319a81961a2..67aad99f17e 100644 --- a/test/autocomplete/EmojiProvider-test.ts +++ b/test/autocomplete/EmojiProvider-test.ts @@ -15,33 +15,36 @@ limitations under the License. */ import EmojiProvider from '../../src/autocomplete/EmojiProvider'; +import { mkStubRoom } from '../test-utils/test-utils'; const EMOJI_SHORTNAMES = [ - ':+1', - ':heart', - ':grinning', - ':hand', - ':man', - ':sweat', - ':monkey', - ':boat', - ':mailbox', - ':cop', - ':bow', - ':kiss', - ':golf', + ":+1", + ":heart", + ":grinning", + ":hand", + ":man", + ":sweat", + ":monkey", + ":boat", + ":mailbox", + ":cop", + ":bow", + ":kiss", + ":golf", ]; // Some emoji shortcodes are too short and do not actually trigger autocompletion until the ending `:`. // This means that we cannot compare their autocompletion before and after the ending `:` and have // to simply assert that the final completion with the colon is the exact emoji. const TOO_SHORT_EMOJI_SHORTNAME = [ - { emojiShortcode: ':o', expectedEmoji: '⭕️' }, + { emojiShortcode: ":o", expectedEmoji: "⭕️" }, ]; describe('EmojiProvider', function() { + const testRoom = mkStubRoom(undefined, undefined, undefined); + it.each(EMOJI_SHORTNAMES)('Returns consistent results after final colon %s', async function(emojiShortcode) { - const ep = new EmojiProvider('test-room'); + const ep = new EmojiProvider(testRoom); const range = { "beginning": true, "start": 0, "end": 3 }; const completionsBeforeColon = await ep.getCompletions(emojiShortcode, range); const completionsAfterColon = await ep.getCompletions(emojiShortcode + ':', range); @@ -54,8 +57,8 @@ describe('EmojiProvider', function() { it.each( TOO_SHORT_EMOJI_SHORTNAME, - )("Returns correct results after final colon %s", async ({ emojiShortcode, expectedEmoji }) => { - const ep = new EmojiProvider('test-room'); + )('Returns correct results after final colon %s', async ({ emojiShortcode, expectedEmoji }) => { + const ep = new EmojiProvider(testRoom); const range = { "beginning": true, "start": 0, "end": 3 }; const completions = await ep.getCompletions(emojiShortcode + ':', range); From 6ebabf17abb0d7b6558b98ad7a1bcd6c8e8d0207 Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Mon, 4 Apr 2022 01:51:16 -0700 Subject: [PATCH 08/11] Fix variable name and test reporting. Signed-off-by: Ryan Browne --- test/autocomplete/EmojiProvider-test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/autocomplete/EmojiProvider-test.ts b/test/autocomplete/EmojiProvider-test.ts index 67aad99f17e..a64312fa297 100644 --- a/test/autocomplete/EmojiProvider-test.ts +++ b/test/autocomplete/EmojiProvider-test.ts @@ -17,7 +17,7 @@ limitations under the License. import EmojiProvider from '../../src/autocomplete/EmojiProvider'; import { mkStubRoom } from '../test-utils/test-utils'; -const EMOJI_SHORTNAMES = [ +const EMOJI_SHORTCODES = [ ":+1", ":heart", ":grinning", @@ -36,14 +36,14 @@ const EMOJI_SHORTNAMES = [ // Some emoji shortcodes are too short and do not actually trigger autocompletion until the ending `:`. // This means that we cannot compare their autocompletion before and after the ending `:` and have // to simply assert that the final completion with the colon is the exact emoji. -const TOO_SHORT_EMOJI_SHORTNAME = [ +const TOO_SHORT_EMOJI_SHORTCODE = [ { emojiShortcode: ":o", expectedEmoji: "⭕️" }, ]; describe('EmojiProvider', function() { const testRoom = mkStubRoom(undefined, undefined, undefined); - it.each(EMOJI_SHORTNAMES)('Returns consistent results after final colon %s', async function(emojiShortcode) { + it.each(EMOJI_SHORTCODES)('Returns consistent results after final colon %s', async function(emojiShortcode) { const ep = new EmojiProvider(testRoom); const range = { "beginning": true, "start": 0, "end": 3 }; const completionsBeforeColon = await ep.getCompletions(emojiShortcode, range); @@ -56,8 +56,8 @@ describe('EmojiProvider', function() { }); it.each( - TOO_SHORT_EMOJI_SHORTNAME, - )('Returns correct results after final colon %s', async ({ emojiShortcode, expectedEmoji }) => { + TOO_SHORT_EMOJI_SHORTCODE, + )('Returns correct results after final colon $emojiShortcode', async ({ emojiShortcode, expectedEmoji }) => { const ep = new EmojiProvider(testRoom); const range = { "beginning": true, "start": 0, "end": 3 }; const completions = await ep.getCompletions(emojiShortcode + ':', range); From 0317419c69326ef067270b724625a1c958216cd6 Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Fri, 8 Apr 2022 22:49:27 -0700 Subject: [PATCH 09/11] Use str.replace with a regex. Signed-off-by: Ryan Browne --- src/autocomplete/EmojiProvider.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index b83f97d36c1..4c051a71269 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -64,16 +64,10 @@ function score(query, space) { } function colonsTrimmed(str: string): string { - // Trim off leading and potentially trailing `:` to correctly - // match the emoji data as they exist in emojibase. - let returned = str; - if (str[0] === ':') { - returned = returned.substring(1); - } - if (returned[returned.length - 1] === ':') { - returned = returned.slice(0, -1); - } - return returned; + // Trim off leading and potentially trailing `:` to correctly match the emoji data as they exist in emojibase. + // Notes: The regex is pinned to the start and end of the string so that we can use the lazy-capturing `*?` matcher. + // It needs to be lazy so that the trailing `:` is not captured in the replacement group, if it exists. + return str.replace(/^:(.*?):?$/, "$1"); } export default class EmojiProvider extends AutocompleteProvider { From 220cb0efb8de247158c11daf9170464a57cc3af2 Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Sat, 9 Apr 2022 03:08:39 -0700 Subject: [PATCH 10/11] Use an improved regex that does not have have to iterate through the entire string, and can just backtrack at most the last 2 characters. Signed-off-by: Ryan Browne --- src/autocomplete/EmojiProvider.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 4c051a71269..f00de7fc895 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -64,10 +64,8 @@ function score(query, space) { } function colonsTrimmed(str: string): string { - // Trim off leading and potentially trailing `:` to correctly match the emoji data as they exist in emojibase. - // Notes: The regex is pinned to the start and end of the string so that we can use the lazy-capturing `*?` matcher. - // It needs to be lazy so that the trailing `:` is not captured in the replacement group, if it exists. - return str.replace(/^:(.*?):?$/, "$1"); + // Trim off leading and potentially trailing `:` to correctly match the emoji shortcode names as they exist in emojibase. + return str.replace(/^:(.*[^:]):?$/, "$1"); } export default class EmojiProvider extends AutocompleteProvider { From 3989a1ff0023d44119d697ec739866435f7e4fe9 Mon Sep 17 00:00:00 2001 From: Ryan Browne Date: Mon, 11 Apr 2022 00:19:59 -0700 Subject: [PATCH 11/11] Revert "Use an improved regex that does not have have to iterate through the entire string, and can just backtrack at most the last 2 characters." This regex is very efficient, but requires a specific form of the emoji shortcode that it is not clear is within our control. This is a restriction that is not required by the technicalities of solving the bug this PR is attempting to fix. (It requires that an emoji shortcode end with a colon.) This reverts commit 220cb0efb8de247158c11daf9170464a57cc3af2. Signed-off-by: Ryan Browne --- src/autocomplete/EmojiProvider.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index f00de7fc895..4c051a71269 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -64,8 +64,10 @@ function score(query, space) { } function colonsTrimmed(str: string): string { - // Trim off leading and potentially trailing `:` to correctly match the emoji shortcode names as they exist in emojibase. - return str.replace(/^:(.*[^:]):?$/, "$1"); + // Trim off leading and potentially trailing `:` to correctly match the emoji data as they exist in emojibase. + // Notes: The regex is pinned to the start and end of the string so that we can use the lazy-capturing `*?` matcher. + // It needs to be lazy so that the trailing `:` is not captured in the replacement group, if it exists. + return str.replace(/^:(.*?):?$/, "$1"); } export default class EmojiProvider extends AutocompleteProvider {