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