1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-01-17 13:38:37 +01:00

Add unicode sequences support (#4326)

* Add failing test

* Handle sequences

* Uncomment test cases

* Handle RTL unicode sequences

* Remove esrever

* Add tests

* Use iterator instead of Array.from

* Add changeset

* Rename split to splitByCharacterDistance

* Make reverse optional

* Fix casing

* Fix yarn.lock

* Fix tests

* Remove fast-deep-equal after bad merge
This commit is contained in:
Jimmy Oliger 2021-08-13 12:50:52 +02:00 committed by GitHub
parent a1f925bddf
commit 0025900349
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 722 additions and 99 deletions

View File

@ -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.

View File

@ -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'],
},

View File

@ -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",

View File

@ -14,8 +14,6 @@
"dist/"
],
"dependencies": {
"@types/esrever": "^0.2.0",
"esrever": "^0.2.0",
"immer": "^8.0.1",
"is-plain-object": "^3.0.0",
"tiny-warning": "^1.0.3"

View File

@ -1,5 +1,4 @@
import isPlainObject from 'is-plain-object'
import { reverse as reverseText } from 'esrever'
import {
Ancestor,
@ -25,7 +24,11 @@ import {
POINT_REFS,
RANGE_REFS,
} from '../utils/weak-maps'
import { getWordDistance, getCharacterDistance } from '../utils/string'
import {
getWordDistance,
getCharacterDistance,
splitByCharacterDistance,
} from '../utils/string'
import { Descendant } from './node'
import { Element } from './element'
@ -1340,7 +1343,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
}
}
@ -1381,8 +1383,14 @@ 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)
// 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`.
@ -1413,11 +1421,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
}

View File

