mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-07-31 20:40:19 +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
|
||||
* placed.
|
||||
* Return all the positions in `at` range where a `Point` can be placed.
|
||||
*
|
||||
* By default it will move forward by individual offsets at a time, but you
|
||||
* can pass the `unit: 'character'` option to moved forward one character, word,
|
||||
* or line at at time.
|
||||
* By default, moves forward by individual offsets at a time, but
|
||||
* the `unit` option can be used to to move by character, word, line, or block.
|
||||
*
|
||||
* The `reverse` option can be used to change iteration direction.
|
||||
*
|
||||
* 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
|
||||
* voids option, then iteration will occur.
|
||||
* `voids` option, then iteration will occur.
|
||||
*/
|
||||
|
||||
*positions(
|
||||
@@ -1243,54 +1243,70 @@ export const Editor: EditorInterface = {
|
||||
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 [start, end] = Range.edges(range)
|
||||
const first = reverse ? end : start
|
||||
let string = ''
|
||||
let available = 0
|
||||
let offset = 0
|
||||
let distance: number | null = null
|
||||
let isNewBlock = false
|
||||
let blockText = ''
|
||||
let distance = 0 // Distance for leafText to catch up to blockText.
|
||||
let leafTextRemaining = 0
|
||||
let leafTextOffset = 0
|
||||
|
||||
const advance = () => {
|
||||
if (distance == null) {
|
||||
if (unit === 'character') {
|
||||
distance = getCharacterDistance(string)
|
||||
} else if (unit === 'word') {
|
||||
distance = getWordDistance(string)
|
||||
} 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
|
||||
}
|
||||
|
||||
// Iterate through all nodes in range, grabbing entire textual content
|
||||
// of block nodes in blockText, and text nodes in leafText.
|
||||
// Exploits the fact that nodes are sequenced in such a way that we first
|
||||
// encounter the block node, then all of its text nodes, so when iterating
|
||||
// through the blockText and leafText we just need to remember a window of
|
||||
// one block node and leaf node, respectively.
|
||||
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)) {
|
||||
// Void nodes are a special case, so by default we will always
|
||||
// yield their first point. If the voids option is set to true,
|
||||
// then we will iterate over their content
|
||||
// yield their first point. If the `voids` option is set to true,
|
||||
// then we will iterate over their content.
|
||||
if (!voids && editor.isVoid(node)) {
|
||||
yield Editor.start(editor, path)
|
||||
continue
|
||||
}
|
||||
|
||||
if (editor.isInline(node)) {
|
||||
continue
|
||||
}
|
||||
// Inline element nodes are ignored as they don't themselves
|
||||
// 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)) {
|
||||
// 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)
|
||||
? end
|
||||
: Editor.end(editor, path)
|
||||
@@ -1298,46 +1314,90 @@ export const Editor: EditorInterface = {
|
||||
? start
|
||||
: Editor.start(editor, path)
|
||||
|
||||
const text = Editor.string(editor, { anchor: s, focus: e }, { voids })
|
||||
string = reverse ? reverseText(text) : text
|
||||
blockText = Editor.string(editor, { anchor: s, focus: e }, { voids })
|
||||
blockText = reverse ? reverseText(blockText) : blockText
|
||||
isNewBlock = true
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* TEXT LEAF NODE - Iterate through text content, yielding
|
||||
* positions every `distance` offset according to `unit`.
|
||||
*/
|
||||
if (Text.isText(node)) {
|
||||
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) {
|
||||
available = reverse ? first.offset : available - first.offset
|
||||
offset = first.offset
|
||||
leafTextRemaining = reverse
|
||||
? 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') {
|
||||
yield { path, offset }
|
||||
yield { path, offset: leafTextOffset }
|
||||
isNewBlock = false
|
||||
}
|
||||
|
||||
// Yield positions every (dynamically calculated) `distance` offset.
|
||||
while (true) {
|
||||
// If there's no more string and there is no more characters to skip, continue to the next block.
|
||||
if (string === '' && distance === null) {
|
||||
break
|
||||
} else {
|
||||
advance()
|
||||
// If `leafText` has caught up with `blockText` (distance=0),
|
||||
// and if blockText is exhausted, break to get another block node,
|
||||
// otherwise advance blockText forward by the new `distance`.
|
||||
if (distance === 0) {
|
||||
if (blockText === '') break
|
||||
distance = calcDistance(blockText, unit)
|
||||
blockText = blockText.slice(distance)
|
||||
}
|
||||
|
||||
// If the available space hasn't overflow, we have another point to
|
||||
// yield in the current text node.
|
||||
if (available >= 0) {
|
||||
yield { path, offset }
|
||||
} else {
|
||||
// Advance `leafText` by the current `distance`.
|
||||
leafTextOffset = reverse
|
||||
? leafTextOffset - distance
|
||||
: leafTextOffset + distance
|
||||
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
|
||||
}
|
||||
|
||||
// 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