Skip to content

Instantly share code, notes, and snippets.

@nurdism
Last active July 7, 2024 21:16
Show Gist options
  • Save nurdism/de2e7e42c89b72913773461f8a91fad7 to your computer and use it in GitHub Desktop.
Save nurdism/de2e7e42c89b72913773461f8a91fad7 to your computer and use it in GitHub Desktop.
Emoji CSS tile sheet and keywords generator
import * as fs from 'fs'
import * as path from 'path'
import sharp from 'sharp'
import { fileURLToPath } from 'url'
import emoji from 'emoji-datasource-twitter/emoji.json'
import converter from 'discord-emoji-converter'
import emojilib from 'emojilib'
export interface DataGroupList {
id: string
name: string
emojis: string[]
}
export interface DataEmoji {
unicode?: string
variants?: string[]
}
export interface EmojiData {
groups: DataGroupList[]
keywords: Record<string, string[]>
emojis: Record<string, DataEmoji>
}
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const datasource: EmojiDatasource[] = emoji as EmojiDatasource[]
interface EmojiDatasource {
name: string
unified: string
non_qualified: string | null
docomo: string | null
au: string | null
softbank: string | null
google: string | null
image: string
sheet_x: number
sheet_y: number
short_name: string
short_names: string[]
text: string | null
texts: string | null
category: string
sort_order: number
added_in: string
has_img_apple: boolean
has_img_google: boolean
has_img_twitter: boolean
has_img_facebook: boolean
skin_variations: {
[id: string]: {
unified: string
image: string
sheet_x: number
sheet_y: number
added_in: string
has_img_apple: boolean
has_img_google: boolean
has_img_twitter: boolean
has_img_facebook: boolean
}
}
obsoletes: string
obsoleted_by: string
}
const SHEET_COLUMNS = 61
const MULTIPLY = 100 / SHEET_COLUMNS
const css: string[] = []
const keywords: Record<string, string[]> = {}
const emojis: Record<string, DataEmoji> = {}
const groups: Record<string, string[]> = {}
const bypass = ['skin_tone_2', 'skin_tone_3', 'skin_tone_4', 'skin_tone_5', 'skin_tone_6', 'eye_in_speech_bubble']
for (const source of datasource) {
const unified = source.unified.split('-').map((v) => v.toLowerCase())
let short_name = source.short_name.replace(/\-/g, '_').toLowerCase()
let char = String.fromCodePoint(...source.unified.split('-').map((v) => parseInt(v, 16)))
if (bypass.includes(short_name)) {
console.log(short_name, 'bypassing...')
continue
}
if (!source.has_img_twitter) {
console.log(short_name, 'not available for set twitter')
continue
}
let id = short_name
try {
let discord_id = converter.getShortcode(char).replace(/:/g, '')
if (discord_id) {
id = discord_id
}
} catch (err) {
console.log(`${char} :${id}: has no discord id!`)
}
// keywords
let words: string[] = []
for (const id of Object.keys(emojilib)) {
if (unified.includes(id.codePointAt(0)!.toString(16))) {
words = [id, ...emojilib[id]]
break
}
}
if (words.length == 0) {
console.log(id, 'no keywords')
}
for (let name of source.short_names) {
name = name.replace('-', '_').toLowerCase()
if (!words.includes(name)) {
words.push(name)
}
}
keywords[id] = words
// keywords
let group = ''
switch (source.category) {
case 'Symbols':
group = 'symbols'
break
case 'Activities':
group = 'activity'
break
case 'Flags':
group = 'flags'
break
case 'Travel & Places':
group = 'travel'
break
case 'Food & Drink':
group = 'food'
break
case 'Animals & Nature':
group = 'nature'
break
case 'People & Body':
group = 'people'
break
case 'Smileys & Emotion':
group = 'emotion'
break
case 'Objects':
group = 'objects'
break
case 'Skin Tones':
continue
default:
console.log(`unknown category ${source.category}`)
continue
}
const emoji: DataEmoji = {}
if (source.unified) {
emoji.unicode = source.unified.toLowerCase()
}
if (source.skin_variations) {
emoji.variants = Object.values(source.skin_variations).map((v) => v.unified.toLowerCase())
}
if (!groups[group]) {
groups[group] = [id]
} else {
groups[group].push(id)
}
if (!emoji.unicode || emojis[id] !== undefined) {
throw new Error('duplicate emoji')
}
// list
emojis[id] = emoji
// css
// prettier-ignore
css.push(`&[data-emoji='${id}'] {background-position:${MULTIPLY * source.sheet_x}% ${MULTIPLY * source.sheet_y}%;content-visibility:hidden;}`)
}
fs.writeFile(
path.join(__dirname, 'emoji.scss'),
`
.emoji {
display: inline-block;
background-size: ${SHEET_COLUMNS * 100}%;
background-image: url('sheet.webp');
background-repeat: no-repeat;
vertical-align: bottom;
height: 40px;
width: 40px;
&.blank{background-image: none;}
${css.map((v) => ` ${v}`).join('\n')}
}
`,
() => {
console.log('_emoji.scss done')
},
)
for (const k of Object.keys(groups)) {
groups[k] = groups[k].filter((item, pos) => groups[k].indexOf(item) == pos)
}
const people = [...groups['people'], ...groups['emotion']]
const data: EmojiData = {
groups: [
/*
{
id: 'emotion',
name: 'Emotion',
list: groups['emotion'] ? groups['emotion'] : [],
},
*/
{
id: 'people',
name: 'People',
emojis: people.filter((item, pos) => people.indexOf(item) == pos),
},
{
id: 'nature',
name: 'Nature',
emojis: groups['nature'] ? groups['nature'] : [],
},
{
id: 'food',
name: 'Food',
emojis: groups['food'] ? groups['food'] : [],
},
{
id: 'activity',
name: 'Activity',
emojis: groups['activity'] ? groups['activity'] : [],
},
{
id: 'travel',
name: 'Travel',
emojis: groups['travel'] ? groups['travel'] : [],
},
{
id: 'objects',
name: 'Objects',
emojis: groups['objects'] ? groups['objects'] : [],
},
{
id: 'symbols',
name: 'Symbols',
emojis: groups['symbols'] ? groups['symbols'] : [],
},
{
id: 'flags',
name: 'Flags',
emojis: groups['flags'] ? groups['flags'] : [],
},
],
emojis,
keywords,
}
sharp('node_modules/emoji-datasource-twitter/img/twitter/sheets/64.png').toFile(path.join(__dirname, 'sheet.webp'), (err, info) => {
if (err) {
console.error(err)
return
}
console.log('emoji.webp done')
})
fs.writeFile(path.join(__dirname, 'data.json'), JSON.stringify(data), (err) => {
if (err) {
console.error(err)
return
}
console.log('emoji.json done')
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment