From e5dbd00cba1c6c1cb339c533527ec5df9b8dcea1 Mon Sep 17 00:00:00 2001 From: Vjacheslav Trushkin Date: Sat, 24 Dec 2022 20:44:29 +0200 Subject: [PATCH] chore(utils): restructure emoji code to handle sequences with custom properties, fix errors --- packages/utils/package.json | 18 +- packages/utils/src/emoji/cleanup.ts | 7 - packages/utils/src/emoji/regex/create.ts | 13 +- packages/utils/src/emoji/test/components.ts | 92 +- packages/utils/src/emoji/test/copy.ts | 275 ----- packages/utils/src/emoji/test/missing.ts | 154 +++ packages/utils/src/emoji/test/name.ts | 320 +---- packages/utils/src/emoji/test/parse.ts | 164 ++- packages/utils/src/emoji/test/similar.ts | 88 ++ packages/utils/src/emoji/test/tree.ts | 199 ++++ packages/utils/src/emoji/test/variations.ts | 104 +- packages/utils/src/index.ts | 8 +- packages/utils/tests/emoji-cleanup-test.ts | 6 +- .../tests/emoji-optional-variations-test.ts | 57 +- packages/utils/tests/emoji-testdata-test.ts | 1039 +++++++++-------- 15 files changed, 1336 insertions(+), 1208 deletions(-) delete mode 100644 packages/utils/src/emoji/test/copy.ts create mode 100644 packages/utils/src/emoji/test/missing.ts create mode 100644 packages/utils/src/emoji/test/similar.ts create mode 100644 packages/utils/src/emoji/test/tree.ts diff --git a/packages/utils/package.json b/packages/utils/package.json index 104bd48..7241848 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -142,10 +142,10 @@ "import": "./lib/emoji/test/components.mjs", "types": "./lib/emoji/test/components.d.ts" }, - "./lib/emoji/test/copy": { - "require": "./lib/emoji/test/copy.cjs", - "import": "./lib/emoji/test/copy.mjs", - "types": "./lib/emoji/test/copy.d.ts" + "./lib/emoji/test/missing": { + "require": "./lib/emoji/test/missing.cjs", + "import": "./lib/emoji/test/missing.mjs", + "types": "./lib/emoji/test/missing.d.ts" }, "./lib/emoji/test/name": { "require": "./lib/emoji/test/name.cjs", @@ -157,6 +157,16 @@ "import": "./lib/emoji/test/parse.mjs", "types": "./lib/emoji/test/parse.d.ts" }, + "./lib/emoji/test/tree": { + "require": "./lib/emoji/test/tree.cjs", + "import": "./lib/emoji/test/tree.mjs", + "types": "./lib/emoji/test/tree.d.ts" + }, + "./lib/emoji/test/similar": { + "require": "./lib/emoji/test/similar.cjs", + "import": "./lib/emoji/test/similar.mjs", + "types": "./lib/emoji/test/similar.d.ts" + }, "./lib/emoji/test/variations": { "require": "./lib/emoji/test/variations.cjs", "import": "./lib/emoji/test/variations.mjs", diff --git a/packages/utils/src/emoji/cleanup.ts b/packages/utils/src/emoji/cleanup.ts index ce20aad..439b81b 100644 --- a/packages/utils/src/emoji/cleanup.ts +++ b/packages/utils/src/emoji/cleanup.ts @@ -92,13 +92,6 @@ export function joinEmojiSequences( return results; } -/** - * Remove variations - */ -export function removeEmojiVariations(sequence: number[]): number[] { - return sequence.filter((code) => code !== vs16Emoji); -} - /** * Get unqualified sequence */ diff --git a/packages/utils/src/emoji/regex/create.ts b/packages/utils/src/emoji/regex/create.ts index 54dd005..11a872a 100644 --- a/packages/utils/src/emoji/regex/create.ts +++ b/packages/utils/src/emoji/regex/create.ts @@ -1,5 +1,6 @@ import { getSequenceFromEmojiStringOrKeyword } from '../cleanup'; import { convertEmojiSequenceToUTF32 } from '../convert'; +import type { EmojiTestData } from '../test/parse'; import { getQualifiedEmojiVariations } from '../test/variations'; import { createEmojisTree, parseEmojiTree } from './tree'; @@ -38,7 +39,7 @@ export function createOptimisedRegexForEmojiSequences( */ export function createOptimisedRegex( emojis: (string | number[])[], - testData?: number[][] + testData?: EmojiTestData ): string { // Convert to numbers let sequences = emojis.map((item) => @@ -48,7 +49,15 @@ export function createOptimisedRegex( ); // Add variations - sequences = getQualifiedEmojiVariations(sequences, testData); + // Temporary convert to object with 'sequence' property + sequences = getQualifiedEmojiVariations( + sequences.map((sequence) => { + return { + sequence, + }; + }), + testData + ).map((item) => item.sequence); // Parse return createOptimisedRegexForEmojiSequences(sequences); diff --git a/packages/utils/src/emoji/test/components.ts b/packages/utils/src/emoji/test/components.ts index 252779e..9234af6 100644 --- a/packages/utils/src/emoji/test/components.ts +++ b/packages/utils/src/emoji/test/components.ts @@ -1,5 +1,6 @@ import { emojiComponents, EmojiComponentType } from '../data'; -import type { EmojiSequenceToStringCallback, EmojiTestDataItem } from './parse'; +import { getEmojiSequenceKeyword } from '../format'; +import type { EmojiTestData, EmojiTestDataItem } from './parse'; export interface EmojiTestDataComponentsMap { // Keywords @@ -20,8 +21,7 @@ export interface EmojiTestDataComponentsMap { * Map components from test data */ export function mapEmojiTestDataComponents( - testSequences: Record, - convert: EmojiSequenceToStringCallback + testSequences: EmojiTestData ): EmojiTestDataComponentsMap { const results: EmojiTestDataComponentsMap = { converted: new Map(), @@ -35,7 +35,7 @@ export function mapEmojiTestDataComponents( const type = key as EmojiComponentType; const range = emojiComponents[type]; for (let number = range[0]; number <= range[1]; number++) { - const keyword = convert([number]); + const keyword = getEmojiSequenceKeyword([number]); const item = testSequences[keyword]; if (!item) { throw new Error( @@ -57,3 +57,87 @@ export function mapEmojiTestDataComponents( return results; } + +/** + * Sequence with components + */ +export type EmojiSequenceWithComponents = (EmojiComponentType | number)[]; + +/** + * Convert to string + */ +export function emojiSequenceWithComponentsToString( + sequence: EmojiSequenceWithComponents +): string { + return sequence + .map((item) => (typeof item === 'number' ? item.toString(16) : item)) + .join('-'); +} + +/** + * Entry in sequence + */ +export interface EmojiSequenceComponentEntry { + // Index in sequence + index: number; + + // Component type + type: EmojiComponentType; +} + +/** + * Find variations in sequence + */ +export function findEmojiComponentsInSequence( + sequence: number[] +): EmojiSequenceComponentEntry[] { + const components: EmojiSequenceComponentEntry[] = []; + + for (let index = 0; index < sequence.length; index++) { + const code = sequence[index]; + for (const key in emojiComponents) { + const type = key as EmojiComponentType; + const range = emojiComponents[type]; + if (code >= range[0] && code < range[1]) { + components.push({ + index, + type, + }); + break; + } + } + } + + return components; +} + +/** + * Component values + */ +export type EmojiSequenceComponentValues = Partial< + Record +>; + +/** + * Replace components in sequence + */ +export function replaceEmojiComponentsInCombinedSequence( + sequence: EmojiSequenceWithComponents, + values: EmojiSequenceComponentValues +): number[] { + const indexes: Record = { + 'hair-style': 0, + 'skin-tone': 0, + }; + return sequence.map((item) => { + if (typeof item === 'number') { + return item; + } + const index = indexes[item]++; + const list = values[item]; + if (!list || !list.length) { + throw new Error(`Cannot replace ${item}: no valid values provided`); + } + return list[index >= list.length ? list.length - 1 : index]; + }); +} diff --git a/packages/utils/src/emoji/test/copy.ts b/packages/utils/src/emoji/test/copy.ts deleted file mode 100644 index e7cdef5..0000000 --- a/packages/utils/src/emoji/test/copy.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { getUnqualifiedEmojiSequence } from '../cleanup'; -import { emojiComponents, EmojiComponentType } from '../data'; -import { getEmojiSequenceKeyword } from '../format'; -import { mapEmojiTestDataComponents } from './components'; -import { EmojiComponentsMapItem, getEmojiComponentsMap } from './name'; -import { EmojiTestDataItem, mapEmojiTestDataBySequence } from './parse'; - -type SequenceType = 'qualified' | 'unqualified'; -interface SequenceData { - type: SequenceType; - sequence: number[]; - key: string; -} -type Sequences = Record; - -type ComponentsIteration = Required>; - -/** - * Get components iteration - */ -function addToComponentsIteration( - components: ComponentsIteration, - attr: EmojiComponentType, - value: number -): ComponentsIteration { - const result: ComponentsIteration = { - 'hair-style': components['hair-style'].slice(0), - 'skin-tone': components['skin-tone'].slice(0), - }; - result[attr].push(value); - return result; -} - -/** - * Replace components with number in sequence - */ -function addComponentsToSequence( - sequence: (EmojiComponentType | number)[], - components: ComponentsIteration -): number[] { - const indexes: Required> = { - 'hair-style': 0, - 'skin-tone': 0, - }; - return sequence.map((value) => { - if (typeof value === 'number') { - return value; - } - const index = indexes[value]++; - return components[value][index]; - }); -} - -/** - * Get sequence variations - */ -function getSequence(sequence: number[]): Sequences { - const qualified: SequenceData = { - type: 'qualified', - sequence, - key: getEmojiSequenceKeyword(sequence), - }; - - const unqualifiedSequence = getUnqualifiedEmojiSequence(sequence); - const unqualified: SequenceData = - unqualifiedSequence.length === sequence.length - ? { - ...qualified, - type: 'unqualified', - } - : { - type: 'unqualified', - sequence: unqualifiedSequence, - key: getEmojiSequenceKeyword(unqualifiedSequence), - }; - - return { - qualified, - unqualified, - }; -} - -/** - * Item to copy - */ -interface EmojiSequenceToCopy { - // Source: sequence and name - source: number[]; - sourceName: string; - - // Target: sequence and name - target: number[]; - targetName: string; -} - -/** - * Get sequences - * - * Returns map, where key is item to add, value is source - */ -export function getEmojisSequencesToCopy( - sequences: number[][], - testData: EmojiTestDataItem[] -): EmojiSequenceToCopy[] { - const results: EmojiSequenceToCopy[] = []; - - // Prepare stuff - const componentsMap = mapEmojiTestDataComponents( - mapEmojiTestDataBySequence(testData, getEmojiSequenceKeyword), - getEmojiSequenceKeyword - ); - const componentsMapItems = getEmojiComponentsMap(testData, componentsMap); - - // Get all existing emojis - const existingItems = Object.create(null) as Record; - const copiedItems = Object.create(null) as Record; - sequences.forEach((sequence) => { - existingItems[getEmojiSequenceKeyword(sequence)] = sequence; - }); - - // Check if item exists - const itemExists = (sequence: Sequences): SequenceType | undefined => { - return existingItems[sequence.qualified.key] - ? 'qualified' - : existingItems[sequence.unqualified.key] - ? 'unqualified' - : void 0; - }; - const itemWasCopied = (sequence: Sequences): SequenceType | undefined => { - return copiedItems[sequence.qualified.key] - ? 'qualified' - : copiedItems[sequence.unqualified.key] - ? 'unqualified' - : void 0; - }; - - // Copy item - const addToCopy = ( - source: SequenceData, - sourceName: string, - target: SequenceData, - targetName: string - ) => { - copiedItems[target.key] = target.sequence; - results.push({ - source: source.sequence, - sourceName, - target: target.sequence, - targetName, - }); - }; - - // Get name - const getName = ( - item: EmojiComponentsMapItem, - components: ComponentsIteration - ) => { - let name = item.name; - for (const key in emojiComponents) { - const type = key as EmojiComponentType; - for (let i = 0; i < components[type].length; i++) { - const num = components[type][i]; - const text = componentsMap.names.get(num) as string; - name = name.replace(`{${type}-${i}}`, text); - } - } - return name; - }; - - // Check item and its children - const checkItem = ( - parentItem: EmojiComponentsMapItem, - parentSequence: SequenceData, - parentComponents: ComponentsIteration, - onlyIfExists = true - ) => { - const children = parentItem.children; - if (!children) { - return; - } - for (const key in emojiComponents) { - const type = key as EmojiComponentType; - if (children[type]) { - // Check emojis - const childItem = children[type]; - const range = emojiComponents[type]; - - // Add each item in range - for (let num = range[0]; num < range[1]; num++) { - const components = addToComponentsIteration( - parentComponents, - type, - num - ); - const sequence = addComponentsToSequence( - childItem.sequence, - components - ); - const sequences = getSequence(sequence); - - // Check if already exists - const existingSequence = itemExists(sequences); - if (existingSequence) { - // Already exists - checkItem( - childItem, - sequences[existingSequence], - components, - onlyIfExists - ); - continue; - } - - // Check if was copied - let copiedSequence = itemWasCopied(sequences); - if (copiedSequence && onlyIfExists) { - // Cannot parse nested items yet - continue; - } - - // Copy - if (!copiedSequence) { - // Copy sequence - copiedSequence = parentSequence.type; - addToCopy( - parentSequence, - getName(parentItem, parentComponents), - sequences[copiedSequence], - getName(childItem, components) - ); - } - - // Check child items - checkItem( - childItem, - sequences[copiedSequence], - components, - onlyIfExists - ); - } - } - } - }; - - // Check all items - componentsMapItems.forEach((mainItem) => { - const sequence = getSequence(mainItem.sequence as number[]); - const type = itemExists(sequence); - if (!type) { - // Base emoji is missing: nothing to do - return; - } - - checkItem( - mainItem, - sequence[type], - { - 'hair-style': [], - 'skin-tone': [], - }, - true - ); - checkItem( - mainItem, - sequence[type], - { - 'hair-style': [], - 'skin-tone': [], - }, - false - ); - }); - - return results; -} diff --git a/packages/utils/src/emoji/test/missing.ts b/packages/utils/src/emoji/test/missing.ts new file mode 100644 index 0000000..0f744ff --- /dev/null +++ b/packages/utils/src/emoji/test/missing.ts @@ -0,0 +1,154 @@ +import { getUnqualifiedEmojiSequence } from '../cleanup'; +import { emojiComponents, EmojiComponentType } from '../data'; +import { getEmojiSequenceKeyword } from '../format'; +import { + replaceEmojiComponentsInCombinedSequence, + EmojiSequenceComponentValues, +} from './components'; +import type { EmojiComponentsTree, EmojiComponentsTreeItem } from './tree'; + +/** + * Base type to extend + */ +interface BaseSequenceItem { + sequence: number[]; + + // If present, will be set in value too + // String version of sequence without variation unicode + sequenceKey?: string; +} + +/** + * Find missing emojis + * + * Result includes missing items, which are extended from items that needs to + * be copied. To identify which emojis to copy, source object should include + * something like `iconName` key that points to icon sequence represents. + */ +export function findMissingEmojis( + sequences: T[], + testDataTree: EmojiComponentsTree +): T[] { + const results: T[] = []; + + const existingItems = Object.create(null) as Record; + const copiedItems = Object.create(null) as Record; + + // Get all existing sequences + sequences.forEach((item) => { + const sequence = getUnqualifiedEmojiSequence(item.sequence); + const key = getEmojiSequenceKeyword(sequence); + if ( + !existingItems[key] || + // If multiple matches for same sequence exist, use longest version + existingItems[key].sequence.length < item.sequence.length + ) { + existingItems[key] = item; + } + }); + + // Function to iterate sequences + const iterate = ( + type: EmojiComponentType, + parentTree: EmojiComponentsTreeItem, + parentValues: Required, + parentItem: T, + deep: boolean + ) => { + const childTree = parentTree.children?.[type]; + if (!childTree) { + return; + } + + // Sequence exists + const range = emojiComponents[type]; + for (let number = range[0]; number < range[1]; number++) { + // Create new values + const values: Required = { + 'hair-style': [...parentValues['hair-style']], + 'skin-tone': [...parentValues['skin-tone']], + }; + values[type].push(number); + + // Generate sequence + const sequence = replaceEmojiComponentsInCombinedSequence( + childTree.item.sequence, + values + ); + const key = getEmojiSequenceKeyword( + getUnqualifiedEmojiSequence(sequence) + ); + + // Get item + const oldItem = existingItems[key]; + let item: T; + if (oldItem) { + // Exists + item = oldItem; + } else { + // Check if already created + item = copiedItems[key]; + if (!item) { + // Create new item + item = { + ...parentItem, + sequence, + }; + if (item.sequenceKey) { + item.sequenceKey = key; + } + copiedItems[key] = item; + results.push(item); + } + } + + // Check child elements + if (deep || oldItem) { + for (const key in values) { + iterate( + key as EmojiComponentType, + childTree, + values, + item, + deep + ); + } + } + } + }; + + // Function to check tree item + const parse = (key: string, deep: boolean) => { + const treeItem = testDataTree[key]; + const sequenceKey = treeItem.item.sequenceKey; + + // Check if item actually exists + const rootItem = existingItems[sequenceKey]; + if (!rootItem) { + return; + } + + // Parse tree + const values: Required = { + 'skin-tone': [], + 'hair-style': [], + }; + for (const key in values) { + iterate( + key as EmojiComponentType, + treeItem, + values, + rootItem, + deep + ); + } + }; + + // Shallow check first, then full check + for (const key in testDataTree) { + parse(key, false); + parse(key, true); + } + + return results; +} diff --git a/packages/utils/src/emoji/test/name.ts b/packages/utils/src/emoji/test/name.ts index 707ddef..d99d8fb 100644 --- a/packages/utils/src/emoji/test/name.ts +++ b/packages/utils/src/emoji/test/name.ts @@ -1,19 +1,14 @@ -import { emojiComponents, EmojiComponentType, vs16Emoji } from '../data'; -import { getEmojiSequenceKeyword } from '../format'; -import { +import { emojiComponents, EmojiComponentType } from '../data'; +import type { + EmojiSequenceComponentEntry, EmojiTestDataComponentsMap, - mapEmojiTestDataComponents, } from './components'; -import { EmojiTestDataItem, mapEmojiTestDataBySequence } from './parse'; - -interface EmojiNameVariation { - // Index in sequence - index: number; - - // Component type - type: EmojiComponentType; -} +/** + * Split emoji name in base name and variations + * + * Variations are also split in strings and emoji components with indexes pointing to sequence + */ export interface SplitEmojiName { // Base name base: string; @@ -22,7 +17,7 @@ export interface SplitEmojiName { key: string; // Variations - variations?: (string | EmojiNameVariation)[]; + variations?: (string | EmojiSequenceComponentEntry)[]; // Number of components components?: number; @@ -55,293 +50,52 @@ export function splitEmojiNameVariations( } // Get variations - let startIndex = 0; - let components = 0; - const keyParts: string[] = []; - const variations = parts + const variations: (string | EmojiSequenceComponentEntry)[] = parts .join(nameSplit) .split(variationSplit) - .map((text) => { + .filter((text) => { const type = componentsData.types[text]; if (!type) { // Not a component - if (!ignoredVariations.has(text)) { - keyParts.push(text); - } - return text; + return !ignoredVariations.has(text); } // Component - const range = emojiComponents[type]; - while (startIndex < sequence.length) { - const num = sequence[startIndex]; - startIndex++; - if (num >= range[0] && num <= range[1]) { - // Got range match - components++; - return { - index: startIndex - 1, - type, - }; - } - } - - // Ran out of sequence - throw new Error( - `Cannot find variation in sequence for "${name}", [${sequence.join( - ' ' - )}]` - ); + return false; }); const key = base + - (keyParts.length ? nameSplit + keyParts.join(variationSplit) : ''); - - return { + (variations.length ? nameSplit + variations.join(variationSplit) : ''); + const result: SplitEmojiName = { base, key, - variations, - components, - }; -} - -/** - * Merge component types - */ -function mergeComponentTypes(value: EmojiComponentType[]) { - return '[' + value.join(',') + ']'; -} - -type ComponentsCount = Required>; - -function mergeComponentsCount(value: ComponentsCount) { - const keys: EmojiComponentType[] = []; - for (const key in emojiComponents) { - const type = key as EmojiComponentType; - for (let i = 0; i < value[type]; i++) { - keys.push(type); - } - } - return keys.length ? mergeComponentTypes(keys) : ''; -} - -/** - * Map item - */ -type EmojiComponentsMapItemSequence = (EmojiComponentType | number)[]; -export interface EmojiComponentsMapItem { - // Name, with `{skin-tone-1}` (type + index) placeholders - name: string; - - // Sequence - sequence: EmojiComponentsMapItemSequence; - - // Child element(s) - children?: Record; -} - -/** - * Get map of emoji components - * - * Result includes emoji sequences with largest number of characters (usually fully-qualified) - * Only sequences with components are returned - */ -export function getEmojiComponentsMap( - testData: EmojiTestDataItem[], - componentsMap?: EmojiTestDataComponentsMap -): EmojiComponentsMapItem[] { - // Prepare stuff - const components = - componentsMap || - mapEmojiTestDataComponents( - mapEmojiTestDataBySequence(testData, getEmojiSequenceKeyword), - getEmojiSequenceKeyword - ); - - // Function to clean sequence - const cleanSequence = (sequence: number[]): string => { - return getEmojiSequenceKeyword( - sequence.filter( - (num) => num !== vs16Emoji && !components.converted.has(num) - ) - ); }; - // Map all items - interface SplitListItem { - item: EmojiTestDataItem; - split: SplitEmojiName; - components: ComponentsCount; - } - type SplitList = Record; - const splitData = Object.create(null) as Record; - const defaultSplitDataKey = 'default'; - - testData.forEach((item) => { - // Split it - const split = splitEmojiNameVariations( - item.name, - item.sequence, - components - ); - const parent = - splitData[split.key] || - (splitData[split.key] = Object.create(null) as SplitList); - - // Create unique key based on component types - let sequenceKey = defaultSplitDataKey; - const itemComponents: ComponentsCount = { - 'hair-style': 0, - 'skin-tone': 0, - }; - if (split.components) { - split.variations?.forEach((item) => { - if (typeof item !== 'string') { - itemComponents[item.type]++; - } - }); - sequenceKey = - mergeComponentsCount(itemComponents) || defaultSplitDataKey; - } - - // Get item if already exists - const prevItem = parent[sequenceKey]; - if (!prevItem) { - parent[sequenceKey] = { - item, - split, - components: itemComponents, - }; - return; - } - - if ( - cleanSequence(prevItem.item.sequence) !== - cleanSequence(item.sequence) - ) { - // console.log(prevItem.item); - // console.log(item); - throw new Error(`Mismatched items with same key: ${sequenceKey}`); - } - - if (item.sequence.length > prevItem.item.sequence.length) { - // Keep longer sequence - parent[sequenceKey] = { - item, - split, - components: itemComponents, - }; - } - }); - - // Parse all items - const results: EmojiComponentsMapItem[] = []; - for (const key in splitData) { - const items = splitData[key]; - - // Function to get item - const getItem = ( - components: ComponentsCount - ): EmojiComponentsMapItem | undefined => { - const key = mergeComponentsCount(components) || defaultSplitDataKey; - const item = items[key]; - if (!item) { - return; - } - - const split = item.split; - const variations = split.variations; - - // Get sequence - const sequence = item.item.sequence.slice( - 0 - ) as EmojiComponentsMapItemSequence; - variations?.forEach((chunk) => { - if (typeof chunk === 'string') { - return; - } - sequence[chunk.index] = chunk.type; - }); - - // Get name - const counter: ComponentsCount = { - 'hair-style': 0, - 'skin-tone': 0, - }; - const nameVariations = variations?.map((chunk) => { - if (typeof chunk === 'string') { - return chunk; - } - const count = counter[chunk.type]++; - if (components[chunk.type] < count) { - throw new Error('Bad variations order'); - } - return `{${chunk.type}-${count}}`; - }); - const name = - split.base + - (nameVariations?.length - ? nameSplit + nameVariations.join(variationSplit) - : ''); - - return { - name, - sequence, - }; - }; - - const checkChildren = ( - parent: EmojiComponentsMapItem, - components: ComponentsCount - ): boolean => { - // Attempt to add each type - let found = false; - for (const key in emojiComponents) { - const type = key as EmojiComponentType; - - // Find child item - const childComponents = { - ...components, - }; - childComponents[type]++; - const childItem = getItem(childComponents); - - // Get sequence for child item - if (childItem) { - found = true; - - // Add child item, check its children - const children = - parent.children || - (parent.children = {} as Record< - EmojiComponentType, - EmojiComponentsMapItem - >); - children[type] = childItem; - checkChildren(childItem, childComponents); - } - } - return found; - }; - - // Get main item - const mainItem = getItem({ - 'hair-style': 0, - 'skin-tone': 0, - }); - if (mainItem) { - if ( - checkChildren(mainItem, { - 'hair-style': 0, - 'skin-tone': 0, - }) - ) { - // Found item with children - results.push(mainItem); + // Check sequence for variations + let components = 0; + for (let index = 0; index < sequence.length; index++) { + const num = sequence[index]; + for (const key in emojiComponents) { + const type = key as EmojiComponentType; + const range = emojiComponents[type]; + if (num >= range[0] && num < range[1]) { + // Within range + variations.push({ + index, + type, + }); + components++; } } } - return results; + if (variations.length) { + result.variations = variations; + } + if (components) { + result.components = components; + } + + return result; } diff --git a/packages/utils/src/emoji/test/parse.ts b/packages/utils/src/emoji/test/parse.ts index 0d8f2ca..28b488b 100644 --- a/packages/utils/src/emoji/test/parse.ts +++ b/packages/utils/src/emoji/test/parse.ts @@ -21,21 +21,21 @@ const allowedStatus: Set = new Set([ ]); /** - * Callback for converting sequence to string + * Base item */ -export type EmojiSequenceToStringCallback = (value: number[]) => string; - -/** - * Test data item - */ -export interface EmojiTestDataItem { +export interface BaseEmojiTestDataItem { // Group and subgroup group: string; subgroup: string; - // Code points as string, lower case, dash separated - code: string; + // Version when emoji was added + version: string; +} +/** + * Test data item + */ +export interface EmojiTestDataItem extends BaseEmojiTestDataItem { // Code points as numbers, UTF-32 sequence: number[]; @@ -45,20 +45,62 @@ export interface EmojiTestDataItem { // Status status: EmojiStatus; - // Version when emoji was added - version: string; - // Emoji name name: string; } +export type EmojiTestData = Record; + +/** + * Get qualified variations from parsed test file + * + * Key is unqualified emoji, value is longest fully qualified emoji + */ +function getQualifiedTestData(data: EmojiTestData): EmojiTestData { + const results = Object.create(null) as EmojiTestData; + + for (const key in data) { + const item = data[key]; + const sequence = getUnqualifiedEmojiSequence(item.sequence); + const shortKey = getEmojiSequenceKeyword(sequence); + + // Check if values mismatch, set results to longest value + if ( + !results[shortKey] || + results[shortKey].sequence.length < sequence.length + ) { + results[shortKey] = item; + } + } + + return results; +} + /** * Get all emoji sequences from test file * - * Returns all emojis as UTF-32 sequences + * Returns all emojis as UTF-32 sequences, where: + * key = unqualified sequence (without \uFE0F) + * value = qualified sequence (with \uFE0F) + * + * Duplicate items that have different versions with and without \uFE0F are + * listed only once, with unqualified sequence as key and longest possible + * qualified sequence as value + * + * Example of 3 identical entries: + * '1F441 FE0F 200D 1F5E8 FE0F' + * '1F441 200D 1F5E8 FE0F' + * '1F441 FE0F 200D 1F5E8' + * '1F441 200D 1F5E8' + * + * Out of these entries, only one item will be returned with: + * key = '1f441-200d-1f5e8' (converted to lower case, separated with dash) + * value.sequence = [0x1F441, 0xFE0F, 0x200D, 0x1F5E8, 0xFE0F] + * value.status = 'fully-qualified' + * other properties in value are identical for all versions */ -export function parseEmojiTestFile(data: string): EmojiTestDataItem[] { - const results: EmojiTestDataItem[] = []; +export function parseEmojiTestFile(data: string): EmojiTestData { + const results = Object.create(null) as EmojiTestData; let group: string | undefined; let subgroup: string | undefined; @@ -106,11 +148,8 @@ export function parseEmojiTestFile(data: string): EmojiTestDataItem[] { return; } - const code = firstChunkParts[0] - .trim() - .replace(/\s+/g, '-') - .toLowerCase(); - if (!code || !code.match(/^[a-f0-9]+[a-f0-9-]*[a-f0-9]+$/)) { + const code = firstChunkParts[0].trim(); + if (!code || !code.match(/^[A-F0-9]+[A-F0-9\s]*[A-F0-9]+$/)) { return; } @@ -133,87 +172,24 @@ export function parseEmojiTestFile(data: string): EmojiTestDataItem[] { } const name = secondChunkParts.join(' '); + // Get sequence and convert it to cleaned up string + const sequence = getEmojiSequenceFromString(code); + const key = getEmojiSequenceKeyword(sequence); + // Add item - results.push({ + if (results[key]) { + throw new Error(`Duplicate entry for "${code}"`); + } + results[key] = { group, subgroup, - code, - sequence: getEmojiSequenceFromString(code), + sequence, emoji, status, version, name, - }); + }; }); - return results; -} - -/** - * Get qualified variations from parsed test file - * - * Key is unqualified emoji, value is longest fully qualified emoji - */ -export function getQualifiedEmojiSequencesMap( - sequences: number[][] -): Map; -export function getQualifiedEmojiSequencesMap( - sequences: number[][], - toString: (value: number[]) => string -): Record; -export function getQualifiedEmojiSequencesMap( - sequences: number[][], - toString?: (value: number[]) => string -): Map | Record { - const convert = toString || getEmojiSequenceKeyword; - const results = Object.create(null) as Record; - - for (let i = 0; i < sequences.length; i++) { - const value = convert(sequences[i]); - const unqualified = convert(getUnqualifiedEmojiSequence(sequences[i])); - // Check if values mismatch, set results to longest value - if ( - !results[unqualified] || - results[unqualified].length < value.length - ) { - results[unqualified] = value; - } - } - - // Return - if (toString) { - return results; - } - - const map: Map = new Map(); - for (const key in results) { - const value = results[key]; - map.set( - getEmojiSequenceFromString(key), - getEmojiSequenceFromString(value) - ); - } - return map; -} - -/** - * Map data by sequence - */ -export function mapEmojiTestDataBySequence( - testData: EmojiTestDataItem[], - convert: EmojiSequenceToStringCallback -): Record { - const testSequences = Object.create(null) as Record< - string, - EmojiTestDataItem - >; - for (let i = 0; i < testData.length; i++) { - const item = testData[i]; - const keyword = convert(item.sequence); - if (testSequences[keyword]) { - throw new Error(`Duplicate entries for "${keyword}"`); - } - testSequences[keyword] = item; - } - return testSequences; + return getQualifiedTestData(results); } diff --git a/packages/utils/src/emoji/test/similar.ts b/packages/utils/src/emoji/test/similar.ts new file mode 100644 index 0000000..1393ad3 --- /dev/null +++ b/packages/utils/src/emoji/test/similar.ts @@ -0,0 +1,88 @@ +import { vs16Emoji } from '../data'; +import { + EmojiSequenceWithComponents, + emojiSequenceWithComponentsToString, + EmojiTestDataComponentsMap, + mapEmojiTestDataComponents, +} from './components'; +import { SplitEmojiName, splitEmojiNameVariations } from './name'; +import type { + BaseEmojiTestDataItem, + EmojiTestData, + EmojiTestDataItem, +} from './parse'; + +/** + * Similar test data items as one item + */ +export interface CombinedEmojiTestDataItem extends BaseEmojiTestDataItem { + // Name, split + name: SplitEmojiName; + + // Sequence without variations, but with '{skin-tone}' + sequenceKey: string; + + // Sequence with components + sequence: EmojiSequenceWithComponents; +} + +export type SimilarEmojiTestData = Record; + +/** + * Find components in item, generate CombinedEmojiTestDataItem + */ +export function findComponentsInEmojiTestItem( + item: EmojiTestDataItem, + componentsData: EmojiTestDataComponentsMap +): CombinedEmojiTestDataItem { + // Split name + const name = splitEmojiNameVariations( + item.name, + item.sequence, + componentsData + ); + + // Update sequence + const sequence = [...item.sequence] as EmojiSequenceWithComponents; + name.variations?.forEach((item) => { + if (typeof item !== 'string') { + sequence[item.index] = item.type; + } + }); + + // Generate new key based on sequence + const sequenceKey = emojiSequenceWithComponentsToString( + sequence.filter((code) => code !== vs16Emoji) + ); + + return { + ...item, + name, + sequenceKey, + sequence, + }; +} + +/** + * Combine similar items in one iteratable item + */ +export function combineSimilarEmojiTestData( + data: EmojiTestData, + componentsData?: EmojiTestDataComponentsMap +): SimilarEmojiTestData { + const results = Object.create(null) as SimilarEmojiTestData; + componentsData = componentsData || mapEmojiTestDataComponents(data); + + for (const key in data) { + const sourceItem = data[key]; + if (sourceItem.status !== 'component') { + const item = findComponentsInEmojiTestItem( + sourceItem, + componentsData + ); + results[item.sequenceKey] = item; + } + } + + return results; +} diff --git a/packages/utils/src/emoji/test/tree.ts b/packages/utils/src/emoji/test/tree.ts new file mode 100644 index 0000000..466a8f3 --- /dev/null +++ b/packages/utils/src/emoji/test/tree.ts @@ -0,0 +1,199 @@ +import { emojiComponents, EmojiComponentType } from '../data'; +import type { + SimilarEmojiTestData, + CombinedEmojiTestDataItem, +} from './similar'; + +/** + * List of components + */ +type ComponentsCount = Required>; + +/** + * Extended tree item + */ +interface TreeSplitEmojiTestDataItem extends CombinedEmojiTestDataItem { + // Components + components: ComponentsCount; + + // Components, stringified + componentsKey: string; +} + +/** + * Tree item + */ +export interface EmojiComponentsTreeItem { + // Item + item: TreeSplitEmojiTestDataItem; + + // Child element(s) + children?: Record; +} + +export type EmojiComponentsTree = Record; + +/** + * Merge types for unique key + */ +function mergeComponentTypes(value: EmojiComponentType[]) { + return '[' + value.join(',') + ']'; +} + +/** + * Merge count for unique key + */ +function mergeComponentsCount(value: ComponentsCount): string { + const keys: EmojiComponentType[] = []; + for (const key in emojiComponents) { + const type = key as EmojiComponentType; + for (let i = 0; i < value[type]; i++) { + keys.push(type); + } + } + return keys.length ? mergeComponentTypes(keys) : ''; +} + +/** + * Group data + */ +interface GroupItem { + item: TreeSplitEmojiTestDataItem; + parsed?: true; +} +type GroupItems = Record; + +/** + * Get item from group + */ +function getGroupItem( + items: GroupItems, + components: ComponentsCount +): TreeSplitEmojiTestDataItem | undefined { + const key = mergeComponentsCount(components); + const item = items[key]; + if (item) { + item.parsed = true; + return item.item; + } +} + +/** + * Convert test data to dependencies tree, based on components + */ +export function getEmojiTestDataTree( + data: SimilarEmojiTestData +): EmojiComponentsTree { + // Group items by base name + const groups = Object.create(null) as Record; + for (const key in data) { + const item = data[key]; + const text = item.name.key; + const parent = groups[text] || (groups[text] = {} as GroupItems); + + // Generate key + const components: ComponentsCount = { + 'hair-style': 0, + 'skin-tone': 0, + }; + item.sequence.forEach((value) => { + if (typeof value !== 'number') { + components[value]++; + } + }); + const componentsKey = mergeComponentsCount(components); + if (parent[componentsKey]) { + throw new Error(`Duplicate components tree item for "${text}"`); + } + parent[componentsKey] = { + item: { + ...item, + components, + componentsKey, + }, + }; + } + + // Sort items + const results = Object.create(null) as EmojiComponentsTree; + for (const key in groups) { + const items = groups[key]; + + const check = ( + parent: EmojiComponentsTreeItem, + parentComponents: EmojiComponentType[], + type: EmojiComponentType + ): true | undefined => { + const item = parse(parentComponents, [type]); + if (item) { + const children = + parent.children || + (parent.children = + {} as Required['children']); + children[type] = item; + return true; + } + }; + + const parse = ( + parentComponents: EmojiComponentType[], + newComponents: EmojiComponentType[] + ): EmojiComponentsTreeItem | undefined => { + // Merge parameters + const components: ComponentsCount = { + 'hair-style': 0, + 'skin-tone': 0, + }; + const componentsList = parentComponents.concat(newComponents); + componentsList.forEach((type) => { + components[type]++; + }); + + // Get item + let item = getGroupItem(items, components); + if ( + !item && + newComponents.length === 1 && + newComponents[0] === 'skin-tone' + ) { + // Attempt double skin tone + const doubleComponents = { + ...components, + }; + doubleComponents['skin-tone']++; + item = getGroupItem(items, doubleComponents); + } + if (item) { + // Check child items + const result: EmojiComponentsTreeItem = { + item, + }; + + // Try adding children + for (const key in emojiComponents) { + check(result, componentsList, key as EmojiComponentType); + } + return result; + } + }; + + const root = parse([], []); + if (!root) { + throw new Error(`Cannot find parent item for "${key}"`); + } + + // Make sure all child items are checked + for (const itemsKey in items) { + if (!items[itemsKey].parsed) { + throw new Error(`Error generating tree for "${key}"`); + } + } + + // Make sure root is not empty + if (root.children) { + results[key] = root; + } + } + + return results; +} diff --git a/packages/utils/src/emoji/test/variations.ts b/packages/utils/src/emoji/test/variations.ts index ac38111..27c7c85 100644 --- a/packages/utils/src/emoji/test/variations.ts +++ b/packages/utils/src/emoji/test/variations.ts @@ -1,13 +1,12 @@ import { - getEmojiSequenceFromString, + getUnqualifiedEmojiSequence, joinEmojiSequences, - removeEmojiVariations, splitEmojiSequences, } from '../cleanup'; import { convertEmojiSequenceToUTF32 } from '../convert'; import { keycapEmoji, vs16Emoji } from '../data'; import { getEmojiSequenceKeyword } from '../format'; -import { EmojiTestDataItem, getQualifiedEmojiSequencesMap } from './parse'; +import type { EmojiTestData } from './parse'; /** * Get qualified sequence, adding optional `FE0F` wherever it might exist @@ -36,6 +35,17 @@ export function guessQualifiedEmojiSequence(sequence: number[]): number[] { return joinEmojiSequences(split); } +/** + * Base type to extend + */ +interface BaseSequenceItem { + sequence: number[]; + + // If present, will be set in value too + // String version of sequence without variation unicode + sequenceKey?: string; +} + /** * Get qualified variations for emojis * @@ -45,48 +55,54 @@ export function guessQualifiedEmojiSequence(sequence: number[]): number[] { * If missing or emoji is missing in test data, `FE0F` is added to every single code emoji. * It can also be an array of sequences. */ -export function getQualifiedEmojiVariations( - sequences: number[][], - testData?: (number[] | EmojiTestDataItem)[] -): number[][]; -export function getQualifiedEmojiVariations( - sequences: number[][], - testData: (number[] | EmojiTestDataItem)[], - toString: (value: number[]) => string -): string[]; -export function getQualifiedEmojiVariations( - sequences: number[][], - testData: (number[] | EmojiTestDataItem)[] = [], - toString?: (value: number[]) => string -): number[][] | string[] { - const convert = toString || getEmojiSequenceKeyword; - const testSequences = testData.map((item) => - item instanceof Array ? item : item.sequence + +export function getQualifiedEmojiVariation( + item: T, + testData?: EmojiTestData +): T { + // Convert to UTF-32, get unqualified sequence + const unqualifiedSequence = getUnqualifiedEmojiSequence( + convertEmojiSequenceToUTF32(item.sequence) ); - // Map test data - const testDataMap = getQualifiedEmojiSequencesMap(testSequences, convert); + // Check test data. Key is unqualified sequence + const key = getEmojiSequenceKeyword(unqualifiedSequence); + const testDataItem = testData?.[key]; - // Parse all sequences - const set: Set = new Set(); - - sequences.forEach((sequence) => { - // Convert to UTF-32, remove variations - const convertedSequence = convertEmojiSequenceToUTF32(sequence); - const cleanSequence = removeEmojiVariations(convertedSequence); - - // Check test data - const mapKey = convert(cleanSequence); - if (testDataMap[mapKey]) { - // Got item from test data - set.add(testDataMap[mapKey]); - return; - } - - // Not in test data: guess variations - set.add(convert(guessQualifiedEmojiSequence(cleanSequence))); - }); - - const results = Array.from(set); - return toString ? results : results.map(getEmojiSequenceFromString); + const result: T = { + ...item, + sequence: testDataItem + ? testDataItem.sequence + : guessQualifiedEmojiSequence(unqualifiedSequence), + }; + if (result.sequenceKey) { + result.sequenceKey = key; + } + return result; +} + +/** + * Get qualified emoji variations for set of emojis, ignoring duplicate entries + */ +export function getQualifiedEmojiVariations( + items: T[], + testData?: EmojiTestData +): T[] { + // Parse all sequences + const results = Object.create(null) as Record; + + for (let i = 0; i < items.length; i++) { + const result = getQualifiedEmojiVariation(items[i], testData); + const key = getEmojiSequenceKeyword( + getUnqualifiedEmojiSequence(result.sequence) + ); + if ( + !results[key] || + results[key].sequence.length < result.sequence.length + ) { + results[key] = result; + } + } + + return Object.values(results); } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 8567d2c..d354ef0 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -87,7 +87,6 @@ export { loadIcon } from './loader/loader'; export { getEmojiSequenceFromString, getUnqualifiedEmojiSequence, - removeEmojiVariations, } from './emoji/cleanup'; export { getEmojiCodePoint, @@ -103,12 +102,9 @@ export { getEmojiSequenceString, getEmojiSequenceKeyword, } from './emoji/format'; -export { - parseEmojiTestFile, - getQualifiedEmojiSequencesMap, -} from './emoji/test/parse'; +export { parseEmojiTestFile } from './emoji/test/parse'; export { getQualifiedEmojiVariations } from './emoji/test/variations'; -export { getEmojisSequencesToCopy } from './emoji/test/copy'; +// export { getEmojisSequencesToCopy } from './emoji/test/copy'; export { createOptimisedRegex, createOptimisedRegexForEmojiSequences, diff --git a/packages/utils/tests/emoji-cleanup-test.ts b/packages/utils/tests/emoji-cleanup-test.ts index a25c8bc..1a82c15 100644 --- a/packages/utils/tests/emoji-cleanup-test.ts +++ b/packages/utils/tests/emoji-cleanup-test.ts @@ -2,7 +2,7 @@ import { convertEmojiSequenceToUTF32 } from '../lib/emoji/convert'; import { getEmojiSequenceFromString, joinEmojiSequences, - removeEmojiVariations, + getUnqualifiedEmojiSequence, splitEmojiSequences, } from '../lib/emoji/cleanup'; @@ -37,7 +37,7 @@ describe('Testing formatting emoji cleanup', () => { expect(joinEmojiSequences(split)).toEqual(sequence); // Remove variations - expect(removeEmojiVariations(sequence)).toEqual([ + expect(getUnqualifiedEmojiSequence(sequence)).toEqual([ 0x1f441, 0x200d, 0x1f5e8, ]); }); @@ -63,6 +63,6 @@ describe('Testing formatting emoji cleanup', () => { expect(joinEmojiSequences(split)).toEqual(sequence); // Remove variations (does nothing for this sequence) - expect(removeEmojiVariations(sequence)).toEqual(sequence); + expect(getUnqualifiedEmojiSequence(sequence)).toEqual(sequence); }); }); diff --git a/packages/utils/tests/emoji-optional-variations-test.ts b/packages/utils/tests/emoji-optional-variations-test.ts index 326e419..d513dfc 100644 --- a/packages/utils/tests/emoji-optional-variations-test.ts +++ b/packages/utils/tests/emoji-optional-variations-test.ts @@ -2,10 +2,7 @@ import { readFile, writeFile, unlink } from 'node:fs/promises'; import { emojiVersion } from '../lib/emoji/data'; import { getEmojiSequenceFromString } from '../lib/emoji/cleanup'; import { getEmojiSequenceString } from '../lib/emoji/format'; -import { - getQualifiedEmojiSequencesMap, - parseEmojiTestFile, -} from '../lib/emoji/test/parse'; +import { parseEmojiTestFile } from '../lib/emoji/test/parse'; import { getQualifiedEmojiVariations } from '../lib/emoji/test/variations'; describe('Qualified variations of emoji sequences', () => { @@ -61,11 +58,18 @@ describe('Qualified variations of emoji sequences', () => { // mix of simple and complex, with and without variation '1F9D7 1F3FE 200D 2640 FE0F', '1F9D7 1F3FF 200D 2642 ', - ].map(getEmojiSequenceFromString); + ].map((source) => { + const sequence = getEmojiSequenceFromString(source); + return { + source, + sequence, + }; + }); + const results = getQualifiedEmojiVariations(sequences); expect( - results.map((sequence) => - getEmojiSequenceString(sequence, { + results.map((item) => + getEmojiSequenceString(item.sequence, { separator: ' ', case: 'upper', format: 'utf-32', @@ -98,23 +102,17 @@ describe('Qualified variations of emoji sequences', () => { console.warn('Test skipped: test data is not available'); return; } - const testData = parseEmojiTestFile(data); - const testDataSequences = testData.map((item) => item.sequence); - // Make sure testData contains both fully-qualified and unqualified emojis - const testDataStrings = new Set(testData.map((item) => item.code)); + // Make sure testData keys contain only unqualified emojis + const testDataStrings = new Set(Object.keys(testData)); expect(testDataStrings.has('1f600')).toBe(true); expect(testDataStrings.has('263a')).toBe(true); - expect(testDataStrings.has('263a-fe0f')).toBe(true); + expect(testDataStrings.has('263a-fe0f')).toBe(false); - // Test getQualifiedEmojiSequencesMap - const unqualifiedTest = getQualifiedEmojiSequencesMap( - testDataSequences, - getEmojiSequenceString - ); - expect(unqualifiedTest['1f600']).toBe('1f600'); - expect(unqualifiedTest['263a']).toBe('263a-fe0f'); + // Make sure values contain qualified emojis + expect(testData['1f600'].sequence).toEqual([0x1f600]); + expect(testData['263a'].sequence).toEqual([0x263a, 0xfe0f]); // Sequences to test const sequences = [ @@ -131,17 +129,20 @@ describe('Qualified variations of emoji sequences', () => { // complex emoji, exists in file '1F9D1 1F3FE 200D 2764 200D 1F9D1 1F3FB', // simple emoji, not in test file - '1234', + '25F0', // fake keycap, not in test file '2345 20E3 200D 1235', - ].map(getEmojiSequenceFromString); - const results = getQualifiedEmojiVariations( - sequences, - testDataSequences - ); + ].map((source) => { + const sequence = getEmojiSequenceFromString(source); + return { + source, + sequence, + }; + }); + const results = getQualifiedEmojiVariations(sequences, testData); expect( - results.map((sequence) => - getEmojiSequenceString(sequence, { + results.map((item) => + getEmojiSequenceString(item.sequence, { separator: ' ', case: 'upper', format: 'utf-32', @@ -162,7 +163,7 @@ describe('Qualified variations of emoji sequences', () => { // complex emoji, exists in file '1F9D1 1F3FE 200D 2764 FE0F 200D 1F9D1 1F3FB', // simple emoji, not in test file - '1234 FE0F', + '25F0 FE0F', // fake keycap, not in test file '2345 FE0F 20E3 200D 1235 FE0F', ]); diff --git a/packages/utils/tests/emoji-testdata-test.ts b/packages/utils/tests/emoji-testdata-test.ts index 5ca160e..b542800 100644 --- a/packages/utils/tests/emoji-testdata-test.ts +++ b/packages/utils/tests/emoji-testdata-test.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { readFile, writeFile, unlink } from 'node:fs/promises'; -import { getEmojiSequenceString } from '../lib/emoji/format'; import { splitUTF32Number } from '../lib/emoji/convert'; import { startUTF32Pair1, @@ -8,22 +7,16 @@ import { endUTF32Pair, minUTF32, emojiVersion, - vs16Emoji, - EmojiComponentType, } from '../lib/emoji/data'; +import { parseEmojiTestFile } from '../lib/emoji/test/parse'; import { - parseEmojiTestFile, - mapEmojiTestDataBySequence, - EmojiTestDataItem, -} from '../lib/emoji/test/parse'; -import { mapEmojiTestDataComponents } from '../lib/emoji/test/components'; -import { - splitEmojiNameVariations, - SplitEmojiName, - getEmojiComponentsMap, -} from '../lib/emoji/test/name'; -import { getEmojisSequencesToCopy } from '../lib/emoji/test/copy'; -import { getQualifiedEmojiVariations } from '../lib/emoji/test/variations'; + mapEmojiTestDataComponents, + replaceEmojiComponentsInCombinedSequence, +} from '../lib/emoji/test/components'; +import { splitEmojiNameVariations } from '../lib/emoji/test/name'; +import { combineSimilarEmojiTestData } from '../lib/emoji/test/similar'; +import { getEmojiTestDataTree } from '../lib/emoji/test/tree'; +import { findMissingEmojis } from '../lib/emoji/test/missing'; describe('Testing unicode test data', () => { async function fetchEmojiTestData(): Promise { @@ -60,18 +53,6 @@ describe('Testing unicode test data', () => { return data; } - function sequenceToString( - sequence: (EmojiComponentType | number)[] - ): string { - return sequence - .map((item) => - typeof item === 'string' - ? item - : item.toString(16).toLowerCase() - ) - .join('-'); - } - let data: string | undefined; beforeAll(async () => { @@ -88,7 +69,7 @@ describe('Testing unicode test data', () => { const utf16: Set = new Set(); const utf32: Set = new Set(); - parseEmojiTestFile(data).forEach((item) => { + Object.values(parseEmojiTestFile(data)).forEach((item) => { item.sequence.forEach((code) => { if (code < minUTF32) { utf16.add(code); @@ -166,192 +147,197 @@ describe('Testing unicode test data', () => { expect(utf32SecondRange!.max).toBeLessThan(endUTF32Pair); }); - it('Splitting emoji names', () => { + it('Splitting emoji names and variations', () => { if (!data) { console.warn('Test skipped: test data is not available'); return; } const testData = parseEmojiTestFile(data); - const mappedTestData = mapEmojiTestDataBySequence( - testData, - getEmojiSequenceString - ); - const components = mapEmojiTestDataComponents( - mappedTestData, - getEmojiSequenceString - ); + const components = mapEmojiTestDataComponents(testData); - // Few items without variations - let item: EmojiTestDataItem; - let baseItem: EmojiTestDataItem; - [ - '1f600', - '1f636-200d-1f32b-fe0f', - '1f62e-200d-1f4a8', - '1f5ef-fe0f', - '1f44b', - ].forEach((key) => { - item = mappedTestData[key]; - expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: item.name, - key: item.name, - }); + // Test splitting name + expect( + splitEmojiNameVariations( + 'people holding hands: medium-light skin tone', + [129489, 127996, 8205, 129309, 8205, 129489, 127996], + components + ) + ).toEqual({ + base: 'people holding hands', + key: 'people holding hands', + variations: [ + { + index: 1, + type: 'skin-tone', + }, + { + index: 6, + type: 'skin-tone', + }, + ], + components: 2, }); + // Split data + const splitTestData = combineSimilarEmojiTestData(testData, components); + + // Check basic items + expect(testData['1f600']).toBeDefined(); + const sourceItem1 = testData['1f600']; + + expect(splitTestData['1f600']).toEqual({ + ...sourceItem1, + sequence: [0x1f600], + sequenceKey: '1f600', + name: { + // Same name and key + base: sourceItem1.name, + key: sourceItem1.name, + }, + }); + + // Basic item with variation + expect(testData['263a']).toBeDefined(); + const sourceItem2 = testData['263a']; + + expect(splitTestData['263a']).toEqual({ + ...sourceItem2, + // Make sure sequence contains 'FE0F', but key does not + sequence: [0x263a, 0xfe0f], + sequenceKey: '263a', + name: { + // Same name and key + base: sourceItem2.name, + key: sourceItem2.name, + }, + }); + + // Skin tone + expect(testData['1f44b-1f3fb']).toBeDefined(); + expect(splitTestData['1f44b-1f3fb']).toBeUndefined(); + expect(splitTestData['1f44b-skin-tone']).toBeDefined(); + const sourceItem3 = testData['1f44b-1f3ff']; + + expect(splitTestData['1f44b-skin-tone']).toEqual({ + ...sourceItem3, + // Sequence should contain 'skin-tone' + sequence: [0x1f44b, 'skin-tone'], + sequenceKey: '1f44b-skin-tone', + name: { + // Name without skin tone + base: 'waving hand', + key: 'waving hand', + components: 1, + variations: [ + { + index: 1, + type: 'skin-tone', + }, + ], + }, + }); + + // Not a skin tone + expect(testData['30-20e3']).toBeDefined(); + const sourceItem4 = testData['30-20e3']; + + expect(splitTestData['30-20e3']).toEqual({ + ...sourceItem4, + sequenceKey: '30-20e3', + name: { + // Name without number tone + base: 'keycap', + // Key should contain variation + key: 'keycap: 0', + variations: ['0'], + }, + }); + + // Items should not have skin tones + for (const key in splitTestData) { + const item = splitTestData[key]; + if (item.sequenceKey.indexOf('1f3fc') !== -1) { + console.error(key, item); + expect(item.sequenceKey.indexOf('1f3fc')).toBe(-1); + } + } + }); + + it('Merging variations', () => { + // Nothing to replace + expect(replaceEmojiComponentsInCombinedSequence([0x1f3c3], {})).toEqual( + [0x1f3c3] + ); + // One skin tone - baseItem = mappedTestData['1f590']; - item = mappedTestData['1f590-1f3fb']; expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: baseItem.name, - key: baseItem.name, - components: 1, - variations: [ - { - index: 1, - type: 'skin-tone', - }, - ], - }); - item = mappedTestData['1f590-1f3ff']; - expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: baseItem.name, - key: baseItem.name, - components: 1, - variations: [ - { - index: 1, - type: 'skin-tone', - }, - ], - }); + replaceEmojiComponentsInCombinedSequence([0x1f3c3, 'skin-tone'], { + 'skin-tone': [0x1f3fe], + }) + ).toEqual([0x1f3c3, 0x1f3fe]); - // Flag, no base item - item = mappedTestData['1f1e6-1f1f6']; + // Hair style expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: 'flag', - key: item.name, - components: 0, - variations: ['Antarctica'], - }); + replaceEmojiComponentsInCombinedSequence( + [0x1f468, 0x200d, 'hair-style'], + { + 'skin-tone': [0x1f3fe], // unused + 'hair-style': [0x1f9b2], + } + ) + ).toEqual([0x1f468, 0x200d, 0x1f9b2]); - // Keycap, no base item - item = mappedTestData['23-fe0f-20e3']; + // Mix expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: 'keycap', - key: item.name, - components: 0, - variations: ['#'], - }); + replaceEmojiComponentsInCombinedSequence( + [0x1f468, 'skin-tone', 0x200d, 'hair-style'], + { + 'skin-tone': [0x1f3fe], + 'hair-style': [0x1f9b1], + } + ) + ).toEqual([0x1f468, 0x1f3fe, 0x200d, 0x1f9b1]); - // Variations of same base item - baseItem = mappedTestData['1f468']; - item = mappedTestData['1f468-1f3fd']; + // Mutiple skin tones expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: baseItem.name, - key: baseItem.name, - components: 1, - variations: [ + replaceEmojiComponentsInCombinedSequence( + [ + 0x1f469, + 'skin-tone', + 0x200d, + 0x1f91d, + 0x200d, + 0x1f468, + 'skin-tone', + ], { - index: 1, - type: 'skin-tone', - }, - ], - }); - item = mappedTestData['1f468-200d-1f9b0']; - expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: baseItem.name, - key: baseItem.name, - components: 1, - variations: [ - { - index: 2, - type: 'hair-style', - }, - ], - }); - item = mappedTestData['1f468-1f3fd-200d-1f9b0']; - expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: baseItem.name, - key: baseItem.name, - components: 2, - variations: [ - { - index: 1, - type: 'skin-tone', - }, - { - index: 3, - type: 'hair-style', - }, - ], - }); + 'skin-tone': [0x1f3fc, 0x1f3ff], + } + ) + ).toEqual([ + 0x1f469, 0x1f3fc, 0x200d, 0x1f91d, 0x200d, 0x1f468, 0x1f3ff, + ]); - // Variations of same base item with custom stuff - baseItem = mappedTestData['1f48f']; - item = mappedTestData['1f48f-1f3fd']; + // Double skin tones expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: baseItem.name, - key: baseItem.name, - components: 1, - variations: [ + replaceEmojiComponentsInCombinedSequence( + [ + 0x1f469, + 'skin-tone', + 0x200d, + 0x1f91d, + 0x200d, + 0x1f468, + 'skin-tone', + ], { - index: 1, - type: 'skin-tone', - }, - ], - }); - item = mappedTestData['1f469-200d-2764-200d-1f48b-200d-1f468']; - expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: baseItem.name, - key: item.name, - components: 0, - variations: ['woman', 'man'], - }); - item = - mappedTestData[ - '1f9d1-1f3fb-200d-2764-fe0f-200d-1f48b-200d-1f9d1-1f3fd' - ]; - expect( - splitEmojiNameVariations(item.name, item.sequence, components) - ).toEqual({ - base: baseItem.name, - key: baseItem.name, - // key: baseItem.name + ': person, person', - components: 2, - variations: [ - 'person', - 'person', - { - index: 1, - type: 'skin-tone', - }, - { - index: 9, - type: 'skin-tone', - }, - ], - }); + 'skin-tone': [0x1f3fc], + } + ) + ).toEqual([ + 0x1f469, 0x1f3fc, 0x200d, 0x1f91d, 0x200d, 0x1f468, 0x1f3fc, + ]); }); it('Checking parent items for all variations', () => { @@ -361,245 +347,351 @@ describe('Testing unicode test data', () => { } const testData = parseEmojiTestFile(data); - const mappedTestData = mapEmojiTestDataBySequence( - testData, - getEmojiSequenceString - ); - const components = mapEmojiTestDataComponents( - mappedTestData, - getEmojiSequenceString - ); + const components = mapEmojiTestDataComponents(testData); - // Parse all items - // [key][sequence] = sample item - interface Item { - item: EmojiTestDataItem; - split: SplitEmojiName; - components: EmojiComponentType[]; - } - type ItemsList = Record; - const results = Object.create(null) as Record; - testData.forEach((item) => { - const split = splitEmojiNameVariations( - item.name, - item.sequence, - components - ); - const parent = - results[split.key] || - (results[split.key] = Object.create(null) as ItemsList); + // Split data and get tree + const splitTestData = combineSimilarEmojiTestData(testData, components); + const tree = getEmojiTestDataTree(splitTestData); - // Create unique key based on component types - let sequenceKey = 'default'; - const itemComponents: EmojiComponentType[] = []; - if (split.components) { - split.variations?.forEach((item) => { - if (typeof item !== 'string') { - itemComponents.push(item.type); - } - }); - if (itemComponents.length) { - sequenceKey = '[' + itemComponents.join(', ') + ']'; - } - } - - const prevItem = parent[sequenceKey]; - if (!prevItem) { - parent[sequenceKey] = { - item, - split, - components: itemComponents, - }; - return; - } - - // Compare items - const cleanSequence = (sequence: number[]): string => { - return getEmojiSequenceString( - sequence.filter( - (num) => - num !== vs16Emoji && !components.converted.has(num) - ) - ); - }; - - if ( - cleanSequence(prevItem.item.sequence) !== - cleanSequence(item.sequence) - ) { - console.log(prevItem.item); - console.log(item); - throw new Error( - `Mismatched items with same key: ${sequenceKey}` - ); - } - - if (item.sequence.length > prevItem.item.sequence.length) { - // Keep longer sequence - parent[sequenceKey] = { - item, - split, - components: itemComponents, - }; - } - }); - - // Validate all items - const allVariations: Set = new Set(); - for (const key in results) { - const items = results[key]; - const stringify = (value: EmojiComponentType[]) => - '[' + value.join(',') + ']'; - - const itemComponents = Object.create(null) as Record< - string, - EmojiComponentType[] - >; - const tested: Set = new Set(); - for (const key2 in items) { - const item = items[key2]; - const componentsValue = stringify(item.components); - allVariations.add(componentsValue); - if (componentsValue in itemComponents) { - console.log(items); - throw new Error( - `Duplicate "${componentsValue}" variations` - ); - } - itemComponents[componentsValue] = item.components; - } - - // Make sure all items exist - const checkParents = (list: EmojiComponentType[]) => { - const skippedTypes = new Set(); - for (let i = 0; i < list.length; i++) { - const sequence = list.slice(0, i).concat(list.slice(i + 1)); - const key = stringify(sequence); - const skipped = list[i]; - const parent = itemComponents[key]; - if (!parent) { - // Missing parent - console.log(items); - throw new Error( - `Missing parent for "${key}" variation` - ); - } - - if (!skippedTypes.has(skipped)) { - skippedTypes.add(skipped); - const testKey = key + ':' + skipped; - if (tested.has(testKey)) { - // Multiple child variations ??? - console.log(items); - throw new Error( - `Multiple parents for "${testKey}" variation` - ); - } - tested.add(testKey); - } - } - }; - for (const stringified in itemComponents) { - checkParents(itemComponents[stringified]); - } - } - // console.log(allVariations); - }); - - it('Testing emoji components map', () => { - if (!data) { - console.warn('Test skipped: test data is not available'); - return; - } - - const testData = parseEmojiTestFile(data); - const map = getEmojiComponentsMap(testData); - - // Simple item: should not exists - const item1 = map.find( - (item) => sequenceToString(item.sequence) === '1f601' - ); - expect(item1).toBeUndefined(); - - // Item with skin tones - const item2 = map.find( - (item) => sequenceToString(item.sequence) === '1f596' - ); - expect(item2).toEqual({ - name: 'vulcan salute', - sequence: [0x1f596], + // Simple item + expect(tree['person running']).toEqual({ + item: { + group: 'People & Body', + subgroup: 'person-activity', + sequence: [0x1f3c3], + emoji: String.fromCodePoint(0x1f3c3), + status: 'fully-qualified', + version: 'E0.6', + name: { + base: 'person running', + key: 'person running', + }, + sequenceKey: '1f3c3', + components: { + 'skin-tone': 0, + 'hair-style': 0, + }, + componentsKey: '', + }, children: { 'skin-tone': { - name: 'vulcan salute: {skin-tone-0}', - sequence: [0x1f596, 'skin-tone'], + item: { + group: 'People & Body', + subgroup: 'person-activity', + sequence: [0x1f3c3, 'skin-tone'], + emoji: + String.fromCodePoint(0x1f3c3) + + String.fromCodePoint(0x1f3ff), + status: 'fully-qualified', + version: 'E1.0', + name: { + base: 'person running', + key: 'person running', + components: 1, + variations: [ + { + type: 'skin-tone', + index: 1, + }, + ], + }, + sequenceKey: '1f3c3-skin-tone', + components: { + 'skin-tone': 1, + 'hair-style': 0, + }, + componentsKey: '[skin-tone]', + }, }, }, }); - // Item with hair style and skin tones - const item3 = map.find( - (item) => sequenceToString(item.sequence) === '1f469' - ); - expect(item3).toEqual({ - name: 'woman', - sequence: [0x1f469], + // Skin tone and hair style + expect(tree['man']).toEqual({ + item: { + group: 'People & Body', + subgroup: 'person', + sequence: [0x1f468], + emoji: String.fromCodePoint(0x1f468), + status: 'fully-qualified', + version: 'E0.6', + name: { + base: 'man', + key: 'man', + }, + sequenceKey: '1f468', + components: { + 'skin-tone': 0, + 'hair-style': 0, + }, + componentsKey: '', + }, children: { + 'hair-style': { + item: { + group: 'People & Body', + subgroup: 'person', + sequence: [0x1f468, 0x200d, 'hair-style'], + emoji: + String.fromCodePoint(0x1f468) + + String.fromCodePoint(0x200d) + + String.fromCodePoint(0x1f9b2), + status: 'fully-qualified', + version: 'E11.0', + name: { + base: 'man', + key: 'man', + components: 1, + variations: [ + { + type: 'hair-style', + index: 2, + }, + ], + }, + sequenceKey: '1f468-200d-hair-style', + components: { + 'skin-tone': 0, + 'hair-style': 1, + }, + componentsKey: '[hair-style]', + }, + children: { + 'skin-tone': { + item: { + group: 'People & Body', + subgroup: 'person', + sequence: [ + 0x1f468, + 'skin-tone', + 0x200d, + 'hair-style', + ], + emoji: + String.fromCodePoint(0x1f468) + + String.fromCodePoint(0x1f3ff) + + String.fromCodePoint(0x200d) + + String.fromCodePoint(0x1f9b2), + status: 'fully-qualified', + version: 'E11.0', + name: { + base: 'man', + key: 'man', + components: 2, + variations: [ + { + type: 'skin-tone', + index: 1, + }, + { + type: 'hair-style', + index: 3, + }, + ], + }, + sequenceKey: '1f468-skin-tone-200d-hair-style', + components: { + 'skin-tone': 1, + 'hair-style': 1, + }, + componentsKey: '[hair-style,skin-tone]', + }, + }, + }, + }, 'skin-tone': { - name: 'woman: {skin-tone-0}', - sequence: [0x1f469, 'skin-tone'], + item: { + group: 'People & Body', + subgroup: 'person', + sequence: [0x1f468, 'skin-tone'], + emoji: + String.fromCodePoint(0x1f468) + + String.fromCodePoint(0x1f3ff), + status: 'fully-qualified', + version: 'E1.0', + name: { + base: 'man', + key: 'man', + components: 1, + variations: [ + { + type: 'skin-tone', + index: 1, + }, + ], + }, + sequenceKey: '1f468-skin-tone', + components: { + 'skin-tone': 1, + 'hair-style': 0, + }, + componentsKey: '[skin-tone]', + }, children: { 'hair-style': { - name: 'woman: {skin-tone-0}, {hair-style-0}', - sequence: [ - 0x1f469, - 'skin-tone', - 0x200d, - 'hair-style', - ], - }, - }, - }, - 'hair-style': { - name: 'woman: {hair-style-0}', - sequence: [0x1f469, 0x200d, 'hair-style'], - children: { - 'skin-tone': { - name: 'woman: {skin-tone-0}, {hair-style-0}', - sequence: [ - 0x1f469, - 'skin-tone', - 0x200d, - 'hair-style', - ], + item: { + group: 'People & Body', + subgroup: 'person', + sequence: [ + 0x1f468, + 'skin-tone', + 0x200d, + 'hair-style', + ], + emoji: + String.fromCodePoint(0x1f468) + + String.fromCodePoint(0x1f3ff) + + String.fromCodePoint(0x200d) + + String.fromCodePoint(0x1f9b2), + status: 'fully-qualified', + version: 'E11.0', + name: { + base: 'man', + key: 'man', + components: 2, + variations: [ + { + type: 'skin-tone', + index: 1, + }, + { + type: 'hair-style', + index: 3, + }, + ], + }, + sequenceKey: '1f468-skin-tone-200d-hair-style', + components: { + 'skin-tone': 1, + 'hair-style': 1, + }, + componentsKey: '[hair-style,skin-tone]', + }, }, }, }, }, }); - // Item with multiple skin tones - const item4 = map.find( - (item) => sequenceToString(item.sequence) === '1f46b' - ); - expect(item4).toEqual({ - name: 'woman and man holding hands', - sequence: [0x1f46b], + // Double skin tone + expect(tree['people holding hands']).toEqual({ + item: { + group: 'People & Body', + subgroup: 'family', + sequence: [0x1f9d1, 0x200d, 0x1f91d, 0x200d, 0x1f9d1], + emoji: + String.fromCodePoint(0x1f9d1) + + String.fromCodePoint(0x200d) + + String.fromCodePoint(0x1f91d) + + String.fromCodePoint(0x200d) + + String.fromCodePoint(0x1f9d1), + status: 'fully-qualified', + version: 'E12.0', + name: { + base: 'people holding hands', + key: 'people holding hands', + }, + sequenceKey: '1f9d1-200d-1f91d-200d-1f9d1', + components: { + 'skin-tone': 0, + 'hair-style': 0, + }, + componentsKey: '', + }, children: { 'skin-tone': { - name: 'woman and man holding hands: {skin-tone-0}', - sequence: [0x1f46b, 'skin-tone'], + item: { + group: 'People & Body', + subgroup: 'family', + sequence: [ + 0x1f9d1, + 'skin-tone', + 0x200d, + 0x1f91d, + 0x200d, + 0x1f9d1, + 'skin-tone', + ], + emoji: + String.fromCodePoint(0x1f9d1) + + String.fromCodePoint(0x1f3ff) + + String.fromCodePoint(0x200d) + + String.fromCodePoint(0x1f91d) + + String.fromCodePoint(0x200d) + + String.fromCodePoint(0x1f9d1) + + String.fromCodePoint(0x1f3ff), + status: 'fully-qualified', + version: 'E12.0', + name: { + base: 'people holding hands', + key: 'people holding hands', + components: 2, + variations: [ + { + type: 'skin-tone', + index: 1, + }, + { + type: 'skin-tone', + index: 6, + }, + ], + }, + sequenceKey: + '1f9d1-skin-tone-200d-1f91d-200d-1f9d1-skin-tone', + components: { + 'skin-tone': 2, + 'hair-style': 0, + }, + componentsKey: '[skin-tone,skin-tone]', + }, children: { 'skin-tone': { - name: 'woman and man holding hands: {skin-tone-0}, {skin-tone-1}', - sequence: [ - 0x1f469, - 'skin-tone', - 0x200d, - 0x1f91d, - 0x200d, - 0x1f468, - 'skin-tone', - ], + item: { + group: 'People & Body', + subgroup: 'family', + sequence: [ + 0x1f9d1, + 'skin-tone', + 0x200d, + 0x1f91d, + 0x200d, + 0x1f9d1, + 'skin-tone', + ], + emoji: + String.fromCodePoint(0x1f9d1) + + String.fromCodePoint(0x1f3ff) + + String.fromCodePoint(0x200d) + + String.fromCodePoint(0x1f91d) + + String.fromCodePoint(0x200d) + + String.fromCodePoint(0x1f9d1) + + String.fromCodePoint(0x1f3ff), + status: 'fully-qualified', + version: 'E12.0', + name: { + base: 'people holding hands', + key: 'people holding hands', + components: 2, + variations: [ + { + type: 'skin-tone', + index: 1, + }, + { + type: 'skin-tone', + index: 6, + }, + ], + }, + sequenceKey: + '1f9d1-skin-tone-200d-1f91d-200d-1f9d1-skin-tone', + components: { + 'skin-tone': 2, + 'hair-style': 0, + }, + componentsKey: '[skin-tone,skin-tone]', + }, }, }, }, @@ -607,63 +699,94 @@ describe('Testing unicode test data', () => { }); }); - it('Checking for missing sequences', () => { + it('Finding missing emojis', () => { if (!data) { console.warn('Test skipped: test data is not available'); return; } const testData = parseEmojiTestFile(data); - const sequences = getQualifiedEmojiVariations( - testData.map((item) => item.sequence), - testData + const components = mapEmojiTestDataComponents(testData); + + // Split data and get tree + const splitTestData = combineSimilarEmojiTestData(testData, components); + const tree = getEmojiTestDataTree(splitTestData); + + // Use test data + const testList = []; + for (const sequenceKey in testData) { + testList.push({ + ...testData[sequenceKey], + sequenceKey, + }); + } + const missing = new Set( + findMissingEmojis(testList, tree).map((item) => item.sequenceKey) + ); + expect(missing.size).toBe(30); + expect(missing.has('1faf1-1f3fb-200d-1faf2-1f3fb')).toBe(true); + expect(missing.has('1faf1-1f3fb-200d-1faf2-1f3fc')).toBe(false); + + // Only one emoji + const missing2 = findMissingEmojis( + [ + // Main icon + { + iconName: 'kiss', + sequence: [0x1f48f], + sequenceKey: '1f48f', + }, + // Only one skin tone to test sources for double skin tone + { + iconName: 'kiss-medium-skin-tone', + sequence: [0x1f48f, 0x1f3fd], + sequenceKey: '1f48f-1f3fd', + }, + ], + tree ); - const missing = getEmojisSequencesToCopy(sequences, testData); - - // Should be 30 entries for 15.0 - // TODO: update for newer versions - expect(missing.length).toBe(30); - - // Two identical tones. Not a valid emoji, but optimises regex - expect( - missing.find( - (item) => item.sourceName === 'handshake: light skin tone' - ) - ).toEqual({ - source: [0x1f91d, 0x1f3fb], - sourceName: 'handshake: light skin tone', - target: [0x1faf1, 0x1f3fb, 0x200d, 0x1faf2, 0x1f3fb], - targetName: 'handshake: light skin tone, light skin tone', + // Should be 29 missing emojis + expect(missing2.length).toBe(29); + const missing2Map = {} as Record; + missing2.forEach((item) => { + missing2Map[item.sequenceKey] = item.iconName; }); - - // Check with custom data: only base icon - const missing2 = getEmojisSequencesToCopy([[0x1f91d]], testData); - - // Missing icons: [skin-tone], [skin-tone, skin-tone] - expect(missing2.length).toBe(5 + 5 * 5); - expect( - missing2.find( - (item) => item.targetName === 'handshake: light skin tone' - ) - ).toEqual({ - source: [0x1f91d], - sourceName: 'handshake', - target: [0x1f91d, 0x1f3fb], - targetName: 'handshake: light skin tone', - }); - expect( - missing2.find( - (item) => - item.targetName === - 'handshake: medium-light skin tone, light skin tone' - ) - ).toEqual({ - // Should be copied from first component match - source: [0x1f91d, 0x1f3fc], - sourceName: 'handshake: medium-light skin tone', - target: [0x1faf1, 0x1f3fc, 0x200d, 0x1faf2, 0x1f3fb], - targetName: 'handshake: medium-light skin tone, light skin tone', + expect(missing2Map).toEqual({ + '1f48f-1f3fb': 'kiss', + '1f48f-1f3fc': 'kiss', + '1f9d1-1f3fd-200d-2764-200d-1f48b-200d-1f9d1-1f3fb': + 'kiss-medium-skin-tone', + '1f9d1-1f3fd-200d-2764-200d-1f48b-200d-1f9d1-1f3fc': + 'kiss-medium-skin-tone', + '1f9d1-1f3fd-200d-2764-200d-1f48b-200d-1f9d1-1f3fd': + 'kiss-medium-skin-tone', + '1f9d1-1f3fd-200d-2764-200d-1f48b-200d-1f9d1-1f3fe': + 'kiss-medium-skin-tone', + '1f9d1-1f3fd-200d-2764-200d-1f48b-200d-1f9d1-1f3ff': + 'kiss-medium-skin-tone', + '1f48f-1f3fe': 'kiss', + '1f48f-1f3ff': 'kiss', + '1f9d1-1f3fb-200d-2764-200d-1f48b-200d-1f9d1-1f3fb': 'kiss', + '1f9d1-1f3fb-200d-2764-200d-1f48b-200d-1f9d1-1f3fc': 'kiss', + '1f9d1-1f3fb-200d-2764-200d-1f48b-200d-1f9d1-1f3fd': 'kiss', + '1f9d1-1f3fb-200d-2764-200d-1f48b-200d-1f9d1-1f3fe': 'kiss', + '1f9d1-1f3fb-200d-2764-200d-1f48b-200d-1f9d1-1f3ff': 'kiss', + '1f9d1-1f3fc-200d-2764-200d-1f48b-200d-1f9d1-1f3fb': 'kiss', + '1f9d1-1f3fc-200d-2764-200d-1f48b-200d-1f9d1-1f3fc': 'kiss', + '1f9d1-1f3fc-200d-2764-200d-1f48b-200d-1f9d1-1f3fd': 'kiss', + '1f9d1-1f3fc-200d-2764-200d-1f48b-200d-1f9d1-1f3fe': 'kiss', + '1f9d1-1f3fc-200d-2764-200d-1f48b-200d-1f9d1-1f3ff': 'kiss', + '1f9d1-1f3fe-200d-2764-200d-1f48b-200d-1f9d1-1f3fb': 'kiss', + '1f9d1-1f3fe-200d-2764-200d-1f48b-200d-1f9d1-1f3fc': 'kiss', + '1f9d1-1f3fe-200d-2764-200d-1f48b-200d-1f9d1-1f3fd': 'kiss', + '1f9d1-1f3fe-200d-2764-200d-1f48b-200d-1f9d1-1f3fe': 'kiss', + '1f9d1-1f3fe-200d-2764-200d-1f48b-200d-1f9d1-1f3ff': 'kiss', + '1f9d1-1f3ff-200d-2764-200d-1f48b-200d-1f9d1-1f3fb': 'kiss', + '1f9d1-1f3ff-200d-2764-200d-1f48b-200d-1f9d1-1f3fc': 'kiss', + '1f9d1-1f3ff-200d-2764-200d-1f48b-200d-1f9d1-1f3fd': 'kiss', + '1f9d1-1f3ff-200d-2764-200d-1f48b-200d-1f9d1-1f3fe': 'kiss', + '1f9d1-1f3ff-200d-2764-200d-1f48b-200d-1f9d1-1f3ff': 'kiss', }); }); });