|
1 | | -const React = require('react') |
2 | | -const Emoji = require('./index'); |
3 | | -require('./default-svg.css') |
| 1 | +import React from 'react' |
| 2 | +import * as Emoji from './index' |
| 3 | +import './default-svg.css' |
4 | 4 |
|
5 | | -function reactReplaceEmojis(reactChild, options) { |
6 | | - let newReactChild; |
7 | | - if (Array.isArray(reactChild.props.children)) { |
8 | | - let newChildren = []; |
9 | | - for (let i in reactChild.props.children) { |
10 | | - if (!reactChild.props.children.hasOwnProperty(i)) continue |
11 | | - const child = reactChild.props.children[Number(i)]; |
12 | | - if (React.isValidElement(child)) { |
13 | | - newChildren[i] = reactReplaceEmojis(child, options) |
14 | | - } else { |
15 | | - newChildren[i] = React.cloneElement( |
16 | | - reactChild, |
17 | | - {}, |
18 | | - replaceEmojis(child, options) |
19 | | - ) |
20 | | - } |
| 5 | +export default function reactReplaceEmojis(reactElement, options) { |
| 6 | + // shortcut -> no children means nothing to replace |
| 7 | + if (!reactElement.props.children) return reactElement |
| 8 | + let newReactElement, |
| 9 | + hasMultipleChildren = Array.isArray(reactElement.props.children), |
| 10 | + newChildren = [] |
| 11 | + |
| 12 | + // if element has multiple children call the recursive function once for each |
| 13 | + if (hasMultipleChildren) { |
| 14 | + for (let i in reactElement.props.children) { |
| 15 | + if (!reactElement.props.children.hasOwnProperty(i)) continue |
| 16 | + const child = reactElement.props.children[Number(i)]; |
| 17 | + newChildren[i] = _applyCorrectReplace(child, options) |
21 | 18 | } |
22 | | - newReactChild = React.cloneElement( |
23 | | - reactChild, |
24 | | - {}, |
25 | | - newChildren |
26 | | - ); |
27 | | - } else { |
28 | | - newReactChild = React.cloneElement( |
29 | | - reactChild, |
30 | | - {}, |
31 | | - replaceEmojis(reactChild.props.children, options) |
32 | | - ) |
33 | | - } |
34 | | - return newReactChild; |
| 19 | + } else // if element has only one child call the recursive function directly |
| 20 | + newChildren = _applyCorrectReplace(reactElement.props.children, options) |
| 21 | + |
| 22 | + newReactElement = React.cloneElement( |
| 23 | + reactElement, |
| 24 | + {}, |
| 25 | + newChildren |
| 26 | + ); |
| 27 | + return newReactElement; |
35 | 28 | } |
36 | 29 |
|
37 | | -function replaceEmojis(string, options) { |
| 30 | +const _applyCorrectReplace = (child, options) => |
| 31 | + React.isValidElement(child) |
| 32 | + ? reactReplaceEmojis(child, options) |
| 33 | + : replaceEmojis(child, options) |
| 34 | + |
| 35 | +export function replaceEmojis(string, options) { |
| 36 | + if (!string) return; |
| 37 | + let array = [string] |
| 38 | + |
38 | 39 | options = { |
39 | 40 | size: typeof options?.size === 'string' ? options.size : undefined, |
40 | 41 | outline: typeof options?.outline === 'boolean' ? options.outline : undefined |
41 | | - } |
42 | | - if (!string) return; |
43 | | - const emojis = string.match(/[\p{Emoji}\u200d\ufe0f]+/gu); |
44 | | - if (!emojis) return string; |
| 42 | + }; |
45 | 43 |
|
46 | | - string = string.split(/([\p{Emoji}\u200d\ufe0f]+)/gu); |
| 44 | + /* |
| 45 | + * matches all emojis matches all attached components |
| 46 | + * \p{Extended_Pictographic}[\u{1f3fb}-\u{1f3ff}\u{1f9b0}-\u{1f9b3}]? un-matches "text style" |
| 47 | + * (\u200d\p{Extended_Pictographic}[\u{1f3fb}-\u{1f3ff}\u{1f9b0}-\u{1f9b3}]?)*[\u2640\u2642]?\ufe0f?(?!\ufe0e)/gu |
| 48 | + * matches all joined (ZWJ) emojis with all attached components matches attached gender |
| 49 | + */ |
47 | 50 |
|
48 | | - // replace emojis with SVGs |
49 | | - emojis.forEach((emoji, i) => { |
50 | | - // get the char codes of the emojis |
51 | | - let unicode = ""; |
| 51 | + const regex = /\p{Extended_Pictographic}[\u{1f3fb}-\u{1f3ff}\u{1f9b0}-\u{1f9b3}]?(\u200d\p{Extended_Pictographic}[\u{1f3fb}-\u{1f3ff}\u{1f9b0}-\u{1f9b3}]?)*[\u2640\u2642]?\ufe0f?(?!\ufe0e)/gu; |
| 52 | + let m, j = 0; |
52 | 53 |
|
53 | | - function getNextChar(pointer) { |
54 | | - const subUnicode = emoji.codePointAt(pointer); |
55 | | - if (!subUnicode) return; |
56 | | - if (!(subUnicode >= 56320 && subUnicode <= 57343)) { // 56320-57343: Low Surrogates Character |
57 | | - unicode += '-' + subUnicode.toString(16); |
58 | | - } |
59 | | - getNextChar(++pointer); |
| 54 | + while ((m = regex.exec(string)) !== null) { |
| 55 | + // This is necessary to avoid infinite loops with zero-width matches |
| 56 | + if (m.index === regex.lastIndex) { |
| 57 | + regex.lastIndex++; |
60 | 58 | } |
61 | 59 |
|
62 | | - getNextChar(0); |
63 | | - unicode = unicode.substr(1); |
| 60 | + // find the code for the emoji |
| 61 | + let emojiName = 'U', done = false |
| 62 | + for (let i = 0; !done; i++) { |
| 63 | + let subUnicode = m[0].codePointAt(i) |
| 64 | + // dismiss low surrogates characters (56320-57343) |
| 65 | + if ((subUnicode >= 56320 && subUnicode <= 57343)) continue |
| 66 | + emojiName += '_' + subUnicode?.toString(16).toUpperCase() |
64 | 67 |
|
65 | | - const emojiName = `U_${unicode.toUpperCase().replace(/-/g, '_')}`; |
| 68 | + // check if is done: if this hexcode is longer than 4, check the next but one codepoint |
| 69 | + done = m[0].codePointAt(i)?.toString(16).length > 4 |
| 70 | + ? !m[0].codePointAt(i + 2) |
| 71 | + : !m[0].codePointAt(i + 1) |
| 72 | + } |
66 | 73 |
|
67 | | - const emojiIndex = string.indexOf(emoji); |
68 | 74 | let emojiSvg = Emoji[emojiName]; |
69 | | - |
70 | | - options.key = i |
71 | | - |
| 75 | + options.key = j |
| 76 | + j++ |
72 | 77 | if (emojiSvg) { |
73 | | - string[emojiIndex] = React.createElement(emojiSvg, options); |
| 78 | + // gets last string ['String with {Emoji} and {Emoji} in it'] -> 'String with {Emoji} and {Emoji} in it' |
| 79 | + let workingString = array.pop() |
| 80 | + // 'String with {Emoji} and {Emoji} in it' -> ['String with ', ' and ', ' in it'] |
| 81 | + workingString = workingString.split(m[0]) |
| 82 | + // [] -> ['String with '] |
| 83 | + // ['String with ', ' and ', ' in it'] -> [' and ', ' in it'] |
| 84 | + array.push(workingString.shift()) |
| 85 | + // ['String with '] -> ['String with ', <Emoji>] |
| 86 | + array.push(React.createElement(emojiSvg, options)) |
| 87 | + // [' and ', ' in it'] -> ' and {Emoji} in it' |
| 88 | + // ['String with ', <Emoji>] -> ['String with ', <Emoji>, ' and {Emoji} in it'] |
| 89 | + array.push(workingString.join(m[0])) |
74 | 90 | } else { |
75 | 91 | console.warn('SVG not found: ' + emojiName); |
76 | 92 | } |
77 | | - }) |
78 | | - |
79 | | - // return converted react HTML |
80 | | - return string; |
81 | | -} |
| 93 | + } |
82 | 94 |
|
83 | | -reactReplaceEmojis.replaceEmojis = replaceEmojis |
84 | | -for (const key in Emoji) { |
85 | | - if (!Emoji.hasOwnProperty(key)) continue |
86 | | - reactReplaceEmojis[key] = Emoji[key] |
| 95 | + return array; |
87 | 96 | } |
88 | 97 |
|
89 | | -module.exports = reactReplaceEmojis |
| 98 | +export * from './index'; |
0 commit comments