From 54d76d6b56d90c17bc0ee8ab322fa05367018315 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 10 Apr 2022 15:25:07 -0500 Subject: [PATCH] Move emoji utils into its own module --- app/soapbox/components/ui/emoji/emoji.tsx | 28 +--------------- app/soapbox/utils/__tests__/emoji.test.ts | 39 +++++++++++++++++++++++ app/soapbox/utils/emoji.ts | 35 ++++++++++++++++++++ 3 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 app/soapbox/utils/__tests__/emoji.test.ts create mode 100644 app/soapbox/utils/emoji.ts diff --git a/app/soapbox/components/ui/emoji/emoji.tsx b/app/soapbox/components/ui/emoji/emoji.tsx index e9bcda2ed..59d00df2d 100644 --- a/app/soapbox/components/ui/emoji/emoji.tsx +++ b/app/soapbox/components/ui/emoji/emoji.tsx @@ -1,34 +1,8 @@ import React from 'react'; +import { removeVS16s, toCodePoints } from 'soapbox/utils/emoji'; import { joinPublicPath } from 'soapbox/utils/static'; -// Taken from twemoji-parser -// https://github.com/twitter/twemoji-parser/blob/a97ef3994e4b88316812926844d51c296e889f76/src/index.js -const removeVS16s = (rawEmoji: string): string => { - const vs16RegExp = /\uFE0F/g; - const zeroWidthJoiner = String.fromCharCode(0x200d); - return rawEmoji.indexOf(zeroWidthJoiner) < 0 ? rawEmoji.replace(vs16RegExp, '') : rawEmoji; -}; - -const toCodePoints = (unicodeSurrogates: string): string[] => { - const points = []; - let char = 0; - let previous = 0; - let i = 0; - while (i < unicodeSurrogates.length) { - char = unicodeSurrogates.charCodeAt(i++); - if (previous) { - points.push((0x10000 + ((previous - 0xd800) << 10) + (char - 0xdc00)).toString(16)); - previous = 0; - } else if (char > 0xd800 && char <= 0xdbff) { - previous = char; - } else { - points.push(char.toString(16)); - } - } - return points; -}; - interface IEmoji extends React.ImgHTMLAttributes { emoji: string, } diff --git a/app/soapbox/utils/__tests__/emoji.test.ts b/app/soapbox/utils/__tests__/emoji.test.ts new file mode 100644 index 000000000..ab1a4ddaa --- /dev/null +++ b/app/soapbox/utils/__tests__/emoji.test.ts @@ -0,0 +1,39 @@ +import { + removeVS16s, + toCodePoints, +} from '../emoji'; + +const ASCII_HEART = '❀'; // '\u2764\uFE0F' +const RED_HEART_RGI = '❀️'; // '\u2764' +const JOY = 'πŸ˜‚'; + +describe('removeVS16s()', () => { + it('removes Variation Selector-16 characters from emoji', () => { + // Sanity check + expect(ASCII_HEART).not.toBe(RED_HEART_RGI); + + // It normalizes an emoji with VS16s + expect(removeVS16s(RED_HEART_RGI)).toBe(ASCII_HEART); + + // Leaves a regular emoji alone + expect(removeVS16s(JOY)).toBe(JOY); + }); +}); + +describe('toCodePoints()', () => { + it('converts a plain emoji', () => { + expect(toCodePoints('πŸ˜‚')).toEqual(['1f602']); + }); + + it('converts a VS16 emoji', () => { + expect(toCodePoints(RED_HEART_RGI)).toEqual(['2764', 'fe0f']); + }); + + it('converts an ASCII character', () => { + expect(toCodePoints(ASCII_HEART)).toEqual(['2764']); + }); + + it('converts a sequence emoji', () => { + expect(toCodePoints('πŸ‡ΊπŸ‡Έ')).toEqual(['1f1fa', '1f1f8']); + }); +}); diff --git a/app/soapbox/utils/emoji.ts b/app/soapbox/utils/emoji.ts new file mode 100644 index 000000000..1d6da69d1 --- /dev/null +++ b/app/soapbox/utils/emoji.ts @@ -0,0 +1,35 @@ +// Taken from twemoji-parser +// https://github.com/twitter/twemoji-parser/blob/a97ef3994e4b88316812926844d51c296e889f76/src/index.js + +/** Remove Variation Selector-16 characters from emoji */ +// https://emojipedia.org/variation-selector-16/ +const removeVS16s = (rawEmoji: string): string => { + const vs16RegExp = /\uFE0F/g; + const zeroWidthJoiner = String.fromCharCode(0x200d); + return rawEmoji.indexOf(zeroWidthJoiner) < 0 ? rawEmoji.replace(vs16RegExp, '') : rawEmoji; +}; + +/** Convert emoji into an array of Unicode codepoints */ +const toCodePoints = (unicodeSurrogates: string): string[] => { + const points = []; + let char = 0; + let previous = 0; + let i = 0; + while (i < unicodeSurrogates.length) { + char = unicodeSurrogates.charCodeAt(i++); + if (previous) { + points.push((0x10000 + ((previous - 0xd800) << 10) + (char - 0xdc00)).toString(16)); + previous = 0; + } else if (char > 0xd800 && char <= 0xdbff) { + previous = char; + } else { + points.push(char.toString(16)); + } + } + return points; +}; + +export { + removeVS16s, + toCodePoints, +};