UNPKG

browserslist

Version:

Share target browsers between different front-end tools, like Autoprefixer, Stylelint and babel-env-preset

137 lines (122 loc) 3.9 kB
var SPACE = /\s/ function flatten(array) { if (!Array.isArray(array)) return [array] // Iterative flatten: `reduce`+`concat` copies the accumulator on every step, // which is O(n²) once a query resolves to many nodes. var result = [] var stack = [array] while (stack.length) { var item = stack.pop() if (Array.isArray(item)) { for (var i = item.length - 1; i >= 0; i--) { stack.push(item[i]) } } else { result.push(item) } } return result } function matchQuery(all, query) { var node = { query: query } if (query.indexOf('not ') === 0) { node.not = true query = query.slice(4) } for (var name in all) { var type = all[name] var match = query.match(type.regexp) if (match) { node.type = name for (var i = 0; i < type.matches.length; i++) { node[type.matches[i]] = match[i + 1] } return node } } node.type = 'unknown' return node } function pushClause(all, qs, text, compose) { var node = matchQuery(all, text.trim()) node.compose = compose qs.push(node) } // Splits a query block into clauses on the `\s+and\s+`, `\s+or\s+` and `,\s*` // delimiters in a single left-to-right pass. The previous implementation grew // a suffix one character at a time and re-tested anchored `\s+…` regexps at // every length, which backtracks across whitespace runs and is O(n²) — a // small padded query could freeze the event loop for tens of seconds. function parseBlock(all, block, qs) { if (block.length === 0) return var len = block.length var clauseStart = 0 // Compose for the clause currently being read; the leftmost clause is `or`. var compose = 'or' var i = 0 while (i < len) { var ch = block[i] if (ch === ',') { // `,\s*` delimiter. A delimiter at the very start (i === 0) has no left // clause — the original never emits one there. if (i !== 0) pushClause(all, qs, block.slice(clauseStart, i), compose) i++ while (i < len && SPACE.test(block[i])) i++ compose = 'or' clauseStart = i continue } if (SPACE.test(ch)) { // Possible `\s+and\s+` or `\s+or\s+`. Scan the whitespace run once // (linear, no backtracking), then check the following keyword. var q = i while (q < len && SPACE.test(block[q])) q++ if ( q + 3 < len && (block[q] === 'a' || block[q] === 'A') && (block[q + 1] === 'n' || block[q + 1] === 'N') && (block[q + 2] === 'd' || block[q + 2] === 'D') && SPACE.test(block[q + 3]) ) { // The leading `\s+` of `\s+and\s+` absorbs whitespace at the block // start, so a delimiter at i === 0 has no left clause. if (i !== 0) pushClause(all, qs, block.slice(clauseStart, i), compose) var afterAnd = q + 3 while (afterAnd < len && SPACE.test(block[afterAnd])) afterAnd++ compose = 'and' i = afterAnd clauseStart = afterAnd continue } else if ( q + 2 < len && (block[q] === 'o' || block[q] === 'O') && (block[q + 1] === 'r' || block[q + 1] === 'R') && SPACE.test(block[q + 2]) ) { if (i !== 0) pushClause(all, qs, block.slice(clauseStart, i), compose) var afterOr = q + 2 while (afterOr < len && SPACE.test(block[afterOr])) afterOr++ compose = 'or' i = afterOr clauseStart = afterOr continue } else { // Whitespace inside a clause; skip the run and keep reading. i = q continue } } i++ } pushClause(all, qs, block.slice(clauseStart), compose) } module.exports = function parse(all, queries) { if (!Array.isArray(queries)) queries = [queries] return flatten( queries.map(function (block) { var qs = [] parseBlock(all, block, qs) return qs }) ) }