Skip to content
91 linesCodeBlameRaw
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.
17import { readFileSync, writeFileSync } from "node:fs";
18import { dirname, join, resolve } from "node:path";
19import { fileURLToPath } from "node:url";
20
21const source = process.argv[2];
22if (!source) {
23 console.error("Usage: node scripts/emoji/generate.mjs <emojibase-data package folder>");
24 process.exit(1);
25}
26const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
27const read = (path) => JSON.parse(readFileSync(join(source, path), "utf8"));
28
29const version = read("package.json").version;
30const compact = read("en/compact.json");
31const iamcal = read("en/shortcodes/iamcal.json");
32const github = read("en/shortcodes/github.json");
33
34/** The groups the picker shows, in order; Emojibase's 2 (skin tone components) is left out. */
35const 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];
46const groupIndex = new Map(GROUPS.map(([id], index) => [id, index]));
47const 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.
51const VS16 = String.fromCodePoint(0xfe0f);
52const plain = (unicode) =>
53 unicode.endsWith(VS16) && /^\p{Emoji_Presentation}$/u.test(unicode.slice(0, -1)) ? unicode.slice(0, -1) : unicode;
54
55const 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
63const 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
74const data = { source: `emojibase-data ${version} (MIT)`, groups: GROUPS.map(([, label]) => label), emoji: entries };
75writeFileSync(join(root, "apps/web/app/components/emoji/data.json"), JSON.stringify(data));
76
77const standard = new Set();
78for (const set of [iamcal, github]) for (const codes of Object.values(set)) for (const code of [codes].flat()) standard.add(code);
79writeFileSync(
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 */
86export const STANDARD_SHORTCODES: ReadonlySet<string> = new Set(
87 ${JSON.stringify([...standard].sort().join(" "))}.split(" "),
88);
89`,
90);
91console.log(`${entries.length} emoji, ${standard.size} shortcodes, from emojibase-data ${version}`);