Skip to content

Commit 1fe6d0c

Browse files
authored
refactor: New Editor.positions based off PR#3644 (ianstormtaylor#4199)
Also fixes `Editor.positions` bug ianstormtaylor#3458 that was fixed in parallel in ianstormtaylor#4073, but includes refactorings as discussed in ianstormtaylor#3644. vs ianstormtaylor#3458 - Updated to include changes from later PRs (ianstormtaylor#3957) - Does not add test cases (relies on those from ianstormtaylor#4073) - Minor improvements on comments
1 parent 25a6994 commit 1fe6d0c

1 file changed

Lines changed: 119 additions & 59 deletions

File tree

packages/slate/src/interfaces/editor.ts

Lines changed: 119 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,16 +1211,16 @@ export const Editor: EditorInterface = {
12111211
},
12121212

12131213
/**
1214-
* Iterate through all of the positions in the document where a `Point` can be
1215-
* placed.
1214+
* Return all the positions in `at` range where a `Point` can be placed.
12161215
*
1217-
* By default it will move forward by individual offsets at a time, but you
1218-
* can pass the `unit: 'character'` option to moved forward one character, word,
1219-
* or line at at time.
1216+
* By default, moves forward by individual offsets at a time, but
1217+
* the `unit` option can be used to to move by character, word, line, or block.
1218+
*
1219+
* The `reverse` option can be used to change iteration direction.
12201220
*
12211221
* Note: By default void nodes are treated as a single point and iteration
12221222
* will not happen inside their content unless you pass in true for the
1223-
* voids option, then iteration will occur.
1223+
* `voids` option, then iteration will occur.
12241224
*/
12251225

