You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
186 lines
4.6 KiB
186 lines
4.6 KiB
import { |
|
EMOJI_MODE_NATIVE, |
|
EMOJI_MODE_NATIVE_WITH_FLAGS, |
|
EMOJI_TYPE_UNICODE, |
|
EMOJI_TYPE_CUSTOM, |
|
} from './constants'; |
|
import { |
|
loadCustomEmojiByShortcode, |
|
loadEmojiByHexcode, |
|
LocaleNotLoadedError, |
|
} from './database'; |
|
import { importEmojiData } from './loader'; |
|
import { emojiToUnicodeHex } from './normalize'; |
|
import type { |
|
EmojiLoadedState, |
|
EmojiMode, |
|
EmojiState, |
|
EmojiStateCustom, |
|
EmojiStateUnicode, |
|
ExtraCustomEmojiMap, |
|
} from './types'; |
|
import { |
|
anyEmojiRegex, |
|
emojiLogger, |
|
isCustomEmoji, |
|
isUnicodeEmoji, |
|
stringHasUnicodeFlags, |
|
} from './utils'; |
|
|
|
const log = emojiLogger('render'); |
|
|
|
type TokenizedText = (string | EmojiState)[]; |
|
|
|
/** |
|
* Tokenizes text into strings and emoji states. |
|
* @param text Text to tokenize. |
|
* @returns Array of strings and emoji states. |
|
*/ |
|
export function tokenizeText(text: string): TokenizedText { |
|
if (!text.trim()) { |
|
return [text]; |
|
} |
|
|
|
const tokens = []; |
|
let lastIndex = 0; |
|
for (const match of text.matchAll(anyEmojiRegex())) { |
|
if (match.index > lastIndex) { |
|
tokens.push(text.slice(lastIndex, match.index)); |
|
} |
|
|
|
const code = match[0]; |
|
|
|
if (code.startsWith(':') && code.endsWith(':')) { |
|
// Custom emoji |
|
tokens.push({ |
|
type: EMOJI_TYPE_CUSTOM, |
|
code, |
|
} satisfies EmojiStateCustom); |
|
} else { |
|
// Unicode emoji |
|
tokens.push({ |
|
type: EMOJI_TYPE_UNICODE, |
|
code: code, |
|
} satisfies EmojiStateUnicode); |
|
} |
|
lastIndex = match.index + code.length; |
|
} |
|
if (lastIndex < text.length) { |
|
tokens.push(text.slice(lastIndex)); |
|
} |
|
return tokens; |
|
} |
|
|
|
/** |
|
* Parses emoji string to extract emoji state. |
|
* @param code Hex code or custom shortcode. |
|
* @param customEmoji Extra custom emojis. |
|
*/ |
|
export function stringToEmojiState( |
|
code: string, |
|
customEmoji: ExtraCustomEmojiMap = {}, |
|
): EmojiState | null { |
|
if (isUnicodeEmoji(code)) { |
|
return { |
|
type: EMOJI_TYPE_UNICODE, |
|
code: emojiToUnicodeHex(code), |
|
}; |
|
} |
|
|
|
if (isCustomEmoji(code)) { |
|
const shortCode = code.slice(1, -1); |
|
return { |
|
type: EMOJI_TYPE_CUSTOM, |
|
code: shortCode, |
|
data: customEmoji[shortCode], |
|
}; |
|
} |
|
|
|
return null; |
|
} |
|
|
|
/** |
|
* Loads emoji data into the given state if not already loaded. |
|
* @param state Emoji state to load data for. |
|
* @param locale Locale to load data for. Only for Unicode emoji. |
|
* @param retry Internal. Whether this is a retry after loading the locale. |
|
*/ |
|
export async function loadEmojiDataToState( |
|
state: EmojiState, |
|
locale: string, |
|
retry = false, |
|
): Promise<EmojiLoadedState | null> { |
|
if (isStateLoaded(state)) { |
|
return state; |
|
} |
|
|
|
// First, try to load the data from IndexedDB. |
|
try { |
|
// This is duplicative, but that's because TS can't distinguish the state type easily. |
|
if (state.type === EMOJI_TYPE_UNICODE) { |
|
const data = await loadEmojiByHexcode(state.code, locale); |
|
if (data) { |
|
return { |
|
...state, |
|
data, |
|
}; |
|
} |
|
} else { |
|
const data = await loadCustomEmojiByShortcode(state.code); |
|
if (data) { |
|
return { |
|
...state, |
|
data, |
|
}; |
|
} |
|
} |
|
// If not found, assume it's not an emoji and return null. |
|
log( |
|
'Could not find emoji %s of type %s for locale %s', |
|
state.code, |
|
state.type, |
|
locale, |
|
); |
|
return null; |
|
} catch (err: unknown) { |
|
// If the locale is not loaded, load it and retry once. |
|
if (!retry && err instanceof LocaleNotLoadedError) { |
|
log( |
|
'Error loading emoji %s for locale %s, loading locale and retrying.', |
|
state.code, |
|
locale, |
|
); |
|
await importEmojiData(locale); // Use this from the loader file as it can be awaited. |
|
return loadEmojiDataToState(state, locale, true); |
|
} |
|
|
|
console.warn('Error loading emoji data, not retrying:', state, locale, err); |
|
return null; |
|
} |
|
} |
|
|
|
export function isStateLoaded(state: EmojiState): state is EmojiLoadedState { |
|
return !!state.data; |
|
} |
|
|
|
/** |
|
* Determines if the given token should be rendered as an image based on the emoji mode. |
|
* @param state Emoji state to parse. |
|
* @param mode Rendering mode. |
|
* @returns Whether to render as an image. |
|
*/ |
|
export function shouldRenderImage(state: EmojiState, mode: EmojiMode): boolean { |
|
if (state.type === EMOJI_TYPE_UNICODE) { |
|
// If the mode is native or native with flags for non-flag emoji |
|
// we can just append the text node directly. |
|
if ( |
|
mode === EMOJI_MODE_NATIVE || |
|
(mode === EMOJI_MODE_NATIVE_WITH_FLAGS && |
|
!stringHasUnicodeFlags(state.code)) |
|
) { |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
}
|
|
|