137 lines
3.8 KiB
JavaScript
137 lines
3.8 KiB
JavaScript
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
|
|
})
|
|
)
|
|
}
|