Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lexical-utils] Fix: Modify $reverseDfs to be a right-to-left variant of $dfs #7112

Merged
merged 1 commit into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 86 additions & 71 deletions packages/lexical-utils/src/__tests__/unit/LexicalNodeHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
$getNodeByKey,
$getRoot,
$isElementNode,
ElementNode,
LexicalEditor,
LexicalNode,
NodeKey,
} from 'lexical';
import {
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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<LexicalNode>,
): Iterable<DFSKeyPair> {
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()})),
);
});
});

Expand Down Expand Up @@ -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];
Expand All @@ -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);
});
});
});
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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(),
},
]);
});
Expand Down
93 changes: 52 additions & 41 deletions packages/lexical-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ const iteratorNotDone: <T>(value: T) => Readonly<{done: false; value: T}> = <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).
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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.
Expand Down
Loading