import { View, Template, KeyedTemplate } from '../core/view'; import { ScopeError, SourceError, Source } from '../../utils/debug'; import * as xml from '../../xml'; import { isString, isObject } from '../../utils/types'; import { getComponentModule } from './component-builder'; import type { ComponentModule } from './component-builder'; import { Device } from '../../platform'; import { profile } from '../../profiling'; import { android, ios, visionos, loadCustomComponent, defaultNameSpaceMatcher, getExports, Builder } from './index'; export namespace xml2ui { /** * Pipes and filters: * https://en.wikipedia.org/wiki/Pipeline_(software) */ interface XmlProducer { pipe(next: Next): Next; } interface XmlConsumer { parse(args: xml.ParserEvent); } interface ParseInputData extends String { default?: string; } export class XmlProducerBase implements XmlProducer { private _next: XmlConsumer; public pipe(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: ParseInputData) { if (__UI_USE_XML_PARSER__) { const 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 (isString(value)) { xmlParser.parse(value); } else if (isObject(value) && isString(value.default)) { xmlParser.parse(value.default); } } } } interface ErrorFormatter { (e: Error, p: xml.Position): Error; } export function PositionErrorFormat(e: Error, p: xml.Position): Error { return new ScopeError(e, 'Parsing XML at ' + p.line + ':' + p.column); } export function SourceErrorFormat(uri): ErrorFormatter { return (e: Error, p: xml.Position) => { const source = p ? new Source(uri, p.line, p.column) : new Source(uri, -1, -1); e = new SourceError(e, source, 'Building UI from XML.'); return e; }; } interface SourceTracker { (component: any, p: xml.Position): void; } export function ComponentSourceTracker(uri): SourceTracker { return (component: any, p: xml.Position) => { if (!Source.get(component)) { const source = p ? new Source(uri, p.line, p.column) : new Source(uri, -1, -1); Source.set(component, source); } }; } 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 { if (value) { const toLower = value.toLowerCase(); return toLower === android || toLower === ios || toLower === visionos; } return false; } private static isCurentPlatform(value: string): boolean { return value && value.toLowerCase() === 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: ComponentModule; name: string; elementName: string; templateItems: Array; errorFormat: ErrorFormatter; sourceTracker: SourceTracker; } /** * 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; private _templateProperty: TemplateProperty; private _nestingLevel: number; private _state: TemplateParser.State; private parent: XmlStateConsumer; private _setTemplateProperty: boolean; constructor(parent: XmlStateConsumer, templateProperty: TemplateProperty, setTemplateProperty = true) { this.parent = parent; this._context = templateProperty.context; this._recordedXmlStream = new Array(); this._templateProperty = templateProperty; this._nestingLevel = 0; this._state = TemplateParser.State.EXPECTING_START; this._setTemplateProperty = setTemplateProperty; } 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; if (this._setTemplateProperty && this._templateProperty.name in this._templateProperty.parent.component) { const template = this.buildTemplate(); this._templateProperty.parent.component[this._templateProperty.name] = template; } } } public buildTemplate(): Template { if (__UI_USE_XML_PARSER__) { const context = this._context; const errorFormat = this._templateProperty.errorFormat; const sourceTracker = this._templateProperty.sourceTracker; const template: Template =