|
|
- 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;
|