ikemoji 1.0.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/i +0 -0
- package/index.js +173 -65
- package/package.json +1 -1
package/i
ADDED
|
File without changes
|
package/index.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram Bot API
|
|
3
|
-
*
|
|
4
|
-
* ✔ UTF-16 correct
|
|
5
|
-
* ✔ Handles
|
|
6
|
-
* ✔
|
|
2
|
+
* Telegram Bot API Custom Emoji Helper by (ikjava)
|
|
3
|
+
* ✔ Keeps original emoji in text (REQUIRED by Telegram)
|
|
4
|
+
* ✔ UTF-16 correct offsets
|
|
5
|
+
* ✔ Handles multiple emojis
|
|
6
|
+
* ✔ Button helpers (Bot API 9.4+)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
10
10
|
|
|
11
|
+
// ─────────────────────────────────────────────
|
|
12
|
+
// CORE UTILS
|
|
13
|
+
// ─────────────────────────────────────────────
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
|
-
* Get UTF-16 code unit length
|
|
16
|
+
* Get UTF-16 code unit length of a string
|
|
13
17
|
*/
|
|
14
18
|
function getUtf16Length(str) {
|
|
15
19
|
let length = 0;
|
|
@@ -20,52 +24,63 @@ function getUtf16Length(str) {
|
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
|
-
*
|
|
27
|
+
* Check if string is an emoji
|
|
28
|
+
*/
|
|
29
|
+
function isEmo(str) {
|
|
30
|
+
return /\p{Extended_Pictographic}/u.test(str);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Merge and sort multiple entity arrays by offset
|
|
35
|
+
*/
|
|
36
|
+
function mergeEntities(baseEntities, newEntities) {
|
|
37
|
+
const all = [...baseEntities, ...newEntities];
|
|
38
|
+
all.sort((a, b) => a.offset - b.offset);
|
|
39
|
+
return all;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────
|
|
43
|
+
// TEXT / ENTITY HELPERS
|
|
44
|
+
// ─────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Replace emojis in text with custom emoji entities.
|
|
48
|
+
* IMPORTANT: Original emoji chars are kept in output — Telegram requires them.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} text
|
|
51
|
+
* @param {Object} emojiMap - { '⭐': 'custom_emoji_id', ... }
|
|
52
|
+
* @param {Object} [options]
|
|
53
|
+
* @param {boolean} [options.skipUnmapped=true] - Drop unmapped emojis from output
|
|
54
|
+
* @returns {{ text: string, entities: Array }}
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* const { text, entities } = emo('Hello ⭐!', { '⭐': '5368324170671202286' });
|
|
58
|
+
* bot.sendMessage(chatId, text, { entities });
|
|
24
59
|
*/
|
|
25
60
|
function emo(text, emojiMap, options = {}) {
|
|
26
|
-
const {
|
|
61
|
+
const { skipUnmapped = true } = options;
|
|
27
62
|
const entities = [];
|
|
28
63
|
let out = '';
|
|
29
|
-
let outOffset = 0;
|
|
64
|
+
let outOffset = 0;
|
|
30
65
|
|
|
31
66
|
for (const { segment } of segmenter.segment(text)) {
|
|
32
67
|
const id = emojiMap[segment];
|
|
33
|
-
|
|
34
68
|
if (id) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
custom_emoji_id: id
|
|
45
|
-
});
|
|
46
|
-
outOffset += 1;
|
|
47
|
-
} else {
|
|
48
|
-
// Keep original emoji
|
|
49
|
-
out += segment;
|
|
50
|
-
const segmentLength = getUtf16Length(segment);
|
|
51
|
-
entities.push({
|
|
52
|
-
type: 'custom_emoji',
|
|
53
|
-
offset: outOffset,
|
|
54
|
-
length: segmentLength,
|
|
55
|
-
custom_emoji_id: id
|
|
56
|
-
});
|
|
57
|
-
outOffset += segmentLength;
|
|
58
|
-
}
|
|
69
|
+
out += segment;
|
|
70
|
+
const segmentLength = getUtf16Length(segment);
|
|
71
|
+
entities.push({
|
|
72
|
+
type: 'custom_emoji',
|
|
73
|
+
offset: outOffset,
|
|
74
|
+
length: segmentLength,
|
|
75
|
+
custom_emoji_id: id,
|
|
76
|
+
});
|
|
77
|
+
outOffset += segmentLength;
|
|
59
78
|
} else {
|
|
60
|
-
// Not a mapped emoji
|
|
61
79
|
const isEmoji = isEmo(segment);
|
|
62
|
-
|
|
63
80
|
if (!skipUnmapped || !isEmoji) {
|
|
64
|
-
// Keep this segment (regular text, spaces, or unmapped emojis)
|
|
65
81
|
out += segment;
|
|
66
82
|
outOffset += getUtf16Length(segment);
|
|
67
83
|
}
|
|
68
|
-
// If skipUnmapped=true and it's an unmapped emoji, skip it entirely
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
86
|
|
|
@@ -73,7 +88,12 @@ function emo(text, emojiMap, options = {}) {
|
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
/**
|
|
76
|
-
* Generate entities
|
|
91
|
+
* Generate custom_emoji entities for an already-built string.
|
|
92
|
+
* Use this when you have the final text and just need the entity list.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} text
|
|
95
|
+
* @param {Object} emojiMap
|
|
96
|
+
* @returns {Array} entities
|
|
77
97
|
*/
|
|
78
98
|
function emoEnt(text, emojiMap) {
|
|
79
99
|
const entities = [];
|
|
@@ -86,7 +106,7 @@ function emoEnt(text, emojiMap) {
|
|
|
86
106
|
type: 'custom_emoji',
|
|
87
107
|
offset,
|
|
88
108
|
length: segmentLength,
|
|
89
|
-
custom_emoji_id: emojiMap[segment]
|
|
109
|
+
custom_emoji_id: emojiMap[segment],
|
|
90
110
|
});
|
|
91
111
|
}
|
|
92
112
|
offset += getUtf16Length(segment);
|
|
@@ -96,7 +116,10 @@ function emoEnt(text, emojiMap) {
|
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
/**
|
|
99
|
-
* Find all unique emojis in
|
|
119
|
+
* Find all unique emojis present in a string
|
|
120
|
+
*
|
|
121
|
+
* @param {string} text
|
|
122
|
+
* @returns {string[]}
|
|
100
123
|
*/
|
|
101
124
|
function findEmo(text) {
|
|
102
125
|
const set = new Set();
|
|
@@ -107,59 +130,144 @@ function findEmo(text) {
|
|
|
107
130
|
}
|
|
108
131
|
|
|
109
132
|
/**
|
|
110
|
-
* Validate entities array
|
|
133
|
+
* Validate a custom_emoji entities array
|
|
134
|
+
*
|
|
135
|
+
* @param {Array} entities
|
|
136
|
+
* @param {string} [text] - If provided, checks offsets don't exceed text length
|
|
137
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
111
138
|
*/
|
|
112
139
|
function checkEnt(entities, text) {
|
|
113
140
|
const errors = [];
|
|
114
141
|
const textLength = text ? getUtf16Length(text) : Infinity;
|
|
115
|
-
|
|
142
|
+
|
|
116
143
|
if (entities.length > 100) {
|
|
117
144
|
errors.push(`Too many entities (${entities.length})`);
|
|
118
145
|
}
|
|
119
|
-
|
|
146
|
+
|
|
120
147
|
entities.forEach((e, i) => {
|
|
121
|
-
if (e.type !== 'custom_emoji')
|
|
148
|
+
if (e.type !== 'custom_emoji')
|
|
122
149
|
errors.push(`Entity ${i}: invalid type "${e.type}"`);
|
|
123
|
-
|
|
124
|
-
if (!e.custom_emoji_id) {
|
|
150
|
+
if (!e.custom_emoji_id)
|
|
125
151
|
errors.push(`Entity ${i}: missing custom_emoji_id`);
|
|
126
|
-
|
|
127
|
-
if (typeof e.length !== 'number' || e.length < 1) {
|
|
152
|
+
if (typeof e.length !== 'number' || e.length < 1)
|
|
128
153
|
errors.push(`Entity ${i}: invalid length ${e.length}`);
|
|
129
|
-
|
|
130
|
-
if (typeof e.offset !== 'number' || e.offset < 0) {
|
|
154
|
+
if (typeof e.offset !== 'number' || e.offset < 0)
|
|
131
155
|
errors.push(`Entity ${i}: invalid offset ${e.offset}`);
|
|
132
|
-
|
|
133
|
-
if (text && (e.offset + e.length > textLength)) {
|
|
156
|
+
if (text && e.offset + e.length > textLength)
|
|
134
157
|
errors.push(`Entity ${i}: offset ${e.offset} + length ${e.length} exceeds text length ${textLength}`);
|
|
135
|
-
}
|
|
136
158
|
});
|
|
137
|
-
|
|
159
|
+
|
|
138
160
|
return { valid: errors.length === 0, errors };
|
|
139
161
|
}
|
|
140
162
|
|
|
163
|
+
// ─────────────────────────────────────────────
|
|
164
|
+
// BUTTON HELPERS (Bot API 9.4+)
|
|
165
|
+
// ─────────────────────────────────────────────
|
|
166
|
+
|
|
141
167
|
/**
|
|
142
|
-
*
|
|
168
|
+
* Add a custom emoji icon to any button object.
|
|
169
|
+
* Works for both InlineKeyboardButton and KeyboardButton.
|
|
170
|
+
* Requires bot owner to have Telegram Premium.
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} button - Existing button object
|
|
173
|
+
* @param {string} emojiId - Custom emoji ID
|
|
174
|
+
* @returns {Object}
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* btnEmo({ text: '⭐ Star', callback_data: 'star' }, '5368324170671202286')
|
|
143
178
|
*/
|
|
144
|
-
function
|
|
145
|
-
return
|
|
179
|
+
function btnEmo(button, emojiId) {
|
|
180
|
+
return { ...button, icon_custom_emoji_id: emojiId };
|
|
146
181
|
}
|
|
147
182
|
|
|
148
183
|
/**
|
|
149
|
-
*
|
|
184
|
+
* Add a custom emoji icon + color style to a button.
|
|
185
|
+
*
|
|
186
|
+
* @param {Object} button
|
|
187
|
+
* @param {string} emojiId
|
|
188
|
+
* @param {'default'|'secondary'|'destructive'} [style]
|
|
189
|
+
* @returns {Object}
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* btnEmoStyled({ text: '🗑 Delete', callback_data: 'del' }, '5447644880824181073', 'destructive')
|
|
150
193
|
*/
|
|
151
|
-
function
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
return
|
|
194
|
+
function btnEmoStyled(button, emojiId, style) {
|
|
195
|
+
const result = { ...button, icon_custom_emoji_id: emojiId };
|
|
196
|
+
if (style) result.style = style;
|
|
197
|
+
return result;
|
|
155
198
|
}
|
|
156
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Build one keyboard row from an array of button definitions.
|
|
202
|
+
* Each def may include shorthand `emojiId` and `style` fields
|
|
203
|
+
* that get wired to icon_custom_emoji_id / style automatically.
|
|
204
|
+
*
|
|
205
|
+
* @param {Array<Object>} defs
|
|
206
|
+
* @returns {Array<Object>} InlineKeyboardButton[]
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* buildRow([
|
|
210
|
+
* { text: '⭐', callback_data: 'a', emojiId: '111' },
|
|
211
|
+
* { text: '💬', callback_data: 'b', emojiId: '222' },
|
|
212
|
+
* ])
|
|
213
|
+
*/
|
|
214
|
+
function buildRow(defs) {
|
|
215
|
+
return defs.map(({ emojiId, style, ...button }) => {
|
|
216
|
+
if (emojiId) button.icon_custom_emoji_id = emojiId;
|
|
217
|
+
if (style) button.style = style;
|
|
218
|
+
return button;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Build a complete inline keyboard reply_markup from a 2D array of button defs.
|
|
224
|
+
*
|
|
225
|
+
* @param {Array<Array<Object>>} rows
|
|
226
|
+
* @returns {{ inline_keyboard: Array<Array<Object>> }}
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* const markup = buildKeyboard([
|
|
230
|
+
* [{ text: '⭐ Star', callback_data: 'star', emojiId: '111' }],
|
|
231
|
+
* [{ text: '🗑 Delete', callback_data: 'del', emojiId: '222', style: 'destructive' }],
|
|
232
|
+
* ]);
|
|
233
|
+
* bot.sendMessage(chatId, text, { entities, ...markup });
|
|
234
|
+
*/
|
|
235
|
+
function buildKeyboard(rows) {
|
|
236
|
+
return { inline_keyboard: rows.map(buildRow) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Build a reply keyboard markup from a 2D array of button defs.
|
|
241
|
+
*
|
|
242
|
+
* @param {Array<Array<Object>>} rows
|
|
243
|
+
* @param {Object} [opts] - Extra options e.g. { one_time_keyboard: true }
|
|
244
|
+
* @returns {{ keyboard, resize_keyboard, ...opts }}
|
|
245
|
+
*/
|
|
246
|
+
function buildReplyKeyboard(rows, opts = {}) {
|
|
247
|
+
return {
|
|
248
|
+
keyboard: rows.map(buildRow),
|
|
249
|
+
resize_keyboard: true,
|
|
250
|
+
...opts,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─────────────────────────────────────────────
|
|
255
|
+
// EXPORTS
|
|
256
|
+
// ─────────────────────────────────────────────
|
|
257
|
+
|
|
157
258
|
module.exports = {
|
|
259
|
+
// Text / entities
|
|
158
260
|
emo,
|
|
159
261
|
emoEnt,
|
|
160
262
|
findEmo,
|
|
161
263
|
checkEnt,
|
|
162
|
-
isEmo,
|
|
163
264
|
mergeEntities,
|
|
164
|
-
getUtf16Length
|
|
265
|
+
getUtf16Length,
|
|
266
|
+
isEmo,
|
|
267
|
+
// Buttons
|
|
268
|
+
btnEmo,
|
|
269
|
+
btnEmoStyled,
|
|
270
|
+
buildRow,
|
|
271
|
+
buildKeyboard,
|
|
272
|
+
buildReplyKeyboard,
|
|
165
273
|
};
|