orca/src/shared/emoji-shortcode-catalog.ts

70 lines
2.4 KiB
TypeScript

import emojiShortcodes from 'emojibase-data/en/shortcodes/emojibase.json'
export type StandardEmojiShortcodeEntry = {
emoji: string
shortcode: string
}
// Skin-tone aliases (`wave_tone3`) are ~40% of the dataset and would drown the suggestion list.
const SKIN_TONE_SHORTCODE = /_tone\d(?:-\d)?$/
const CATALOG = Object.entries(emojiShortcodes).flatMap(([hexcode, value]) => {
const shortcodes = (typeof value === 'string' ? [value] : value).filter(
(shortcode) => !SKIN_TONE_SHORTCODE.test(shortcode)
)
return shortcodes.length > 0 ? [{ emoji: hexcodeToEmoji(hexcode), shortcodes }] : []
})
export const STANDARD_EMOJI_SHORTCODE_ENTRIES: readonly StandardEmojiShortcodeEntry[] =
CATALOG.flatMap(({ emoji, shortcodes }) => shortcodes.map((shortcode) => ({ emoji, shortcode })))
const PRIMARY_SHORTCODE_BY_EMOJI = new Map(
CATALOG.map(({ emoji, shortcodes }) => [
normalizeEmojiLookup(emoji),
primaryShortcode(shortcodes)
])
)
/**
* Pick the alias that reads best as a branch or directory name: skip `+1`/`-1` so the name
* starts with a letter, then cryptic stubs (👎 `no`, ✌ `v`) and the `flag_xx` namespacing
* prefix, both of which have a spelled-out alias (`thumbsdown`, `victory`, `germany`).
*/
function primaryShortcode(shortcodes: readonly string[]): string {
const named = shortcodes.filter((candidate) => /^[a-z]/i.test(candidate))
return (
named.find((candidate) => candidate.length >= 3 && !candidate.startsWith('flag_')) ??
named.find((candidate) => candidate.length >= 3) ??
named[0] ??
shortcodes[0]
)
}
const EMOJI_SEGMENTER = new Intl.Segmenter('en', { granularity: 'grapheme' })
export function replaceKnownEmojiWithShortcodes(input: string): string {
return Array.from(EMOJI_SEGMENTER.segment(input), ({ segment }) => {
const shortcode = PRIMARY_SHORTCODE_BY_EMOJI.get(normalizeEmojiLookup(segment))
return shortcode ? ` ${shortcode.replaceAll('_', '-')} ` : segment
}).join('')
}
function normalizeEmojiLookup(emoji: string): string {
return Array.from(emoji)
.filter((character) => {
const codepoint = character.codePointAt(0)
return (
character !== '\ufe0f' &&
(codepoint === undefined || codepoint < 0x1f3fb || codepoint > 0x1f3ff)
)
})
.join('')
}
function hexcodeToEmoji(hexcode: string): string {
return hexcode
.split('-')
.map((codepoint) => String.fromCodePoint(Number.parseInt(codepoint, 16)))
.join('')
}