| 1 | #!/usr/bin/env node |
| 2 | // Makes g1t's emoji data from Emojibase (https://emojibase.dev, MIT): |
| 3 | // |
| 4 | // npm pack emojibase-data@17.0.0 && tar xzf emojibase-data-17.0.0.tgz |
| 5 | // node scripts/emoji/generate.mjs ./package |
| 6 | // |
| 7 | // It writes two files, both checked in so nothing downloads at build time: |
| 8 | // |
| 9 | // - apps/web/app/components/emoji/data.json: every emoji the picker shows, |
| 10 | // with its name, shortcodes, keywords, group and skin tones. The picker |
| 11 | // loads it on first open, so it never adds to the site's main bundle. |
| 12 | // - services/chat/src/standard-emoji.ts: the standard shortcodes, which a |
| 13 | // workspace's own emoji may not take. |
| 14 | // |
| 15 | // Shortcodes are the familiar chat set (iamcal's), then GitHub's for any |
| 16 | // that set lacks. |
| 17 | import { readFileSync, writeFileSync } from "node:fs"; |
| 18 | import { dirname, join, resolve } from "node:path"; |
| 19 | import { fileURLToPath } from "node:url"; |
| 20 | |
| 21 | const source = process.argv[2]; |
| 22 | if (!source) { |
| 23 | console.error("Usage: node scripts/emoji/generate.mjs <emojibase-data package folder>"); |
| 24 | process.exit(1); |
| 25 | } |
| 26 | const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); |
| 27 | const read = (path) => JSON.parse(readFileSync(join(source, path), "utf8")); |
| 28 | |
| 29 | const version = read("package.json").version; |
| 30 | const compact = read("en/compact.json"); |
| 31 | const iamcal = read("en/shortcodes/iamcal.json"); |
| 32 | const github = read("en/shortcodes/github.json"); |
| 33 | |
| 34 | /** The groups the picker shows, in order; Emojibase's 2 (skin tone components) is left out. */ |
| 35 | const GROUPS = [ |
| 36 | [0, "Smileys & emotion"], |
| 37 | [1, "People & body"], |
| 38 | [3, "Animals & nature"], |
| 39 | [4, "Food & drink"], |
| 40 | [5, "Travel & places"], |
| 41 | [6, "Activities"], |
| 42 | [7, "Objects"], |
| 43 | [8, "Symbols"], |
| 44 | [9, "Flags"], |
| 45 | ]; |
| 46 | const groupIndex = new Map(GROUPS.map(([id], index) => [id, index])); |
| 47 | const TONES = ["light skin tone", "medium-light skin tone", "medium skin tone", "medium-dark skin tone", "dark skin tone"]; |
| 48 | |
| 49 | // An emoji shown as one by default needs no variation selector after it |
| 50 | // ("👍", not "👍" + U+FE0F): one spelling, so reactions compare equal. |
| 51 | const VS16 = String.fromCodePoint(0xfe0f); |
| 52 | const plain = (unicode) => |
| 53 | unicode.endsWith(VS16) && /^\p{Emoji_Presentation}$/u.test(unicode.slice(0, -1)) ? unicode.slice(0, -1) : unicode; |
| 54 | |
| 55 | const codesOf = (hexcode) => { |
| 56 | const list = []; |
| 57 | for (const set of [iamcal[hexcode], github[hexcode]]) { |
| 58 | for (const code of [set ?? []].flat()) if (!list.includes(code)) list.push(code); |
| 59 | } |
| 60 | return list; |
| 61 | }; |
| 62 | |
| 63 | const entries = compact |
| 64 | .filter((emoji) => groupIndex.has(emoji.group)) |
| 65 | .sort((a, b) => a.order - b.order) |
| 66 | .map((emoji) => { |
| 67 | const entry = [plain(emoji.unicode), emoji.label, codesOf(emoji.hexcode).join(" "), (emoji.tags ?? []).join(" "), groupIndex.get(emoji.group)]; |
| 68 | // Only the five single-tone variants: two-person emoji have mixed ones too. |
| 69 | const skins = TONES.map((tone) => (emoji.skins ?? []).find((skin) => skin.label.endsWith(`: ${tone}`))?.unicode).map((skin) => skin && plain(skin)); |
| 70 | if (skins.every(Boolean)) entry.push(skins); |
| 71 | return entry; |
| 72 | }); |
| 73 | |
| 74 | const data = { source: `emojibase-data ${version} (MIT)`, groups: GROUPS.map(([, label]) => label), emoji: entries }; |
| 75 | writeFileSync(join(root, "apps/web/app/components/emoji/data.json"), JSON.stringify(data)); |
| 76 | |
| 77 | const standard = new Set(); |
| 78 | for (const set of [iamcal, github]) for (const codes of Object.values(set)) for (const code of [codes].flat()) standard.add(code); |
| 79 | writeFileSync( |
| 80 | join(root, "services/chat/src/standard-emoji.ts"), |
| 81 | `/** |
| 82 | * The standard emoji shortcodes, which a workspace's own emoji may not |
| 83 | * take. Made by scripts/emoji/generate.mjs from emojibase-data ${version} |
| 84 | * (MIT); do not edit by hand. |
| 85 | */ |
| 86 | export const STANDARD_SHORTCODES: ReadonlySet<string> = new Set( |
| 87 | ${JSON.stringify([...standard].sort().join(" "))}.split(" "), |
| 88 | ); |
| 89 | `, |
| 90 | ); |
| 91 | console.log(`${entries.length} emoji, ${standard.size} shortcodes, from emojibase-data ${version}`); |