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

Adding support for RoXMLElement #599

Merged
merged 11 commits into from
Jan 19, 2021
Merged
2 changes: 2 additions & 0 deletions src/brsTypes/components/BrsObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RoDateTime } from "./RoDateTime";
import { Timespan } from "./Timespan";
import { createNodeByType } from "./RoSGNode";
import { RoRegex } from "./RoRegex";
import { RoXMLElement } from "./RoXMLElement";
import { BrsString, BrsBoolean } from "../BrsType";
import { RoString } from "./RoString";
import { roBoolean } from "./RoBoolean";
Expand Down Expand Up @@ -32,6 +33,7 @@ export const BrsObjects = new Map<string, Function>([
"roregex",
(_: Interpreter, expression: BrsString, flags: BrsString) => new RoRegex(expression, flags),
],
["roxmlelement", (_: Interpreter) => new RoXMLElement()],
["rostring", (_: Interpreter) => new RoString()],
["roboolean", (_: Interpreter, literal: BrsBoolean) => new roBoolean(literal)],
["rodouble", (_: Interpreter, literal: Double) => new roDouble(literal)],
Expand Down
98 changes: 98 additions & 0 deletions src/brsTypes/components/RoXMLElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { RoAssociativeArray } from "./RoAssociativeArray";
import { RoArray } from "./RoArray";
import { BrsComponent } from "./BrsComponent";
import { BrsBoolean, BrsString, BrsValue, ValueKind } from "../BrsType";
import { BrsType } from "..";
import { Callable, StdlibArgument } from "../Callable";
import { Interpreter } from "../../interpreter";
import { XmlDocument, XmlElement } from "xmldoc";

export class RoXMLElement extends BrsComponent implements BrsValue {
readonly kind = ValueKind.Object;
private xmlNode?: any;

constructor() {
super("roXMLElement");

this.registerMethods({
ifXMLEelement: [this.parse, this.getName, this.getNamedElementsCi, this.getAttributes],
Copy link
Collaborator

Choose a reason for hiding this comment

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

there is a typo in the interface name ifXMLElement

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

});
}

public setXMLElem(xmlElem: XmlElement) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this could be private as it doesn't seem to be used outside this class

Copy link
Owner

Choose a reason for hiding this comment

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

Once it's private, this function doesn't need to exist at all! A simple this.xmlNode = xmlElem would suffice, since we don't seem to be doing anything special here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ohh, forgot that RoXMLElement are friend (from C++, where other class/methods can access private members) for other RoXMLElement objects

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

this.xmlNode = xmlElem;
return this;
}

toString(parent?: BrsType) {
return "<Component: roXMLElement>";
}

equalTo(other: BrsType) {
return BrsBoolean.False;
}

private parse = new Callable("parse", {
signature: {
args: [new StdlibArgument("str", ValueKind.String)],
returns: ValueKind.Boolean,
},
impl: (interpreter: Interpreter, str: BrsString) => {
try {
this.xmlNode = new XmlDocument(str.value);
Copy link
Collaborator

Choose a reason for hiding this comment

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

does XmlDocument return anything to indicate that the string was parsed correctly? something like a boolean value? or does it throws an exception in every case?

Copy link
Contributor

Choose a reason for hiding this comment

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

It throws an exception only if the XML can not be parsed at all, but it doesn't if it can parse at least part of XML and it returns true in that case. The similar behavior we have on Roku.

return BrsBoolean.from(true);
} catch (err) {
this.xmlNode = undefined;
return BrsBoolean.from(false);
}
},
});

private getName = new Callable("getName", {
signature: {
args: [],
returns: ValueKind.String,
},
impl: (interpreter: Interpreter, str: BrsString) => {
return new BrsString(this.xmlNode?.name ?? "");
},
});

private getNamedElementsCi = new Callable("getNamedElementsCi", {
signature: {
args: [new StdlibArgument("str", ValueKind.String)],
returns: ValueKind.Object,
},
impl: (interpreter: Interpreter, str: BrsString) => {
let neededKey = str.value.toLowerCase();
let arr = [];
for (let item of this.xmlNode?.children ?? []) {
if (item.name?.toLowerCase() === neededKey) {
arr.push(new RoXMLElement().setXMLElem(item));
}
}
return new RoArray(arr);
},
});

private getAttributes = new Callable("getAttributes", {
signature: {
args: [],
returns: ValueKind.Object,
},
impl: (interpreter: Interpreter, str: BrsString) => {
return this.convertObjectToBrsAA(this.xmlNode?.attr ?? {});
},
});

private convertObjectToBrsAA(object_: any) {
let array = [];
for (let key in object_) {
array.push({
name: new BrsString(key),
value: new BrsString(object_[key]),
});
}
return new RoAssociativeArray(array);
}
}
1 change: 1 addition & 0 deletions src/brsTypes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export * from "./components/RoAssociativeArray";
export * from "./components/Timespan";
export * from "./components/BrsObjects";
export * from "./components/RoRegex";
export * from "./components/RoXMLElement";
export * from "./components/RoString";
export * from "./components/RoBoolean";
export * from "./components/RoDouble";
Expand Down
93 changes: 93 additions & 0 deletions test/brsTypes/components/RoXMLElement.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const brs = require("brs");
const { BrsString, RoXMLElement } = brs.types;
const { Interpreter } = require("../../../lib/interpreter");

