You have two DOM trees that are structural mirrors of each other: same shape, same number of children at every level, same order — but their text and attributes may differ (think the same page rendered in two languages, or before and after a translation pass). Given a node somewhere inside the first tree, find the node in the second tree that sits in the exact same spot. This is a real problem behind tools that sync a selection, a highlight, or a cursor position from one rendered copy of a document to another.
The key insight: identify a node by its position — the path of child indices from the root down to it — not by its tag, id, or text, because the mirror may contain repeated or identical elements.
function correspondingNodeAcrossPages(
rootA: Node, // root of the first tree
rootB: Node, // root of the second tree (a structural mirror of rootA)
nodeA: Node, // a node somewhere inside rootA
): Node; // the node in rootB at the same path from the root
Index positions are taken over childNodes (so text nodes count toward the index), making the mapping an exact structural mirror. If nodeA === rootA, return rootB.
const a = document.createElement('div');
a.innerHTML = '<ul><li>one</li><li>two</li><li>three</li></ul>';
const b = document.createElement('div');
b.innerHTML = '<ul><li>uno</li><li>dos</li><li>tres</li></ul>';
const secondLiA = a.querySelectorAll('li')[1]; // <li>two</li>
correspondingNodeAcrossPages(a, b, secondLiA);
// → <li>dos</li> (the 2nd li in b, NOT the first)
// nodeA is the root itself
correspondingNodeAcrossPages(a, b, a); // → b
// a text node maps to the corresponding text node
const textA = a.querySelector('li').childNodes[0]; // the "one" text node
correspondingNodeAcrossPages(a, b, textA); // → the "uno" text node
<li>). Matching by tag, id, or text would return the wrong sibling; only the position is reliable.childNodes, not children. childNodes includes text and comment nodes, so indices count every node. This keeps the mapping an exact structural mirror and lets you map text nodes too.nodeA is rootA, the answer is rootB — there is no path to walk.rootB, not a clone or a copy.