1871 lines
42 KiB
JavaScript
1871 lines
42 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
var TokenType = require('./const').TokenType;
|
||
|
var Scanner = require('./scanner');
|
||
|
var List = require('../utils/list');
|
||
|
var needPositions;
|
||
|
var filename;
|
||
|
var scanner;
|
||
|
|
||
|
var SCOPE_ATRULE_EXPRESSION = 1;
|
||
|
var SCOPE_SELECTOR = 2;
|
||
|
var SCOPE_VALUE = 3;
|
||
|
|
||
|
var specialFunctions = {};
|
||
|
specialFunctions[SCOPE_ATRULE_EXPRESSION] = {
|
||
|
url: getUri
|
||
|
};
|
||
|
specialFunctions[SCOPE_SELECTOR] = {
|
||
|
url: getUri,
|
||
|
not: getNotFunction
|
||
|
};
|
||
|
specialFunctions[SCOPE_VALUE] = {
|
||
|
url: getUri,
|
||
|
expression: getOldIEExpression,
|
||
|
var: getVarFunction
|
||
|
};
|
||
|
|
||
|
var initialContext = {
|
||
|
stylesheet: getStylesheet,
|
||
|
atrule: getAtrule,
|
||
|
atruleExpression: getAtruleExpression,
|
||
|
ruleset: getRuleset,
|
||
|
selector: getSelector,
|
||
|
simpleSelector: getSimpleSelector,
|
||
|
block: getBlock,
|
||
|
declaration: getDeclaration,
|
||
|
value: getValue
|
||
|
};
|
||
|
|
||
|
var blockMode = {
|
||
|
'declaration': true,
|
||
|
'property': true
|
||
|
};
|
||
|
|
||
|
function parseError(message) {
|
||
|
var error = new Error(message);
|
||
|
var offset = 0;
|
||
|
var line = 1;
|
||
|
var column = 1;
|
||
|
var lines;
|
||
|
|
||
|
if (scanner.token !== null) {
|
||
|
offset = scanner.token.offset;
|
||
|
line = scanner.token.line;
|
||
|
column = scanner.token.column;
|
||
|
} else if (scanner.prevToken !== null) {
|
||
|
lines = scanner.prevToken.value.trimRight();
|
||
|
offset = scanner.prevToken.offset + lines.length;
|
||
|
lines = lines.split(/\n|\r\n?|\f/);
|
||
|
line = scanner.prevToken.line + lines.length - 1;
|
||
|
column = lines.length > 1
|
||
|
? lines[lines.length - 1].length + 1
|
||
|
: scanner.prevToken.column + lines[lines.length - 1].length;
|
||
|
}
|
||
|
|
||
|
error.name = 'CssSyntaxError';
|
||
|
error.parseError = {
|
||
|
offset: offset,
|
||
|
line: line,
|
||
|
column: column
|
||
|
};
|
||
|
|
||
|
throw error;
|
||
|
}
|
||
|
|
||
|
function eat(tokenType) {
|
||
|
if (scanner.token !== null && scanner.token.type === tokenType) {
|
||
|
scanner.next();
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
parseError(tokenType + ' is expected');
|
||
|
}
|
||
|
|
||
|
function expectIdentifier(name, eat) {
|
||
|
if (scanner.token !== null) {
|
||
|
if (scanner.token.type === TokenType.Identifier &&
|
||
|
scanner.token.value.toLowerCase() === name) {
|
||
|
if (eat) {
|
||
|
scanner.next();
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
parseError('Identifier `' + name + '` is expected');
|
||
|
}
|
||
|
|
||
|
function expectAny(what) {
|
||
|
if (scanner.token !== null) {
|
||
|
for (var i = 1, type = scanner.token.type; i < arguments.length; i++) {
|
||
|
if (type === arguments[i]) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
parseError(what + ' is expected');
|
||
|
}
|
||
|
|
||
|
function getInfo() {
|
||
|
if (needPositions && scanner.token) {
|
||
|
return {
|
||
|
source: filename,
|
||
|
offset: scanner.token.offset,
|
||
|
line: scanner.token.line,
|
||
|
column: scanner.token.column
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
|
||
|
}
|
||
|
|
||
|
function removeTrailingSpaces(list) {
|
||
|
while (list.tail) {
|
||
|
if (list.tail.data.type === 'Space') {
|
||
|
list.remove(list.tail);
|
||
|
} else {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getStylesheet(nested) {
|
||
|
var child = null;
|
||
|
var node = {
|
||
|
type: 'StyleSheet',
|
||
|
info: getInfo(),
|
||
|
rules: new List()
|
||
|
};
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.Space:
|
||
|
scanner.next();
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
case TokenType.Comment:
|
||
|
// ignore comments except exclamation comments on top level
|
||
|
if (nested || scanner.token.value.charAt(2) !== '!') {
|
||
|
scanner.next();
|
||
|
child = null;
|
||
|
} else {
|
||
|
child = getComment();
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case TokenType.Unknown:
|
||
|
child = getUnknown();
|
||
|
break;
|
||
|
|
||
|
case TokenType.CommercialAt:
|
||
|
child = getAtrule();
|
||
|
break;
|
||
|
|
||
|
case TokenType.RightCurlyBracket:
|
||
|
if (!nested) {
|
||
|
parseError('Unexpected right curly brace');
|
||
|
}
|
||
|
|
||
|
break scan;
|
||
|
|
||
|
default:
|
||
|
child = getRuleset();
|
||
|
}
|
||
|
|
||
|
if (child !== null) {
|
||
|
node.rules.insert(List.createItem(child));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
// '//' ...
|
||
|
// TODO: remove it as wrong thing
|
||
|
function getUnknown() {
|
||
|
var info = getInfo();
|
||
|
var value = scanner.token.value;
|
||
|
|
||
|
eat(TokenType.Unknown);
|
||
|
|
||
|
return {
|
||
|
type: 'Unknown',
|
||
|
info: info,
|
||
|
value: value
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function isBlockAtrule() {
|
||
|
for (var offset = 1, cursor; cursor = scanner.lookup(offset); offset++) {
|
||
|
var type = cursor.type;
|
||
|
|
||
|
if (type === TokenType.RightCurlyBracket) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (type === TokenType.LeftCurlyBracket ||
|
||
|
type === TokenType.CommercialAt) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function getAtruleExpression() {
|
||
|
var child = null;
|
||
|
var node = {
|
||
|
type: 'AtruleExpression',
|
||
|
info: getInfo(),
|
||
|
sequence: new List()
|
||
|
};
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.Semicolon:
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.LeftCurlyBracket:
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.Space:
|
||
|
if (node.sequence.isEmpty()) {
|
||
|
scanner.next(); // ignore spaces in beginning
|
||
|
child = null;
|
||
|
} else {
|
||
|
child = getS();
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case TokenType.Comment: // ignore comments
|
||
|
scanner.next();
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
case TokenType.Comma:
|
||
|
child = getOperator();
|
||
|
break;
|
||
|
|
||
|
case TokenType.Colon:
|
||
|
child = getPseudo();
|
||
|
break;
|
||
|
|
||
|
case TokenType.LeftParenthesis:
|
||
|
child = getBraces(SCOPE_ATRULE_EXPRESSION);
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
child = getAny(SCOPE_ATRULE_EXPRESSION);
|
||
|
}
|
||
|
|
||
|
if (child !== null) {
|
||
|
node.sequence.insert(List.createItem(child));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
removeTrailingSpaces(node.sequence);
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function getAtrule() {
|
||
|
eat(TokenType.CommercialAt);
|
||
|
|
||
|
var node = {
|
||
|
type: 'Atrule',
|
||
|
info: getInfo(),
|
||
|
name: readIdent(false),
|
||
|
expression: getAtruleExpression(),
|
||
|
block: null
|
||
|
};
|
||
|
|
||
|
if (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.Semicolon:
|
||
|
scanner.next(); // {
|
||
|
break;
|
||
|
|
||
|
case TokenType.LeftCurlyBracket:
|
||
|
scanner.next(); // {
|
||
|
|
||
|
if (isBlockAtrule()) {
|
||
|
node.block = getBlock();
|
||
|
} else {
|
||
|
node.block = getStylesheet(true);
|
||
|
}
|
||
|
|
||
|
eat(TokenType.RightCurlyBracket);
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
parseError('Unexpected input');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function getRuleset() {
|
||
|
return {
|
||
|
type: 'Ruleset',
|
||
|
info: getInfo(),
|
||
|
selector: getSelector(),
|
||
|
block: getBlockWithBrackets()
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getSelector() {
|
||
|
var isBadSelector = false;
|
||
|
var lastComma = true;
|
||
|
var node = {
|
||
|
type: 'Selector',
|
||
|
info: getInfo(),
|
||
|
selectors: new List()
|
||
|
};
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.LeftCurlyBracket:
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.Comma:
|
||
|
if (lastComma) {
|
||
|
isBadSelector = true;
|
||
|
}
|
||
|
|
||
|
lastComma = true;
|
||
|
scanner.next();
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
if (!lastComma) {
|
||
|
isBadSelector = true;
|
||
|
}
|
||
|
|
||
|
lastComma = false;
|
||
|
node.selectors.insert(List.createItem(getSimpleSelector()));
|
||
|
|
||
|
if (node.selectors.tail.data.sequence.isEmpty()) {
|
||
|
isBadSelector = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (lastComma) {
|
||
|
isBadSelector = true;
|
||
|
// parseError('Unexpected trailing comma');
|
||
|
}
|
||
|
|
||
|
if (isBadSelector) {
|
||
|
node.selectors = new List();
|
||
|
}
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function getSimpleSelector(nested) {
|
||
|
var child = null;
|
||
|
var combinator = null;
|
||
|
var node = {
|
||
|
type: 'SimpleSelector',
|
||
|
info: getInfo(),
|
||
|
sequence: new List()
|
||
|
};
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.Comma:
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.LeftCurlyBracket:
|
||
|
if (nested) {
|
||
|
parseError('Unexpected input');
|
||
|
}
|
||
|
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.RightParenthesis:
|
||
|
if (!nested) {
|
||
|
parseError('Unexpected input');
|
||
|
}
|
||
|
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.Comment:
|
||
|
scanner.next();
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
case TokenType.Space:
|
||
|
child = null;
|
||
|
if (!combinator && node.sequence.head) {
|
||
|
combinator = getCombinator();
|
||
|
} else {
|
||
|
scanner.next();
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case TokenType.PlusSign:
|
||
|
case TokenType.GreaterThanSign:
|
||
|
case TokenType.Tilde:
|
||
|
case TokenType.Solidus:
|
||
|
if (combinator && combinator.name !== ' ') {
|
||
|
parseError('Unexpected combinator');
|
||
|
}
|
||
|
|
||
|
child = null;
|
||
|
combinator = getCombinator();
|
||
|
break;
|
||
|
|
||
|
case TokenType.FullStop:
|
||
|
child = getClass();
|
||
|
break;
|
||
|
|
||
|
case TokenType.LeftSquareBracket:
|
||
|
child = getAttribute();
|
||
|
break;
|
||
|
|
||
|
case TokenType.NumberSign:
|
||
|
child = getShash();
|
||
|
break;
|
||
|
|
||
|
case TokenType.Colon:
|
||
|
child = getPseudo();
|
||
|
break;
|
||
|
|
||
|
case TokenType.LowLine:
|
||
|
case TokenType.Identifier:
|
||
|
case TokenType.Asterisk:
|
||
|
child = getNamespacedIdentifier(false);
|
||
|
break;
|
||
|
|
||
|
case TokenType.HyphenMinus:
|
||
|
case TokenType.DecimalNumber:
|
||
|
child = tryGetPercentage() || getNamespacedIdentifier(false);
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
parseError('Unexpected input');
|
||
|
}
|
||
|
|
||
|
if (child !== null) {
|
||
|
if (combinator !== null) {
|
||
|
node.sequence.insert(List.createItem(combinator));
|
||
|
combinator = null;
|
||
|
}
|
||
|
|
||
|
node.sequence.insert(List.createItem(child));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (combinator && combinator.name !== ' ') {
|
||
|
parseError('Unexpected combinator');
|
||
|
}
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function getDeclarations() {
|
||
|
var child = null;
|
||
|
var declarations = new List();
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.RightCurlyBracket:
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.Space:
|
||
|
case TokenType.Comment:
|
||
|
scanner.next();
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
case TokenType.Semicolon: // ;
|
||
|
scanner.next();
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
child = getDeclaration();
|
||
|
}
|
||
|
|
||
|
if (child !== null) {
|
||
|
declarations.insert(List.createItem(child));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return declarations;
|
||
|
}
|
||
|
|
||
|
function getBlockWithBrackets() {
|
||
|
var info = getInfo();
|
||
|
var node;
|
||
|
|
||
|
eat(TokenType.LeftCurlyBracket);
|
||
|
node = {
|
||
|
type: 'Block',
|
||
|
info: info,
|
||
|
declarations: getDeclarations()
|
||
|
};
|
||
|
eat(TokenType.RightCurlyBracket);
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function getBlock() {
|
||
|
return {
|
||
|
type: 'Block',
|
||
|
info: getInfo(),
|
||
|
declarations: getDeclarations()
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getDeclaration(nested) {
|
||
|
var info = getInfo();
|
||
|
var property = getProperty();
|
||
|
var value;
|
||
|
|
||
|
eat(TokenType.Colon);
|
||
|
|
||
|
// check it's a filter
|
||
|
if (/filter$/.test(property.name.toLowerCase()) && checkProgid()) {
|
||
|
value = getFilterValue();
|
||
|
} else {
|
||
|
value = getValue(nested);
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
type: 'Declaration',
|
||
|
info: info,
|
||
|
property: property,
|
||
|
value: value
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getProperty() {
|
||
|
var name = '';
|
||
|
var node = {
|
||
|
type: 'Property',
|
||
|
info: getInfo(),
|
||
|
name: null
|
||
|
};
|
||
|
|
||
|
for (; scanner.token !== null; scanner.next()) {
|
||
|
var type = scanner.token.type;
|
||
|
|
||
|
if (type !== TokenType.Solidus &&
|
||
|
type !== TokenType.Asterisk &&
|
||
|
type !== TokenType.DollarSign) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
name += scanner.token.value;
|
||
|
}
|
||
|
|
||
|
node.name = name + readIdent(true);
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function getValue(nested) {
|
||
|
var child = null;
|
||
|
var node = {
|
||
|
type: 'Value',
|
||
|
info: getInfo(),
|
||
|
important: false,
|
||
|
sequence: new List()
|
||
|
};
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.RightCurlyBracket:
|
||
|
case TokenType.Semicolon:
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.RightParenthesis:
|
||
|
if (!nested) {
|
||
|
parseError('Unexpected input');
|
||
|
}
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.Space:
|
||
|
child = getS();
|
||
|
break;
|
||
|
|
||
|
case TokenType.Comment: // ignore comments
|
||
|
scanner.next();
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
case TokenType.NumberSign:
|
||
|
child = getVhash();
|
||
|
break;
|
||
|
|
||
|
case TokenType.Solidus:
|
||
|
case TokenType.Comma:
|
||
|
child = getOperator();
|
||
|
break;
|
||
|
|
||
|
case TokenType.LeftParenthesis:
|
||
|
case TokenType.LeftSquareBracket:
|
||
|
child = getBraces(SCOPE_VALUE);
|
||
|
break;
|
||
|
|
||
|
case TokenType.ExclamationMark:
|
||
|
node.important = getImportant();
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
// check for unicode range: U+0F00, U+0F00-0FFF, u+0F00??
|
||
|
if (scanner.token.type === TokenType.Identifier) {
|
||
|
var prefix = scanner.token.value;
|
||
|
if (prefix === 'U' || prefix === 'u') {
|
||
|
if (scanner.lookupType(1, TokenType.PlusSign)) {
|
||
|
scanner.next(); // U or u
|
||
|
scanner.next(); // +
|
||
|
|
||
|
child = {
|
||
|
type: 'Identifier',
|
||
|
info: getInfo(), // FIXME: wrong position
|
||
|
name: prefix + '+' + readUnicodeRange(true)
|
||
|
};
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
child = getAny(SCOPE_VALUE);
|
||
|
}
|
||
|
|
||
|
if (child !== null) {
|
||
|
node.sequence.insert(List.createItem(child));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
removeTrailingSpaces(node.sequence);
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
// any = string | percentage | dimension | number | uri | functionExpression | funktion | unary | operator | ident
|
||
|
function getAny(scope) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.String:
|
||
|
return getString();
|
||
|
|
||
|
case TokenType.LowLine:
|
||
|
case TokenType.Identifier:
|
||
|
break;
|
||
|
|
||
|
case TokenType.FullStop:
|
||
|
case TokenType.DecimalNumber:
|
||
|
case TokenType.HyphenMinus:
|
||
|
case TokenType.PlusSign:
|
||
|
var number = tryGetNumber();
|
||
|
|
||
|
if (number !== null) {
|
||
|
if (scanner.token !== null) {
|
||
|
if (scanner.token.type === TokenType.PercentSign) {
|
||
|
return getPercentage(number);
|
||
|
} else if (scanner.token.type === TokenType.Identifier) {
|
||
|
return getDimension(number.value);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return number;
|
||
|
}
|
||
|
|
||
|
if (scanner.token.type === TokenType.HyphenMinus) {
|
||
|
var next = scanner.lookup(1);
|
||
|
if (next && (next.type === TokenType.Identifier || next.type === TokenType.HyphenMinus)) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (scanner.token.type === TokenType.HyphenMinus ||
|
||
|
scanner.token.type === TokenType.PlusSign) {
|
||
|
return getOperator();
|
||
|
}
|
||
|
|
||
|
parseError('Unexpected input');
|
||
|
|
||
|
default:
|
||
|
parseError('Unexpected input');
|
||
|
}
|
||
|
|
||
|
var ident = getIdentifier(false);
|
||
|
|
||
|
if (scanner.token !== null && scanner.token.type === TokenType.LeftParenthesis) {
|
||
|
return getFunction(scope, ident);
|
||
|
}
|
||
|
|
||
|
return ident;
|
||
|
}
|
||
|
|
||
|
function readAttrselector() {
|
||
|
expectAny('Attribute selector (=, ~=, ^=, $=, *=, |=)',
|
||
|
TokenType.EqualsSign, // =
|
||
|
TokenType.Tilde, // ~=
|
||
|
TokenType.CircumflexAccent, // ^=
|
||
|
TokenType.DollarSign, // $=
|
||
|
TokenType.Asterisk, // *=
|
||
|
TokenType.VerticalLine // |=
|
||
|
);
|
||
|
|
||
|
var name;
|
||
|
|
||
|
if (scanner.token.type === TokenType.EqualsSign) {
|
||
|
name = '=';
|
||
|
scanner.next();
|
||
|
} else {
|
||
|
name = scanner.token.value + '=';
|
||
|
scanner.next();
|
||
|
eat(TokenType.EqualsSign);
|
||
|
}
|
||
|
|
||
|
return name;
|
||
|
}
|
||
|
|
||
|
// '[' S* attrib_name ']'
|
||
|
// '[' S* attrib_name S* attrib_match S* [ IDENT | STRING ] S* attrib_flags? S* ']'
|
||
|
function getAttribute() {
|
||
|
var node = {
|
||
|
type: 'Attribute',
|
||
|
info: getInfo(),
|
||
|
name: null,
|
||
|
operator: null,
|
||
|
value: null,
|
||
|
flags: null
|
||
|
};
|
||
|
|
||
|
eat(TokenType.LeftSquareBracket);
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
node.name = getNamespacedIdentifier(true);
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
if (scanner.token !== null && scanner.token.type !== TokenType.RightSquareBracket) {
|
||
|
// avoid case `[name i]`
|
||
|
if (scanner.token.type !== TokenType.Identifier) {
|
||
|
node.operator = readAttrselector();
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
if (scanner.token !== null && scanner.token.type === TokenType.String) {
|
||
|
node.value = getString();
|
||
|
} else {
|
||
|
node.value = getIdentifier(false);
|
||
|
}
|
||
|
|
||
|
readSC();
|
||
|
}
|
||
|
|
||
|
// attribute flags
|
||
|
if (scanner.token !== null && scanner.token.type === TokenType.Identifier) {
|
||
|
node.flags = scanner.token.value;
|
||
|
|
||
|
scanner.next();
|
||
|
readSC();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
eat(TokenType.RightSquareBracket);
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function getBraces(scope) {
|
||
|
var close;
|
||
|
var child = null;
|
||
|
var node = {
|
||
|
type: 'Braces',
|
||
|
info: getInfo(),
|
||
|
open: scanner.token.value,
|
||
|
close: null,
|
||
|
sequence: new List()
|
||
|
};
|
||
|
|
||
|
if (scanner.token.type === TokenType.LeftParenthesis) {
|
||
|
close = TokenType.RightParenthesis;
|
||
|
} else {
|
||
|
close = TokenType.RightSquareBracket;
|
||
|
}
|
||
|
|
||
|
// left brace
|
||
|
scanner.next();
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case close:
|
||
|
node.close = scanner.token.value;
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.Space:
|
||
|
child = getS();
|
||
|
break;
|
||
|
|
||
|
case TokenType.Comment:
|
||
|
scanner.next();
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
case TokenType.NumberSign: // ??
|
||
|
child = getVhash();
|
||
|
break;
|
||
|
|
||
|
case TokenType.LeftParenthesis:
|
||
|
case TokenType.LeftSquareBracket:
|
||
|
child = getBraces(scope);
|
||
|
break;
|
||
|
|
||
|
case TokenType.Solidus:
|
||
|
case TokenType.Asterisk:
|
||
|
case TokenType.Comma:
|
||
|
case TokenType.Colon:
|
||
|
child = getOperator();
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
child = getAny(scope);
|
||
|
}
|
||
|
|
||
|
if (child !== null) {
|
||
|
node.sequence.insert(List.createItem(child));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
removeTrailingSpaces(node.sequence);
|
||
|
|
||
|
// right brace
|
||
|
eat(close);
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
// '.' ident
|
||
|
function getClass() {
|
||
|
var info = getInfo();
|
||
|
|
||
|
eat(TokenType.FullStop);
|
||
|
|
||
|
return {
|
||
|
type: 'Class',
|
||
|
info: info,
|
||
|
name: readIdent(false)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// '#' ident
|
||
|
function getShash() {
|
||
|
var info = getInfo();
|
||
|
|
||
|
eat(TokenType.NumberSign);
|
||
|
|
||
|
return {
|
||
|
type: 'Id',
|
||
|
info: info,
|
||
|
name: readIdent(false)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// + | > | ~ | /deep/
|
||
|
function getCombinator() {
|
||
|
var info = getInfo();
|
||
|
var combinator;
|
||
|
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.Space:
|
||
|
combinator = ' ';
|
||
|
scanner.next();
|
||
|
break;
|
||
|
|
||
|
case TokenType.PlusSign:
|
||
|
case TokenType.GreaterThanSign:
|
||
|
case TokenType.Tilde:
|
||
|
combinator = scanner.token.value;
|
||
|
scanner.next();
|
||
|
break;
|
||
|
|
||
|
case TokenType.Solidus:
|
||
|
combinator = '/deep/';
|
||
|
scanner.next();
|
||
|
|
||
|
expectIdentifier('deep', true);
|
||
|
|
||
|
eat(TokenType.Solidus);
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
parseError('Combinator (+, >, ~, /deep/) is expected');
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
type: 'Combinator',
|
||
|
info: info,
|
||
|
name: combinator
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// '/*' .* '*/'
|
||
|
function getComment() {
|
||
|
var info = getInfo();
|
||
|
var value = scanner.token.value;
|
||
|
var len = value.length;
|
||
|
|
||
|
if (len > 4 && value.charAt(len - 2) === '*' && value.charAt(len - 1) === '/') {
|
||
|
len -= 2;
|
||
|
}
|
||
|
|
||
|
scanner.next();
|
||
|
|
||
|
return {
|
||
|
type: 'Comment',
|
||
|
info: info,
|
||
|
value: value.substring(2, len)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// special reader for units to avoid adjoined IE hacks (i.e. '1px\9')
|
||
|
function readUnit() {
|
||
|
if (scanner.token !== null && scanner.token.type === TokenType.Identifier) {
|
||
|
var unit = scanner.token.value;
|
||
|
var backSlashPos = unit.indexOf('\\');
|
||
|
|
||
|
// no backslash in unit name
|
||
|
if (backSlashPos === -1) {
|
||
|
scanner.next();
|
||
|
return unit;
|
||
|
}
|
||
|
|
||
|
// patch token
|
||
|
scanner.token.value = unit.substr(backSlashPos);
|
||
|
scanner.token.offset += backSlashPos;
|
||
|
scanner.token.column += backSlashPos;
|
||
|
|
||
|
// return unit w/o backslash part
|
||
|
return unit.substr(0, backSlashPos);
|
||
|
}
|
||
|
|
||
|
parseError('Identifier is expected');
|
||
|
}
|
||
|
|
||
|
// number ident
|
||
|
function getDimension(number) {
|
||
|
return {
|
||
|
type: 'Dimension',
|
||
|
info: getInfo(),
|
||
|
value: number || readNumber(),
|
||
|
unit: readUnit()
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// number "%"
|
||
|
function tryGetPercentage() {
|
||
|
var number = tryGetNumber();
|
||
|
|
||
|
if (number && scanner.token !== null && scanner.token.type === TokenType.PercentSign) {
|
||
|
return getPercentage(number);
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
function getPercentage(number) {
|
||
|
var info;
|
||
|
|
||
|
if (!number) {
|
||
|
info = getInfo();
|
||
|
number = readNumber();
|
||
|
} else {
|
||
|
info = number.info;
|
||
|
number = number.value;
|
||
|
}
|
||
|
|
||
|
eat(TokenType.PercentSign);
|
||
|
|
||
|
return {
|
||
|
type: 'Percentage',
|
||
|
info: info,
|
||
|
value: number
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// ident '(' functionBody ')' |
|
||
|
// not '(' <simpleSelector>* ')'
|
||
|
function getFunction(scope, ident) {
|
||
|
var defaultArguments = getFunctionArguments;
|
||
|
|
||
|
if (!ident) {
|
||
|
ident = getIdentifier(false);
|
||
|
}
|
||
|
|
||
|
// parse special functions
|
||
|
var name = ident.name.toLowerCase();
|
||
|
|
||
|
if (specialFunctions.hasOwnProperty(scope)) {
|
||
|
if (specialFunctions[scope].hasOwnProperty(name)) {
|
||
|
return specialFunctions[scope][name](scope, ident);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return getFunctionInternal(defaultArguments, scope, ident);
|
||
|
}
|
||
|
|
||
|
function getFunctionInternal(functionArgumentsReader, scope, ident) {
|
||
|
var args;
|
||
|
|
||
|
eat(TokenType.LeftParenthesis);
|
||
|
args = functionArgumentsReader(scope);
|
||
|
eat(TokenType.RightParenthesis);
|
||
|
|
||
|
return {
|
||
|
type: scope === SCOPE_SELECTOR ? 'FunctionalPseudo' : 'Function',
|
||
|
info: ident.info,
|
||
|
name: ident.name,
|
||
|
arguments: args
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getFunctionArguments(scope) {
|
||
|
var args = new List();
|
||
|
var argument = null;
|
||
|
var child = null;
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.RightParenthesis:
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.Space:
|
||
|
child = getS();
|
||
|
break;
|
||
|
|
||
|
case TokenType.Comment: // ignore comments
|
||
|
scanner.next();
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
case TokenType.NumberSign: // TODO: not sure it should be here
|
||
|
child = getVhash();
|
||
|
break;
|
||
|
|
||
|
case TokenType.LeftParenthesis:
|
||
|
case TokenType.LeftSquareBracket:
|
||
|
child = getBraces(scope);
|
||
|
break;
|
||
|
|
||
|
case TokenType.Comma:
|
||
|
if (argument) {
|
||
|
removeTrailingSpaces(argument.sequence);
|
||
|
} else {
|
||
|
args.insert(List.createItem({
|
||
|
type: 'Argument',
|
||
|
sequence: new List()
|
||
|
}));
|
||
|
}
|
||
|
scanner.next();
|
||
|
readSC();
|
||
|
argument = null;
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
case TokenType.Solidus:
|
||
|
case TokenType.Asterisk:
|
||
|
case TokenType.Colon:
|
||
|
case TokenType.EqualsSign:
|
||
|
child = getOperator();
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
child = getAny(scope);
|
||
|
}
|
||
|
|
||
|
if (argument === null) {
|
||
|
argument = {
|
||
|
type: 'Argument',
|
||
|
sequence: new List()
|
||
|
};
|
||
|
args.insert(List.createItem(argument));
|
||
|
}
|
||
|
|
||
|
if (child !== null) {
|
||
|
argument.sequence.insert(List.createItem(child));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (argument !== null) {
|
||
|
removeTrailingSpaces(argument.sequence);
|
||
|
}
|
||
|
|
||
|
return args;
|
||
|
}
|
||
|
|
||
|
function getVarFunction(scope, ident) {
|
||
|
return getFunctionInternal(getVarFunctionArguments, scope, ident);
|
||
|
}
|
||
|
|
||
|
function getNotFunctionArguments() {
|
||
|
var args = new List();
|
||
|
var wasSelector = false;
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.RightParenthesis:
|
||
|
if (!wasSelector) {
|
||
|
parseError('Simple selector is expected');
|
||
|
}
|
||
|
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.Comma:
|
||
|
if (!wasSelector) {
|
||
|
parseError('Simple selector is expected');
|
||
|
}
|
||
|
|
||
|
wasSelector = false;
|
||
|
scanner.next();
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
wasSelector = true;
|
||
|
args.insert(List.createItem(getSimpleSelector(true)));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return args;
|
||
|
}
|
||
|
|
||
|
function getNotFunction(scope, ident) {
|
||
|
var args;
|
||
|
|
||
|
eat(TokenType.LeftParenthesis);
|
||
|
args = getNotFunctionArguments(scope);
|
||
|
eat(TokenType.RightParenthesis);
|
||
|
|
||
|
return {
|
||
|
type: 'Negation',
|
||
|
info: ident.info,
|
||
|
// name: ident.name, // TODO: add name?
|
||
|
sequence: args // FIXME: -> arguments?
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// var '(' ident (',' <declaration-value>)? ')'
|
||
|
function getVarFunctionArguments() { // TODO: special type Variable?
|
||
|
var args = new List();
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
args.insert(List.createItem({
|
||
|
type: 'Argument',
|
||
|
sequence: new List([getIdentifier(true)])
|
||
|
}));
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
if (scanner.token !== null && scanner.token.type === TokenType.Comma) {
|
||
|
eat(TokenType.Comma);
|
||
|
readSC();
|
||
|
|
||
|
args.insert(List.createItem({
|
||
|
type: 'Argument',
|
||
|
sequence: new List([getValue(true)])
|
||
|
}));
|
||
|
|
||
|
readSC();
|
||
|
}
|
||
|
|
||
|
return args;
|
||
|
}
|
||
|
|
||
|
// url '(' ws* (string | raw) ws* ')'
|
||
|
function getUri(scope, ident) {
|
||
|
var node = {
|
||
|
type: 'Url',
|
||
|
info: ident.info,
|
||
|
// name: ident.name,
|
||
|
value: null
|
||
|
};
|
||
|
|
||
|
eat(TokenType.LeftParenthesis); // (
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
if (scanner.token.type === TokenType.String) {
|
||
|
node.value = getString();
|
||
|
readSC();
|
||
|
} else {
|
||
|
var rawInfo = getInfo();
|
||
|
var raw = '';
|
||
|
|
||
|
for (; scanner.token !== null; scanner.next()) {
|
||
|
var type = scanner.token.type;
|
||
|
|
||
|
if (type === TokenType.Space ||
|
||
|
type === TokenType.LeftParenthesis ||
|
||
|
type === TokenType.RightParenthesis) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
raw += scanner.token.value;
|
||
|
}
|
||
|
|
||
|
node.value = {
|
||
|
type: 'Raw',
|
||
|
info: rawInfo,
|
||
|
value: raw
|
||
|
};
|
||
|
|
||
|
readSC();
|
||
|
}
|
||
|
|
||
|
eat(TokenType.RightParenthesis); // )
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
// expression '(' raw ')'
|
||
|
function getOldIEExpression(scope, ident) {
|
||
|
var balance = 0;
|
||
|
var raw = '';
|
||
|
|
||
|
eat(TokenType.LeftParenthesis);
|
||
|
|
||
|
for (; scanner.token !== null; scanner.next()) {
|
||
|
if (scanner.token.type === TokenType.RightParenthesis) {
|
||
|
if (balance === 0) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
balance--;
|
||
|
} else if (scanner.token.type === TokenType.LeftParenthesis) {
|
||
|
balance++;
|
||
|
}
|
||
|
|
||
|
raw += scanner.token.value;
|
||
|
}
|
||
|
|
||
|
eat(TokenType.RightParenthesis);
|
||
|
|
||
|
return {
|
||
|
type: 'Function',
|
||
|
info: ident.info,
|
||
|
name: ident.name,
|
||
|
arguments: new List([{
|
||
|
type: 'Argument',
|
||
|
sequence: new List([{
|
||
|
type: 'Raw',
|
||
|
value: raw
|
||
|
}])
|
||
|
}])
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function readUnicodeRange(tryNext) {
|
||
|
var hex = '';
|
||
|
|
||
|
for (; scanner.token !== null; scanner.next()) {
|
||
|
if (scanner.token.type !== TokenType.DecimalNumber &&
|
||
|
scanner.token.type !== TokenType.Identifier) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
hex += scanner.token.value;
|
||
|
}
|
||
|
|
||
|
if (!/^[0-9a-f]{1,6}$/i.test(hex)) {
|
||
|
parseError('Unexpected input');
|
||
|
}
|
||
|
|
||
|
// U+abc???
|
||
|
if (tryNext) {
|
||
|
for (; hex.length < 6 && scanner.token !== null; scanner.next()) {
|
||
|
if (scanner.token.type !== TokenType.QuestionMark) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
hex += scanner.token.value;
|
||
|
tryNext = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// U+aaa-bbb
|
||
|
if (tryNext) {
|
||
|
if (scanner.token !== null && scanner.token.type === TokenType.HyphenMinus) {
|
||
|
scanner.next();
|
||
|
|
||
|
var next = readUnicodeRange(false);
|
||
|
|
||
|
if (!next) {
|
||
|
parseError('Unexpected input');
|
||
|
}
|
||
|
|
||
|
hex += '-' + next;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return hex;
|
||
|
}
|
||
|
|
||
|
function readIdent(varAllowed) {
|
||
|
var name = '';
|
||
|
|
||
|
// optional first -
|
||
|
if (scanner.token !== null && scanner.token.type === TokenType.HyphenMinus) {
|
||
|
name = '-';
|
||
|
scanner.next();
|
||
|
|
||
|
if (varAllowed && scanner.token !== null && scanner.token.type === TokenType.HyphenMinus) {
|
||
|
name = '--';
|
||
|
scanner.next();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
expectAny('Identifier',
|
||
|
TokenType.LowLine,
|
||
|
TokenType.Identifier
|
||
|
);
|
||
|
|
||
|
if (scanner.token !== null) {
|
||
|
name += scanner.token.value;
|
||
|
scanner.next();
|
||
|
|
||
|
for (; scanner.token !== null; scanner.next()) {
|
||
|
var type = scanner.token.type;
|
||
|
|
||
|
if (type !== TokenType.LowLine &&
|
||
|
type !== TokenType.Identifier &&
|
||
|
type !== TokenType.DecimalNumber &&
|
||
|
type !== TokenType.HyphenMinus) {
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
name += scanner.token.value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return name;
|
||
|
}
|
||
|
|
||
|
function getNamespacedIdentifier(checkColon) {
|
||
|
if (scanner.token === null) {
|
||
|
parseError('Unexpected end of input');
|
||
|
}
|
||
|
|
||
|
var info = getInfo();
|
||
|
var name;
|
||
|
|
||
|
if (scanner.token.type === TokenType.Asterisk) {
|
||
|
checkColon = false;
|
||
|
name = '*';
|
||
|
scanner.next();
|
||
|
} else {
|
||
|
name = readIdent(false);
|
||
|
}
|
||
|
|
||
|
if (scanner.token !== null) {
|
||
|
if (scanner.token.type === TokenType.VerticalLine &&
|
||
|
scanner.lookupType(1, TokenType.EqualsSign) === false) {
|
||
|
name += '|';
|
||
|
|
||
|
if (scanner.next() !== null) {
|
||
|
if (scanner.token.type === TokenType.HyphenMinus ||
|
||
|
scanner.token.type === TokenType.Identifier ||
|
||
|
scanner.token.type === TokenType.LowLine) {
|
||
|
name += readIdent(false);
|
||
|
} else if (scanner.token.type === TokenType.Asterisk) {
|
||
|
checkColon = false;
|
||
|
name += '*';
|
||
|
scanner.next();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (checkColon && scanner.token !== null && scanner.token.type === TokenType.Colon) {
|
||
|
scanner.next();
|
||
|
name += ':' + readIdent(false);
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
type: 'Identifier',
|
||
|
info: info,
|
||
|
name: name
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getIdentifier(varAllowed) {
|
||
|
return {
|
||
|
type: 'Identifier',
|
||
|
info: getInfo(),
|
||
|
name: readIdent(varAllowed)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// ! ws* important
|
||
|
function getImportant() { // TODO?
|
||
|
// var info = getInfo();
|
||
|
|
||
|
eat(TokenType.ExclamationMark);
|
||
|
|
||
|
readSC();
|
||
|
|
||
|
// return {
|
||
|
// type: 'Identifier',
|
||
|
// info: info,
|
||
|
// name: readIdent(false)
|
||
|
// };
|
||
|
|
||
|
expectIdentifier('important');
|
||
|
|
||
|
readIdent(false);
|
||
|
|
||
|
// should return identifier in future for original source restoring as is
|
||
|
// returns true for now since it's fit to optimizer purposes
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// odd | even | number? n
|
||
|
function getNth() {
|
||
|
expectAny('Number, odd or even',
|
||
|
TokenType.Identifier,
|
||
|
TokenType.DecimalNumber
|
||
|
);
|
||
|
|
||
|
var info = getInfo();
|
||
|
var value = scanner.token.value;
|
||
|
var cmpValue;
|
||
|
|
||
|
if (scanner.token.type === TokenType.DecimalNumber) {
|
||
|
var next = scanner.lookup(1);
|
||
|
if (next !== null &&
|
||
|
next.type === TokenType.Identifier &&
|
||
|
next.value.toLowerCase() === 'n') {
|
||
|
value += next.value;
|
||
|
scanner.next();
|
||
|
}
|
||
|
} else {
|
||
|
var cmpValue = value.toLowerCase();
|
||
|
if (cmpValue !== 'odd' && cmpValue !== 'even' && cmpValue !== 'n') {
|
||
|
parseError('Unexpected identifier');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
scanner.next();
|
||
|
|
||
|
return {
|
||
|
type: 'Nth',
|
||
|
info: info,
|
||
|
value: value
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getNthSelector() {
|
||
|
var info = getInfo();
|
||
|
var sequence = new List();
|
||
|
var node;
|
||
|
var child = null;
|
||
|
|
||
|
eat(TokenType.Colon);
|
||
|
expectIdentifier('nth', false);
|
||
|
|
||
|
node = {
|
||
|
type: 'FunctionalPseudo',
|
||
|
info: info,
|
||
|
name: readIdent(false),
|
||
|
arguments: new List([{
|
||
|
type: 'Argument',
|
||
|
sequence: sequence
|
||
|
}])
|
||
|
};
|
||
|
|
||
|
eat(TokenType.LeftParenthesis);
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.RightParenthesis:
|
||
|
break scan;
|
||
|
|
||
|
case TokenType.Space:
|
||
|
case TokenType.Comment:
|
||
|
scanner.next();
|
||
|
child = null;
|
||
|
break;
|
||
|
|
||
|
case TokenType.HyphenMinus:
|
||
|
case TokenType.PlusSign:
|
||
|
child = getOperator();
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
child = getNth();
|
||
|
}
|
||
|
|
||
|
if (child !== null) {
|
||
|
sequence.insert(List.createItem(child));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
eat(TokenType.RightParenthesis);
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function readNumber() {
|
||
|
var wasDigits = false;
|
||
|
var number = '';
|
||
|
var offset = 0;
|
||
|
|
||
|
if (scanner.lookupType(offset, TokenType.HyphenMinus)) {
|
||
|
number = '-';
|
||
|
offset++;
|
||
|
}
|
||
|
|
||
|
if (scanner.lookupType(offset, TokenType.DecimalNumber)) {
|
||
|
wasDigits = true;
|
||
|
number += scanner.lookup(offset).value;
|
||
|
offset++;
|
||
|
}
|
||
|
|
||
|
if (scanner.lookupType(offset, TokenType.FullStop)) {
|
||
|
number += '.';
|
||
|
offset++;
|
||
|
}
|
||
|
|
||
|
if (scanner.lookupType(offset, TokenType.DecimalNumber)) {
|
||
|
wasDigits = true;
|
||
|
number += scanner.lookup(offset).value;
|
||
|
offset++;
|
||
|
}
|
||
|
|
||
|
if (wasDigits) {
|
||
|
while (offset--) {
|
||
|
scanner.next();
|
||
|
}
|
||
|
|
||
|
return number;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
function tryGetNumber() {
|
||
|
var info = getInfo();
|
||
|
var number = readNumber();
|
||
|
|
||
|
if (number !== null) {
|
||
|
return {
|
||
|
type: 'Number',
|
||
|
info: info,
|
||
|
value: number
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// '/' | '*' | ',' | ':' | '=' | '+' | '-'
|
||
|
// TODO: remove '=' since it's wrong operator, but theat as operator
|
||
|
// to make old things like `filter: alpha(opacity=0)` works
|
||
|
function getOperator() {
|
||
|
var node = {
|
||
|
type: 'Operator',
|
||
|
info: getInfo(),
|
||
|
value: scanner.token.value
|
||
|
};
|
||
|
|
||
|
scanner.next();
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function getFilterValue() { // TODO
|
||
|
var progid;
|
||
|
var node = {
|
||
|
type: 'Value',
|
||
|
info: getInfo(),
|
||
|
important: false,
|
||
|
sequence: new List()
|
||
|
};
|
||
|
|
||
|
while (progid = checkProgid()) {
|
||
|
node.sequence.insert(List.createItem(getProgid(progid)));
|
||
|
}
|
||
|
|
||
|
readSC(node);
|
||
|
|
||
|
if (scanner.token !== null && scanner.token.type === TokenType.ExclamationMark) {
|
||
|
node.important = getImportant();
|
||
|
}
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
// 'progid:' ws* 'DXImageTransform.Microsoft.' ident ws* '(' .* ')'
|
||
|
function checkProgid() {
|
||
|
function checkSC(offset) {
|
||
|
for (var cursor; cursor = scanner.lookup(offset); offset++) {
|
||
|
if (cursor.type !== TokenType.Space &&
|
||
|
cursor.type !== TokenType.Comment) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return offset;
|
||
|
}
|
||
|
|
||
|
var offset = checkSC(0);
|
||
|
|
||
|
if (scanner.lookup(offset + 1) === null ||
|
||
|
scanner.lookup(offset + 0).value.toLowerCase() !== 'progid' ||
|
||
|
scanner.lookup(offset + 1).type !== TokenType.Colon) {
|
||
|
return false; // fail
|
||
|
}
|
||
|
|
||
|
offset += 2;
|
||
|
offset = checkSC(offset);
|
||
|
|
||
|
if (scanner.lookup(offset + 5) === null ||
|
||
|
scanner.lookup(offset + 0).value.toLowerCase() !== 'dximagetransform' ||
|
||
|
scanner.lookup(offset + 1).type !== TokenType.FullStop ||
|
||
|
scanner.lookup(offset + 2).value.toLowerCase() !== 'microsoft' ||
|
||
|
scanner.lookup(offset + 3).type !== TokenType.FullStop ||
|
||
|
scanner.lookup(offset + 4).type !== TokenType.Identifier) {
|
||
|
return false; // fail
|
||
|
}
|
||
|
|
||
|
offset += 5;
|
||
|
offset = checkSC(offset);
|
||
|
|
||
|
if (scanner.lookupType(offset, TokenType.LeftParenthesis) === false) {
|
||
|
return false; // fail
|
||
|
}
|
||
|
|
||
|
for (var cursor; cursor = scanner.lookup(offset); offset++) {
|
||
|
if (cursor.type === TokenType.RightParenthesis) {
|
||
|
return cursor;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
function getProgid(progidEnd) {
|
||
|
var value = '';
|
||
|
var node = {
|
||
|
type: 'Progid',
|
||
|
info: getInfo(),
|
||
|
value: null
|
||
|
};
|
||
|
|
||
|
if (!progidEnd) {
|
||
|
progidEnd = checkProgid();
|
||
|
}
|
||
|
|
||
|
if (!progidEnd) {
|
||
|
parseError('progid is expected');
|
||
|
}
|
||
|
|
||
|
readSC(node);
|
||
|
|
||
|
var rawInfo = getInfo();
|
||
|
for (; scanner.token && scanner.token !== progidEnd; scanner.next()) {
|
||
|
value += scanner.token.value;
|
||
|
}
|
||
|
|
||
|
eat(TokenType.RightParenthesis);
|
||
|
value += ')';
|
||
|
|
||
|
node.value = {
|
||
|
type: 'Raw',
|
||
|
info: rawInfo,
|
||
|
value: value
|
||
|
};
|
||
|
|
||
|
readSC(node);
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
// <pseudo-element> | <nth-selector> | <pseudo-class>
|
||
|
function getPseudo() {
|
||
|
var next = scanner.lookup(1);
|
||
|
|
||
|
if (next === null) {
|
||
|
scanner.next();
|
||
|
parseError('Colon or identifier is expected');
|
||
|
}
|
||
|
|
||
|
if (next.type === TokenType.Colon) {
|
||
|
return getPseudoElement();
|
||
|
}
|
||
|
|
||
|
if (next.type === TokenType.Identifier &&
|
||
|
next.value.toLowerCase() === 'nth') {
|
||
|
return getNthSelector();
|
||
|
}
|
||
|
|
||
|
return getPseudoClass();
|
||
|
}
|
||
|
|
||
|
// :: ident
|
||
|
function getPseudoElement() {
|
||
|
var info = getInfo();
|
||
|
|
||
|
eat(TokenType.Colon);
|
||
|
eat(TokenType.Colon);
|
||
|
|
||
|
return {
|
||
|
type: 'PseudoElement',
|
||
|
info: info,
|
||
|
name: readIdent(false)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// : ( ident | function )
|
||
|
function getPseudoClass() {
|
||
|
var info = getInfo();
|
||
|
var ident = eat(TokenType.Colon) && getIdentifier(false);
|
||
|
|
||
|
if (scanner.token !== null && scanner.token.type === TokenType.LeftParenthesis) {
|
||
|
return getFunction(SCOPE_SELECTOR, ident);
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
type: 'PseudoClass',
|
||
|
info: info,
|
||
|
name: ident.name
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// ws
|
||
|
function getS() {
|
||
|
var node = {
|
||
|
type: 'Space'
|
||
|
// value: scanner.token.value
|
||
|
};
|
||
|
|
||
|
scanner.next();
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function readSC() {
|
||
|
// var nodes = [];
|
||
|
|
||
|
scan:
|
||
|
while (scanner.token !== null) {
|
||
|
switch (scanner.token.type) {
|
||
|
case TokenType.Space:
|
||
|
scanner.next();
|
||
|
// nodes.push(getS());
|
||
|
break;
|
||
|
|
||
|
case TokenType.Comment:
|
||
|
scanner.next();
|
||
|
// nodes.push(getComment());
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
break scan;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
|
||
|
// return nodes.length ? new List(nodes) : null;
|
||
|
}
|
||
|
|
||
|
// node: String
|
||
|
function getString() {
|
||
|
var node = {
|
||
|
type: 'String',
|
||
|
info: getInfo(),
|
||
|
value: scanner.token.value
|
||
|
};
|
||
|
|
||
|
scanner.next();
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
// # ident
|
||
|
function getVhash() {
|
||
|
var info = getInfo();
|
||
|
var value;
|
||
|
|
||
|
eat(TokenType.NumberSign);
|
||
|
|
||
|
expectAny('Number or identifier',
|
||
|
TokenType.DecimalNumber,
|
||
|
TokenType.Identifier
|
||
|
);
|
||
|
|
||
|
value = scanner.token.value;
|
||
|
|
||
|
if (scanner.token.type === TokenType.DecimalNumber &&
|
||
|
scanner.lookupType(1, TokenType.Identifier)) {
|
||
|
scanner.next();
|
||
|
value += scanner.token.value;
|
||
|
}
|
||
|
|
||
|
scanner.next();
|
||
|
|
||
|
return {
|
||
|
type: 'Hash',
|
||
|
info: info,
|
||
|
value: value
|
||
|
};
|
||
|
}
|
||
|
|
||
|
module.exports = function parse(source, options) {
|
||
|
var ast;
|
||
|
|
||
|
if (!options || typeof options !== 'object') {
|
||
|
options = {};
|
||
|
}
|
||
|
|
||
|
var context = options.context || 'stylesheet';
|
||
|
needPositions = Boolean(options.positions);
|
||
|
filename = options.filename || '<unknown>';
|
||
|
|
||
|
if (!initialContext.hasOwnProperty(context)) {
|
||
|
throw new Error('Unknown context `' + context + '`');
|
||
|
}
|
||
|
|
||
|
scanner = new Scanner(source, blockMode.hasOwnProperty(context), options.line, options.column);
|
||
|
scanner.next();
|
||
|
ast = initialContext[context]();
|
||
|
|
||
|
scanner = null;
|
||
|
|
||
|
// console.log(JSON.stringify(ast, null, 4));
|
||
|
return ast;
|
||
|
};
|