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