/** * Bake the vendored emojibase snapshot into a compact, statically-imported * TS module for `$libs/emoji`. Mirrors the lucide-icon precedent * (`scripts/generate-icon-categories.js`): a third-party dataset lives raw in * `static/` and a generator prunes it into a baked module — no runtime dep. * * Source : static/emoji/compact.json + messages.json (emojibase-data@16, MIT). * Output : src/libs/emoji/data.ts (DO NOT EDIT — regenerate via this script). * Run : npm run generate:emoji-data * * Pruning: keep pickable emoji only (drop the `component` group — skin-tone / * hair modifiers are not pickable glyphs). Per entry we keep the glyph, the * CLDR name, the group index, a space-joined keyword string (search), and — * for single-person tone-capable emoji — the five Fitzpatrick tone glyphs. */ import { readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, '..'); interface CompactSkin { hexcode: string; unicode: string; label: string; } interface CompactEmoji { hexcode: string; label: string; unicode: string; group?: number; order?: number; tags?: string[]; skins?: CompactSkin[]; } interface Messages { groups: { order: number; key: string; message: string }[]; skinTones: { key: string; message: string }[]; } const COMPONENT_GROUP = 2; // skin-tone + hair modifiers — never pickable. // Fitzpatrick modifiers in light→dark order (the tone-selector order). const TONE_MODIFIERS = [ { slot: 1, key: 'light', mod: '1F3FB' }, { slot: 2, key: 'medium-light', mod: '1F3FC' }, { slot: 3, key: 'medium', mod: '1F3FD' }, { slot: 4, key: 'medium-dark', mod: '1F3FE' }, { slot: 5, key: 'dark', mod: '1F3FF' } ] as const; const stripVS = (hex: string) => hex.replace(/-FE0F/g, ''); const isMod = (codepoint: string) => /^1F3F[B-F]$/.test(codepoint); const compact: CompactEmoji[] = JSON.parse( readFileSync(resolve(root, 'static/emoji/compact.json'), 'utf8') ); const messages: Messages = JSON.parse( readFileSync(resolve(root, 'static/emoji/messages.json'), 'utf8') ); // Groups (excl. component), in canonical order. const groups = messages.groups .filter((g) => g.order !== COMPONENT_GROUP) .sort((a, b) => a.order - b.order) .map((g) => ({ index: g.order, key: g.key, label: g.message })); // Skin tones in light→dark order, labels from messages. const toneLabel = (key: string) => messages.skinTones.find((t) => t.key === key)?.message ?? key; const skinTones = TONE_MODIFIERS.map((t) => ({ slot: t.slot, key: t.key, label: toneLabel(t.key) })); /** * Five uniform-tone glyphs for a single-person tone-capable emoji, else null. * A variant qualifies when it carries EXACTLY ONE Fitzpatrick modifier and * removing it yields the base sequence — this matches both suffix tones * (`👋` → `👋🏻`) and infix ZWJ tones (`man: beard` → tone before the ZWJ), * while rejecting multi-person combos (couples/kiss/holding-hands) whose skins * carry two modifiers. Those 7 are deferred to a v2 refinement. */ function toneGlyphs(e: CompactEmoji): string[] | null { if (!e.skins || e.skins.length === 0) return null; const base = stripVS(e.hexcode); const out: string[] = []; for (const { mod } of TONE_MODIFIERS) { const skin = e.skins.find((s) => { const mods = s.hexcode.split('-').filter(isMod); return ( mods.length === 1 && mods[0] === mod && stripVS(s.hexcode.replace(`-${mod}`, '')) === base ); }); if (!skin) return null; out.push(skin.unicode); } return out; } interface Entry { u: string; n: string; g: number; k?: string; s?: string[]; } const entries: Entry[] = compact .filter((e) => e.group !== undefined && e.group !== COMPONENT_GROUP) .sort((a, b) => a.group! - b.group! || (a.order ?? 0) - (b.order ?? 0)) .map((e) => { const entry: Entry = { u: e.unicode, n: e.label, g: e.group! }; const kw = (e.tags ?? []).filter((t) => t && t !== e.label).join(' '); if (kw) entry.k = kw; const tones = toneGlyphs(e); if (tones) entry.s = tones; return entry; }); const toneCount = entries.filter((e) => e.s).length; // Emit — one entry per line, compact object literals, minimal quoting. const q = (s: string) => JSON.stringify(s); const line = (e: Entry) => { const parts = [`u:${q(e.u)}`, `n:${q(e.n)}`, `g:${e.g}`]; if (e.k) parts.push(`k:${q(e.k)}`); if (e.s) parts.push(`s:[${e.s.map(q).join(',')}]`); return `\t{${parts.join(',')}}`; }; const body = `// AUTO-GENERATED by scripts/generate-emoji-data.ts — DO NOT EDIT. // Source: emojibase-data@16 (MIT), pruned from static/emoji/{compact,messages}.json. // Regenerate: npm run generate:emoji-data import type { EmojiEntry, EmojiGroup, SkinTone } from './types' export const EMOJI_GROUPS: readonly EmojiGroup[] = ${JSON.stringify(groups)} export const SKIN_TONES: readonly SkinTone[] = ${JSON.stringify(skinTones)} export const EMOJI: readonly EmojiEntry[] = [ ${entries.map(line).join(',\n')} ] `; writeFileSync(resolve(root, 'src/libs/emoji/data.ts'), body, 'utf8'); console.log( `emoji-data: ${entries.length} pickable · ${toneCount} tone-capable · ${groups.length} groups → src/libs/emoji/data.ts` );