Merge pull request #1125 from NativeScript/cankov/xml-source-tracing

Cankov/xml source tracing
This commit is contained in:
Panayot Cankov
2015-11-25 16:23:27 +02:00
14 changed files with 753 additions and 361 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

@@ -0,0 +1,14 @@
<Page shownModally="onShownModally">
<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>
</Page>

View File

@@ -835,4 +835,49 @@ export function test_parse_template_property() {
var button = <Button>templateView.getChildAt(0);
TKUnit.assert(button, "Expected the TemplateView's template to create a button child.");
TKUnit.assertEqual(button.text, "Click!", "Expected child Button to have text 'Click!'");
}
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" +
" ↳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;
try {
builder.load(__dirname + "/errors/non-existing-element.xml");
} 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");
}
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

@@ -1 +1 @@
{"eventType":"StartElement","elementName":"DocumentElement","attributes":{"param":"value"}}{"eventType":"StartElement","elementName":"First.Element","attributes":{"some.attr":"some.value"}}{"eventType":"Text","data":"\n ¶ Some Text ®\n "}{"eventType":"EndElement","elementName":"First.Element"}{"eventType":"StartElement","elementName":"SecondElement","attributes":{"param2":"something"}}{"eventType":"Text","data":"\n Pre-Text "}{"eventType":"StartElement","elementName":"Inline"}{"eventType":"Text","data":"Inlined text"}{"eventType":"EndElement","elementName":"Inline"}{"eventType":"Text","data":" Post-text.\n "}{"eventType":"EndElement","elementName":"SecondElement"}{"eventType":"StartElement","elementName":"entities"}{"eventType":"Text","data":"Xml tags begin with \"<\" and end with \">\" Ampersand is & and apostrophe is '"}{"eventType":"EndElement","elementName":"entities"}{"eventType":"StartElement","elementName":"script"}{"eventType":"CDATA","data":"\nfunction sum(a,b)\n{\n return a+b;\n}\n"}{"eventType":"EndElement","elementName":"script"}{"eventType":"Comment","data":"\n Hello,\n I am a multi-line XML comment.\n"}{"eventType":"EndElement","elementName":"DocumentElement"}
{"eventType":"StartElement","position":{"line":2,"column":1},"elementName":"DocumentElement","attributes":{"param":"value"}}{"eventType":"StartElement","position":{"line":3,"column":3},"elementName":"First.Element","attributes":{"some.attr":"some.value"}}{"eventType":"Text","position":{"line":3,"column":41},"data":"\n ¶ Some Text ®\n "}{"eventType":"EndElement","position":{"line":5,"column":3},"elementName":"First.Element"}{"eventType":"StartElement","position":{"line":7,"column":3},"elementName":"SecondElement","attributes":{"param2":"something"}}{"eventType":"Text","position":{"line":7,"column":37},"data":"\n Pre-Text "}{"eventType":"StartElement","position":{"line":8,"column":14},"elementName":"Inline"}{"eventType":"Text","position":{"line":8,"column":22},"data":"Inlined text"}{"eventType":"EndElement","position":{"line":8,"column":34},"elementName":"Inline"}{"eventType":"Text","position":{"line":8,"column":43},"data":" Post-text.\n "}{"eventType":"EndElement","position":{"line":9,"column":3},"elementName":"SecondElement"}{"eventType":"StartElement","position":{"line":10,"column":3},"elementName":"entities"}{"eventType":"Text","position":{"line":10,"column":13},"data":"Xml tags begin with \"<\" and end with \">\" Ampersand is & and apostrophe is '"}{"eventType":"EndElement","position":{"line":10,"column":123},"elementName":"entities"}{"eventType":"StartElement","position":{"line":11,"column":3},"elementName":"script"}{"eventType":"CDATA","position":{"line":12,"column":5},"data":"\nfunction sum(a,b)\n{\n return a+b;\n}\n"}{"eventType":"EndElement","position":{"line":18,"column":3},"elementName":"script"}{"eventType":"Comment","position":{"line":19,"column":3},"data":"\n Hello,\n I am a multi-line XML comment.\n"}{"eventType":"EndElement","position":{"line":23,"column":1},"elementName":"DocumentElement"}

View File

