···1919```javascript
2020import { morph } from "morphlex";
21212222-morph(currentNode, guideNode);
2222+morph(currentNode, referenceNode);
2323```
2424+2525+The `currentNode` will be morphed into the state of the `referenceNode`. The `referenceNode` will not be mutated in this process.
24262527## Run tests
2628
+1-1
dist/morphlex.d.ts
···11-export declare function morph(node: ChildNode, guide: ChildNode): void;
11+export declare function morph(node: ChildNode, reference: ChildNode): void;
+54-53
dist/morphlex.js
···11-export function morph(node, guide) {
11+export function morph(node, reference) {
22 const idMap = new Map();
33- if (isParentNode(node) && isParentNode(guide)) {
33+ if (isParentNode(node) && isParentNode(reference)) {
44 populateIdSets(node, idMap);
55- populateIdSets(guide, idMap);
55+ populateIdSets(reference, idMap);
66 }
77- morphNodes(node, guide, idMap);
77+ morphNodes(node, reference, idMap);
88}
99// For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements.
1010function populateIdSets(node, idMap) {
···2323 }
2424}
2525// This is where we actually morph the nodes. The `morph` function exists to set up the `idMap`.
2626-function morphNodes(node, guide, idMap) {
2727- if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) {
2626+function morphNodes(node, ref, idMap) {
2727+ if (isElement(node) && isElement(ref) && node.tagName === ref.tagName) {
2828 // We need to check if the element is an input, option, or textarea here, because they have
2929 // special attributes not covered by the isEqualNode check.
3030- if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(guide)) return;
3030+ if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(ref)) return;
3131 else {
3232- if (node.hasAttributes() || guide.hasAttributes()) morphAttributes(node, guide);
3333- if (node.hasChildNodes() || guide.hasChildNodes()) morphChildNodes(node, guide, idMap);
3232+ if (node.hasAttributes() || ref.hasAttributes()) morphAttributes(node, ref);
3333+ if (node.hasChildNodes() || ref.hasChildNodes()) morphChildNodes(node, ref, idMap);
3434 }
3535 } else {
3636- if (node.isEqualNode(guide)) return;
3737- else if (isText(node) && isText(guide)) {
3838- if (node.textContent !== guide.textContent) node.textContent = guide.textContent;
3939- } else if (isComment(node) && isComment(guide)) {
4040- if (node.nodeValue !== guide.nodeValue) node.nodeValue = guide.nodeValue;
4141- } else node.replaceWith(guide.cloneNode(true));
3636+ if (node.isEqualNode(ref)) return;
3737+ else if (isText(node) && isText(ref)) {
3838+ if (node.textContent !== ref.textContent) node.textContent = ref.textContent;
3939+ } else if (isComment(node) && isComment(ref)) {
4040+ if (node.nodeValue !== ref.nodeValue) node.nodeValue = ref.nodeValue;
4141+ } else node.replaceWith(ref.cloneNode(true));
4242 }
4343}
4444-function morphAttributes(elem, guide) {
4545- // Remove any excess attributes from the element that aren’t present in the guide.
4646- for (const { name } of elem.attributes) guide.hasAttribute(name) || elem.removeAttribute(name);
4747- // Copy attributes from the guide to the element, if they don’t already match.
4848- for (const { name, value } of guide.attributes) elem.getAttribute(name) === value || elem.setAttribute(name, value);
4949- elem.nodeValue;
5050- // For certain types of elements, we need to do some extra work to ensure the element’s state matches the guide’s state.
5151- if (isInput(elem) && isInput(guide)) {
5252- if (elem.checked !== guide.checked) elem.checked = guide.checked;
5353- if (elem.disabled !== guide.disabled) elem.disabled = guide.disabled;
5454- if (elem.indeterminate !== guide.indeterminate) elem.indeterminate = guide.indeterminate;
5555- if (elem.type !== "file" && elem.value !== guide.value) elem.value = guide.value;
5656- } else if (isOption(elem) && isOption(guide) && elem.selected !== guide.selected) elem.selected = guide.selected;
5757- else if (isTextArea(elem) && isTextArea(guide)) {
5858- if (elem.value !== guide.value) elem.value = guide.value;
5959- const text = elem.firstChild;
6060- if (text && isText(text) && text.textContent !== guide.value) text.textContent = guide.value;
4444+function morphAttributes(elm, ref) {
4545+ // Remove any excess attributes from the element that aren’t present in the reference.
4646+ for (const { name } of elm.attributes) ref.hasAttribute(name) || elm.removeAttribute(name);
4747+ // Copy attributes from the reference to the element, if they don’t already match.
4848+ for (const { name, value } of ref.attributes) elm.getAttribute(name) === value || elm.setAttribute(name, value);
4949+ elm.nodeValue;
5050+ // For certain types of elements, we need to do some extra work to ensure
5151+ // the element’s state matches the reference elements’ state.
5252+ if (isInput(elm) && isInput(ref)) {
5353+ if (elm.checked !== ref.checked) elm.checked = ref.checked;
5454+ if (elm.disabled !== ref.disabled) elm.disabled = ref.disabled;
5555+ if (elm.indeterminate !== ref.indeterminate) elm.indeterminate = ref.indeterminate;
5656+ if (elm.type !== "file" && elm.value !== ref.value) elm.value = ref.value;
5757+ } else if (isOption(elm) && isOption(ref) && elm.selected !== ref.selected) elm.selected = ref.selected;
5858+ else if (isTextArea(elm) && isTextArea(ref)) {
5959+ if (elm.value !== ref.value) elm.value = ref.value;
6060+ const text = elm.firstChild;
6161+ if (text && isText(text) && text.textContent !== ref.value) text.textContent = ref.value;
6162 }
6263}
6363-// Iterates over the child nodes of the guide element, morphing the main element’s child nodes to match.
6464-function morphChildNodes(elem, guide, idMap) {
6464+// Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match.
6565+function morphChildNodes(elem, ref, idMap) {
6566 const childNodes = [...elem.childNodes];
6666- const guideChildNodes = [...guide.childNodes];
6767- for (let i = 0; i < guideChildNodes.length; i++) {
6767+ const refChildNodes = [...ref.childNodes];
6868+ for (let i = 0; i < refChildNodes.length; i++) {
6869 const child = childNodes.at(i);
6969- const guideChild = guideChildNodes.at(i);
7070- if (child && guideChild) morphChildNode(child, guideChild, elem, idMap);
7171- else if (guideChild) elem.appendChild(guideChild.cloneNode(true));
7070+ const refChild = refChildNodes.at(i);
7171+ if (child && refChild) morphChildNode(child, refChild, elem, idMap);
7272+ else if (refChild) elem.appendChild(refChild.cloneNode(true));
7273 else if (child) child.remove();
7374 }
7475 // Remove any excess child nodes from the main element. This is separate because
7576 // the loop above might modify the length of the main element’s child nodes.
7676- while (elem.childNodes.length > guide.childNodes.length) elem.lastChild?.remove();
7777+ while (elem.childNodes.length > ref.childNodes.length) elem.lastChild?.remove();
7778}
7878-function morphChildNode(child, guide, parent, idMap) {
7979- if (isElement(child) && isElement(guide)) morphChildElement(child, guide, parent, idMap);
8080- else morphNodes(child, guide, idMap);
7979+function morphChildNode(child, ref, parent, idMap) {
8080+ if (isElement(child) && isElement(ref)) morphChildElement(child, ref, parent, idMap);
8181+ else morphNodes(child, ref, idMap);
8182}
8282-function morphChildElement(child, guide, parent, idMap) {
8383- const guideIdSet = idMap.get(guide);
8383+function morphChildElement(child, ref, parent, idMap) {
8484+ const refIdSet = idMap.get(ref);
8485 // Generate the array in advance of the loop
8585- const guideSetArray = guideIdSet ? [...guideIdSet] : [];
8686+ const refSetArray = refIdSet ? [...refIdSet] : [];
8687 let currentNode = child;
8788 let nextMatchByTagName = null;
8889 // Try find a match by idSet, while also looking out for the next best match by tagName.
8990 while (currentNode) {
9091 if (isElement(currentNode)) {
9191- if (currentNode.id === guide.id) {
9292+ if (currentNode.id === ref.id) {
9293 parent.insertBefore(currentNode, child);
9393- return morphNodes(currentNode, guide, idMap);
9494+ return morphNodes(currentNode, ref, idMap);
9495 } else if (currentNode.id !== "") {
9596 const currentIdSet = idMap.get(currentNode);
9696- if (currentIdSet && guideSetArray.some((it) => currentIdSet.has(it))) {
9797+ if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) {
9798 parent.insertBefore(currentNode, child);
9898- return morphNodes(currentNode, guide, idMap);
9999+ return morphNodes(currentNode, ref, idMap);
99100 }
100100- } else if (!nextMatchByTagName && currentNode.tagName === guide.tagName) {
101101+ } else if (!nextMatchByTagName && currentNode.tagName === ref.tagName) {
101102 nextMatchByTagName = currentNode;
102103 }
103104 }
···105106 }
106107 if (nextMatchByTagName) {
107108 parent.insertBefore(nextMatchByTagName, child);
108108- morphNodes(nextMatchByTagName, guide, idMap);
109109- } else child.replaceWith(guide.cloneNode(true));
109109+ morphNodes(nextMatchByTagName, ref, idMap);
110110+ } else child.replaceWith(ref.cloneNode(true));
110111}
111112// We cannot use `instanceof` when nodes might be from different documents,
112113// so we use type guards instead. This keeps TypeScript happy, while doing
+54-53
src/morphlex.ts
···11type IdSet = Set<string>;
22type IdMap = Map<Node, IdSet>;
3344-export function morph(node: ChildNode, guide: ChildNode): void {
44+export function morph(node: ChildNode, reference: ChildNode): void {
55 const idMap: IdMap = new Map();
6677- if (isParentNode(node) && isParentNode(guide)) {
77+ if (isParentNode(node) && isParentNode(reference)) {
88 populateIdSets(node, idMap);
99- populateIdSets(guide, idMap);
99+ populateIdSets(reference, idMap);
1010 }
11111212- morphNodes(node, guide, idMap);
1212+ morphNodes(node, reference, idMap);
1313}
14141515// For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements.
···3434}
35353636// This is where we actually morph the nodes. The `morph` function exists to set up the `idMap`.
3737-function morphNodes(node: ChildNode, guide: ChildNode, idMap: IdMap): void {
3838- if (isElement(node) && isElement(guide) && node.tagName === guide.tagName) {
3737+function morphNodes(node: ChildNode, ref: ChildNode, idMap: IdMap): void {
3838+ if (isElement(node) && isElement(ref) && node.tagName === ref.tagName) {
3939 // We need to check if the element is an input, option, or textarea here, because they have
4040 // special attributes not covered by the isEqualNode check.
4141- if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(guide)) return;
4141+ if (!isInput(node) && !isOption(node) && !isTextArea(node) && node.isEqualNode(ref)) return;
4242 else {
4343- if (node.hasAttributes() || guide.hasAttributes()) morphAttributes(node, guide);
4444- if (node.hasChildNodes() || guide.hasChildNodes()) morphChildNodes(node, guide, idMap);
4343+ if (node.hasAttributes() || ref.hasAttributes()) morphAttributes(node, ref);
4444+ if (node.hasChildNodes() || ref.hasChildNodes()) morphChildNodes(node, ref, idMap);
4545 }
4646 } else {
4747- if (node.isEqualNode(guide)) return;
4848- else if (isText(node) && isText(guide)) {
4949- if (node.textContent !== guide.textContent) node.textContent = guide.textContent;
5050- } else if (isComment(node) && isComment(guide)) {
5151- if (node.nodeValue !== guide.nodeValue) node.nodeValue = guide.nodeValue;
5252- } else node.replaceWith(guide.cloneNode(true));
4747+ if (node.isEqualNode(ref)) return;
4848+ else if (isText(node) && isText(ref)) {
4949+ if (node.textContent !== ref.textContent) node.textContent = ref.textContent;
5050+ } else if (isComment(node) && isComment(ref)) {
5151+ if (node.nodeValue !== ref.nodeValue) node.nodeValue = ref.nodeValue;
5252+ } else node.replaceWith(ref.cloneNode(true));
5353 }
5454}
55555656-function morphAttributes(elem: Element, guide: Element): void {
5757- // Remove any excess attributes from the element that aren’t present in the guide.
5858- for (const { name } of elem.attributes) guide.hasAttribute(name) || elem.removeAttribute(name);
5656+function morphAttributes(elm: Element, ref: Element): void {
5757+ // Remove any excess attributes from the element that aren’t present in the reference.
5858+ for (const { name } of elm.attributes) ref.hasAttribute(name) || elm.removeAttribute(name);
59596060- // Copy attributes from the guide to the element, if they don’t already match.
6161- for (const { name, value } of guide.attributes) elem.getAttribute(name) === value || elem.setAttribute(name, value);
6060+ // Copy attributes from the reference to the element, if they don’t already match.
6161+ for (const { name, value } of ref.attributes) elm.getAttribute(name) === value || elm.setAttribute(name, value);
62626363- elem.nodeValue;
6363+ elm.nodeValue;
64646565- // For certain types of elements, we need to do some extra work to ensure the element’s state matches the guide’s state.
6666- if (isInput(elem) && isInput(guide)) {
6767- if (elem.checked !== guide.checked) elem.checked = guide.checked;
6868- if (elem.disabled !== guide.disabled) elem.disabled = guide.disabled;
6969- if (elem.indeterminate !== guide.indeterminate) elem.indeterminate = guide.indeterminate;
7070- if (elem.type !== "file" && elem.value !== guide.value) elem.value = guide.value;
7171- } else if (isOption(elem) && isOption(guide) && elem.selected !== guide.selected) elem.selected = guide.selected;
7272- else if (isTextArea(elem) && isTextArea(guide)) {
7373- if (elem.value !== guide.value) elem.value = guide.value;
6565+ // For certain types of elements, we need to do some extra work to ensure
6666+ // the element’s state matches the reference elements’ state.
6767+ if (isInput(elm) && isInput(ref)) {
6868+ if (elm.checked !== ref.checked) elm.checked = ref.checked;
6969+ if (elm.disabled !== ref.disabled) elm.disabled = ref.disabled;
7070+ if (elm.indeterminate !== ref.indeterminate) elm.indeterminate = ref.indeterminate;
7171+ if (elm.type !== "file" && elm.value !== ref.value) elm.value = ref.value;
7272+ } else if (isOption(elm) && isOption(ref) && elm.selected !== ref.selected) elm.selected = ref.selected;
7373+ else if (isTextArea(elm) && isTextArea(ref)) {
7474+ if (elm.value !== ref.value) elm.value = ref.value;
74757575- const text = elem.firstChild;
7676- if (text && isText(text) && text.textContent !== guide.value) text.textContent = guide.value;
7676+ const text = elm.firstChild;
7777+ if (text && isText(text) && text.textContent !== ref.value) text.textContent = ref.value;
7778 }
7879}
79808080-// Iterates over the child nodes of the guide element, morphing the main element’s child nodes to match.
8181-function morphChildNodes(elem: Element, guide: Element, idMap: IdMap): void {
8181+// Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match.
8282+function morphChildNodes(elem: Element, ref: Element, idMap: IdMap): void {
8283 const childNodes = [...elem.childNodes];
8383- const guideChildNodes = [...guide.childNodes];
8484+ const refChildNodes = [...ref.childNodes];
84858585- for (let i = 0; i < guideChildNodes.length; i++) {
8686+ for (let i = 0; i < refChildNodes.length; i++) {
8687 const child = childNodes.at(i);
8787- const guideChild = guideChildNodes.at(i);
8888+ const refChild = refChildNodes.at(i);
88898989- if (child && guideChild) morphChildNode(child, guideChild, elem, idMap);
9090- else if (guideChild) elem.appendChild(guideChild.cloneNode(true));
9090+ if (child && refChild) morphChildNode(child, refChild, elem, idMap);
9191+ else if (refChild) elem.appendChild(refChild.cloneNode(true));
9192 else if (child) child.remove();
9293 }
93949495 // Remove any excess child nodes from the main element. This is separate because
9596 // the loop above might modify the length of the main element’s child nodes.
9696- while (elem.childNodes.length > guide.childNodes.length) elem.lastChild?.remove();
9797+ while (elem.childNodes.length > ref.childNodes.length) elem.lastChild?.remove();
9798}
98999999-function morphChildNode(child: ChildNode, guide: ChildNode, parent: Element, idMap: IdMap): void {
100100- if (isElement(child) && isElement(guide)) morphChildElement(child, guide, parent, idMap);
101101- else morphNodes(child, guide, idMap);
100100+function morphChildNode(child: ChildNode, ref: ChildNode, parent: Element, idMap: IdMap): void {
101101+ if (isElement(child) && isElement(ref)) morphChildElement(child, ref, parent, idMap);
102102+ else morphNodes(child, ref, idMap);
102103}
103104104104-function morphChildElement(child: Element, guide: Element, parent: Element, idMap: IdMap): void {
105105- const guideIdSet = idMap.get(guide);
105105+function morphChildElement(child: Element, ref: Element, parent: Element, idMap: IdMap): void {
106106+ const refIdSet = idMap.get(ref);
106107107108 // Generate the array in advance of the loop
108108- const guideSetArray = guideIdSet ? [...guideIdSet] : [];
109109+ const refSetArray = refIdSet ? [...refIdSet] : [];
109110110111 let currentNode: ChildNode | null = child;
111112 let nextMatchByTagName: ChildNode | null = null;
···113114 // Try find a match by idSet, while also looking out for the next best match by tagName.
114115 while (currentNode) {
115116 if (isElement(currentNode)) {
116116- if (currentNode.id === guide.id) {
117117+ if (currentNode.id === ref.id) {
117118 parent.insertBefore(currentNode, child);
118118- return morphNodes(currentNode, guide, idMap);
119119+ return morphNodes(currentNode, ref, idMap);
119120 } else if (currentNode.id !== "") {
120121 const currentIdSet = idMap.get(currentNode);
121122122122- if (currentIdSet && guideSetArray.some((it) => currentIdSet.has(it))) {
123123+ if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) {
123124 parent.insertBefore(currentNode, child);
124124- return morphNodes(currentNode, guide, idMap);
125125+ return morphNodes(currentNode, ref, idMap);
125126 }
126126- } else if (!nextMatchByTagName && currentNode.tagName === guide.tagName) {
127127+ } else if (!nextMatchByTagName && currentNode.tagName === ref.tagName) {
127128 nextMatchByTagName = currentNode;
128129 }
129130 }
···133134134135 if (nextMatchByTagName) {
135136 parent.insertBefore(nextMatchByTagName, child);
136136- morphNodes(nextMatchByTagName, guide, idMap);
137137- } else child.replaceWith(guide.cloneNode(true));
137137+ morphNodes(nextMatchByTagName, ref, idMap);
138138+ } else child.replaceWith(ref.cloneNode(true));
138139}
139140140141// We cannot use `instanceof` when nodes might be from different documents,