describe("RoXMLElement", () => {
let xmlParser;
let interpreter;

describe("test methods for object with successful parsing", () => {
beforeEach(() => {
xmlParser = new RoXMLElement();
interpreter = new Interpreter();
let parse = xmlParser.getMethod("parse");
parse.call(interpreter, new BrsString(getXmlString()));
});

it("getName", () => {
let getName = xmlParser.getMethod("getName");
expect(getName.call(interpreter)).toEqual(new BrsString("tag1"));
});

it("getNamedElementsCi", () => {
let getNamedElementsCi = xmlParser.getMethod("getNamedElementsCi");
expect(getNamedElementsCi.call(interpreter, new BrsString("any")).elements).toEqual([]);

let children = getNamedElementsCi.call(interpreter, new BrsString("CHiLd1"));
expect(children.elements).not.toEqual([]);
expect(children.elements.length).toEqual(2);

let getName = children.elements[0].getMethod("getName");
expect(getName.call(interpreter)).toEqual(new BrsString("Child1"));

getName = children.elements[1].getMethod("getName");
expect(getName.call(interpreter)).toEqual(new BrsString("CHILD1"));
});

it("getAttributes", () => {
let getAttributes = xmlParser.getMethod("getAttributes");
expect(getAttributes.call(interpreter).elements).not.toEqual(new Map());
expect(getAttributes.call(interpreter).elements).not.toEqual(
new Map([
["id", "someId"],
["someAtr", "0"],
])
);
});
});

describe.each([
["test methods for default object", () => {}],
[
"test methods for object with failed parsing",
() => {
let parse = xmlParser.getMethod("parse");
parse.call(interpreter, new BrsString('>bad_tag id="12" < some text >/bad_tag<'));
},
],
])("%s", (name, tryParse) => {
xmlParser = new RoXMLElement();
interpreter = new Interpreter();
tryParse();

let getName = xmlParser.getMethod("getName");
expect(getName).toBeTruthy();
expect(getName.call(interpreter)).toEqual(new BrsString(""));

let getNamedElementsCi = xmlParser.getMethod("getNamedElementsCi");
expect(getNamedElementsCi).toBeTruthy();
expect(getNamedElementsCi.call(interpreter, new BrsString("any")).elements).toEqual([]);

let getAttributes = xmlParser.getMethod("getAttributes");
expect(getAttributes).toBeTruthy();
expect(getAttributes.call(interpreter).elements).toEqual(new Map());
});

describe.each([
["<tag>some text<tag>", true],
["<tag>some text <child1> child's text </child1> </tag>", true],
[getXmlString(), true],
['>bad_tag id="12" < some text >/bad_tag<', false],
["", false],
])("test parse with string %s", (xmlString, expected) => {
xmlParser = new RoXMLElement();
interpreter = new Interpreter();

let parse = xmlParser.getMethod("parse");
expect(parse.call(interpreter, new BrsString(xmlString)).value).toBe(expected);
});
});

function getXmlString() {
return '<tag1 id="someId" someAtr="0"> <Child1 id="id1"></Child1> <CHILD1 id="id2"></CHILD1> </tag1>';
}