1- var AND_REGEXP = / ^ \s + a n d \s + ( .* ) / i
2- var OR_REGEXP = / ^ (?: , \s * | \s + o r \s + ) ( .* ) / i
1+ var SPACE = / \s /
32
43function 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
2122function 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
67127module . 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