blob: 89c1d430e599c50bc8a14c8d297ad9a24ae61173 [file] [log] [blame]
'use strict';
var SyntaxParseError = require('./error').SyntaxParseError;
var TAB = 9;
var N = 10;
var F = 12;
var R = 13;
var SPACE = 32;
var EXCLAMATIONMARK = 33; // !
var NUMBERSIGN = 35; // #
var PERCENTSIGN = 37; // %
var AMPERSAND = 38; // &
var APOSTROPHE = 39; // '
var LEFTPARENTHESIS = 40; // (
var RIGHTPARENTHESIS = 41; // )
var ASTERISK = 42; // *
var PLUSSIGN = 43; // +
var COMMA = 44; // ,
var SOLIDUS = 47; // /
var LESSTHANSIGN = 60; // <
var GREATERTHANSIGN = 62; // >
var QUESTIONMARK = 63; // ?
var LEFTSQUAREBRACKET = 91; // [
var RIGHTSQUAREBRACKET = 93; // ]
var LEFTCURLYBRACKET = 123; // {
var VERTICALLINE = 124; // |
var RIGHTCURLYBRACKET = 125; // }
var COMBINATOR_PRECEDENCE = {
' ': 1,
'&&': 2,
'||': 3,
'|': 4
};
var MULTIPLIER_DEFAULT = {
comma: false,
min: 1,
max: 1
};
var MULTIPLIER_ZERO_OR_MORE = {
comma: false,
min: 0,
max: 0
};
var MULTIPLIER_ONE_OR_MORE = {
comma: false,
min: 1,
max: 0
};
var MULTIPLIER_ONE_OR_MORE_COMMA_SEPARATED = {
comma: true,
min: 1,
max: 0
};
var MULTIPLIER_ZERO_OR_ONE = {
comma: false,
min: 0,
max: 1
};
var NAME_CHAR = (function() {
var array = typeof Uint32Array === 'function' ? new Uint32Array(128) : new Array(128);
for (var i = 0; i < 128; i++) {
array[i] = /[a-zA-Z0-9\-]/.test(String.fromCharCode(i)) ? 1 : 0;
}
return array;
})();
var Tokenizer = function(str) {
this.str = str;
this.pos = 0;
};
Tokenizer.prototype = {
charCode: function() {
return this.pos < this.str.length ? this.str.charCodeAt(this.pos) : 0;
},
nextCharCode: function() {
return this.pos + 1 < this.str.length ? this.str.charCodeAt(this.pos + 1) : 0;
},
substringToPos: function(end) {
return this.str.substring(this.pos, this.pos = end);
},
eat: function(code) {
if (this.charCode() !== code) {
error(this, this.pos, 'Expect `' + String.fromCharCode(code) + '`');
}
this.pos++;
}
};
function scanSpaces(tokenizer) {
var end = tokenizer.pos + 1;
for (; end < tokenizer.str.length; end++) {
var code = tokenizer.str.charCodeAt(end);
if (code !== R && code !== N && code !== F && code !== SPACE && code !== TAB) {
break;
}
}
return tokenizer.substringToPos(end);
}
function scanWord(tokenizer) {
var end = tokenizer.pos;
for (; end < tokenizer.str.length; end++) {
var code = tokenizer.str.charCodeAt(end);
if (code >= 128 || NAME_CHAR[code] === 0) {
break;
}
}
if (tokenizer.pos === end) {
error(tokenizer, tokenizer.pos, 'Expect a keyword');
}
return tokenizer.substringToPos(end);
}
function scanNumber(tokenizer) {
var end = tokenizer.pos;
for (; end < tokenizer.str.length; end++) {
var code = tokenizer.str.charCodeAt(end);
if (code < 48 || code > 57) {
break;
}
}
if (tokenizer.pos === end) {
error(tokenizer, tokenizer.pos, 'Expect a number');
}
return tokenizer.substringToPos(end);
}
function scanString(tokenizer) {
var end = tokenizer.str.indexOf('\'', tokenizer.pos + 1);
if (end === -1) {
error(tokenizer, tokenizer.str.length, 'Expect a quote');
}
return tokenizer.substringToPos(end + 1);
}
function readMultiplierRange(tokenizer, comma) {
var min = null;
var max = null;
tokenizer.eat(LEFTCURLYBRACKET);
min = scanNumber(tokenizer);
if (tokenizer.charCode() === COMMA) {
tokenizer.pos++;
if (tokenizer.charCode() !== RIGHTCURLYBRACKET) {
max = scanNumber(tokenizer);
}
} else {
max = min;
}
tokenizer.eat(RIGHTCURLYBRACKET);
return {
comma: comma,
min: Number(min),
max: max ? Number(max) : 0
};
}
function readMultiplier(tokenizer) {
switch (tokenizer.charCode()) {
case ASTERISK:
tokenizer.pos++;
return MULTIPLIER_ZERO_OR_MORE;
case PLUSSIGN:
tokenizer.pos++;
return MULTIPLIER_ONE_OR_MORE;
case QUESTIONMARK:
tokenizer.pos++;
return MULTIPLIER_ZERO_OR_ONE;
case NUMBERSIGN:
tokenizer.pos++;
if (tokenizer.charCode() !== LEFTCURLYBRACKET) {
return MULTIPLIER_ONE_OR_MORE_COMMA_SEPARATED;
}
return readMultiplierRange(tokenizer, true);
case LEFTCURLYBRACKET:
return readMultiplierRange(tokenizer, false);
}
return MULTIPLIER_DEFAULT;
}
function maybeMultiplied(tokenizer, node) {
var multiplier = readMultiplier(tokenizer);
if (multiplier !== MULTIPLIER_DEFAULT) {
return {
type: 'Group',
terms: [node],
combinator: '|', // `|` combinator is simplest in implementation (and therefore faster)
disallowEmpty: false,
multiplier: multiplier,
explicit: false
};
}
return node;
}
function readProperty(tokenizer) {
var name;
tokenizer.eat(LESSTHANSIGN);
tokenizer.eat(APOSTROPHE);
name = scanWord(tokenizer);
tokenizer.eat(APOSTROPHE);
tokenizer.eat(GREATERTHANSIGN);
return maybeMultiplied(tokenizer, {
type: 'Property',
name: name
});
}
function readType(tokenizer) {
var name;
tokenizer.eat(LESSTHANSIGN);
name = scanWord(tokenizer);
if (tokenizer.charCode() === LEFTPARENTHESIS &&
tokenizer.nextCharCode() === RIGHTPARENTHESIS) {
tokenizer.pos += 2;
name += '()';
}
tokenizer.eat(GREATERTHANSIGN);
return maybeMultiplied(tokenizer, {
type: 'Type',
name: name
});
}
function readKeywordOrFunction(tokenizer) {
var children = null;
var name;
name = scanWord(tokenizer);
if (tokenizer.charCode() === LEFTPARENTHESIS) {
tokenizer.pos++;
children = readImplicitGroup(tokenizer);
tokenizer.eat(RIGHTPARENTHESIS);
return maybeMultiplied(tokenizer, {
type: 'Function',
name: name,
children: children
});
}
return maybeMultiplied(tokenizer, {
type: 'Keyword',
name: name
});
}
function regroupTerms(terms, combinators) {
function createGroup(terms, combinator) {
return {
type: 'Group',
terms: terms,
combinator: combinator,
disallowEmpty: false,
multiplier: MULTIPLIER_DEFAULT,
explicit: false
};
}
combinators = Object.keys(combinators).sort(function(a, b) {
return COMBINATOR_PRECEDENCE[a] - COMBINATOR_PRECEDENCE[b];
});
while (combinators.length > 0) {
var combinator = combinators.shift();
for (var i = 0, subgroupStart = 0; i < terms.length; i++) {
var term = terms[i];
if (term.type === 'Combinator') {
if (term.value === combinator) {
if (subgroupStart === -1) {
subgroupStart = i - 1;
}
terms.splice(i, 1);
i--;
} else {
if (subgroupStart !== -1 && i - subgroupStart > 1) {
terms.splice(
subgroupStart,
i - subgroupStart,
createGroup(terms.slice(subgroupStart, i), combinator)
);
i = subgroupStart + 1;
}
subgroupStart = -1;
}
}
}
if (subgroupStart !== -1 && combinators.length) {
terms.splice(
subgroupStart,
i - subgroupStart,
createGroup(terms.slice(subgroupStart, i), combinator)
);
}
}
return combinator;
}
function readImplicitGroup(tokenizer) {
var terms = [];
var combinators = {};
var token;
var prevToken = null;
var prevTokenPos = tokenizer.pos;
while (token = peek(tokenizer)) {
if (token.type !== 'Spaces') {
if (token.type === 'Combinator') {
// check for combinator in group beginning and double combinator sequence
if (prevToken === null || prevToken.type === 'Combinator') {
error(tokenizer, prevTokenPos, 'Unexpected combinator');
}
combinators[token.value] = true;
} else if (prevToken !== null && prevToken.type !== 'Combinator') {
combinators[' '] = true; // a b
terms.push({
type: 'Combinator',
value: ' '
});
}
terms.push(token);
prevToken = token;
prevTokenPos = tokenizer.pos;
}
}
// check for combinator in group ending
if (prevToken !== null && prevToken.type === 'Combinator') {
error(tokenizer, tokenizer.pos - prevTokenPos, 'Unexpected combinator');
}
return {
type: 'Group',
terms: terms,
combinator: regroupTerms(terms, combinators) || ' ',
disallowEmpty: false,
multiplier: MULTIPLIER_DEFAULT,
explicit: false
};
}
function readGroup(tokenizer) {
var result;
tokenizer.eat(LEFTSQUAREBRACKET);
result = readImplicitGroup(tokenizer);
tokenizer.eat(RIGHTSQUAREBRACKET);
result.explicit = true;
result.multiplier = readMultiplier(tokenizer);
if (tokenizer.charCode() === EXCLAMATIONMARK) {
tokenizer.pos++;
result.disallowEmpty = true;
}
return result;
}
function peek(tokenizer) {
var code = tokenizer.charCode();
if (code < 128 && NAME_CHAR[code] === 1) {
return readKeywordOrFunction(tokenizer);
}
switch (code) {
case LEFTSQUAREBRACKET:
return readGroup(tokenizer);
case LESSTHANSIGN:
if (tokenizer.nextCharCode() === APOSTROPHE) {
return readProperty(tokenizer);
} else {
return readType(tokenizer);
}
case VERTICALLINE:
return {
type: 'Combinator',
value: tokenizer.substringToPos(tokenizer.nextCharCode() === VERTICALLINE ? tokenizer.pos + 2 : tokenizer.pos + 1)
};
case AMPERSAND:
tokenizer.pos++;
tokenizer.eat(AMPERSAND);
return {
type: 'Combinator',
value: '&&'
};
case COMMA:
tokenizer.pos++;
return {
type: 'Comma',
value: ','
};
case SOLIDUS:
tokenizer.pos++;
return {
type: 'Slash',
value: '/'
};
case PERCENTSIGN: // looks like exception, needs for attr()'s <type-or-unit>
tokenizer.pos++;
return {
type: 'Percent',
value: '%'
};
case LEFTPARENTHESIS:
tokenizer.pos++;
var children = readImplicitGroup(tokenizer);
tokenizer.eat(RIGHTPARENTHESIS);
return {
type: 'Parentheses',
children: children
};
case APOSTROPHE:
return {
type: 'String',
value: scanString(tokenizer)
};
case SPACE:
case TAB:
case N:
case R:
case F:
return {
type: 'Spaces',
value: scanSpaces(tokenizer)
};
}
}
function error(tokenizer, pos, msg) {
throw new SyntaxParseError(msg || 'Unexpected input', tokenizer.str, pos);
}
function parse(str) {
var tokenizer = new Tokenizer(str);
var result = readImplicitGroup(tokenizer);
if (tokenizer.pos !== str.length) {
error(tokenizer, tokenizer.pos);
}
// reduce redundant groups with single group term
if (result.terms.length === 1 && result.terms[0].type === 'Group') {
result = result.terms[0];
}
return result;
}
// warm up parse to elimitate code branches that never execute
// fix soft deoptimizations (insufficient type feedback)
parse('[a&&<b>#|<\'c\'>*||e(){2,} f{2} /,(% g#{1,2})]!');
module.exports = parse;