import debug from 'debug'; import { emojiRegexPolyfill } from '@/mastodon/polyfills'; import { VARIATION_SELECTOR_CODE } from './constants'; export function emojiLogger(segment: string) { if (typeof window === 'undefined') { return debug(`emojis:worker:${segment}`); } return debug(`emojis:${segment}`); } export function isUnicodeEmoji(input: string): boolean { return ( input.length > 0 && new RegExp(`^(${EMOJI_REGEX})+$`, supportedFlags()).test(input) ); } export function stringHasUnicodeFlags(input: string): boolean { 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; // Use the polyfill regex or the Unicode property escapes if supported. const EMOJI_REGEX = emojiRegexPolyfill?.source ?? '\\p{RGI_Emoji}'; export function isCustomEmoji(input: string): boolean { return new RegExp(`^${CUSTOM_EMOJI_REGEX.source}$`, 'i').test(input); } export function anyEmojiRegex() { return new RegExp( `${EMOJI_REGEX}|${CUSTOM_EMOJI_REGEX.source}`, supportedFlags('gi'), ); } export function emojiToUnicodeHex(emoji: string): string { const codes: string[] = []; for (const char of emoji) { const code = char.codePointAt(0); if (code !== undefined) { codes.push(code.toString(16).toUpperCase().padStart(4, '0')); } } // Handles how Emojibase removes the variation selector for single code emojis. // See: https://emojibase.dev/docs/spec/#merged-variation-selectors if ( codes.at(1) === VARIATION_SELECTOR_CODE.toString(16).toUpperCase() && codes.length === 2 ) { codes.pop(); } return codes.join('-'); } const CHARS_ALLOWED_AROUND_EMOJI = // eslint-disable-next-line no-control-regex /[>< …\u0009-\u000d\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/; // TODO: Move to picker file when that's being built out. export function insertEmojiAtPosition( text: string, emoji: string, position = text.length, ): string { const isShortcode = isCustomEmoji(emoji); const needsSpace = isShortcode && position > 0 && !CHARS_ALLOWED_AROUND_EMOJI.test(text[position - 1] ?? ''); return `${text.slice(0, position)}${needsSpace ? ' ' : ''}${emoji} ${text.slice(position)}`; } function supportsRegExpSets() { return 'unicodeSets' in RegExp.prototype; } function supportedFlags(flags = '') { if (supportsRegExpSets()) { return `${flags}v`; } return flags; }