const ELEMENTS_KEY = '__xml_elements__';
const ALIASES_KEY = '__xml_aliases__';
const TRANSIENT_KEY = '__xml_transient__';
/**
* give XML parser type hint and alias.
* Element name is title case by default if no alias provide.
* @param prototype the js object for type hint
* @param alias the attribute name or tag name in XML
*/
export function XmlElement(prototype: any = '', alias: string = '') {
return function (target: any, propertyKey: string) {
const elements = target[ELEMENTS_KEY] || {};
elements[propertyKey] = prototype;
target[ELEMENTS_KEY] = elements;
if (alias) {
const aliases = target[ALIASES_KEY] || {};
aliases[alias] = propertyKey;
target[ALIASES_KEY] = aliases;
}
};
}
/**
* map XML name to class property name
* @param alias the attribute name or tag name in XML
*/
export function XmlAlias(alias: string) {
return function (target: any, propertyKey: string) {
const aliases = target[ALIASES_KEY] || {};
aliases[alias] = propertyKey;
target[ALIASES_KEY] = aliases;
};
}
/**
* ignore this property when build XML
*/
export function XmlTransient() {
return function (target: any, propertyKey: string) {
const transients = target[TRANSIENT_KEY] || {};
transients[propertyKey] = true;
target[TRANSIENT_KEY] = transients;
};
}
export class XmlParser {
private parser = new DOMParser();
parse<T>(xml: string, obj?: T): T {
const doc = this.parseDocument(xml);
return this.docToObject(doc, obj);
}
parseDocument(xml: string): Document {
return this.parser.parseFromString(xml, 'application/xml');
}
parseElements<T>(xml: string, tagName: string, obj?: T): T[] {
const doc = this.parseDocument(xml);
const elements = doc.getElementsByTagName(tagName);
return this.elementsToList<T>(elements, obj);
}
docToObject<T>(doc: Document, obj?: T): T {
return this.elementToObject<T>(doc.documentElement, obj);
}
elementsToList<T>(elements: HTMLCollectionOf<Element> | NodeListOf<Element> | Element[], obj?: T): T[] {
const list: T[] = [];
for (let i = 0; i < elements.length; ++i) {
const element = elements[i];
const item = this.elementToObject<T>(element, obj);
list.push(item);
}
return list;
}
elementToObject<T>(element: Element, obj?: T): T {
let empty = true;
let data = {};
for (let i = 0; i < element.attributes.length; ++i) {
const attr = element.attributes.item(i);
if (attr.name.startsWith('xmlns')) {
continue;
}
const name = obj && obj[ALIASES_KEY] && obj[ALIASES_KEY][attr.name] || camelCase(attr.name);
data[name] = attr.value;
if (obj && typeof obj[name] === 'number') {
data[name] = +data[name];
} else if (obj && typeof obj[name] === 'boolean') {
data[name] = data[name] != 'false';
}
empty = false;
}
let child = element.firstChild;
const map = new Map<string, Element[]>();
while (child) {
if (child.nodeType === Node.ELEMENT_NODE) {
const elements = map.get(child.nodeName) || [];
elements.push(child as Element);
map.set(child.nodeName, elements);
}
child = child.nextSibling;
}
map.forEach((elements, name) => {
name = obj && obj[ALIASES_KEY] && obj[ALIASES_KEY][name] || camelCase(name);
let isArray = elements.length > 1;
if (obj) {
if (Array.isArray(obj[name])) {
isArray = true;
} else if (typeof obj[name] === 'string' || typeof obj[name] === 'number' || typeof obj[name] === 'boolean') {
isArray = false;
}
}
const type = obj && (obj[ELEMENTS_KEY] && obj[ELEMENTS_KEY][name] || obj[name]);
if (isArray) {
data[name] = this.elementsToList(elements, type) || [];
empty = false;
} else if (elements.length > 0) {
data[name] = this.elementToObject(elements[0], type);
if (obj && typeof obj[name] === 'number') {
data[name] = +data[name];
} else if (obj && typeof obj[name] === 'boolean') {
data[name] = data[name] != 'false';
}
empty = false;
}
});
if (empty) {
data = element.textContent;
} else {
data = Object.assign(obj && obj.constructor ? new (<any>obj.constructor)() : {}, obj, data);
}
return data as T;
}
}
function camelCase(name: string): string {
return name.substr(0, 1).toLowerCase() + name.substr(1);
}
function titleCase(name: string): string {
return name.substr(0, 1).toUpperCase() + name.substr(1);
}
function toString(value: any): string {
return value === null || typeof value === 'undefined' ? '' : value + '';
}
export const parser = new XmlParser();
export class TypedXmlBuilder {
private readonly serializer = new XMLSerializer();
build(name: string, obj: any): string {
const doc = document.implementation.createDocument('', '', null);
doc.appendChild(this._build(doc, name, obj));
return this.serializer.serializeToString(doc);
}
private _build(doc: Document, name: string, obj: any, prototype?: any): Node {
const element = doc.createElement(name);
if (obj != null && typeof obj === 'object') {
const item = prototype || obj;
Object.keys(obj).forEach((key) => {
if (item[TRANSIENT_KEY] && item[TRANSIENT_KEY][key]) {
return;
}
const value = obj[key];
const name = this.getName(item, key);
const type = item[ELEMENTS_KEY] && item[ELEMENTS_KEY][key];
if (Array.isArray(value)) {
for (let e of value) {
element.appendChild(this._build(doc, name, e, type));
}
} else if (typeof type !== 'undefined' || typeof value === 'object') {
element.appendChild(this._build(doc, name, value === null || typeof value === 'undefined' ? type : value, type));
} else {
element.setAttribute(camelCase(name), toString(value));
}
});
} else {
element.appendChild(doc.createTextNode(toString(obj)));
}
return element;
}
private getName(obj: any, key: string) {
key = titleCase(key);
const aliases = obj[ALIASES_KEY];
if (aliases) {
for (let alias of Object.keys(aliases)) {
if (titleCase(aliases[alias]) === key) {
return alias;
}
}
}
return key;
}
}
export const xmlBuilder = new TypedXmlBuilder();
export class XmlElementRef {
private readonly name: string;
private readonly builder: XmlBuilder;
private value: string;
private attributes: Map<string, string> = new Map<string, string>();
private children: XmlElementRef[] = [];
constructor(name: string, builder: XmlBuilder) {
this.name = name;
this.builder = builder;
}
/**
* Add the Attribute to the Element.
* @param name the Attribute name
* @param value the Attribute value
*/
addAttribute(name: string, value: any): XmlElementRef {
this.attributes.set(name, toString(value));
return this;
}
/**
* Add a child Text Node.
* @param name the Node name
* @param value the text value
*/
addText(name: string, value: any): XmlElementRef {
const child = new XmlElementRef(name, this.builder);
child.value = toString(value);
this.children.push(child);
return this;
}
/**
* Add a child HTMLElement.
* @param name the Element name
*/
addElement(name: string): XmlElementRef {
const child = new XmlElementRef(name, this.builder);
this.children.push(child);
return child;
}
/**
* Return to the XmlBuilder.
*/
and(): XmlBuilder {
return this.builder;
}
build(doc: Document): Node {
const element = doc.createElement(this.name);
if (this.value) {
element.appendChild(doc.createTextNode(this.value));
}
this.attributes.forEach((value, key) => {
element.setAttribute(key, value);
});
this.children.forEach((child) => {
element.appendChild(child.build(doc));
});
return element;
}
}
export class XmlBuilder {
private readonly serializer = new XMLSerializer();
private readonly element: XmlElementRef;
constructor(root: string) {
this.element = new XmlElementRef(root, this);
}
/**
* Return the root Element.
*/
root(): XmlElementRef {
return this.element;
}
/**
* @deprecated use root().addAttribute(name, value)
*/
addAttribute(name: string, value: any): XmlBuilder {
this.element.addAttribute(name, toString(value));
return this;
}
/**
* Add a Text Node.
* @param name the Node name
* @param value the text value
*/
addText(name: string, value: any): XmlBuilder {
this.element.addText(name, toString(value));
return this;
}
/**
* Add a HTMLElement.
* @param name the Element name
*/
addElement(name: string): XmlElementRef {
return this.element.addElement(name);
}
/**
* Build XML document to string.
*/
build(): string {
const doc = document.implementation.createDocument('', '', null);
doc.appendChild(this.element.build(doc));
return this.serializer.serializeToString(doc);
}
}