mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat(core): make css parsers tree-shakable (#9496)
This commit is contained in:
committed by
Nathan Walker
parent
3e2e5dfe9d
commit
dd5f24a737
@@ -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'));
|
||||
|
||||
698
packages/core/css/CSS3Parser.ts
Normal file
698
packages/core/css/CSS3Parser.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
122
packages/core/css/CSSNativeScript.ts
Normal file
122
packages/core/css/CSSNativeScript.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -830,819 +830,3 @@ export function parseSelector(text: string, start = 0): Parsed<Selector> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
7
packages/core/global-types.d.ts
vendored
7
packages/core/global-types.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "easysax",
|
||||
"description": "pure javascript xml parser",
|
||||
"sideEffects": false,
|
||||
"keywords": [
|
||||
"xml",
|
||||
"sax",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"sideEffects": false,
|
||||
"name": "polymer-expressions",
|
||||
"main": "polymer-expressions",
|
||||
"types": "polymer-expressions.d.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 (<any>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 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 };
|
||||
}
|
||||
if (__UI_USE_XML_PARSER__) {
|
||||
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;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
520
packages/core/ui/builder/xml2ui.ts
Normal file
520
packages/core/ui/builder/xml2ui.ts
Normal 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; };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,7 +343,8 @@ export class Binding {
|
||||
}
|
||||
|
||||
let newValue = value;
|
||||
if (this.options.expression) {
|
||||
if (__UI_USE_EXTERNAL_RENDERER__) {
|
||||
} else if (this.options.expression) {
|
||||
const changedModel = {};
|
||||
changedModel[bc.bindingValueKey] = value;
|
||||
changedModel[bc.newPropertyValueKey] = value;
|
||||
@@ -373,38 +374,40 @@ export class Binding {
|
||||
}
|
||||
|
||||
private _getExpressionValue(expression: string, isBackConvert: boolean, changedModel: any): any {
|
||||
try {
|
||||
const exp = PolymerExpressions.getExpression(expression);
|
||||
if (exp) {
|
||||
const context = (this.source && this.source.get && this.source.get()) || global;
|
||||
const model = {};
|
||||
const addedProps = [];
|
||||
const resources = bindableResources.get();
|
||||
for (const prop in resources) {
|
||||
if (resources.hasOwnProperty(prop) && !context.hasOwnProperty(prop)) {
|
||||
context[prop] = resources[prop];
|
||||
addedProps.push(prop);
|
||||
if (!__UI_USE_EXTERNAL_RENDERER__) {
|
||||
try {
|
||||
const exp = PolymerExpressions.getExpression(expression);
|
||||
if (exp) {
|
||||
const context = (this.source && this.source.get && this.source.get()) || global;
|
||||
const model = {};
|
||||
const addedProps = [];
|
||||
const resources = bindableResources.get();
|
||||
for (const prop in resources) {
|
||||
if (resources.hasOwnProperty(prop) && !context.hasOwnProperty(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);
|
||||
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 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 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) {
|
||||
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 (__UI_USE_EXTERNAL_RENDERER__ || !this.options.expression) {
|
||||
if (changedPropertyIndex > -1) {
|
||||
const props = sourceProps.slice(changedPropertyIndex + 1);
|
||||
const propsLength = props.length;
|
||||
@@ -443,6 +439,13 @@ export class Binding {
|
||||
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.
|
||||
@@ -516,7 +519,8 @@ export class Binding {
|
||||
}
|
||||
|
||||
private getSourcePropertyValue() {
|
||||
if (this.options.expression) {
|
||||
if (__UI_USE_EXTERNAL_RENDERER__) {
|
||||
} else if (this.options.expression) {
|
||||
const changedModel = {};
|
||||
changedModel[bc.bindingValueKey] = this.source ? this.source.get() : undefined;
|
||||
const expressionValue = this._getExpressionValue(this.options.expression, false, changedModel);
|
||||
|
||||
@@ -27,7 +27,8 @@ export abstract class ListViewBase extends ContainerView implements ListViewDefi
|
||||
public _defaultTemplate: KeyedTemplate = {
|
||||
key: 'default',
|
||||
createView: () => {
|
||||
if (this.itemTemplate) {
|
||||
if (__UI_USE_EXTERNAL_RENDERER__) {
|
||||
} else if (this.itemTemplate) {
|
||||
return Builder.parse(this.itemTemplate, this);
|
||||
}
|
||||
|
||||
@@ -196,7 +197,11 @@ export const itemTemplatesProperty = new Property<ListViewBase, string | Array<K
|
||||
name: 'itemTemplates',
|
||||
valueConverter: (value) => {
|
||||
if (typeof value === 'string') {
|
||||
return Builder.parseMultipleTemplates(value, null);
|
||||
if (__UI_USE_XML_PARSER__) {
|
||||
return Builder.parseMultipleTemplates(value, null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
@@ -129,7 +129,11 @@ export class Repeater extends CustomLayoutView {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -223,7 +227,11 @@ export const itemTemplatesProperty = new Property<Repeater, string | Array<Keyed
|
||||
affectsLayout: true,
|
||||
valueConverter: (value) => {
|
||||
if (typeof value === 'string') {
|
||||
return Builder.parseMultipleTemplates(value, null);
|
||||
if (__UI_USE_XML_PARSER__) {
|
||||
return Builder.parseMultipleTemplates(value, null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
@@ -218,23 +218,19 @@ class CSSSource {
|
||||
@profile
|
||||
private parseCSSAst() {
|
||||
if (this._source) {
|
||||
switch (parser) {
|
||||
case 'css-tree':
|
||||
this._ast = cssTreeParse(this._source, this._file);
|
||||
|
||||
return;
|
||||
case 'nativescript': {
|
||||
const cssparser = new CSS3Parser(this._source);
|
||||
const stylesheet = cssparser.parseAStylesheet();
|
||||
const cssNS = new CSSNativeScript();
|
||||
this._ast = cssNS.parseStylesheet(stylesheet);
|
||||
|
||||
return;
|
||||
}
|
||||
case 'rework':
|
||||
this._ast = parseCss(this._source, { source: this._file });
|
||||
|
||||
return;
|
||||
if (__CSS_PARSER__ === 'css-tree') {
|
||||
const cssTreeParse = require('../../css/css-tree-parser').cssTreeParse;
|
||||
this._ast = cssTreeParse(this._source, this._file);
|
||||
} else if (__CSS_PARSER__ === 'nativescript') {
|
||||
const CSS3Parser = require('../../css/CSS3Parser').CSS3Parser;
|
||||
const CSSNativeScript = require('../../css/CSSNativeScript').CSSNativeScript;
|
||||
const cssparser = new CSS3Parser(this._source);
|
||||
const stylesheet = cssparser.parseAStylesheet();
|
||||
const cssNS = new CSSNativeScript();
|
||||
this._ast = cssNS.parseStylesheet(stylesheet);
|
||||
} else if (__CSS_PARSER__ === 'rework') {
|
||||
const parseCss = require('../../css').parse;
|
||||
this._ast = parseCss(this._source, { source: this._file });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// https://github.com/NativeScript/nativescript-dev-webpack/issues/932
|
||||
|
||||
import * as definition from '.';
|
||||
const easysax = require('../js-libs/easysax');
|
||||
import { EasySAXParser } from '../js-libs/easysax';
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,9 +3,7 @@ import Config from 'webpack-chain';
|
||||
import svelte from '../../src/configuration/svelte';
|
||||
import { init } from '../../src';
|
||||
|
||||
jest.mock('__jest__/svelte.config.js', () => {
|
||||
|
||||
}, { virtual: true })
|
||||
jest.mock('__jest__/svelte.config.js', () => {}, { virtual: true });
|
||||
|
||||
describe('svelte configuration', () => {
|
||||
const platforms = ['ios', 'android'];
|
||||
|
||||
@@ -3,5 +3,5 @@ import { copyRules, additionalCopyRules } from '../src/helpers/copyRules';
|
||||
afterEach(() => {
|
||||
// Clear copy rules
|
||||
copyRules.clear();
|
||||
additionalCopyRules.length = 0
|
||||
additionalCopyRules.length = 0;
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import { applyFileReplacements } from '../helpers/fileReplacements';
|
||||
import { addCopyRule, applyCopyRules } from '../helpers/copyRules';
|
||||
import { WatchStatePlugin } from '../plugins/WatchStatePlugin';
|
||||
import { getProjectFilePath } from '../helpers/project';
|
||||
import { projectUsesCustomFlavor } from '../helpers/flavor';
|
||||
import { hasDependency } from '../helpers/dependencies';
|
||||
import { applyDotEnvPlugin } from '../helpers/dotEnv';
|
||||
import { env as _env, IWebpackEnv } from '../index';
|
||||
@@ -352,6 +353,8 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
|
||||
__NS_DEV_HOST_IPS__:
|
||||
mode === 'development' ? JSON.stringify(getIPS()) : `[]`,
|
||||
__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
|
||||
__UI_USE_XML_PARSER__: true,
|
||||
__UI_USE_EXTERNAL_RENDERER__: projectUsesCustomFlavor(),
|
||||
__ANDROID__: platform === 'android',
|
||||
__IOS__: platform === 'ios',
|
||||
/* for compat only */ 'global.isAndroid': platform === 'android',
|
||||
|
||||
@@ -2,6 +2,28 @@ import { defaultConfigs } from '@nativescript/webpack';
|
||||
import { getAllDependencies } from './dependencies';
|
||||
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
|
||||
* (vue, angular, react, svelete, typescript, javascript...)
|
||||
|
||||
@@ -9,7 +9,7 @@ import { addVirtualEntry, addVirtualModule } from './virtualModules';
|
||||
import { applyFileReplacements } from './fileReplacements';
|
||||
import { addCopyRule, removeCopyRule } from './copyRules';
|
||||
import { error, info, warn, warnOnce } from './log';
|
||||
import { determineProjectFlavor } from './flavor';
|
||||
import { determineProjectFlavor, projectUsesCustomFlavor } from './flavor';
|
||||
import { getValue } from './config';
|
||||
import { getIPS } from './host';
|
||||
import {
|
||||
@@ -47,6 +47,7 @@ export default {
|
||||
},
|
||||
flavor: {
|
||||
determineProjectFlavor,
|
||||
projectUsesCustomFlavor,
|
||||
},
|
||||
host: {
|
||||
getIPS,
|
||||
|
||||
Reference in New Issue
Block a user