diff --git a/packages/core/bundle-entry-points.ts b/packages/core/bundle-entry-points.ts index cab50f044..41c31fb6c 100644 --- a/packages/core/bundle-entry-points.ts +++ b/packages/core/bundle-entry-points.ts @@ -4,7 +4,10 @@ import './globals'; // Register "dynamically" loaded module that need to be resolved by the // XML/component builders. import * as coreUIModules from './ui/index'; -global.registerModule('@nativescript/core/ui', () => coreUIModules); +if (__UI_USE_EXTERNAL_RENDERER__) { +} else { + global.registerModule('@nativescript/core/ui', () => coreUIModules); +} // global.registerModule('text/formatted-string', () => require('./text/formatted-string')); // global.registerModule('text/span', () => require('./text/span')); diff --git a/packages/core/css/CSS3Parser.ts b/packages/core/css/CSS3Parser.ts new file mode 100644 index 000000000..e5f17c296 --- /dev/null +++ b/packages/core/css/CSS3Parser.ts @@ -0,0 +1,698 @@ + +export interface Stylesheet { + rules: Rule[]; +} +export type Rule = QualifiedRule | AtRule; + +export interface AtRule { + type: 'at-rule'; + name: string; + prelude: InputToken[]; + block: SimpleBlock; +} +export interface QualifiedRule { + type: 'qualified-rule'; + prelude: InputToken[]; + block: SimpleBlock; +} + +// const nonQuoteURLRegEx = /(:?[^\)\s\t\n\r\f\'\"\(]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*/gym; // TODO: non-printable code points omitted + +export type InputToken = '(' | ')' | '{' | '}' | '[' | ']' | ':' | ';' | ',' | ' ' | '^=' | '|=' | '$=' | '*=' | '~=' | '' | undefined | /* */ InputTokenObject | FunctionInputToken | FunctionToken | SimpleBlock | AtKeywordToken; + +export const enum TokenObjectType { + /** + * + */ + string = 1, + /** + * + */ + delim = 2, + /** + * + */ + number = 3, + /** + * + */ + percentage = 4, + /** + * + */ + dimension = 5, + /** + * + */ + ident = 6, + /** + * + */ + url = 7, + /** + * + * This is a token indicating a function's leading: ( + */ + functionToken = 8, + /** + * + */ + simpleBlock = 9, + /** + * + */ + comment = 10, + /** + * + */ + atKeyword = 11, + /** + * + */ + hash = 12, + /** + * + * This is a complete consumed function: ([ [, ]*])")" + */ + function = 14, +} + +export interface InputTokenObject { + type: TokenObjectType; + text: string; +} + +/** + * This is a "(" token. + */ +export interface FunctionInputToken extends InputTokenObject { + name: string; +} + +/** + * This is a completely parsed function like "([component [, component]*])". + */ +export interface FunctionToken extends FunctionInputToken { + components: any[]; +} + +export interface SimpleBlock extends InputTokenObject { + associatedToken: InputToken; + values: InputToken[]; +} + +export type AtKeywordToken = InputTokenObject; + +const commentRegEx = /(\/\*(?:[^\*]|\*[^\/])*\*\/)/gmy; +// eslint-disable-next-line no-control-regex +const nameRegEx = /-?(?:(?:[a-zA-Z_]|[^\x00-\x7F]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))(?:[a-zA-Z_0-9\-]*|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)/gmy; +const numberRegEx = /[\+\-]?(?:\d+\.\d+|\d+|\.\d+)(?:[eE][\+\-]?\d+)?/gmy; +const doubleQuoteStringRegEx = /"((?:[^\n\r\f\"]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)(:?"|$)/gmy; // Besides $n, parse escape + +const whitespaceRegEx = /[\s\t\n\r\f]*/gmy; + +const singleQuoteStringRegEx = /'((?:[^\n\r\f\']|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)(:?'|$)/gmy; // Besides $n, parse escape + +/** + * CSS parser following relatively close: + * CSS Syntax Module Level 3 + * https://www.w3.org/TR/css-syntax-3/ + */ + +export class CSS3Parser { + private nextInputCodePointIndex = 0; + private reconsumedInputToken: InputToken; + private topLevelFlag: boolean; + + constructor(private text: string) { } + + /** + * For testing purposes. + * This method allows us to run and assert the proper working of the tokenizer. + */ + tokenize(): InputToken[] { + const tokens: InputToken[] = []; + let inputToken: InputToken; + do { + inputToken = this.consumeAToken(); + tokens.push(inputToken); + } while (inputToken); + + return tokens; + } + + /** + * 4.3.1. Consume a token + * https://www.w3.org/TR/css-syntax-3/#consume-a-token + */ + private consumeAToken(): InputToken { + if (this.reconsumedInputToken) { + const result = this.reconsumedInputToken; + this.reconsumedInputToken = null; + + return result; + } + const char = this.text[this.nextInputCodePointIndex]; + switch (char) { + case '"': + return this.consumeAStringToken(); + case "'": + return this.consumeAStringToken(); + case '(': + case ')': + case ',': + case ':': + case ';': + case '[': + case ']': + case '{': + case '}': + this.nextInputCodePointIndex++; + + return char; + case '#': + return this.consumeAHashToken() || this.consumeADelimToken(); + case ' ': + case '\t': + case '\n': + case '\r': + case '\f': + return this.consumeAWhitespace(); + case '@': + return this.consumeAtKeyword() || this.consumeADelimToken(); + // TODO: Only if this is valid escape, otherwise it is a parse error + case '\\': + return this.consumeAnIdentLikeToken() || this.consumeADelimToken(); + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return this.consumeANumericToken(); + case 'u': + case 'U': + if (this.text[this.nextInputCodePointIndex + 1] === '+') { + const thirdChar = this.text[this.nextInputCodePointIndex + 2]; + if ((thirdChar >= '0' && thirdChar <= '9') || thirdChar === '?') { + // TODO: Handle unicode stuff such as U+002B + throw new Error('Unicode tokens not supported!'); + } + } + + return this.consumeAnIdentLikeToken() || this.consumeADelimToken(); + case '$': + case '*': + case '^': + case '|': + case '~': + return this.consumeAMatchToken() || this.consumeADelimToken(); + case '-': + return this.consumeANumericToken() || this.consumeAnIdentLikeToken() || this.consumeCDC() || this.consumeADelimToken(); + case '+': + case '.': + return this.consumeANumericToken() || this.consumeADelimToken(); + case '/': + return this.consumeAComment() || this.consumeADelimToken(); + case '<': + return this.consumeCDO() || this.consumeADelimToken(); + case undefined: + return undefined; + default: + return this.consumeAnIdentLikeToken() || this.consumeADelimToken(); + } + } + + private consumeADelimToken(): InputToken { + return { + type: TokenObjectType.delim, + text: this.text[this.nextInputCodePointIndex++], + }; + } + + private consumeAWhitespace(): InputToken { + whitespaceRegEx.lastIndex = this.nextInputCodePointIndex; + whitespaceRegEx.exec(this.text); + this.nextInputCodePointIndex = whitespaceRegEx.lastIndex; + + return ' '; + } + + private consumeAHashToken(): InputTokenObject { + this.nextInputCodePointIndex++; + const hashName = this.consumeAName(); + if (hashName) { + return { type: TokenObjectType.hash, text: '#' + hashName.text }; + } + this.nextInputCodePointIndex--; + + return null; + } + + private consumeCDO(): '' | null { + if (this.text.substr(this.nextInputCodePointIndex, 3) === '-->') { + this.nextInputCodePointIndex += 3; + + return '-->'; + } + + return null; + } + + private consumeAMatchToken(): '*=' | '$=' | '|=' | '~=' | '^=' | null { + if (this.text[this.nextInputCodePointIndex + 1] === '=') { + const token = this.text.substr(this.nextInputCodePointIndex, 2); + this.nextInputCodePointIndex += 2; + + return <'*=' | '$=' | '|=' | '~=' | '^='>token; + } + + return null; + } + + /** + * 4.3.2. Consume a numeric token + * https://www.w3.org/TR/css-syntax-3/#consume-a-numeric-token + */ + private consumeANumericToken(): InputToken { + numberRegEx.lastIndex = this.nextInputCodePointIndex; + const result = numberRegEx.exec(this.text); + if (!result) { + return null; + } + this.nextInputCodePointIndex = numberRegEx.lastIndex; + if (this.text[this.nextInputCodePointIndex] === '%') { + return { type: TokenObjectType.percentage, text: result[0] }; // TODO: Push the actual number and unit here... + } + + const name = this.consumeAName(); + if (name) { + return { + type: TokenObjectType.dimension, + text: result[0] + name.text, + }; + } + + return { type: TokenObjectType.number, text: result[0] }; + } + + /** + * 4.3.3. Consume an ident-like token + * https://www.w3.org/TR/css-syntax-3/#consume-an-ident-like-token + */ + private consumeAnIdentLikeToken(): InputToken { + const name = this.consumeAName(); + if (!name) { + return null; + } + if (this.text[this.nextInputCodePointIndex] === '(') { + this.nextInputCodePointIndex++; + if (name.text.toLowerCase() === 'url') { + return this.consumeAURLToken(); + } + + return { + type: TokenObjectType.functionToken, + name: name.text, + text: name.text + '(', + }; + } + + return name; + } + + /** + * 4.3.4. Consume a string token + * https://www.w3.org/TR/css-syntax-3/#consume-a-string-token + */ + private consumeAStringToken(): InputTokenObject { + const char = this.text[this.nextInputCodePointIndex]; + let result: RegExpExecArray; + if (char === "'") { + singleQuoteStringRegEx.lastIndex = this.nextInputCodePointIndex; + result = singleQuoteStringRegEx.exec(this.text); + if (!result) { + return null; + } + this.nextInputCodePointIndex = singleQuoteStringRegEx.lastIndex; + } else if (char === '"') { + doubleQuoteStringRegEx.lastIndex = this.nextInputCodePointIndex; + result = doubleQuoteStringRegEx.exec(this.text); + if (!result) { + return null; + } + this.nextInputCodePointIndex = doubleQuoteStringRegEx.lastIndex; + } + + // TODO: Handle bad-string. + // TODO: Perform string escaping. + return { type: TokenObjectType.string, text: result[0] }; + } + + /** + * 4.3.5. Consume a url token + * https://www.w3.org/TR/css-syntax-3/#consume-a-url-token + */ + private consumeAURLToken(): InputToken { + const start = this.nextInputCodePointIndex - 3 /* url */ - 1; /* ( */ + const urlToken: InputToken = { + type: TokenObjectType.url, + text: undefined, + }; + this.consumeAWhitespace(); + if (this.nextInputCodePointIndex >= this.text.length) { + return urlToken; + } + const nextInputCodePoint = this.text[this.nextInputCodePointIndex]; + if (nextInputCodePoint === '"' || nextInputCodePoint === "'") { + const stringToken = this.consumeAStringToken(); + // TODO: Handle bad-string. + // TODO: Set value instead. + urlToken.text = stringToken.text; + this.consumeAWhitespace(); + if (this.text[this.nextInputCodePointIndex] === ')' || this.nextInputCodePointIndex >= this.text.length) { + this.nextInputCodePointIndex++; + const end = this.nextInputCodePointIndex; + urlToken.text = this.text.substring(start, end); + + return urlToken; + } else { + // TODO: Handle bad-url. + return null; + } + } + + while (this.nextInputCodePointIndex < this.text.length) { + const char = this.text[this.nextInputCodePointIndex++]; + switch (char) { + case ')': + return urlToken; + case ' ': + case '\t': + case '\n': + case '\r': + case '\f': + this.consumeAWhitespace(); + if (this.text[this.nextInputCodePointIndex] === ')') { + this.nextInputCodePointIndex++; + + return urlToken; + } else { + // TODO: Bar url! Consume remnants. + return null; + } + case '"': + case "'": + // TODO: Parse error! Bar url! Consume remnants. + return null; + case '\\': + // TODO: Escape! + throw new Error('Escaping not yet supported!'); + default: + // TODO: Non-printable chars - error. + urlToken.text += char; + } + } + + return urlToken; + } + + /** + * 4.3.11. Consume a name + * https://www.w3.org/TR/css-syntax-3/#consume-a-name + */ + private consumeAName(): InputTokenObject { + nameRegEx.lastIndex = this.nextInputCodePointIndex; + const result = nameRegEx.exec(this.text); + if (!result) { + return null; + } + this.nextInputCodePointIndex = nameRegEx.lastIndex; + + // TODO: Perform string escaping. + return { type: TokenObjectType.ident, text: result[0] }; + } + + private consumeAtKeyword(): InputTokenObject { + this.nextInputCodePointIndex++; + const name = this.consumeAName(); + if (name) { + return { type: TokenObjectType.atKeyword, text: name.text }; + } + this.nextInputCodePointIndex--; + + return null; + } + + private consumeAComment(): InputToken { + if (this.text[this.nextInputCodePointIndex + 1] === '*') { + commentRegEx.lastIndex = this.nextInputCodePointIndex; + const result = commentRegEx.exec(this.text); + if (!result) { + return null; // TODO: Handle + } + this.nextInputCodePointIndex = commentRegEx.lastIndex; + + // The CSS spec tokenizer does not emmit comment tokens + return this.consumeAToken(); + } + + return null; + } + + private reconsumeTheCurrentInputToken(currentInputToken: InputToken) { + this.reconsumedInputToken = currentInputToken; + } + + /** + * 5.3.1. Parse a stylesheet + * https://www.w3.org/TR/css-syntax-3/#parse-a-stylesheet + */ + public parseAStylesheet(): Stylesheet { + this.topLevelFlag = true; + return { + rules: this.consumeAListOfRules(), + }; + } + + /** + * 5.4.1. Consume a list of rules + * https://www.w3.org/TR/css-syntax-3/#consume-a-list-of-rules + */ + public consumeAListOfRules(): Rule[] { + const rules: Rule[] = []; + let inputToken: InputToken; + while ((inputToken = this.consumeAToken())) { + switch (inputToken) { + case ' ': + continue; + case '': { + if (this.topLevelFlag) { + continue; + } + this.reconsumeTheCurrentInputToken(inputToken); + const atRule = this.consumeAnAtRule(); + if (atRule) { + rules.push(atRule); + } + continue; + } + } + if ((inputToken).type === TokenObjectType.atKeyword) { + this.reconsumeTheCurrentInputToken(inputToken); + const atRule = this.consumeAnAtRule(); + if (atRule) { + rules.push(atRule); + } + continue; + } + this.reconsumeTheCurrentInputToken(inputToken); + const qualifiedRule = this.consumeAQualifiedRule(); + if (qualifiedRule) { + rules.push(qualifiedRule); + } + } + + return rules; + } + + /** + * 5.4.2. Consume an at-rule + * https://www.w3.org/TR/css-syntax-3/#consume-an-at-rule + */ + public consumeAnAtRule(): AtRule { + let inputToken = this.consumeAToken(); + const atRule: AtRule = { + type: 'at-rule', + name: (inputToken).text, + prelude: [], + block: undefined, + }; + while ((inputToken = this.consumeAToken())) { + if (inputToken === ';') { + return atRule; + } else if (inputToken === '{') { + atRule.block = this.consumeASimpleBlock(inputToken); + + return atRule; + } else if ((inputToken).type === TokenObjectType.simpleBlock && (inputToken).associatedToken === '{') { + atRule.block = inputToken; + + return atRule; + } + this.reconsumeTheCurrentInputToken(inputToken); + const component = this.consumeAComponentValue(); + if (component) { + atRule.prelude.push(component); + } + } + + return atRule; + } + + /** + * 5.4.3. Consume a qualified rule + * https://www.w3.org/TR/css-syntax-3/#consume-a-qualified-rule + */ + public consumeAQualifiedRule(): QualifiedRule { + const qualifiedRule: QualifiedRule = { + type: 'qualified-rule', + prelude: [], + block: undefined, + }; + let inputToken: InputToken; + while ((inputToken = this.consumeAToken())) { + if (inputToken === '{') { + qualifiedRule.block = this.consumeASimpleBlock(inputToken); + + return qualifiedRule; + } else if ((inputToken).type === TokenObjectType.simpleBlock) { + const simpleBlock: SimpleBlock = inputToken; + if (simpleBlock.associatedToken === '{') { + qualifiedRule.block = simpleBlock; + + return qualifiedRule; + } + } + this.reconsumeTheCurrentInputToken(inputToken); + const componentValue = this.consumeAComponentValue(); + if (componentValue) { + qualifiedRule.prelude.push(componentValue); + } + } + + // TODO: This is a parse error, log parse errors! + return null; + } + + /** + * 5.4.6. Consume a component value + * https://www.w3.org/TR/css-syntax-3/#consume-a-component-value + */ + private consumeAComponentValue(): InputToken { + // const inputToken = this.consumeAToken(); + const inputToken = this.consumeAToken(); + switch (inputToken) { + case '{': + case '[': + case '(': + this.nextInputCodePointIndex++; + + return this.consumeASimpleBlock(inputToken); + } + if (typeof inputToken === 'object' && inputToken.type === TokenObjectType.functionToken) { + return this.consumeAFunction((inputToken).name); + } + + return inputToken; + } + + /** + * 5.4.7. Consume a simple block + * https://www.w3.org/TR/css-syntax-3/#consume-a-simple-block + */ + private consumeASimpleBlock(associatedToken: InputToken): SimpleBlock { + const endianToken: ']' | '}' | ')' = { + '[': ']', + '{': '}', + '(': ')', + }[associatedToken]; + const start = this.nextInputCodePointIndex - 1; + const block: SimpleBlock = { + type: TokenObjectType.simpleBlock, + text: undefined, + associatedToken, + values: [], + }; + let nextInputToken; + while ((nextInputToken = this.text[this.nextInputCodePointIndex])) { + if (nextInputToken === endianToken) { + this.nextInputCodePointIndex++; + const end = this.nextInputCodePointIndex; + block.text = this.text.substring(start, end); + + return block; + } + const value = this.consumeAComponentValue(); + if (value) { + block.values.push(value); + } + } + block.text = this.text.substring(start); + + return block; + } + + /** + * 5.4.8. Consume a function + * https://www.w3.org/TR/css-syntax-3/#consume-a-function + */ + private consumeAFunction(name: string): InputToken { + const start = this.nextInputCodePointIndex; + const funcToken: FunctionToken = { + type: TokenObjectType.function, + name, + text: undefined, + components: [], + }; + do { + if (this.nextInputCodePointIndex >= this.text.length) { + funcToken.text = name + '(' + this.text.substring(start); + + return funcToken; + } + const nextInputToken = this.text[this.nextInputCodePointIndex]; + switch (nextInputToken) { + case ')': { + this.nextInputCodePointIndex++; + const end = this.nextInputCodePointIndex; + funcToken.text = name + '(' + this.text.substring(start, end); + + return funcToken; + } + default: { + const component = this.consumeAComponentValue(); + if (component) { + funcToken.components.push(component); + } + } + // TODO: Else we won't advance + } + } while (true); + } +} diff --git a/packages/core/css/CSSNativeScript.ts b/packages/core/css/CSSNativeScript.ts new file mode 100644 index 000000000..16c2c792e --- /dev/null +++ b/packages/core/css/CSSNativeScript.ts @@ -0,0 +1,122 @@ +import { InputToken, Stylesheet, Rule, AtRule, QualifiedRule } from './CSS3Parser'; + +/** + * Consume a CSS3 parsed stylesheet and convert the rules and selectors to the + * NativeScript internal JSON representation. + */ + +export class CSSNativeScript { + public parseStylesheet(stylesheet: Stylesheet): any { + return { + type: 'stylesheet', + stylesheet: { + rules: this.parseRules(stylesheet.rules), + }, + }; + } + + private parseRules(rules: Rule[]): any { + return rules.map((rule) => this.parseRule(rule)); + } + + private parseRule(rule: Rule): any { + if (rule.type === 'at-rule') { + return this.parseAtRule(rule); + } else if (rule.type === 'qualified-rule') { + return this.parseQualifiedRule(rule); + } + } + + private parseAtRule(rule: AtRule): any { + if (rule.name === 'import') { + // TODO: We have used an "@import { url('path somewhere'); }" at few places. + return { + import: rule.prelude + .map((m) => (typeof m === 'string' ? m : m.text)) + .join('') + .trim(), + type: 'import', + }; + } + + return; + } + + private parseQualifiedRule(rule: QualifiedRule): any { + return { + type: 'rule', + selectors: this.preludeToSelectorsStringArray(rule.prelude), + declarations: this.ruleBlockToDeclarations(rule.block.values), + }; + } + + private ruleBlockToDeclarations(declarationsInputTokens: InputToken[]): { type: 'declaration'; property: string; value: string; }[] { + // return declarationsInputTokens; + const declarations: { + type: 'declaration'; + property: string; + value: string; + }[] = []; + + let property = ''; + let value = ''; + let reading: 'property' | 'value' = 'property'; + + for (let i = 0; i < declarationsInputTokens.length; i++) { + const inputToken = declarationsInputTokens[i]; + if (reading === 'property') { + if (inputToken === ':') { + reading = 'value'; + } else if (typeof inputToken === 'string') { + property += inputToken; + } else { + property += inputToken.text; + } + } else { + if (inputToken === ';') { + property = property.trim(); + value = value.trim(); + declarations.push({ type: 'declaration', property, value }); + property = ''; + value = ''; + reading = 'property'; + } else if (typeof inputToken === 'string') { + value += inputToken; + } else { + value += inputToken.text; + } + } + } + property = property.trim(); + value = value.trim(); + if (property || value) { + declarations.push({ type: 'declaration', property, value }); + } + + return declarations; + } + + private preludeToSelectorsStringArray(prelude: InputToken[]): string[] { + const selectors = []; + let selector = ''; + prelude.forEach((inputToken) => { + if (typeof inputToken === 'string') { + if (inputToken === ',') { + if (selector) { + selectors.push(selector.trim()); + } + selector = ''; + } else { + selector += inputToken; + } + } else if (typeof inputToken === 'object') { + selector += inputToken.text; + } + }); + if (selector) { + selectors.push(selector.trim()); + } + + return selectors; + } +} diff --git a/packages/core/css/parser.ts b/packages/core/css/parser.ts index 98a46a633..95dfd4c94 100644 --- a/packages/core/css/parser.ts +++ b/packages/core/css/parser.ts @@ -830,819 +830,3 @@ export function parseSelector(text: string, start = 0): Parsed { return { start, end, value }; } - -export interface Stylesheet { - rules: Rule[]; -} -export type Rule = QualifiedRule | AtRule; - -export interface AtRule { - type: 'at-rule'; - name: string; - prelude: InputToken[]; - block: SimpleBlock; -} -export interface QualifiedRule { - type: 'qualified-rule'; - prelude: InputToken[]; - block: SimpleBlock; -} - -const whitespaceRegEx = /[\s\t\n\r\f]*/gmy; - -const singleQuoteStringRegEx = /'((?:[^\n\r\f\']|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)(:?'|$)/gmy; // Besides $n, parse escape -const doubleQuoteStringRegEx = /"((?:[^\n\r\f\"]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)(:?"|$)/gmy; // Besides $n, parse escape - -const commentRegEx = /(\/\*(?:[^\*]|\*[^\/])*\*\/)/gmy; -const numberRegEx = /[\+\-]?(?:\d+\.\d+|\d+|\.\d+)(?:[eE][\+\-]?\d+)?/gmy; -// eslint-disable-next-line no-control-regex -const nameRegEx = /-?(?:(?:[a-zA-Z_]|[^\x00-\x7F]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))(?:[a-zA-Z_0-9\-]*|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)/gmy; -// const nonQuoteURLRegEx = /(:?[^\)\s\t\n\r\f\'\"\(]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*/gym; // TODO: non-printable code points omitted - -type InputToken = '(' | ')' | '{' | '}' | '[' | ']' | ':' | ';' | ',' | ' ' | '^=' | '|=' | '$=' | '*=' | '~=' | '' | undefined | /* */ InputTokenObject | FunctionInputToken | FunctionToken | SimpleBlock | AtKeywordToken; - -export const enum TokenObjectType { - /** - * - */ - string = 1, - /** - * - */ - delim = 2, - /** - * - */ - number = 3, - /** - * - */ - percentage = 4, - /** - * - */ - dimension = 5, - /** - * - */ - ident = 6, - /** - * - */ - url = 7, - /** - * - * This is a token indicating a function's leading: ( - */ - functionToken = 8, - /** - * - */ - simpleBlock = 9, - /** - * - */ - comment = 10, - /** - * - */ - atKeyword = 11, - /** - * - */ - hash = 12, - /** - * - * This is a complete consumed function: ([ [, ]*])")" - */ - function = 14, -} - -interface InputTokenObject { - type: TokenObjectType; - text: string; -} - -/** - * This is a "(" token. - */ -interface FunctionInputToken extends InputTokenObject { - name: string; -} - -/** - * This is a completely parsed function like "([component [, component]*])". - */ -interface FunctionToken extends FunctionInputToken { - components: any[]; -} - -interface SimpleBlock extends InputTokenObject { - associatedToken: InputToken; - values: InputToken[]; -} - -type AtKeywordToken = InputTokenObject; - -/** - * CSS parser following relatively close: - * CSS Syntax Module Level 3 - * https://www.w3.org/TR/css-syntax-3/ - */ -export class CSS3Parser { - private nextInputCodePointIndex = 0; - private reconsumedInputToken: InputToken; - private topLevelFlag: boolean; - - constructor(private text: string) {} - - /** - * For testing purposes. - * This method allows us to run and assert the proper working of the tokenizer. - */ - tokenize(): InputToken[] { - const tokens: InputToken[] = []; - let inputToken: InputToken; - do { - inputToken = this.consumeAToken(); - tokens.push(inputToken); - } while (inputToken); - - return tokens; - } - - /** - * 4.3.1. Consume a token - * https://www.w3.org/TR/css-syntax-3/#consume-a-token - */ - private consumeAToken(): InputToken { - if (this.reconsumedInputToken) { - const result = this.reconsumedInputToken; - this.reconsumedInputToken = null; - - return result; - } - const char = this.text[this.nextInputCodePointIndex]; - switch (char) { - case '"': - return this.consumeAStringToken(); - case "'": - return this.consumeAStringToken(); - case '(': - case ')': - case ',': - case ':': - case ';': - case '[': - case ']': - case '{': - case '}': - this.nextInputCodePointIndex++; - - return char; - case '#': - return this.consumeAHashToken() || this.consumeADelimToken(); - case ' ': - case '\t': - case '\n': - case '\r': - case '\f': - return this.consumeAWhitespace(); - case '@': - return this.consumeAtKeyword() || this.consumeADelimToken(); - // TODO: Only if this is valid escape, otherwise it is a parse error - case '\\': - return this.consumeAnIdentLikeToken() || this.consumeADelimToken(); - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - return this.consumeANumericToken(); - case 'u': - case 'U': - if (this.text[this.nextInputCodePointIndex + 1] === '+') { - const thirdChar = this.text[this.nextInputCodePointIndex + 2]; - if ((thirdChar >= '0' && thirdChar <= '9') || thirdChar === '?') { - // TODO: Handle unicode stuff such as U+002B - throw new Error('Unicode tokens not supported!'); - } - } - - return this.consumeAnIdentLikeToken() || this.consumeADelimToken(); - case '$': - case '*': - case '^': - case '|': - case '~': - return this.consumeAMatchToken() || this.consumeADelimToken(); - case '-': - return this.consumeANumericToken() || this.consumeAnIdentLikeToken() || this.consumeCDC() || this.consumeADelimToken(); - case '+': - case '.': - return this.consumeANumericToken() || this.consumeADelimToken(); - case '/': - return this.consumeAComment() || this.consumeADelimToken(); - case '<': - return this.consumeCDO() || this.consumeADelimToken(); - case undefined: - return undefined; - default: - return this.consumeAnIdentLikeToken() || this.consumeADelimToken(); - } - } - - private consumeADelimToken(): InputToken { - return { - type: TokenObjectType.delim, - text: this.text[this.nextInputCodePointIndex++], - }; - } - - private consumeAWhitespace(): InputToken { - whitespaceRegEx.lastIndex = this.nextInputCodePointIndex; - whitespaceRegEx.exec(this.text); - this.nextInputCodePointIndex = whitespaceRegEx.lastIndex; - - return ' '; - } - - private consumeAHashToken(): InputTokenObject { - this.nextInputCodePointIndex++; - const hashName = this.consumeAName(); - if (hashName) { - return { type: TokenObjectType.hash, text: '#' + hashName.text }; - } - this.nextInputCodePointIndex--; - - return null; - } - - private consumeCDO(): '' | null { - if (this.text.substr(this.nextInputCodePointIndex, 3) === '-->') { - this.nextInputCodePointIndex += 3; - - return '-->'; - } - - return null; - } - - private consumeAMatchToken(): '*=' | '$=' | '|=' | '~=' | '^=' | null { - if (this.text[this.nextInputCodePointIndex + 1] === '=') { - const token = this.text.substr(this.nextInputCodePointIndex, 2); - this.nextInputCodePointIndex += 2; - - return <'*=' | '$=' | '|=' | '~=' | '^='>token; - } - - return null; - } - - /** - * 4.3.2. Consume a numeric token - * https://www.w3.org/TR/css-syntax-3/#consume-a-numeric-token - */ - private consumeANumericToken(): InputToken { - numberRegEx.lastIndex = this.nextInputCodePointIndex; - const result = numberRegEx.exec(this.text); - if (!result) { - return null; - } - this.nextInputCodePointIndex = numberRegEx.lastIndex; - if (this.text[this.nextInputCodePointIndex] === '%') { - return { type: TokenObjectType.percentage, text: result[0] }; // TODO: Push the actual number and unit here... - } - - const name = this.consumeAName(); - if (name) { - return { - type: TokenObjectType.dimension, - text: result[0] + name.text, - }; - } - - return { type: TokenObjectType.number, text: result[0] }; - } - - /** - * 4.3.3. Consume an ident-like token - * https://www.w3.org/TR/css-syntax-3/#consume-an-ident-like-token - */ - private consumeAnIdentLikeToken(): InputToken { - const name = this.consumeAName(); - if (!name) { - return null; - } - if (this.text[this.nextInputCodePointIndex] === '(') { - this.nextInputCodePointIndex++; - if (name.text.toLowerCase() === 'url') { - return this.consumeAURLToken(); - } - - return { - type: TokenObjectType.functionToken, - name: name.text, - text: name.text + '(', - }; - } - - return name; - } - - /** - * 4.3.4. Consume a string token - * https://www.w3.org/TR/css-syntax-3/#consume-a-string-token - */ - private consumeAStringToken(): InputTokenObject { - const char = this.text[this.nextInputCodePointIndex]; - let result: RegExpExecArray; - if (char === "'") { - singleQuoteStringRegEx.lastIndex = this.nextInputCodePointIndex; - result = singleQuoteStringRegEx.exec(this.text); - if (!result) { - return null; - } - this.nextInputCodePointIndex = singleQuoteStringRegEx.lastIndex; - } else if (char === '"') { - doubleQuoteStringRegEx.lastIndex = this.nextInputCodePointIndex; - result = doubleQuoteStringRegEx.exec(this.text); - if (!result) { - return null; - } - this.nextInputCodePointIndex = doubleQuoteStringRegEx.lastIndex; - } - - // TODO: Handle bad-string. - // TODO: Perform string escaping. - return { type: TokenObjectType.string, text: result[0] }; - } - - /** - * 4.3.5. Consume a url token - * https://www.w3.org/TR/css-syntax-3/#consume-a-url-token - */ - private consumeAURLToken(): InputToken { - const start = this.nextInputCodePointIndex - 3 /* url */ - 1; /* ( */ - const urlToken: InputToken = { - type: TokenObjectType.url, - text: undefined, - }; - this.consumeAWhitespace(); - if (this.nextInputCodePointIndex >= this.text.length) { - return urlToken; - } - const nextInputCodePoint = this.text[this.nextInputCodePointIndex]; - if (nextInputCodePoint === '"' || nextInputCodePoint === "'") { - const stringToken = this.consumeAStringToken(); - // TODO: Handle bad-string. - // TODO: Set value instead. - urlToken.text = stringToken.text; - this.consumeAWhitespace(); - if (this.text[this.nextInputCodePointIndex] === ')' || this.nextInputCodePointIndex >= this.text.length) { - this.nextInputCodePointIndex++; - const end = this.nextInputCodePointIndex; - urlToken.text = this.text.substring(start, end); - - return urlToken; - } else { - // TODO: Handle bad-url. - return null; - } - } - - while (this.nextInputCodePointIndex < this.text.length) { - const char = this.text[this.nextInputCodePointIndex++]; - switch (char) { - case ')': - return urlToken; - case ' ': - case '\t': - case '\n': - case '\r': - case '\f': - this.consumeAWhitespace(); - if (this.text[this.nextInputCodePointIndex] === ')') { - this.nextInputCodePointIndex++; - - return urlToken; - } else { - // TODO: Bar url! Consume remnants. - return null; - } - case '"': - case "'": - // TODO: Parse error! Bar url! Consume remnants. - return null; - case '\\': - // TODO: Escape! - throw new Error('Escaping not yet supported!'); - default: - // TODO: Non-printable chars - error. - urlToken.text += char; - } - } - - return urlToken; - } - - /** - * 4.3.11. Consume a name - * https://www.w3.org/TR/css-syntax-3/#consume-a-name - */ - private consumeAName(): InputTokenObject { - nameRegEx.lastIndex = this.nextInputCodePointIndex; - const result = nameRegEx.exec(this.text); - if (!result) { - return null; - } - this.nextInputCodePointIndex = nameRegEx.lastIndex; - - // TODO: Perform string escaping. - return { type: TokenObjectType.ident, text: result[0] }; - } - - private consumeAtKeyword(): InputTokenObject { - this.nextInputCodePointIndex++; - const name = this.consumeAName(); - if (name) { - return { type: TokenObjectType.atKeyword, text: name.text }; - } - this.nextInputCodePointIndex--; - - return null; - } - - private consumeAComment(): InputToken { - if (this.text[this.nextInputCodePointIndex + 1] === '*') { - commentRegEx.lastIndex = this.nextInputCodePointIndex; - const result = commentRegEx.exec(this.text); - if (!result) { - return null; // TODO: Handle - } - this.nextInputCodePointIndex = commentRegEx.lastIndex; - - // The CSS spec tokenizer does not emmit comment tokens - return this.consumeAToken(); - } - - return null; - } - - private reconsumeTheCurrentInputToken(currentInputToken: InputToken) { - this.reconsumedInputToken = currentInputToken; - } - - /** - * 5.3.1. Parse a stylesheet - * https://www.w3.org/TR/css-syntax-3/#parse-a-stylesheet - */ - public parseAStylesheet(): Stylesheet { - this.topLevelFlag = true; - return { - rules: this.consumeAListOfRules(), - }; - } - - /** - * 5.4.1. Consume a list of rules - * https://www.w3.org/TR/css-syntax-3/#consume-a-list-of-rules - */ - public consumeAListOfRules(): Rule[] { - const rules: Rule[] = []; - let inputToken: InputToken; - while ((inputToken = this.consumeAToken())) { - switch (inputToken) { - case ' ': - continue; - case '': { - if (this.topLevelFlag) { - continue; - } - this.reconsumeTheCurrentInputToken(inputToken); - const atRule = this.consumeAnAtRule(); - if (atRule) { - rules.push(atRule); - } - continue; - } - } - if ((inputToken).type === TokenObjectType.atKeyword) { - this.reconsumeTheCurrentInputToken(inputToken); - const atRule = this.consumeAnAtRule(); - if (atRule) { - rules.push(atRule); - } - continue; - } - this.reconsumeTheCurrentInputToken(inputToken); - const qualifiedRule = this.consumeAQualifiedRule(); - if (qualifiedRule) { - rules.push(qualifiedRule); - } - } - - return rules; - } - - /** - * 5.4.2. Consume an at-rule - * https://www.w3.org/TR/css-syntax-3/#consume-an-at-rule - */ - public consumeAnAtRule(): AtRule { - let inputToken = this.consumeAToken(); - const atRule: AtRule = { - type: 'at-rule', - name: (inputToken).text, - prelude: [], - block: undefined, - }; - while ((inputToken = this.consumeAToken())) { - if (inputToken === ';') { - return atRule; - } else if (inputToken === '{') { - atRule.block = this.consumeASimpleBlock(inputToken); - - return atRule; - } else if ((inputToken).type === TokenObjectType.simpleBlock && (inputToken).associatedToken === '{') { - atRule.block = inputToken; - - return atRule; - } - this.reconsumeTheCurrentInputToken(inputToken); - const component = this.consumeAComponentValue(); - if (component) { - atRule.prelude.push(component); - } - } - - return atRule; - } - - /** - * 5.4.3. Consume a qualified rule - * https://www.w3.org/TR/css-syntax-3/#consume-a-qualified-rule - */ - public consumeAQualifiedRule(): QualifiedRule { - const qualifiedRule: QualifiedRule = { - type: 'qualified-rule', - prelude: [], - block: undefined, - }; - let inputToken: InputToken; - while ((inputToken = this.consumeAToken())) { - if (inputToken === '{') { - qualifiedRule.block = this.consumeASimpleBlock(inputToken); - - return qualifiedRule; - } else if ((inputToken).type === TokenObjectType.simpleBlock) { - const simpleBlock: SimpleBlock = inputToken; - if (simpleBlock.associatedToken === '{') { - qualifiedRule.block = simpleBlock; - - return qualifiedRule; - } - } - this.reconsumeTheCurrentInputToken(inputToken); - const componentValue = this.consumeAComponentValue(); - if (componentValue) { - qualifiedRule.prelude.push(componentValue); - } - } - - // TODO: This is a parse error, log parse errors! - return null; - } - - /** - * 5.4.6. Consume a component value - * https://www.w3.org/TR/css-syntax-3/#consume-a-component-value - */ - private consumeAComponentValue(): InputToken { - // const inputToken = this.consumeAToken(); - const inputToken = this.consumeAToken(); - switch (inputToken) { - case '{': - case '[': - case '(': - this.nextInputCodePointIndex++; - - return this.consumeASimpleBlock(inputToken); - } - if (typeof inputToken === 'object' && inputToken.type === TokenObjectType.functionToken) { - return this.consumeAFunction((inputToken).name); - } - - return inputToken; - } - - /** - * 5.4.7. Consume a simple block - * https://www.w3.org/TR/css-syntax-3/#consume-a-simple-block - */ - private consumeASimpleBlock(associatedToken: InputToken): SimpleBlock { - const endianToken: ']' | '}' | ')' = { - '[': ']', - '{': '}', - '(': ')', - }[associatedToken]; - const start = this.nextInputCodePointIndex - 1; - const block: SimpleBlock = { - type: TokenObjectType.simpleBlock, - text: undefined, - associatedToken, - values: [], - }; - let nextInputToken; - while ((nextInputToken = this.text[this.nextInputCodePointIndex])) { - if (nextInputToken === endianToken) { - this.nextInputCodePointIndex++; - const end = this.nextInputCodePointIndex; - block.text = this.text.substring(start, end); - - return block; - } - const value = this.consumeAComponentValue(); - if (value) { - block.values.push(value); - } - } - block.text = this.text.substring(start); - - return block; - } - - /** - * 5.4.8. Consume a function - * https://www.w3.org/TR/css-syntax-3/#consume-a-function - */ - private consumeAFunction(name: string): InputToken { - const start = this.nextInputCodePointIndex; - const funcToken: FunctionToken = { - type: TokenObjectType.function, - name, - text: undefined, - components: [], - }; - do { - if (this.nextInputCodePointIndex >= this.text.length) { - funcToken.text = name + '(' + this.text.substring(start); - - return funcToken; - } - const nextInputToken = this.text[this.nextInputCodePointIndex]; - switch (nextInputToken) { - case ')': { - this.nextInputCodePointIndex++; - const end = this.nextInputCodePointIndex; - funcToken.text = name + '(' + this.text.substring(start, end); - - return funcToken; - } - default: { - const component = this.consumeAComponentValue(); - if (component) { - funcToken.components.push(component); - } - } - // TODO: Else we won't advance - } - } while (true); - } -} - -/** - * Consume a CSS3 parsed stylesheet and convert the rules and selectors to the - * NativeScript internal JSON representation. - */ -export class CSSNativeScript { - public parseStylesheet(stylesheet: Stylesheet): any { - return { - type: 'stylesheet', - stylesheet: { - rules: this.parseRules(stylesheet.rules), - }, - }; - } - - private parseRules(rules: Rule[]): any { - return rules.map((rule) => this.parseRule(rule)); - } - - private parseRule(rule: Rule): any { - if (rule.type === 'at-rule') { - return this.parseAtRule(rule); - } else if (rule.type === 'qualified-rule') { - return this.parseQualifiedRule(rule); - } - } - - private parseAtRule(rule: AtRule): any { - if (rule.name === 'import') { - // TODO: We have used an "@import { url('path somewhere'); }" at few places. - return { - import: rule.prelude - .map((m) => (typeof m === 'string' ? m : m.text)) - .join('') - .trim(), - type: 'import', - }; - } - - return; - } - - private parseQualifiedRule(rule: QualifiedRule): any { - return { - type: 'rule', - selectors: this.preludeToSelectorsStringArray(rule.prelude), - declarations: this.ruleBlockToDeclarations(rule.block.values), - }; - } - - private ruleBlockToDeclarations(declarationsInputTokens: InputToken[]): { type: 'declaration'; property: string; value: string }[] { - // return declarationsInputTokens; - const declarations: { - type: 'declaration'; - property: string; - value: string; - }[] = []; - - let property = ''; - let value = ''; - let reading: 'property' | 'value' = 'property'; - - for (let i = 0; i < declarationsInputTokens.length; i++) { - const inputToken = declarationsInputTokens[i]; - if (reading === 'property') { - if (inputToken === ':') { - reading = 'value'; - } else if (typeof inputToken === 'string') { - property += inputToken; - } else { - property += inputToken.text; - } - } else { - if (inputToken === ';') { - property = property.trim(); - value = value.trim(); - declarations.push({ type: 'declaration', property, value }); - property = ''; - value = ''; - reading = 'property'; - } else if (typeof inputToken === 'string') { - value += inputToken; - } else { - value += inputToken.text; - } - } - } - property = property.trim(); - value = value.trim(); - if (property || value) { - declarations.push({ type: 'declaration', property, value }); - } - - return declarations; - } - - private preludeToSelectorsStringArray(prelude: InputToken[]): string[] { - const selectors = []; - let selector = ''; - prelude.forEach((inputToken) => { - if (typeof inputToken === 'string') { - if (inputToken === ',') { - if (selector) { - selectors.push(selector.trim()); - } - selector = ''; - } else { - selector += inputToken; - } - } else if (typeof inputToken === 'object') { - selector += inputToken.text; - } - }); - if (selector) { - selectors.push(selector.trim()); - } - - return selectors; - } -} diff --git a/packages/core/global-types.d.ts b/packages/core/global-types.d.ts index 94aaf956e..fa03c4782 100644 --- a/packages/core/global-types.d.ts +++ b/packages/core/global-types.d.ts @@ -128,6 +128,13 @@ declare namespace NodeJS { rootLayout: any; } } +declare const __DEV__: string; +declare const __CSS_PARSER__: string; +declare const __NS_WEBPACK__: boolean; +declare const __UI_USE_EXTERNAL_RENDERER__: boolean; +declare const __UI_USE_XML_PARSER__: boolean; +declare const __ANDROID__: boolean; +declare const __IOS__: boolean; declare function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): number; declare function clearTimeout(timeoutId: number): void; diff --git a/packages/core/js-libs/easysax/package.json b/packages/core/js-libs/easysax/package.json index d87b54080..72f92b94a 100644 --- a/packages/core/js-libs/easysax/package.json +++ b/packages/core/js-libs/easysax/package.json @@ -1,6 +1,7 @@ { "name": "easysax", "description": "pure javascript xml parser", + "sideEffects": false, "keywords": [ "xml", "sax", diff --git a/packages/core/js-libs/esprima/package.json b/packages/core/js-libs/esprima/package.json index 1ef41b869..3ee3e4bd7 100644 --- a/packages/core/js-libs/esprima/package.json +++ b/packages/core/js-libs/esprima/package.json @@ -3,6 +3,7 @@ "description": "ECMAScript parsing infrastructure for multipurpose analysis", "homepage": "http://esprima.org", "main": "esprima", + "sideEffects": false, "types": "esprima.d.ts", "bin": { "esparse": "./bin/esparse.js", diff --git a/packages/core/js-libs/polymer-expressions/package.json b/packages/core/js-libs/polymer-expressions/package.json index d7dd8c857..099137b47 100644 --- a/packages/core/js-libs/polymer-expressions/package.json +++ b/packages/core/js-libs/polymer-expressions/package.json @@ -1,4 +1,5 @@ { + "sideEffects": false, "name": "polymer-expressions", "main": "polymer-expressions", "types": "polymer-expressions.d.ts" diff --git a/packages/core/ui/builder/index.ts b/packages/core/ui/builder/index.ts index e1b077017..315ff8825 100644 --- a/packages/core/ui/builder/index.ts +++ b/packages/core/ui/builder/index.ts @@ -5,19 +5,18 @@ import { ViewEntry } from '../frame'; import { Page } from '../page'; // Types. -import { debug, ScopeError, SourceError, Source } from '../../utils/debug'; -import * as xml from '../../xml'; -import { isString, isObject, isDefined } from '../../utils/types'; +import { debug } from '../../utils/debug'; +import { isDefined } from '../../utils/types'; import { setPropertyValue, getComponentModule } from './component-builder'; import type { ComponentModule } from './component-builder'; -import { platformNames, Device } from '../../platform'; -import { profile } from '../../profiling'; +import { platformNames } from '../../platform'; import { sanitizeModuleName } from './module-name-sanitizer'; import { resolveModuleName } from '../../module-name-resolver'; +import { xml2ui } from './xml2ui'; -const ios = platformNames.ios.toLowerCase(); -const android = platformNames.android.toLowerCase(); -const defaultNameSpaceMatcher = /tns\.xsd$/i; +export const ios = platformNames.ios.toLowerCase(); +export const android = platformNames.android.toLowerCase(); +export const defaultNameSpaceMatcher = /tns\.xsd$/i; export interface LoadOptions { path: string; @@ -147,7 +146,7 @@ function loadInternal(moduleName: string, moduleExports: any): ComponentModule { return componentModule; } -function loadCustomComponent(componentNamespace: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: View, isRootComponent = true, moduleNamePath?: string): ComponentModule { +export function loadCustomComponent(componentNamespace: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: View, isRootComponent = true, moduleNamePath?: string): ComponentModule { if (!parentPage && context) { // Read the parent page that was passed down below // https://github.com/NativeScript/NativeScript/issues/1639 @@ -207,7 +206,7 @@ function loadCustomComponent(componentNamespace: string, componentName?: string, return result; } -function getExports(instance: ViewBase): any { +export function getExports(instance: ViewBase): any { const isView = !!instance._domId; if (!isView) { return (instance).exports || instance; @@ -224,525 +223,26 @@ function getExports(instance: ViewBase): any { } function parseInternal(value: string, context: any, xmlModule?: string, moduleName?: string): ComponentModule { - let start: xml2ui.XmlStringParser; - let ui: xml2ui.ComponentParser; - - const errorFormat = debug && xmlModule ? xml2ui.SourceErrorFormat(xmlModule) : xml2ui.PositionErrorFormat; - const componentSourceTracker = - debug && xmlModule - ? xml2ui.ComponentSourceTracker(xmlModule) - : () => { - // no-op - }; - - (start = new xml2ui.XmlStringParser(errorFormat)).pipe(new xml2ui.PlatformFilter()).pipe(new xml2ui.XmlStateParser((ui = new xml2ui.ComponentParser(context, errorFormat, componentSourceTracker, moduleName)))); - - start.parse(value); - - return ui.rootComponentModule; -} - -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) { - 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; - } - - 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 { - const context = this._context; - const errorFormat = this._templateProperty.errorFormat; - const sourceTracker = this._templateProperty.sourceTracker; - const template: Template =