@@ -469,11 +469,24 @@ EasySAXParser.prototype.parse = function(xml) {
, stop // используется при разборе "namespace" . если встретился неизвестное пространство то события не генерируются
, _nsmatrix
, ok
, pos = 0, ln = 0, lnStart = -2, lnEnd = -1
;
function getStringNode() {
return xml.substring(i, j+1)
};
function findLineAndColumnFromPos() {
while (lnStart < lnEnd && lnEnd < pos) {
lnStart = lnEnd;
lnEnd = xml.indexOf("\n", lnEnd + 1);
++ln;
}
return { line: ln, column: pos - lnStart };
}
function position(p) {
pos = p;
return findLineAndColumnFromPos;
}
while(j !== -1) {
stop = stopIndex > 0;
@@ -487,7 +500,7 @@ EasySAXParser.prototype.parse = function(xml) {
if (i === -1) { // конец разбора
if (nodestack.length) {
this.onError('end file');
this.onError('end file', position(j));
return;
};
@@ -495,7 +508,7 @@ EasySAXParser.prototype.parse = function(xml) {
};
if (j !== i && !stop) {
ok = this.onTextNode(xml.substring(j, i), unEntities);
ok = this.onTextNode(xml.substring(j, i), unEntities, position(j));
if (ok === false) return;
};
@@ -506,13 +519,13 @@ EasySAXParser.prototype.parse = function(xml) {
if (w === 91 && xml.substr(i+3, 6) === 'CDATA[') { // 91 == "["
j = xml.indexOf(']]>', i);
if (j === -1) {
this.onError('cdata');
this.onError('cdata', position(i));
return;
};
//x = xml.substring(i+9, j);
if (!stop) {
ok = this.onCDATA(xml.substring(i+9, j), false);
ok = this.onCDATA(xml.substring(i+9, j), false, position(i));
if (ok === false) return;
};
@@ -523,13 +536,13 @@ EasySAXParser.prototype.parse = function(xml) {
if (w === 45 && xml.charCodeAt(i+3) === 45) { // 45 == "-"
j = xml.indexOf('-->', i);
if (j === -1) {
this.onError('expected -->');
this.onError('expected -->', position(i));
return;
};
if (this.is_onComment && !stop) {
ok = this.onComment(xml.substring(i+4, j), unEntities);
ok = this.onComment(xml.substring(i+4, j), unEntities, position(i));
if (ok === false) return;
};
@@ -539,12 +552,12 @@ EasySAXParser.prototype.parse = function(xml) {
j = xml.indexOf('>', i+1);
if (j === -1) {
this.onError('expected ">"');
this.onError('expected ">"', position(i + 1));
return;
};
if (this.is_onAttention && !stop) {
ok = this.onAttention(xml.substring(i, j+1), unEntities);
ok = this.onAttention(xml.substring(i, j+1), unEntities, position(i));
if (ok === false) return;
};
@@ -555,12 +568,12 @@ EasySAXParser.prototype.parse = function(xml) {
if (w === 63) { // "?"
j = xml.indexOf('?>', i);
if (j === -1) { // error
this.onError('...?>');
this.onError('...?>', position(i));
return;
};
if (this.is_onQuestion) {
ok = this.onQuestion(xml.substring(i, j+2));
ok = this.onQuestion(xml.substring(i, j+2), position(i));
if (ok === false) return;
};
@@ -572,7 +585,7 @@ EasySAXParser.prototype.parse = function(xml) {
j = xml.indexOf('>', i+1);
if (j == -1) { // error
this.onError('...>');
this.onError('...>', position(i + 1));
return;
};
@@ -589,7 +602,7 @@ EasySAXParser.prototype.parse = function(xml) {
//console.log()
if (xml.substring(i+2, q) !== x) {
this.onError('close tagname');
this.onError('close tagname', position(i + 2));
return;
};
@@ -601,7 +614,7 @@ EasySAXParser.prototype.parse = function(xml) {
continue;
};
this.onError('close tag');
this.onError('close tag', position(i + 2));
return;
};
@@ -619,7 +632,7 @@ EasySAXParser.prototype.parse = function(xml) {
};
if ( !(w > 96 && w < 123 || w > 64 && w <91) ) {
this.onError('first char nodeName');
this.onError('first char nodeName', position(i + 1));
return;
};
@@ -636,7 +649,7 @@ EasySAXParser.prototype.parse = function(xml) {
break;
};
this.onError('invalid nodeName');
this.onError('invalid nodeName', position(i + 1));
return;
};
@@ -718,7 +731,7 @@ EasySAXParser.prototype.parse = function(xml) {
var that = this;
ok = this.onStartNode(elem, function() { return that.getAttrs() }, unEntities, tagend
, getStringNode
, getStringNode, position(i)
);
if (ok === false) {
@@ -730,7 +743,7 @@ EasySAXParser.prototype.parse = function(xml) {
if (tagend) {
ok = this.onEndNode(elem, unEntities, tagstart
, getStringNode
, getStringNode, position(i)
);
if (ok === false) {

View File

@@ -319,8 +319,8 @@
"apps/tests/xml-declaration/mainPage.ts",
"apps/tests/xml-declaration/mymodule/MyControl.ts",
"apps/tests/xml-declaration/mymodulewithxml/MyControl.ts",
"apps/tests/xml-declaration/xml-declaration-tests.ts",
"apps/tests/xml-declaration/template-builder-tests/template-view.ts",
"apps/tests/xml-declaration/xml-declaration-tests.ts",
"apps/tests/xml-parser-tests/xml-parser-tests.ts",
"apps/transforms/app.ts",
"apps/transforms/main-page.ts",
@@ -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",
@@ -658,6 +656,8 @@
"ui/web-view/web-view.android.ts",
"ui/web-view/web-view.d.ts",
"ui/web-view/web-view.ios.ts",
"utils/debug.d.ts",
"utils/debug.ts",
"utils/module-merge.ts",
"utils/number-utils.ts",
"utils/types.d.ts",

View File

@@ -3,23 +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");
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 debug = require("utils/debug");
import builder = require("ui/builder");
export function parse(value: string | view.Template, context: any): view.View {
if (types.isString(value)) {
@@ -41,151 +31,20 @@ export function parse(value: string | view.Template, context: any): view.View {
}
}
function parseInternal(value: string, context: any): 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>();
function parseInternal(value: string, context: any, uri?: string): componentBuilder.ComponentModule {
var start: xml2ui.XmlStringParser;
var ui: xml2ui.ComponentParser;
var errorFormat = (debug.debug && uri) ? xml2ui.SourceErrorFormat(uri) : xml2ui.PositionErrorFormat;
(start = new xml2ui.XmlStringParser(errorFormat))
.pipe(new xml2ui.PlatformFilter())
.pipe(new xml2ui.XmlStateParser(ui = new xml2ui.ComponentParser(context, errorFormat)));
var templateBuilder: templateBuilderDef.TemplateBuilder;
start.parse(value);
var currentPlatformContext: string;
// Parse the XML.
var xmlParser = new xml.XmlParser((args: xml.ParserEvent) => {
if (args.eventType === xml.ParserEventType.StartElement) {
if (isPlatform(args.elementName)) {
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();
}
}
}, (e) => {
throw new Error("XML parse error: " + e.message);
}, 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 {
@@ -271,7 +130,7 @@ function loadInternal(fileName: string, context?: any): componentBuilder.Compone
throw new Error("Error loading file " + fileName + " :" + error.message);
}
var text = file.readTextSync(onError);
componentModule = parseInternal(text, context);
componentModule = parseInternal(text, context, fileName);
}
if (componentModule && componentModule.component) {
@@ -282,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;
@@ -329,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

@@ -5,6 +5,7 @@ import fs = require("file-system");
import bindingBuilder = require("./binding-builder");
import platform = require("platform");
import pages = require("ui/page");
import debug = require("utils/debug");
//the imports below are needed for special property registration
import "ui/layouts/dock-layout";
@@ -60,7 +61,7 @@ export function getComponentModule(elementName: string, namespace: string, attri
// Create instance of the component.
instance = new instanceType();
} catch (ex) {
throw new Error("Cannot create module " + moduleId + ". " + ex + ". StackTrace: " + ex.stack);
throw new debug.ScopeError(ex, "Module '" + moduleId + "' not found for element '" + (namespace ? namespace + ":" : "") + elementName + "'.");
}
if (attributes) {

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 + '"';
}

92
utils/debug.d.ts vendored Normal file
View File

@@ -0,0 +1,92 @@
declare module "utils/debug" {
/**
* A runtime option indicating whether the build has debugging enabled.
*/
export var debug: boolean;
/**
* A class encapsulating information for source code origin.
*/
export class Source {
/**
* Creates a new Source instance by given uri, line and column.
*/
constructor(uri: string, line: number, column: number);
/**
* Gets the URI of the source document;
*/
uri: string;
/**
* Gets the line in the source document.
*/
line: number;
/**
* Gets the position in the source document.
*/
column: number;
/**
* Get the source of an object.
*/
public static get(object: any): Source;
/**
* Set the source of an object.
*/
public static set(object: any, src: Source);
}
/**
* An Error class that provides additional context to an error.
*/
export class ScopeError implements Error {
/**
* Creates a new ScopeError providing addtional context to the child error.
* @param child The child error to extend.
* @param message Additional message to prepend to the child error.
*/
constructor(child: Error, message?: string);
/**
* Gets the child error.
*/
child: Error;
/**
* Gets the error message.
*/
message: string;
/**
* Gets the stack trace.
*/
stack: string;
/**
* Gets the error name.
*/
name: string;
}
/**
* Represents a scope error providing addiot
*/
export class SourceError extends ScopeError {
/**
* Creates a new SourceError by child error, source and optional message.
* @param child The child error to extend.
* @param source The source where the error occured.
* @param message Additonal message to prepend along the source location and the child error's message.
*/
constructor(child: Error, source: Source, message?: string);
/**
* Gets the error source.
*/
source: Source;
}
}

83
utils/debug.ts Normal file
View File

@@ -0,0 +1,83 @@
import { knownFolders } from "file-system"
export var debug = true;
// TODO: Get this from the runtimes...
var applicationRootPath = knownFolders.currentApp().path;
applicationRootPath = applicationRootPath.substr(0, applicationRootPath.length - "app/".length);
export class Source {
private _uri: string;
private _line: number;
private _column: number;
private static _source: symbol = Symbol("source");
private static _appRoot: string;
constructor(uri: string, line: number, column: number) {
if (uri.length > applicationRootPath.length && uri.substr(0, applicationRootPath.length) === applicationRootPath) {
this._uri = "file://" + uri.substr(applicationRootPath.length);
} else {
this._uri = uri;
}
this._line = line;
this._column = column;
}
get uri(): string { return this._uri; }
get line(): number { return this._line; }
get column(): number { return this._column; }
public toString() {
return this._uri + ":" + this._line + ":" + this._column;
}
public static get(object: any): Source {
return object[Source._source];
}
public static set(object: any, src: Source) {
object[Source._source] = src;
}
}
export class ScopeError implements Error {
private _child: Error;
private _message: string;
constructor(child: Error, message?: string) {
if (!child) {
throw new Error("Required child error!");
}
this._child = child;
this._message = message;
}
get child() { return this._child; }
get message() {
if (this._message && this._childMessage) {
// It is a ↳ but the ios fails to show this symbol at the moment.
return this._message + "\n \u21B3" + this._childMessage.replace("\n", "\n ");
}
return this._message || this._childMessage || undefined;
}
get name() { return this.child.name; }
get stack() { return (<any>this.child).stack; }
private get _childMessage(): string {
return this.child.message;
}
public toString() { return "Error: " + this.message; }
}
export class SourceError extends ScopeError {
private _source: Source;
constructor(child: Error, source: Source, message?: string) {
super(child, message ? message + " @" + source + "" : source + "");
this._source = source;
}
get source() { return this._source; }
}

42
xml/xml.d.ts vendored
View File

@@ -33,6 +33,21 @@ declare module "xml" {
*/
static Comment: string;
}
/**
* Defines a position within string, in line and column form.
*/
interface Position {
/**
* The line number. The first line is at index 1.
*/
line: number;
/**
* The column number. The first character is at index 1.
*/
column: number;
}
/**
* Provides information for a parser event.
@@ -43,6 +58,11 @@ declare module "xml" {
* Returns the type of the parser event. This is one of the ParserEventType static members.
*/
eventType: string;
/**
* Get the position in the xml string where the event was generated.
*/
position: Position;
/**
* If namespace processing is enabled, returns the prefix of the element in case the eventType is ParserEventType.StartElement or ParserEventType.EndElement.
@@ -80,18 +100,18 @@ declare module "xml" {
*/
class XmlParser {
/**
* Creates a new instance of the XmlParser class.
* @param onEvent The callback to execute when a parser event occurs. The 'event' parameter contains information about the event.
* @param onError The callback to execute when a parser error occurs. The 'error' parameter contains the error.
* @param processNamespaces Specifies whether namespaces should be processed.
*/
constructor(onEvent: (event: ParserEvent) => void, onError?: (error: Error) => void, processNamespaces?: boolean, angularSyntax?: boolean);
/**
* Creates a new instance of the XmlParser class.
* @param onEvent The callback to execute when a parser event occurs. The 'event' parameter contains information about the event.
* @param onError The callback to execute when a parser error occurs. The 'error' parameter contains the error.
* @param processNamespaces Specifies whether namespaces should be processed.
*/
constructor(onEvent: (event: ParserEvent) => void, onError?: (error: Error, position: Position) => void, processNamespaces?: boolean, angularSyntax?: boolean);
/**
* Parses the supplied xml string.
* @param xmlString The string containing the xml to parse.
*/
/**
* Parses the supplied xml string.
* @param xmlString The string containing the xml to parse.
*/
parse(xmlString: string): void;
}
}

View File

@@ -11,14 +11,16 @@ export class ParserEventType implements definition.ParserEventType {
export class ParserEvent implements definition.ParserEvent {
private _eventType: string;
private _position: definition.Position;
private _prefix: string;
private _namespace: string;
private _elementName: string;
private _attributes: Object;
private _data: string;
constructor(eventType: string, prefix?: string, namespace?: string, elementName?: string, attributes?: Object, data?: string) {
constructor(eventType: string, position: definition.Position, prefix?: string, namespace?: string, elementName?: string, attributes?: Object, data?: string) {
this._eventType = eventType;
this._position = position;
this._prefix = prefix;
this._namespace = namespace;
this._elementName = elementName;
@@ -29,6 +31,7 @@ export class ParserEvent implements definition.ParserEvent {
public toString(): string {
return JSON.stringify({
eventType: this.eventType,
position: this.position,
prefix: this.prefix,
namespace: this.namespace,
elementName: this.elementName,
@@ -40,6 +43,10 @@ export class ParserEvent implements definition.ParserEvent {
public get eventType(): string {
return this._eventType;
}
public get position(): definition.Position {
return this._position;
}
public get prefix(): string {
return this._prefix;
@@ -103,12 +110,12 @@ export class XmlParser implements definition.XmlParser {
private _processNamespaces: boolean;
private _namespaceStack: Array<any>;
constructor(onEvent: (event: definition.ParserEvent) => void, onError?: (error: Error) => void, processNamespaces?: boolean) {
constructor(onEvent: (event: definition.ParserEvent) => void, onError?: (error: Error, position: definition.Position) => void, processNamespaces?: boolean) {
this._processNamespaces = processNamespaces;
this._parser = new easysax.EasySAXParser();
var that = this;
this._parser.on('startNode', function (elem, attr, uq, str, tagend) {
this._parser.on('startNode', function (elem, attr, uq, tagend, str, pos) {
var attributes = attr();
if (attributes === true) {//HACK: For some reason easysax returns the true literal when an element has no attributes.
@@ -139,15 +146,15 @@ export class XmlParser implements definition.XmlParser {
name = resolved.name;
}
onEvent(new ParserEvent(ParserEventType.StartElement, prefix, namespace, name, attributes, undefined));
onEvent(new ParserEvent(ParserEventType.StartElement, pos(), prefix, namespace, name, attributes, undefined));
});
this._parser.on('textNode', function (text, uq) {
this._parser.on('textNode', function (text, uq, pos) {
var data = uq(XmlParser._dereferenceEntities(text));// Decode entity references such as &lt; and &gt;
onEvent(new ParserEvent(ParserEventType.Text, undefined, undefined, undefined, undefined, data));
onEvent(new ParserEvent(ParserEventType.Text, pos(), undefined, undefined, undefined, undefined, data));
});
this._parser.on('endNode', function (elem, uq, tagstart, str) {
this._parser.on('endNode', function (elem, uq, tagstart, str, pos) {
var prefix = undefined;
var namespace = undefined;
@@ -160,24 +167,24 @@ export class XmlParser implements definition.XmlParser {
name = resolved.name;
}
onEvent(new ParserEvent(ParserEventType.EndElement, prefix, namespace, name, undefined, undefined));
onEvent(new ParserEvent(ParserEventType.EndElement, pos(), prefix, namespace, name, undefined, undefined));
if (that._processNamespaces) {
that._namespaceStack.pop();
}
});
this._parser.on('cdata', function (data) {
onEvent(new ParserEvent(ParserEventType.CDATA, undefined, undefined, undefined, undefined, data));
this._parser.on('cdata', function (data, res, pos) {
onEvent(new ParserEvent(ParserEventType.CDATA, pos(), undefined, undefined, undefined, undefined, data));
});
this._parser.on('comment', function (text) {
onEvent(new ParserEvent(ParserEventType.Comment, undefined, undefined, undefined, undefined, text));
this._parser.on('comment', function (text, uq, pos) {
onEvent(new ParserEvent(ParserEventType.Comment, pos(), undefined, undefined, undefined, undefined, text));
});
if (onError) {
this._parser.on('error', function (msg) {
onError(new Error(msg));
this._parser.on('error', function (msg, pos) {
onError(new Error(msg), pos());
});
}
}