Skip to content

Commit bc059ee

Browse files
authored
Merge pull request #5142 from traPtitech/feat/update-stamp-categories
feat: Unicode絵文字セット更新
2 parents 7a7a694 + da08953 commit bc059ee

6 files changed

Lines changed: 268 additions & 113 deletions

File tree

build/gen-unicode_emojis.js

Lines changed: 0 additions & 110 deletions
This file was deleted.

build/gen-unicode_emojis.ts

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import fs from 'fs/promises'
2+
import { styleText } from 'util'
3+
4+
import axios from 'axios'
5+
6+
// 開発環境で実行するスクリプトのため, console の使用は問題ない.
7+
/* eslint no-console: 0 */
8+
9+
const OUTPUT_EMOJIS_FILE = './src/assets/unicode_emojis.json'
10+
const OUTPUT_ALT_NAMES_FILE = './src/assets/emoji_altname_table.json'
11+
12+
// https://github.com/traPtitech/traQ/blob/d6d3981a412e5b259c04bcfcef6b87c58b3c7267/utils/twemoji/installer.go#L30
13+
const UNICODE_EMOJIS_META_URL =
14+
'https://raw.githubusercontent.com/joypixels/emoji-assets/v9.0.0/emoji.json'
15+
const UNICODE_EMOJIS_CATEGORIES_URL =
16+
'https://raw.githubusercontent.com/joypixels/emoji-toolkit/9.0.0/categories.json'
17+
18+
// https://github.com/traPtitech/traQ/blob/1ebbfabc64891ea6181ad4b3f417702dd5cad5ca/utils/validator/rules.go#L75-L79
19+
const TRAQ_STAMP_NAME_RULE = /^[a-zA-Z0-9_-]{1,32}$/
20+
21+
// ZWJ (Zero Width Joiner): 複数の Unicode 文字を結合して1つの絵文字として表示するために使用される特殊な文字.
22+
const ZWJ = String.fromCodePoint(0x200d)
23+
24+
// https://github.com/traPtitech/traQ/blob/d6d3981a412e5b259c04bcfcef6b87c58b3c7267/utils/twemoji/installer.go#L44-L58
25+
const replaceNameMap: Record<string, string> = {
26+
// 英数字以外の文字が含まれているので置き換え
27+
'pi\u00f1ata': 'pinata',
28+
// 長すぎるので置き換え
29+
face_with_open_eyes_and_hand_over_mouth: 'face_with_open_eyes_hand',
30+
hand_with_index_finger_and_thumb_crossed: 'hand_index_finger_thumb_crossed',
31+
man_in_motorized_wheelchair_facing_right: 'man_powered_wheelchair_right',
32+
man_in_manual_wheelchair_facing_right: 'man_manual_wheelchair_right',
33+
woman_in_manual_wheelchair_facing_right: 'woman_manual_wheelchair_right',
34+
woman_in_motorized_wheelchair_facing_right: 'woman_powered_wheelchair_right',
35+
person_in_motorized_wheelchair_facing_right:
36+
'person_powered_wheelchair_right',
37+
person_in_manual_wheelchair_facing_right: 'person_manual_wheelchair_right',
38+
woman_with_white_cane_facing_right: 'woman_white_cane_facing_right',
39+
person_with_white_cane_facing_right: 'person_white_cane_facing_right'
40+
}
41+
42+
/**
43+
* スタンプ名重複の検査
44+
*/
45+
function checkDuplicateStampNames(emojis: { [unicodeString: string]: string }) {
46+
const nameToUnicodeStringsMap = Object.entries(emojis).reduce(
47+
(grouped, [unicodeString, name]) => {
48+
if (!grouped[name]) {
49+
grouped[name] = []
50+
}
51+
grouped[name].push(unicodeString)
52+
return grouped
53+
},
54+
{} as Record<string, string[]>
55+
)
56+
const duplicateNames = Object.entries(nameToUnicodeStringsMap).filter(
57+
([, unicodeStrings]) => unicodeStrings.length > 1
58+
)
59+
if (duplicateNames.length > 0) {
60+
console.error(
61+
styleText('bgRed', 'ERROR'),
62+
'Duplicate stamp names found. Please fix `replaceNameMap` for these emojis:',
63+
Object.fromEntries(duplicateNames),
64+
'\n'
65+
)
66+
throw new Error('Duplicate stamp names found')
67+
}
68+
}
69+
70+
/**
71+
* traQ 上の制約に合わないスタンプ名の検査
72+
*/
73+
function checkInvalidStampNames(emojis: { [unicodeString: string]: string }) {
74+
const invalidEmojiNames = Object.values(emojis).filter(
75+
name => !TRAQ_STAMP_NAME_RULE.test(name)
76+
)
77+
if (invalidEmojiNames.length > 0) {
78+
console.error(
79+
styleText('bgRed', 'ERROR'),
80+
'Invalid emoji names found. Please fix `replaceNameMap` for these names:',
81+
invalidEmojiNames,
82+
'\n'
83+
)
84+
throw new Error('Invalid stamp names found')
85+
}
86+
}
87+
88+
/**
89+
* データ取得
90+
*/
91+
async function fetchData() {
92+
const [{ data: emojis }, { data: categories }] = await Promise.all([
93+
axios.get(UNICODE_EMOJIS_META_URL) as Promise<{
94+
data: Record<
95+
string,
96+
{
97+
name: string
98+
category: string
99+
order: number
100+
shortname: string
101+
shortname_alternates: string[]
102+
}
103+
>
104+
}>,
105+
axios.get(UNICODE_EMOJIS_CATEGORIES_URL) as Promise<{
106+
data: { order: number; category: string; category_label: string }[]
107+
}>
108+
])
109+
return { emojis, categories }
110+
}
111+
112+
async function main() {
113+
const { emojis, categories } = await fetchData()
114+
115+
const categoryMap = Object.fromEntries(
116+
categories.map(c => [
117+
c.category,
118+
{
119+
order: c.order,
120+
category: c.category,
121+
category_label: c.category_label,
122+
emojis: [] as { name: string; order: number }[]
123+
}
124+
])
125+
)
126+
categoryMap['regional'] = {
127+
order: categories.length + 1,
128+
category: 'regional',
129+
category_label: 'Alphabets',
130+
emojis: []
131+
}
132+
133+
const unicodeTable: Record<string, string> = {}
134+
const altNameTable: Record<string, string> = {
135+
0: 'zero',
136+
1: 'one',
137+
2: 'two',
138+
3: 'three',
139+
4: 'four',
140+
5: 'five',
141+
6: 'six',
142+
7: 'seven',
143+
8: 'eight',
144+
9: 'nine'
145+
}
146+
Object.entries(emojis).forEach(([key, e]) => {
147+
if (e.category === 'modifier') return
148+
if (e.name.endsWith('skin tone')) return
149+
150+
let name = e.shortname.replaceAll(/^:|:$/g, '')
151+
const replacedName = replaceNameMap[name]
152+
if (replacedName !== undefined) {
153+
name = replacedName
154+
}
155+
156+
const category = categoryMap[e.category]
157+
if (!category) {
158+
console.error(
159+
styleText('bgRed', 'ERROR'),
160+
`Category not found for emoji:`,
161+
e,
162+
'\n'
163+
)
164+
throw new Error(`Category "${e.category}" not found`)
165+
}
166+
category.emojis.push({ name, order: e.order })
167+
168+
const unicodeString = key
169+
.split('-')
170+
.map(codePoint => String.fromCodePoint(parseInt(codePoint, 16)))
171+
.join(ZWJ)
172+
unicodeTable[unicodeString] = name
173+
174+
e.shortname_alternates.forEach(altName => {
175+
altNameTable[altName.replaceAll(/^:|:$/g, '')] = name
176+
})
177+
})
178+
179+
checkDuplicateStampNames(unicodeTable)
180+
checkInvalidStampNames(unicodeTable)
181+
182+
const result = Object.entries(categoryMap).map(([key, category]) => {
183+
if (key === 'regional') {
184+
category.emojis.sort((a, b) => (a.order > b.order ? -1 : 1))
185+
} else {
186+
category.emojis.sort((a, b) => (a.order < b.order ? -1 : 1))
187+
}
188+
return {
189+
category: category.category,
190+
emojis: category.emojis.map(({ name }) => name)
191+
}
192+
})
193+
194+
const sortedAltNameTable = Object.fromEntries(
195+
Object.entries(altNameTable).sort((a, b) => (a[0] > b[0] ? 1 : -1))
196+
)
197+
const sortedUnicodeTable = Object.fromEntries(
198+
Object.entries(unicodeTable).sort((a, b) => (a[0] > b[0] ? 1 : -1))
199+
)
200+
201+
await Promise.all([
202+
fs.writeFile(OUTPUT_EMOJIS_FILE, JSON.stringify(result)),
203+
fs.writeFile(
204+
OUTPUT_ALT_NAMES_FILE,
205+
JSON.stringify({
206+
altNameTable: sortedAltNameTable,
207+
unicodeTable: sortedUnicodeTable
208+
})
209+
)
210+
])
211+
console.log(styleText('bgCyan', 'INFO'), 'Generated:', [
212+
OUTPUT_EMOJIS_FILE,
213+
OUTPUT_ALT_NAMES_FILE
214+
])
215+
}
216+
217+
await main().catch(e => {
218+
console.error(e)
219+
process.exit(1)
220+
})

0 commit comments

Comments
 (0)