@ckeditor/ckeditor5-emoji 44.2.0-alpha.7 → 44.2.0-alpha.9

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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ import { Plugin } from 'ckeditor5/src/core.js';
6
+ import type { EmojiCdnResource, EmojiEntry } from './emojirepository.js';
7
+ /**
8
+ * The Emoji utilities plugin.
9
+ */
10
+ export default class EmojiUtils extends Plugin {
11
+ /**
12
+ * @inheritDoc
13
+ */
14
+ static get pluginName(): "EmojiUtils";
15
+ /**
16
+ * @inheritDoc
17
+ */
18
+ static get isOfficialPlugin(): true;
19
+ /**
20
+ * Checks if the emoji is supported by verifying the emoji version supported by the system first.
21
+ * Then checks if emoji contains a zero width joiner (ZWJ), and if yes, then checks if it is supported by the system.
22
+ */
23
+ isEmojiSupported(item: EmojiCdnResource, emojiSupportedVersionByOs: number, container: HTMLDivElement): boolean;
24
+ /**
25
+ * Checks the supported emoji version by the OS, by sampling some representatives from different emoji releases.
26
+ */
27
+ getEmojiSupportedVersionByOs(): number;
28
+ /**
29
+ * Check for ZWJ (zero width joiner) character.
30
+ */
31
+ hasZwj(emoji: string): boolean;
32
+ /**
33
+ * Checks whether the emoji is supported in the operating system.
34
+ */
35
+ isEmojiZwjSupported(item: EmojiCdnResource, container: HTMLDivElement): boolean;
36
+ /**
37
+ * Returns the width of the provided node.
38
+ */
39
+ getNodeWidth(container: HTMLDivElement, node: string): number;
40
+ /**
41
+ * Creates a div for emoji width testing purposes.
42
+ */
43
+ createEmojiWidthTestingContainer(): HTMLDivElement;
44
+ /**
45
+ * Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
46
+ */
47
+ normalizeEmojiSkinTone(item: EmojiCdnResource): EmojiEntry;
48
+ /**
49
+ * Checks whether the emoji belongs to a group that is allowed.
50
+ */
51
+ isEmojiCategoryAllowed(item: EmojiCdnResource): boolean;
52
+ /**
53
+ * A function used to determine if emoji is supported by detecting pixels.
54
+ *
55
+ * Referenced for unit testing purposes. Kept in a separate file because of licensing.
56
+ */
57
+ private static _isEmojiSupported;
58
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ import { Plugin } from 'ckeditor5/src/core.js';
6
+ import isEmojiSupported from './utils/isemojisupported.js';
7
+ /**
8
+ * @module emoji/emojiutils
9
+ */
10
+ const SKIN_TONE_MAP = {
11
+ 0: 'default',
12
+ 1: 'light',
13
+ 2: 'medium-light',
14
+ 3: 'medium',
15
+ 4: 'medium-dark',
16
+ 5: 'dark'
17
+ };
18
+ /**
19
+ * A map representing an emoji and its release version.
20
+ * It's used to identify a user's minimal supported emoji level.
21
+ */
22
+ const EMOJI_SUPPORT_LEVEL = {
23
+ '🫩': 16,
24
+ '🫨': 15.1 // Shaking head. Although the version of emoji is 15, it is used to detect versions 15 and 15.1.
25
+ };
26
+ const BASELINE_EMOJI_WIDTH = 24;
27
+ /**
28
+ * The Emoji utilities plugin.
29
+ */
30
+ class EmojiUtils extends Plugin {
31
+ /**
32
+ * @inheritDoc
33
+ */
34
+ static get pluginName() {
35
+ return 'EmojiUtils';
36
+ }
37
+ /**
38
+ * @inheritDoc
39
+ */
40
+ static get isOfficialPlugin() {
41
+ return true;
42
+ }
43
+ /**
44
+ * Checks if the emoji is supported by verifying the emoji version supported by the system first.
45
+ * Then checks if emoji contains a zero width joiner (ZWJ), and if yes, then checks if it is supported by the system.
46
+ */
47
+ isEmojiSupported(item, emojiSupportedVersionByOs, container) {
48
+ const isEmojiVersionSupported = item.version <= emojiSupportedVersionByOs;
49
+ if (!isEmojiVersionSupported) {
50
+ return false;
51
+ }
52
+ if (!this.hasZwj(item.emoji)) {
53
+ return true;
54
+ }
55
+ return this.isEmojiZwjSupported(item, container);
56
+ }
57
+ /**
58
+ * Checks the supported emoji version by the OS, by sampling some representatives from different emoji releases.
59
+ */
60
+ getEmojiSupportedVersionByOs() {
61
+ return Object.entries(EMOJI_SUPPORT_LEVEL)
62
+ .reduce((currentVersion, [emoji, newVersion]) => {
63
+ if (newVersion > currentVersion && EmojiUtils._isEmojiSupported(emoji)) {
64
+ return newVersion;
65
+ }
66
+ return currentVersion;
67
+ }, 0);
68
+ }
69
+ /**
70
+ * Check for ZWJ (zero width joiner) character.
71
+ */
72
+ hasZwj(emoji) {
73
+ return emoji.includes('\u200d');
74
+ }
75
+ /**
76
+ * Checks whether the emoji is supported in the operating system.
77
+ */
78
+ isEmojiZwjSupported(item, container) {
79
+ const emojiWidth = this.getNodeWidth(container, item.emoji);
80
+ // On Windows, some supported emoji are ~50% bigger than the baseline emoji, but what we really want to guard
81
+ // against are the ones that are 2x the size, because those are truly broken (person with red hair = person with
82
+ // floating red wig, black cat = cat with black square, polar bear = bear with snowflake, etc.)
83
+ // So here we set the threshold at 1.8 times the size of the baseline emoji.
84
+ return emojiWidth < BASELINE_EMOJI_WIDTH * 1.8;
85
+ }
86
+ /**
87
+ * Returns the width of the provided node.
88
+ */
89
+ getNodeWidth(container, node) {
90
+ const span = document.createElement('span');
91
+ span.textContent = node;
92
+ container.appendChild(span);
93
+ const nodeWidth = span.offsetWidth;
94
+ container.removeChild(span);
95
+ return nodeWidth;
96
+ }
97
+ /**
98
+ * Creates a div for emoji width testing purposes.
99
+ */
100
+ createEmojiWidthTestingContainer() {
101
+ const container = document.createElement('div');
102
+ container.setAttribute('aria-hidden', 'true');
103
+ container.style.position = 'absolute';
104
+ container.style.left = '-9999px';
105
+ container.style.whiteSpace = 'nowrap';
106
+ container.style.fontSize = BASELINE_EMOJI_WIDTH + 'px';
107
+ return container;
108
+ }
109
+ /**
110
+ * Adds default skin tone property to each emoji. If emoji defines other skin tones, they are added as well.
111
+ */
112
+ normalizeEmojiSkinTone(item) {
113
+ const entry = {
114
+ ...item,
115
+ skins: {
116
+ default: item.emoji
117
+ }
118
+ };
119
+ if (item.skins) {
120
+ item.skins.forEach(skin => {
121
+ const skinTone = SKIN_TONE_MAP[skin.tone];
122
+ entry.skins[skinTone] = skin.emoji;
123
+ });
124
+ }
125
+ return entry;
126
+ }
127
+ /**
128
+ * Checks whether the emoji belongs to a group that is allowed.
129
+ */
130
+ isEmojiCategoryAllowed(item) {
131
+ // Category group=2 contains skin tones only, which we do not want to render.
132
+ return item.group !== 2;
133
+ }
134
+ }
135
+ /**
136
+ * A function used to determine if emoji is supported by detecting pixels.
137
+ *
138
+ * Referenced for unit testing purposes. Kept in a separate file because of licensing.
139
+ */
140
+ EmojiUtils._isEmojiSupported = isEmojiSupported;
141
+ export default EmojiUtils;
package/src/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export { default as Emoji } from './emoji.js';
9
9
  export { default as EmojiMention } from './emojimention.js';
10
10
  export { default as EmojiPicker } from './emojipicker.js';
11
11
  export { default as EmojiRepository } from './emojirepository.js';
12
+ export { default as EmojiUtils } from './emojiutils.js';
12
13
  export { default as EmojiCommand } from './emojicommand.js';
13
14
  export type { EmojiConfig } from './emojiconfig.js';
14
15
  import './augmentation.js';
package/src/index.js CHANGED
@@ -9,5 +9,6 @@ export { default as Emoji } from './emoji.js';
9
9
  export { default as EmojiMention } from './emojimention.js';
10
10
  export { default as EmojiPicker } from './emojipicker.js';
11
11
  export { default as EmojiRepository } from './emojirepository.js';
12
+ export { default as EmojiUtils } from './emojiutils.js';
12
13
  export { default as EmojiCommand } from './emojicommand.js';
13
14
  import './augmentation.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @license Copyright (c) 2023, Koala Interactive SAS
3
+ * For licensing, see https://github.com/koala-interactive/is-emoji-supported/blob/master/LICENSE.md
4
+ */
5
+ /**
6
+ * @module emoji/utils/isemojisupported
7
+ */
8
+ /**
9
+ * Checks if the two pixels parts are the same using canvas.
10
+ */
11
+ export default function isEmojiSupported(unicode: string): boolean;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @license Copyright (c) 2023, Koala Interactive SAS
3
+ * For licensing, see https://github.com/koala-interactive/is-emoji-supported/blob/master/LICENSE.md
4
+ */
5
+ /**
6
+ * @module emoji/utils/isemojisupported
7
+ */
8
+ /**
9
+ * Checks if the two pixels parts are the same using canvas.
10
+ */
11
+ export default function isEmojiSupported(unicode) {
12
+ const ctx = getCanvas();
13
+ /* istanbul ignore next -- @preserve */
14
+ if (!ctx) {
15
+ return false;
16
+ }
17
+ const CANVAS_HEIGHT = 25;
18
+ const CANVAS_WIDTH = 20;
19
+ const textSize = Math.floor(CANVAS_HEIGHT / 2);
20
+ // Initialize canvas context.
21
+ ctx.font = textSize + 'px Arial, Sans-Serif';
22
+ ctx.textBaseline = 'top';
23
+ ctx.canvas.width = CANVAS_WIDTH * 2;
24
+ ctx.canvas.height = CANVAS_HEIGHT;
25
+ ctx.clearRect(0, 0, CANVAS_WIDTH * 2, CANVAS_HEIGHT);
26
+ // Draw in red on the left.
27
+ ctx.fillStyle = '#FF0000';
28
+ ctx.fillText(unicode, 0, 22);
29
+ // Draw in blue on right.
30
+ ctx.fillStyle = '#0000FF';
31
+ ctx.fillText(unicode, CANVAS_WIDTH, 22);
32
+ const a = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT).data;
33
+ const count = a.length;
34
+ let i = 0;
35
+ // Search the first visible pixel.
36
+ for (; i < count && !a[i + 3]; i += 4)
37
+ ;
38
+ // No visible pixel.
39
+ /* istanbul ignore next -- @preserve */
40
+ if (i >= count) {
41
+ return false;
42
+ }
43
+ // Emoji has immutable color, so we check the color of the emoji in two different colors.
44
+ // the result show be the same.
45
+ const x = CANVAS_WIDTH + ((i / 4) % CANVAS_WIDTH);
46
+ const y = Math.floor(i / 4 / CANVAS_WIDTH);
47
+ const b = ctx.getImageData(x, y, 1, 1).data;
48
+ if (a[i] !== b[0] || a[i + 2] !== b[2]) {
49
+ return false;
50
+ }
51
+ //Some emojis consist of different ones, so they will show multiple characters if they are not supported.
52
+ /* istanbul ignore next -- @preserve */
53
+ if (ctx.measureText(unicode).width >= CANVAS_WIDTH) {
54
+ return false;
55
+ }
56
+ // Supported.
57
+ return true;
58
+ }
59
+ ;
60
+ function getCanvas() {
61
+ try {
62
+ return document.createElement('canvas').getContext('2d', { willReadFrequently: true });
63
+ }
64
+ catch {
65
+ /* istanbul ignore next -- @preserve */
66
+ return null;
67
+ }
68
+ }