feat(core): make css parsers tree-shakable (#9496)

This commit is contained in:
farfromrefuge
2021-08-10 22:12:16 +02:00
committed by Nathan Walker
parent 3e2e5dfe9d
commit dd5f24a737
20 changed files with 1484 additions and 1411 deletions

View File

@ -4,7 +4,10 @@ import './globals';
// Register "dynamically" loaded module that need to be resolved by the // Register "dynamically" loaded module that need to be resolved by the
// XML/component builders. // XML/component builders.
import * as coreUIModules from './ui/index'; 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/formatted-string', () => require('./text/formatted-string'));
// global.registerModule('text/span', () => require('./text/span')); // global.registerModule('text/span', () => require('./text/span'));

View File

@ -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 | /* <EOF-token> */ InputTokenObject | FunctionInputToken | FunctionToken | SimpleBlock | AtKeywordToken;
export const enum TokenObjectType {
/**
* <string-token>
*/
string = 1,
/**
* <delim-token>
*/
delim = 2,
/**
* <number-token>
*/
number = 3,
/**
* <percentage-token>
*/
percentage = 4,
/**
* <dimension-token>
*/
dimension = 5,
/**
* <ident-token>
*/
ident = 6,
/**
* <url-token>
*/
url = 7,
/**
* <function-token>
* This is a token indicating a function's leading: <ident-token>(
*/
functionToken = 8,
/**
* <simple-block>
*/
simpleBlock = 9,
/**
* <comment-token>
*/
comment = 10,
/**
* <at-keyword-token>
*/
atKeyword = 11,
/**
* <hash-token>
*/
hash = 12,
/**
* <function>
* This is a complete consumed function: <function-token>([<component-value> [, <component-value>]*])")"
*/
function = 14,
}
export interface InputTokenObject {
type: TokenObjectType;
text: string;
}
/**
* This is a "<ident>(" token.
*/
export interface FunctionInputToken extends InputTokenObject {
name: string;
}
/**
* This is a completely parsed function like "<ident>([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 <any>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, 4) === '<!--') {
this.nextInputCodePointIndex += 4;
return '<!--';
}
return null;
}
private consumeCDC(): '-->' | 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 <FunctionInputToken>{
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 <bad-comment>
}
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 '<!--':
case '-->': {
if (this.topLevelFlag) {
continue;
}
this.reconsumeTheCurrentInputToken(inputToken);
const atRule = this.consumeAnAtRule();
if (atRule) {
rules.push(atRule);
}
continue;
}
}
if ((<InputTokenObject>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: (<AtKeywordToken>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 ((<InputTokenObject>inputToken).type === TokenObjectType.simpleBlock && (<SimpleBlock>inputToken).associatedToken === '{') {
atRule.block = <SimpleBlock>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 ((<InputTokenObject>inputToken).type === TokenObjectType.simpleBlock) {
const simpleBlock: 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((<FunctionInputToken>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: ']' | '}' | ')' = {
'[': ']',
'{': '}',
'(': ')',
}[<any>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);
}
}

View File

@ -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 <any>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;
}
}

View File

@ -830,819 +830,3 @@ export function parseSelector(text: string, start = 0): Parsed<Selector> {
return { start, end, value }; 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 | /* <EOF-token> */ InputTokenObject | FunctionInputToken | FunctionToken | SimpleBlock | AtKeywordToken;
export const enum TokenObjectType {
/**
* <string-token>
*/
string = 1,
/**
* <delim-token>
*/
delim = 2,
/**
* <number-token>
*/
number = 3,
/**
* <percentage-token>
*/
percentage = 4,
/**
* <dimension-token>
*/
dimension = 5,
/**
* <ident-token>
*/
ident = 6,
/**
* <url-token>
*/
url = 7,
/**
* <function-token>
* This is a token indicating a function's leading: <ident-token>(
*/
functionToken = 8,
/**
* <simple-block>
*/
simpleBlock = 9,
/**
* <comment-token>
*/
comment = 10,
/**
* <at-keyword-token>
*/
atKeyword = 11,
/**
* <hash-token>
*/
hash = 12,
/**
* <function>
* This is a complete consumed function: <function-token>([<component-value> [, <component-value>]*])")"
*/
function = 14,
}
interface InputTokenObject {
type: TokenObjectType;
text: string;
}
/**
* This is a "<ident>(" token.
*/
interface FunctionInputToken extends InputTokenObject {
name: string;
}
/**
* This is a completely parsed function like "<ident>([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 <any>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, 4) === '<!--') {
this.nextInputCodePointIndex += 4;
return '<!--';
}
return null;
}
private consumeCDC(): '-->' | 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 <FunctionInputToken>{
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 <bad-comment>
}
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 '<!--':
case '-->': {
if (this.topLevelFlag) {
continue;
}
this.reconsumeTheCurrentInputToken(inputToken);
const atRule = this.consumeAnAtRule();
if (atRule) {
rules.push(atRule);
}
continue;
}
}
if ((<InputTokenObject>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: (<AtKeywordToken>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 ((<InputTokenObject>inputToken).type === TokenObjectType.simpleBlock && (<SimpleBlock>inputToken).associatedToken === '{') {
atRule.block = <SimpleBlock>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 ((<InputTokenObject>inputToken).type === TokenObjectType.simpleBlock) {
const simpleBlock: 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((<FunctionInputToken>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: ']' | '}' | ')' = {
'[': ']',
'{': '}',
'(': ')',
}[<any>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 <any>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;
}
}

View File

@ -128,6 +128,13 @@ declare namespace NodeJS {
rootLayout: any; 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 setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): number;
declare function clearTimeout(timeoutId: number): void; declare function clearTimeout(timeoutId: number): void;

View File

@ -1,6 +1,7 @@
{ {
"name": "easysax", "name": "easysax",
"description": "pure javascript xml parser", "description": "pure javascript xml parser",
"sideEffects": false,
"keywords": [ "keywords": [
"xml", "xml",
"sax", "sax",

View File

@ -3,6 +3,7 @@
"description": "ECMAScript parsing infrastructure for multipurpose analysis", "description": "ECMAScript parsing infrastructure for multipurpose analysis",
"homepage": "http://esprima.org", "homepage": "http://esprima.org",
"main": "esprima", "main": "esprima",
"sideEffects": false,
"types": "esprima.d.ts", "types": "esprima.d.ts",
"bin": { "bin": {
"esparse": "./bin/esparse.js", "esparse": "./bin/esparse.js",

View File

@ -1,4 +1,5 @@
{ {
"sideEffects": false,
"name": "polymer-expressions", "name": "polymer-expressions",
"main": "polymer-expressions", "main": "polymer-expressions",
"types": "polymer-expressions.d.ts" "types": "polymer-expressions.d.ts"

View File

@ -5,19 +5,18 @@ import { ViewEntry } from '../frame';
import { Page } from '../page'; import { Page } from '../page';
// Types. // Types.
import { debug, ScopeError, SourceError, Source } from '../../utils/debug'; import { debug } from '../../utils/debug';
import * as xml from '../../xml'; import { isDefined } from '../../utils/types';
import { isString, isObject, isDefined } from '../../utils/types';
import { setPropertyValue, getComponentModule } from './component-builder'; import { setPropertyValue, getComponentModule } from './component-builder';
import type { ComponentModule } from './component-builder'; import type { ComponentModule } from './component-builder';
import { platformNames, Device } from '../../platform'; import { platformNames } from '../../platform';
import { profile } from '../../profiling';
import { sanitizeModuleName } from './module-name-sanitizer'; import { sanitizeModuleName } from './module-name-sanitizer';
import { resolveModuleName } from '../../module-name-resolver'; import { resolveModuleName } from '../../module-name-resolver';
import { xml2ui } from './xml2ui';
const ios = platformNames.ios.toLowerCase(); export const ios = platformNames.ios.toLowerCase();
const android = platformNames.android.toLowerCase(); export const android = platformNames.android.toLowerCase();
const defaultNameSpaceMatcher = /tns\.xsd$/i; export const defaultNameSpaceMatcher = /tns\.xsd$/i;
export interface LoadOptions { export interface LoadOptions {
path: string; path: string;
@ -147,7 +146,7 @@ function loadInternal(moduleName: string, moduleExports: any): ComponentModule {
return 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) { if (!parentPage && context) {
// Read the parent page that was passed down below // Read the parent page that was passed down below
// https://github.com/NativeScript/NativeScript/issues/1639 // https://github.com/NativeScript/NativeScript/issues/1639
@ -207,7 +206,7 @@ function loadCustomComponent(componentNamespace: string, componentName?: string,
return result; return result;
} }
function getExports(instance: ViewBase): any { export function getExports(instance: ViewBase): any {
const isView = !!instance._domId; const isView = !!instance._domId;
if (!isView) { if (!isView) {
return (<any>instance).exports || instance; return (<any>instance).exports || instance;
@ -224,525 +223,26 @@ function getExports(instance: ViewBase): any {
} }
function parseInternal(value: string, context: any, xmlModule?: string, moduleName?: string): ComponentModule { function parseInternal(value: string, context: any, xmlModule?: string, moduleName?: string): ComponentModule {
let start: xml2ui.XmlStringParser; if (__UI_USE_XML_PARSER__) {
let ui: xml2ui.ComponentParser; let start: xml2ui.XmlStringParser;
let ui: xml2ui.ComponentParser;
const errorFormat = debug && xmlModule ? xml2ui.SourceErrorFormat(xmlModule) : xml2ui.PositionErrorFormat;
const componentSourceTracker = const errorFormat = debug && xmlModule ? xml2ui.SourceErrorFormat(xmlModule) : xml2ui.PositionErrorFormat;
debug && xmlModule const componentSourceTracker =
? xml2ui.ComponentSourceTracker(xmlModule) debug && xmlModule
: () => { ? xml2ui.ComponentSourceTracker(xmlModule)
// no-op : () => {
}; // no-op
};
(start = new xml2ui.XmlStringParser(errorFormat)).pipe(new xml2ui.PlatformFilter()).pipe(new xml2ui.XmlStateParser((ui = new xml2ui.ComponentParser(context, errorFormat, componentSourceTracker, moduleName))));
(start = new xml2ui.XmlStringParser(errorFormat)).pipe(new xml2ui.PlatformFilter()).pipe(new xml2ui.XmlStateParser((ui = new xml2ui.ComponentParser(context, errorFormat, componentSourceTracker, moduleName))));
start.parse(value);
start.parse(value);
return ui.rootComponentModule;
} return ui.rootComponentModule;
} else {
namespace xml2ui { return null;
/**
* 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);
}
interface ParseInputData extends String {
default?: string;
}
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: 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(<string>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<string>;
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<xml.ParserEvent>;
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<xml.ParserEvent>();
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 = <Template>profile('Template()', () => {
let start: xml2ui.XmlArgsReplay;
let ui: xml2ui.ComponentParser;
(start = new xml2ui.XmlArgsReplay(this._recordedXmlStream, errorFormat))
// No platform filter, it has been filtered already
.pipe(new XmlStateParser((ui = new ComponentParser(context, errorFormat, sourceTracker))));
start.replay();
return ui.rootComponentModule.component;
});
return template;
}
}
export class MultiTemplateParser implements XmlStateConsumer {
private _childParsers = new Array<TemplateParser>();
private _value: KeyedTemplate[];
get value(): KeyedTemplate[] {
return this._value;
}
constructor(private parent: XmlStateConsumer, private templateProperty: TemplateProperty) {}
public parse(args: xml.ParserEvent): XmlStateConsumer {
if (args.eventType === xml.ParserEventType.StartElement && args.elementName === 'template') {
const childParser = new TemplateParser(this, this.templateProperty, false);
childParser['key'] = args.attributes['key'];
this._childParsers.push(childParser);
return childParser;
}
if (args.eventType === xml.ParserEventType.EndElement) {
const name = ComponentParser.getComplexPropertyName(args.elementName);
if (name === this.templateProperty.name) {
const templates = new Array<KeyedTemplate>();
for (let i = 0; i < this._childParsers.length; i++) {
templates.push({
key: this._childParsers[i]['key'],
createView: this._childParsers[i].buildTemplate(),
});
}
this._value = templates;
return this.parent.parse(args);
}
}
return this;
}
}
export namespace TemplateParser {
export const enum State {
EXPECTING_START,
PARSING,
FINISHED,
}
}
export class ComponentParser implements XmlStateConsumer {
private static KNOWNCOLLECTIONS = 'knownCollections';
private static KNOWNTEMPLATES = 'knownTemplates';
private static KNOWNMULTITEMPLATES = 'knownMultiTemplates';
public rootComponentModule: ComponentModule;
private context: any;
private currentRootView: View;
private parents = new Array<ComponentModule>();
private complexProperties = new Array<ComponentParser.ComplexProperty>();
private error: ErrorFormatter;
private sourceTracker: SourceTracker;
constructor(context: any, errorFormat: ErrorFormatter, sourceTracker: SourceTracker, private moduleName?: string) {
this.context = context;
this.error = errorFormat;
this.sourceTracker = sourceTracker;
}
@profile
private buildComponent(args: xml.ParserEvent): ComponentModule {
if (args.prefix && args.namespace) {
// Custom components
return loadCustomComponent(args.namespace, args.elementName, args.attributes, this.context, this.currentRootView, !this.currentRootView, this.moduleName);
} else {
// Default components
let namespace = args.namespace;
if (defaultNameSpaceMatcher.test(namespace || '')) {
//Ignore the default ...tns.xsd namespace URL
namespace = undefined;
}
return getComponentModule(args.elementName, namespace, args.attributes, this.context, this.moduleName, !this.currentRootView);
}
}
public parse(args: xml.ParserEvent): XmlStateConsumer {
// Get the current parent.
const parent = this.parents[this.parents.length - 1];
const 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)) {
const name = ComponentParser.getComplexPropertyName(args.elementName);
const complexProperty: ComponentParser.ComplexProperty = {
parent: parent,
name: name,
items: [],
};
this.complexProperties.push(complexProperty);
if (ComponentParser.isKnownTemplate(name, parent.exports)) {
return new TemplateParser(this, {
context: (parent ? getExports(parent.component) : null) || this.context, // Passing 'context' won't work if you set "codeFile" on the page
parent: parent,
name: name,
elementName: args.elementName,
templateItems: [],
errorFormat: this.error,
sourceTracker: this.sourceTracker,
});
}
if (ComponentParser.isKnownMultiTemplate(name, parent.exports)) {
const parser = new MultiTemplateParser(this, {
context: (parent ? getExports(parent.component) : null) || this.context, // Passing 'context' won't work if you set "codeFile" on the page
parent: parent,
name: name,
elementName: args.elementName,
templateItems: [],
errorFormat: this.error,
sourceTracker: this.sourceTracker,
});
complexProperty.parser = parser;
return parser;
}
} else {
const componentModule = this.buildComponent(args);
if (componentModule) {
this.sourceTracker(componentModule.component, args.position);
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.currentRootView = this.rootComponentModule.component;
if ((<any>this.currentRootView).exports) {
this.context = (<any>this.currentRootView).exports;
}
}
}
// 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 (complexProperty.parser) {
parent.component[complexProperty.name] = complexProperty.parser.value;
} else 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 isString(name) && name.indexOf('.') !== -1;
}
public static getComplexPropertyName(fullName: string): string {
let name: string;
if (isString(fullName)) {
const names = fullName.split('.');
name = names[names.length - 1];
}
return name;
}
private static isKnownTemplate(name: string, exports: any): boolean {
return Builder.knownTemplates.has(name);
}
private static isKnownMultiTemplate(name: string, exports: any): boolean {
return Builder.knownMultiTemplates.has(name);
}
private static addToComplexProperty(parent: ComponentModule, complexProperty: ComponentParser.ComplexProperty, elementModule: ComponentModule) {
// If property name is known collection we populate array with elements.
const 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 Builder.knownCollections.has(name);
}
}
export namespace ComponentParser {
export interface ComplexProperty {
parent: ComponentModule;
name: string;
items?: Array<any>;
parser?: { value: any };
}
} }
} }

View File

@ -0,0 +1,520 @@
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 { ComponentModule } from './component-builder';
import { Device } from '../../platform';
import { profile } from '../../profiling';
import { android, ios, loadCustomComponent, defaultNameSpaceMatcher, getExports, Builder } from './index';
export 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);
}
interface ParseInputData extends String {
default?: string;
}
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: 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(<string>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<string>;
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<xml.ParserEvent>;
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<xml.ParserEvent>();
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 = <Template>profile('Template()', () => {
let start: xml2ui.XmlArgsReplay;
let ui: xml2ui.ComponentParser;
(start = new xml2ui.XmlArgsReplay(this._recordedXmlStream, errorFormat))
// No platform filter, it has been filtered already
.pipe(new XmlStateParser((ui = new ComponentParser(context, errorFormat, sourceTracker))));
start.replay();
return ui.rootComponentModule.component;
});
return template;
} else {
return null;
}
}
}
export class MultiTemplateParser implements XmlStateConsumer {
private _childParsers = new Array<TemplateParser>();
private _value: KeyedTemplate[];
get value(): KeyedTemplate[] {
return this._value;
}
constructor(private parent: XmlStateConsumer, private templateProperty: TemplateProperty) { }
public parse(args: xml.ParserEvent): XmlStateConsumer {
if (args.eventType === xml.ParserEventType.StartElement && args.elementName === 'template') {
const childParser = new TemplateParser(this, this.templateProperty, false);
childParser['key'] = args.attributes['key'];
this._childParsers.push(childParser);
return childParser;
}
if (args.eventType === xml.ParserEventType.EndElement) {
const name = ComponentParser.getComplexPropertyName(args.elementName);
if (name === this.templateProperty.name) {
const templates = new Array<KeyedTemplate>();
for (let i = 0; i < this._childParsers.length; i++) {
templates.push({
key: this._childParsers[i]['key'],
createView: this._childParsers[i].buildTemplate(),
});
}
this._value = templates;
return this.parent.parse(args);
}
}
return this;
}
}
export namespace TemplateParser {
export const enum State {
EXPECTING_START,
PARSING,
FINISHED
}
}
export class ComponentParser implements XmlStateConsumer {
private static KNOWNCOLLECTIONS = 'knownCollections';
private static KNOWNTEMPLATES = 'knownTemplates';
private static KNOWNMULTITEMPLATES = 'knownMultiTemplates';
public rootComponentModule: ComponentModule;
private context: any;
private currentRootView: View;
private parents = new Array<ComponentModule>();
private complexProperties = new Array<ComponentParser.ComplexProperty>();
private error: ErrorFormatter;
private sourceTracker: SourceTracker;
constructor(context: any, errorFormat: ErrorFormatter, sourceTracker: SourceTracker, private moduleName?: string) {
this.context = context;
this.error = errorFormat;
this.sourceTracker = sourceTracker;
}
@profile
private buildComponent(args: xml.ParserEvent): ComponentModule {
if (args.prefix && args.namespace) {
// Custom components
return loadCustomComponent(args.namespace, args.elementName, args.attributes, this.context, this.currentRootView, !this.currentRootView, this.moduleName);
} else {
// Default components
let namespace = args.namespace;
if (defaultNameSpaceMatcher.test(namespace || '')) {
//Ignore the default ...tns.xsd namespace URL
namespace = undefined;
}
return getComponentModule(args.elementName, namespace, args.attributes, this.context, this.moduleName, !this.currentRootView);
}
}
public parse(args: xml.ParserEvent): XmlStateConsumer {
// Get the current parent.
const parent = this.parents[this.parents.length - 1];
const 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)) {
const name = ComponentParser.getComplexPropertyName(args.elementName);
const complexProperty: ComponentParser.ComplexProperty = {
parent: parent,
name: name,
items: [],
};
this.complexProperties.push(complexProperty);
if (ComponentParser.isKnownTemplate(name, parent.exports)) {
return new TemplateParser(this, {
context: (parent ? getExports(parent.component) : null) || this.context,
parent: parent,
name: name,
elementName: args.elementName,
templateItems: [],
errorFormat: this.error,
sourceTracker: this.sourceTracker,
});
}
if (ComponentParser.isKnownMultiTemplate(name, parent.exports)) {
const parser = new MultiTemplateParser(this, {
context: (parent ? getExports(parent.component) : null) || this.context,
parent: parent,
name: name,
elementName: args.elementName,
templateItems: [],
errorFormat: this.error,
sourceTracker: this.sourceTracker,
});
complexProperty.parser = parser;
return parser;
}
} else {
const componentModule = this.buildComponent(args);
if (componentModule) {
this.sourceTracker(componentModule.component, args.position);
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.currentRootView = this.rootComponentModule.component;
if ((<any>this.currentRootView).exports) {
this.context = (<any>this.currentRootView).exports;
}
}
}
// 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 (complexProperty.parser) {
parent.component[complexProperty.name] = complexProperty.parser.value;
} else 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 isString(name) && name.indexOf('.') !== -1;
}
public static getComplexPropertyName(fullName: string): string {
let name: string;
if (isString(fullName)) {
const names = fullName.split('.');
name = names[names.length - 1];
}
return name;
}
private static isKnownTemplate(name: string, exports: any): boolean {
return Builder.knownTemplates.has(name);
}
private static isKnownMultiTemplate(name: string, exports: any): boolean {
return Builder.knownMultiTemplates.has(name);
}
private static addToComplexProperty(parent: ComponentModule, complexProperty: ComponentParser.ComplexProperty, elementModule: ComponentModule) {
// If property name is known collection we populate array with elements.
const 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 Builder.knownCollections.has(name);
}
}
export namespace ComponentParser {
export interface ComplexProperty {
parent: ComponentModule;
name: string;
items?: Array<any>;
parser?: { value: any; };
}
}
}

View File

@ -343,7 +343,8 @@ export class Binding {
} }
let newValue = value; let newValue = value;
if (this.options.expression) { if (__UI_USE_EXTERNAL_RENDERER__) {
} else if (this.options.expression) {
const changedModel = {}; const changedModel = {};
changedModel[bc.bindingValueKey] = value; changedModel[bc.bindingValueKey] = value;
changedModel[bc.newPropertyValueKey] = value; changedModel[bc.newPropertyValueKey] = value;
@ -373,38 +374,40 @@ export class Binding {
} }
private _getExpressionValue(expression: string, isBackConvert: boolean, changedModel: any): any { private _getExpressionValue(expression: string, isBackConvert: boolean, changedModel: any): any {
try { if (!__UI_USE_EXTERNAL_RENDERER__) {
const exp = PolymerExpressions.getExpression(expression); try {
if (exp) { const exp = PolymerExpressions.getExpression(expression);
const context = (this.source && this.source.get && this.source.get()) || global; if (exp) {
const model = {}; const context = (this.source && this.source.get && this.source.get()) || global;
const addedProps = []; const model = {};
const resources = bindableResources.get(); const addedProps = [];
for (const prop in resources) { const resources = bindableResources.get();
if (resources.hasOwnProperty(prop) && !context.hasOwnProperty(prop)) { for (const prop in resources) {
context[prop] = resources[prop]; if (resources.hasOwnProperty(prop) && !context.hasOwnProperty(prop)) {
addedProps.push(prop); context[prop] = resources[prop];
addedProps.push(prop);
}
} }
this.prepareContextForExpression(context, expression, addedProps);
model[contextKey] = context;
const result = exp.getValue(model, isBackConvert, changedModel ? changedModel : model);
// clear added props
const addedPropsLength = addedProps.length;
for (let i = 0; i < addedPropsLength; i++) {
delete context[addedProps[i]];
}
addedProps.length = 0;
return result;
} }
this.prepareContextForExpression(context, expression, addedProps); return new Error(expression + ' is not a valid expression.');
model[contextKey] = context; } catch (e) {
const result = exp.getValue(model, isBackConvert, changedModel ? changedModel : model); const errorMessage = 'Run-time error occured in file: ' + e.sourceURL + ' at line: ' + e.line + ' and column: ' + e.column;
// clear added props
const addedPropsLength = addedProps.length;
for (let i = 0; i < addedPropsLength; i++) {
delete context[addedProps[i]];
}
addedProps.length = 0;
return result; return new Error(errorMessage);
} }
return new Error(expression + ' is not a valid expression.');
} catch (e) {
const errorMessage = 'Run-time error occured in file: ' + e.sourceURL + ' at line: ' + e.line + ' and column: ' + e.column;
return new Error(errorMessage);
} }
} }
@ -421,14 +424,7 @@ export class Binding {
} }
} }
if (this.options.expression) { if (__UI_USE_EXTERNAL_RENDERER__ || !this.options.expression) {
const expressionValue = this._getExpressionValue(this.options.expression, false, undefined);
if (expressionValue instanceof Error) {
Trace.write(expressionValue.message, Trace.categories.Binding, Trace.messageType.error);
} else {
this.updateTarget(expressionValue);
}
} else {
if (changedPropertyIndex > -1) { if (changedPropertyIndex > -1) {
const props = sourceProps.slice(changedPropertyIndex + 1); const props = sourceProps.slice(changedPropertyIndex + 1);
const propsLength = props.length; const propsLength = props.length;
@ -443,6 +439,13 @@ export class Binding {
this.updateTarget(data.value); this.updateTarget(data.value);
} }
} }
} else {
const expressionValue = this._getExpressionValue(this.options.expression, false, undefined);
if (expressionValue instanceof Error) {
Trace.write(expressionValue.message, Trace.categories.Binding, Trace.messageType.error);
} else {
this.updateTarget(expressionValue);
}
} }
// we need to do this only if nested objects are used as source and some middle object has changed. // we need to do this only if nested objects are used as source and some middle object has changed.
@ -516,7 +519,8 @@ export class Binding {
} }
private getSourcePropertyValue() { private getSourcePropertyValue() {
if (this.options.expression) { if (__UI_USE_EXTERNAL_RENDERER__) {
} else if (this.options.expression) {
const changedModel = {}; const changedModel = {};
changedModel[bc.bindingValueKey] = this.source ? this.source.get() : undefined; changedModel[bc.bindingValueKey] = this.source ? this.source.get() : undefined;
const expressionValue = this._getExpressionValue(this.options.expression, false, changedModel); const expressionValue = this._getExpressionValue(this.options.expression, false, changedModel);

View File

@ -27,7 +27,8 @@ export abstract class ListViewBase extends ContainerView implements ListViewDefi
public _defaultTemplate: KeyedTemplate = { public _defaultTemplate: KeyedTemplate = {
key: 'default', key: 'default',
createView: () => { createView: () => {
if (this.itemTemplate) { if (__UI_USE_EXTERNAL_RENDERER__) {
} else if (this.itemTemplate) {
return Builder.parse(this.itemTemplate, this); return Builder.parse(this.itemTemplate, this);
} }
@ -196,7 +197,11 @@ export const itemTemplatesProperty = new Property<ListViewBase, string | Array<K
name: 'itemTemplates', name: 'itemTemplates',
valueConverter: (value) => { valueConverter: (value) => {
if (typeof value === 'string') { if (typeof value === 'string') {
return Builder.parseMultipleTemplates(value, null); if (__UI_USE_XML_PARSER__) {
return Builder.parseMultipleTemplates(value, null);
} else {
return null;
}
} }
return value; return value;

View File

@ -129,7 +129,11 @@ export class Repeater extends CustomLayoutView {
} }
if (!viewToAdd) { if (!viewToAdd) {
viewToAdd = this.itemTemplate ? Builder.parse(this.itemTemplate, this) : this._getDefaultItemContent(i); if (__UI_USE_EXTERNAL_RENDERER__) {
viewToAdd = this._getDefaultItemContent(i)
} else {
viewToAdd = this.itemTemplate ? Builder.parse(this.itemTemplate, this) : this._getDefaultItemContent(i);
}
} }
viewToAdd.bindingContext = dataItem; viewToAdd.bindingContext = dataItem;
@ -223,7 +227,11 @@ export const itemTemplatesProperty = new Property<Repeater, string | Array<Keyed
affectsLayout: true, affectsLayout: true,
valueConverter: (value) => { valueConverter: (value) => {
if (typeof value === 'string') { if (typeof value === 'string') {
return Builder.parseMultipleTemplates(value, null); if (__UI_USE_XML_PARSER__) {
return Builder.parseMultipleTemplates(value, null);
} else {
return null;
}
} }
return value; return value;

View File

@ -218,23 +218,19 @@ class CSSSource {
@profile @profile
private parseCSSAst() { private parseCSSAst() {
if (this._source) { if (this._source) {
switch (parser) { if (__CSS_PARSER__ === 'css-tree') {
case 'css-tree': const cssTreeParse = require('../../css/css-tree-parser').cssTreeParse;
this._ast = cssTreeParse(this._source, this._file); this._ast = cssTreeParse(this._source, this._file);
} else if (__CSS_PARSER__ === 'nativescript') {
return; const CSS3Parser = require('../../css/CSS3Parser').CSS3Parser;
case 'nativescript': { const CSSNativeScript = require('../../css/CSSNativeScript').CSSNativeScript;
const cssparser = new CSS3Parser(this._source); const cssparser = new CSS3Parser(this._source);
const stylesheet = cssparser.parseAStylesheet(); const stylesheet = cssparser.parseAStylesheet();
const cssNS = new CSSNativeScript(); const cssNS = new CSSNativeScript();
this._ast = cssNS.parseStylesheet(stylesheet); this._ast = cssNS.parseStylesheet(stylesheet);
} else if (__CSS_PARSER__ === 'rework') {
return; const parseCss = require('../../css').parse;
} this._ast = parseCss(this._source, { source: this._file });
case 'rework':
this._ast = parseCss(this._source, { source: this._file });
return;
} }
} }
} }

View File

@ -2,7 +2,6 @@
// https://github.com/NativeScript/nativescript-dev-webpack/issues/932 // https://github.com/NativeScript/nativescript-dev-webpack/issues/932
import * as definition from '.'; import * as definition from '.';
const easysax = require('../js-libs/easysax');
import { EasySAXParser } from '../js-libs/easysax'; import { EasySAXParser } from '../js-libs/easysax';
/** /**

View File

@ -3,9 +3,7 @@ import Config from 'webpack-chain';
import svelte from '../../src/configuration/svelte'; import svelte from '../../src/configuration/svelte';
import { init } from '../../src'; import { init } from '../../src';
jest.mock('__jest__/svelte.config.js', () => { jest.mock('__jest__/svelte.config.js', () => {}, { virtual: true });
}, { virtual: true })
describe('svelte configuration', () => { describe('svelte configuration', () => {
const platforms = ['ios', 'android']; const platforms = ['ios', 'android'];

View File

@ -3,5 +3,5 @@ import { copyRules, additionalCopyRules } from '../src/helpers/copyRules';
afterEach(() => { afterEach(() => {
// Clear copy rules // Clear copy rules
copyRules.clear(); copyRules.clear();
additionalCopyRules.length = 0 additionalCopyRules.length = 0;
}); });

View File

@ -15,6 +15,7 @@ import { applyFileReplacements } from '../helpers/fileReplacements';
import { addCopyRule, applyCopyRules } from '../helpers/copyRules'; import { addCopyRule, applyCopyRules } from '../helpers/copyRules';
import { WatchStatePlugin } from '../plugins/WatchStatePlugin'; import { WatchStatePlugin } from '../plugins/WatchStatePlugin';
import { getProjectFilePath } from '../helpers/project'; import { getProjectFilePath } from '../helpers/project';
import { projectUsesCustomFlavor } from '../helpers/flavor';
import { hasDependency } from '../helpers/dependencies'; import { hasDependency } from '../helpers/dependencies';
import { applyDotEnvPlugin } from '../helpers/dotEnv'; import { applyDotEnvPlugin } from '../helpers/dotEnv';
import { env as _env, IWebpackEnv } from '../index'; import { env as _env, IWebpackEnv } from '../index';
@ -352,6 +353,8 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
__NS_DEV_HOST_IPS__: __NS_DEV_HOST_IPS__:
mode === 'development' ? JSON.stringify(getIPS()) : `[]`, mode === 'development' ? JSON.stringify(getIPS()) : `[]`,
__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')), __CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
__UI_USE_XML_PARSER__: true,
__UI_USE_EXTERNAL_RENDERER__: projectUsesCustomFlavor(),
__ANDROID__: platform === 'android', __ANDROID__: platform === 'android',
__IOS__: platform === 'ios', __IOS__: platform === 'ios',
/* for compat only */ 'global.isAndroid': platform === 'android', /* for compat only */ 'global.isAndroid': platform === 'android',

View File

@ -2,6 +2,28 @@ import { defaultConfigs } from '@nativescript/webpack';
import { getAllDependencies } from './dependencies'; import { getAllDependencies } from './dependencies';
import { error } from './log'; import { error } from './log';
/**
* Utility to determine the project flavor based on installed dependencies
* (vue, angular, react, svelete, typescript, javascript...)
*/
export function projectUsesCustomFlavor(): boolean {
const dependencies = getAllDependencies();
return [
'vue',
'angular',
'react',
'svelte'
].includes(determineProjectFlavor())
if (dependencies.includes('nativescript-vue') ||
dependencies.includes('@nativescript/angular') ||
dependencies.includes('react-nativescript') ||
dependencies.includes('svelte-native')
) {
return true;
}
return false;
}
/** /**
* Utility to determine the project flavor based on installed dependencies * Utility to determine the project flavor based on installed dependencies
* (vue, angular, react, svelete, typescript, javascript...) * (vue, angular, react, svelete, typescript, javascript...)

View File

@ -9,7 +9,7 @@ import { addVirtualEntry, addVirtualModule } from './virtualModules';
import { applyFileReplacements } from './fileReplacements'; import { applyFileReplacements } from './fileReplacements';
import { addCopyRule, removeCopyRule } from './copyRules'; import { addCopyRule, removeCopyRule } from './copyRules';
import { error, info, warn, warnOnce } from './log'; import { error, info, warn, warnOnce } from './log';
import { determineProjectFlavor } from './flavor'; import { determineProjectFlavor, projectUsesCustomFlavor } from './flavor';
import { getValue } from './config'; import { getValue } from './config';
import { getIPS } from './host'; import { getIPS } from './host';
import { import {
@ -47,6 +47,7 @@ export default {
}, },
flavor: { flavor: {
determineProjectFlavor, determineProjectFlavor,
projectUsesCustomFlavor,
}, },
host: { host: {
getIPS, getIPS,