1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-05 14:57:25 +02:00

refactor: New Editor.positions based off PR#3644 (#4199)

Also fixes `Editor.positions` bug #3458 that was fixed in parallel in #4073,
but includes refactorings as discussed in #3644.

vs #3458
- Updated to include changes from later PRs (#3957)
- Does not add test cases (relies on those from #4073)
- Minor improvements on comments
This commit is contained in:
Bjørn Stabell
2021-04-14 08:30:16 -07:00
committed by GitHub
parent 25a69949d7
commit 1fe6d0cef1

View File

@@ -1211,16 +1211,16 @@ export const Editor: EditorInterface = {
}, },
/** /**
* Iterate through all of the positions in the document where a `Point` can be * Return all the positions in `at` range where a `Point` can be placed.
* placed.
* *
* By default it will move forward by individual offsets at a time, but you * By default, moves forward by individual offsets at a time, but
* can pass the `unit: 'character'` option to moved forward one character, word, * the `unit` option can be used to to move by character, word, line, or block.
* or line at at time. *
* The `reverse` option can be used to change iteration direction.
* *
* Note: By default void nodes are treated as a single point and iteration * Note: By default void nodes are treated as a single point and iteration
* will not happen inside their content unless you pass in true for the * will not happen inside their content unless you pass in true for the
* voids option, then iteration will occur. * `voids` option, then iteration will occur.
*/ */
*positions( *positions(
@@ -1243,54 +1243,70 @@ export const Editor: EditorInterface = {
return return
} }
/**
* Algorithm notes:
*
* Each step `distance` is dynamic depending on the underlying text
* and the `unit` specified. Each step, e.g., a line or word, may
* span multiple text nodes, so we iterate through the text both on
* two levels in step-sync:
*
* `leafText` stores the text on a text leaf level, and is advanced
* through using the counters `leafTextOffset` and `leafTextRemaining`.
*
* `blockText` stores the text on a block level, and is shortened
* by `distance` every time it is advanced.
*
* We only maintain a window of one blockText and one leafText because
* a block node always appears before all of its leaf nodes.
*/
const range = Editor.range(editor, at) const range = Editor.range(editor, at)
const [start, end] = Range.edges(range) const [start, end] = Range.edges(range)
const first = reverse ? end : start const first = reverse ? end : start
let string = ''
let available = 0
let offset = 0
let distance: number | null = null
let isNewBlock = false let isNewBlock = false
let blockText = ''
let distance = 0 // Distance for leafText to catch up to blockText.
let leafTextRemaining = 0
let leafTextOffset = 0
const advance = () => { // Iterate through all nodes in range, grabbing entire textual content
if (distance == null) { // of block nodes in blockText, and text nodes in leafText.
if (unit === 'character') { // Exploits the fact that nodes are sequenced in such a way that we first
distance = getCharacterDistance(string) // encounter the block node, then all of its text nodes, so when iterating
} else if (unit === 'word') { // through the blockText and leafText we just need to remember a window of
distance = getWordDistance(string) // one block node and leaf node, respectively.
} else if (unit === 'line' || unit === 'block') {
distance = string.length
} else {
distance = 1
}
string = string.slice(distance)
}
// Add or substract the offset.
offset = reverse ? offset - distance : offset + distance
// Subtract the distance traveled from the available text.
available = available - distance!
// If the available had room to spare, reset the distance so that it will
// advance again next time. Otherwise, set it to the overflow amount.
distance = available >= 0 ? null : 0 - available
}
for (const [node, path] of Editor.nodes(editor, { at, reverse, voids })) { for (const [node, path] of Editor.nodes(editor, { at, reverse, voids })) {
/*
* ELEMENT NODE - Yield position(s) for voids, collect blockText for blocks
*/
if (Element.isElement(node)) { if (Element.isElement(node)) {
// Void nodes are a special case, so by default we will always // Void nodes are a special case, so by default we will always
// yield their first point. If the voids option is set to true, // yield their first point. If the `voids` option is set to true,
// then we will iterate over their content // then we will iterate over their content.
if (!voids && editor.isVoid(node)) { if (!voids && editor.isVoid(node)) {
yield Editor.start(editor, path) yield Editor.start(editor, path)
continue continue
} }
if (editor.isInline(node)) { // Inline element nodes are ignored as they don't themselves
continue // contribute to `blockText` or `leafText` - their parent and
} // children do.
if (editor.isInline(node)) continue
// Block element node - set `blockText` to its text content.
if (Editor.hasInlines(editor, node)) { if (Editor.hasInlines(editor, node)) {
// We always exhaust block nodes before encountering a new one:
// console.assert(blockText === '',
// `blockText='${blockText}' - `+
// `not exhausted before new block node`, path)
// Ensure range considered is capped to `range`, in the
// start/end edge cases where block extends beyond range.
// Equivalent to this, but presumably more performant:
// blockRange = Editor.range(editor, ...Editor.edges(editor, path))
// blockRange = Range.intersection(range, blockRange) // intersect
// blockText = Editor.string(editor, blockRange, { voids })
const e = Path.isAncestor(path, end.path) const e = Path.isAncestor(path, end.path)
? end ? end
: Editor.end(editor, path) : Editor.end(editor, path)
@@ -1298,46 +1314,90 @@ export const Editor: EditorInterface = {
? start ? start
: Editor.start(editor, path) : Editor.start(editor, path)
const text = Editor.string(editor, { anchor: s, focus: e }, { voids }) blockText = Editor.string(editor, { anchor: s, focus: e }, { voids })
string = reverse ? reverseText(text) : text blockText = reverse ? reverseText(blockText) : blockText
isNewBlock = true isNewBlock = true
} }
} }
/*
* TEXT LEAF NODE - Iterate through text content, yielding
* positions every `distance` offset according to `unit`.
*/
if (Text.isText(node)) { if (Text.isText(node)) {
const isFirst = Path.equals(path, first.path) const isFirst = Path.equals(path, first.path)
available = node.text.length
offset = reverse ? available : 0
// Proof that we always exhaust text nodes before encountering a new one:
// console.assert(leafTextRemaining <= 0,
// `leafTextRemaining=${leafTextRemaining} - `+
// `not exhausted before new leaf text node`, path)
// Reset `leafText` counters for new text node.
if (isFirst) { if (isFirst) {
available = reverse ? first.offset : available - first.offset leafTextRemaining = reverse
offset = first.offset ? first.offset
: node.text.length - first.offset
leafTextOffset = first.offset // Works for reverse too.
} else {
leafTextRemaining = node.text.length
leafTextOffset = reverse ? leafTextRemaining : 0
} }
// Yield position at the start of node (potentially).
if (isFirst || isNewBlock || unit === 'offset') { if (isFirst || isNewBlock || unit === 'offset') {
yield { path, offset } yield { path, offset: leafTextOffset }
isNewBlock = false
} }
// Yield positions every (dynamically calculated) `distance` offset.
while (true) { while (true) {
// If there's no more string and there is no more characters to skip, continue to the next block. // If `leafText` has caught up with `blockText` (distance=0),
if (string === '' && distance === null) { // and if blockText is exhausted, break to get another block node,
break // otherwise advance blockText forward by the new `distance`.
} else { if (distance === 0) {
advance() if (blockText === '') break
distance = calcDistance(blockText, unit)
blockText = blockText.slice(distance)
} }
// If the available space hasn't overflow, we have another point to // Advance `leafText` by the current `distance`.
// yield in the current text node. leafTextOffset = reverse
if (available >= 0) { ? leafTextOffset - distance
yield { path, offset } : leafTextOffset + distance
} else { leafTextRemaining = leafTextRemaining - distance
// If `leafText` is exhausted, break to get a new leaf node
// and set distance to the overflow amount, so we'll (maybe)
// catch up to blockText in the next leaf text node.
if (leafTextRemaining < 0) {
distance = -leafTextRemaining
break break
} }
// Successfully walked `distance` offsets through `leafText`
// to catch up with `blockText`, so we can reset `distance`
// and yield this position in this node.
distance = 0
yield { path, offset: leafTextOffset }
} }
isNewBlock = false
} }
} }
// Proof that upon completion, we've exahusted both leaf and block text:
// console.assert(leafTextRemaining <= 0, "leafText wasn't exhausted")
// console.assert(blockText === '', "blockText wasn't exhausted")
// Helper:
// Return the distance in offsets for a step of size `unit` on given string.
function calcDistance(text: string, unit: string) {
if (unit === 'character') {
return getCharacterDistance(text)
} else if (unit === 'word') {
return getWordDistance(text)
} else if (unit === 'line' || unit === 'block') {
return text.length
}
return 1
}
}, },
/** /**