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.
Files changed (3) hide show
  1. package/i +0 -0
  2. package/index.js +159 -36
  3. 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
- * Replace emojis with custom emoji entities
23
- * IMPORTANT: stripOriginal MUST be false for Telegram Bot API
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 existing text with emojis
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 text
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
- * Check if string is emoji
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 isEmo(str) {
130
- return /\p{Extended_Pictographic}/u.test(str);
179
+ function btnEmo(button, emojiId) {
180
+ return { ...button, icon_custom_emoji_id: emojiId };
131
181
  }
132
182
 
133
183
  /**
134
- * Merge and sort entity arrays
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 mergeEntities(baseEntities, newEntities) {
137
- const all = [...baseEntities, ...newEntities];
138
- all.sort((a, b) => a.offset - b.offset);
139
- return all;
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikemoji",
3
- "version": "1.0.5",
3
+ "version": "2.0.0",
4
4
  "description": "Telegram Bot API 9.4 custom emoji helper - UTF-16 correct, ZWJ/skin tone safe, Telegraf compatible",
5
5
  "main": "index.js",
6
6
  "scripts": {