diff --git a/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts b/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts index 734f2636dfa..dcd8f52f6de 100644 --- a/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts +++ b/packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts @@ -12,7 +12,9 @@ import { $getNodeByKey, $getRoot, $isElementNode, + ElementNode, LexicalEditor, + LexicalNode, NodeKey, } from 'lexical'; import { @@ -21,15 +23,24 @@ import { invariant, } from 'lexical/src/__tests__/utils'; -import {$dfs, $getNextSiblingOrParentSibling, $reverseDfs} from '../..'; +import { + $dfs, + $firstToLastIterator, + $getNextSiblingOrParentSibling, + $lastToFirstIterator, + $reverseDfs, +} from '../..'; + +interface DFSKeyPair { + depth: number; + node: NodeKey; +} describe('LexicalNodeHelpers tests', () => { initializeUnitTest((testEnv) => { describe('dfs order', () => { - let expectedKeys: Array<{ - depth: number; - node: NodeKey; - }> = []; + let expectedKeys: DFSKeyPair[]; + let reverseExpectedKeys: DFSKeyPair[]; /** * R @@ -38,6 +49,8 @@ describe('LexicalNodeHelpers tests', () => { * T1 T2 T3 T6 * * DFS: R, P1, B1, T1, B2, T2, T3, P2, T4, T5, B3, T6 + * + * Reverse DFS: R, P2, B3, T6, T5, T4, P1, B2, T3, T2, B1, T1 */ beforeEach(async () => { const editor: LexicalEditor = testEnv.editor; @@ -72,56 +85,56 @@ describe('LexicalNodeHelpers tests', () => { block3.append(text6); - expectedKeys = [ - { - depth: 0, - node: root.getKey(), - }, - { - depth: 1, - node: paragraph1.getKey(), - }, - { - depth: 2, - node: block1.getKey(), - }, - { - depth: 3, - node: text1.getKey(), - }, - { - depth: 2, - node: block2.getKey(), - }, - { - depth: 3, - node: text2.getKey(), - }, - { - depth: 3, - node: text3.getKey(), - }, - { - depth: 1, - node: paragraph2.getKey(), - }, - { - depth: 2, - node: text4.getKey(), - }, - { - depth: 2, - node: text5.getKey(), - }, - { - depth: 2, - node: block3.getKey(), - }, - { - depth: 3, - node: text6.getKey(), - }, - ]; + function* keysForNode( + depth: number, + node: LexicalNode, + $getChildren: (element: ElementNode) => Iterable, + ): Iterable { + yield {depth, node: node.getKey()}; + if ($isElementNode(node)) { + const childDepth = depth + 1; + for (const child of $getChildren(node)) { + yield* keysForNode(childDepth, child, $getChildren); + } + } + } + + expectedKeys = [...keysForNode(0, root, $firstToLastIterator)]; + reverseExpectedKeys = [...keysForNode(0, root, $lastToFirstIterator)]; + // R, P1, B1, T1, B2, T2, T3, P2, T4, T5, B3, T6 + expect(expectedKeys).toEqual( + [ + root, + paragraph1, + block1, + text1, + block2, + text2, + text3, + paragraph2, + text4, + text5, + block3, + text6, + ].map((n) => ({depth: n.getParentKeys().length, node: n.getKey()})), + ); + // R, P2, B3, T6, T5, T4, P1, B2, T3, T2, B1, T1 + expect(reverseExpectedKeys).toEqual( + [ + root, + paragraph2, + block3, + text6, + text5, + text4, + paragraph1, + block2, + text3, + text2, + block1, + text1, + ].map((n) => ({depth: n.getParentKeys().length, node: n.getKey()})), + ); }); }); @@ -150,12 +163,12 @@ describe('LexicalNodeHelpers tests', () => { test('Reverse DFS node order', async () => { const editor: LexicalEditor = testEnv.editor; editor.getEditorState().read(() => { - const expectedNodes = expectedKeys - .map(({depth, node: nodeKey}) => ({ + const expectedNodes = reverseExpectedKeys.map( + ({depth, node: nodeKey}) => ({ depth, node: $getNodeByKey(nodeKey)!.getLatest(), - })) - .reverse(); + }), + ); const first = expectedNodes[0]; const second = expectedNodes[1]; @@ -167,9 +180,7 @@ describe('LexicalNodeHelpers tests', () => { expectedNodes.slice(1, expectedNodes.length - 1), ); expect($reverseDfs()).toEqual(expectedNodes); - expect($reverseDfs($getRoot().getLastDescendant()!)).toEqual( - expectedNodes, - ); + expect($reverseDfs($getRoot())).toEqual(expectedNodes); }); }); }); @@ -206,6 +217,8 @@ describe('LexicalNodeHelpers tests', () => { const block3 = $createTestElementNode(); invariant($isElementNode(block1)); + // this will (only) change the latest state of block1 + // all other nodes will be the same version block1.append(block3); expect($dfs(root!)).toEqual([ @@ -265,28 +278,30 @@ describe('LexicalNodeHelpers tests', () => { const block3 = $createTestElementNode(); invariant($isElementNode(block1)); + // this will (only) change the latest state of block1 + // all other nodes will be the same version block1.append(block3); expect($reverseDfs()).toEqual([ { - depth: 2, - node: block2!.getLatest(), + depth: 0, + node: root!.getLatest(), }, { - depth: 3, - node: block3.getLatest(), + depth: 1, + node: paragraph!.getLatest(), }, { depth: 2, - node: block1.getLatest(), + node: block2!.getLatest(), }, { - depth: 1, - node: paragraph!.getLatest(), + depth: 2, + node: block1.getLatest(), }, { - depth: 0, - node: root!.getLatest(), + depth: 3, + node: block3.getLatest(), }, ]); }); diff --git a/packages/lexical-utils/src/index.ts b/packages/lexical-utils/src/index.ts index e98cd572887..b850a6052d5 100644 --- a/packages/lexical-utils/src/index.ts +++ b/packages/lexical-utils/src/index.ts @@ -214,7 +214,7 @@ const iteratorNotDone: (value: T) => Readonly<{done: false; value: T}> = ( ) => ({done: false, value}); /** - * $dfs iterator. Tree traversal is done on the fly as new values are requested with O(1) memory. + * $dfs iterator (left to right). Tree traversal is done on the fly as new values are requested with O(1) memory. * @param startNode - The node to start the search, if omitted, it will start at the root node. * @param endNode - The node to end the search, if omitted, it will find all descendants of the startingNode. * @returns An iterator, each yielded value is a DFSNode. It will always return at least 1 node (the start node). @@ -300,6 +300,39 @@ export function $getNextSiblingOrParentSibling( return [node_, depthDiff]; } +/** + * Returns the Node's previous sibling when this exists, otherwise the closest parent previous sibling. For example + * R -> P -> T1, T2 + * -> P2 + * returns T1 for node T2, P for node P2, and null for node P + * @param node LexicalNode. + * @returns An array (tuple) containing the found Lexical node and the depth difference, or null, if this node doesn't exist. + */ +function $getPreviousSiblingOrParentSibling( + node: LexicalNode, +): null | [LexicalNode, number] { + let node_: null | LexicalNode = node; + // Find immediate sibling or nearest parent sibling + let sibling = null; + let depthDiff = 0; + + while (sibling === null && node_ !== null) { + sibling = node_.getPreviousSibling(); + + if (sibling === null) { + node_ = node_.getParent(); + depthDiff--; + } else { + node_ = sibling; + } + } + + if (node_ === null) { + return null; + } + return [node_, depthDiff]; +} + export function $getDepth(node: LexicalNode): number { let innerNode: LexicalNode | null = node; let depth = 0; @@ -343,23 +376,18 @@ export function $getNextRightPreorderNode( } /** - * An iterator which will traverse the tree in exactly the reversed order of $dfsIterator. Tree traversal is done - * on the fly as new values are requested with O(1) memory. - * @param startNode - The node to start the search. If omitted, it will start at the last leaf node in the tree. - * @param endNode - The node to end the search. If omitted, it will work backwards all the way to the root node + * $dfs iterator (right to left). Tree traversal is done on the fly as new values are requested with O(1) memory. + * @param startNode - The node to start the search, if omitted, it will start at the root node. + * @param endNode - The node to end the search, if omitted, it will find all descendants of the startingNode. * @returns An iterator, each yielded value is a DFSNode. It will always return at least 1 node (the start node). */ export function $reverseDfsIterator( startNode?: LexicalNode, endNode?: LexicalNode, ): DFSIterator { - const start = ( - startNode || - $getRoot().getLastDescendant() || - $getRoot() - ).getLatest(); + const start = (startNode || $getRoot()).getLatest(); const startDepth = $getDepth(start); - const end = endNode || $getRoot(); + const end = endNode; let node: null | LexicalNode = start; let depth = startDepth; let isFirstNext = true; @@ -376,16 +404,22 @@ export function $reverseDfsIterator( if (node === end) { return iteratorDone; } - if (node.getPreviousSibling()) { + + if ($isElementNode(node) && node.getChildrenSize() > 0) { + node = node.getLastChild(); + depth++; + } else { let depthDiff; - [node, depthDiff] = $getPreviousSiblingsLastDescendantOrPreviousSibling( - node, - ) || [null, 0]; + [node, depthDiff] = $getPreviousSiblingOrParentSibling(node) || [ + null, + 0, + ]; depth += depthDiff; - } else { - node = node.getParent(); - depth--; + if (end == null && depth <= startDepth) { + node = null; + } } + if (node === null) { return iteratorDone; } @@ -398,29 +432,6 @@ export function $reverseDfsIterator( return iterator; } -/** - * Returns the previous sibling's last descendant (when it exists) or the previous sibling. - * R -> P -> T1 - * -> T2 - * -> P2 - * returns T1 for node T2, T2 for node P2, and null for node T1 or P. - * @param node LexicalNode. - * @returns an array (tuple) coontaining the found Lexical node and the depth difference, or null, if this node doesn't exist. - */ -function $getPreviousSiblingsLastDescendantOrPreviousSibling( - node: LexicalNode, -): null | [LexicalNode, number] { - let _node: LexicalNode | null = node.getPreviousSibling(); - let depthDiff = 0; - while ($isElementNode(_node) && _node.getChildrenSize() > 0) { - _node = _node.getLastChild(); - depthDiff++; - } - if (_node === null) { - return null; - } - return [_node, depthDiff]; -} /** * Takes a node and traverses up its ancestors (toward the root node) * in order to find a specific type of node.