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:
@@ -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
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user