Skip to content

Commit 7fe595f

Browse files
Merge pull request #76 from Mochitto/feat/node-transformations
Added `Node.transformChildren`, `Node.replaceWith`
2 parents 8b07c9f + 776bb4c commit 7fe595f

File tree

3 files changed

+121
-0
lines changed

3 files changed

+121
-0
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
All notable changes to this project from version 1.2.0 upwards are documented in this file.
33
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
44

5+
## [Unreleased]
6+
7+
### Added
8+
- `Node.transformChildren` to run a function on all the children of a Node
9+
- `Node.replaceWith` to replace a Node with another
10+
511
## [1.6.29] – 2024-07-09
612

713
### Fixed

src/model/model.ts

+51
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,57 @@ export abstract class Node extends Origin implements Destination {
340340
return this;
341341
}
342342

343+
/**
344+
* Replace the current Node with another Node
345+
*
346+
* @throws Error - if this.parent === undefined
347+
*/
348+
replaceWith(other: Node): void {
349+
if (!this.parent) {
350+
throw new Error("Cannot replace a Node that has no parent")
351+
}
352+
353+
this.parent.transformChildren(node => node === this ? other : node);
354+
}
355+
356+
/**
357+
* Apply the given `operation` function to
358+
* all of the Node's children.
359+
*
360+
* It's possible to use both pure functions,
361+
* to replace the nodes, and functions that mutate
362+
* the nodes in-place.
363+
*/
364+
transformChildren(operation: (node: Node) => Node): void {
365+
const nodesNames = this.getChildNames();
366+
367+
nodesNames.forEach(nodeName => {
368+
const propertyValue = this[nodeName];
369+
if (propertyValue instanceof Node) {
370+
const newNode = operation(propertyValue);
371+
/*
372+
* Identity check;
373+
* If the variable is pointing to the same object,
374+
* the operation changed the Node in-place.
375+
*/
376+
if (newNode !== propertyValue) {
377+
this.setChild(nodeName, newNode);
378+
}
379+
380+
} else if (Array.isArray(propertyValue)) {
381+
propertyValue.forEach((element, index) => {
382+
if (element instanceof Node) {
383+
const newNode = operation(element);
384+
// Another identity check
385+
if (newNode !== element) {
386+
(propertyValue as Node[])[index] = newNode;
387+
}
388+
}
389+
});
390+
}
391+
});
392+
}
393+
343394
withOrigin(origin?: Origin): this {
344395
this.origin = origin;
345396
return this;

tests/nodes/nodes.test.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { expect } from "chai";
2+
import { Box, SomeNode, SomeNodeInPackage } from "../nodes";
3+
4+
describe('Node.transformChildren', () => {
5+
let childNode1: SomeNode;
6+
let childNode2: SomeNode;
7+
let boxNode: Box;
8+
9+
beforeEach(() => {
10+
childNode1 = new SomeNode("Child1");
11+
childNode2 = new SomeNode("Child2");
12+
13+
boxNode = new Box("BoxNode", [childNode1, childNode2]);
14+
})
15+
16+
it('should apply in-place transformation to each child node', () => {
17+
const inPlaceTransformation = (node: SomeNode): SomeNode => {
18+
node.a = node.a?.toUpperCase();
19+
return node;
20+
};
21+
22+
boxNode.transformChildren(inPlaceTransformation);
23+
24+
expect(boxNode.contents[0]["a"]).to.eq("CHILD1");
25+
expect(boxNode.contents[0]).to.eq(childNode1);
26+
expect(boxNode.contents[1]["a"]).to.eq("CHILD2");
27+
expect(boxNode.contents[1]).to.eq(childNode2);
28+
});
29+
30+
it('should replace children nodes when used with pure-functions', () => {
31+
const replaceTransformation = (node: SomeNode): SomeNode => {
32+
return new SomeNode(node.a?.toUpperCase())
33+
};
34+
35+
boxNode.transformChildren(replaceTransformation);
36+
37+
expect(boxNode.contents[0]["a"]).to.eq("CHILD1");
38+
expect(boxNode.contents[0]).to.not.eq(childNode1);
39+
expect(boxNode.contents[1]["a"]).to.eq("CHILD2");
40+
expect(boxNode.contents[1]).to.not.eq(childNode2);
41+
});
42+
});
43+
44+
describe('Node.replaceWith', () => {
45+
it('should replace a child node with another node', () => {
46+
const childNode1 = new SomeNode("Child1");
47+
const childNode2 = new SomeNode("Child2");
48+
49+
const parentNode = new SomeNodeInPackage("ParentNode");
50+
51+
parentNode.setChild('someNode', childNode1);
52+
expect(parentNode.getChildren('someNode')).to.eql([childNode1]);
53+
childNode1.replaceWith(childNode2);
54+
expect(parentNode.getChildren('someNode')).to.eql([childNode2]);
55+
});
56+
57+
it('should throw error if parent is not set', () => {
58+
const childNode1 = new SomeNode("Child1");
59+
60+
const nodeWithoutParent = new SomeNode("NodeWithoutParent");
61+
expect(() => nodeWithoutParent.replaceWith(childNode1)).to.throw('Cannot replace a Node that has no parent');
62+
});
63+
});
64+

0 commit comments

Comments
 (0)