From b35359f9586ce76796bfc5832ab528a9916b45cd Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Mon, 31 May 2021 16:49:31 +0200 Subject: [PATCH 01/14] Add failing test --- .../slate/test/transforms/delete/emojis/inline-end-reverse.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/slate/test/transforms/delete/emojis/inline-end-reverse.tsx b/packages/slate/test/transforms/delete/emojis/inline-end-reverse.tsx index 4d5682de76..3686d94569 100644 --- a/packages/slate/test/transforms/delete/emojis/inline-end-reverse.tsx +++ b/packages/slate/test/transforms/delete/emojis/inline-end-reverse.tsx @@ -10,7 +10,7 @@ export const input = ( - word๐Ÿ“› + word๐Ÿ‡ซ๐Ÿ‡ท From cfa5fe53b372780bd6c36da6eddc036c994067b4 Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Mon, 31 May 2021 16:50:06 +0200 Subject: [PATCH 02/14] Handle sequences --- package.json | 2 +- packages/slate/src/utils/string.ts | 236 ++++++++++++++++++++++------ packages/slate/test/utils/string.ts | 83 ++++++++++ 3 files changed, 274 insertions(+), 47 deletions(-) create mode 100644 packages/slate/test/utils/string.ts diff --git a/package.json b/package.json index 13603bccc1..00abf9690d 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "internal:release:next": "yarn prerelease && yarn changeset publish --tag next", "serve": "cd ./site && next", "start": "npm-run-all --parallel --print-label watch serve", - "test": "mocha --require ./config/babel/register.cjs ./packages/*/test/index.js", + "test": "mocha --require ./config/babel/register.cjs ./packages/*/test/**/*.{js,ts}", "test:custom": "mocha --require ./config/babel/register.cjs ./packages/slate/test/index.js", "test:inspect": "yarn test --inspect-brk", "test:integration": "run-p -r serve cypress:run", diff --git a/packages/slate/src/utils/string.ts b/packages/slate/src/utils/string.ts index fc54831baf..8f383cf3a3 100644 --- a/packages/slate/src/utils/string.ts +++ b/packages/slate/src/utils/string.ts @@ -5,9 +5,6 @@ const SPACE = /\s/ const PUNCTUATION = /[\u0021-\u0023\u0025-\u002A\u002C-\u002F\u003A\u003B\u003F\u0040\u005B-\u005D\u005F\u007B\u007D\u00A1\u00A7\u00AB\u00B6\u00B7\u00BB\u00BF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E3B\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]/ const CHAMELEON = /['\u2018\u2019]/ -const SURROGATE_START = 0xd800 -const SURROGATE_END = 0xdfff -const ZERO_WIDTH_JOINER = 0x200d /** * Get the distance to the end of the first character in a string of text. @@ -16,59 +13,119 @@ const ZERO_WIDTH_JOINER = 0x200d export const getCharacterDistance = (text: string): number => { let offset = 0 // prev types: - // SURR: surrogate pair - // MOD: modifier (technically also surrogate pair) + // NSEQ: non sequenceable codepoint. + // MOD: modifier // ZWJ: zero width joiner // VAR: variation selector - // BMP: sequenceable character from basic multilingual plane - let prev: 'SURR' | 'MOD' | 'ZWJ' | 'VAR' | 'BMP' | null = null - let charCode = text.charCodeAt(0) - - while (charCode) { - if (isSurrogate(charCode)) { - const modifier = isModifier(charCode, text, offset) - - // Early returns are the heart of this function, where we decide if previous and current - // codepoints should form a single character (in terms of how many of them should selection - // jump over). - if (prev === 'SURR' || prev === 'BMP') { + // BMP: sequenceable codepoint from basic multilingual plane + // RI: regional indicator + // KC: keycap + // Tag: tag + let prev: + | 'NSEQ' + | 'MOD' + | 'ZWJ' + | 'VAR' + | 'BMP' + | 'RI' + | 'KC' + | 'TAG' + | null = null + + while (true) { + const code = text.codePointAt(offset) + if (!code) break + + // Check if code point is part of a sequence. + if (isZWJ(code)) { + offset += 1 + prev = 'ZWJ' + + continue + } + + if (isKeycap(code)) { + if (prev === 'KC') { break } - offset += 2 - prev = modifier ? 'MOD' : 'SURR' - charCode = text.charCodeAt(offset) - // Absolutely fine to `continue` without any checks because if `charCode` is NaN (which - // is the case when out of `text` range), next `while` loop won"t execute and we"re done. + offset += 1 + prev = 'KC' continue } + if (isCombiningEnclosingKeycap(code)) { + offset += 1 + break + } - if (charCode === ZERO_WIDTH_JOINER) { + if (isVariationSelector(code)) { offset += 1 - prev = 'ZWJ' - charCode = text.charCodeAt(offset) + + if (prev === 'BMP') { + break + } + + prev = 'VAR' continue } - if (isBMPEmoji(charCode)) { + if (isBMPEmoji(code)) { if (prev && prev !== 'ZWJ' && prev !== 'VAR') { break } + offset += 1 prev = 'BMP' - charCode = text.charCodeAt(offset) continue } - if (isVariationSelector(charCode)) { - if (prev && prev !== 'ZWJ') { + if (isModifier(code)) { + offset += 2 + prev = 'MOD' + + continue + } + + if (isBlackFlag(code)) { + if (prev === 'TAG') { break } - offset += 1 - prev = 'VAR' - charCode = text.charCodeAt(offset) + offset += 2 + prev = 'TAG' + continue + } + if (prev === 'TAG' && isTag(code)) { + offset += 2 + + if (isCancelTag(code)) break + + continue + } + + if (isRegionalIndicator(code)) { + offset += 2 + + if (prev === 'RI') { + break + } + + prev = 'RI' + + continue + } + + if (!isBMP(code)) { + // If previous code point is not sequenceable, it means we are not in a + // sequence. + if (prev === 'NSEQ') { + break + } + + offset += 2 + prev = 'NSEQ' + continue } @@ -146,25 +203,14 @@ const isWordCharacter = (char: string, remaining: string): boolean => { return true } -/** - * Determines if `code` is a surrogate - */ - -const isSurrogate = (code: number): boolean => - SURROGATE_START <= code && code <= SURROGATE_END - /** * Does `code` form Modifier with next one. * * https://emojipedia.org/modifiers/ */ -const isModifier = (code: number, text: string, offset: number): boolean => { - if (code === 0xd83c) { - const next = text.charCodeAt(offset + 1) - return next <= 0xdfff && next >= 0xdffb - } - return false +const isModifier = (code: number): boolean => { + return code >= 0x1f3fb && code <= 0x1f3ff } /** @@ -177,6 +223,30 @@ const isVariationSelector = (code: number): boolean => { return code <= 0xfe0f && code >= 0xfe00 } +/** + * Is `code` a code point used in keycap sequence. + * + * https://emojipedia.org/emoji-keycap-sequence/ + */ + +const isKeycap = (code: number): boolean => { + return ( + (code >= 0x30 && code <= 0x39) || // digits + code === 0x23 || // number sign + code === 0x2a + ) +} + +/** + * Is `code` a Combining Enclosing Keycap. + * + * https://emojipedia.org/combining-enclosing-keycap/ + */ + +const isCombiningEnclosingKeycap = (code: number): boolean => { + return code === 0x20e3 +} + /** * Is `code` one of the BMP codes used in emoji sequences. * @@ -195,6 +265,80 @@ const isBMPEmoji = (code: number): boolean => { code === 0x2620 || // scull (โ˜ ) code === 0x2695 || // medical (โš•) code === 0x2708 || // plane (โœˆ๏ธ) - code === 0x25ef // large circle (โ—ฏ) + code === 0x25ef || // large circle (โ—ฏ) + code === 0x2b06 || // up arrow (โฌ†) + code === 0x2197 || // up-right arrow (โ†—) + code === 0x27a1 || // right arrow (โžก) + code === 0x2198 || // down-right arrow (โ†˜) + code === 0x2b07 || // down arrow (โฌ‡) + code === 0x2199 || // down-left arrow (โ†™) + code === 0x2b05 || // left arrow (โฌ…) + code === 0x2196 || // up-left arrow (โ†–) + code === 0x2195 || // up-down arrow (โ†•) + code === 0x2194 || // left-right arrow (โ†”) + code === 0x21a9 || // right arrow curving left (โ†ฉ) + code === 0x21aa || // left arrow curving right (โ†ช) + code === 0x2934 || // right arrow curving up (โคด) + code === 0x2935 // right arrow curving down (โคต) ) } + +/** + * Is `code` a Regional Indicator. + * + * https://en.wikipedia.org/wiki/Regional_indicator_symbol + */ + +const isRegionalIndicator = (code: number): boolean => { + return code >= 0x1f1e6 && code <= 0x1f1ff +} + +/** + * Is `code` from basic multilingual plane. + * + * https://codepoints.net/basic_multilingual_plane + */ + +const isBMP = (code: number): boolean => { + return code <= 0xffff +} + +/** + * Is `code` a Zero Width Joiner. + * + * https://emojipedia.org/zero-width-joiner/ + */ + +const isZWJ = (code: number): boolean => { + return code === 0x200d +} + +/** + * Is `code` a Black Flag. + * + * https://emojipedia.org/black-flag/ + */ + +const isBlackFlag = (code: number): boolean => { + return code === 0x1f3f4 +} + +/** + * Is `code` a Tag. + * + * https://emojipedia.org/emoji-tag-sequence/ + */ + +const isTag = (code: number): boolean => { + return code >= 0xe0000 && code <= 0xe007f +} + +/** + * Is `code` a Cancel Tag. + * + * https://emojipedia.org/cancel-tag/ + */ + +const isCancelTag = (code: number): boolean => { + return code === 0xe007f +} diff --git a/packages/slate/test/utils/string.ts b/packages/slate/test/utils/string.ts new file mode 100644 index 0000000000..8a5538fcba --- /dev/null +++ b/packages/slate/test/utils/string.ts @@ -0,0 +1,83 @@ +import assert from 'assert' +import { getCharacterDistance } from '../../src/utils/string' + +const regularCases = [ + // ['a', 1], + // ['0', 1], + // [' ', 1], + // ['๐Ÿ™‚', 2], + // ['โฌ…๏ธ', 2], + ['๐Ÿด', 2], +] as const + +const sequences = [ + ['๐Ÿ‘โ€๐Ÿ—จ', 5], + ['๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง', 11], + ['๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆณ', 7], +] as const + +const regionalIndicators = [ + '๐Ÿ‡ง๐Ÿ‡ช', + '๐Ÿ‡ง๐Ÿ‡ซ', + '๐Ÿ‡ง๐Ÿ‡ฌ', + '๐Ÿ‡ง๐Ÿ‡ญ', + '๐Ÿ‡ง๐Ÿ‡ฎ', + '๐Ÿ‡ง๐Ÿ‡ฏ', + '๐Ÿ‡ง๐Ÿ‡ฑ', + '๐Ÿ‡ง๐Ÿ‡ฒ', + '๐Ÿ‡ง๐Ÿ‡ณ', + '๐Ÿ‡ง๐Ÿ‡ด', +] + +const keycaps = [ + '#๏ธโƒฃ', + '*๏ธโƒฃ', + '0๏ธโƒฃ', + '1๏ธโƒฃ', + '2๏ธโƒฃ', + '3๏ธโƒฃ', + '4๏ธโƒฃ', + '5๏ธโƒฃ', + '6๏ธโƒฃ', + '7๏ธโƒฃ', + '8๏ธโƒฃ', + '9๏ธโƒฃ', +] + +const tags = [ + ['๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ', 14], + ['๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ', 14], + ['๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ', 14], +] as const + +describe('getCharacterDistance', () => { + regularCases.forEach(([str, length]) => { + it(`regular case ${str}`, () => { + assert.strictEqual(getCharacterDistance(str + str), length) + }) + }) + + regionalIndicators.forEach(str => { + it(`regional indicator ${str}`, () => { + assert.strictEqual(getCharacterDistance(str + str), 4) + }) + }) + + keycaps.forEach(str => { + it(`keycap ${str}`, () => { + assert.strictEqual(getCharacterDistance(str + str), 3) + }) + }) + + tags.forEach(([str, length]) => { + it(`tag ${str}`, () => { + assert.strictEqual(getCharacterDistance(str + str), length) + }) + }) + + sequences.forEach(([str, length]) => { + it(`sequence ${str}`, () => { + assert.strictEqual(getCharacterDistance(str + str), length) + }) + }) +}) From fc5dc363deb77701816dc38927851aae4285f0dc Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Wed, 2 Jun 2021 11:09:26 +0200 Subject: [PATCH 03/14] Uncomment test cases --- packages/slate/test/utils/string.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/slate/test/utils/string.ts b/packages/slate/test/utils/string.ts index 8a5538fcba..6ac592dcee 100644 --- a/packages/slate/test/utils/string.ts +++ b/packages/slate/test/utils/string.ts @@ -2,11 +2,11 @@ import assert from 'assert' import { getCharacterDistance } from '../../src/utils/string' const regularCases = [ - // ['a', 1], - // ['0', 1], - // [' ', 1], - // ['๐Ÿ™‚', 2], - // ['โฌ…๏ธ', 2], + ['a', 1], + ['0', 1], + [' ', 1], + ['๐Ÿ™‚', 2], + ['โฌ…๏ธ', 2], ['๐Ÿด', 2], ] as const From d29412d344b8c33179bec6bda7a95810f63e371a Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Tue, 8 Jun 2021 16:38:56 +0200 Subject: [PATCH 04/14] Handle RTL unicode sequences --- packages/slate/src/interfaces/editor.ts | 13 ++- packages/slate/src/utils/string.ts | 131 +++++++++++++++--------- packages/slate/test/utils/string.ts | 86 +++++++++++----- 3 files changed, 150 insertions(+), 80 deletions(-) diff --git a/packages/slate/src/interfaces/editor.ts b/packages/slate/src/interfaces/editor.ts index cbc3cd85b1..c38562fe81 100644 --- a/packages/slate/src/interfaces/editor.ts +++ b/packages/slate/src/interfaces/editor.ts @@ -25,7 +25,7 @@ import { POINT_REFS, RANGE_REFS, } from '../utils/weak-maps' -import { getWordDistance, getCharacterDistance } from '../utils/string' +import { getWordDistance, getCharacterDistance, split } from '../utils/string' import { Descendant } from './node' import { Element } from './element' @@ -1337,7 +1337,6 @@ export const Editor: EditorInterface = { : Editor.start(editor, path) blockText = Editor.string(editor, { anchor: s, focus: e }, { voids }) - blockText = reverse ? reverseText(blockText) : blockText isNewBlock = true } } @@ -1378,8 +1377,8 @@ export const Editor: EditorInterface = { // otherwise advance blockText forward by the new `distance`. if (distance === 0) { if (blockText === '') break - distance = calcDistance(blockText, unit) - blockText = blockText.slice(distance) + distance = calcDistance(blockText, unit, reverse) + blockText = split(blockText, distance, reverse)[1] } // Advance `leafText` by the current `distance`. @@ -1410,11 +1409,11 @@ export const Editor: EditorInterface = { // Helper: // Return the distance in offsets for a step of size `unit` on given string. - function calcDistance(text: string, unit: string) { + function calcDistance(text: string, unit: string, reverse: boolean) { if (unit === 'character') { - return getCharacterDistance(text) + return getCharacterDistance(text, reverse) } else if (unit === 'word') { - return getWordDistance(text) + return getWordDistance(text, reverse) } else if (unit === 'line' || unit === 'block') { return text.length } diff --git a/packages/slate/src/utils/string.ts b/packages/slate/src/utils/string.ts index 8f383cf3a3..8b0fb77b3a 100644 --- a/packages/slate/src/utils/string.ts +++ b/packages/slate/src/utils/string.ts @@ -10,8 +10,10 @@ const CHAMELEON = /['\u2018\u2019]/ * Get the distance to the end of the first character in a string of text. */ -export const getCharacterDistance = (text: string): number => { - let offset = 0 +export const getCharacterDistance = (text: string, isRTL = false): number => { + const isLTR = !isRTL + + let dist = 0 // prev types: // NSEQ: non sequenceable codepoint. // MOD: modifier @@ -32,36 +34,43 @@ export const getCharacterDistance = (text: string): number => { | 'TAG' | null = null - while (true) { - const code = text.codePointAt(offset) + const codepoints = Array.from(text) + for (let i = 0; i < codepoints.length; i++) { + const j = isRTL ? codepoints.length - 1 - i : i + const codepoint = codepoints[j] + + const code = codepoint.codePointAt(0) if (!code) break - // Check if code point is part of a sequence. + // Check if codepoint is part of a sequence. if (isZWJ(code)) { - offset += 1 + dist += codepoint.length prev = 'ZWJ' continue } - if (isKeycap(code)) { + const [isKeycapStart, isKeycapEnd] = isLTR + ? [isKeycap, isCombiningEnclosingKeycap] + : [isCombiningEnclosingKeycap, isKeycap] + if (isKeycapStart(code)) { if (prev === 'KC') { break } - offset += 1 + dist += codepoint.length prev = 'KC' continue } - if (isCombiningEnclosingKeycap(code)) { - offset += 1 + if (isKeycapEnd(code)) { + dist += codepoint.length break } if (isVariationSelector(code)) { - offset += 1 + dist += codepoint.length - if (prev === 'BMP') { + if (isLTR && prev === 'BMP') { break } @@ -71,41 +80,48 @@ export const getCharacterDistance = (text: string): number => { } if (isBMPEmoji(code)) { - if (prev && prev !== 'ZWJ' && prev !== 'VAR') { + if (isLTR && prev && prev !== 'ZWJ' && prev !== 'VAR') { break } - offset += 1 - prev = 'BMP' + dist += codepoint.length + + if (isRTL && prev === 'VAR') { + break + } + prev = 'BMP' continue } if (isModifier(code)) { - offset += 2 + dist += codepoint.length prev = 'MOD' continue } - if (isBlackFlag(code)) { - if (prev === 'TAG') { - break - } - offset += 2 + const [isTagStart, isTagEnd] = isLTR + ? [isBlackFlag, isCancelTag] + : [isCancelTag, isBlackFlag] + if (isTagStart(code)) { + if (prev === 'TAG') break + + dist += codepoint.length prev = 'TAG' continue } + if (isTagEnd(code)) { + dist += codepoint.length + break + } if (prev === 'TAG' && isTag(code)) { - offset += 2 - - if (isCancelTag(code)) break - + dist += codepoint.length continue } if (isRegionalIndicator(code)) { - offset += 2 + dist += codepoint.length if (prev === 'RI') { break @@ -123,7 +139,7 @@ export const getCharacterDistance = (text: string): number => { break } - offset += 2 + dist += codepoint.length prev = 'NSEQ' continue @@ -131,8 +147,8 @@ export const getCharacterDistance = (text: string): number => { // Modifier 'groups up' with what ever character is before that (even whitespace), need to // look ahead. - if (prev === 'MOD') { - offset += 1 + if (isLTR && prev === 'MOD') { + dist += codepoint.length break } @@ -140,37 +156,52 @@ export const getCharacterDistance = (text: string): number => { break } - return offset || 1 + return dist || 1 } /** * Get the distance to the end of the first word in a string of text. */ -export const getWordDistance = (text: string): number => { - let length = 0 - let i = 0 +export const getWordDistance = (text: string, isRTL = false): number => { + let dist = 0 let started = false - let char - while ((char = text.charAt(i))) { - const l = getCharacterDistance(char) - char = text.slice(i, i + l) - const rest = text.slice(i + l) + while (text.length > 0) { + const charDist = getCharacterDistance(text, isRTL) + const [char, remaining] = split(text, charDist, isRTL) - if (isWordCharacter(char, rest)) { + if (isWordCharacter(char, remaining, isRTL)) { started = true - length += l + dist += charDist } else if (!started) { - length += l + dist += charDist } else { break } - i += l + text = remaining + } + + return dist +} + +/** + * Split a string at a given distance starting from the end when `isRTL` is set + * to `true`. + */ + +export const split = ( + str: string, + dist: number, + isRTL = false +): [string, string] => { + if (isRTL) { + const at = str.length - dist + return [str.slice(at, str.length), str.slice(0, at)] } - return length + return [str.slice(0, dist), str.slice(dist)] } /** @@ -178,7 +209,11 @@ export const getWordDistance = (text: string): number => { * because sometimes you must read subsequent characters to truly determine it. */ -const isWordCharacter = (char: string, remaining: string): boolean => { +const isWordCharacter = ( + char: string, + remaining: string, + isRTL = false +): boolean => { if (SPACE.test(char)) { return false } @@ -186,12 +221,10 @@ const isWordCharacter = (char: string, remaining: string): boolean => { // Chameleons count as word characters as long as they're in a word, so // recurse to see if the next one is a word character or not. if (CHAMELEON.test(char)) { - let next = remaining.charAt(0) - const length = getCharacterDistance(next) - next = remaining.slice(0, length) - const rest = remaining.slice(length) + const charDist = getCharacterDistance(remaining, isRTL) + const [nextChar, nextRemaining] = split(remaining, charDist, isRTL) - if (isWordCharacter(next, rest)) { + if (isWordCharacter(nextChar, nextRemaining, isRTL)) { return true } } diff --git a/packages/slate/test/utils/string.ts b/packages/slate/test/utils/string.ts index 6ac592dcee..8691eef664 100644 --- a/packages/slate/test/utils/string.ts +++ b/packages/slate/test/utils/string.ts @@ -1,7 +1,7 @@ import assert from 'assert' -import { getCharacterDistance } from '../../src/utils/string' +import { getCharacterDistance, getWordDistance } from '../../src/utils/string' -const regularCases = [ +const codepoints = [ ['a', 1], ['0', 1], [' ', 1], @@ -10,13 +10,13 @@ const regularCases = [ ['๐Ÿด', 2], ] as const -const sequences = [ +const zwjSequences = [ ['๐Ÿ‘โ€๐Ÿ—จ', 5], ['๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง', 11], ['๐Ÿ‘จ๐Ÿฟโ€๐Ÿฆณ', 7], ] as const -const regionalIndicators = [ +const regionalIndicatorSequences = [ '๐Ÿ‡ง๐Ÿ‡ช', '๐Ÿ‡ง๐Ÿ‡ซ', '๐Ÿ‡ง๐Ÿ‡ฌ', @@ -29,7 +29,7 @@ const regionalIndicators = [ '๐Ÿ‡ง๐Ÿ‡ด', ] -const keycaps = [ +const keycapSequences = [ '#๏ธโƒฃ', '*๏ธโƒฃ', '0๏ธโƒฃ', @@ -44,40 +44,78 @@ const keycaps = [ '9๏ธโƒฃ', ] -const tags = [ +const tagSequences = [ ['๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ', 14], ['๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ', 14], ['๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ', 14], ] as const -describe('getCharacterDistance', () => { - regularCases.forEach(([str, length]) => { - it(`regular case ${str}`, () => { - assert.strictEqual(getCharacterDistance(str + str), length) +const dirs = ['ltr', 'rtl'] + +dirs.forEach(dir => { + const isRTL = dir === 'rtl' + + describe(`getCharacterDistance - ${dir}`, () => { + codepoints.forEach(([str, dist]) => { + it(str, () => { + assert.strictEqual(getCharacterDistance(str + str, isRTL), dist) + }) }) - }) - regionalIndicators.forEach(str => { - it(`regional indicator ${str}`, () => { - assert.strictEqual(getCharacterDistance(str + str), 4) + zwjSequences.forEach(([str, dist]) => { + it(str, () => { + assert.strictEqual(getCharacterDistance(str + str, isRTL), dist) + }) + }) + + regionalIndicatorSequences.forEach(str => { + it(str, () => { + assert.strictEqual(getCharacterDistance(str + str, isRTL), 4) + }) + }) + + keycapSequences.forEach(str => { + it(str, () => { + assert.strictEqual(getCharacterDistance(str + str, isRTL), 3) + }) }) - }) - keycaps.forEach(str => { - it(`keycap ${str}`, () => { - assert.strictEqual(getCharacterDistance(str + str), 3) + tagSequences.forEach(([str, dist]) => { + it(str, () => { + assert.strictEqual(getCharacterDistance(str + str, isRTL), dist) + }) }) }) +}) + +const ltrCases = [ + ['hello foobarbaz', 5], + ['๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ ๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ', 28], + ["Don't do this", 5], + ["I'm ok", 3], +] as const - tags.forEach(([str, length]) => { - it(`tag ${str}`, () => { - assert.strictEqual(getCharacterDistance(str + str), length) +const rtlCases = [ + ['hello foobarbaz', 9], + ['๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ ๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ', 14], + ["Don't", 5], + ["Don't do this", 4], + ["I'm", 3], + ['Tags ๐Ÿด๓ ง๓ ข๓ ฅ๓ ฎ๓ ง๓ ฟ๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ', 28], +] as const + +describe(`getWordDistance - ltr`, () => { + ltrCases.forEach(([str, dist]) => { + it(str, () => { + assert.strictEqual(getWordDistance(str), dist) }) }) +}) - sequences.forEach(([str, length]) => { - it(`sequence ${str}`, () => { - assert.strictEqual(getCharacterDistance(str + str), length) +describe(`getWordDistance - rtl`, () => { + rtlCases.forEach(([str, dist]) => { + it(str, () => { + assert.strictEqual(getWordDistance(str, true), dist) }) }) }) From 0f0ec1ff0f9b5b532f7b9d88759d6915723ff1c8 Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Tue, 8 Jun 2021 16:47:25 +0200 Subject: [PATCH 05/14] Remove esrever --- config/rollup/rollup.config.js | 1 - packages/slate/package.json | 2 -- packages/slate/src/interfaces/editor.ts | 1 - yarn.lock | 10 ---------- 4 files changed, 14 deletions(-) diff --git a/config/rollup/rollup.config.js b/config/rollup/rollup.config.js index 8a68303666..0e70a4c3ef 100644 --- a/config/rollup/rollup.config.js +++ b/config/rollup/rollup.config.js @@ -58,7 +58,6 @@ function configure(pkg, env, target) { // we have to manually specify named exports here for them to work. // https://github.com/rollup/rollup-plugin-commonjs#custom-named-exports namedExports: { - esrever: ['reverse'], 'react-dom': ['findDOMNode'], 'react-dom/server': ['renderToStaticMarkup'], }, diff --git a/packages/slate/package.json b/packages/slate/package.json index 81453448a2..83076e5824 100644 --- a/packages/slate/package.json +++ b/packages/slate/package.json @@ -14,8 +14,6 @@ "dist/" ], "dependencies": { - "@types/esrever": "^0.2.0", - "esrever": "^0.2.0", "fast-deep-equal": "^3.1.3", "immer": "^8.0.1", "is-plain-object": "^3.0.0", diff --git a/packages/slate/src/interfaces/editor.ts b/packages/slate/src/interfaces/editor.ts index c38562fe81..3d37598192 100644 --- a/packages/slate/src/interfaces/editor.ts +++ b/packages/slate/src/interfaces/editor.ts @@ -1,5 +1,4 @@ import isPlainObject from 'is-plain-object' -import { reverse as reverseText } from 'esrever' import { Ancestor, diff --git a/yarn.lock b/yarn.lock index 771e2ca19e..f744b17e62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2469,11 +2469,6 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== -"@types/esrever@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@types/esrever/-/esrever-0.2.0.tgz#96404a2284b2c7527f08a1e957f8a31705f9880f" - integrity sha512-5NI6TeGzVEy/iBcuYtcPzzIC6EqlfQ2+UZ54vT0ulq8bPNGAy8UJD+XcsAyEOcnYFUjOVWuUV+k4/rVkxt9/XQ== - "@types/estree@*": version "0.0.45" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884" @@ -5447,11 +5442,6 @@ esrecurse@^4.1.0: dependencies: estraverse "^4.1.0" -esrever@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/esrever/-/esrever-0.2.0.tgz#96e9d28f4f1b1a76784cd5d490eaae010e7407b8" - integrity sha1-lunSj08bGnZ4TNXUkOquAQ50B7g= - estraverse@^4.1.0, estraverse@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" From d1ffe9b2e75f749b7fa82eca16ea79dd41f78312 Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Tue, 8 Jun 2021 17:24:31 +0200 Subject: [PATCH 06/14] Add tests --- .../transforms/move/emojis/keycap-reverse.tsx | 32 +++++++++++++++++++ .../test/transforms/move/emojis/keycap.tsx | 32 +++++++++++++++++++ .../transforms/move/emojis/ri-reverse.tsx | 32 +++++++++++++++++++ .../slate/test/transforms/move/emojis/ri.tsx | 32 +++++++++++++++++++ .../transforms/move/emojis/tag-reverse.tsx | 32 +++++++++++++++++++ .../slate/test/transforms/move/emojis/tag.tsx | 32 +++++++++++++++++++ .../transforms/move/emojis/zwj-reverse.tsx | 32 +++++++++++++++++++ .../slate/test/transforms/move/emojis/zwj.tsx | 32 +++++++++++++++++++ 8 files changed, 256 insertions(+) create mode 100644 packages/slate/test/transforms/move/emojis/keycap-reverse.tsx create mode 100644 packages/slate/test/transforms/move/emojis/keycap.tsx create mode 100644 packages/slate/test/transforms/move/emojis/ri-reverse.tsx create mode 100644 packages/slate/test/transforms/move/emojis/ri.tsx create mode 100644 packages/slate/test/transforms/move/emojis/tag-reverse.tsx create mode 100644 packages/slate/test/transforms/move/emojis/tag.tsx create mode 100644 packages/slate/test/transforms/move/emojis/zwj-reverse.tsx create mode 100644 packages/slate/test/transforms/move/emojis/zwj.tsx diff --git a/packages/slate/test/transforms/move/emojis/keycap-reverse.tsx b/packages/slate/test/transforms/move/emojis/keycap-reverse.tsx new file mode 100644 index 0000000000..dd04335f14 --- /dev/null +++ b/packages/slate/test/transforms/move/emojis/keycap-reverse.tsx @@ -0,0 +1,32 @@ +/** @jsx jsx */ +import { Transforms } from 'slate' +import { jsx } from '../../..' + +export const run = editor => { + Transforms.move(editor, { reverse: true }) +} +export const input = ( + + + + + word5๏ธโƒฃ + + + + + +) +export const output = ( + + + + + word + + 5๏ธโƒฃ + + + + +) diff --git a/packages/slate/test/transforms/move/emojis/keycap.tsx b/packages/slate/test/transforms/move/emojis/keycap.tsx new file mode 100644 index 0000000000..17fa329854 --- /dev/null +++ b/packages/slate/test/transforms/move/emojis/keycap.tsx @@ -0,0 +1,32 @@ +/** @jsx jsx */ +import { Transforms } from 'slate' +import { jsx } from '../../..' + +export const run = editor => { + Transforms.move(editor) +} +export const input = ( + + + + + word + + 5๏ธโƒฃ + + + + +) +export const output = ( + + + + + word5๏ธโƒฃ + + + + + +) diff --git a/packages/slate/test/transforms/move/emojis/ri-reverse.tsx b/packages/slate/test/transforms/move/emojis/ri-reverse.tsx new file mode 100644 index 0000000000..b1fd30d703 --- /dev/null +++ b/packages/slate/test/transforms/move/emojis/ri-reverse.tsx @@ -0,0 +1,32 @@ +/** @jsx jsx */ +import { Transforms } from 'slate' +import { jsx } from '../../..' + +export const run = editor => { + Transforms.move(editor, { reverse: true }) +} +export const input = ( + + + + + word๐Ÿ‡ซ๐Ÿ‡ท + + + + + +) +export const output = ( + + + + + word + + ๐Ÿ‡ซ๐Ÿ‡ท + + + + +) diff --git a/packages/slate/test/transforms/move/emojis/ri.tsx b/packages/slate/test/transforms/move/emojis/ri.tsx new file mode 100644 index 0000000000..b2bc205b4b --- /dev/null +++ b/packages/slate/test/transforms/move/emojis/ri.tsx @@ -0,0 +1,32 @@ +/** @jsx jsx */ +import { Transforms } from 'slate' +import { jsx } from '../../..' + +export const run = editor => { + Transforms.move(editor) +} +export const input = ( + + + + + word + + ๐Ÿ‡ซ๐Ÿ‡ท + + + + +) +export const output = ( + + + + + word๐Ÿ‡ซ๐Ÿ‡ท + + + + + +) diff --git a/packages/slate/test/transforms/move/emojis/tag-reverse.tsx b/packages/slate/test/transforms/move/emojis/tag-reverse.tsx new file mode 100644 index 0000000000..200b430c1e --- /dev/null +++ b/packages/slate/test/transforms/move/emojis/tag-reverse.tsx @@ -0,0 +1,32 @@ +/** @jsx jsx */ +import { Transforms } from 'slate' +import { jsx } from '../../..' + +export const run = editor => { + Transforms.move(editor, { reverse: true }) +} +export const input = ( + + + + + word๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ + + + + + +) +export const output = ( + + + + + word + + ๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ + + + + +) diff --git a/packages/slate/test/transforms/move/emojis/tag.tsx b/packages/slate/test/transforms/move/emojis/tag.tsx new file mode 100644 index 0000000000..6b9985bda9 --- /dev/null +++ b/packages/slate/test/transforms/move/emojis/tag.tsx @@ -0,0 +1,32 @@ +/** @jsx jsx */ +import { Transforms } from 'slate' +import { jsx } from '../../..' + +export const run = editor => { + Transforms.move(editor) +} +export const input = ( + + + + + word + + ๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ + + + + +) +export const output = ( + + + + + word๐Ÿด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ + + + + + +) diff --git a/packages/slate/test/transforms/move/emojis/zwj-reverse.tsx b/packages/slate/test/transforms/move/emojis/zwj-reverse.tsx new file mode 100644 index 0000000000..720caf1aef --- /dev/null +++ b/packages/slate/test/transforms/move/emojis/zwj-reverse.tsx @@ -0,0 +1,32 @@ +/** @jsx jsx */ +import { Transforms } from 'slate' +import { jsx } from '../../..' + +export const run = editor => { + Transforms.move(editor, { reverse: true }) +} +export const input = ( + + + + + word๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง + + + + + +) +export const output = ( + + + + + word + + ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง + + + + +) diff --git a/packages/slate/test/transforms/move/emojis/zwj.tsx b/packages/slate/test/transforms/move/emojis/zwj.tsx new file mode 100644 index 0000000000..bf0269db9b --- /dev/null +++ b/packages/slate/test/transforms/move/emojis/zwj.tsx @@ -0,0 +1,32 @@ +/** @jsx jsx */ +import { Transforms } from 'slate' +import { jsx } from '../../..' + +export const run = editor => { + Transforms.move(editor) +} +export const input = ( + + + + + word + + ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง + + + + +) +export const output = ( + + + + + word๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง + + + + + +) From c466af1a9fed5bb81a535f06c9c571dbf3877e95 Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Wed, 9 Jun 2021 09:01:56 +0200 Subject: [PATCH 07/14] Use iterator instead of Array.from --- packages/slate/src/utils/string.ts | 52 ++++++++++++++++++++++++++--- packages/slate/test/utils/string.ts | 25 +++++++++++++- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/slate/src/utils/string.ts b/packages/slate/src/utils/string.ts index 8b0fb77b3a..ae76663bdf 100644 --- a/packages/slate/src/utils/string.ts +++ b/packages/slate/src/utils/string.ts @@ -10,7 +10,7 @@ const CHAMELEON = /['\u2018\u2019]/ * Get the distance to the end of the first character in a string of text. */ -export const getCharacterDistance = (text: string, isRTL = false): number => { +export const getCharacterDistance = (str: string, isRTL = false): number => { const isLTR = !isRTL let dist = 0 @@ -34,11 +34,9 @@ export const getCharacterDistance = (text: string, isRTL = false): number => { | 'TAG' | null = null - const codepoints = Array.from(text) - for (let i = 0; i < codepoints.length; i++) { - const j = isRTL ? codepoints.length - 1 - i : i - const codepoint = codepoints[j] + const codepoints = isLTR ? str : codepointsIteratorRTL(str) + for (const codepoint of codepoints) { const code = codepoint.codePointAt(0) if (!code) break @@ -375,3 +373,47 @@ const isTag = (code: number): boolean => { const isCancelTag = (code: number): boolean => { return code === 0xe007f } + +/** + * Iterate on codepoints from right to left. + */ + +export const codepointsIteratorRTL = function*(str: string) { + const end = str.length - 1 + + for (let i = 0; i < str.length; i++) { + const char1 = str.charAt(end - i) + + if (isLowSurrogate(char1.charCodeAt(0))) { + const char2 = str.charAt(end - i - 1) + if (isHighSurrogate(char2.charCodeAt(0))) { + yield char2 + char1 + + i++ + continue + } + } + + yield char1 + } +} + +/** + * Is `charCode` a high surrogate. + * + * https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates + */ + +const isHighSurrogate = (charCode: number) => { + return charCode >= 0xd800 && charCode <= 0xdbff +} + +/** + * Is `charCode` a low surrogate. + * + * https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates + */ + +const isLowSurrogate = (charCode: number) => { + return charCode >= 0xdc00 && charCode <= 0xdfff +} diff --git a/packages/slate/test/utils/string.ts b/packages/slate/test/utils/string.ts index 8691eef664..bef02c6d40 100644 --- a/packages/slate/test/utils/string.ts +++ b/packages/slate/test/utils/string.ts @@ -1,5 +1,9 @@ import assert from 'assert' -import { getCharacterDistance, getWordDistance } from '../../src/utils/string' +import { + getCharacterDistance, + getWordDistance, + codepointsIteratorRTL, +} from '../../src/utils/string' const codepoints = [ ['a', 1], @@ -119,3 +123,22 @@ describe(`getWordDistance - rtl`, () => { }) }) }) + +const cases = [ + ...[...codepoints, ...zwjSequences, ...tagSequences, ...rtlCases].map( + ([str]) => str + ), + ...keycapSequences, + ...regionalIndicatorSequences, +] + +describe('codepointsIteratorRTL', () => { + cases.forEach(str => { + it(str, () => { + const arr1 = [...codepointsIteratorRTL(str)] + const arr2 = Array.from(str).reverse() + + assert.deepStrictEqual(arr1, arr2) + }) + }) +}) From e1ec58986ae7424f8a4e5587f152cc1af62cb52b Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Thu, 10 Jun 2021 10:52:52 +0200 Subject: [PATCH 08/14] Add changeset --- .changeset/lucky-schools-bake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lucky-schools-bake.md diff --git a/.changeset/lucky-schools-bake.md b/.changeset/lucky-schools-bake.md new file mode 100644 index 0000000000..b55c00e4a5 --- /dev/null +++ b/.changeset/lucky-schools-bake.md @@ -0,0 +1,5 @@ +--- +'slate': minor +--- + +Add support for [flag](https://emojipedia.org/emoji-flag-sequence/), [keycap](https://emojipedia.org/emoji-keycap-sequence/) and [tag](https://emojipedia.org/emoji-tag-sequence/) unicode sequences. From 06c0828941c8829f92217cb8df04a52481f13b10 Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Thu, 12 Aug 2021 12:13:19 +0200 Subject: [PATCH 09/14] Rename split to splitByCharacterDistance --- packages/slate/src/interfaces/editor.ts | 14 ++++++++++++-- packages/slate/src/utils/string.ts | 16 ++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/slate/src/interfaces/editor.ts b/packages/slate/src/interfaces/editor.ts index d04c99d161..95a2f80b34 100644 --- a/packages/slate/src/interfaces/editor.ts +++ b/packages/slate/src/interfaces/editor.ts @@ -24,7 +24,11 @@ import { POINT_REFS, RANGE_REFS, } from '../utils/weak-maps' -import { getWordDistance, getCharacterDistance, split } from '../utils/string' +import { + getWordDistance, + getCharacterDistance, + splitByCharacterDistance, +} from '../utils/string' import { Descendant } from './node' import { Element } from './element' @@ -1378,7 +1382,13 @@ export const Editor: EditorInterface = { if (distance === 0) { if (blockText === '') break distance = calcDistance(blockText, unit, reverse) - blockText = split(blockText, distance, reverse)[1] + // Split the string at the previously found distance and use the + // remaining string for the next iteration. + blockText = splitByCharacterDistance( + blockText, + distance, + reverse + )[1] } // Advance `leafText` by the current `distance`. diff --git a/packages/slate/src/utils/string.ts b/packages/slate/src/utils/string.ts index ae76663bdf..91f3878424 100644 --- a/packages/slate/src/utils/string.ts +++ b/packages/slate/src/utils/string.ts @@ -167,7 +167,7 @@ export const getWordDistance = (text: string, isRTL = false): number => { while (text.length > 0) { const charDist = getCharacterDistance(text, isRTL) - const [char, remaining] = split(text, charDist, isRTL) + const [char, remaining] = splitByCharacterDistance(text, charDist, isRTL) if (isWordCharacter(char, remaining, isRTL)) { started = true @@ -185,14 +185,14 @@ export const getWordDistance = (text: string, isRTL = false): number => { } /** - * Split a string at a given distance starting from the end when `isRTL` is set - * to `true`. + * Split a string in two parts at a given distance starting from the end when + * `isRTL` is set to `true`. */ -export const split = ( +export const splitByCharacterDistance = ( str: string, dist: number, - isRTL = false + isRTL?: boolean ): [string, string] => { if (isRTL) { const at = str.length - dist @@ -220,7 +220,11 @@ const isWordCharacter = ( // recurse to see if the next one is a word character or not. if (CHAMELEON.test(char)) { const charDist = getCharacterDistance(remaining, isRTL) - const [nextChar, nextRemaining] = split(remaining, charDist, isRTL) + const [nextChar, nextRemaining] = splitByCharacterDistance( + remaining, + charDist, + isRTL + ) if (isWordCharacter(nextChar, nextRemaining, isRTL)) { return true From 42c5aa1238c90b7c4c520ce4ce8de24c89dc493c Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Thu, 12 Aug 2021 12:13:30 +0200 Subject: [PATCH 10/14] Make reverse optional --- packages/slate/src/interfaces/editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/slate/src/interfaces/editor.ts b/packages/slate/src/interfaces/editor.ts index 95a2f80b34..beef431c6e 100644 --- a/packages/slate/src/interfaces/editor.ts +++ b/packages/slate/src/interfaces/editor.ts @@ -1419,7 +1419,7 @@ export const Editor: EditorInterface = { // Helper: // Return the distance in offsets for a step of size `unit` on given string. - function calcDistance(text: string, unit: string, reverse: boolean) { + function calcDistance(text: string, unit: string, reverse?: boolean) { if (unit === 'character') { return getCharacterDistance(text, reverse) } else if (unit === 'word') { From cfa7282735dccf9a4b8e3f8a5c78bdc3f1097ee9 Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Thu, 12 Aug 2021 12:13:42 +0200 Subject: [PATCH 11/14] Fix casing --- packages/slate/src/utils/string.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/slate/src/utils/string.ts b/packages/slate/src/utils/string.ts index 91f3878424..61e629dfb8 100644 --- a/packages/slate/src/utils/string.ts +++ b/packages/slate/src/utils/string.ts @@ -22,7 +22,7 @@ export const getCharacterDistance = (str: string, isRTL = false): number => { // BMP: sequenceable codepoint from basic multilingual plane // RI: regional indicator // KC: keycap - // Tag: tag + // TAG: tag let prev: | 'NSEQ' | 'MOD' From 5170085a393cb49db61b78b593c481ddc847c61b Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Thu, 12 Aug 2021 14:27:21 +0200 Subject: [PATCH 12/14] Fix yarn.lock --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index f6ef5210f8..0df7a00eb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5715,7 +5715,7 @@ faker@^4.1.0: resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f" integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8= -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== From 651b9fce1c0a1f2b7f849dd10b6e18f870c6caa8 Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Thu, 12 Aug 2021 14:42:25 +0200 Subject: [PATCH 13/14] Fix tests --- packages/slate/test/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/slate/test/index.js b/packages/slate/test/index.js index 81fc835088..21cf698b8e 100644 --- a/packages/slate/test/index.js +++ b/packages/slate/test/index.js @@ -37,7 +37,7 @@ describe('slate', () => { assert.deepEqual(editor.children, output.children) assert.deepEqual(editor.selection, output.selection) }) - fixtures(__dirname, 'utils', ({ module }) => { + fixtures(__dirname, 'utils/deep-equal', ({ module }) => { let { input, test, output } = module if (Editor.isEditor(input)) { input = withTest(input) From f584aeb9701fa10d4410c9222909eb95482dfe2a Mon Sep 17 00:00:00 2001 From: Jimmy Oliger Date: Thu, 12 Aug 2021 14:49:37 +0200 Subject: [PATCH 14/14] Remove fast-deep-equal after bad merge --- packages/slate/package.json | 1 - yarn.lock | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/slate/package.json b/packages/slate/package.json index 7bc8f02141..8b20080b8c 100644 --- a/packages/slate/package.json +++ b/packages/slate/package.json @@ -14,7 +14,6 @@ "dist/" ], "dependencies": { - "fast-deep-equal": "^3.1.3", "immer": "^8.0.1", "is-plain-object": "^3.0.0", "tiny-warning": "^1.0.3" diff --git a/yarn.lock b/yarn.lock index 0df7a00eb3..f6ef5210f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5715,7 +5715,7 @@ faker@^4.1.0: resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f" integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8= -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: +fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==