Visitar URL original
Rewrite structure parsing to make it always fast · browserslist/browserslist@d7e9e65 · GitHub
Skip to content

Commit d7e9e65

Browse files
committed
Rewrite structure parsing to make it always fast
1 parent ec4a55e commit d7e9e65

1 file changed

Lines changed: 94 additions & 36 deletions

File tree

‎parse.js‎

Lines changed: 94 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
var AND_REGEXP = /^\s+and\s+(.*)/i
2-
var OR_REGEXP = /^(?:,\s*|\s+or\s+)(.*)/i
1+
var SPACE = /\s/
32

43
function flatten(array) {
54
if (!Array.isArray(array)) return [array]
6-
return array.reduce(function (a, b) {
7-
return a.concat(flatten(b))
8-
}, [])
9-
}
10-
11-
function find(string, predicate) {
12-
for (var max = string.length, n = 1; n <= max; n++) {
13-
var parsed = string.substr(-n, n)
14-
if (predicate(parsed, n, max)) {
15-
return string.slice(0, -n)
5+
// Iterative flatten: `reduce`+`concat` copies the accumulator on every step,
6+
// which is O(n²) once a query resolves to many nodes.
7+
var result = []
8+
var stack = [array]
9+
while (stack.length) {
10+
var item = stack.pop()
11+
if (Array.isArray(item)) {
12+
for (var i = item.length - 1; i >= 0; i--) {
13+
stack.push(item[i])
14+
}
15+
} else {
16+
result.push(item)
1617
}
1718
}
18-
return ''
19+
return result
1920
}
2021

2122
function matchQuery(all, query) {
@@ -41,37 +42,94 @@ function matchQuery(all, query) {
4142
return node
4243
}
4344

44-
function matchBlock(all, string, qs) {
45-
var node
46-
return find(string, function (parsed, n, max) {
47-
if (AND_REGEXP.test(parsed)) {
48-
node = matchQuery(all, parsed.match(AND_REGEXP)[1])
49-
node.compose = 'and'
50-
qs.unshift(node)
51-
return true
52-
} else if (OR_REGEXP.test(parsed)) {
53-
node = matchQuery(all, parsed.match(OR_REGEXP)[1])
54-
node.compose = 'or'
55-
qs.unshift(node)
56-
return true
57-
} else if (n === max) {
58-
node = matchQuery(all, parsed.trim())
59-
node.compose = 'or'
60-
qs.unshift(node)
61-
return true
45+
function pushClause(all, qs, text, compose) {
46+
var node = matchQuery(all, text.trim())
47+
node.compose = compose
48+
qs.push(node)
49+
}
50+
51+
// Splits a query block into clauses on the `\s+and\s+`, `\s+or\s+` and `,\s*`
52+
// delimiters in a single left-to-right pass. The previous implementation grew
53+
// a suffix one character at a time and re-tested anchored `\s+…` regexps at
54+
// every length, which backtracks across whitespace runs and is O(n²) — a
55+
// small padded query could freeze the event loop for tens of seconds.
56+
function parseBlock(all, block, qs) {
57+
if (block.length === 0) return
58+
59+
var len = block.length
60+
var clauseStart = 0
61+
// Compose for the clause currently being read; the leftmost clause is `or`.
62+
var compose = 'or'
63+
var i = 0
64+
65+
while (i < len) {
66+
var ch = block[i]
67+
68+
if (ch === ',') {
69+
// `,\s*` delimiter. A delimiter at the very start (i === 0) has no left
70+
// clause — the original never emits one there.
71+
if (i !== 0) pushClause(all, qs, block.slice(clauseStart, i), compose)
72+
i++
73+
while (i < len && SPACE.test(block[i])) i++
74+
compose = 'or'
75+
clauseStart = i
76+
continue
6277
}
63-
return false
64-
})
78+
79+
if (SPACE.test(ch)) {
80+
// Possible `\s+and\s+` or `\s+or\s+`. Scan the whitespace run once
81+
// (linear, no backtracking), then check the following keyword.
82+
var q = i
83+
while (q < len && SPACE.test(block[q])) q++
84+
85+
if (
86+
q + 3 < len &&
87+
(block[q] === 'a' || block[q] === 'A') &&
88+
(block[q + 1] === 'n' || block[q + 1] === 'N') &&
89+
(block[q + 2] === 'd' || block[q + 2] === 'D') &&
90+
SPACE.test(block[q + 3])
91+
) {
92+
// The leading `\s+` of `\s+and\s+` absorbs whitespace at the block
93+
// start, so a delimiter at i === 0 has no left clause.
94+
if (i !== 0) pushClause(all, qs, block.slice(clauseStart, i), compose)
95+
var afterAnd = q + 3
96+
while (afterAnd < len && SPACE.test(block[afterAnd])) afterAnd++
97+
compose = 'and'
98+
i = afterAnd
99+
clauseStart = afterAnd
100+
continue
101+
} else if (
102+
q + 2 < len &&
103+
(block[q] === 'o' || block[q] === 'O') &&
104+
(block[q + 1] === 'r' || block[q + 1] === 'R') &&
105+
SPACE.test(block[q + 2])
106+
) {
107+
if (i !== 0) pushClause(all, qs, block.slice(clauseStart, i), compose)
108+
var afterOr = q + 2
109+
while (afterOr < len && SPACE.test(block[afterOr])) afterOr++
110+
compose = 'or'
111+
i = afterOr
112+
clauseStart = afterOr
113+
continue
114+
} else {
115+
// Whitespace inside a clause; skip the run and keep reading.
116+
i = q
117+
continue
118+
}
119+
}
120+
121+
i++
122+
}
123+
124+
pushClause(all, qs, block.slice(clauseStart), compose)
65125
}
66126

67127
module.exports = function parse(all, queries) {
68128
if (!Array.isArray(queries)) queries = [queries]
69129
return flatten(
70130
queries.map(function (block) {
71131
var qs = []
72-
do {
73-
block = matchBlock(all, block, qs)
74-
} while (block)
132+
parseBlock(all, block, qs)
75133
return qs
76134
})
77135
)

0 commit comments

Comments
 (0)