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

@@ -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 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;
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
}
// Parse the XML.
var xmlParser = new xml.XmlParser((args: xml.ParserEvent) => {
try {
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();
}
}
} 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>;
}
}
}