@nativescript/core
Version:
A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.
534 lines • 19.1 kB
JavaScript
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 {
constructor(text) {
this.text = text;
this.nextInputCodePointIndex = 0;
}
/**
* For testing purposes.
* This method allows us to run and assert the proper working of the tokenizer.
*/
tokenize() {
const tokens = [];
let 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
*/
consumeAToken() {
if (this.reconsumedInputToken) {
const result = this.reconsumedInputToken;
this.reconsumedInputToken = null;
return result;
}
const char = this.text[this.nextInputCodePointIndex];
switch (char) {
case '"':
return this.consumeAStringToken();
case "'":
return this.consumeAStringToken();
case '(':
case ')':
case ',':
case ':':
case ';':
case '[':
case ']':
case '{':
case '}':
this.nextInputCodePointIndex++;
return char;
case '#':
return this.consumeAHashToken() || this.consumeADelimToken();
case ' ':
case '\t':
case '\n':
case '\r':
case '\f':
return this.consumeAWhitespace();
case '@':
return this.consumeAtKeyword() || this.consumeADelimToken();
// TODO: Only if this is valid escape, otherwise it is a parse error
case '\\':
return this.consumeAnIdentLikeToken() || this.consumeADelimToken();
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return this.consumeANumericToken();
case 'u':
case 'U':
if (this.text[this.nextInputCodePointIndex + 1] === '+') {
const thirdChar = this.text[this.nextInputCodePointIndex + 2];
if ((thirdChar >= '0' && thirdChar <= '9') || thirdChar === '?') {
// TODO: Handle unicode stuff such as U+002B
throw new Error('Unicode tokens not supported!');
}
}
return this.consumeAnIdentLikeToken() || this.consumeADelimToken();
case '$':
case '*':
case '^':
case '|':
case '~':
return this.consumeAMatchToken() || this.consumeADelimToken();
case '-':
return this.consumeANumericToken() || this.consumeAnIdentLikeToken() || this.consumeCDC() || this.consumeADelimToken();
case '+':
case '.':
return this.consumeANumericToken() || this.consumeADelimToken();
case '/':
return this.consumeAComment() || this.consumeADelimToken();
case '<':
return this.consumeCDO() || this.consumeADelimToken();
case undefined:
return undefined;
default:
return this.consumeAnIdentLikeToken() || this.consumeADelimToken();
}
}
consumeADelimToken() {
return {
type: 2 /* TokenObjectType.delim */,
text: this.text[this.nextInputCodePointIndex++],
};
}
consumeAWhitespace() {
whitespaceRegEx.lastIndex = this.nextInputCodePointIndex;
whitespaceRegEx.exec(this.text);
this.nextInputCodePointIndex = whitespaceRegEx.lastIndex;
return ' ';
}
consumeAHashToken() {
this.nextInputCodePointIndex++;
const hashName = this.consumeAName();
if (hashName) {
return { type: 12 /* TokenObjectType.hash */, text: '#' + hashName.text };
}
this.nextInputCodePointIndex--;
return null;
}
consumeCDO() {
if (this.text.substr(this.nextInputCodePointIndex, 4) === '<!--') {
this.nextInputCodePointIndex += 4;
return '<!--';
}
return null;
}
consumeCDC() {
if (this.text.substr(this.nextInputCodePointIndex, 3) === '-->') {
this.nextInputCodePointIndex += 3;
return '-->';
}
return null;
}
consumeAMatchToken() {
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
*/
consumeANumericToken() {
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: 4 /* TokenObjectType.percentage */, text: result[0] }; // TODO: Push the actual number and unit here...
}
const name = this.consumeAName();
if (name) {
return {
type: 5 /* TokenObjectType.dimension */,
text: result[0] + name.text,
};
}
return { type: 3 /* 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
*/
consumeAnIdentLikeToken() {
const name = this.consumeAName();
if (!name) {
return null;
}
if (this.text[this.nextInputCodePointIndex] === '(') {
this.nextInputCodePointIndex++;
if (name.text.toLowerCase() === 'url') {
return this.consumeAURLToken();
}
return {
type: 8 /* 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
*/
consumeAStringToken() {
const char = this.text[this.nextInputCodePointIndex];
let result;
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: 1 /* TokenObjectType.string */, text: result[0] };
}
/**
* 4.3.5. Consume a url token
* https://www.w3.org/TR/css-syntax-3/#consume-a-url-token
*/
consumeAURLToken() {
const start = this.nextInputCodePointIndex - 3 /* url */ - 1; /* ( */
const urlToken = {
type: 7 /* 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
*/
consumeAName() {
nameRegEx.lastIndex = this.nextInputCodePointIndex;
const result = nameRegEx.exec(this.text);
if (!result) {
return null;
}
this.nextInputCodePointIndex = nameRegEx.lastIndex;
// TODO: Perform string escaping.
return { type: 6 /* TokenObjectType.ident */, text: result[0] };
}
consumeAtKeyword() {
this.nextInputCodePointIndex++;
const name = this.consumeAName();
if (name) {
return { type: 11 /* TokenObjectType.atKeyword */, text: name.text };
}
this.nextInputCodePointIndex--;
return null;
}
consumeAComment() {
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;
}
reconsumeTheCurrentInputToken(currentInputToken) {
this.reconsumedInputToken = currentInputToken;
}
/**
* 5.3.1. Parse a stylesheet
* https://www.w3.org/TR/css-syntax-3/#parse-a-stylesheet
*/
parseAStylesheet() {
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
*/
consumeAListOfRules() {
const rules = [];
let 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 (inputToken.type === 11 /* 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
*/
consumeAnAtRule() {
let inputToken = this.consumeAToken();
const atRule = {
type: 'at-rule',
name: inputToken.text,
prelude: [],
block: undefined,
};
while ((inputToken = this.consumeAToken())) {
if (inputToken === ';') {
return atRule;
}
else if (inputToken === '{') {
atRule.block = this.consumeASimpleBlock(inputToken);
return atRule;
}
else if (inputToken.type === 9 /* TokenObjectType.simpleBlock */ && inputToken.associatedToken === '{') {
atRule.block = inputToken;
return atRule;
}
this.reconsumeTheCurrentInputToken(inputToken);
const component = this.consumeAComponentValue();
if (component) {
atRule.prelude.push(component);
}
}
return atRule;
}
/**
* 5.4.3. Consume a qualified rule
* https://www.w3.org/TR/css-syntax-3/#consume-a-qualified-rule
*/
consumeAQualifiedRule() {
const qualifiedRule = {
type: 'qualified-rule',
prelude: [],
block: undefined,
};
let inputToken;
while ((inputToken = this.consumeAToken())) {
if (inputToken === '{') {
qualifiedRule.block = this.consumeASimpleBlock(inputToken);
return qualifiedRule;
}
else if (inputToken.type === 9 /* TokenObjectType.simpleBlock */) {
const 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
*/
consumeAComponentValue() {
// 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 === 8 /* TokenObjectType.functionToken */) {
return this.consumeAFunction(inputToken.name);
}
return inputToken;
}
/**
* 5.4.7. Consume a simple block
* https://www.w3.org/TR/css-syntax-3/#consume-a-simple-block
*/
consumeASimpleBlock(associatedToken) {
const endianToken = {
'[': ']',
'{': '}',
'(': ')',
}[associatedToken];
const start = this.nextInputCodePointIndex - 1;
const block = {
type: 9 /* 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
*/
consumeAFunction(name) {
const start = this.nextInputCodePointIndex;
const funcToken = {
type: 14 /* 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);
}
}
//# sourceMappingURL=CSS3Parser.js.map