| 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 |
| }) |
| ) |
| } |