12261226
*positions(
@@ -1243,100 +1243,160 @@ export const Editor: EditorInterface = {
12431243
return
12441244
}
12451245

1246+
/**
1247+
* Algorithm notes:
1248+
*
1249+
* Each step `distance` is dynamic depending on the underlying text
1250+
* and the `unit` specified. Each step, e.g., a line or word, may
1251+
* span multiple text nodes, so we iterate through the text both on
1252+
* two levels in step-sync:
1253+
*
1254+
* `leafText` stores the text on a text leaf level, and is advanced
1255+
* through using the counters `leafTextOffset` and `leafTextRemaining`.
1256+
*
1257+
* `blockText` stores the text on a block level, and is shortened
1258+
* by `distance` every time it is advanced.
1259+
*
1260+
* We only maintain a window of one blockText and one leafText because
1261+
* a block node always appears before all of its leaf nodes.
1262+
*/
1263+
12461264
const range = Editor.range(editor, at)
12471265
const [start, end] = Range.edges(range)
12481266
const first = reverse ? end : start
1249-
let string = ''
1250-
let available = 0
1251-
let offset = 0
1252-
let distance: number | null = null
12531267
let isNewBlock = false
1254-
1255-
const advance = () => {
1256-
if (distance == null) {
1257-
if (unit === 'character') {
1258-
distance = getCharacterDistance(string)
1259-
} else if (unit === 'word') {
1260-
distance = getWordDistance(string)
1261-
} else if (unit === 'line' || unit === 'block') {
1262-
distance = string.length
1263-
} else {
1264-
distance = 1
1265-
}
1266-
1267-
string = string.slice(distance)
1268-
}
1269-
1270-
// Add or substract the offset.
1271-
offset = reverse ? offset - distance : offset + distance
1272-
// Subtract the distance traveled from the available text.
1273-
available = available - distance!
1274-
// If the available had room to spare, reset the distance so that it will
1275-
// advance again next time. Otherwise, set it to the overflow amount.
1276-
distance = available >= 0 ? null : 0 - available
1277-
}
1278-
1268+
let blockText = ''
1269+
let distance = 0 // Distance for leafText to catch up to blockText.
1270+
let leafTextRemaining = 0
1271+
let leafTextOffset = 0
1272+
1273+
// Iterate through all nodes in range, grabbing entire textual content
1274+
// of block nodes in blockText, and text nodes in leafText.
1275+
// Exploits the fact that nodes are sequenced in such a way that we first
1276+
// encounter the block node, then all of its text nodes, so when iterating
1277+
// through the blockText and leafText we just need to remember a window of
1278+
// one block node and leaf node, respectively.
12791279
for (const [node, path] of Editor.nodes(editor, { at, reverse, voids })) {
1280+
/*
1281+
* ELEMENT NODE - Yield position(s) for voids, collect blockText for blocks
1282+
*/
12801283
if (Element.isElement(node)) {
12811284
// Void nodes are a special case, so by default we will always
1282-
// yield their first point. If the voids option is set to true,
1283-
// then we will iterate over their content
1285+
// yield their first point. If the `voids` option is set to true,
1286+
// then we will iterate over their content.
12841287
if (!voids && editor.isVoid(node)) {
12851288
yield Editor.start(editor, path)
12861289
continue
12871290
}
12881291

1289-
if (editor.isInline(node)) {
1290-
continue
1291-
}
1292+
// Inline element nodes are ignored as they don't themselves
1293+
// contribute to `blockText` or `leafText` - their parent and
1294+
// children do.
1295+
if (editor.isInline(node)) continue
12921296

1297+
// Block element node - set `blockText` to its text content.
12931298
if (Editor.hasInlines(editor, node)) {
1299+
// We always exhaust block nodes before encountering a new one:
1300+
// console.assert(blockText === '',
1301+
// `blockText='${blockText}' - `+
1302+
// `not exhausted before new block node`, path)
1303+
1304+
// Ensure range considered is capped to `range`, in the
1305+
// start/end edge cases where block extends beyond range.
1306+
// Equivalent to this, but presumably more performant:
1307+
// blockRange = Editor.range(editor, ...Editor.edges(editor, path))
1308+
// blockRange = Range.intersection(range, blockRange) // intersect
1309+
// blockText = Editor.string(editor, blockRange, { voids })
12941310
const e = Path.isAncestor(path, end.path)
12951311
? end
12961312
: Editor.end(editor, path)
12971313
const s = Path.isAncestor(path, start.path)
12981314
? start
12991315
: Editor.start(editor, path)
13001316

1301-
const text = Editor.string(editor, { anchor: s, focus: e }, { voids })
1302-
string = reverse ? reverseText(text) : text
1317+
blockText = Editor.string(editor, { anchor: s, focus: e }, { voids })
1318+
blockText = reverse ? reverseText(blockText) : blockText
13031319
isNewBlock = true
13041320
}
13051321
}
13061322

1323+
/*
1324+
* TEXT LEAF NODE - Iterate through text content, yielding
1325+
* positions every `distance` offset according to `unit`.
1326+
*/
13071327
if (Text.isText(node)) {
13081328
const isFirst = Path.equals(path, first.path)
1309-
available = node.text.length
1310-
offset = reverse ? available : 0
13111329

1330+
// Proof that we always exhaust text nodes before encountering a new one:
1331+
// console.assert(leafTextRemaining <= 0,
1332+
// `leafTextRemaining=${leafTextRemaining} - `+
1333+
// `not exhausted before new leaf text node`, path)
1334+
1335+
// Reset `leafText` counters for new text node.
13121336
if (isFirst) {
1313-
available = reverse ? first.offset : available - first.offset
1314-
offset = first.offset
1337+
leafTextRemaining = reverse
1338+
? first.offset
1339+
: node.text.length - first.offset
1340+
leafTextOffset = first.offset // Works for reverse too.
1341+
} else {
1342+
leafTextRemaining = node.text.length
1343+
leafTextOffset = reverse ? leafTextRemaining : 0
13151344
}
13161345

1346+
// Yield position at the start of node (potentially).
13171347
if (isFirst || isNewBlock || unit === 'offset') {
1318-
yield { path, offset }
1348+
yield { path, offset: leafTextOffset }
1349+
isNewBlock = false
13191350
}
13201351

1352+
// Yield positions every (dynamically calculated) `distance` offset.
13211353
while (true) {
1322-
// If there's no more string and there is no more characters to skip, continue to the next block.
1323-
if (string === '' && distance === null) {
1324-
break
1325-
} else {
1326-
advance()
1354+
// If `leafText` has caught up with `blockText` (distance=0),
1355+
// and if blockText is exhausted, break to get another block node,
1356+
// otherwise advance blockText forward by the new `distance`.
1357+
if (distance === 0) {
1358+
if (blockText === '') break
1359+
distance = calcDistance(blockText, unit)
1360+
blockText = blockText.slice(distance)
13271361
}
13281362

1329-
// If the available space hasn't overflow, we have another point to
1330-
// yield in the current text node.
1331-
if (available >= 0) {
1332-
yield { path, offset }
1333-
} else {
1363+
// Advance `leafText` by the current `distance`.
1364+
leafTextOffset = reverse
1365+
? leafTextOffset - distance
1366+
: leafTextOffset + distance
1367+
leafTextRemaining = leafTextRemaining - distance
1368+
1369+
// If `leafText` is exhausted, break to get a new leaf node
1370+
// and set distance to the overflow amount, so we'll (maybe)
1371+
// catch up to blockText in the next leaf text node.
1372+
if (leafTextRemaining < 0) {
1373+
distance = -leafTextRemaining
13341374
break
13351375
}
1336-
}
13371376

1338-
isNewBlock = false
1377+
// Successfully walked `distance` offsets through `leafText`
1378+
// to catch up with `blockText`, so we can reset `distance`
1379+
// and yield this position in this node.
1380+
distance = 0
1381+
yield { path, offset: leafTextOffset }
1382+
}
1383+
}
1384+
}
1385+
// Proof that upon completion, we've exahusted both leaf and block text:
1386+
// console.assert(leafTextRemaining <= 0, "leafText wasn't exhausted")
1387+
// console.assert(blockText === '', "blockText wasn't exhausted")
1388+
1389+
// Helper:
1390+
// Return the distance in offsets for a step of size `unit` on given string.
1391+
function calcDistance(text: string, unit: string) {
1392+
if (unit === 'character') {
1393+
return getCharacterDistance(text)
1394+
} else if (unit === 'word') {
1395+
return getWordDistance(text)
1396+
} else if (unit === 'line' || unit === 'block') {
1397+
return text.length
13391398
}
1399+
return 1
13401400
}
13411401
},
13421402

0 commit comments

Comments
 (0)