Provides legacy fallback for browser that don't support regex flag v (#35659)

This commit is contained in:
Echo 2025-08-04 19:15:46 +02:00 committed by GitHub
parent cb0b608fa7
commit 28b0e5ee78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 63 additions and 21 deletions

View File

@ -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';

View File

@ -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));
}

View File

@ -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}';

View File

@ -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;

View File

@ -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",

View File

@ -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"