ikemoji 1.0.2 → 1.0.3

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 (2) hide show
  1. package/index.js +83 -26
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,99 +1,155 @@
1
1
  /**
2
2
  * Telegram Bot API 9.4 – Custom Emoji Helper
3
- * Author: ikjava
3
+ * Author: ikjava (fixed version)
4
4
  * ✔ UTF-16 correct
5
5
  * ✔ ZWJ / skin tone safe
6
- * ✔ length = 1
7
6
  * ✔ Handles multiple consecutive emojis and variation selectors
8
7
  * ✔ Telegraf compatible
8
+ * ✔ Properly handles spaces and all text
9
9
  */
10
10
 
11
11
  const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
12
12
 
13
+ /**
14
+ * Get UTF-16 code unit length (what Telegram API uses)
15
+ * Emojis like 😀 = 2 units, regular chars = 1 unit
16
+ */
17
+ function getUtf16Length(str) {
18
+ let length = 0;
19
+ for (const char of str) {
20
+ // Characters outside BMP (>U+FFFF) take 2 UTF-16 code units
21
+ length += char.codePointAt(0) > 0xFFFF ? 2 : 1;
22
+ }
23
+ return length;
24
+ }
25
+
26
+ /**
27
+ * Main function: Replace emojis with custom emoji entities
28
+ * @param {string} text - Original text with emojis
29
+ * @param {Object} emojiMap - Map of emoji -> custom_emoji_id
30
+ * @param {Object} options - { stripOriginal: true, skipUnmapped: true }
31
+ * @returns {{ text: string, entities: Array }}
32
+ */
13
33
  function emo(text, emojiMap, options = {}) {
14
34
  const { stripOriginal = true, skipUnmapped = true } = options;
15
-
16
35
  const entities = [];
17
36
  let out = '';
18
- let offset = 0;
37
+ let outOffset = 0; // Current UTF-16 offset in output string
19
38
 
20
39
  for (const { segment } of segmenter.segment(text)) {
21
40
  const id = emojiMap[segment];
22
-
41
+
23
42
  if (id) {
43
+ // This emoji has a custom replacement
24
44
  if (stripOriginal) {
25
- out += '▫️'; // placeholder
45
+ // Replace with invisible placeholder (1 UTF-16 unit)
46
+ out += '\u200B'; // Zero-width space
26
47
  entities.push({
27
48
  type: 'custom_emoji',
28
- offset,
49
+ offset: outOffset,
29
50
  length: 1,
30
51
  custom_emoji_id: id
31
52
  });
32
- offset += 1;
53
+ outOffset += 1;
33
54
  } else {
55
+ // Keep original emoji and add custom entity over it
34
56
  out += segment;
57
+ const segmentUtf16Length = getUtf16Length(segment);
35
58
  entities.push({
36
59
  type: 'custom_emoji',
37
- offset,
38
- length: 1,
60
+ offset: outOffset,
61
+ length: segmentUtf16Length,
39
62
  custom_emoji_id: id
40
63
  });
41
- offset += segment.length;
64
+ outOffset += segmentUtf16Length;
42
65
  }
43
66
  } else {
67
+ // Not in map
44
68
  if (!skipUnmapped || !isEmo(segment)) {
69
+ // Keep this segment (text, space, or unmapped emoji)
45
70
  out += segment;
46
- offset += segment.length;
71
+ outOffset += getUtf16Length(segment);
47
72
  }
73
+ // If skipUnmapped=true and it's an emoji, we skip it entirely
48
74
  }
49
75
  }
50
76
 
51
77
  return { text: out, entities };
52
78
  }
53
79
 
80
+ /**
81
+ * Generate entities without modifying text
82
+ * Useful when you want to keep original text but add custom emoji entities
83
+ */
54
84
  function emoEnt(text, emojiMap) {
55
85
  const entities = [];
56
86
  let offset = 0;
57
87
 
58
88
  for (const { segment } of segmenter.segment(text)) {
59
89
  if (emojiMap[segment]) {
60
- entities.push({ type: 'custom_emoji', offset, length: 1, custom_emoji_id: emojiMap[segment] });
90
+ const segmentUtf16Length = getUtf16Length(segment);
91
+ entities.push({
92
+ type: 'custom_emoji',
93
+ offset,
94
+ length: segmentUtf16Length,
95
+ custom_emoji_id: emojiMap[segment]
96
+ });
61
97
  }
62
- offset += segment.length;
98
+ offset += getUtf16Length(segment);
63
99
  }
64
100
 
65
101
  return entities;
66
102
  }
67
103
 
104
+ /**
105
+ * Find all unique emojis in text
106
+ */
68
107
  function findEmo(text) {
69
108
  const set = new Set();
70
-
71
109
  for (const { segment } of segmenter.segment(text)) {
72
110
  if (isEmo(segment)) set.add(segment);
73
111
  }
74
-
75
112
  return [...set];
76
113
  }
77
114
 
115
+ /**
116
+ * Validate entities array for Telegram API
117
+ */
78
118
  function checkEnt(entities) {
79
119
  const errors = [];
80
-
81
- if (entities.length > 100) errors.push(`Too many entities (${entities.length})`);
82
-
120
+
121
+ if (entities.length > 100) {
122
+ errors.push(`Too many entities (${entities.length})`);
123
+ }
124
+
83
125
  entities.forEach((e, i) => {
84
- if (e.type !== 'custom_emoji') errors.push(`Entity ${i}: invalid type`);
85
- if (!e.custom_emoji_id) errors.push(`Entity ${i}: missing custom_emoji_id`);
86
- if (e.length !== 1) errors.push(`Entity ${i}: length must be 1`);
87
- if (typeof e.offset !== 'number' || e.offset < 0) errors.push(`Entity ${i}: invalid offset`);
126
+ if (e.type !== 'custom_emoji') {
127
+ errors.push(`Entity ${i}: invalid type`);
128
+ }
129
+ if (!e.custom_emoji_id) {
130
+ errors.push(`Entity ${i}: missing custom_emoji_id`);
131
+ }
132
+ if (typeof e.length !== 'number' || e.length < 1) {
133
+ errors.push(`Entity ${i}: length must be >= 1`);
134
+ }
135
+ if (typeof e.offset !== 'number' || e.offset < 0) {
136
+ errors.push(`Entity ${i}: invalid offset`);
137
+ }
88
138
  });
89
-
139
+
90
140
  return { valid: errors.length === 0, errors };
91
141
  }
92
142
 
143
+ /**
144
+ * Check if string contains emoji
145
+ */
93
146
  function isEmo(str) {
94
147
  return /\p{Extended_Pictographic}/u.test(str);
95
148
  }
96
149
 
150
+ /**
151
+ * Merge and sort multiple entity arrays
152
+ */
97
153
  function mergeEntities(baseEntities, newEntities) {
98
154
  const all = [...baseEntities, ...newEntities];
99
155
  all.sort((a, b) => a.offset - b.offset);
@@ -106,5 +162,6 @@ module.exports = {
106
162
  findEmo,
107
163
  checkEnt,
108
164
  isEmo,
109
- mergeEntities
110
- };
165
+ mergeEntities,
166
+ getUtf16Length
167
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikemoji",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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": {