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.
Files changed (3) hide show
  1. package/i +0 -0
  2. package/index.js +173 -65
  3. 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 9.4 – Custom Emoji Helper
3
- * Author: ikjava (fixed version)
4
- * ✔ UTF-16 correct
5
- * ✔ Handles spaces, multiple emojis, and all text correctly
6
- * ✔ Telegraf compatible
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 (what Telegram API uses)
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
- * Main function: Replace emojis with custom emoji entities
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 { stripOriginal = true, skipUnmapped = true } = options;
61
+ const { skipUnmapped = true } = options;
27
62
  const entities = [];
28
63
  let out = '';
29
- let outOffset = 0; // UTF-16 offset in the OUTPUT string
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
- // Mapped emoji found
36
- if (stripOriginal) {
37
- // Replace with placeholder
38
- const placeholder = '\u200B'; // Zero-width space (1 UTF-16 unit)
39
- out += placeholder;
40
- entities.push({
41
- type: 'custom_emoji',
42
- offset: outOffset,
43
- length: 1,
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 without modifying text
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 text
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
- * Check if string contains 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')
143
178
  */
144
- function isEmo(str) {
145
- return /\p{Extended_Pictographic}/u.test(str);
179
+ function btnEmo(button, emojiId) {
180
+ return { ...button, icon_custom_emoji_id: emojiId };
146
181
  }
147
182
 
148
183
  /**
149
- * Merge and sort multiple 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')
150
193
  */
151
- function mergeEntities(baseEntities, newEntities) {
152
- const all = [...baseEntities, ...newEntities];
153
- all.sort((a, b) => a.offset - b.offset);
154
- 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;
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikemoji",
3
- "version": "1.0.4",
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": {