1
+ import {
2
+ filter ,
3
+ map ,
4
+ race ,
5
+ regexp ,
6
+ } from './parser.utils'
7
+
1
8
import {
2
9
isSymbol ,
3
10
toString ,
@@ -24,44 +31,18 @@ const toKey = arg => {
24
31
return toString ( arg )
25
32
}
26
33
27
- const quotes = [ '"' , '\'' ]
28
-
29
34
/**
30
- * Tests whether <code>index </code>th char of <code>str</code> is a quote. <br />
31
- * Quotes are <code>"</code> and <code>'</code>.
35
+ * Strip slashes preceding occurences of <code>quote </code> from <code>str</code><br />
36
+ * Possible quotes are <code>"</code> and <code>'</code>.
32
37
* @function
33
38
* @param {string } str The string
34
- * @param {number } index Index of the char to test
35
- * @return {{ quoted: boolean, quote: string } } A boolean <code>quoted</code>, true if <code>str.charAt(index)</code> is a quote and the <code>quote</code>.
39
+ * @param {string } quote The quote to unescape
40
+ * @return {string } The unescaped string
36
41
* @memberof core
37
42
* @private
38
43
* @since 1.0.0
39
44
*/
40
- const isQuoteChar = ( str , index ) => {
41
- const char = str . charAt ( index )
42
- const quote = quotes . find ( c => c === char )
43
- return {
44
- quoted : Boolean ( quote ) ,
45
- quote,
46
- }
47
- }
48
-
49
- const escapedQuotesRegexps = { }
50
- for ( const quote of quotes )
51
- escapedQuotesRegexps [ quote ] = new RegExp ( `\\\\${ quote } ` , 'g' )
52
-
53
- /**
54
- * Strip slashes preceding occurences of <code>quote</code> from <code>str</code><br />
55
- * Possible quotes are <code>"</code> and <code>'</code>.
56
- * @function
57
- * @param {string } str The string
58
- * @param {string } quote The quote to unescape
59
- * @return {string } The unescaped string
60
- * @memberof core
61
- * @private
62
- * @since 1.0.0
63
- */
64
- const unescapeQuotes = ( str , quote ) => str . replace ( escapedQuotesRegexps [ quote ] , quote )
45
+ const unescapeQuotes = ( str , quote ) => str . replace ( new RegExp ( `\\\\${ quote } ` , 'g' ) , quote )
65
46
66
47
/**
67
48
* Converts <code>str</code> to a slice index.
@@ -77,13 +58,25 @@ const toSliceIndex = str => str === '' ? undefined : Number(str)
77
58
/**
78
59
* Tests whether <code>arg</code> is a valid slice index, that is <code>undefined</code> or a valid int.
79
60
* @function
61
+ * @memberof core
80
62
* @param {* } arg The value to test
81
63
* @return {boolean } True if <code>arg</code> is a valid slice index, false otherwise.
82
64
* @private
83
65
* @since 1.0.0
84
66
*/
85
67
const isSliceIndex = arg => arg === undefined || Number . isSafeInteger ( arg )
86
68
69
+ /**
70
+ * Tests whether <code>arg</code> is a valid slice index once converted to a number.
71
+ * @function
72
+ * @memberof core
73
+ * @param {* } arg The value to test
74
+ * @return {boolean } True if <code>arg</code> is a valid slice index once converted to a number, false otherwise.
75
+ * @private
76
+ * @since 1.0.0
77
+ */
78
+ const isSliceIndexString = arg => isSliceIndex ( arg ? Number ( arg ) : undefined )
79
+
87
80
/**
88
81
* Wraps <code>fn</code> allowing to call it with an array instead of a string.<br />
89
82
* The returned function behaviour is :<br />
@@ -102,6 +95,50 @@ const allowingArrays = fn => arg => {
102
95
return fn ( toString ( arg ) )
103
96
}
104
97
98
+ const emptyStringParser = str => str . length === 0 ? [ ] : null
99
+
100
+ const quotedBracketNotationParser = map (
101
+ regexp ( / ^ \[ ( [ ' " ] ) ( .* ?[ ^ \\ ] ) \1\] ? \. ? ( .* ) $ / ) ,
102
+ ( [ quote , property , rest ] ) => [ unescapeQuotes ( property , quote ) , ...stringToPath ( rest ) ] ,
103
+ )
104
+
105
+ const incompleteQuotedBracketNotationParser = map (
106
+ regexp ( / ^ \[ [ " ' ] ( .* ) $ / ) ,
107
+ ( [ rest ] ) => rest ? [ rest ] : [ ] ,
108
+ )
109
+
110
+ const bareBracketNotationParser = map (
111
+ regexp ( / ^ \[ ( [ ^ \] ] * ) \] \. ? ( .* ) $ / ) ,
112
+ ( [ property , rest ] ) => {
113
+ return isIndex ( Number ( property ) )
114
+ ? [ Number ( property ) , ...stringToPath ( rest ) ]
115
+ : [ property , ...stringToPath ( rest ) ]
116
+ } ,
117
+ )
118
+
119
+ const incompleteBareBracketNotationParser = map (
120
+ regexp ( / ^ \[ ( .* ) $ / ) ,
121
+ ( [ rest ] ) => rest ? [ rest ] : [ ] ,
122
+ )
123
+
124
+ const sliceNotationParser = map (
125
+ filter (
126
+ regexp ( / ^ \[ ( [ ^ : \] ] * ) : ( [ ^ : \] ] * ) \] \. ? ( .* ) $ / ) ,
127
+ ( [ sliceStart , sliceEnd ] ) => isSliceIndexString ( sliceStart ) && isSliceIndexString ( sliceEnd ) ,
128
+ ) ,
129
+ ( [ sliceStart , sliceEnd , rest ] ) => [ [ toSliceIndex ( sliceStart ) , toSliceIndex ( sliceEnd ) ] , ...stringToPath ( rest ) ] ,
130
+ )
131
+
132
+ const pathSegmentEndedByDotParser = map (
133
+ regexp ( / ^ ( [ ^ . [ ] * ?) \. ( .* ) $ / ) ,
134
+ ( [ beforeDot , afterDot ] ) => [ beforeDot , ...stringToPath ( afterDot ) ] ,
135
+ )
136
+
137
+ const pathSegmentEndedByBracketParser = map (
138
+ regexp ( / ^ ( [ ^ . [ ] * ?) ( \[ .* ) $ / ) ,
139
+ ( [ beforeBracket , atBracket ] ) => [ beforeBracket , ...stringToPath ( atBracket ) ] ,
140
+ )
141
+
105
142
/**
106
143
* Converts <code>str</code> to a path represented as an array of keys.
107
144
* @function
@@ -111,141 +148,17 @@ const allowingArrays = fn => arg => {
111
148
* @private
112
149
* @since 1.0.0
113
150
*/
114
- const stringToPath = str => {
115
- const path = [ ]
116
- let index = 0
117
-
118
- while ( true ) { // eslint-disable-line no-constant-condition
119
- // Look for new dot or opening square bracket
120
- const nextPointIndex = str . indexOf ( '.' , index )
121
- const nextBracketIndex = str . indexOf ( '[' , index )
122
-
123
- // If neither one is found add the end of str to the path and stop
124
- if ( nextPointIndex === - 1 && nextBracketIndex === - 1 ) {
125
- path . push ( str . substring ( index ) )
126
- break
127
- }
128
-
129
- let isArrayNotation = false
130
-
131
- // If a dot is found before an opening square bracket
132
- if ( nextPointIndex !== - 1 && ( nextBracketIndex === - 1 || nextPointIndex < nextBracketIndex ) ) {
133
- // Add the text preceding the dot to the path and move index after the dot
134
- path . push ( str . substring ( index , nextPointIndex ) )
135
- index = nextPointIndex + 1
136
-
137
- // If an opening square bracket follows the dot,
138
- // enable array notation and move index after the bracket
139
- if ( nextBracketIndex === nextPointIndex + 1 ) {
140
- isArrayNotation = true
141
- index = nextBracketIndex + 1
142
- }
143
-
144
- // If an opening square bracket is found before a dot
145
- } else if ( nextBracketIndex !== - 1 ) {
146
- // Enable array notation
147
- isArrayNotation = true
148
-
149
- // If any text precedes the bracket, add it to the path
150
- if ( nextBracketIndex !== index )
151
- path . push ( str . substring ( index , nextBracketIndex ) )
152
-
153
- // Move index after the bracket
154
- index = nextBracketIndex + 1
155
- }
156
-
157
- // If array notation is enabled
158
- if ( isArrayNotation ) {
159
- // Check if next character is a string quote
160
- const { quoted, quote } = isQuoteChar ( str , index )
161
-
162
- // If array index is a quoted string
163
- if ( quoted ) {
164
- // Move index after the string quote
165
- index ++
166
-
167
- // Look for the next unescaped matching string quote
168
- let endQuoteIndex , quotedIndex = index
169
- do {
170
- endQuoteIndex = str . indexOf ( quote , quotedIndex )
171
- quotedIndex = endQuoteIndex + 1
172
- } while ( endQuoteIndex !== - 1 && str . charAt ( endQuoteIndex - 1 ) === '\\' )
173
-
174
- // If no end quote found, stop if end of str is reached, or continue to next iteration
175
- if ( endQuoteIndex === - 1 ) {
176
- if ( index !== str . length ) path . push ( str . substring ( index ) )
177
- break
178
- }
179
-
180
- // Add the content of quotes to the path, unescaping escaped quotes
181
- path . push ( unescapeQuotes ( str . substring ( index , endQuoteIndex ) , quote ) )
182
-
183
- // Move index after end quote
184
- index = endQuoteIndex + 1
185
-
186
- // If next character is a closing square bracket, move index after it
187
- if ( str . charAt ( index ) === ']' ) index ++
188
-
189
- // Stop if end of str has been reached
190
- if ( index === str . length ) break
191
-
192
- // If next character is a dot, move index after it (skip it)
193
- if ( str . charAt ( index ) === '.' ) index ++
194
-
195
- } else { // If array index is not a quoted string
196
-
197
- // Look for the closing square bracket
198
- const closingBracketIndex = str . indexOf ( ']' , index )
199
-
200
- // If no closing bracket found, stop if end of str is reached, or continue to next iteration
201
- if ( closingBracketIndex === - 1 ) {
202
- if ( index !== str . length ) path . push ( str . substring ( index ) )
203
- break
204
- }
205
-
206
- // Fetch the content of brackets and move index after closing bracket
207
- const arrayIndexValue = str . substring ( index , closingBracketIndex )
208
- index = closingBracketIndex + 1
209
-
210
- // If next character is a dot, move index after it (skip it)
211
- if ( str . charAt ( index ) === '.' ) index ++
212
-
213
- // Shorthand: if array index is the whole slice add it to path
214
- if ( arrayIndexValue === ':' ) {
215
- path . push ( [ undefined , undefined ] )
216
- } else {
217
-
218
- // Look for a slice quote
219
- const sliceDelimIndex = arrayIndexValue . indexOf ( ':' )
220
-
221
- // If no slice quote found
222
- if ( sliceDelimIndex === - 1 ) {
223
- // Parse array index as a number
224
- const nArrayIndexValue = Number ( arrayIndexValue )
225
-
226
- // Add array index to path, either as a valid index (positive int), or as a string
227
- path . push ( isIndex ( nArrayIndexValue ) ? nArrayIndexValue : arrayIndexValue )
228
-
229
- } else { // If a slice quote is found
230
-
231
- // Fetch slice start and end, and parse them as slice indexes (empty or valid int)
232
- const sliceStart = arrayIndexValue . substring ( 0 , sliceDelimIndex ) , sliceEnd = arrayIndexValue . substring ( sliceDelimIndex + 1 )
233
- const nSliceStart = toSliceIndex ( sliceStart ) , nSliceEnd = toSliceIndex ( sliceEnd )
234
-
235
- // Add array index to path, as a slice if both slice indexes are valid (undefined or int), or as a string
236
- path . push ( isSliceIndex ( nSliceStart ) && isSliceIndex ( nSliceEnd ) ? [ nSliceStart , nSliceEnd ] : arrayIndexValue )
237
- }
238
- }
239
-
240
- // Stop if end of string has been reached
241
- if ( index === str . length ) break
242
- }
243
- }
244
-
245
- }
246
-
247
- return path
248
- }
151
+ const stringToPath = race ( [
152
+ emptyStringParser ,
153
+ quotedBracketNotationParser ,
154
+ incompleteQuotedBracketNotationParser ,
155
+ sliceNotationParser ,
156
+ bareBracketNotationParser ,
157
+ incompleteBareBracketNotationParser ,
158
+ pathSegmentEndedByDotParser ,
159
+ pathSegmentEndedByBracketParser ,
160
+ str => [ str ] ,
161
+ ] )
249
162
250
163
const MAX_CACHE_SIZE = 1000
251
164
const cache = new Map ( )
0 commit comments