diff --git a/benchmark/slate/changes/normalize.js b/benchmark/slate/changes/normalize.js index 2ec71af55..b0f178524 100644 --- a/benchmark/slate/changes/normalize.js +++ b/benchmark/slate/changes/normalize.js @@ -4,8 +4,8 @@ const h = require('../../helpers/h') const { Editor } = require('slate') -module.exports.default = function(change) { - change +module.exports.default = function(editor) { + editor .normalize() .moveForward(5) .normalize() diff --git a/packages/slate/src/interfaces/element.js b/packages/slate/src/interfaces/element.js index 1e635cc23..480280126 100644 --- a/packages/slate/src/interfaces/element.js +++ b/packages/slate/src/interfaces/element.js @@ -112,7 +112,7 @@ class ElementInterface { } /** - * Recursively find all descendant nodes by `iterator`. + * Recursively find a descendant node by `iterator`. * * @param {Function} iterator * @return {Node|Null} @@ -131,6 +131,45 @@ class ElementInterface { return found } + /** + * Recursively find a descendant node and its path by `iterator`. + * + * @param {Function} iterator + * @return {Null|[Node, List]} + */ + + findDescendantAndPath( + iterator, + pathToThisNode = PathUtils.create([]), + findLast = false + ) { + let found + let foundPath + + this.forEachDescendantWithPath( + (node, path, nodes) => { + if (iterator(node, path, nodes)) { + found = node + foundPath = path + return false + } + }, + pathToThisNode, + findLast + ) + + return found ? [found, foundPath] : null + } + + // Easy helpers to avoid needing to pass findLast boolean + findFirstDescendantAndPath(iterator, pathToThisNode) { + return this.findDescendantAndPath(iterator, pathToThisNode, false) + } + + findLastDescendantAndPath(iterator, pathToThisNode) { + return this.findDescendantAndPath(iterator, pathToThisNode, true) + } + /** * Recursively iterate over all descendant nodes with `iterator`. If the * iterator returns false it will break the loop. @@ -156,6 +195,39 @@ class ElementInterface { return ret } + /** + * Recursively iterate over all descendant nodes with `iterator`. If the + * iterator returns false it will break the loop. + * Calls iterator with node and path. + * + * @param {Function} iterator + * @param {List} path + * @param {Boolean} findLast - whether to iterate in reverse order + */ + + forEachDescendantWithPath(iterator, path = PathUtils.create([]), findLast) { + let nodes = this.nodes + let ret + + if (findLast) nodes = nodes.reverse() + + nodes.forEach((child, i) => { + const childPath = path.concat(i) + + if (iterator(child, childPath, nodes) === false) { + ret = false + return false + } + + if (child.object !== 'text') { + ret = child.forEachDescendantWithPath(iterator, childPath, findLast) + return ret + } + }) + + return ret + } + /** * Get a set of the active marks in a `range`. * @@ -169,31 +241,33 @@ class ElementInterface { if (range.isCollapsed) { const { start } = range - return this.getMarksAtPosition(start.key, start.offset).toSet() + return this.getMarksAtPosition(start.path, start.offset).toSet() } const { start, end } = range - let startKey = start.key + let startPath = start.path let startOffset = start.offset - let endKey = end.key + let endPath = end.path let endOffset = end.offset - let startText = this.getDescendant(startKey) + let startText = this.getDescendant(startPath) + let endText = this.getDescendant(endPath) - if (startKey !== endKey) { - while (startKey !== endKey && endOffset === 0) { - const endText = this.getPreviousText(endKey) - endKey = endText.key + if (!PathUtils.isEqual(startPath, endPath)) { + while (!PathUtils.isEqual(startPath, endPath) && endOffset === 0) { + ;[endText, endPath] = this.getPreviousTextAndPath(endPath) endOffset = endText.text.length } - while (startKey !== endKey && startOffset === startText.text.length) { - startText = this.getNextText(startKey) - startKey = startText.key + while ( + !PathUtils.isEqual(startPath, endPath) && + startOffset === startText.text.length + ) { + ;[startText, startPath] = this.getNextTextAndPath(startPath) startOffset = 0 } } - if (startKey === endKey) { + if (PathUtils.isEqual(startPath, endPath)) { return startText.getActiveMarksBetweenOffsets(startOffset, endOffset) } @@ -202,21 +276,23 @@ class ElementInterface { startText.text.length ) if (startMarks.size === 0) return Set() - const endText = this.getDescendant(endKey) const endMarks = endText.getActiveMarksBetweenOffsets(0, endOffset) let marks = startMarks.intersect(endMarks) + // If marks is already empty, the active marks is empty - if (marks.size === 0) return marks + if (marks.size === 0) { + return marks + } - let text = this.getNextText(startKey) + ;[startText, startPath] = this.getNextTextAndPath(startPath) - while (text.key !== endKey) { - if (text.text.length !== 0) { - marks = marks.intersect(text.getActiveMarks()) + while (!PathUtils.isEqual(startPath, endPath)) { + if (startText.text.length !== 0) { + marks = marks.intersect(startText.getActiveMarks()) if (marks.size === 0) return Set() } - text = this.getNextText(text.key) + ;[startText, startPath] = this.getNextTextAndPath(startPath) } return marks } @@ -342,8 +418,8 @@ class ElementInterface { getChild(path) { path = this.resolvePath(path) - if (!path) return null - const child = path.size === 1 ? this.nodes.get(path.first()) : null + if (!path || path.size > 1) return null + const child = this.nodes.get(path.first()) return child } @@ -475,11 +551,16 @@ class ElementInterface { getDescendant(path) { path = this.resolvePath(path) - if (!path) return null + if (!path || !path.size) return null - const deep = path.flatMap(x => ['nodes', x]) - const ret = this.getIn(deep) - return ret + let node = this + + path.forEach(index => { + node = node.getIn(['nodes', index]) + return !!node + }) + + return node } /** @@ -546,14 +627,14 @@ class ElementInterface { /** * Get the furthest ancestor of a node. * - * @param {Path} path + * @param {List|String} path * @return {Node|Null} */ getFurthestAncestor(path) { path = this.resolvePath(path) - if (!path) return null - const furthest = path.size ? this.nodes.get(path.first()) : null + if (!path || !path.size) return null + const furthest = this.nodes.get(path.first()) return furthest } @@ -582,7 +663,7 @@ class ElementInterface { } /** - * Get the furthest ancestor of a node that has only one child. + * Get the furthest ancestor of a node, where all ancestors to that point only have one child. * * @param {Path} path * @return {Node|Null} @@ -616,7 +697,7 @@ class ElementInterface { /** * Get the closest inline nodes for each text node in the node, as an array. * - * @return {List} + * @return {Array} */ getInlinesAsArray() { @@ -719,10 +800,10 @@ class ElementInterface { if (range.isCollapsed) { // PERF: range is not cachable, use key and offset as proxies for cache - return this.getMarksAtPosition(start.key, start.offset) + return this.getMarksAtPosition(start.path, start.offset) } - const text = this.getDescendant(start.key) + const text = this.getDescendant(start.path) const marks = text.getMarksAtIndex(start.offset + 1) return marks } @@ -744,7 +825,7 @@ class ElementInterface { * Get the bottom-most descendants in a `range` as an array * * @param {Range} range - * @return {Array} + * @return {Array} */ getLeafBlocksAtRangeAsArray(range) { @@ -752,17 +833,57 @@ class ElementInterface { if (range.isUnset) return [] const { start, end } = range - const startBlock = this.getClosestBlock(start.key) + return this.getLeafBlocksBetweenPathPositionsAsArray(start.path, end.path) + } + + /** + * Get the bottom-most descendants between two paths as an array + * + * @param {List|Null} startPath + * @param {List|Null} endPath + * @return {Array} + */ + + getLeafBlocksBetweenPathPositionsAsArray(startPath, endPath) { // PERF: the most common case is when the range is in a single block node, // where we can avoid a lot of iterating of the tree. - if (start.key === end.key) return [startBlock] + if (startPath && endPath && PathUtils.isEqual(startPath, endPath)) { + return [this.getClosestBlock(startPath)] + } else if (!startPath && !endPath) { + return this.getBlocksAsArray() + } - const endBlock = this.getClosestBlock(end.key) - const blocks = this.getBlocksAsArray() - const startIndex = blocks.indexOf(startBlock) - const endIndex = blocks.indexOf(endBlock) - return blocks.slice(startIndex, endIndex + 1) + const startIndex = startPath ? startPath.get(0, 0) : 0 + const endIndex = endPath + ? endPath.get(0, this.nodes.size - 1) + : this.nodes.size - 1 + + let array = [] + + this.nodes.slice(startIndex, endIndex + 1).forEach((node, i) => { + if (node.object !== 'block') { + return + } else if (node.isLeafBlock()) { + array.push(node) + } else { + const childStartPath = + startPath && i === 0 ? PathUtils.drop(startPath) : null + const childEndPath = + endPath && i === endIndex - startIndex + ? PathUtils.drop(endPath) + : null + + array = array.concat( + node.getLeafBlocksBetweenPathPositionsAsArray( + childStartPath, + childEndPath + ) + ) + } + }) + + return array } /** @@ -783,7 +904,7 @@ class ElementInterface { * Get the bottom-most inline nodes for each text node in a `range` as an array. * * @param {Range} range - * @return {Array} + * @return {Array} */ getLeafInlinesAtRangeAsArray(range) { @@ -829,27 +950,30 @@ class ElementInterface { /** * Get a set of marks in a `position`, the equivalent of a collapsed range * - * @param {string} key + * @param {List|string} key * @param {number} offset * @return {Set} */ - getMarksAtPosition(key, offset) { - const text = this.getDescendant(key) + getMarksAtPosition(path, offset) { + path = this.resolvePath(path) + const text = this.getDescendant(path) const currentMarks = text.getMarksAtIndex(offset) if (offset !== 0) return currentMarks - const closestBlock = this.getClosestBlock(key) + const closestBlock = this.getClosestBlock(path) if (closestBlock.text === '') { // insert mark for empty block; the empty block are often created by split node or add marks in a range including empty blocks return currentMarks } - const previous = this.getPreviousText(key) + const previous = this.getPreviousTextAndPath(path) if (!previous) return Set() - if (closestBlock.hasDescendant(previous.key)) { - return previous.getMarksAtIndex(previous.text.length) + const [previousText, previousPath] = previous + + if (closestBlock.hasDescendant(previousPath)) { + return previous.getMarksAtIndex(previousText.text.length) } return currentMarks @@ -897,28 +1021,20 @@ class ElementInterface { } /** - * Get the block node before a descendant text node by `key`. + * Get the block node after a descendant text node by `path`. * - * @param {String} key + * @param {List|String} path * @return {Node|Null} */ - getNextBlock(key) { - const child = this.assertDescendant(key) - let last + getNextBlock(path) { + path = this.resolvePath(path) + const match = this.getNextDeepMatchingNodeAndPath( + path, + n => n.object === 'block' + ) - if (child.object === 'block') { - last = child.getLastText() - } else { - const block = this.getClosestBlock(key) - last = block.getLastText() - } - - const next = this.getNextText(last.key) - if (!next) return null - - const closest = this.getClosestBlock(next.key) - return closest + return match ? match[0] : null } /** @@ -946,6 +1062,74 @@ class ElementInterface { return null } + /** + * Get the next node in the tree from a node that matches iterator + * + * This will not only check for siblings but instead move up the tree + * returning the next ancestor if no sibling is found. + * + * @param {List} path + * @return {Node|Null} + */ + + getNextMatchingNodeAndPath(path, iterator = () => true) { + if (!path) return null + + for (let i = path.size; i > 0; i--) { + const p = path.slice(0, i) + + let nextPath = PathUtils.increment(p) + let nextNode = this.getNode(nextPath) + + while (nextNode && !iterator(nextNode)) { + nextPath = PathUtils.increment(nextPath) + nextNode = this.getNode(nextPath) + } + + if (nextNode) return [nextNode, nextPath] + } + + return null + } + + /** + * Get the next, deepest node in the tree from a node that matches iterator + * + * This will not only check for siblings but instead move up the tree + * returning the next ancestor if no sibling is found. + * + * @param {List} path + * @param {Function} iterator + * @return {Node|Null} + */ + + getNextDeepMatchingNodeAndPath(path, iterator = () => true) { + const match = this.getNextMatchingNodeAndPath(path) + + if (!match) return null + + let [nextNode, nextPath] = match + + let childMatch + + const assign = () => { + childMatch = + nextNode.object !== 'text' && + nextNode.findFirstDescendantAndPath(iterator, nextPath) + return childMatch + } + + while (assign(childMatch)) { + ;[nextNode, nextPath] = childMatch + } + + if (!nextNode) return null + + return iterator(nextNode) + ? [nextNode, nextPath] + : this.getNextDeepMatchingNodeAndPath(match[1], iterator) + } + /** * Get the next sibling of a node. * @@ -979,6 +1163,16 @@ class ElementInterface { return text } + getNextTextAndPath(path) { + if (!path) return null + if (!path.size) return null + const match = this.getNextDeepMatchingNodeAndPath( + path, + n => n.object === 'text' + ) + return match + } + /** * Get all of the nodes in a `range`. This includes all of the * text nodes inside the range and all ancestors of those text @@ -1049,23 +1243,28 @@ class ElementInterface { } /** - * Get the offset for a descendant text node by `key`. + * Get the offset for a descendant text node by `path` or `key`. * - * @param {String} key + * @param {List|string} path * @return {Number} */ - getOffset(key) { - this.assertDescendant(key) + getOffset(path) { + path = this.resolvePath(path) + this.assertDescendant(path) // Calculate the offset of the nodes before the highest child. - const child = this.getFurthestAncestor(key) + const index = path.first() + const offset = this.nodes - .takeUntil(n => n === child) + .slice(0, index) .reduce((memo, n) => memo + n.text.length, 0) // Recurse if need be. - const ret = this.hasChild(key) ? offset : offset + child.getOffset(key) + const ret = + path.size === 1 + ? offset + : offset + this.nodes.get(index).getOffset(PathUtils.drop(path)) return ret } @@ -1088,7 +1287,7 @@ class ElementInterface { } const { start } = range - const offset = this.getOffset(start.key) + start.offset + const offset = this.getOffset(start.path) + start.offset return offset } @@ -1119,14 +1318,14 @@ class ElementInterface { } if (range.isCollapsed) { - // PERF: range is not cachable, use key and offset as proxies for cache - return this.getMarksAtPosition(start.key, start.offset) + // PERF: range is not cachable, use path? and offset as proxies for cache + return this.getMarksAtPosition(start.path, start.offset) } const marks = this.getOrderedMarksBetweenPositions( - start.key, + start.path, start.offset, - end.key, + end.path, end.offset ) @@ -1137,28 +1336,34 @@ class ElementInterface { * Get a set of the marks in a `range`. * PERF: arguments use key and offset for utilizing cache * - * @param {string} startKey + * @param {List|string} startPath * @param {number} startOffset - * @param {string} endKey + * @param {List|string} endPath * @param {number} endOffset * @returns {OrderedSet} */ - getOrderedMarksBetweenPositions(startKey, startOffset, endKey, endOffset) { - if (startKey === endKey) { - const startText = this.getDescendant(startKey) + getOrderedMarksBetweenPositions(startPath, startOffset, endPath, endOffset) { + startPath = this.resolvePath(startPath) + endPath = this.resolvePath(endPath) + + const startText = this.getDescendant(startPath) + + if (PathUtils.isEqual(startPath, endPath)) { return startText.getMarksBetweenOffsets(startOffset, endOffset) } - const texts = this.getTextsBetweenPositionsAsArray(startKey, endKey) + const endText = this.getDescendant(endPath) + + const texts = this.getTextsBetweenPathPositionsAsArray(startPath, endPath) return OrderedSet().withMutations(result => { texts.forEach(text => { - if (text.key === startKey) { + if (text.key === startText.key) { result.union( text.getMarksBetweenOffsets(startOffset, text.text.length) ) - } else if (text.key === endKey) { + } else if (text.key === endText.key) { result.union(text.getMarksBetweenOffsets(0, endOffset)) } else { result.union(text.getMarks()) @@ -1196,28 +1401,20 @@ class ElementInterface { } /** - * Get the block node before a descendant text node by `key`. + * Get the block node before a descendant text node by `path`. * - * @param {String} key + * @param {List|String} path * @return {Node|Null} */ - getPreviousBlock(key) { - const child = this.assertDescendant(key) - let first + getPreviousBlock(path) { + path = this.resolvePath(path) + const match = this.getPreviousDeepMatchingNodeAndPath( + path, + n => n.object === 'block' + ) - if (child.object === 'block') { - first = child.getFirstText() - } else { - const block = this.getClosestBlock(key) - first = block.getFirstText() - } - - const previous = this.getPreviousText(first.key) - if (!previous) return null - - const closest = this.getClosestBlock(previous.key) - return closest + return match ? match[0] : null } /** @@ -1232,16 +1429,8 @@ class ElementInterface { if (range.isUnset) return List() const { start, end } = range - const startBlock = this.getFurthestBlock(start.key) - // PERF: the most common case is when the range is in a single block node, - // where we can avoid a lot of iterating of the tree. - if (start.key === end.key) return List([startBlock]) - - const endBlock = this.getFurthestBlock(end.key) - const startIndex = this.nodes.indexOf(startBlock) - const endIndex = this.nodes.indexOf(endBlock) - return this.nodes.slice(startIndex, endIndex + 1) + return this.nodes.slice(start.path.first(), end.path.first() + 1) } /** @@ -1303,6 +1492,76 @@ class ElementInterface { return null } + /** + * Get the previous node in the tree from a node that matches iterator + * + * This will not only check for siblings but instead move up the tree + * returning the previous ancestor if no sibling is found. + * + * @param {List} path + * @return {Node|Null} + */ + + getPreviousMatchingNodeAndPath(path, iterator = () => true) { + if (!path) return null + + for (let i = path.size; i > 0; i--) { + const p = path.slice(0, i) + if (p.last() === 0) continue + + let previousPath = PathUtils.decrement(p) + let previousNode = this.getNode(previousPath) + + while (previousNode && !iterator(previousNode)) { + previousPath = PathUtils.decrement(previousPath) + previousNode = this.getNode(previousPath) + } + + if (previousNode) return [previousNode, previousPath] + } + + return null + } + + /** + * Get the next previous in the tree from a node that matches iterator + * + * This will not only check for siblings but instead move up the tree + * returning the previous ancestor if no sibling is found. + * Once a node is found, the last deepest child matching is returned + * + * @param {List} path + * @param {Function} iterator + * @return {Node|Null} + */ + + getPreviousDeepMatchingNodeAndPath(path, iterator = () => true) { + const match = this.getPreviousMatchingNodeAndPath(path) + + if (!match) return null + + let [previousNode, previousPath] = match + + let childMatch + + const assign = () => { + childMatch = + previousNode.object !== 'text' && + previousNode.findLastDescendantAndPath(iterator, previousPath) + return childMatch + } + + while (assign(childMatch)) { + ;[previousNode, previousPath] = childMatch + } + + if (!previousNode) return null + + return iterator(previousNode) + ? [previousNode, previousPath] + : this.getPreviousDeepMatchingNodeAndPath(match[1], iterator) + } + /** * Get the previous sibling of a node. * @@ -1321,7 +1580,7 @@ class ElementInterface { } /** - * Get the text node after a descendant text node. + * Get the text node before a descendant text node. * * @param {List|String} path * @return {Node|Null} @@ -1333,8 +1592,18 @@ class ElementInterface { if (!path.size) return null const previous = this.getPreviousNode(path) if (!previous) return null - const text = previous.getLastText() - return text + const match = previous.getLastText() + return match + } + + getPreviousTextAndPath(path) { + if (!path) return null + if (!path.size) return null + const match = this.getPreviousDeepMatchingNodeAndPath( + path, + n => n.object === 'text' + ) + return match } /** @@ -1455,58 +1724,92 @@ class ElementInterface { } /** - * Get all of the text nodes in a `range`. + * Get all of the text nodes in a `range` as a List. * * @param {Range} range * @return {List} */ getTextsAtRange(range) { - range = this.resolveRange(range) - if (range.isUnset) return List() - const { start, end } = range - const list = List(this.getTextsBetweenPositionsAsArray(start.key, end.key)) - - return list + const arr = this.getTextsAtRangeAsArray(range) + return List(arr) } /** * Get all of the text nodes in a `range` as an array. * * @param {Range} range - * @return {Array} + * @return {Array} */ getTextsAtRangeAsArray(range) { range = this.resolveRange(range) if (range.isUnset) return [] const { start, end } = range - const texts = this.getTextsBetweenPositionsAsArray(start.key, end.key) + const texts = this.getTextsBetweenPathPositionsAsArray(start.path, end.path) return texts } /** * Get all of the text nodes in a `range` as an array. - * PERF: use key in arguments for cache + * PERF: use key / path in arguments for cache * - * @param {string} startKey - * @param {string} endKey + * @param {List|string} startPath + * @param {List|string} endPath * @returns {Array} */ - getTextsBetweenPositionsAsArray(startKey, endKey) { - const startText = this.getDescendant(startKey) + getTextsBetweenPositionsAsArray(startPath, endPath) { + startPath = this.resolvePath(startPath) + endPath = this.resolvePath(endPath) + return this.getTextsBetweenPathPositionsAsArray(startPath, endPath) + } + + /** + * Get all of the text nodes in a `range` as an array. + * + * @param {List|falsey} startPath + * @param {List|falsey} endPath + * @returns {Array} + */ + + getTextsBetweenPathPositionsAsArray(startPath, endPath) { // PERF: the most common case is when the range is in a single text node, // where we can avoid a lot of iterating of the tree. - if (startKey === endKey) return [startText] + if (startPath && endPath && PathUtils.isEqual(startPath, endPath)) { + return [this.getDescendant(startPath)] + } else if (!startPath && !endPath) { + return this.getTextsAsArray() + } - const endText = this.getDescendant(endKey) - const texts = this.getTextsAsArray() - const start = texts.indexOf(startText) - const end = texts.indexOf(endText, start) - const ret = texts.slice(start, end + 1) - return ret + const startIndex = startPath ? startPath.get(0, 0) : 0 + const endIndex = endPath + ? endPath.get(0, this.nodes.size - 1) + : this.nodes.size - 1 + + let array = [] + + this.nodes.slice(startIndex, endIndex + 1).forEach((node, i) => { + if (node.object === 'text') { + array.push(node) + } else { + // For the node at start and end of this list, we want to provide a start and end path + // For other nodes, we can just get all their text nodes, they are between the paths + const childStartPath = + startPath && i === 0 ? PathUtils.drop(startPath) : null + const childEndPath = + endPath && i === endIndex - startIndex + ? PathUtils.drop(endPath) + : null + + array = array.concat( + node.getTextsBetweenPathPositionsAsArray(childStartPath, childEndPath) + ) + } + }) + + return array } /** @@ -1619,9 +1922,10 @@ class ElementInterface { isLeafBlock() { const { object, nodes } = this + if (object !== 'block') return false if (!nodes.size) return true - const first = nodes.first() - return object === 'block' && first.object !== 'block' + + return nodes.first().object !== 'block' } /** @@ -1632,16 +1936,17 @@ class ElementInterface { isLeafInline() { const { object, nodes } = this + if (object !== 'inline') return false if (!nodes.size) return true - const first = nodes.first() - return object === 'inline' && first.object !== 'inline' + + return nodes.first().object !== 'inline' } /** * Check whether a descendant node is inside a range. This will return true for all * text nodes inside the range and all ancestors of those text nodes up to this node. * - * @param {List|Key} path + * @param {List|string} path * @param {Range} range * @return {Node} */ @@ -2005,7 +2310,7 @@ for (const method of ASSERTS) { memoize(ElementInterface.prototype, [ 'getBlocksAsArray', - 'getBlocksAtRangeAsArray', + 'getLeafBlocksAtRangeAsArray', 'getBlocksByTypeAsArray', 'getDecorations', 'getFragmentAtRange', @@ -2028,7 +2333,7 @@ memoize(ElementInterface.prototype, [ 'getTextAtOffset', 'getTextDirection', 'getTextsAsArray', - 'getTextsBetweenPositionsAsArray', + 'getTextsBetweenPathPositionsAsArray', ]) /** diff --git a/packages/slate/src/models/point.js b/packages/slate/src/models/point.js index 777c9b1da..30cd475c8 100644 --- a/packages/slate/src/models/point.js +++ b/packages/slate/src/models/point.js @@ -355,7 +355,23 @@ class Point extends Record(DEFAULTS) { } const { key, offset, path } = this - const target = node.getNode(key || path) + + // PERF: this function gets called a lot. + // to avoid creating the key -> path lookup table, we attempt to look up by path first. + let target = path && node.getNode(path) + + if (!target) { + target = node.getNode(key) + + if (target) { + // There is a misalignment of path and key + const point = this.merge({ + path: node.getPath(key), + }) + + return point + } + } if (!target) { warning(false, "A point's `path` or `key` invalid and was reset!") @@ -388,6 +404,8 @@ class Point extends Record(DEFAULTS) { if (target && path && key && key !== target.key) { warning(false, "A point's `key` did not match its `path`!") + + // TODO: if we look up by path above and it differs by key, do we want to reset it to looking up by key? } const point = this.merge({ diff --git a/packages/slate/src/utils/path-utils.js b/packages/slate/src/utils/path-utils.js index 749cbc858..2e35ddcaa 100644 --- a/packages/slate/src/utils/path-utils.js +++ b/packages/slate/src/utils/path-utils.js @@ -212,7 +212,7 @@ function isYounger(path, target) { * Lift a `path` to refer to its parent. * * @param {List} path - * @return {Array} + * @return {List} */ function lift(path) { @@ -220,6 +220,18 @@ function lift(path) { return parent } +/** + * Drop a `path`, returning the path from the first child. + * + * @param {List} path + * @return {List} + */ + +function drop(path) { + const relative = path.slice(1) + return relative +} + /** * Get the maximum length of paths `a` and `b`. * @@ -391,6 +403,7 @@ export default { isSibling, isYounger, lift, + drop, max, min, relate, diff --git a/packages/slate/test/models/node/get-active-marks-at-range.js/different-marks-across-blocks.js b/packages/slate/test/models/node/get-active-marks-at-range.js/different-marks-across-blocks.js new file mode 100644 index 000000000..c60237c22 --- /dev/null +++ b/packages/slate/test/models/node/get-active-marks-at-range.js/different-marks-across-blocks.js @@ -0,0 +1,34 @@ +/** @jsx h */ + +import h from '../../../helpers/h' +import { Set } from 'immutable' + +export const input = ( + + + + wo + rd + + + + middle + + + + unmarked + + + another + + unselected marked text + + + +) + +export default function({ document, selection }) { + return document.getActiveMarksAtRange(selection) +} + +export const output = Set() diff --git a/packages/slate/test/models/node/get-active-marks-at-range.js/mixed-marks-across-range.js b/packages/slate/test/models/node/get-active-marks-at-range.js/mixed-marks-across-range.js new file mode 100644 index 000000000..e335a3aaf --- /dev/null +++ b/packages/slate/test/models/node/get-active-marks-at-range.js/mixed-marks-across-range.js @@ -0,0 +1,38 @@ +/** @jsx h */ + +import h from '../../../helpers/h' +import { Set } from 'immutable' +import { Mark } from 'slate' + +export const input = ( + + + + wo + rd + + + + + middle + + + + + + + + another + + + unselected marked text + + + +) + +export default function({ document, selection }) { + return document.getActiveMarksAtRange(selection) +} + +export const output = Set.of(Mark.create('a')) diff --git a/packages/slate/test/models/node/get-active-marks-at-range.js/same-mark-across-blocks.js b/packages/slate/test/models/node/get-active-marks-at-range.js/same-mark-across-blocks.js new file mode 100644 index 000000000..c47729b42 --- /dev/null +++ b/packages/slate/test/models/node/get-active-marks-at-range.js/same-mark-across-blocks.js @@ -0,0 +1,34 @@ +/** @jsx h */ + +import h from '../../../helpers/h' +import { Set } from 'immutable' +import { Mark } from 'slate' + +export const input = ( + + + + wo + rd + + + + middle + + + + + + another + + unselected marked text + + + +) + +export default function({ document, selection }) { + return document.getActiveMarksAtRange(selection) +} + +export const output = Set.of(Mark.create('a')) diff --git a/packages/slate/test/models/node/get-closest/by-key.js b/packages/slate/test/models/node/get-closest/by-key.js new file mode 100644 index 000000000..22d992888 --- /dev/null +++ b/packages/slate/test/models/node/get-closest/by-key.js @@ -0,0 +1,26 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + two + + three + four + + +) + +export default function({ document, selection }) { + return document.getClosestBlock(selection.end.key) +} + +export const output = ( + + two + +) diff --git a/packages/slate/test/models/node/get-closest/get-block-parent-inline.js b/packages/slate/test/models/node/get-closest/get-block-parent-inline.js new file mode 100644 index 000000000..c7b392467 --- /dev/null +++ b/packages/slate/test/models/node/get-closest/get-block-parent-inline.js @@ -0,0 +1,30 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + + two + + + three + four + + +) + +export default function({ document, selection }) { + return document.getClosestBlock(selection.end.path) +} + +export const output = ( + + + two + + +) diff --git a/packages/slate/test/models/node/get-closest/top-level.js b/packages/slate/test/models/node/get-closest/top-level.js new file mode 100644 index 000000000..60703e9b7 --- /dev/null +++ b/packages/slate/test/models/node/get-closest/top-level.js @@ -0,0 +1,21 @@ +/** @jsx h */ + +import h from '../../../helpers/h' +import { PathUtils } from 'slate' + +export const input = ( + + + one + two + three + four + + +) + +export default function({ document, selection }) { + return document.getClosestBlock(PathUtils.create([1])) +} + +export const output = null diff --git a/packages/slate/test/models/node/get-fragment-at-range/across-block-with-marks.js b/packages/slate/test/models/node/get-fragment-at-range/across-block-with-marks.js new file mode 100644 index 000000000..91faa3aef --- /dev/null +++ b/packages/slate/test/models/node/get-fragment-at-range/across-block-with-marks.js @@ -0,0 +1,40 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + wo + rd + + + middle + + + + another + + + + +) + +export default function({ document, selection }) { + return document.getFragmentAtRange(selection) +} + +export const output = ( + + + rd + + + middle + + + an + + +) diff --git a/packages/slate/test/models/node/get-furthest-only-child/multiple-nodes-in-nested-block.js b/packages/slate/test/models/node/get-furthest-only-child/multiple-nodes-in-nested-block.js new file mode 100644 index 000000000..9dd10a117 --- /dev/null +++ b/packages/slate/test/models/node/get-furthest-only-child/multiple-nodes-in-nested-block.js @@ -0,0 +1,23 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + + one + two + + + + +) + +export default function(value) { + const { document } = value + return document.getFurthestOnlyChildAncestor('a') +} + +export const output = null diff --git a/packages/slate/test/models/node/get-furthest-only-child/multiple-nodes.js b/packages/slate/test/models/node/get-furthest-only-child/multiple-nodes.js new file mode 100644 index 000000000..a4c867b2e --- /dev/null +++ b/packages/slate/test/models/node/get-furthest-only-child/multiple-nodes.js @@ -0,0 +1,21 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + one + two + + + +) + +export default function(value) { + const { document } = value + return document.getFurthestOnlyChildAncestor('a') +} + +export const output = null diff --git a/packages/slate/test/models/node/get-next-block/by-key.js b/packages/slate/test/models/node/get-next-block/by-key.js new file mode 100644 index 000000000..6004f50d3 --- /dev/null +++ b/packages/slate/test/models/node/get-next-block/by-key.js @@ -0,0 +1,22 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + two + + three + four + + +) + +export default function({ document, selection }) { + return document.getNextBlock(selection.end.key) +} + +export const output = three diff --git a/packages/slate/test/models/node/get-next-block/multiple-siblings-to-bypass.js b/packages/slate/test/models/node/get-next-block/multiple-siblings-to-bypass.js new file mode 100644 index 000000000..90cc97864 --- /dev/null +++ b/packages/slate/test/models/node/get-next-block/multiple-siblings-to-bypass.js @@ -0,0 +1,24 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + two + + + + three + four + + +) + +export default function({ document, selection }) { + return document.getNextBlock(selection.end.path) +} + +export const output = three diff --git a/packages/slate/test/models/node/get-next-block/next-block-is-ancestor-sibling.js b/packages/slate/test/models/node/get-next-block/next-block-is-ancestor-sibling.js new file mode 100644 index 000000000..84f2f102a --- /dev/null +++ b/packages/slate/test/models/node/get-next-block/next-block-is-ancestor-sibling.js @@ -0,0 +1,22 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + two + + three + four + + +) + +export default function({ document, selection }) { + return document.getNextBlock(selection.end.path) +} + +export const output = three diff --git a/packages/slate/test/models/node/get-next-block/next-block-is-in-sibling-with-deeper-blocks.js b/packages/slate/test/models/node/get-next-block/next-block-is-in-sibling-with-deeper-blocks.js new file mode 100644 index 000000000..1618f9dd5 --- /dev/null +++ b/packages/slate/test/models/node/get-next-block/next-block-is-in-sibling-with-deeper-blocks.js @@ -0,0 +1,27 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + 1 + + 2 + + + 3 + + 3.1 + + + 4 + + +) + +export default function({ document, selection }) { + return document.getNextBlock(selection.end.path) +} + +export const output = 3 diff --git a/packages/slate/test/models/node/get-next-block/next-block-is-sibling-descendent.js b/packages/slate/test/models/node/get-next-block/next-block-is-sibling-descendent.js new file mode 100644 index 000000000..15ef47df0 --- /dev/null +++ b/packages/slate/test/models/node/get-next-block/next-block-is-sibling-descendent.js @@ -0,0 +1,24 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + two + + + three + + four + + +) + +export default function({ document, selection }) { + return document.getNextBlock(selection.end.path) +} + +export const output = three diff --git a/packages/slate/test/models/node/get-next-block/next-block-is-sibling.js b/packages/slate/test/models/node/get-next-block/next-block-is-sibling.js new file mode 100644 index 000000000..ea25cd59c --- /dev/null +++ b/packages/slate/test/models/node/get-next-block/next-block-is-sibling.js @@ -0,0 +1,21 @@ +/** @jsx h */ + +import h from '../../../helpers/h' +import PathUtils from '../../../../src/utils/path-utils' + +export const input = ( + + + one + two + three + four + + +) + +export default function({ document }) { + return document.getNextBlock(PathUtils.create([1])) +} + +export const output = three diff --git a/packages/slate/test/models/node/get-next-block/no-next-block.js b/packages/slate/test/models/node/get-next-block/no-next-block.js new file mode 100644 index 000000000..3f5ef1b5c --- /dev/null +++ b/packages/slate/test/models/node/get-next-block/no-next-block.js @@ -0,0 +1,25 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + two + + three + + + four + five + + + +) + +export default function({ document, selection }) { + return document.getNextBlock(selection.end.path) +} + +export const output = null diff --git a/packages/slate/test/models/node/get-next-block/no-next-node.js b/packages/slate/test/models/node/get-next-block/no-next-node.js new file mode 100644 index 000000000..b10f89ae1 --- /dev/null +++ b/packages/slate/test/models/node/get-next-block/no-next-node.js @@ -0,0 +1,24 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + two + + three + + + four + + + +) + +export default function({ document, selection }) { + return document.getNextBlock(selection.end.path) +} + +export const output = null diff --git a/packages/slate/test/models/node/get-next-node/next-node-is-ancestor-sibling-with-nested-blocks.js b/packages/slate/test/models/node/get-next-node/next-node-is-ancestor-sibling-with-nested-blocks.js new file mode 100644 index 000000000..c444422e4 --- /dev/null +++ b/packages/slate/test/models/node/get-next-node/next-node-is-ancestor-sibling-with-nested-blocks.js @@ -0,0 +1,28 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + two + + + three + + four + + +) + +export default function({ document, selection }) { + return document.getNextNode(selection.end.path) +} + +export const output = ( + + three + +) diff --git a/packages/slate/test/models/node/get-next-node/next-node-is-ancestor-sibling.js b/packages/slate/test/models/node/get-next-node/next-node-is-ancestor-sibling.js new file mode 100644 index 000000000..83e4ba799 --- /dev/null +++ b/packages/slate/test/models/node/get-next-node/next-node-is-ancestor-sibling.js @@ -0,0 +1,22 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + two + + three + four + + +) + +export default function({ document, selection }) { + return document.getNextNode(selection.end.path) +} + +export const output = three diff --git a/packages/slate/test/models/node/get-previous-block/by-key.js b/packages/slate/test/models/node/get-previous-block/by-key.js new file mode 100644 index 000000000..3bde78497 --- /dev/null +++ b/packages/slate/test/models/node/get-previous-block/by-key.js @@ -0,0 +1,22 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + two + + three + four + + +) + +export default function({ document, selection }) { + return document.getPreviousBlock(selection.end.key) +} + +export const output = one diff --git a/packages/slate/test/models/node/get-previous-block/multiple-siblings-to-bypass.js b/packages/slate/test/models/node/get-previous-block/multiple-siblings-to-bypass.js new file mode 100644 index 000000000..aa542a8ff --- /dev/null +++ b/packages/slate/test/models/node/get-previous-block/multiple-siblings-to-bypass.js @@ -0,0 +1,24 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + + + two + + three + four + + +) + +export default function({ document, selection }) { + return document.getPreviousBlock(selection.end.path) +} + +export const output = one diff --git a/packages/slate/test/models/node/get-previous-block/no-prev-block.js b/packages/slate/test/models/node/get-previous-block/no-prev-block.js new file mode 100644 index 000000000..f5e3473f7 --- /dev/null +++ b/packages/slate/test/models/node/get-previous-block/no-prev-block.js @@ -0,0 +1,25 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + zer + one + + two + three + + four + + + +) + +export default function({ document, selection }) { + return document.getPreviousBlock(selection.end.path) +} + +export const output = null diff --git a/packages/slate/test/models/node/get-previous-block/no-prev-node.js b/packages/slate/test/models/node/get-previous-block/no-prev-node.js new file mode 100644 index 000000000..ee006a4ec --- /dev/null +++ b/packages/slate/test/models/node/get-previous-block/no-prev-node.js @@ -0,0 +1,24 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + one + + two + + three + + four + + +) + +export default function({ document, selection }) { + return document.getPreviousBlock(selection.end.path) +} + +export const output = null diff --git a/packages/slate/test/models/node/get-previous-block/prev-block-is-ancestor-sibling.js b/packages/slate/test/models/node/get-previous-block/prev-block-is-ancestor-sibling.js new file mode 100644 index 000000000..d4f81976f --- /dev/null +++ b/packages/slate/test/models/node/get-previous-block/prev-block-is-ancestor-sibling.js @@ -0,0 +1,22 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + two + + three + + four + + +) + +export default function({ document, selection }) { + return document.getPreviousBlock(selection.end.path) +} + +export const output = two diff --git a/packages/slate/test/models/node/get-previous-block/prev-block-is-in-sibling-with-deeper-blocks.js b/packages/slate/test/models/node/get-previous-block/prev-block-is-in-sibling-with-deeper-blocks.js new file mode 100644 index 000000000..a3661cb6d --- /dev/null +++ b/packages/slate/test/models/node/get-previous-block/prev-block-is-in-sibling-with-deeper-blocks.js @@ -0,0 +1,32 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + 1 + + 2 + + 2.1 + + + + + 3.1 + + 3.2 + + + 4> + + + +) + +export default function({ document, selection }) { + return document.getPreviousBlock(selection.end.path) +} + +export const output = 3.2 diff --git a/packages/slate/test/models/node/get-previous-block/prev-block-is-sibling-descendent.js b/packages/slate/test/models/node/get-previous-block/prev-block-is-sibling-descendent.js new file mode 100644 index 000000000..2b49bdfcd --- /dev/null +++ b/packages/slate/test/models/node/get-previous-block/prev-block-is-sibling-descendent.js @@ -0,0 +1,24 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + one + + two + + + three + + four + + +) + +export default function({ document, selection }) { + return document.getPreviousBlock(selection.end.path) +} + +export const output = one diff --git a/packages/slate/test/models/node/get-previous-block/prev-block-is-sibling.js b/packages/slate/test/models/node/get-previous-block/prev-block-is-sibling.js new file mode 100644 index 000000000..7d13b0458 --- /dev/null +++ b/packages/slate/test/models/node/get-previous-block/prev-block-is-sibling.js @@ -0,0 +1,21 @@ +/** @jsx h */ + +import h from '../../../helpers/h' +import PathUtils from '../../../../src/utils/path-utils' + +export const input = ( + + + one + two + three + four + + +) + +export default function({ document }) { + return document.getPreviousBlock(PathUtils.create([2])) +} + +export const output = two diff --git a/packages/slate/test/models/node/get-selection-indexes/across-blocks-from-nested-node.js b/packages/slate/test/models/node/get-selection-indexes/across-blocks-from-nested-node.js new file mode 100644 index 000000000..86fd70b7a --- /dev/null +++ b/packages/slate/test/models/node/get-selection-indexes/across-blocks-from-nested-node.js @@ -0,0 +1,35 @@ +/** @jsx h */ + +import h from '../../../helpers/h' +import PathUtils from '../../../../src/utils/path-utils' + +export const input = ( + + + + wo + rd + + + + middle + + + + unmarked + + + another + + unselected marked text + + + +) + +export default function({ document, selection }) { + const node = document.getDescendant(PathUtils.create([1])) + return node.getSelectionIndexes(selection, [1]) +} + +export const output = { start: 0, end: 2 } diff --git a/packages/slate/test/models/node/get-selection-indexes/across-blocks.js b/packages/slate/test/models/node/get-selection-indexes/across-blocks.js new file mode 100644 index 000000000..25a690991 --- /dev/null +++ b/packages/slate/test/models/node/get-selection-indexes/across-blocks.js @@ -0,0 +1,33 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + wo + rd + + + + middle + + + + unmarked + + + another + + unselected marked text + + + +) + +export default function({ document, selection }) { + return document.getSelectionIndexes(selection) +} + +export const output = { start: 0, end: 4 } diff --git a/packages/slate/test/models/node/get-selection-indexes/in-single-block-from-middle-nested-node.js b/packages/slate/test/models/node/get-selection-indexes/in-single-block-from-middle-nested-node.js new file mode 100644 index 000000000..903bddb0d --- /dev/null +++ b/packages/slate/test/models/node/get-selection-indexes/in-single-block-from-middle-nested-node.js @@ -0,0 +1,26 @@ +/** @jsx h */ + +import h from '../../../helpers/h' +import PathUtils from '../../../../src/utils/path-utils' + +export const input = ( + + + + before + start + + inline text + end + after + + + +) + +export default function({ document, selection }) { + const node = document.getDescendant(PathUtils.create([0, 2])) + return node.getSelectionIndexes(selection, [0, 2]) +} + +export const output = { start: 0, end: 1 } diff --git a/packages/slate/test/models/node/get-selection-indexes/in-single-block-from-parent-node.js b/packages/slate/test/models/node/get-selection-indexes/in-single-block-from-parent-node.js new file mode 100644 index 000000000..ec1ac4ad9 --- /dev/null +++ b/packages/slate/test/models/node/get-selection-indexes/in-single-block-from-parent-node.js @@ -0,0 +1,26 @@ +/** @jsx h */ + +import h from '../../../helpers/h' +import PathUtils from '../../../../src/utils/path-utils' + +export const input = ( + + + + before + start + + inline text + end + after + + + +) + +export default function({ document, selection }) { + const node = document.getDescendant(PathUtils.create([0])) + return node.getSelectionIndexes(selection, [0]) +} + +export const output = { start: 0, end: 4 } diff --git a/packages/slate/test/models/node/get-selection-indexes/in-single-block.js b/packages/slate/test/models/node/get-selection-indexes/in-single-block.js new file mode 100644 index 000000000..5b65f07e8 --- /dev/null +++ b/packages/slate/test/models/node/get-selection-indexes/in-single-block.js @@ -0,0 +1,24 @@ +/** @jsx h */ + +import h from '../../../helpers/h' + +export const input = ( + + + + before + start + + inline text + end + after + + + +) + +export default function({ document, selection }) { + return document.getSelectionIndexes(selection) +} + +export const output = { start: 0, end: 1 }