diff --git a/src/compat/_internal/regexUnicode.ts b/src/compat/_internal/regexUnicode.ts new file mode 100644 index 000000000..dd329cfae --- /dev/null +++ b/src/compat/_internal/regexUnicode.ts @@ -0,0 +1,33 @@ +/** + * Matches a single visible symbol (grapheme cluster) in a Unicode `string`, + * combining surrogate pairs, combining marks, variation selectors, Fitzpatrick + * modifiers, regional-indicator pairs, and zero-width-joiner sequences into a + * single match. + * + * Ported from lodash to preserve identical behavior. + * @link https://github.com/lodash/lodash/blob/4.17.21-es/_unicodeSize.js + */ + +const rsAstralRange = '\\ud800-\\udfff'; +const rsComboMarksRange = '\\u0300-\\u036f'; +const reComboHalfMarksRange = '\\ufe20-\\ufe2f'; +const rsComboSymbolsRange = '\\u20d0-\\u20ff'; +const rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange; +const rsVarRange = '\\ufe0e\\ufe0f'; + +const rsAstral = `[${rsAstralRange}]`; +const rsCombo = `[${rsComboRange}]`; +const rsFitz = '\\ud83c[\\udffb-\\udfff]'; +const rsModifier = `(?:${rsCombo}|${rsFitz})`; +const rsNonAstral = `[^${rsAstralRange}]`; +const rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}'; +const rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]'; +const rsZWJ = '\\u200d'; + +const reOptMod = `${rsModifier}?`; +const rsOptVar = `[${rsVarRange}]?`; +const rsOptJoin = `(?:${rsZWJ}(?:${[rsNonAstral, rsRegional, rsSurrPair].join('|')})${rsOptVar}${reOptMod})*`; +const rsSeq = rsOptVar + reOptMod + rsOptJoin; +const rsSymbol = `(?:${[`${rsNonAstral}${rsCombo}?`, rsCombo, rsRegional, rsSurrPair, rsAstral].join('|')})`; + +export const regexUnicode = RegExp(`${rsFitz}(?=${rsFitz})|${rsSymbol}${rsSeq}`, 'g'); diff --git a/src/compat/_internal/unicodeSize.ts b/src/compat/_internal/unicodeSize.ts index bc5b9371d..3631084dc 100644 --- a/src/compat/_internal/unicodeSize.ts +++ b/src/compat/_internal/unicodeSize.ts @@ -1,42 +1,16 @@ +import { regexUnicode } from './regexUnicode.ts'; + /** - * Counts the number of visible symbols (grapheme clusters) in a Unicode `string`, - * combining surrogate pairs, combining marks, variation selectors, Fitzpatrick - * modifiers, regional-indicator pairs, and zero-width-joiner sequences into a - * single symbol. + * Counts the number of visible symbols (grapheme clusters) in a Unicode `string`. * * Ported from lodash to preserve identical behavior. * @link https://github.com/lodash/lodash/blob/4.17.21-es/_unicodeSize.js */ - -const rsAstralRange = '\\ud800-\\udfff'; -const rsComboMarksRange = '\\u0300-\\u036f'; -const reComboHalfMarksRange = '\\ufe20-\\ufe2f'; -const rsComboSymbolsRange = '\\u20d0-\\u20ff'; -const rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange; -const rsVarRange = '\\ufe0e\\ufe0f'; - -const rsAstral = `[${rsAstralRange}]`; -const rsCombo = `[${rsComboRange}]`; -const rsFitz = '\\ud83c[\\udffb-\\udfff]'; -const rsModifier = `(?:${rsCombo}|${rsFitz})`; -const rsNonAstral = `[^${rsAstralRange}]`; -const rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}'; -const rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]'; -const rsZWJ = '\\u200d'; - -const reOptMod = `${rsModifier}?`; -const rsOptVar = `[${rsVarRange}]?`; -const rsOptJoin = `(?:${rsZWJ}(?:${[rsNonAstral, rsRegional, rsSurrPair].join('|')})${rsOptVar}${reOptMod})*`; -const rsSeq = rsOptVar + reOptMod + rsOptJoin; -const rsSymbol = `(?:${[`${rsNonAstral}${rsCombo}?`, rsCombo, rsRegional, rsSurrPair, rsAstral].join('|')})`; - -const reUnicode = RegExp(`${rsFitz}(?=${rsFitz})|${rsSymbol}${rsSeq}`, 'g'); - export function unicodeSize(str: string): number { let result = 0; - reUnicode.lastIndex = 0; + regexUnicode.lastIndex = 0; - while (reUnicode.test(str)) { + while (regexUnicode.test(str)) { ++result; } diff --git a/src/compat/_internal/unicodeToArray.ts b/src/compat/_internal/unicodeToArray.ts new file mode 100644 index 000000000..15920445e --- /dev/null +++ b/src/compat/_internal/unicodeToArray.ts @@ -0,0 +1,11 @@ +import { regexUnicode } from './regexUnicode.ts'; + +/** + * Splits a Unicode `string` into an array of its visible symbols (grapheme clusters). + * + * Ported from lodash to preserve identical behavior. + * @link https://github.com/lodash/lodash/blob/4.17.21-es/_unicodeToArray.js + */ +export function unicodeToArray(str: string): string[] { + return str.match(regexUnicode) || []; +} diff --git a/src/compat/string/split.spec.ts b/src/compat/string/split.spec.ts index ed7c745c9..9fd58bea3 100644 --- a/src/compat/string/split.spec.ts +++ b/src/compat/string/split.spec.ts @@ -32,4 +32,11 @@ describe('split', () => { const result = split('abc', 'b').join(','); expect(result).toBe('a,c'); }); + + it('should split unicode strings into symbols when `separator` is an empty string', () => { + expect(split('Ame\u0301lie', '')).toEqual(['A', 'm', 'e\u0301', 'l', 'i', 'e']); + expect(split('a😀b', '')).toEqual(['a', '😀', 'b']); + expect(split('👨‍👩‍👧🇰🇷', '')).toEqual(['👨‍👩‍👧', '🇰🇷']); + expect(split('a😀b', '', 2)).toEqual(['a', '😀']); + }); }); diff --git a/src/compat/string/split.ts b/src/compat/string/split.ts index d685a9f17..adf1573d5 100644 --- a/src/compat/string/split.ts +++ b/src/compat/string/split.ts @@ -1,3 +1,5 @@ +import { regexMultiByte } from '../_internal/regexMultiByte.ts'; +import { unicodeToArray } from '../_internal/unicodeToArray.ts'; import { toString } from '../util/toString.ts'; /** @@ -37,5 +39,11 @@ export function split(string: string | null | undefined, separator?: RegExp | st export function split(string: string | null | undefined, index: string | number, guard: object): string[]; export function split(string: any, separator?: any, limit?: any): string[] { - return toString(string).split(separator as string, limit); + const str = toString(string); + + if (separator === '' && regexMultiByte.test(str)) { + return unicodeToArray(str).slice(0, limit === undefined ? undefined : limit >>> 0); + } + + return str.split(separator as string, limit); }