|
|
var Tokenizer = require('./tokenizer'); var TAB = 9; var N = 10; var F = 12; var R = 13; var SPACE = 32; var EXCLAMATIONMARK = 33; // !
var NUMBERSIGN = 35; // #
var AMPERSAND = 38; // &
var APOSTROPHE = 39; // '
var LEFTPARENTHESIS = 40; // (
var RIGHTPARENTHESIS = 41; // )
var ASTERISK = 42; // *
var PLUSSIGN = 43; // +
var COMMA = 44; // ,
var HYPERMINUS = 45; // -
var LESSTHANSIGN = 60; // <
var GREATERTHANSIGN = 62; // >
var QUESTIONMARK = 63; // ?
var COMMERCIALAT = 64; // @
var LEFTSQUAREBRACKET = 91; // [
var RIGHTSQUAREBRACKET = 93; // ]
var LEFTCURLYBRACKET = 123; // {
var VERTICALLINE = 124; // |
var RIGHTCURLYBRACKET = 125; // }
var INFINITY = 8734; // ∞
var NAME_CHAR = createCharMap(function(ch) { return /[a-zA-Z0-9\-]/.test(ch); }); var COMBINATOR_PRECEDENCE = { ' ': 1, '&&': 2, '||': 3, '|': 4 };
function createCharMap(fn) { var array = typeof Uint32Array === 'function' ? new Uint32Array(128) : new Array(128); for (var i = 0; i < 128; i++) { array[i] = fn(String.fromCharCode(i)) ? 1 : 0; } return array; }
function scanSpaces(tokenizer) { return tokenizer.substringToPos( tokenizer.findWsEnd(tokenizer.pos) ); }
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) { tokenizer.error('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) { tokenizer.error('Expect a number'); }
return tokenizer.substringToPos(end); }
function scanString(tokenizer) { var end = tokenizer.str.indexOf('\'', tokenizer.pos + 1);
if (end === -1) { tokenizer.pos = tokenizer.str.length; tokenizer.error('Expect an apostrophe'); }
return tokenizer.substringToPos(end + 1); }
function readMultiplierRange(tokenizer) { 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 { min: Number(min), max: max ? Number(max) : 0 }; }
function readMultiplier(tokenizer) { var range = null; var comma = false;
switch (tokenizer.charCode()) { case ASTERISK: tokenizer.pos++;
range = { min: 0, max: 0 };
break;
case PLUSSIGN: tokenizer.pos++;
range = { min: 1, max: 0 };
break;
case QUESTIONMARK: tokenizer.pos++;
range = { min: 0, max: 1 };
break;
case NUMBERSIGN: tokenizer.pos++;
comma = true;
if (tokenizer.charCode() === LEFTCURLYBRACKET) { range = readMultiplierRange(tokenizer); } else { range = { min: 1, max: 0 }; }
break;
case LEFTCURLYBRACKET: range = readMultiplierRange(tokenizer); break;
default: return null; }
return { type: 'Multiplier', comma: comma, min: range.min, max: range.max, term: null }; }
function maybeMultiplied(tokenizer, node) { var multiplier = readMultiplier(tokenizer);
if (multiplier !== null) { multiplier.term = node; return multiplier; }
return node; }
function maybeToken(tokenizer) { var ch = tokenizer.peek();
if (ch === '') { return null; }
return { type: 'Token', value: ch }; }
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 }); }
// https://drafts.csswg.org/css-values-3/#numeric-ranges
// 4.1. Range Restrictions and Range Definition Notation
//
// Range restrictions can be annotated in the numeric type notation using CSS bracketed
// range notation—[min,max]—within the angle brackets, after the identifying keyword,
// indicating a closed range between (and including) min and max.
// For example, <integer [0, 10]> indicates an integer between 0 and 10, inclusive.
function readTypeRange(tokenizer) { // use null for Infinity to make AST format JSON serializable/deserializable
var min = null; // -Infinity
var max = null; // Infinity
var sign = 1;
tokenizer.eat(LEFTSQUAREBRACKET);
if (tokenizer.charCode() === HYPERMINUS) { tokenizer.peek(); sign = -1; }
if (sign == -1 && tokenizer.charCode() === INFINITY) { tokenizer.peek(); } else { min = sign * Number(scanNumber(tokenizer)); }
scanSpaces(tokenizer); tokenizer.eat(COMMA); scanSpaces(tokenizer);
if (tokenizer.charCode() === INFINITY) { tokenizer.peek(); } else { sign = 1;
if (tokenizer.charCode() === HYPERMINUS) { tokenizer.peek(); sign = -1; }
max = sign * Number(scanNumber(tokenizer)); }
tokenizer.eat(RIGHTSQUAREBRACKET);
// If no range is indicated, either by using the bracketed range notation
// or in the property description, then [−∞,∞] is assumed.
if (min === null && max === null) { return null; }
return { type: 'Range', min: min, max: max }; }
function readType(tokenizer) { var name; var opts = null;
tokenizer.eat(LESSTHANSIGN); name = scanWord(tokenizer);
if (tokenizer.charCode() === LEFTPARENTHESIS && tokenizer.nextCharCode() === RIGHTPARENTHESIS) { tokenizer.pos += 2; name += '()'; }
if (tokenizer.charCodeAt(tokenizer.findWsEnd(tokenizer.pos)) === LEFTSQUAREBRACKET) { scanSpaces(tokenizer); opts = readTypeRange(tokenizer); }
tokenizer.eat(GREATERTHANSIGN);
return maybeMultiplied(tokenizer, { type: 'Type', name: name, opts: opts }); }
function readKeywordOrFunction(tokenizer) { var name;
name = scanWord(tokenizer);
if (tokenizer.charCode() === LEFTPARENTHESIS) { tokenizer.pos++;
return { type: 'Function', name: name }; }
return maybeMultiplied(tokenizer, { type: 'Keyword', name: name }); }
function regroupTerms(terms, combinators) { function createGroup(terms, combinator) { return { type: 'Group', terms: terms, combinator: combinator, disallowEmpty: false, 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') { tokenizer.pos = prevTokenPos; tokenizer.error('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') { tokenizer.pos -= prevTokenPos; tokenizer.error('Unexpected combinator'); }
return { type: 'Group', terms: terms, combinator: regroupTerms(terms, combinators) || ' ', disallowEmpty: false, explicit: false }; }
function readGroup(tokenizer) { var result;
tokenizer.eat(LEFTSQUAREBRACKET); result = readImplicitGroup(tokenizer); tokenizer.eat(RIGHTSQUAREBRACKET);
result.explicit = true;
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 RIGHTSQUAREBRACKET: // don't eat, stop scan a group
break;
case LEFTSQUAREBRACKET: return maybeMultiplied(tokenizer, readGroup(tokenizer));
case LESSTHANSIGN: return tokenizer.nextCharCode() === APOSTROPHE ? readProperty(tokenizer) : 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' };
case APOSTROPHE: return maybeMultiplied(tokenizer, { type: 'String', value: scanString(tokenizer) });
case SPACE: case TAB: case N: case R: case F: return { type: 'Spaces', value: scanSpaces(tokenizer) };
case COMMERCIALAT: code = tokenizer.nextCharCode();
if (code < 128 && NAME_CHAR[code] === 1) { tokenizer.pos++; return { type: 'AtKeyword', name: scanWord(tokenizer) }; }
return maybeToken(tokenizer);
case ASTERISK: case PLUSSIGN: case QUESTIONMARK: case NUMBERSIGN: case EXCLAMATIONMARK: // prohibited tokens (used as a multiplier start)
break;
case LEFTCURLYBRACKET: // LEFTCURLYBRACKET is allowed since mdn/data uses it w/o quoting
// check next char isn't a number, because it's likely a disjoined multiplier
code = tokenizer.nextCharCode();
if (code < 48 || code > 57) { return maybeToken(tokenizer); }
break;
default: return maybeToken(tokenizer); } }
function parse(source) { var tokenizer = new Tokenizer(source); var result = readImplicitGroup(tokenizer);
if (tokenizer.pos !== source.length) { tokenizer.error('Unexpected input'); }
// 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() f{2} /,(% g#{1,2} h{2,})]!');
module.exports = parse;
|