mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 11:01:21 +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
|
// Register "dynamically" loaded module that need to be resolved by the
|
||||||
// XML/component builders.
|
// XML/component builders.
|
||||||
import * as coreUIModules from './ui/index';
|
import * as coreUIModules from './ui/index';
|
||||||
global.registerModule('@nativescript/core/ui', () => coreUIModules);
|
if (__UI_USE_EXTERNAL_RENDERER__) {
|
||||||
|
} else {
|
||||||
|
global.registerModule('@nativescript/core/ui', () => coreUIModules);
|
||||||
|
}
|
||||||
|
|
||||||
// global.registerModule('text/formatted-string', () => require('./text/formatted-string'));
|
// global.registerModule('text/formatted-string', () => require('./text/formatted-string'));
|
||||||
// global.registerModule('text/span', () => require('./text/span'));
|
// global.registerModule('text/span', () => require('./text/span'));
|
||||||
|
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 };
|
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;
|
rootLayout: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
declare const __DEV__: string;
|
||||||
|
declare const __CSS_PARSER__: string;
|
||||||
|
declare const __NS_WEBPACK__: boolean;
|
||||||
|
declare const __UI_USE_EXTERNAL_RENDERER__: boolean;
|
||||||
|
declare const __UI_USE_XML_PARSER__: boolean;
|
||||||
|
declare const __ANDROID__: boolean;
|
||||||
|
declare const __IOS__: boolean;
|
||||||
|
|
||||||
declare function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): number;
|
declare function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): number;
|
||||||
declare function clearTimeout(timeoutId: number): void;
|
declare function clearTimeout(timeoutId: number): void;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "easysax",
|
"name": "easysax",
|
||||||
"description": "pure javascript xml parser",
|
"description": "pure javascript xml parser",
|
||||||
|
"sideEffects": false,
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"xml",
|
"xml",
|
||||||
"sax",
|
"sax",
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"description": "ECMAScript parsing infrastructure for multipurpose analysis",
|
"description": "ECMAScript parsing infrastructure for multipurpose analysis",
|
||||||
"homepage": "http://esprima.org",
|
"homepage": "http://esprima.org",
|
||||||
"main": "esprima",
|
"main": "esprima",
|
||||||
|
"sideEffects": false,
|
||||||
"types": "esprima.d.ts",
|
"types": "esprima.d.ts",
|
||||||
"bin": {
|
"bin": {
|
||||||
"esparse": "./bin/esparse.js",
|
"esparse": "./bin/esparse.js",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"sideEffects": false,
|
||||||
"name": "polymer-expressions",
|
"name": "polymer-expressions",
|
||||||
"main": "polymer-expressions",
|
"main": "polymer-expressions",
|
||||||
"types": "polymer-expressions.d.ts"
|
"types": "polymer-expressions.d.ts"
|
||||||
|
@ -5,19 +5,18 @@ import { ViewEntry } from '../frame';
|
|||||||
import { Page } from '../page';
|
import { Page } from '../page';
|
||||||
|
|
||||||
// Types.
|
// Types.
|
||||||
import { debug, ScopeError, SourceError, Source } from '../../utils/debug';
|
import { debug } from '../../utils/debug';
|
||||||
import * as xml from '../../xml';
|
import { isDefined } from '../../utils/types';
|
||||||
import { isString, isObject, isDefined } from '../../utils/types';
|
|
||||||
import { setPropertyValue, getComponentModule } from './component-builder';
|
import { setPropertyValue, getComponentModule } from './component-builder';
|
||||||
import type { ComponentModule } from './component-builder';
|
import type { ComponentModule } from './component-builder';
|
||||||
import { platformNames, Device } from '../../platform';
|
import { platformNames } from '../../platform';
|
||||||
import { profile } from '../../profiling';
|
|
||||||
import { sanitizeModuleName } from './module-name-sanitizer';
|
import { sanitizeModuleName } from './module-name-sanitizer';
|
||||||
import { resolveModuleName } from '../../module-name-resolver';
|
import { resolveModuleName } from '../../module-name-resolver';
|
||||||
|
import { xml2ui } from './xml2ui';
|
||||||
|
|
||||||
const ios = platformNames.ios.toLowerCase();
|
export const ios = platformNames.ios.toLowerCase();
|
||||||
const android = platformNames.android.toLowerCase();
|
export const android = platformNames.android.toLowerCase();
|
||||||
const defaultNameSpaceMatcher = /tns\.xsd$/i;
|
export const defaultNameSpaceMatcher = /tns\.xsd$/i;
|
||||||
|
|
||||||
export interface LoadOptions {
|
export interface LoadOptions {
|
||||||
path: string;
|
path: string;
|
||||||
@ -147,7 +146,7 @@ function loadInternal(moduleName: string, moduleExports: any): ComponentModule {
|
|||||||
return componentModule;
|
return componentModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCustomComponent(componentNamespace: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: View, isRootComponent = true, moduleNamePath?: string): ComponentModule {
|
export function loadCustomComponent(componentNamespace: string, componentName?: string, attributes?: Object, context?: Object, parentPage?: View, isRootComponent = true, moduleNamePath?: string): ComponentModule {
|
||||||
if (!parentPage && context) {
|
if (!parentPage && context) {
|
||||||
// Read the parent page that was passed down below
|
// Read the parent page that was passed down below
|
||||||
// https://github.com/NativeScript/NativeScript/issues/1639
|
// https://github.com/NativeScript/NativeScript/issues/1639
|
||||||
@ -207,7 +206,7 @@ function loadCustomComponent(componentNamespace: string, componentName?: string,
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExports(instance: ViewBase): any {
|
export function getExports(instance: ViewBase): any {
|
||||||
const isView = !!instance._domId;
|
const isView = !!instance._domId;
|
||||||
if (!isView) {
|
if (!isView) {
|
||||||
return (<any>instance).exports || instance;
|
return (<any>instance).exports || instance;
|
||||||
@ -224,525 +223,26 @@ function getExports(instance: ViewBase): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseInternal(value: string, context: any, xmlModule?: string, moduleName?: string): ComponentModule {
|
function parseInternal(value: string, context: any, xmlModule?: string, moduleName?: string): ComponentModule {
|
||||||
let start: xml2ui.XmlStringParser;
|
if (__UI_USE_XML_PARSER__) {
|
||||||
let ui: xml2ui.ComponentParser;
|
let start: xml2ui.XmlStringParser;
|
||||||
|
let ui: xml2ui.ComponentParser;
|
||||||
const errorFormat = debug && xmlModule ? xml2ui.SourceErrorFormat(xmlModule) : xml2ui.PositionErrorFormat;
|
|
||||||
const componentSourceTracker =
|
const errorFormat = debug && xmlModule ? xml2ui.SourceErrorFormat(xmlModule) : xml2ui.PositionErrorFormat;
|
||||||
debug && xmlModule
|
const componentSourceTracker =
|
||||||
? xml2ui.ComponentSourceTracker(xmlModule)
|
debug && xmlModule
|
||||||
: () => {
|
? xml2ui.ComponentSourceTracker(xmlModule)
|
||||||
// no-op
|
: () => {
|
||||||
};
|
// no-op
|
||||||
|
};
|
||||||
(start = new xml2ui.XmlStringParser(errorFormat)).pipe(new xml2ui.PlatformFilter()).pipe(new xml2ui.XmlStateParser((ui = new xml2ui.ComponentParser(context, errorFormat, componentSourceTracker, moduleName))));
|
|
||||||
|
(start = new xml2ui.XmlStringParser(errorFormat)).pipe(new xml2ui.PlatformFilter()).pipe(new xml2ui.XmlStateParser((ui = new xml2ui.ComponentParser(context, errorFormat, componentSourceTracker, moduleName))));
|
||||||
start.parse(value);
|
|
||||||
|
start.parse(value);
|
||||||
return ui.rootComponentModule;
|
|
||||||
}
|
return ui.rootComponentModule;
|
||||||
|
} else {
|
||||||
namespace xml2ui {
|
return null;
|
||||||
/**
|
|
||||||
* Pipes and filters:
|
|
||||||
* https://en.wikipedia.org/wiki/Pipeline_(software)
|
|
||||||
*/
|
|
||||||
interface XmlProducer {
|
|
||||||
pipe<Next extends XmlConsumer>(next: Next): Next;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface XmlConsumer {
|
|
||||||
parse(args: xml.ParserEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParseInputData extends String {
|
|
||||||
default?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class XmlProducerBase implements XmlProducer {
|
|
||||||
private _next: XmlConsumer;
|
|
||||||
public pipe<Next extends XmlConsumer>(next: Next) {
|
|
||||||
this._next = next;
|
|
||||||
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
protected next(args: xml.ParserEvent) {
|
|
||||||
this._next.parse(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class XmlStringParser extends XmlProducerBase implements XmlProducer {
|
|
||||||
private error: ErrorFormatter;
|
|
||||||
|
|
||||||
constructor(error?: ErrorFormatter) {
|
|
||||||
super();
|
|
||||||
this.error = error || PositionErrorFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
public parse(value: ParseInputData) {
|
|
||||||
const xmlParser = new xml.XmlParser(
|
|
||||||
(args: xml.ParserEvent) => {
|
|
||||||
try {
|
|
||||||
this.next(args);
|
|
||||||
} catch (e) {
|
|
||||||
throw this.error(e, args.position);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(e, p) => {
|
|
||||||
throw this.error(e, p);
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isString(value)) {
|
|
||||||
xmlParser.parse(<string>value);
|
|
||||||
} else if (isObject(value) && isString(value.default)) {
|
|
||||||
xmlParser.parse(value.default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ErrorFormatter {
|
|
||||||
(e: Error, p: xml.Position): Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PositionErrorFormat(e: Error, p: xml.Position): Error {
|
|
||||||
return new ScopeError(e, 'Parsing XML at ' + p.line + ':' + p.column);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SourceErrorFormat(uri): ErrorFormatter {
|
|
||||||
return (e: Error, p: xml.Position) => {
|
|
||||||
const source = p ? new Source(uri, p.line, p.column) : new Source(uri, -1, -1);
|
|
||||||
e = new SourceError(e, source, 'Building UI from XML.');
|
|
||||||
|
|
||||||
return e;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SourceTracker {
|
|
||||||
(component: any, p: xml.Position): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ComponentSourceTracker(uri): SourceTracker {
|
|
||||||
return (component: any, p: xml.Position) => {
|
|
||||||
if (!Source.get(component)) {
|
|
||||||
const source = p ? new Source(uri, p.line, p.column) : new Source(uri, -1, -1);
|
|
||||||
Source.set(component, source);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PlatformFilter extends XmlProducerBase implements XmlProducer, XmlConsumer {
|
|
||||||
private currentPlatformContext: string;
|
|
||||||
|
|
||||||
public parse(args: xml.ParserEvent) {
|
|
||||||
if (args.eventType === xml.ParserEventType.StartElement) {
|
|
||||||
if (PlatformFilter.isPlatform(args.elementName)) {
|
|
||||||
if (this.currentPlatformContext) {
|
|
||||||
throw new Error("Already in '" + this.currentPlatformContext + "' platform context and cannot switch to '" + args.elementName + "' platform! Platform tags cannot be nested.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPlatformContext = args.elementName;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.eventType === xml.ParserEventType.EndElement) {
|
|
||||||
if (PlatformFilter.isPlatform(args.elementName)) {
|
|
||||||
this.currentPlatformContext = undefined;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentPlatformContext && !PlatformFilter.isCurentPlatform(this.currentPlatformContext)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.next(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isPlatform(value: string): boolean {
|
|
||||||
if (value) {
|
|
||||||
const toLower = value.toLowerCase();
|
|
||||||
|
|
||||||
return toLower === android || toLower === ios;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isCurentPlatform(value: string): boolean {
|
|
||||||
return value && value.toLowerCase() === Device.os.toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class XmlArgsReplay extends XmlProducerBase implements XmlProducer {
|
|
||||||
private error: ErrorFormatter;
|
|
||||||
private args: xml.ParserEvent[];
|
|
||||||
|
|
||||||
constructor(args: xml.ParserEvent[], errorFormat: ErrorFormatter) {
|
|
||||||
super();
|
|
||||||
this.args = args;
|
|
||||||
this.error = errorFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
public replay() {
|
|
||||||
this.args.forEach((args: xml.ParserEvent) => {
|
|
||||||
try {
|
|
||||||
this.next(args);
|
|
||||||
} catch (e) {
|
|
||||||
throw this.error(e, args.position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TemplateProperty {
|
|
||||||
context?: any;
|
|
||||||
parent: ComponentModule;
|
|
||||||
name: string;
|
|
||||||
elementName: string;
|
|
||||||
templateItems: Array<string>;
|
|
||||||
errorFormat: ErrorFormatter;
|
|
||||||
sourceTracker: SourceTracker;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It is a state pattern
|
|
||||||
* https://en.wikipedia.org/wiki/State_pattern
|
|
||||||
*/
|
|
||||||
export class XmlStateParser implements XmlConsumer {
|
|
||||||
private state: XmlStateConsumer;
|
|
||||||
|
|
||||||
constructor(state: XmlStateConsumer) {
|
|
||||||
this.state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
parse(args: xml.ParserEvent) {
|
|
||||||
this.state = this.state.parse(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface XmlStateConsumer extends XmlConsumer {
|
|
||||||
parse(args: xml.ParserEvent): XmlStateConsumer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TemplateParser implements XmlStateConsumer {
|
|
||||||
private _context: any;
|
|
||||||
private _recordedXmlStream: Array<xml.ParserEvent>;
|
|
||||||
private _templateProperty: TemplateProperty;
|
|
||||||
private _nestingLevel: number;
|
|
||||||
private _state: TemplateParser.State;
|
|
||||||
|
|
||||||
private parent: XmlStateConsumer;
|
|
||||||
private _setTemplateProperty: boolean;
|
|
||||||
|
|
||||||
constructor(parent: XmlStateConsumer, templateProperty: TemplateProperty, setTemplateProperty = true) {
|
|
||||||
this.parent = parent;
|
|
||||||
|
|
||||||
this._context = templateProperty.context;
|
|
||||||
this._recordedXmlStream = new Array<xml.ParserEvent>();
|
|
||||||
this._templateProperty = templateProperty;
|
|
||||||
this._nestingLevel = 0;
|
|
||||||
this._state = TemplateParser.State.EXPECTING_START;
|
|
||||||
this._setTemplateProperty = setTemplateProperty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public parse(args: xml.ParserEvent): XmlStateConsumer {
|
|
||||||
if (args.eventType === xml.ParserEventType.StartElement) {
|
|
||||||
this.parseStartElement(args.prefix, args.namespace, args.elementName, args.attributes);
|
|
||||||
} else if (args.eventType === xml.ParserEventType.EndElement) {
|
|
||||||
this.parseEndElement(args.prefix, args.elementName);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._recordedXmlStream.push(args);
|
|
||||||
|
|
||||||
return this._state === TemplateParser.State.FINISHED ? this.parent : this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get elementName(): string {
|
|
||||||
return this._templateProperty.elementName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseStartElement(prefix: string, namespace: string, elementName: string, attributes: Object) {
|
|
||||||
if (this._state === TemplateParser.State.EXPECTING_START) {
|
|
||||||
this._state = TemplateParser.State.PARSING;
|
|
||||||
} else if (this._state === TemplateParser.State.FINISHED) {
|
|
||||||
throw new Error('Template must have exactly one root element but multiple elements were found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
this._nestingLevel++;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseEndElement(prefix: string, elementName: string) {
|
|
||||||
if (this._state === TemplateParser.State.EXPECTING_START) {
|
|
||||||
throw new Error('Template must have exactly one root element but none was found.');
|
|
||||||
} else if (this._state === TemplateParser.State.FINISHED) {
|
|
||||||
throw new Error('No more closing elements expected for this template.');
|
|
||||||
}
|
|
||||||
|
|
||||||
this._nestingLevel--;
|
|
||||||
|
|
||||||
if (this._nestingLevel === 0) {
|
|
||||||
this._state = TemplateParser.State.FINISHED;
|
|
||||||
|
|
||||||
if (this._setTemplateProperty && this._templateProperty.name in this._templateProperty.parent.component) {
|
|
||||||
const template = this.buildTemplate();
|
|
||||||
this._templateProperty.parent.component[this._templateProperty.name] = template;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public buildTemplate(): Template {
|
|
||||||
const context = this._context;
|
|
||||||
const errorFormat = this._templateProperty.errorFormat;
|
|
||||||
const sourceTracker = this._templateProperty.sourceTracker;
|
|
||||||
const template: Template = <Template>profile('Template()', () => {
|
|
||||||
let start: xml2ui.XmlArgsReplay;
|
|
||||||
let ui: xml2ui.ComponentParser;
|
|
||||||
|
|
||||||
(start = new xml2ui.XmlArgsReplay(this._recordedXmlStream, errorFormat))
|
|
||||||
// No platform filter, it has been filtered already
|
|
||||||
.pipe(new XmlStateParser((ui = new ComponentParser(context, errorFormat, sourceTracker))));
|
|
||||||
|
|
||||||
start.replay();
|
|
||||||
|
|
||||||
return ui.rootComponentModule.component;
|
|
||||||
});
|
|
||||||
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MultiTemplateParser implements XmlStateConsumer {
|
|
||||||
private _childParsers = new Array<TemplateParser>();
|
|
||||||
private _value: KeyedTemplate[];
|
|
||||||
|
|
||||||
get value(): KeyedTemplate[] {
|
|
||||||
return this._value;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(private parent: XmlStateConsumer, private templateProperty: TemplateProperty) {}
|
|
||||||
|
|
||||||
public parse(args: xml.ParserEvent): XmlStateConsumer {
|
|
||||||
if (args.eventType === xml.ParserEventType.StartElement && args.elementName === 'template') {
|
|
||||||
const childParser = new TemplateParser(this, this.templateProperty, false);
|
|
||||||
childParser['key'] = args.attributes['key'];
|
|
||||||
this._childParsers.push(childParser);
|
|
||||||
|
|
||||||
return childParser;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.eventType === xml.ParserEventType.EndElement) {
|
|
||||||
const name = ComponentParser.getComplexPropertyName(args.elementName);
|
|
||||||
if (name === this.templateProperty.name) {
|
|
||||||
const templates = new Array<KeyedTemplate>();
|
|
||||||
for (let i = 0; i < this._childParsers.length; i++) {
|
|
||||||
templates.push({
|
|
||||||
key: this._childParsers[i]['key'],
|
|
||||||
createView: this._childParsers[i].buildTemplate(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._value = templates;
|
|
||||||
|
|
||||||
return this.parent.parse(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace TemplateParser {
|
|
||||||
export const enum State {
|
|
||||||
EXPECTING_START,
|
|
||||||
PARSING,
|
|
||||||
FINISHED,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComponentParser implements XmlStateConsumer {
|
|
||||||
private static KNOWNCOLLECTIONS = 'knownCollections';
|
|
||||||
private static KNOWNTEMPLATES = 'knownTemplates';
|
|
||||||
private static KNOWNMULTITEMPLATES = 'knownMultiTemplates';
|
|
||||||
|
|
||||||
public rootComponentModule: ComponentModule;
|
|
||||||
|
|
||||||
private context: any;
|
|
||||||
|
|
||||||
private currentRootView: View;
|
|
||||||
private parents = new Array<ComponentModule>();
|
|
||||||
private complexProperties = new Array<ComponentParser.ComplexProperty>();
|
|
||||||
|
|
||||||
private error: ErrorFormatter;
|
|
||||||
private sourceTracker: SourceTracker;
|
|
||||||
|
|
||||||
constructor(context: any, errorFormat: ErrorFormatter, sourceTracker: SourceTracker, private moduleName?: string) {
|
|
||||||
this.context = context;
|
|
||||||
this.error = errorFormat;
|
|
||||||
this.sourceTracker = sourceTracker;
|
|
||||||
}
|
|
||||||
|
|
||||||
@profile
|
|
||||||
private buildComponent(args: xml.ParserEvent): ComponentModule {
|
|
||||||
if (args.prefix && args.namespace) {
|
|
||||||
// Custom components
|
|
||||||
return loadCustomComponent(args.namespace, args.elementName, args.attributes, this.context, this.currentRootView, !this.currentRootView, this.moduleName);
|
|
||||||
} else {
|
|
||||||
// Default components
|
|
||||||
let namespace = args.namespace;
|
|
||||||
if (defaultNameSpaceMatcher.test(namespace || '')) {
|
|
||||||
//Ignore the default ...tns.xsd namespace URL
|
|
||||||
namespace = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getComponentModule(args.elementName, namespace, args.attributes, this.context, this.moduleName, !this.currentRootView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public parse(args: xml.ParserEvent): XmlStateConsumer {
|
|
||||||
// Get the current parent.
|
|
||||||
const parent = this.parents[this.parents.length - 1];
|
|
||||||
const complexProperty = this.complexProperties[this.complexProperties.length - 1];
|
|
||||||
|
|
||||||
// Create component instance from every element declaration.
|
|
||||||
if (args.eventType === xml.ParserEventType.StartElement) {
|
|
||||||
if (ComponentParser.isComplexProperty(args.elementName)) {
|
|
||||||
const name = ComponentParser.getComplexPropertyName(args.elementName);
|
|
||||||
|
|
||||||
const complexProperty: ComponentParser.ComplexProperty = {
|
|
||||||
parent: parent,
|
|
||||||
name: name,
|
|
||||||
items: [],
|
|
||||||
};
|
|
||||||
this.complexProperties.push(complexProperty);
|
|
||||||
|
|
||||||
if (ComponentParser.isKnownTemplate(name, parent.exports)) {
|
|
||||||
return new TemplateParser(this, {
|
|
||||||
context: (parent ? getExports(parent.component) : null) || this.context, // Passing 'context' won't work if you set "codeFile" on the page
|
|
||||||
parent: parent,
|
|
||||||
name: name,
|
|
||||||
elementName: args.elementName,
|
|
||||||
templateItems: [],
|
|
||||||
errorFormat: this.error,
|
|
||||||
sourceTracker: this.sourceTracker,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ComponentParser.isKnownMultiTemplate(name, parent.exports)) {
|
|
||||||
const parser = new MultiTemplateParser(this, {
|
|
||||||
context: (parent ? getExports(parent.component) : null) || this.context, // Passing 'context' won't work if you set "codeFile" on the page
|
|
||||||
parent: parent,
|
|
||||||
name: name,
|
|
||||||
elementName: args.elementName,
|
|
||||||
templateItems: [],
|
|
||||||
errorFormat: this.error,
|
|
||||||
sourceTracker: this.sourceTracker,
|
|
||||||
});
|
|
||||||
complexProperty.parser = parser;
|
|
||||||
|
|
||||||
return parser;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const componentModule = this.buildComponent(args);
|
|
||||||
|
|
||||||
if (componentModule) {
|
|
||||||
this.sourceTracker(componentModule.component, args.position);
|
|
||||||
if (parent) {
|
|
||||||
if (complexProperty) {
|
|
||||||
// Add component to complex property of parent component.
|
|
||||||
ComponentParser.addToComplexProperty(parent, complexProperty, componentModule);
|
|
||||||
} else if ((<any>parent.component)._addChildFromBuilder) {
|
|
||||||
(<any>parent.component)._addChildFromBuilder(args.elementName, componentModule.component);
|
|
||||||
}
|
|
||||||
} else if (this.parents.length === 0) {
|
|
||||||
// Set root component.
|
|
||||||
this.rootComponentModule = componentModule;
|
|
||||||
|
|
||||||
if (this.rootComponentModule) {
|
|
||||||
this.currentRootView = this.rootComponentModule.component;
|
|
||||||
|
|
||||||
if ((<any>this.currentRootView).exports) {
|
|
||||||
this.context = (<any>this.currentRootView).exports;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the component instance to the parents scope collection.
|
|
||||||
this.parents.push(componentModule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (args.eventType === xml.ParserEventType.EndElement) {
|
|
||||||
if (ComponentParser.isComplexProperty(args.elementName)) {
|
|
||||||
if (complexProperty) {
|
|
||||||
if (complexProperty.parser) {
|
|
||||||
parent.component[complexProperty.name] = complexProperty.parser.value;
|
|
||||||
} else if (parent && (<any>parent.component)._addArrayFromBuilder) {
|
|
||||||
// If parent is AddArrayFromBuilder call the interface method to populate the array property.
|
|
||||||
(<any>parent.component)._addArrayFromBuilder(complexProperty.name, complexProperty.items);
|
|
||||||
complexProperty.items = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove the last complexProperty from the complexProperties collection (move to the previous complexProperty scope).
|
|
||||||
this.complexProperties.pop();
|
|
||||||
} else {
|
|
||||||
// Remove the last parent from the parents collection (move to the previous parent scope).
|
|
||||||
this.parents.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isComplexProperty(name: string): boolean {
|
|
||||||
return isString(name) && name.indexOf('.') !== -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getComplexPropertyName(fullName: string): string {
|
|
||||||
let name: string;
|
|
||||||
|
|
||||||
if (isString(fullName)) {
|
|
||||||
const names = fullName.split('.');
|
|
||||||
name = names[names.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isKnownTemplate(name: string, exports: any): boolean {
|
|
||||||
return Builder.knownTemplates.has(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isKnownMultiTemplate(name: string, exports: any): boolean {
|
|
||||||
return Builder.knownMultiTemplates.has(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static addToComplexProperty(parent: ComponentModule, complexProperty: ComponentParser.ComplexProperty, elementModule: ComponentModule) {
|
|
||||||
// If property name is known collection we populate array with elements.
|
|
||||||
const parentComponent = <any>parent.component;
|
|
||||||
if (ComponentParser.isKnownCollection(complexProperty.name, parent.exports)) {
|
|
||||||
complexProperty.items.push(elementModule.component);
|
|
||||||
} else if (parentComponent._addChildFromBuilder) {
|
|
||||||
parentComponent._addChildFromBuilder(complexProperty.name, elementModule.component);
|
|
||||||
} else {
|
|
||||||
// Or simply assign the value;
|
|
||||||
parentComponent[complexProperty.name] = elementModule.component;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isKnownCollection(name: string, context: any): boolean {
|
|
||||||
return Builder.knownCollections.has(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace ComponentParser {
|
|
||||||
export interface ComplexProperty {
|
|
||||||
parent: ComponentModule;
|
|
||||||
name: string;
|
|
||||||
items?: Array<any>;
|
|
||||||
parser?: { value: any };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
let newValue = value;
|
||||||
if (this.options.expression) {
|
if (__UI_USE_EXTERNAL_RENDERER__) {
|
||||||
|
} else if (this.options.expression) {
|
||||||
const changedModel = {};
|
const changedModel = {};
|
||||||
changedModel[bc.bindingValueKey] = value;
|
changedModel[bc.bindingValueKey] = value;
|
||||||
changedModel[bc.newPropertyValueKey] = value;
|
changedModel[bc.newPropertyValueKey] = value;
|
||||||
@ -373,38 +374,40 @@ export class Binding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getExpressionValue(expression: string, isBackConvert: boolean, changedModel: any): any {
|
private _getExpressionValue(expression: string, isBackConvert: boolean, changedModel: any): any {
|
||||||
try {
|
if (!__UI_USE_EXTERNAL_RENDERER__) {
|
||||||
const exp = PolymerExpressions.getExpression(expression);
|
try {
|
||||||
if (exp) {
|
const exp = PolymerExpressions.getExpression(expression);
|
||||||
const context = (this.source && this.source.get && this.source.get()) || global;
|
if (exp) {
|
||||||
const model = {};
|
const context = (this.source && this.source.get && this.source.get()) || global;
|
||||||
const addedProps = [];
|
const model = {};
|
||||||
const resources = bindableResources.get();
|
const addedProps = [];
|
||||||
for (const prop in resources) {
|
const resources = bindableResources.get();
|
||||||
if (resources.hasOwnProperty(prop) && !context.hasOwnProperty(prop)) {
|
for (const prop in resources) {
|
||||||
context[prop] = resources[prop];
|
if (resources.hasOwnProperty(prop) && !context.hasOwnProperty(prop)) {
|
||||||
addedProps.push(prop);
|
context[prop] = resources[prop];
|
||||||
|
addedProps.push(prop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.prepareContextForExpression(context, expression, addedProps);
|
||||||
|
model[contextKey] = context;
|
||||||
|
const result = exp.getValue(model, isBackConvert, changedModel ? changedModel : model);
|
||||||
|
// clear added props
|
||||||
|
const addedPropsLength = addedProps.length;
|
||||||
|
for (let i = 0; i < addedPropsLength; i++) {
|
||||||
|
delete context[addedProps[i]];
|
||||||
|
}
|
||||||
|
addedProps.length = 0;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.prepareContextForExpression(context, expression, addedProps);
|
return new Error(expression + ' is not a valid expression.');
|
||||||
model[contextKey] = context;
|
} catch (e) {
|
||||||
const result = exp.getValue(model, isBackConvert, changedModel ? changedModel : model);
|
const errorMessage = 'Run-time error occured in file: ' + e.sourceURL + ' at line: ' + e.line + ' and column: ' + e.column;
|
||||||
// clear added props
|
|
||||||
const addedPropsLength = addedProps.length;
|
|
||||||
for (let i = 0; i < addedPropsLength; i++) {
|
|
||||||
delete context[addedProps[i]];
|
|
||||||
}
|
|
||||||
addedProps.length = 0;
|
|
||||||
|
|
||||||
return result;
|
return new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Error(expression + ' is not a valid expression.');
|
|
||||||
} catch (e) {
|
|
||||||
const errorMessage = 'Run-time error occured in file: ' + e.sourceURL + ' at line: ' + e.line + ' and column: ' + e.column;
|
|
||||||
|
|
||||||
return new Error(errorMessage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,14 +424,7 @@ export class Binding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.expression) {
|
if (__UI_USE_EXTERNAL_RENDERER__ || !this.options.expression) {
|
||||||
const expressionValue = this._getExpressionValue(this.options.expression, false, undefined);
|
|
||||||
if (expressionValue instanceof Error) {
|
|
||||||
Trace.write(expressionValue.message, Trace.categories.Binding, Trace.messageType.error);
|
|
||||||
} else {
|
|
||||||
this.updateTarget(expressionValue);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (changedPropertyIndex > -1) {
|
if (changedPropertyIndex > -1) {
|
||||||
const props = sourceProps.slice(changedPropertyIndex + 1);
|
const props = sourceProps.slice(changedPropertyIndex + 1);
|
||||||
const propsLength = props.length;
|
const propsLength = props.length;
|
||||||
@ -443,6 +439,13 @@ export class Binding {
|
|||||||
this.updateTarget(data.value);
|
this.updateTarget(data.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const expressionValue = this._getExpressionValue(this.options.expression, false, undefined);
|
||||||
|
if (expressionValue instanceof Error) {
|
||||||
|
Trace.write(expressionValue.message, Trace.categories.Binding, Trace.messageType.error);
|
||||||
|
} else {
|
||||||
|
this.updateTarget(expressionValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to do this only if nested objects are used as source and some middle object has changed.
|
// we need to do this only if nested objects are used as source and some middle object has changed.
|
||||||
@ -516,7 +519,8 @@ export class Binding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getSourcePropertyValue() {
|
private getSourcePropertyValue() {
|
||||||
if (this.options.expression) {
|
if (__UI_USE_EXTERNAL_RENDERER__) {
|
||||||
|
} else if (this.options.expression) {
|
||||||
const changedModel = {};
|
const changedModel = {};
|
||||||
changedModel[bc.bindingValueKey] = this.source ? this.source.get() : undefined;
|
changedModel[bc.bindingValueKey] = this.source ? this.source.get() : undefined;
|
||||||
const expressionValue = this._getExpressionValue(this.options.expression, false, changedModel);
|
const expressionValue = this._getExpressionValue(this.options.expression, false, changedModel);
|
||||||
|
@ -27,7 +27,8 @@ export abstract class ListViewBase extends ContainerView implements ListViewDefi
|
|||||||
public _defaultTemplate: KeyedTemplate = {
|
public _defaultTemplate: KeyedTemplate = {
|
||||||
key: 'default',
|
key: 'default',
|
||||||
createView: () => {
|
createView: () => {
|
||||||
if (this.itemTemplate) {
|
if (__UI_USE_EXTERNAL_RENDERER__) {
|
||||||
|
} else if (this.itemTemplate) {
|
||||||
return Builder.parse(this.itemTemplate, this);
|
return Builder.parse(this.itemTemplate, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,7 +197,11 @@ export const itemTemplatesProperty = new Property<ListViewBase, string | Array<K
|
|||||||
name: 'itemTemplates',
|
name: 'itemTemplates',
|
||||||
valueConverter: (value) => {
|
valueConverter: (value) => {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return Builder.parseMultipleTemplates(value, null);
|
if (__UI_USE_XML_PARSER__) {
|
||||||
|
return Builder.parseMultipleTemplates(value, null);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
|
@ -129,7 +129,11 @@ export class Repeater extends CustomLayoutView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!viewToAdd) {
|
if (!viewToAdd) {
|
||||||
viewToAdd = this.itemTemplate ? Builder.parse(this.itemTemplate, this) : this._getDefaultItemContent(i);
|
if (__UI_USE_EXTERNAL_RENDERER__) {
|
||||||
|
viewToAdd = this._getDefaultItemContent(i)
|
||||||
|
} else {
|
||||||
|
viewToAdd = this.itemTemplate ? Builder.parse(this.itemTemplate, this) : this._getDefaultItemContent(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewToAdd.bindingContext = dataItem;
|
viewToAdd.bindingContext = dataItem;
|
||||||
@ -223,7 +227,11 @@ export const itemTemplatesProperty = new Property<Repeater, string | Array<Keyed
|
|||||||
affectsLayout: true,
|
affectsLayout: true,
|
||||||
valueConverter: (value) => {
|
valueConverter: (value) => {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return Builder.parseMultipleTemplates(value, null);
|
if (__UI_USE_XML_PARSER__) {
|
||||||
|
return Builder.parseMultipleTemplates(value, null);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
|
@ -218,23 +218,19 @@ class CSSSource {
|
|||||||
@profile
|
@profile
|
||||||
private parseCSSAst() {
|
private parseCSSAst() {
|
||||||
if (this._source) {
|
if (this._source) {
|
||||||
switch (parser) {
|
if (__CSS_PARSER__ === 'css-tree') {
|
||||||
case 'css-tree':
|
const cssTreeParse = require('../../css/css-tree-parser').cssTreeParse;
|
||||||
this._ast = cssTreeParse(this._source, this._file);
|
this._ast = cssTreeParse(this._source, this._file);
|
||||||
|
} else if (__CSS_PARSER__ === 'nativescript') {
|
||||||
return;
|
const CSS3Parser = require('../../css/CSS3Parser').CSS3Parser;
|
||||||
case 'nativescript': {
|
const CSSNativeScript = require('../../css/CSSNativeScript').CSSNativeScript;
|
||||||
const cssparser = new CSS3Parser(this._source);
|
const cssparser = new CSS3Parser(this._source);
|
||||||
const stylesheet = cssparser.parseAStylesheet();
|
const stylesheet = cssparser.parseAStylesheet();
|
||||||
const cssNS = new CSSNativeScript();
|
const cssNS = new CSSNativeScript();
|
||||||
this._ast = cssNS.parseStylesheet(stylesheet);
|
this._ast = cssNS.parseStylesheet(stylesheet);
|
||||||
|
} else if (__CSS_PARSER__ === 'rework') {
|
||||||
return;
|
const parseCss = require('../../css').parse;
|
||||||
}
|
this._ast = parseCss(this._source, { source: this._file });
|
||||||
case 'rework':
|
|
||||||
this._ast = parseCss(this._source, { source: this._file });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// https://github.com/NativeScript/nativescript-dev-webpack/issues/932
|
// https://github.com/NativeScript/nativescript-dev-webpack/issues/932
|
||||||
|
|
||||||
import * as definition from '.';
|
import * as definition from '.';
|
||||||
const easysax = require('../js-libs/easysax');
|
|
||||||
import { EasySAXParser } from '../js-libs/easysax';
|
import { EasySAXParser } from '../js-libs/easysax';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,9 +3,7 @@ import Config from 'webpack-chain';
|
|||||||
import svelte from '../../src/configuration/svelte';
|
import svelte from '../../src/configuration/svelte';
|
||||||
import { init } from '../../src';
|
import { init } from '../../src';
|
||||||
|
|
||||||
jest.mock('__jest__/svelte.config.js', () => {
|
jest.mock('__jest__/svelte.config.js', () => {}, { virtual: true });
|
||||||
|
|
||||||
}, { virtual: true })
|
|
||||||
|
|
||||||
describe('svelte configuration', () => {
|
describe('svelte configuration', () => {
|
||||||
const platforms = ['ios', 'android'];
|
const platforms = ['ios', 'android'];
|
||||||
|
@ -3,5 +3,5 @@ import { copyRules, additionalCopyRules } from '../src/helpers/copyRules';
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// Clear copy rules
|
// Clear copy rules
|
||||||
copyRules.clear();
|
copyRules.clear();
|
||||||
additionalCopyRules.length = 0
|
additionalCopyRules.length = 0;
|
||||||
});
|
});
|
||||||
|
@ -15,6 +15,7 @@ import { applyFileReplacements } from '../helpers/fileReplacements';
|
|||||||
import { addCopyRule, applyCopyRules } from '../helpers/copyRules';
|
import { addCopyRule, applyCopyRules } from '../helpers/copyRules';
|
||||||
import { WatchStatePlugin } from '../plugins/WatchStatePlugin';
|
import { WatchStatePlugin } from '../plugins/WatchStatePlugin';
|
||||||
import { getProjectFilePath } from '../helpers/project';
|
import { getProjectFilePath } from '../helpers/project';
|
||||||
|
import { projectUsesCustomFlavor } from '../helpers/flavor';
|
||||||
import { hasDependency } from '../helpers/dependencies';
|
import { hasDependency } from '../helpers/dependencies';
|
||||||
import { applyDotEnvPlugin } from '../helpers/dotEnv';
|
import { applyDotEnvPlugin } from '../helpers/dotEnv';
|
||||||
import { env as _env, IWebpackEnv } from '../index';
|
import { env as _env, IWebpackEnv } from '../index';
|
||||||
@ -352,6 +353,8 @@ export default function (config: Config, env: IWebpackEnv = _env): Config {
|
|||||||
__NS_DEV_HOST_IPS__:
|
__NS_DEV_HOST_IPS__:
|
||||||
mode === 'development' ? JSON.stringify(getIPS()) : `[]`,
|
mode === 'development' ? JSON.stringify(getIPS()) : `[]`,
|
||||||
__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
|
__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
|
||||||
|
__UI_USE_XML_PARSER__: true,
|
||||||
|
__UI_USE_EXTERNAL_RENDERER__: projectUsesCustomFlavor(),
|
||||||
__ANDROID__: platform === 'android',
|
__ANDROID__: platform === 'android',
|
||||||
__IOS__: platform === 'ios',
|
__IOS__: platform === 'ios',
|
||||||
/* for compat only */ 'global.isAndroid': platform === 'android',
|
/* for compat only */ 'global.isAndroid': platform === 'android',
|
||||||
|
@ -2,6 +2,28 @@ import { defaultConfigs } from '@nativescript/webpack';
|
|||||||
import { getAllDependencies } from './dependencies';
|
import { getAllDependencies } from './dependencies';
|
||||||
import { error } from './log';
|
import { error } from './log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to determine the project flavor based on installed dependencies
|
||||||
|
* (vue, angular, react, svelete, typescript, javascript...)
|
||||||
|
*/
|
||||||
|
export function projectUsesCustomFlavor(): boolean {
|
||||||
|
const dependencies = getAllDependencies();
|
||||||
|
return [
|
||||||
|
'vue',
|
||||||
|
'angular',
|
||||||
|
'react',
|
||||||
|
'svelte'
|
||||||
|
].includes(determineProjectFlavor())
|
||||||
|
if (dependencies.includes('nativescript-vue') ||
|
||||||
|
dependencies.includes('@nativescript/angular') ||
|
||||||
|
dependencies.includes('react-nativescript') ||
|
||||||
|
dependencies.includes('svelte-native')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Utility to determine the project flavor based on installed dependencies
|
* Utility to determine the project flavor based on installed dependencies
|
||||||
* (vue, angular, react, svelete, typescript, javascript...)
|
* (vue, angular, react, svelete, typescript, javascript...)
|
||||||
|
@ -9,7 +9,7 @@ import { addVirtualEntry, addVirtualModule } from './virtualModules';
|
|||||||
import { applyFileReplacements } from './fileReplacements';
|
import { applyFileReplacements } from './fileReplacements';
|
||||||
import { addCopyRule, removeCopyRule } from './copyRules';
|
import { addCopyRule, removeCopyRule } from './copyRules';
|
||||||
import { error, info, warn, warnOnce } from './log';
|
import { error, info, warn, warnOnce } from './log';
|
||||||
import { determineProjectFlavor } from './flavor';
|
import { determineProjectFlavor, projectUsesCustomFlavor } from './flavor';
|
||||||
import { getValue } from './config';
|
import { getValue } from './config';
|
||||||
import { getIPS } from './host';
|
import { getIPS } from './host';
|
||||||
import {
|
import {
|
||||||
@ -47,6 +47,7 @@ export default {
|
|||||||
},
|
},
|
||||||
flavor: {
|
flavor: {
|
||||||
determineProjectFlavor,
|
determineProjectFlavor,
|
||||||
|
projectUsesCustomFlavor,
|
||||||
},
|
},
|
||||||
host: {
|
host: {
|
||||||
getIPS,
|
getIPS,
|
||||||
|
Reference in New Issue
Block a user