Refactoring ui/builder and template builder to preserve source information for templates

This commit is contained in:
Panayot Cankov
2015-11-25 12:45:02 +02:00
parent 8bee3ed2d1
commit 5447b04e86
6 changed files with 454 additions and 329 deletions

View File

@ -0,0 +1,19 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:tc="xml-declaration/template-builder-tests/template-view">
<tc:TemplateView id="template-view">
<tc:TemplateView.template>
<StackLayout>
<!--
At first I was going to put "MenuItem",
as per https://github.com/NativeScript/NativeScript/issues/501,
but then again we may re-introduce "MenuItem" in future and blow this test up.
So here is something unique that has better chance not to appear.
This comment also offsets error's row and column numbers so if you edit,
please do so beyond the unicorn.
-->
<Unicorn backgroundColor="pink" />
<Label text="Modal Page" />
</StackLayout>
</tc:TemplateView.template>
</tc:TemplateView>
</Page>

View File

@ -837,7 +837,7 @@ export function test_parse_template_property() {
TKUnit.assertEqual(button.text, "Click!", "Expected child Button to have text 'Click!'");
}
export function test_ParserError() {
export function test_NonExistingElementError() {
var basePath = "xml-declaration/";
var expectedErrorStart =
"Building UI from XML. @file:///app/" + basePath + "errors/non-existing-element.xml:11:5\n" +
@ -856,3 +856,28 @@ export function test_ParserError() {
}
TKUnit.assertEqual(message.substr(0, expectedErrorStart.length), expectedErrorStart, "Expected load to throw, and the message to start with specific string");
}
export function test_NonExistingElementInTemplateError() {
var basePath = "xml-declaration/";
var expectedErrorStart =
"Building UI from XML. @file:///app/" + basePath + "errors/non-existing-element-in-template.xml:14:17\n" +
" ↳Module 'ui/unicorn' not found for element 'Unicorn'.\n";
if (global.android) {
expectedErrorStart += " ↳Module \"ui/unicorn\" not found";
} else {
expectedErrorStart += " ↳Failed to find module 'ui/unicorn'";
}
var message;
var page = builder.load(__dirname + "/errors/non-existing-element-in-template.xml");
TKUnit.assert(view, "Expected the xml to generate a page");
var templateView = <TemplateView>page.getViewById("template-view");
TKUnit.assert(templateView, "Expected the page to have a TemplateView with 'temaplte-view' id.");
try {
templateView.parseTemplate();
} catch(e) {
message = e.message;
}
TKUnit.assertEqual(message.substr(0, expectedErrorStart.length), expectedErrorStart, "Expected load to throw, and the message to start with specific string");
}

View File

@ -478,8 +478,6 @@
"ui/builder/component-builder.ts",
"ui/builder/special-properties.d.ts",
"ui/builder/special-properties.ts",
"ui/builder/template-builder.d.ts",
"ui/builder/template-builder.ts",
"ui/button/button-common.ts",
"ui/button/button.android.ts",
"ui/button/button.d.ts",

View File

@ -3,24 +3,13 @@ import fs = require("file-system");
import xml = require("xml");
import types = require("utils/types");
import componentBuilder = require("ui/builder/component-builder");
import templateBuilderDef = require("ui/builder/template-builder");
import platform = require("platform");
import definition = require("ui/builder");
import page = require("ui/page");
import fileResolverModule = require("file-system/file-name-resolver");
import trace = require("trace");
import debug = require("utils/debug");
var KNOWNCOLLECTIONS = "knownCollections";
function isPlatform(value: string): boolean {
return value && (value.toLowerCase() === platform.platformNames.android.toLowerCase()
|| value.toLowerCase() === platform.platformNames.ios.toLowerCase());
}
function isCurentPlatform(value: string): boolean {
return value && value.toLowerCase() === platform.device.os.toLowerCase();
}
import builder = require("ui/builder");
export function parse(value: string | view.Template, context: any): view.View {
if (types.isString(value)) {
@ -43,164 +32,19 @@ export function parse(value: string | view.Template, context: any): view.View {
}
function parseInternal(value: string, context: any, uri?: string): componentBuilder.ComponentModule {
var currentPage: page.Page;
var rootComponentModule: componentBuilder.ComponentModule;
// Temporary collection used for parent scope.
var parents = new Array<componentBuilder.ComponentModule>();
var complexProperties = new Array<ComplexProperty>();
var templateBuilder: templateBuilderDef.TemplateBuilder;
var start: xml2ui.XmlStringParser;
var ui: xml2ui.ComponentParser;
var currentPlatformContext: string;
var errorFormat = (debug.debug && uri) ? xml2ui.SourceErrorFormat(uri) : xml2ui.PositionErrorFormat;
var wrapSource: (e: Error, p: xml.Position) => Error;
if (debug.debug && uri) {
wrapSource = (e: Error, p: xml.Position) => {
var source = new debug.Source(uri, p.line, p.column);
e = new debug.SourceError(e, source, "Building UI from XML.");
return e;
}
} else {
wrapSource = e => e; // no-op identity
}
(start = new xml2ui.XmlStringParser(errorFormat))
.pipe(new xml2ui.PlatformFilter())
.pipe(new xml2ui.XmlStateParser(ui = new xml2ui.ComponentParser(context, errorFormat)));
// Parse the XML.
var xmlParser = new xml.XmlParser((args: xml.ParserEvent) => {
try {
if (args.eventType === xml.ParserEventType.StartElement) {
if (isPlatform(args.elementName)) {
start.parse(value);
if (currentPlatformContext) {
throw new Error("Already in '" + currentPlatformContext + "' platform context and cannot switch to '" + args.elementName + "' platform! Platform tags cannot be nested.");
}
currentPlatformContext = args.elementName;
return;
}
}
if (args.eventType === xml.ParserEventType.EndElement) {
if (isPlatform(args.elementName)) {
currentPlatformContext = undefined;
return;
}
}
if (currentPlatformContext && !isCurentPlatform(currentPlatformContext)) {
return;
}
if (templateBuilder) {
var finished = templateBuilder.handleElement(args);
if (finished) {
// Clean-up and continnue
templateBuilder = undefined;
}
else {
// Skip processing untill the template builder finishes his job.
return;
}
}
// Get the current parent.
var parent = parents[parents.length - 1];
var complexProperty = complexProperties[complexProperties.length - 1];
// Create component instance from every element declaration.
if (args.eventType === xml.ParserEventType.StartElement) {
if (isComplexProperty(args.elementName)) {
var name = getComplexProperty(args.elementName);
complexProperties.push({
parent: parent,
name: name,
items: [],
});
if (templateBuilderDef.isKnownTemplate(name, parent.exports)) {
templateBuilder = new templateBuilderDef.TemplateBuilder({
context: parent ? getExports(parent.component) : null, // Passing 'context' won't work if you set "codeFile" on the page
parent: parent,
name: name,
elementName: args.elementName,
templateItems: []
});
}
} else {
var componentModule: componentBuilder.ComponentModule;
if (args.prefix && args.namespace) {
// Custom components
componentModule = loadCustomComponent(args.namespace, args.elementName, args.attributes, context, currentPage);
} else {
// Default components
componentModule = componentBuilder.getComponentModule(args.elementName, args.namespace, args.attributes, context);
}
if (componentModule) {
if (parent) {
if (componentModule.component instanceof view.View) {
if (complexProperty) {
// Add to complex property to component.
addToComplexProperty(parent, complexProperty, componentModule)
} else if ((<any>parent.component)._addChildFromBuilder) {
// Add component to visual tree
(<any>parent.component)._addChildFromBuilder(args.elementName, componentModule.component);
}
} else if (complexProperty) {
// Add component to complex property of parent component.
addToComplexProperty(parent, complexProperty, componentModule);
} else if ((<any>parent.component)._addChildFromBuilder) {
(<any>parent.component)._addChildFromBuilder(args.elementName, componentModule.component);
}
} else if (parents.length === 0) {
// Set root component.
rootComponentModule = componentModule;
if (rootComponentModule && rootComponentModule.component instanceof page.Page) {
currentPage = <page.Page>rootComponentModule.component;
}
}
// Add the component instance to the parents scope collection.
parents.push(componentModule);
}
}
} else if (args.eventType === xml.ParserEventType.EndElement) {
if (isComplexProperty(args.elementName)) {
if (complexProperty) {
if (parent && (<any>parent.component)._addArrayFromBuilder) {
// If parent is AddArrayFromBuilder call the interface method to populate the array property.
(<any>parent.component)._addArrayFromBuilder(complexProperty.name, complexProperty.items);
complexProperty.items = [];
}
}
// Remove the last complexProperty from the complexProperties collection (move to the previous complexProperty scope).
complexProperties.pop();
} else {
// Remove the last parent from the parents collection (move to the previous parent scope).
parents.pop();
}
}
} catch(e) {
throw wrapSource(e, args.position);
}
}, (e, p) => {
throw wrapSource(new Error("XML parse error: " + e.message), p);
}, true);
if (types.isString(value)) {
value = value.replace(/xmlns=("|')http:\/\/((www)|(schemas))\.nativescript\.org\/tns\.xsd\1/, "");
xmlParser.parse(value);
}
return rootComponentModule;
return ui.rootComponentModule;
}
function loadCustomComponent(componentPath: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: page.Page): componentBuilder.ComponentModule {
@ -297,44 +141,6 @@ function loadInternal(fileName: string, context?: any): componentBuilder.Compone
return componentModule;
}
function isComplexProperty(name: string): boolean {
return types.isString(name) && name.indexOf(".") !== -1;
}
function getComplexProperty(fullName: string): string {
var name: string;
if (types.isString(fullName)) {
var names = fullName.split(".");
name = names[names.length - 1];
}
return name;
}
function isKnownCollection(name: string, context: any): boolean {
return KNOWNCOLLECTIONS in context && context[KNOWNCOLLECTIONS] && name in context[KNOWNCOLLECTIONS];
}
function addToComplexProperty(parent: componentBuilder.ComponentModule, complexProperty: ComplexProperty, elementModule: componentBuilder.ComponentModule) {
// If property name is known collection we populate array with elements.
var parentComponent = <any>parent.component;
if (isKnownCollection(complexProperty.name, parent.exports)) {
complexProperty.items.push(elementModule.component);
} else if (parentComponent._addChildFromBuilder) {
parentComponent._addChildFromBuilder(complexProperty.name, elementModule.component);
} else {
// Or simply assign the value;
parentComponent[complexProperty.name] = elementModule.component;
}
}
interface ComplexProperty {
parent: componentBuilder.ComponentModule;
name: string;
items?: Array<any>;
}
function getExports(instance: view.View): any {
var parent = instance.parent;
@ -344,3 +150,400 @@ function getExports(instance: view.View): any {
return parent ? (<any>parent).exports : undefined;
}
namespace xml2ui {
/**
* Pipes and filters:
* https://en.wikipedia.org/wiki/Pipeline_(software)
*/
interface XmlProducer {
pipe<Next extends XmlConsumer>(next: Next): Next;
}
interface XmlConsumer {
parse(args: xml.ParserEvent);
}
export class XmlProducerBase implements XmlProducer {
private _next: XmlConsumer;
public pipe<Next extends XmlConsumer>(next: Next) {
this._next = next;
return next;
}
protected next(args: xml.ParserEvent) {
this._next.parse(args);
}
}
export class XmlStringParser extends XmlProducerBase implements XmlProducer {
private error: ErrorFormatter;
constructor(error?: ErrorFormatter) {
super();
this.error = error || PositionErrorFormat;
}
public parse(value: string) {
var xmlParser = new xml.XmlParser((args: xml.ParserEvent) => {
try {
this.next(args);
} catch(e) {
throw this.error(e, args.position);
}
}, (e, p) => {
throw this.error(e, p);
}, true);
if (types.isString(value)) {
value = value.replace(/xmlns=("|')http:\/\/((www)|(schemas))\.nativescript\.org\/tns\.xsd\1/, "");
xmlParser.parse(value);
}
}
}
interface ErrorFormatter {
(e: Error, p: xml.Position): Error;
}
export function PositionErrorFormat(e: Error, p: xml.Position): Error {
return new debug.ScopeError(e, "Parsing XML at " + p.line + ":" + p.column);
}
export function SourceErrorFormat(uri): ErrorFormatter {
return (e: Error, p: xml.Position) => {
var source = new debug.Source(uri, p.line, p.column);
e = new debug.SourceError(e, source, "Building UI from XML.");
return e;
}
}
export class PlatformFilter extends XmlProducerBase implements XmlProducer, XmlConsumer {
private currentPlatformContext: string;
public parse(args: xml.ParserEvent) {
if (args.eventType === xml.ParserEventType.StartElement) {
if (PlatformFilter.isPlatform(args.elementName)) {
if (this.currentPlatformContext) {
throw new Error("Already in '" + this.currentPlatformContext + "' platform context and cannot switch to '" + args.elementName + "' platform! Platform tags cannot be nested.");
}
this.currentPlatformContext = args.elementName;
return;
}
}
if (args.eventType === xml.ParserEventType.EndElement) {
if (PlatformFilter.isPlatform(args.elementName)) {
this.currentPlatformContext = undefined;
return;
}
}
if (this.currentPlatformContext && !PlatformFilter.isCurentPlatform(this.currentPlatformContext)) {
return;
}
this.next(args);
}
private static isPlatform(value: string): boolean {
return value && (value.toLowerCase() === platform.platformNames.android.toLowerCase()
|| value.toLowerCase() === platform.platformNames.ios.toLowerCase());
}
private static isCurentPlatform(value: string): boolean {
return value && value.toLowerCase() === platform.device.os.toLowerCase();
}
}
export class XmlArgsReplay extends XmlProducerBase implements XmlProducer {
private error: ErrorFormatter;
private args: xml.ParserEvent[];
constructor(args: xml.ParserEvent[], errorFormat: ErrorFormatter) {
super();
this.args = args;
this.error = errorFormat;
}
public replay() {
this.args.forEach((args: xml.ParserEvent) => {
try {
this.next(args);
} catch(e) {
throw this.error(e, args.position);
}
});
}
}
interface TemplateProperty {
context?: any;
parent: componentBuilder.ComponentModule;
name: string;
elementName: string;
templateItems: Array<string>;
errorFormat: ErrorFormatter;
}
/**
* It is a state pattern
* https://en.wikipedia.org/wiki/State_pattern
*/
export class XmlStateParser implements XmlConsumer {
private state: XmlStateConsumer;
constructor(state: XmlStateConsumer) {
this.state = state;
}
parse(args: xml.ParserEvent) {
this.state = this.state.parse(args);
}
}
interface XmlStateConsumer extends XmlConsumer {
parse(args: xml.ParserEvent): XmlStateConsumer;
}
export class TemplateParser implements XmlStateConsumer {
private _context: any;
private _recordedXmlStream: Array<xml.ParserEvent>;
private _templateProperty: TemplateProperty;
private _nestingLevel: number;
private _state: TemplateParser.State;
private parent: XmlStateConsumer;
constructor(parent: XmlStateConsumer, templateProperty: TemplateProperty) {
this.parent = parent;
this._context = templateProperty.context;
this._recordedXmlStream = new Array<xml.ParserEvent>();
this._templateProperty = templateProperty;
this._nestingLevel = 0;
this._state = TemplateParser.State.EXPECTING_START;
}
public parse(args: xml.ParserEvent): XmlStateConsumer {
if (args.eventType === xml.ParserEventType.StartElement) {
this.parseStartElement(args.prefix, args.namespace, args.elementName, args.attributes);
} else if (args.eventType === xml.ParserEventType.EndElement) {
this.parseEndElement(args.prefix, args.elementName);
}
this._recordedXmlStream.push(args);
return this._state === TemplateParser.State.FINISHED ? this.parent : this;
}
public get elementName(): string {
return this._templateProperty.elementName;
}
private parseStartElement(prefix: string, namespace: string, elementName: string, attributes: Object) {
if (this._state === TemplateParser.State.EXPECTING_START) {
this._state = TemplateParser.State.PARSING;
} else if (this._state === TemplateParser.State.FINISHED) {
throw new Error("Template must have exactly one root element but multiple elements were found.");
}
this._nestingLevel++;
}
private parseEndElement(prefix: string, elementName: string) {
if (this._state === TemplateParser.State.EXPECTING_START) {
throw new Error("Template must have exactly one root element but none was found.");
} else if (this._state === TemplateParser.State.FINISHED) {
throw new Error("No more closing elements expected for this template.");
}
this._nestingLevel--;
if (this._nestingLevel === 0) {
this._state = TemplateParser.State.FINISHED;
this.build();
}
}
private build() {
if (this._templateProperty.name in this._templateProperty.parent.component) {
var context = this._context;
var errorFormat = this._templateProperty.errorFormat;
var template: view.Template = () => {
var start: xml2ui.XmlArgsReplay;
var ui: xml2ui.ComponentParser;
(start = new xml2ui.XmlArgsReplay(this._recordedXmlStream, errorFormat))
// No platform filter, it has been filtered allready
.pipe(new XmlStateParser(ui = new ComponentParser(context, errorFormat)));
start.replay();
return ui.rootComponentModule.component;
}
this._templateProperty.parent.component[this._templateProperty.name] = template;
}
}
}
export namespace TemplateParser {
export const enum State {
EXPECTING_START,
PARSING,
FINISHED
}
}
export class ComponentParser implements XmlStateConsumer {
private static KNOWNCOLLECTIONS = "knownCollections";
private static KNOWNTEMPLATES = "knownTemplates";
public rootComponentModule: componentBuilder.ComponentModule;
private context: any;
private currentPage: page.Page;
private parents = new Array<componentBuilder.ComponentModule>();
private complexProperties = new Array<ComponentParser.ComplexProperty>();
private error;
constructor(context: any, errorFormat: ErrorFormatter) {
this.context = context;
this.error = errorFormat;
}
public parse(args: xml.ParserEvent): XmlStateConsumer {
// Get the current parent.
var parent = this.parents[this.parents.length - 1];
var complexProperty = this.complexProperties[this.complexProperties.length - 1];
// Create component instance from every element declaration.
if (args.eventType === xml.ParserEventType.StartElement) {
if (ComponentParser.isComplexProperty(args.elementName)) {
var name = ComponentParser.getComplexPropertyName(args.elementName);
this.complexProperties.push({
parent: parent,
name: name,
items: [],
});
if (ComponentParser.isKnownTemplate(name, parent.exports)) {
return new TemplateParser(this, {
context: parent ? getExports(parent.component) : null, // Passing 'context' won't work if you set "codeFile" on the page
parent: parent,
name: name,
elementName: args.elementName,
templateItems: [],
errorFormat: this.error
});
}
} else {
var componentModule: componentBuilder.ComponentModule;
if (args.prefix && args.namespace) {
// Custom components
componentModule = loadCustomComponent(args.namespace, args.elementName, args.attributes, this.context, this.currentPage);
} else {
// Default components
componentModule = componentBuilder.getComponentModule(args.elementName, args.namespace, args.attributes, this.context);
}
if (componentModule) {
if (parent) {
if (complexProperty) {
// Add component to complex property of parent component.
ComponentParser.addToComplexProperty(parent, complexProperty, componentModule);
} else if ((<any>parent.component)._addChildFromBuilder) {
(<any>parent.component)._addChildFromBuilder(args.elementName, componentModule.component);
}
} else if (this.parents.length === 0) {
// Set root component.
this.rootComponentModule = componentModule;
if (this.rootComponentModule && this.rootComponentModule.component instanceof page.Page) {
this.currentPage = <page.Page>this.rootComponentModule.component;
}
}
// Add the component instance to the parents scope collection.
this.parents.push(componentModule);
}
}
} else if (args.eventType === xml.ParserEventType.EndElement) {
if (ComponentParser.isComplexProperty(args.elementName)) {
if (complexProperty) {
if (parent && (<any>parent.component)._addArrayFromBuilder) {
// If parent is AddArrayFromBuilder call the interface method to populate the array property.
(<any>parent.component)._addArrayFromBuilder(complexProperty.name, complexProperty.items);
complexProperty.items = [];
}
}
// Remove the last complexProperty from the complexProperties collection (move to the previous complexProperty scope).
this.complexProperties.pop();
} else {
// Remove the last parent from the parents collection (move to the previous parent scope).
this.parents.pop();
}
}
return this;
}
private static isComplexProperty(name: string): boolean {
return types.isString(name) && name.indexOf(".") !== -1;
}
private static getComplexPropertyName(fullName: string): string {
var name: string;
if (types.isString(fullName)) {
var names = fullName.split(".");
name = names[names.length - 1];
}
return name;
}
private static isKnownTemplate(name: string, exports: any): boolean {
return ComponentParser.KNOWNTEMPLATES in exports && exports[ComponentParser.KNOWNTEMPLATES] && name in exports[ComponentParser.KNOWNTEMPLATES];
}
private static addToComplexProperty(parent: componentBuilder.ComponentModule, complexProperty: ComponentParser.ComplexProperty, elementModule: componentBuilder.ComponentModule) {
// If property name is known collection we populate array with elements.
var parentComponent = <any>parent.component;
if (ComponentParser.isKnownCollection(complexProperty.name, parent.exports)) {
complexProperty.items.push(elementModule.component);
} else if (parentComponent._addChildFromBuilder) {
parentComponent._addChildFromBuilder(complexProperty.name, elementModule.component);
} else {
// Or simply assign the value;
parentComponent[complexProperty.name] = elementModule.component;
}
}
private static isKnownCollection(name: string, context: any): boolean {
return ComponentParser.KNOWNCOLLECTIONS in context && context[ComponentParser.KNOWNCOLLECTIONS] && name in context[ComponentParser.KNOWNCOLLECTIONS];
}
}
export namespace ComponentParser {
export interface ComplexProperty {
parent: componentBuilder.ComponentModule;
name: string;
items?: Array<any>;
}
}
}

View File

@ -1,28 +0,0 @@
//@private
declare module "ui/builder/template-builder" {
import xml = require("xml");
import page = require("ui/page");
import componentBuilder = require("ui/builder/component-builder");
class TemplateBuilder {
constructor(templateProperty: TemplateProperty);
elementName: string;
/*
* Returns true if the template builder has finished parsing template and the parsing should continue.
* @param args - ParserEvent argument to handle.
*/
handleElement(args: xml.ParserEvent): boolean;
}
export function isKnownTemplate(name: string, exports: any): boolean;
interface TemplateProperty {
context?: any;
parent: componentBuilder.ComponentModule;
name: string;
elementName: string;
templateItems: Array<string>
}
}

View File

@ -1,92 +0,0 @@
import definition = require("ui/builder/template-builder");
import builder = require("ui/builder");
import view = require("ui/core/view");
import page = require("ui/page");
import xml = require("xml");
var KNOWNTEMPLATES = "knownTemplates";
export class TemplateBuilder {
private _context: any;
private _items: Array<string>;
private _templateProperty: definition.TemplateProperty;
private _nestingLevel: number;
constructor(templateProperty: definition.TemplateProperty) {
this._context = templateProperty.context;
this._items = new Array<string>();
this._templateProperty = templateProperty;
this._nestingLevel = 0;
}
public get elementName(): string {
return this._templateProperty.elementName;
}
handleElement(args: xml.ParserEvent): boolean {
if (args.eventType === xml.ParserEventType.StartElement) {
this.addStartElement(args.prefix, args.namespace, args.elementName, args.attributes);
} else if (args.eventType === xml.ParserEventType.EndElement) {
this.addEndElement(args.prefix, args.elementName);
}
if (this.hasFinished()) {
this.build();
return true;
}
else {
return false;
}
}
private addStartElement(prefix: string, namespace: string, elementName: string, attributes: Object) {
this._nestingLevel++;
this._items.push("<" +
getElementNameWithPrefix(prefix, elementName) +
(namespace ? " " + getNamespace(prefix, namespace) : "") +
(attributes ? " " + getAttributesAsString(attributes) : "") +
">");
}
private addEndElement(prefix: string, elementName: string) {
this._nestingLevel--;
if (!this.hasFinished()) {
this._items.push("</" + getElementNameWithPrefix(prefix, elementName) + ">");
}
}
private hasFinished() {
return this._nestingLevel < 0;
}
private build() {
if (this._templateProperty.name in this._templateProperty.parent.component) {
var xml = this._items.join("");
var context = this._context;
var template: view.Template = () => builder.parse(xml, context);
this._templateProperty.parent.component[this._templateProperty.name] = template;
}
}
}
export function isKnownTemplate(name: string, exports: any): boolean {
return KNOWNTEMPLATES in exports && exports[KNOWNTEMPLATES] && name in exports[KNOWNTEMPLATES];
}
function getAttributesAsString(attributes: Object): string {
var result = [];
for (var item in attributes) {
result.push(item + '="' + attributes[item] + '"');
}
return result.join(" ");
}
function getElementNameWithPrefix(prefix: string, elementName: string): string {
return (prefix ? prefix + ":" : "") + elementName;
}
function getNamespace(prefix: string, namespace: string): string {
return 'xmlns:' + prefix + '="' + namespace + '"';
}