From 66a2e69683accbf818e7ae6675bc88608030326b Mon Sep 17 00:00:00 2001 From: chrisradek Date: Thu, 11 May 2017 16:24:06 -0700 Subject: [PATCH 1/5] Initial commit --- packages/xml-builder/package.json | 15 +++++++++++++++ packages/xml-builder/tsconfig.json | 13 +++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 packages/xml-builder/package.json create mode 100644 packages/xml-builder/tsconfig.json diff --git a/packages/xml-builder/package.json b/packages/xml-builder/package.json new file mode 100644 index 0000000000000..09100f8bacb25 --- /dev/null +++ b/packages/xml-builder/package.json @@ -0,0 +1,15 @@ +{ + "name": "@aws/xml-builder", + "private": true, + "version": "0.0.1", + "description": "XML builder for the AWS SDK", + "devDependencies": { + "typescript": "^2.3" + }, + "scripts": { + "prepublishOnly": "tsc", + "test": "tsc" + }, + "author": "aws-javascript-sdk-team@amazon.com", + "license": "UNLICENSED" +} diff --git a/packages/xml-builder/tsconfig.json b/packages/xml-builder/tsconfig.json new file mode 100644 index 0000000000000..f79d56077be60 --- /dev/null +++ b/packages/xml-builder/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "alwaysStrict": true, + "strictNullChecks": true, + "noImplicitAny": true, + "sourceMap": true, + "noImplicitThis": true + }, + "exclude": ["node_modules"] +} From 8f6f0d5b5223a125c219846468d7f3fdbfe9332f Mon Sep 17 00:00:00 2001 From: chrisradek Date: Tue, 23 May 2017 08:15:33 -0700 Subject: [PATCH 2/5] Adds xml-builder package --- packages/xml-builder/.gitignore | 3 + packages/xml-builder/__tests__/index.ts | 11 +++ packages/xml-builder/__tests__/lib/XmlNode.ts | 81 +++++++++++++++++++ packages/xml-builder/__tests__/lib/XmlText.ts | 8 ++ packages/xml-builder/index.ts | 2 + packages/xml-builder/lib/XmlNode.ts | 55 +++++++++++++ packages/xml-builder/lib/XmlText.ts | 13 +++ packages/xml-builder/package.json | 9 ++- packages/xml-builder/tsconfig.json | 6 +- 9 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 packages/xml-builder/.gitignore create mode 100644 packages/xml-builder/__tests__/index.ts create mode 100644 packages/xml-builder/__tests__/lib/XmlNode.ts create mode 100644 packages/xml-builder/__tests__/lib/XmlText.ts create mode 100644 packages/xml-builder/index.ts create mode 100644 packages/xml-builder/lib/XmlNode.ts create mode 100644 packages/xml-builder/lib/XmlText.ts diff --git a/packages/xml-builder/.gitignore b/packages/xml-builder/.gitignore new file mode 100644 index 0000000000000..d1a786583bc8c --- /dev/null +++ b/packages/xml-builder/.gitignore @@ -0,0 +1,3 @@ +**/*.js +**/*.js.map +**/*.d.ts \ No newline at end of file diff --git a/packages/xml-builder/__tests__/index.ts b/packages/xml-builder/__tests__/index.ts new file mode 100644 index 0000000000000..37b0a0e8d875c --- /dev/null +++ b/packages/xml-builder/__tests__/index.ts @@ -0,0 +1,11 @@ +import * as pkg from '../'; + +describe('package index', () => { + it('should define XmlNode', () => { + expect(pkg.XmlNode).toBeDefined(); + }); + + it('should define XmlText', () => { + expect(pkg.XmlText).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/packages/xml-builder/__tests__/lib/XmlNode.ts b/packages/xml-builder/__tests__/lib/XmlNode.ts new file mode 100644 index 0000000000000..d61abe49f4285 --- /dev/null +++ b/packages/xml-builder/__tests__/lib/XmlNode.ts @@ -0,0 +1,81 @@ +import {XmlNode} from '../../lib/XmlNode'; +import {XmlText} from '../../lib/XmlText'; + +describe('XmlNode', () => { + it('creates empty xml documents', () => { + const node = new XmlNode('Xml'); + expect(node.toString()).toBe(''); + }); + + it('nests elements', () => { + const node = new XmlNode('xml', [ + new XmlNode('element') + ]); + expect(node.toString()).toBe(''); + }); + + it('nests elements deeply', () => { + const node = new XmlNode('xml', [ + new XmlNode('a', [ + new XmlNode('b', [ + new XmlNode('c') + ]) + ]) + ]); + expect(node.toString()).toBe(''); + }); + + it('supports flat elements with nested elements', () => { + const node = new XmlNode('xml', [ + new XmlNode('a', [ + new XmlNode('b') + ]), + new XmlNode('c') + ]); + expect(node.toString()).toBe(''); + }); + + it('accepts element values', () => { + const node = new XmlNode('xml', [ + new XmlNode('element', [ + new XmlText('value') + ]) + ]); + expect(node.toString()).toBe('value'); + }); + + it('accepts element attributes', () => { + const node = new XmlNode('xml', [ + new XmlNode('el') + .addAttribute('abc', 123) + .addAttribute('mno', 'xyz') + ]); + expect(node.toString()).toBe(''); + }); + + it('accepts element values and attributes at the same time', () => { + const node = new XmlNode('xml', [ + new XmlNode('el', [ + new XmlText('value') + ]).addAttribute('abc', 'xyz') + ]); + expect(node.toString()).toBe('value'); + }); + + it('accepts attributes on outer elements', () => { + const node = new XmlNode('xml', [ + new XmlNode('out', [ + new XmlNode('c') + ]).addAttribute('a', 'b') + ]).addAttribute('xmlns', 'abc'); + expect(node.toString()).toBe(''); + }); + + it('escapes attribute values and element text', () => { + const node = new XmlNode('xml', [ + new XmlNode('this & that') + ]).addAttribute('xmlns', 'a"b'); + expect(node.toString()).toBe(''); + }); + +}); \ No newline at end of file diff --git a/packages/xml-builder/__tests__/lib/XmlText.ts b/packages/xml-builder/__tests__/lib/XmlText.ts new file mode 100644 index 0000000000000..1ee0547a9e16a --- /dev/null +++ b/packages/xml-builder/__tests__/lib/XmlText.ts @@ -0,0 +1,8 @@ +import {XmlText} from '../../lib/XmlText'; + +describe('XmlText', () => { + it('escapes element text', () => { + const text = new XmlText('this & that are < or > "most"'); + expect(text.toString()).toBe('this & that are < or > "most"'); + }); +}); \ No newline at end of file diff --git a/packages/xml-builder/index.ts b/packages/xml-builder/index.ts new file mode 100644 index 0000000000000..938f56437fac9 --- /dev/null +++ b/packages/xml-builder/index.ts @@ -0,0 +1,2 @@ +export * from './lib/XmlNode'; +export * from './lib/XmlText'; \ No newline at end of file diff --git a/packages/xml-builder/lib/XmlNode.ts b/packages/xml-builder/lib/XmlNode.ts new file mode 100644 index 0000000000000..c7494d552a6d4 --- /dev/null +++ b/packages/xml-builder/lib/XmlNode.ts @@ -0,0 +1,55 @@ + +/** + * Represents an XML node. + */ +export class XmlNode { + + private attributes: {[name: string]: any} = {}; + + constructor(public name: string, private children: XmlNode[] = []) {} + + protected escapeElement(value: string): string { + return value.replace(/&/g, '&').replace(//g, '>'); + } + + protected escapeAttribute(value: string): string { + return value.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + + addAttribute(name: string, value: any): XmlNode { + this.attributes[name] = value; + return this; + } + + addChildNode(child: XmlNode): XmlNode { + this.children.push(child); + return this; + } + + removeAttribute(name: string): XmlNode { + delete this.attributes[name]; + return this; + } + + toString(): string { + const hasChildren = Boolean(this.children.length); + let xmlText = `<${this.name}`; + // add attributes + const attributes = this.attributes; + for (let attributeName in attributes) { + if (!Object.prototype.hasOwnProperty.call(attributes, attributeName)) { + continue; + } + xmlText += ` ${attributeName}="${this.escapeAttribute('' + attributes[attributeName])}"`; + } + // close the tag if there aren't any children + if (!hasChildren) { + xmlText += '/>'; + } else { + xmlText += `>${this.children.map(c => c.toString()).join('')}`; + } + + return xmlText; + } +} + diff --git a/packages/xml-builder/lib/XmlText.ts b/packages/xml-builder/lib/XmlText.ts new file mode 100644 index 0000000000000..b652f9f92cfba --- /dev/null +++ b/packages/xml-builder/lib/XmlText.ts @@ -0,0 +1,13 @@ +import {XmlNode} from './XmlNode'; +/** + * Represents an XML text value. + */ +export class XmlText extends XmlNode { + constructor(private value: string) { + super('text'); + } + + toString(): string { + return this.escapeElement('' + this.value); + } +} \ No newline at end of file diff --git a/packages/xml-builder/package.json b/packages/xml-builder/package.json index 09100f8bacb25..bb57ddd9a28b2 100644 --- a/packages/xml-builder/package.json +++ b/packages/xml-builder/package.json @@ -4,11 +4,18 @@ "version": "0.0.1", "description": "XML builder for the AWS SDK", "devDependencies": { + "@types/jest": "^19.2.3", + "jest": "^20.0.3", "typescript": "^2.3" }, + "dependencies": { + "@aws/service-model": "^0.0.1", + "@aws/types": "^0.0.1" + }, "scripts": { "prepublishOnly": "tsc", - "test": "tsc" + "pretest": "tsc", + "test": "jest" }, "author": "aws-javascript-sdk-team@amazon.com", "license": "UNLICENSED" diff --git a/packages/xml-builder/tsconfig.json b/packages/xml-builder/tsconfig.json index f79d56077be60..52e3cbb7a6e77 100644 --- a/packages/xml-builder/tsconfig.json +++ b/packages/xml-builder/tsconfig.json @@ -3,11 +3,9 @@ "compilerOptions": { "module": "commonjs", "target": "es5", - "alwaysStrict": true, - "strictNullChecks": true, - "noImplicitAny": true, + "strict": true, "sourceMap": true, - "noImplicitThis": true + "declaration": true }, "exclude": ["node_modules"] } From 8c47f050bdd856e734006fdca7bbb9084f7251f6 Mon Sep 17 00:00:00 2001 From: chrisradek Date: Tue, 23 May 2017 09:09:29 -0700 Subject: [PATCH 3/5] Adds XmlNode tests --- packages/xml-builder/__tests__/lib/XmlNode.ts | 59 +++++++++++++++++++ packages/xml-builder/lib/XmlNode.ts | 8 +-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/xml-builder/__tests__/lib/XmlNode.ts b/packages/xml-builder/__tests__/lib/XmlNode.ts index d61abe49f4285..272a62f118047 100644 --- a/packages/xml-builder/__tests__/lib/XmlNode.ts +++ b/packages/xml-builder/__tests__/lib/XmlNode.ts @@ -78,4 +78,63 @@ describe('XmlNode', () => { expect(node.toString()).toBe(''); }); + describe('escapeElement', () => { + it('escapes: & < >', () => { + const node = new XmlNode('xml'); + const value = 'abc 123 &<>"%'; + expect(node.escapeElement(value)).toBe('abc 123 &<>"%'); + }); + }); + + describe('escapeAttribute', () => { + it('escapes: & < > "', () => { + const node = new XmlNode('xml'); + const value = 'abc 123 &<>"%'; + expect(node.escapeAttribute(value)).toBe('abc 123 &<>"%'); + }); + }); + + describe('addAttribute', () => { + it('adds an attribute to the XmlNode', () => { + const node = new XmlNode('xml'); + expect(node.attributes['foo']).toBeUndefined(); + node.addAttribute('foo', 'bar'); + expect(node.attributes['foo']).toBe('bar'); + }); + + it('returns a reference to the XmlNode', () => { + const node = new XmlNode('xml'); + expect(node.addAttribute('foo', 'bar')).toBe(node); + }); + }); + + describe('addChildNode', () => { + it('adds a child to the XmlNode', () => { + const node = new XmlNode('xml'); + expect(node.children.length === 0); + node.addChildNode(new XmlNode('foo')); + expect(node.children.length === 1); + expect(node.toString()).toBe(''); + }); + + it('returns a reference to the XmlNode', () => { + const node = new XmlNode('xml'); + expect(node.addChildNode(new XmlNode('foo'))).toBe(node); + }); + }); + + describe('removeAttribute', () => { + it('removes an attribute from the XmlNode', () => { + const node = new XmlNode('xml'); + node.addAttribute('foo', 'bar'); + expect(node.attributes['foo']).toBe('bar'); + node.removeAttribute('foo'); + expect(node.attributes['foo']).toBeUndefined(); + }); + + it('returns a reference to the XmlNode', () => { + const node = new XmlNode('xml'); + expect(node.removeAttribute('foo')).toBe(node); + }); + }); }); \ No newline at end of file diff --git a/packages/xml-builder/lib/XmlNode.ts b/packages/xml-builder/lib/XmlNode.ts index c7494d552a6d4..2993fc81971c5 100644 --- a/packages/xml-builder/lib/XmlNode.ts +++ b/packages/xml-builder/lib/XmlNode.ts @@ -4,15 +4,15 @@ */ export class XmlNode { - private attributes: {[name: string]: any} = {}; + public attributes: {[name: string]: any} = {}; - constructor(public name: string, private children: XmlNode[] = []) {} + constructor(public name: string, public children: XmlNode[] = []) {} - protected escapeElement(value: string): string { + escapeElement(value: string): string { return value.replace(/&/g, '&').replace(//g, '>'); } - protected escapeAttribute(value: string): string { + escapeAttribute(value: string): string { return value.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } From 7b500aad58f3efca701da7d484f4aa538c3df703 Mon Sep 17 00:00:00 2001 From: chrisradek Date: Wed, 24 May 2017 08:33:31 -0700 Subject: [PATCH 4/5] Marks Xml class fields as private and breaks out escape methods --- packages/xml-builder/__tests__/lib/XmlNode.ts | 22 ++----------- .../__tests__/lib/escape-attribute.ts | 8 +++++ .../__tests__/lib/escape-element.ts | 8 +++++ packages/xml-builder/lib/XmlNode.ts | 31 +++++-------------- packages/xml-builder/lib/XmlText.ts | 11 +++---- packages/xml-builder/lib/escape-attribute.ts | 6 ++++ packages/xml-builder/lib/escape-element.ts | 6 ++++ packages/xml-builder/lib/stringable.ts | 3 ++ 8 files changed, 47 insertions(+), 48 deletions(-) create mode 100644 packages/xml-builder/__tests__/lib/escape-attribute.ts create mode 100644 packages/xml-builder/__tests__/lib/escape-element.ts create mode 100644 packages/xml-builder/lib/escape-attribute.ts create mode 100644 packages/xml-builder/lib/escape-element.ts create mode 100644 packages/xml-builder/lib/stringable.ts diff --git a/packages/xml-builder/__tests__/lib/XmlNode.ts b/packages/xml-builder/__tests__/lib/XmlNode.ts index 272a62f118047..f0138071acc4a 100644 --- a/packages/xml-builder/__tests__/lib/XmlNode.ts +++ b/packages/xml-builder/__tests__/lib/XmlNode.ts @@ -78,25 +78,9 @@ describe('XmlNode', () => { expect(node.toString()).toBe(''); }); - describe('escapeElement', () => { - it('escapes: & < >', () => { - const node = new XmlNode('xml'); - const value = 'abc 123 &<>"%'; - expect(node.escapeElement(value)).toBe('abc 123 &<>"%'); - }); - }); - - describe('escapeAttribute', () => { - it('escapes: & < > "', () => { - const node = new XmlNode('xml'); - const value = 'abc 123 &<>"%'; - expect(node.escapeAttribute(value)).toBe('abc 123 &<>"%'); - }); - }); - describe('addAttribute', () => { it('adds an attribute to the XmlNode', () => { - const node = new XmlNode('xml'); + const node:any = new XmlNode('xml'); expect(node.attributes['foo']).toBeUndefined(); node.addAttribute('foo', 'bar'); expect(node.attributes['foo']).toBe('bar'); @@ -110,7 +94,7 @@ describe('XmlNode', () => { describe('addChildNode', () => { it('adds a child to the XmlNode', () => { - const node = new XmlNode('xml'); + const node:any = new XmlNode('xml'); expect(node.children.length === 0); node.addChildNode(new XmlNode('foo')); expect(node.children.length === 1); @@ -125,7 +109,7 @@ describe('XmlNode', () => { describe('removeAttribute', () => { it('removes an attribute from the XmlNode', () => { - const node = new XmlNode('xml'); + const node:any = new XmlNode('xml'); node.addAttribute('foo', 'bar'); expect(node.attributes['foo']).toBe('bar'); node.removeAttribute('foo'); diff --git a/packages/xml-builder/__tests__/lib/escape-attribute.ts b/packages/xml-builder/__tests__/lib/escape-attribute.ts new file mode 100644 index 0000000000000..d880f8a9f94c8 --- /dev/null +++ b/packages/xml-builder/__tests__/lib/escape-attribute.ts @@ -0,0 +1,8 @@ +import {escapeAttribute} from '../../lib/escape-attribute'; + +describe('escape-attribute', () => { + it('escapes: & < > "', () => { + const value = 'abc 123 &<>"%'; + expect(escapeAttribute(value)).toBe('abc 123 &<>"%'); + }); +}); \ No newline at end of file diff --git a/packages/xml-builder/__tests__/lib/escape-element.ts b/packages/xml-builder/__tests__/lib/escape-element.ts new file mode 100644 index 0000000000000..337b6a98bfd30 --- /dev/null +++ b/packages/xml-builder/__tests__/lib/escape-element.ts @@ -0,0 +1,8 @@ +import {escapeElement} from '../../lib/escape-element'; + +describe('escape-element', () => { + it('escapes: & < >', () => { + const value = 'abc 123 &<>"%'; + expect(escapeElement(value)).toBe('abc 123 &<>"%'); + }); +}); \ No newline at end of file diff --git a/packages/xml-builder/lib/XmlNode.ts b/packages/xml-builder/lib/XmlNode.ts index 2993fc81971c5..e9c5f7641c79e 100644 --- a/packages/xml-builder/lib/XmlNode.ts +++ b/packages/xml-builder/lib/XmlNode.ts @@ -1,27 +1,21 @@ +import {escapeAttribute} from './escape-attribute'; +import {Stringable} from './stringable'; /** * Represents an XML node. */ export class XmlNode { - public attributes: {[name: string]: any} = {}; + private attributes: {[name: string]: any} = {}; - constructor(public name: string, public children: XmlNode[] = []) {} - - escapeElement(value: string): string { - return value.replace(/&/g, '&').replace(//g, '>'); - } - - escapeAttribute(value: string): string { - return value.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - } + constructor(private name: string, private children: Stringable[] = []) {} addAttribute(name: string, value: any): XmlNode { this.attributes[name] = value; return this; } - addChildNode(child: XmlNode): XmlNode { + addChildNode(child: Stringable): XmlNode { this.children.push(child); return this; } @@ -36,20 +30,11 @@ export class XmlNode { let xmlText = `<${this.name}`; // add attributes const attributes = this.attributes; - for (let attributeName in attributes) { - if (!Object.prototype.hasOwnProperty.call(attributes, attributeName)) { - continue; - } - xmlText += ` ${attributeName}="${this.escapeAttribute('' + attributes[attributeName])}"`; - } - // close the tag if there aren't any children - if (!hasChildren) { - xmlText += '/>'; - } else { - xmlText += `>${this.children.map(c => c.toString()).join('')}`; + for (let attributeName of Object.keys(attributes)) { + xmlText += ` ${attributeName}="${escapeAttribute('' + attributes[attributeName])}"`; } - return xmlText; + return xmlText += !hasChildren ? '/>' : `>${this.children.map(c => c.toString()).join('')}`; } } diff --git a/packages/xml-builder/lib/XmlText.ts b/packages/xml-builder/lib/XmlText.ts index b652f9f92cfba..f5bf8e660c2dc 100644 --- a/packages/xml-builder/lib/XmlText.ts +++ b/packages/xml-builder/lib/XmlText.ts @@ -1,13 +1,12 @@ -import {XmlNode} from './XmlNode'; +import {escapeElement} from './escape-element'; +import {Stringable} from './stringable'; /** * Represents an XML text value. */ -export class XmlText extends XmlNode { - constructor(private value: string) { - super('text'); - } +export class XmlText implements Stringable { + constructor(private value: string) {} toString(): string { - return this.escapeElement('' + this.value); + return escapeElement('' + this.value); } } \ No newline at end of file diff --git a/packages/xml-builder/lib/escape-attribute.ts b/packages/xml-builder/lib/escape-attribute.ts new file mode 100644 index 0000000000000..22677c30d922c --- /dev/null +++ b/packages/xml-builder/lib/escape-attribute.ts @@ -0,0 +1,6 @@ +/** + * Escapes characters that can not be in an XML attribute. + */ +export function escapeAttribute(value: string): string { + return value.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} \ No newline at end of file diff --git a/packages/xml-builder/lib/escape-element.ts b/packages/xml-builder/lib/escape-element.ts new file mode 100644 index 0000000000000..a8c808f5e51f0 --- /dev/null +++ b/packages/xml-builder/lib/escape-element.ts @@ -0,0 +1,6 @@ +/** + * Escapes characters that can not be in an XML element. + */ +export function escapeElement(value: string): string { + return value.replace(/&/g, '&').replace(//g, '>'); +} \ No newline at end of file diff --git a/packages/xml-builder/lib/stringable.ts b/packages/xml-builder/lib/stringable.ts new file mode 100644 index 0000000000000..ea5228679be85 --- /dev/null +++ b/packages/xml-builder/lib/stringable.ts @@ -0,0 +1,3 @@ +export interface Stringable { + toString(): string; +} \ No newline at end of file From f46caef7b87fb5f167924cc16af7d34fd52cf1d3 Mon Sep 17 00:00:00 2001 From: chrisradek Date: Wed, 24 May 2017 10:11:37 -0700 Subject: [PATCH 5/5] XmlNode.addAttribute ignores null and undefined values --- packages/xml-builder/__tests__/index.ts | 2 +- packages/xml-builder/__tests__/lib/XmlNode.ts | 10 ++++++++++ packages/xml-builder/lib/XmlNode.ts | 5 ++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/xml-builder/__tests__/index.ts b/packages/xml-builder/__tests__/index.ts index 37b0a0e8d875c..13609e9fb15b1 100644 --- a/packages/xml-builder/__tests__/index.ts +++ b/packages/xml-builder/__tests__/index.ts @@ -1,4 +1,4 @@ -import * as pkg from '../'; +import * as pkg from '../index'; describe('package index', () => { it('should define XmlNode', () => { diff --git a/packages/xml-builder/__tests__/lib/XmlNode.ts b/packages/xml-builder/__tests__/lib/XmlNode.ts index f0138071acc4a..fb4cc05b761e2 100644 --- a/packages/xml-builder/__tests__/lib/XmlNode.ts +++ b/packages/xml-builder/__tests__/lib/XmlNode.ts @@ -71,6 +71,16 @@ describe('XmlNode', () => { expect(node.toString()).toBe(''); }); + it('ignores null and undefined attributes', () => { + const node:any = new XmlNode('xml'); + expect(Object.keys(node.attributes).length).toBe(0); + node.addAttribute('foo', null); + node.addAttribute('bar', undefined); + node.addAttribute('baz', 123); + node.addAttribute('bingo', 'bongo'); + expect(node.toString()).toBe(''); + }); + it('escapes attribute values and element text', () => { const node = new XmlNode('xml', [ new XmlNode('this & that') diff --git a/packages/xml-builder/lib/XmlNode.ts b/packages/xml-builder/lib/XmlNode.ts index e9c5f7641c79e..d8aad4dd864f2 100644 --- a/packages/xml-builder/lib/XmlNode.ts +++ b/packages/xml-builder/lib/XmlNode.ts @@ -31,7 +31,10 @@ export class XmlNode { // add attributes const attributes = this.attributes; for (let attributeName of Object.keys(attributes)) { - xmlText += ` ${attributeName}="${escapeAttribute('' + attributes[attributeName])}"`; + let attribute = attributes[attributeName]; + if (typeof attribute !== 'undefined' && attribute !== null) { + xmlText += ` ${attributeName}="${escapeAttribute('' + attribute)}"`; + } } return xmlText += !hasChildren ? '/>' : `>${this.children.map(c => c.toString()).join('')}`;