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