You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wiki/frontend/scripts/generate-emoji.mjs

124 lines
4.6 KiB

/*
Generates `src/assets/emoji.generated.js` — the grouping the emoji picker is built from.
Two datasets have to meet for a picker to work here:
- `markdown-it-emoji` is the renderer's own vocabulary: `:shortcode:` is what gets written into a
page, and anything outside that map would go in as text that never becomes an emoji. It has no
notion of categories.
- `unicode-emoji-json` carries the CLDR grouping and ordering — Smileys & Emotion, Animals & Nature,
and so on — which is what the picker's tabs are, and no notion of shortcodes.
So this writes out the intersection: each group, in Unicode's own order, as `[shortcode, character]`
pairs — the shortcode is what gets written into the page, the character is what the picker draws in its
grid. The picker cannot offer an emoji the renderer would not draw, because every pair came from the
renderer's own map.
The characters are written here rather than looked up at runtime, even though the editor already
bundles that map for rendering. Reading it would mean importing `markdown-it-emoji/lib/data/full.mjs`
from app code — a path into the package's internals rather than an entry point it publishes, and one
more dependency for the bundler to discover. A few kB of duplicated characters is the cheaper side of
that trade.
`unicode-emoji-json` is a devDependency for that reason: its data ends up here, and here is committed.
Usage: node scripts/generate-emoji.mjs [--check]
*/
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import emojiByGroup from 'unicode-emoji-json/data-by-group.json' with { type: 'json' }
import shortcodeToEmoji from 'markdown-it-emoji/lib/data/full.mjs'
const ROOT = fileURLToPath(new URL('../', import.meta.url))
const OUT = path.join(ROOT, 'src/assets/emoji.generated.js')
/**
* One shortcode per emoji, since several can point at the same character — `laughing` and `satisfied`
* are both 😆, `+1` and `thumbsup` are both 👍.
*
* The first one wins, except that a name starting with a letter beats one that does not: the map's own
* order is close to canonical, and `thumbsup` reads better in a page's source than `+1`.
*/
function buildShortcodeIndex() {
const index = new Map()
for (const [shortcode, emoji] of Object.entries(shortcodeToEmoji)) {
const current = index.get(emoji)
if (!current || (!/^[a-z]/.test(current) && /^[a-z]/.test(shortcode))) {
index.set(emoji, shortcode)
}
}
return index
}
function build() {
const index = buildShortcodeIndex()
const groups = []
const claimed = new Set()
for (const group of emojiByGroup) {
const emoji = []
for (const entry of group.emojis) {
const shortcode = index.get(entry.emoji)
// -> Skipped rather than invented: an emoji the renderer has no shortcode for cannot be written
// into a page as an emoji, so it has no business in a picker
if (shortcode && !claimed.has(shortcode)) {
claimed.add(shortcode)
emoji.push([shortcode, entry.emoji])
}
}
if (emoji.length > 0) {
groups.push({ slug: group.slug, name: group.name, emoji })
}
}
return { groups, claimed }
}
function serialize({ groups, claimed }) {
const body = groups
.map((group) => {
const pairs = group.emoji.map(([code, char]) => `['${code}', '${char}']`).join(', ')
// -> Single quotes, so the generated file matches what oxfmt would write for the rest of `src`
return ` {
slug: '${group.slug}',
name: '${group.name}',
emoji: [${pairs}]
}`
})
.join(',\n')
return `/*
GENERATED by scripts/generate-emoji.mjs — do not edit.
The CLDR emoji groups, in Unicode's order, as \`[shortcode, character]\` pairs. The shortcode is one
\`markdown-it-emoji\` accepts — what a page stores and what the renderer draws — and the character is
what a picker shows. Regenerate with \`npm run emoji\`.
${claimed.size} emoji in ${groups.length} groups.
*/
export const EMOJI_GROUPS = [
${body}
]
`
}
const data = build()
const output = serialize(data)
if (process.argv.includes('--check')) {
const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8') : ''
if (current !== output) {
console.error('emoji.generated.js is out of date — run `npm run emoji`')
process.exit(1)
}
console.log(`OK ${data.claimed.size} emoji, data up to date`)
} else {
fs.mkdirSync(path.dirname(OUT), { recursive: true })
fs.writeFileSync(OUT, output)
console.log(
`wrote ${data.claimed.size} emoji in ${data.groups.length} groups to src/assets/emoji.generated.js (${Buffer.byteLength(output).toLocaleString()} B)`
)
}