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.
- package/index.js +83 -26
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
60
|
+
offset: outOffset,
|
|
61
|
+
length: segmentUtf16Length,
|
|
39
62
|
custom_emoji_id: id
|
|
40
63
|
});
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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')
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
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
|
+
};
|