/* Generates `src/assets/emoji.generated.js` — the grouping the emoji picker is built from. Two datasets have to meet for a picker to work here: - `markdown-it-emoji` is the renderer's own vocabulary: `:shortcode:` is what gets written into a page, and anything outside that map would go in as text that never becomes an emoji. It has no notion of categories. - `unicode-emoji-json` carries the CLDR grouping and ordering — Smileys & Emotion, Animals & Nature, and so on — which is what the picker's tabs are, and no notion of shortcodes. So this writes out the intersection: each group, in Unicode's own order, as `[shortcode, character]` pairs — the shortcode is what gets written into the page, the character is what the picker draws in its grid. The picker cannot offer an emoji the renderer would not draw, because every pair came from the renderer's own map. The characters are written here rather than looked up at runtime, even though the editor already bundles that map for rendering. Reading it would mean importing `markdown-it-emoji/lib/data/full.mjs` from app code — a path into the package's internals rather than an entry point it publishes, and one more dependency for the bundler to discover. A few kB of duplicated characters is the cheaper side of that trade. `unicode-emoji-json` is a devDependency for that reason: its data ends up here, and here is committed. Usage: node scripts/generate-emoji.mjs [--check] */ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import emojiByGroup from 'unicode-emoji-json/data-by-group.json' with { type: 'json' } import shortcodeToEmoji from 'markdown-it-emoji/lib/data/full.mjs' const ROOT = fileURLToPath(new URL('../', import.meta.url)) const OUT = path.join(ROOT, 'src/assets/emoji.generated.js') /** * One shortcode per emoji, since several can point at the same character — `laughing` and `satisfied` * are both 😆, `+1` and `thumbsup` are both 👍. * * The first one wins, except that a name starting with a letter beats one that does not: the map's own * order is close to canonical, and `thumbsup` reads better in a page's source than `+1`. */ function buildShortcodeIndex() { const index = new Map() for (const [shortcode, emoji] of Object.entries(shortcodeToEmoji)) { const current = index.get(emoji) if (!current || (!/^[a-z]/.test(current) && /^[a-z]/.test(shortcode))) { index.set(emoji, shortcode) } } return index } function build() { const index = buildShortcodeIndex() const groups = [] const claimed = new Set() for (const group of emojiByGroup) { const emoji = [] for (const entry of group.emojis) { const shortcode = index.get(entry.emoji) // -> Skipped rather than invented: an emoji the renderer has no shortcode for cannot be written // into a page as an emoji, so it has no business in a picker if (shortcode && !claimed.has(shortcode)) { claimed.add(shortcode) emoji.push([shortcode, entry.emoji]) } } if (emoji.length > 0) { groups.push({ slug: group.slug, name: group.name, emoji }) } } return { groups, claimed } } function serialize({ groups, claimed }) { const body = groups .map((group) => { const pairs = group.emoji.map(([code, char]) => `['${code}', '${char}']`).join(', ') // -> Single quotes, so the generated file matches what oxfmt would write for the rest of `src` return ` { slug: '${group.slug}', name: '${group.name}', emoji: [${pairs}] }` }) .join(',\n') return `/* GENERATED by scripts/generate-emoji.mjs — do not edit. The CLDR emoji groups, in Unicode's order, as \`[shortcode, character]\` pairs. The shortcode is one \`markdown-it-emoji\` accepts — what a page stores and what the renderer draws — and the character is what a picker shows. Regenerate with \`npm run emoji\`. ${claimed.size} emoji in ${groups.length} groups. */ export const EMOJI_GROUPS = [ ${body} ] ` } const data = build() const output = serialize(data) if (process.argv.includes('--check')) { const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8') : '' if (current !== output) { console.error('emoji.generated.js is out of date — run `npm run emoji`') process.exit(1) } console.log(`OK ${data.claimed.size} emoji, data up to date`) } else { fs.mkdirSync(path.dirname(OUT), { recursive: true }) fs.writeFileSync(OUT, output) console.log( `wrote ${data.claimed.size} emoji in ${data.groups.length} groups to src/assets/emoji.generated.js (${Buffer.byteLength(output).toLocaleString()} B)` ) }