diff --git a/src/brsTypes/components/BrsObjects.ts b/src/brsTypes/components/BrsObjects.ts index 495f5d288..16077a3f3 100644 --- a/src/brsTypes/components/BrsObjects.ts +++ b/src/brsTypes/components/BrsObjects.ts @@ -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"; @@ -32,6 +33,7 @@ export const BrsObjects = new Map([ "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)], diff --git a/src/brsTypes/components/RoXMLElement.ts b/src/brsTypes/components/RoXMLElement.ts new file mode 100644 index 000000000..a7e8f473a --- /dev/null +++ b/src/brsTypes/components/RoXMLElement.ts @@ -0,0 +1,90 @@ +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?: XmlElement; + + constructor() { + super("roXMLElement"); + + this.registerMethods({ + ifXMLElement: [this.parse, this.getName, this.getNamedElementsCi, this.getAttributes], + }); + } + + toString(parent?: BrsType) { + return ""; + } + + 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); + return BrsBoolean.True; + } catch (err) { + this.xmlNode = undefined; + return BrsBoolean.False; + } + }, + }); + + private getName = new Callable("getName", { + signature: { + args: [], + returns: ValueKind.String, + }, + impl: (interpreter: Interpreter) => { + 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.type === "element" && item.name?.toLowerCase() === neededKey) { + let childXmlElement = new RoXMLElement(); + childXmlElement.xmlNode = item; + arr.push(childXmlElement); + } + } + return new RoArray(arr); + }, + }); + + private getAttributes = new Callable("getAttributes", { + signature: { + args: [], + returns: ValueKind.Object, + }, + impl: (interpreter: Interpreter) => { + let attrs = this.xmlNode?.attr ?? {}; + return new RoAssociativeArray( + Object.entries(attrs).map(([name, value]) => ({ + name: new BrsString(name), + value: new BrsString(value), + })) + ); + }, + }); +} diff --git a/src/brsTypes/index.ts b/src/brsTypes/index.ts index 5bdcf560e..948ce056f 100644 --- a/src/brsTypes/index.ts +++ b/src/brsTypes/index.ts @@ -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"; diff --git a/test/brsTypes/components/RoXMLElement.test.js b/test/brsTypes/components/RoXMLElement.test.js new file mode 100644 index 000000000..1286aadcd --- /dev/null +++ b/test/brsTypes/components/RoXMLElement.test.js @@ -0,0 +1,100 @@ +const brs = require("brs"); +const { BrsString, RoXMLElement } = brs.types; +const { Interpreter } = require("../../../lib/interpreter"); + +describe("RoXMLElement", () => { + let xmlParser; + let interpreter; + + beforeEach(() => { + xmlParser = new RoXMLElement(); + interpreter = new Interpreter(); + }); + + describe("test methods for object with successful parsed xml", () => { + beforeEach(() => { + 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.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).toEqual( + new Map([ + ["id", new BrsString("someId")], + ["attr1", new BrsString("0")], + ]) + ); + }); + }); + + describe.each([ + ["test methods for object with no parsed xml", () => {}], + [ + "test methods for object with failed parsing of xml", + () => { + let parse = xmlParser.getMethod("parse"); + parse.call(interpreter, new BrsString('>bad_tag id="12" < some text >/bad_tag<')); + }, + ], + ])("%s", (name, tryParse) => { + beforeEach(() => { + tryParse(); + }); + + it("getName", () => { + let getName = xmlParser.getMethod("getName"); + expect(getName).toBeTruthy(); + expect(getName.call(interpreter)).toEqual(new BrsString("")); + }); + + it("getNamedElementsCi", () => { + let getNamedElementsCi = xmlParser.getMethod("getNamedElementsCi"); + expect(getNamedElementsCi).toBeTruthy(); + expect(getNamedElementsCi.call(interpreter, new BrsString("any")).elements).toEqual([]); + }); + + it("getAttributes", () => { + let getAttributes = xmlParser.getMethod("getAttributes"); + expect(getAttributes).toBeTruthy(); + expect(getAttributes.call(interpreter).elements).toEqual(new Map()); + }); + }); + + describe("test parse method with different xml strings", () => { + test.each([ + ["some text", true], + ["some text child's text ", true], + [getXmlString(), true], + ['>bad_tag id="12" < some text >/bad_tag<', false], + ["", false], + ])("test parse with string %s", (xmlString, expected) => { + let parse = xmlParser.getMethod("parse"); + expect(parse.call(interpreter, new BrsString(xmlString)).value).toBe(expected); + }); + }); +}); + +function getXmlString() { + return ' '; +} diff --git a/test/e2e/BrsComponents.test.js b/test/e2e/BrsComponents.test.js index 160df83bd..3ec8fb4b7 100644 --- a/test/e2e/BrsComponents.test.js +++ b/test/e2e/BrsComponents.test.js @@ -188,6 +188,35 @@ describe("end to end brightscript functions", () => { ]); }); + test("components/roXMLElement.brs", () => { + return execute([resourceFile("components", "roXMLElement.brs")], outputStreams).then(() => { + expect(allArgs(outputStreams.stdout.write).filter((arg) => arg !== "\n")).toEqual([ + "xmlParser = ", + "", + "type(xmlParser) = ", + "roXMLElement", + "parse bad xml string, result = ", + "false", + "parse good xml string, result = ", + "true", + "getName() = ", + "tag1", + "getAttributes() = ", + ` =\n` + + `{\n` + + ` id: someId\n` + + ` attr1: 0\n` + + `}`, + 'getNamedElementsCi("child1") count = ', + "2", + "name of first child = ", + "Child1", + "mame of second child = ", + "CHILD1", + ]); + }); + }); + test("components/customComponent.brs", async () => { await execute([resourceFile("components", "customComponent.brs")], outputStreams); diff --git a/test/e2e/resources/components/roXMLElement.brs b/test/e2e/resources/components/roXMLElement.brs new file mode 100644 index 000000000..92ed9b3f2 --- /dev/null +++ b/test/e2e/resources/components/roXMLElement.brs @@ -0,0 +1,13 @@ +sub main() + xmlParser = createObject("roXMLElement") + ?"xmlParser = "xmlParser + ?"type(xmlParser) = "type(xmlParser) + ?"parse bad xml string, result = "xmlParser.parse("some_xml_doc") + ?"parse good xml string, result = "xmlParser.parse(" ") + ?"getName() = " xmlParser.getName() + ?"getAttributes() = " xmlParser.getAttributes() + children = xmlParser.getNamedElementsCi("child1") + ?"getNamedElementsCi(""child1"") count = " children.count() + ?"name of first child = "children[0].getName() + ?"mame of second child = "children[1].getName() +end sub \ No newline at end of file