@ -5,77 +5,148 @@
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.
*/
export const getCharacterDistance = (text: string): number => {
let offset = 0
export const getCharacterDistance = (str: string, isRTL = false): number => {
const isLTR = !isRTL
let dist = 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)
// 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 (charCode) {
if (isSurrogate(charCode)) {
const modifier = isModifier(charCode, text, offset)
const codepoints = isLTR ? str : codepointsIteratorRTL(str)
// 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') {
break
}
for (const codepoint of codepoints) {
const code = codepoint.codePointAt(0)
if (!code) 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.
continue
}
if (charCode === ZERO_WIDTH_JOINER) {
offset += 1
// Check if codepoint is part of a sequence.
if (isZWJ(code)) {
dist += codepoint.length
prev = 'ZWJ'
charCode = text.charCodeAt(offset)
continue
}
if (isBMPEmoji(charCode)) {
if (prev && prev !== 'ZWJ' && prev !== 'VAR') {
const [isKeycapStart, isKeycapEnd] = isLTR
? [isKeycap, isCombiningEnclosingKeycap]
: [isCombiningEnclosingKeycap, isKeycap]
if (isKeycapStart(code)) {
if (prev === 'KC') {
break
}
offset += 1
prev = 'BMP'
charCode = text.charCodeAt(offset)
dist += codepoint.length
prev = 'KC'
continue
}
if (isKeycapEnd(code)) {
dist += codepoint.length
break
}
if (isVariationSelector(charCode)) {
if (prev && prev !== 'ZWJ') {
if (isVariationSelector(code)) {
dist += codepoint.length
if (isLTR && prev === 'BMP') {
break
}
offset += 1
prev = 'VAR'
charCode = text.charCodeAt(offset)
continue
}
if (isBMPEmoji(code)) {
if (isLTR && prev && prev !== 'ZWJ' && prev !== 'VAR') {
break
}
dist += codepoint.length
if (isRTL && prev === 'VAR') {
break
}
prev = 'BMP'
continue
}
if (isModifier(code)) {
dist += codepoint.length
prev = 'MOD'
continue
}
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)) {
dist += codepoint.length
continue
}
if (isRegionalIndicator(code)) {
dist += codepoint.length
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
}
dist += codepoint.length
prev = 'NSEQ'
continue
}
// 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
}
@ -83,37 +154,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] = splitByCharacterDistance(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 length
return dist
}
/**
* Split a string in two parts at a given distance starting from the end when
* `isRTL` is set to `true`.
*/
export const splitByCharacterDistance = (
str: string,
dist: number,
isRTL?: boolean
): [string, string] => {
if (isRTL) {
const at = str.length - dist
return [str.slice(at, str.length), str.slice(0, at)]
}
return [str.slice(0, dist), str.slice(dist)]
}
/**
@ -121,7 +207,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
}
@ -129,12 +219,14 @@ 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] = splitByCharacterDistance(
remaining,
charDist,
isRTL
)
if (isWordCharacter(next, rest)) {
if (isWordCharacter(nextChar, nextRemaining, isRTL)) {
return true
}
}
@ -146,25 +238,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 +258,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 +300,124 @@ 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
}
/**
* 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
}

View File

@ -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)

View File

@ -10,7 +10,7 @@ export const input = (
<block>
<text />
<inline>
word📛
word🇫🇷
<cursor />
</inline>
<text />

View File

@ -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 = (
<editor>
<block>
<text />
<inline>
word5
<cursor />
</inline>
<text />
</block>
</editor>
)
export const output = (
<editor>
<block>
<text />
<inline>
word
<cursor />
5
</inline>
<text />
</block>
</editor>
)

View File

@ -0,0 +1,32 @@
/** @jsx jsx */
import { Transforms } from 'slate'
import { jsx } from '../../..'
export const run = editor => {
Transforms.move(editor)
}
export const input = (
<editor>
<block>
<text />
<inline>
word
<cursor />
5
</inline>
<text />
</block>
</editor>
)
export const output = (
<editor>
<block>
<text />
<inline>
word5
<cursor />
</inline>
<text />
</block>
</editor>
)

View File

@ -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 = (
<editor>
<block>
<text />
<inline>
word🇫🇷
<cursor />
</inline>
<text />
</block>
</editor>
)
export const output = (
<editor>
<block>
<text />
<inline>
word
<cursor />
🇫🇷
</inline>
<text />
</block>
</editor>
)

View File

@ -0,0 +1,32 @@
/** @jsx jsx */
import { Transforms } from 'slate'
import { jsx } from '../../..'
export const run = editor => {
Transforms.move(editor)
}
export const input = (
<editor>
<block>
<text />
<inline>
word
<cursor />
🇫🇷
</inline>
<text />
</block>
</editor>
)
export const output = (
<editor>
<block>
<text />
<inline>
word🇫🇷
<cursor />
</inline>
<text />
</block>
</editor>
)

View File

@ -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 = (
<editor>
<block>
<text />
<inline>
word🏴󠁧󠁢󠁳󠁣󠁴󠁿
<cursor />
</inline>
<text />
</block>
</editor>
)
export const output = (
<editor>
<block>
<text />
<inline>
word
<cursor />
🏴󠁧󠁢󠁳󠁣󠁴󠁿
</inline>
<text />
</block>
</editor>
)

View File

@ -0,0 +1,32 @@
/** @jsx jsx */
import { Transforms } from 'slate'
import { jsx } from '../../..'
export const run = editor => {
Transforms.move(editor)
}
export const input = (
<editor>
<block>
<text />
<inline>
word
<cursor />
🏴󠁧󠁢󠁳󠁣󠁴󠁿
</inline>
<text />
</block>
</editor>
)
export const output = (
<editor>
<block>
<text />
<inline>
word🏴󠁧󠁢󠁳󠁣󠁴󠁿
<cursor />
</inline>
<text />
</block>
</editor>
)

View File

@ -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 = (
<editor>
<block>
<text />
<inline>
word👨👩👧👧
<cursor />
</inline>
<text />
</block>
</editor>
)
export const output = (
<editor>
<block>
<text />
<inline>
word
<cursor />
👨👩👧👧
</inline>
<text />
</block>
</editor>
)

View File

@ -0,0 +1,32 @@
/** @jsx jsx */
import { Transforms } from 'slate'
import { jsx } from '../../..'
export const run = editor => {
Transforms.move(editor)
}
export const input = (
<editor>
<block>
<text />
<inline>
word
<cursor />
👨👩👧👧
</inline>
<text />
</block>
</editor>
)
export const output = (
<editor>
<block>
<text />
<inline>
word👨👩👧👧
<cursor />
</inline>
<text />
</block>
</editor>
)

View File

@ -0,0 +1,144 @@
import assert from 'assert'
import {
getCharacterDistance,
getWordDistance,
codepointsIteratorRTL,
} from '../../src/utils/string'
const codepoints = [
['a', 1],
['0', 1],
[' ', 1],
['🙂', 2],
['⬅️', 2],
['🏴', 2],
] as const
const zwjSequences = [
['👁‍🗨', 5],
['👨‍👩‍👧‍👧', 11],
['👨🏿‍🦳', 7],
] as const
const regionalIndicatorSequences = [
'🇧🇪',
'🇧🇫',
'🇧🇬',
'🇧🇭',
'🇧🇮',
'🇧🇯',
'🇧🇱',
'🇧🇲',
'🇧🇳',
'🇧🇴',
]
const keycapSequences = [
'#️⃣',
'*️⃣',
'0⃣',
'1⃣',
'2⃣',
'3⃣',
'4⃣',
'5⃣',
'6⃣',
'7⃣',
'8⃣',
'9⃣',
]
const tagSequences = [
['🏴󠁧󠁢󠁥󠁮󠁧󠁿', 14],
['🏴󠁧󠁢󠁳󠁣󠁴󠁿', 14],
['🏴󠁧󠁢󠁷󠁬󠁳󠁿', 14],
] as const
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)
})
})
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)
})
})
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
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)
})
})
})
describe(`getWordDistance - rtl`, () => {
rtlCases.forEach(([str, dist]) => {
it(str, () => {
assert.strictEqual(getWordDistance(str, true), dist)
})
})
})
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)
})
})
})

View File

@ -2474,11 +2474,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"
@ -5516,11 +5511,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"