From b8f66e1d33a2ac5f9e87edb9ed7bf72ee56599d0 Mon Sep 17 00:00:00 2001 From: Echo Date: Mon, 4 Aug 2025 19:15:46 +0200 Subject: [PATCH] [Glitch] Provides legacy fallback for browser that don't support regex flag v Port 28b0e5ee787988426da5805374040ec05ffae3cd to glitch-soc Signed-off-by: Claire --- .../glitch/features/emoji/constants.ts | 11 ----- .../flavours/glitch/features/emoji/render.ts | 10 +++-- .../flavours/glitch/features/emoji/utils.ts | 43 ++++++++++++++++--- .../flavours/glitch/polyfills/index.ts | 11 +++++ 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/app/javascript/flavours/glitch/features/emoji/constants.ts b/app/javascript/flavours/glitch/features/emoji/constants.ts index a5ec9e6e2b..09022371b2 100644 --- a/app/javascript/flavours/glitch/features/emoji/constants.ts +++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/emoji/render.ts b/app/javascript/flavours/glitch/features/emoji/render.ts index afc87b88df..d00c88ab4f 100644 --- a/app/javascript/flavours/glitch/features/emoji/render.ts +++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/emoji/utils.ts b/app/javascript/flavours/glitch/features/emoji/utils.ts index 89f8d92646..9cb177e4ad 100644 --- a/app/javascript/flavours/glitch/features/emoji/utils.ts +++ b/app/javascript/flavours/glitch/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 '@/flavours/glitch/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/flavours/glitch/polyfills/index.ts b/app/javascript/flavours/glitch/polyfills/index.ts index c001421c36..0ff0dd7269 100644 --- a/app/javascript/flavours/glitch/polyfills/index.ts +++ b/app/javascript/flavours/glitch/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;