mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-19 07:36:52 +00:00
12ba0d0080
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Alex Grozav <alex@grozav.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
248 lines
7.3 KiB
JavaScript
248 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Generates emojiData.ts from emojibase-data compact English dataset.
|
||
*
|
||
* Usage: node scripts/generate-emoji-data.mjs
|
||
*
|
||
* Output: packages/frontend/@n8n/design-system/src/components/N8nIconPicker/emojiData.ts
|
||
*/
|
||
|
||
import { readFileSync, writeFileSync } from 'node:fs';
|
||
import { resolve, dirname } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { createRequire } from 'node:module';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = resolve(__dirname, '..');
|
||
const require = createRequire(import.meta.url);
|
||
|
||
const OUTPUT_PATH = resolve(
|
||
ROOT,
|
||
'packages/frontend/@n8n/design-system/src/components/N8nIconPicker/emojiData.ts',
|
||
);
|
||
|
||
// Emojibase group IDs to section keys and i18n label keys
|
||
const GROUP_MAP = {
|
||
0: { key: 'people', labelKey: 'iconPicker.emojiSection.people' },
|
||
1: { key: 'people', labelKey: 'iconPicker.emojiSection.people' }, // People & Body merged into People
|
||
3: { key: 'animalsNature', labelKey: 'iconPicker.emojiSection.animalsNature' },
|
||
4: { key: 'foodDrink', labelKey: 'iconPicker.emojiSection.foodDrink' },
|
||
5: { key: 'travelPlaces', labelKey: 'iconPicker.emojiSection.travelPlaces' },
|
||
6: { key: 'activity', labelKey: 'iconPicker.emojiSection.activity' },
|
||
7: { key: 'objects', labelKey: 'iconPicker.emojiSection.objects' },
|
||
8: { key: 'symbols', labelKey: 'iconPicker.emojiSection.symbols' },
|
||
9: { key: 'flags', labelKey: 'iconPicker.emojiSection.flags' },
|
||
};
|
||
|
||
// Ordered section keys for output
|
||
const SECTION_ORDER = [
|
||
'people',
|
||
'animalsNature',
|
||
'foodDrink',
|
||
'activity',
|
||
'travelPlaces',
|
||
'objects',
|
||
'symbols',
|
||
'flags',
|
||
];
|
||
|
||
function buildKeywords(emoji) {
|
||
const words = new Set();
|
||
// Add label words
|
||
if (emoji.label) {
|
||
emoji.label
|
||
.toLowerCase()
|
||
.split(/[\s\-:,]+/)
|
||
.filter((w) => w.length > 0)
|
||
.forEach((w) => words.add(w));
|
||
}
|
||
// Add tags
|
||
if (emoji.tags) {
|
||
emoji.tags.forEach((t) => words.add(t.toLowerCase()));
|
||
}
|
||
return [...words];
|
||
}
|
||
|
||
/** Title-case an emoji label: "grinning face" → "Grinning Face" */
|
||
function titleCase(str) {
|
||
return str.replace(/\b\w/g, (c) => c.toUpperCase());
|
||
}
|
||
|
||
/**
|
||
* Whether a skin tone represents a uniform variation.
|
||
*
|
||
* Single-person emojis use a scalar tone (1–5). Multi-person emojis use one tone
|
||
* per person: emojibase collapses uniform combinations to a scalar but could also
|
||
* represent them as an array whose entries are all equal (e.g. [3, 3]). Mixed
|
||
* combinations such as [1, 2] are not uniform.
|
||
*/
|
||
function isUniformTone(tone) {
|
||
if (typeof tone === 'number') {
|
||
return true;
|
||
}
|
||
if (Array.isArray(tone) && tone.length > 0) {
|
||
return tone.every((value) => value === tone[0]);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** Build a hexcode → tone lookup from the full dataset's skin variations. */
|
||
function buildToneByHexcode(fullData) {
|
||
const toneByHexcode = new Map();
|
||
for (const emoji of fullData) {
|
||
if (Array.isArray(emoji.skins)) {
|
||
for (const skin of emoji.skins) {
|
||
toneByHexcode.set(skin.hexcode, skin.tone);
|
||
}
|
||
}
|
||
}
|
||
return toneByHexcode;
|
||
}
|
||
|
||
function extractSkinTones(emoji, toneByHexcode) {
|
||
if (!emoji.skins || !Array.isArray(emoji.skins) || emoji.skins.length === 0) {
|
||
return undefined;
|
||
}
|
||
// Single-person emojis expose exactly five uniform tones. Multi-person emojis
|
||
// expose every tone combination, ordered with the first person fixed at the
|
||
// light tone — so the first five would be mixed pairs (light+light,
|
||
// light+medium-light, …). Keep only the uniform variations so the five swatches
|
||
// map cleanly onto [light, medium-light, medium, medium-dark, dark].
|
||
const tones = [];
|
||
for (const skin of emoji.skins) {
|
||
if (tones.length >= 5) break;
|
||
if (skin.unicode && isUniformTone(toneByHexcode.get(skin.hexcode))) {
|
||
tones.push(skin.unicode);
|
||
}
|
||
}
|
||
return tones.length === 5 ? tones : undefined;
|
||
}
|
||
|
||
function main() {
|
||
console.log('Reading emojibase-data compact English dataset...');
|
||
|
||
// Resolve from the design-system package where it's installed
|
||
const compactDataPath = require.resolve('emojibase-data/en/compact.json');
|
||
const rawData = JSON.parse(readFileSync(compactDataPath, 'utf-8'));
|
||
|
||
console.log(`Loaded ${rawData.length} emoji entries`);
|
||
|
||
// The compact dataset omits skin `tone` metadata, so load the full dataset to
|
||
// tell uniform skin-tone variations apart from mixed multi-person combinations.
|
||
const fullDataPath = require.resolve('emojibase-data/en/data.json');
|
||
const fullData = JSON.parse(readFileSync(fullDataPath, 'utf-8'));
|
||
const toneByHexcode = buildToneByHexcode(fullData);
|
||
|
||
// Group emojis into sections
|
||
const sections = {};
|
||
for (const sectionKey of SECTION_ORDER) {
|
||
sections[sectionKey] = [];
|
||
}
|
||
|
||
let skipped = 0;
|
||
let totalWithSkins = 0;
|
||
|
||
for (const emoji of rawData) {
|
||
// Skip entries without a group (e.g. component characters, regional indicators without group)
|
||
if (emoji.group === undefined || emoji.group === null) {
|
||
skipped++;
|
||
continue;
|
||
}
|
||
|
||
const groupInfo = GROUP_MAP[emoji.group];
|
||
if (!groupInfo) {
|
||
skipped++;
|
||
continue;
|
||
}
|
||
|
||
const keywords = buildKeywords(emoji);
|
||
const skins = extractSkinTones(emoji, toneByHexcode);
|
||
if (skins) totalWithSkins++;
|
||
|
||
const entry = {
|
||
u: emoji.unicode,
|
||
l: titleCase(emoji.label || ''),
|
||
k: keywords,
|
||
};
|
||
if (skins) {
|
||
entry.s = skins;
|
||
}
|
||
|
||
sections[groupInfo.key].push(entry);
|
||
}
|
||
|
||
// Build output
|
||
let totalEmojis = 0;
|
||
const sectionOutputs = [];
|
||
|
||
for (const sectionKey of SECTION_ORDER) {
|
||
const emojis = sections[sectionKey];
|
||
if (emojis.length === 0) continue;
|
||
totalEmojis += emojis.length;
|
||
|
||
const groupInfo = Object.values(GROUP_MAP).find((g) => g.key === sectionKey);
|
||
|
||
sectionOutputs.push({
|
||
key: sectionKey,
|
||
labelKey: groupInfo.labelKey,
|
||
emojis,
|
||
});
|
||
}
|
||
|
||
console.log(`Processed ${totalEmojis} emojis into ${sectionOutputs.length} sections (skipped ${skipped})`);
|
||
console.log(`${totalWithSkins} emojis have skin tone variants`);
|
||
|
||
// Generate TypeScript
|
||
let output = `// AUTO-GENERATED by scripts/generate-emoji-data.mjs — DO NOT EDIT
|
||
// Source: emojibase-data/en/compact.json
|
||
// Emojis: ${totalEmojis} | Sections: ${sectionOutputs.length} | With skin tones: ${totalWithSkins}
|
||
|
||
export interface EmojiEntry {
|
||
\t/** Emoji unicode character */
|
||
\tu: string;
|
||
\t/** Human-readable CLDR label (e.g. "Grinning Face") */
|
||
\tl: string;
|
||
\t/** Searchable keywords (label words + tags, lowercased) */
|
||
\tk: string[];
|
||
\t/** Skin tone variants [light, medium-light, medium, medium-dark, dark] */
|
||
\ts?: [string, string, string, string, string];
|
||
}
|
||
|
||
export interface EmojiSection {
|
||
\tkey: string;
|
||
\tlabelKey: string;
|
||
\temojis: EmojiEntry[];
|
||
}
|
||
|
||
export const emojiSections: EmojiSection[] = [\n`;
|
||
|
||
for (const section of sectionOutputs) {
|
||
output += `\t{\n`;
|
||
output += `\t\tkey: '${section.key}',\n`;
|
||
output += `\t\tlabelKey: '${section.labelKey}',\n`;
|
||
output += `\t\temojis: [\n`;
|
||
for (const emoji of section.emojis) {
|
||
const l = JSON.stringify(emoji.l);
|
||
const k = JSON.stringify(emoji.k);
|
||
if (emoji.s) {
|
||
const s = JSON.stringify(emoji.s);
|
||
output += `\t\t\t{ u: '${emoji.u}', l: ${l}, k: ${k}, s: ${s} },\n`;
|
||
} else {
|
||
output += `\t\t\t{ u: '${emoji.u}', l: ${l}, k: ${k} },\n`;
|
||
}
|
||
}
|
||
output += `\t\t],\n`;
|
||
output += `\t},\n`;
|
||
}
|
||
|
||
output += `];\n`;
|
||
|
||
writeFileSync(OUTPUT_PATH, output);
|
||
|
||
const sizeKB = Math.round(Buffer.byteLength(output) / 1024);
|
||
console.log(`\nDone! Written to: ${OUTPUT_PATH}`);
|
||
console.log(`File size: ${sizeKB} KB`);
|
||
}
|
||
|
||
main();
|