ikemoji 1.0.5 → 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 +159 -36
- package/package.json +1 -1
package/i
ADDED
|
File without changes
|
package/index.js
CHANGED
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
* ✔ Keeps original emoji in text (REQUIRED by Telegram)
|
|
4
4
|
* ✔ UTF-16 correct offsets
|
|
5
5
|
* ✔ Handles multiple emojis
|
|
6
|
+
* ✔ Button helpers (Bot API 9.4+)
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
9
10
|
|
|
11
|
+
// ─────────────────────────────────────────────
|
|
12
|
+
// CORE UTILS
|
|
13
|
+
// ─────────────────────────────────────────────
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
|
-
* Get UTF-16 code unit length
|
|
16
|
+
* Get UTF-16 code unit length of a string
|
|
12
17
|
*/
|
|
13
18
|
function getUtf16Length(str) {
|
|
14
19
|
let length = 0;
|
|
@@ -19,11 +24,40 @@ function getUtf16Length(str) {
|
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
|
-
*
|
|
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
|
-
// Force stripOriginal to false - Telegram REQUIRES the original emoji
|
|
27
61
|
const { skipUnmapped = true } = options;
|
|
28
62
|
const entities = [];
|
|
29
63
|
let out = '';
|
|
@@ -31,22 +65,18 @@ function emo(text, emojiMap, options = {}) {
|
|
|
31
65
|
|
|
32
66
|
for (const { segment } of segmenter.segment(text)) {
|
|
33
67
|
const id = emojiMap[segment];
|
|
34
|
-
|
|
35
68
|
if (id) {
|
|
36
|
-
// Keep the original emoji (REQUIRED by Telegram)
|
|
37
69
|
out += segment;
|
|
38
70
|
const segmentLength = getUtf16Length(segment);
|
|
39
71
|
entities.push({
|
|
40
72
|
type: 'custom_emoji',
|
|
41
73
|
offset: outOffset,
|
|
42
74
|
length: segmentLength,
|
|
43
|
-
custom_emoji_id: id
|
|
75
|
+
custom_emoji_id: id,
|
|
44
76
|
});
|
|
45
77
|
outOffset += segmentLength;
|
|
46
78
|
} else {
|
|
47
|
-
// Regular text or unmapped emoji
|
|
48
79
|
const isEmoji = isEmo(segment);
|
|
49
|
-
|
|
50
80
|
if (!skipUnmapped || !isEmoji) {
|
|
51
81
|
out += segment;
|
|
52
82
|
outOffset += getUtf16Length(segment);
|
|
@@ -58,7 +88,12 @@ function emo(text, emojiMap, options = {}) {
|
|
|
58
88
|
}
|
|
59
89
|
|
|
60
90
|
/**
|
|
61
|
-
* Generate entities for
|
|
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
|
|
62
97
|
*/
|
|
63
98
|
function emoEnt(text, emojiMap) {
|
|
64
99
|
const entities = [];
|
|
@@ -71,7 +106,7 @@ function emoEnt(text, emojiMap) {
|
|
|
71
106
|
type: 'custom_emoji',
|
|
72
107
|
offset,
|
|
73
108
|
length: segmentLength,
|
|
74
|
-
custom_emoji_id: emojiMap[segment]
|
|
109
|
+
custom_emoji_id: emojiMap[segment],
|
|
75
110
|
});
|
|
76
111
|
}
|
|
77
112
|
offset += getUtf16Length(segment);
|
|
@@ -81,7 +116,10 @@ function emoEnt(text, emojiMap) {
|
|
|
81
116
|
}
|
|
82
117
|
|
|
83
118
|
/**
|
|
84
|
-
* Find all unique emojis in
|
|
119
|
+
* Find all unique emojis present in a string
|
|
120
|
+
*
|
|
121
|
+
* @param {string} text
|
|
122
|
+
* @returns {string[]}
|
|
85
123
|
*/
|
|
86
124
|
function findEmo(text) {
|
|
87
125
|
const set = new Set();
|
|
@@ -92,59 +130,144 @@ function findEmo(text) {
|
|
|
92
130
|
}
|
|
93
131
|
|
|
94
132
|
/**
|
|
95
|
-
* 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[] }}
|
|
96
138
|
*/
|
|
97
139
|
function checkEnt(entities, text) {
|
|
98
140
|
const errors = [];
|
|
99
141
|
const textLength = text ? getUtf16Length(text) : Infinity;
|
|
100
|
-
|
|
142
|
+
|
|
101
143
|
if (entities.length > 100) {
|
|
102
144
|
errors.push(`Too many entities (${entities.length})`);
|
|
103
145
|
}
|
|
104
|
-
|
|
146
|
+
|
|
105
147
|
entities.forEach((e, i) => {
|
|
106
|
-
if (e.type !== 'custom_emoji')
|
|
148
|
+
if (e.type !== 'custom_emoji')
|
|
107
149
|
errors.push(`Entity ${i}: invalid type "${e.type}"`);
|
|
108
|
-
|
|
109
|
-
if (!e.custom_emoji_id) {
|
|
150
|
+
if (!e.custom_emoji_id)
|
|
110
151
|
errors.push(`Entity ${i}: missing custom_emoji_id`);
|
|
111
|
-
|
|
112
|
-
if (typeof e.length !== 'number' || e.length < 1) {
|
|
152
|
+
if (typeof e.length !== 'number' || e.length < 1)
|
|
113
153
|
errors.push(`Entity ${i}: invalid length ${e.length}`);
|
|
114
|
-
|
|
115
|
-
if (typeof e.offset !== 'number' || e.offset < 0) {
|
|
154
|
+
if (typeof e.offset !== 'number' || e.offset < 0)
|
|
116
155
|
errors.push(`Entity ${i}: invalid offset ${e.offset}`);
|
|
117
|
-
|
|
118
|
-
if (text && (e.offset + e.length > textLength)) {
|
|
156
|
+
if (text && e.offset + e.length > textLength)
|
|
119
157
|
errors.push(`Entity ${i}: offset ${e.offset} + length ${e.length} exceeds text length ${textLength}`);
|
|
120
|
-
}
|
|
121
158
|
});
|
|
122
|
-
|
|
159
|
+
|
|
123
160
|
return { valid: errors.length === 0, errors };
|
|
124
161
|
}
|
|
125
162
|
|
|
163
|
+
// ─────────────────────────────────────────────
|
|
164
|
+
// BUTTON HELPERS (Bot API 9.4+)
|
|
165
|
+
// ─────────────────────────────────────────────
|
|
166
|
+
|
|
126
167
|
/**
|
|
127
|
-
*
|
|
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')
|
|
128
178
|
*/
|
|
129
|
-
function
|
|
130
|
-
return
|
|
179
|
+
function btnEmo(button, emojiId) {
|
|
180
|
+
return { ...button, icon_custom_emoji_id: emojiId };
|
|
131
181
|
}
|
|
132
182
|
|
|
133
183
|
/**
|
|
134
|
-
*
|
|
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')
|
|
135
193
|
*/
|
|
136
|
-
function
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
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;
|
|
198
|
+
}
|
|
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
|
+
});
|
|
140
220
|
}
|
|
141
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
|
+
|
|
142
258
|
module.exports = {
|
|
259
|
+
// Text / entities
|
|
143
260
|
emo,
|
|
144
261
|
emoEnt,
|
|
145
262
|
findEmo,
|
|
146
263
|
checkEnt,
|
|
147
|
-
isEmo,
|
|
148
264
|
mergeEntities,
|
|
149
|
-
getUtf16Length
|
|
265
|
+
getUtf16Length,
|
|
266
|
+
isEmo,
|
|
267
|
+
// Buttons
|
|
268
|
+
btnEmo,
|
|
269
|
+
btnEmoStyled,
|
|
270
|
+
buildRow,
|
|
271
|
+
buildKeyboard,
|
|
272
|
+
buildReplyKeyboard,
|
|
150
273
|
};
|