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

Implement clone #672

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
64 changes: 64 additions & 0 deletions src/brsTypes/components/RoSGNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
this.callfunc,
this.issubtype,
this.parentsubtype,
this.clone,
],
ifSGNodeBoundingRect: [this.boundingRect],
});
Expand Down Expand Up @@ -1695,6 +1696,17 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
},
});

/* Returns a copy of the entire node tree or just a shallow copy. */
private clone = new Callable("clone", {
anyatokar marked this conversation as resolved.
Show resolved Hide resolved
signature: {
args: [new StdlibArgument("isDeepCopy", ValueKind.Boolean)],
returns: ValueKind.Object,
},
impl: (interpreter: Interpreter, isDeepCopy: BrsBoolean) => {
return cloneNode(interpreter, this, isDeepCopy.toBoolean());
},
});

/* Returns the subtype of this node as specified when it was created */
private subtype = new Callable("subtype", {
signature: {
Expand Down Expand Up @@ -1914,3 +1926,55 @@ function addChildren(
}
});
}

function cloneNode(
interpreter: Interpreter,
toBeCloned: RoSGNode,
isDeepCopy: Boolean
): RoSGNode | BrsInvalid {
let copy = createNodeByType(interpreter, new BrsString(toBeCloned.nodeSubtype));

if (copy instanceof RoSGNode) {
let originalFields = toBeCloned.getFields();
let copiedFields = new RoAssociativeArray([]);
let addReplace = copiedFields.getMethod("addreplace");

// Copy the fields.
for (let [key, value] of originalFields) {
if (addReplace) {
addReplace.call(
interpreter,
new BrsString(key),
getBrsValueFromFieldType(value.getType(), value.toString())
);
}
}

let update = copy.getMethod("update");

if (update) {
update.call(interpreter, copiedFields, BrsBoolean.True);
}

// A deep clone also copies children.
if (isDeepCopy) {
let appendChild = copy.getMethod("appendchild");
anyatokar marked this conversation as resolved.
Show resolved Hide resolved
let getChildren = toBeCloned.getMethod("getchildren");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code would be a little cleaner if we exposed getChildren in typescript. I wonder if that's worth it. Then the callable would just wrap that response in an RoArray.


if (getChildren && appendChild) {
let children = getChildren.call(interpreter, new Int32(-1), new Int32(0));

if (children instanceof RoArray) {
for (let child of children.getElements()) {
if (child instanceof RoSGNode) {
let childCopy = cloneNode(interpreter, child, isDeepCopy);
appendChild.call(interpreter, childCopy);
}
}
}
}
}
}

return copy;
}
103 changes: 103 additions & 0 deletions test/brsTypes/components/RoSGNode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2514,6 +2514,109 @@ describe("RoSGNode", () => {
expect(result).toBe(BrsInvalid.Instance);
});
});

describe("clone", () => {
let interpreter, originalNode;

beforeEach(() => {
interpreter = new Interpreter();
originalNode = new MarkupGrid([
{ name: new BrsString("field1"), value: new BrsString("a string") },
{ name: new BrsString("field2"), value: new Int32(-19999) },
{ name: new BrsString("field3"), value: BrsBoolean.False },
{ name: new BrsString("<33"), value: new BrsString("") },
]);

child1 = new RoSGNode([
{ name: new BrsString("child1name"), value: new BrsString("1") },
]);
child2 = new RoSGNode([
{ name: new BrsString("child2name"), value: new Int32(2) },
]);
child3 = new RoSGNode([
{ name: new BrsString("child3name"), value: BrsBoolean.False },
]);
grandchild1 = new RoSGNode([
{ name: new BrsString("grandchild1name"), value: new Int32(1.2) },
]);
grandchild2 = new RoSGNode([
{
name: new BrsString("grandchild2name"),
value: new BrsString("second grandchild"),
},
]);

let appendGrandchildren = child1.getMethod("appendchildren");
appendGrandchildren.call(interpreter, new RoArray([grandchild1, grandchild2]));

let appendChildren = originalNode.getMethod("appendchildren");
appendChildren.call(interpreter, new RoArray([child1, child2, child3]));
});

it("return type is same as of subject", () => {
let cloneMethod = originalNode.getMethod("clone");
let copyNode = cloneMethod.call(interpreter, new BrsBoolean(true));

expect(copyNode).toBeInstanceOf(RoSGNode);
expect(copyNode).toBeInstanceOf(MarkupGrid);
});

it("copies field values", () => {
let cloneMethod = originalNode.getMethod("clone");
let copyNode = cloneMethod.call(interpreter, new BrsBoolean(true));

expect(copyNode.get(new BrsString("field1"))).toEqual(
new BrsString("a string")
);
expect(copyNode.get(new BrsString("field2"))).toEqual(new Int32(-19999));
expect(copyNode.get(new BrsString("field3"))).toEqual(BrsBoolean.False);
expect(copyNode.get(new BrsString("<33"))).toEqual(new BrsString(""));

let isSameNode = originalNode.getMethod("isSameNode");
expect(isSameNode.call(interpreter, copyNode)).toEqual(BrsBoolean.False);
});

it("shallow copy doesn't copy children", () => {
let cloneMethod = originalNode.getMethod("clone");
let copyNode = cloneMethod.call(interpreter, new BrsBoolean(false));

let getChildCount = copyNode.getMethod("getchildcount");
let childCount = getChildCount.call(interpreter);

expect(childCount).toEqual(new Int32(0));
});

it("deep clone copies children and grandchildren", () => {
let cloneMethod = originalNode.getMethod("clone");
let copyNode = cloneMethod.call(interpreter, new BrsBoolean(true));

let getChildCount = copyNode.getMethod("getchildcount");
let childCount = getChildCount.call(interpreter);

expect(childCount).toEqual(new Int32(3));

getChild = copyNode.getMethod("getchild");
let child1copy = getChild.call(interpreter, new Int32(0));

expect(child1copy.get(new BrsString("child1name"))).toEqual(new BrsString("1"));

let getGrandchildCount = child1copy.getMethod("getchildcount");
let grandchildCount = getGrandchildCount.call(interpreter);

expect(grandchildCount).toEqual(new Int32(2));

getChild = child1copy.getMethod("getchild");
let grandchild1copy = getChild.call(interpreter, new Int32(0));
let grandchild2copy = getChild.call(interpreter, new Int32(1));

expect(grandchild1copy.get(new BrsString("grandchild1name"))).toEqual(
new Int32(1.2)
);
expect(grandchild2copy.get(new BrsString("grandchild2name"))).toEqual(
new BrsString("second grandchild")
);
});
});
});
});

Expand Down
2 changes: 2 additions & 0 deletions test/e2e/RoSGNode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ describe("components/roSGNode", () => {
"invalid",
"33",
"37",
"updatedId",
"newValue",
"0",
"0",
"0",
Expand Down
10 changes: 10 additions & 0 deletions test/e2e/resources/components/roSGNode/roSGNode.brs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ sub init()
node.ClipStart = 37
?node.ClipStart

' it clones nodes
node = createObject("roSGNode", "ContentNode")
node.update({
id: "updatedId",
newField: "newValue"
}, true)
clonedNode = node.clone(true)
print clonedNode.id
print clonedNode.newField

' ifSGNodeBoundingRect
rect = node.boundingRect()
print rect.x ' => 0
Expand Down