TypeScript XML parse

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);
  }
}