diff --git a/app/javascript/mastodon/features/emoji/constants.ts b/app/javascript/mastodon/features/emoji/constants.ts index a5ec9e6e2b4..09022371b22 100644 --- a/app/javascript/mastodon/features/emoji/constants.ts +++ b/app/javascript/mastodon/features/emoji/constants.ts @@ -15,17 +15,6 @@ export const SKIN_TONE_CODES = [ 0x1f3ff, // Dark skin tone ] as const; -// TODO: Test and create fallback for browsers that do not handle the /v flag. -export const UNICODE_EMOJI_REGEX = /\p{RGI_Emoji}/v; -// See: https://www.unicode.org/reports/tr51/#valid-emoji-tag-sequences -export const UNICODE_FLAG_EMOJI_REGEX = - /\p{RGI_Emoji_Flag_Sequence}|\p{RGI_Emoji_Tag_Sequence}/v; -export const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i; -export const ANY_EMOJI_REGEX = new RegExp( - `(${UNICODE_EMOJI_REGEX.source}|${CUSTOM_EMOJI_REGEX.source})`, - 'gv', -); - // Emoji rendering modes. A mode is what we are using to render emojis, a style is what the user has selected. export const EMOJI_MODE_NATIVE = 'native'; export const EMOJI_MODE_NATIVE_WITH_FLAGS = 'native-flags'; diff --git a/app/javascript/mastodon/features/emoji/render.ts b/app/javascript/mastodon/features/emoji/render.ts index 6486e65a709..8d2299fd89e 100644 --- a/app/javascript/mastodon/features/emoji/render.ts +++ b/app/javascript/mastodon/features/emoji/render.ts @@ -9,7 +9,6 @@ import { EMOJI_TYPE_UNICODE, EMOJI_TYPE_CUSTOM, EMOJI_STATE_MISSING, - ANY_EMOJI_REGEX, } from './constants'; import { searchCustomEmojisByShortcodes, @@ -32,7 +31,12 @@ import type { LocaleOrCustom, UnicodeEmojiToken, } from './types'; -import { emojiLogger, stringHasAnyEmoji, stringHasUnicodeFlags } from './utils'; +import { + anyEmojiRegex, + emojiLogger, + stringHasAnyEmoji, + stringHasUnicodeFlags, +} from './utils'; const log = emojiLogger('render'); @@ -207,7 +211,7 @@ export function tokenizeText(text: string): TokenizedText { const tokens = []; let lastIndex = 0; - for (const match of text.matchAll(ANY_EMOJI_REGEX)) { + for (const match of text.matchAll(anyEmojiRegex())) { if (match.index > lastIndex) { tokens.push(text.slice(lastIndex, match.index)); } diff --git a/app/javascript/mastodon/features/emoji/utils.ts b/app/javascript/mastodon/features/emoji/utils.ts index 89f8d926466..ce359199296 100644 --- a/app/javascript/mastodon/features/emoji/utils.ts +++ b/app/javascript/mastodon/features/emoji/utils.ts @@ -1,23 +1,32 @@ import debug from 'debug'; -import { - CUSTOM_EMOJI_REGEX, - UNICODE_EMOJI_REGEX, - UNICODE_FLAG_EMOJI_REGEX, -} from './constants'; +import { emojiRegexPolyfill } from '@/mastodon/polyfills'; export function emojiLogger(segment: string) { return debug(`emojis:${segment}`); } export function stringHasUnicodeEmoji(input: string): boolean { - return UNICODE_EMOJI_REGEX.test(input); + return new RegExp(EMOJI_REGEX, supportedFlags()).test(input); } export function stringHasUnicodeFlags(input: string): boolean { - return UNICODE_FLAG_EMOJI_REGEX.test(input); + if (supportsRegExpSets()) { + return new RegExp( + '\\p{RGI_Emoji_Flag_Sequence}|\\p{RGI_Emoji_Tag_Sequence}', + 'v', + ).test(input); + } + return new RegExp( + // First range is regional indicator symbols, + // Second is a black flag + 0-9|a-z tag chars + cancel tag. + // See: https://en.wikipedia.org/wiki/Regional_indicator_symbol + '(?:\uD83C[\uDDE6-\uDDFF]){2}|\uD83C\uDFF4(?:\uDB40[\uDC30-\uDC7A])+\uDB40\uDC7F', + ).test(input); } +// Constant as this is supported by all browsers. +const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i; export function stringHasCustomEmoji(input: string) { return CUSTOM_EMOJI_REGEX.test(input); } @@ -25,3 +34,23 @@ export function stringHasCustomEmoji(input: string) { export function stringHasAnyEmoji(input: string) { return stringHasUnicodeEmoji(input) || stringHasCustomEmoji(input); } + +export function anyEmojiRegex() { + return new RegExp( + `${EMOJI_REGEX}|${CUSTOM_EMOJI_REGEX.source}`, + supportedFlags('gi'), + ); +} + +function supportsRegExpSets() { + return 'unicodeSets' in RegExp.prototype; +} + +function supportedFlags(flags = '') { + if (supportsRegExpSets()) { + return `${flags}v`; + } + return flags; +} + +const EMOJI_REGEX = emojiRegexPolyfill?.source ?? '\\p{RGI_Emoji}'; diff --git a/app/javascript/mastodon/polyfills/index.ts b/app/javascript/mastodon/polyfills/index.ts index c001421c363..0ff0dd72690 100644 --- a/app/javascript/mastodon/polyfills/index.ts +++ b/app/javascript/mastodon/polyfills/index.ts @@ -20,5 +20,16 @@ export function loadPolyfills() { loadIntlPolyfills(), // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types needsExtraPolyfills && importExtraPolyfills(), + loadEmojiPolyfills(), ]); } + +// In the case of no /v support, rely on the emojibase data. +async function loadEmojiPolyfills() { + if (!('unicodeSets' in RegExp.prototype)) { + emojiRegexPolyfill = (await import('emojibase-regex/emoji')).default; + } +} + +// Null unless polyfill is needed. +export let emojiRegexPolyfill: RegExp | null = null; diff --git a/package.json b/package.json index ec7b918ef0a..6109b42875b 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "emoji-mart": "npm:emoji-mart-lazyload@latest", "emojibase": "^16.0.0", "emojibase-data": "^16.0.3", + "emojibase-regex": "^16.0.0", "escape-html": "^1.0.3", "fast-glob": "^3.3.3", "fuzzysort": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index a8baa769411..26f7cd409e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2659,6 +2659,7 @@ __metadata: emoji-mart: "npm:emoji-mart-lazyload@latest" emojibase: "npm:^16.0.0" emojibase-data: "npm:^16.0.3" + emojibase-regex: "npm:^16.0.0" escape-html: "npm:^1.0.3" eslint: "npm:^9.23.0" eslint-import-resolver-typescript: "npm:^4.2.5" @@ -6565,6 +6566,13 @@ __metadata: languageName: node linkType: hard +"emojibase-regex@npm:^16.0.0": + version: 16.0.0 + resolution: "emojibase-regex@npm:16.0.0" + checksum: 10c0/8ee5ff798e51caa581434b1cb2f9737e50195093c4efa1739df21a50a5496f80517924787d865e8cf7d6a0b4c90dbedc04bdc506dcbcc582e14cdf0bb47af0f0 + languageName: node + linkType: hard + "emojibase@npm:^16.0.0": version: 16.0.0 resolution: "emojibase@npm:16.0.0"