mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-16 20:11:24 +08:00
Refactoring ui/builder and template builder to preserve source information for templates
This commit is contained in:
@ -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>
|
@ -837,7 +837,7 @@ export function test_parse_template_property() {
|
|||||||
TKUnit.assertEqual(button.text, "Click!", "Expected child Button to have text 'Click!'");
|
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 basePath = "xml-declaration/";
|
||||||
var expectedErrorStart =
|
var expectedErrorStart =
|
||||||
"Building UI from XML. @file:///app/" + basePath + "errors/non-existing-element.xml:11:5\n" +
|
"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");
|
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");
|
||||||
|
}
|
@ -478,8 +478,6 @@
|
|||||||
"ui/builder/component-builder.ts",
|
"ui/builder/component-builder.ts",
|
||||||
"ui/builder/special-properties.d.ts",
|
"ui/builder/special-properties.d.ts",
|
||||||
"ui/builder/special-properties.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-common.ts",
|
||||||
"ui/button/button.android.ts",
|
"ui/button/button.android.ts",
|
||||||
"ui/button/button.d.ts",
|
"ui/button/button.d.ts",
|
||||||
|
@ -3,24 +3,13 @@ import fs = require("file-system");
|
|||||||
import xml = require("xml");
|
import xml = require("xml");
|
||||||
import types = require("utils/types");
|
import types = require("utils/types");
|
||||||
import componentBuilder = require("ui/builder/component-builder");
|
import componentBuilder = require("ui/builder/component-builder");
|
||||||
import templateBuilderDef = require("ui/builder/template-builder");
|
|
||||||
import platform = require("platform");
|
import platform = require("platform");
|
||||||
import definition = require("ui/builder");
|
import definition = require("ui/builder");
|
||||||
import page = require("ui/page");
|
import page = require("ui/page");
|
||||||
import fileResolverModule = require("file-system/file-name-resolver");
|
import fileResolverModule = require("file-system/file-name-resolver");
|
||||||
import trace = require("trace");
|
import trace = require("trace");
|
||||||
import debug = require("utils/debug");
|
import debug = require("utils/debug");
|
||||||
|
import builder = require("ui/builder");
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parse(value: string | view.Template, context: any): view.View {
|
export function parse(value: string | view.Template, context: any): view.View {
|
||||||
if (types.isString(value)) {
|
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 {
|
function parseInternal(value: string, context: any, uri?: string): componentBuilder.ComponentModule {
|
||||||
var currentPage: page.Page;
|
|
||||||
var rootComponentModule: componentBuilder.ComponentModule;
|
var start: xml2ui.XmlStringParser;
|
||||||
// Temporary collection used for parent scope.
|
var ui: xml2ui.ComponentParser;
|
||||||
var parents = new Array<componentBuilder.ComponentModule>();
|
|
||||||
var complexProperties = new Array<ComplexProperty>();
|
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;
|
return ui.rootComponentModule;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCustomComponent(componentPath: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: page.Page): componentBuilder.ComponentModule {
|
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;
|
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 {
|
function getExports(instance: view.View): any {
|
||||||
var parent = instance.parent;
|
var parent = instance.parent;
|
||||||
|
|
||||||
@ -344,3 +150,400 @@ function getExports(instance: view.View): any {
|
|||||||
|
|
||||||
return parent ? (<any>parent).exports : undefined;
|
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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
ui/builder/template-builder.d.ts
vendored
28
ui/builder/template-builder.d.ts
vendored
@ -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>
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 + '"';
|
|
||||||
}
|
|
Reference in New Issue
Block a user