···66 }
77 morphNodes(node, guide, idMap);
88}
99+// For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements.
910function populateIdSets(node, idMap) {
1011 const elementsWithIds = node.querySelectorAll("[id]");
1112 for (const elementWithId of elementsWithIds) {
1213 const id = elementWithId.id;
1414+ // Ignore empty IDs
1315 if (id === "")
1416 continue;
1517 let current = elementWithId;
···2224 }
2325 }
2426}
2727+// This is where we actually morph the nodes. The `morph` function exists to set up the `idMap`.
2528function morphNodes(node, guide, idMap, insertBefore, parent) {
2929+ // TODO: We should extract this into a separate function.
2630 if (parent && insertBefore && insertBefore !== node)
2731 parent.insertBefore(guide, insertBefore);
2832 if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) {
3333+ // We need to check if the element is an input, option, or textarea here, because they have
3434+ // special attributes not covered by the isEqualNode check.
2935 if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(guide))
3036 return;
3137 else {
···5157 }
5258}
5359function morphAttributes(elem, guide) {
6060+ // Remove any excess attributes from the element that aren’t present in the guide.
5461 for (const { name } of elem.attributes)
5562 guide.hasAttribute(name) || elem.removeAttribute(name);
6363+ // Copy attributes from the guide to the element, if they don’t already match.
5664 for (const { name, value } of guide.attributes)
5765 elem.getAttribute(name) === value || elem.setAttribute(name, value);
5866 elem.nodeValue;
6767+ // For certain types of elements, we need to do some extra work to ensure the element’s state matches the guide’s state.
5968 if (isInput(elem) && isInput(guide)) {
6069 if (elem.checked !== guide.checked)
6170 elem.checked = guide.checked;
···7685 text.textContent = guide.value;
7786 }
7887}
8888+// Iterates over the child nodes of the guide element, morphing the main element’s child nodes to match.
7989function morphChildNodes(elem, guide, idMap) {
8090 const childNodes = [...elem.childNodes];
8191 const guideChildNodes = [...guide.childNodes];
···8999 else if (child)
90100 child.remove();
91101 }
102102+ // Remove any excess child nodes from the main element. This is separate because
103103+ // the loop above might modify the length of the main element’s child nodes.
92104 while (elem.childNodes.length > guide.childNodes.length)
93105 elem.lastChild?.remove();
94106}
···100112}
101113function morphChildElement(child, guide, parent, idMap) {
102114 const guideIdSet = idMap.get(guide);
115115+ // Generate the array in advance of the loop
103116 const guideSetArray = guideIdSet ? [...guideIdSet] : [];
104117 let currentNode = child;
105118 let nextMatchByTagName = null;
119119+ // Try find a match by idSet, while also looking out for the next best match by tagName.
106120 while (currentNode) {
107121 if (isElement(currentNode)) {
108122 if (currentNode.id === guide.id) {
···125139 else
126140 child.replaceWith(guide.cloneNode(true));
127141}
142142+// We cannot use `instanceof` when nodes might be from different documents,
143143+// so we use type guards instead. This keeps TypeScript happy, while doing
144144+// the necessary checks at runtime.
128145function isText(node) {
129146 return node.nodeType === 3;
130147}