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:
parent
a1f925bddf
commit
0025900349
5
.changeset/lucky-schools-bake.md
Normal file
5
.changeset/lucky-schools-bake.md
Normal 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.
|
@ -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'],
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -10,7 +10,7 @@ export const input = (
|
||||
<block>
|
||||
<text />
|
||||
<inline>
|
||||
word📛
|
||||
word🇫🇷
|
||||
<cursor />
|
||||
</inline>
|
||||
<text />
|
||||
|
@ -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>
|
||||
)
|
32
packages/slate/test/transforms/move/emojis/keycap.tsx
Normal file
32
packages/slate/test/transforms/move/emojis/keycap.tsx
Normal 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>
|
||||
)
|
32
packages/slate/test/transforms/move/emojis/ri-reverse.tsx
Normal file
32
packages/slate/test/transforms/move/emojis/ri-reverse.tsx
Normal 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>
|
||||
)
|
32
packages/slate/test/transforms/move/emojis/ri.tsx
Normal file
32
packages/slate/test/transforms/move/emojis/ri.tsx
Normal 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>
|
||||
)
|
32
packages/slate/test/transforms/move/emojis/tag-reverse.tsx
Normal file
32
packages/slate/test/transforms/move/emojis/tag-reverse.tsx
Normal 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>
|
||||
)
|
32
packages/slate/test/transforms/move/emojis/tag.tsx
Normal file
32
packages/slate/test/transforms/move/emojis/tag.tsx
Normal 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>
|
||||
)
|
32
packages/slate/test/transforms/move/emojis/zwj-reverse.tsx
Normal file
32
packages/slate/test/transforms/move/emojis/zwj-reverse.tsx
Normal 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>
|
||||
)
|
32
packages/slate/test/transforms/move/emojis/zwj.tsx
Normal file
32
packages/slate/test/transforms/move/emojis/zwj.tsx
Normal 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>
|
||||
)
|
144
packages/slate/test/utils/string.ts
Normal file
144
packages/slate/test/utils/string.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
10
yarn.lock
10
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user