|
116 | 116 |
|
117 | 117 | var identStart = '[\$_a-zA-Z]';
|
118 | 118 | var identPart = '[\$_a-zA-Z0-9]';
|
119 |
| - var ident = identStart + '+' + identPart + '*'; |
120 |
| - var elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; |
121 |
| - var identOrElementIndex = '(?:' + ident + '|' + elementIndex + ')'; |
122 |
| - var path = '(?:' + identOrElementIndex + ')(?:\\s*\\.\\s*' + identOrElementIndex + ')*'; |
123 |
| - var pathRegExp = new RegExp('^' + path + '$'); |
124 |
| - |
125 |
| - function isPathValid(s) { |
126 |
| - if (typeof s != 'string') |
127 |
| - return false; |
128 |
| - s = s.trim(); |
| 119 | + var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$'); |
| 120 | + |
| 121 | + function getPathCharType(char) { |
| 122 | + if (char === undefined) |
| 123 | + return 'eof'; |
| 124 | + |
| 125 | + var code = char.charCodeAt(0); |
| 126 | + |
| 127 | + switch(code) { |
| 128 | + case 0x5B: // [ |
| 129 | + case 0x5D: // ] |
| 130 | + case 0x2E: // . |
| 131 | + case 0x22: // " |
| 132 | + case 0x27: // ' |
| 133 | + case 0x30: // 0 |
| 134 | + return char; |
| 135 | + |
| 136 | + case 0x5F: // _ |
| 137 | + case 0x24: // $ |
| 138 | + return 'ident'; |
| 139 | + |
| 140 | + case 0x20: // Space |
| 141 | + case 0x09: // Tab |
| 142 | + case 0x0A: // Newline |
| 143 | + case 0x0D: // Return |
| 144 | + case 0xA0: // No-break space |
| 145 | + case 0xFEFF: // Byte Order Mark |
| 146 | + case 0x2028: // Line Separator |
| 147 | + case 0x2029: // Paragraph Separator |
| 148 | + return 'ws'; |
| 149 | + } |
129 | 150 |
|
130 |
| - if (s == '') |
131 |
| - return true; |
| 151 | + // a-z, A-Z |
| 152 | + if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A)) |
| 153 | + return 'ident'; |
132 | 154 |
|
133 |
| - if (s[0] == '.') |
134 |
| - return false; |
| 155 | + // 1-9 |
| 156 | + if (0x31 <= code && code <= 0x39) |
| 157 | + return 'number'; |
135 | 158 |
|
136 |
| - return pathRegExp.test(s); |
| 159 | + return 'else'; |
137 | 160 | }
|
138 | 161 |
|
139 |
| - var constructorIsPrivate = {}; |
| 162 | + var pathStateMachine = { |
| 163 | + 'beforePath': { |
| 164 | + 'ws': ['beforePath'], |
| 165 | + 'ident': ['inIdent', 'append'], |
| 166 | + '[': ['beforeElement'], |
| 167 | + 'eof': ['afterPath'] |
| 168 | + }, |
140 | 169 |
|
141 |
| - function Path(s, privateToken) { |
142 |
| - if (privateToken !== constructorIsPrivate) |
143 |
| - throw Error('Use Path.get to retrieve path objects'); |
| 170 | + 'inPath': { |
| 171 | + 'ws': ['inPath'], |
| 172 | + '.': ['beforeIdent'], |
| 173 | + '[': ['beforeElement'], |
| 174 | + 'eof': ['afterPath'] |
| 175 | + }, |
144 | 176 |
|
145 |
| - if (s.trim() == '') |
146 |
| - return this; |
| 177 | + 'beforeIdent': { |
| 178 | + 'ws': ['beforeIdent'], |
| 179 | + 'ident': ['inIdent', 'append'] |
| 180 | + }, |
147 | 181 |
|
148 |
| - if (isIndex(s)) { |
149 |
| - this.push(s); |
150 |
| - return this; |
| 182 | + 'inIdent': { |
| 183 | + 'ident': ['inIdent', 'append'], |
| 184 | + '0': ['inIdent', 'append'], |
| 185 | + 'number': ['inIdent', 'append'], |
| 186 | + 'ws': ['inPath', 'push'], |
| 187 | + '.': ['beforeIdent', 'push'], |
| 188 | + '[': ['beforeElement', 'push'], |
| 189 | + 'eof': ['afterPath', 'push'] |
| 190 | + }, |
| 191 | + |
| 192 | + 'beforeElement': { |
| 193 | + 'ws': ['beforeElement'], |
| 194 | + '0': ['afterZero', 'append'], |
| 195 | + 'number': ['inIndex', 'append'], |
| 196 | + "'": ['inSingleQuote', 'append', ''], |
| 197 | + '"': ['inDoubleQuote', 'append', ''] |
| 198 | + }, |
| 199 | + |
| 200 | + 'afterZero': { |
| 201 | + 'ws': ['afterElement', 'push'], |
| 202 | + ']': ['inPath', 'push'] |
| 203 | + }, |
| 204 | + |
| 205 | + 'inIndex': { |
| 206 | + '0': ['inIndex', 'append'], |
| 207 | + 'number': ['inIndex', 'append'], |
| 208 | + 'ws': ['afterElement'], |
| 209 | + ']': ['inPath', 'push'] |
| 210 | + }, |
| 211 | + |
| 212 | + 'inSingleQuote': { |
| 213 | + "'": ['afterElement'], |
| 214 | + 'eof': ['error'], |
| 215 | + 'else': ['inSingleQuote', 'append'] |
| 216 | + }, |
| 217 | + |
| 218 | + 'inDoubleQuote': { |
| 219 | + '"': ['afterElement'], |
| 220 | + 'eof': ['error'], |
| 221 | + 'else': ['inDoubleQuote', 'append'] |
| 222 | + }, |
| 223 | + |
| 224 | + 'afterElement': { |
| 225 | + 'ws': ['afterElement'], |
| 226 | + ']': ['inPath', 'push'] |
151 | 227 | }
|
| 228 | + } |
| 229 | + |
| 230 | + function noop() {} |
| 231 | + |
| 232 | + function parsePath(path) { |
| 233 | + var keys = []; |
| 234 | + var index = -1; |
| 235 | + var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath'; |
| 236 | + |
| 237 | + var actions = { |
| 238 | + push: function() { |
| 239 | + if (key === undefined) |
| 240 | + return; |
| 241 | + |
| 242 | + keys.push(key); |
| 243 | + key = undefined; |
| 244 | + }, |
| 245 | + |
| 246 | + append: function() { |
| 247 | + if (key === undefined) |
| 248 | + key = newChar |
| 249 | + else |
| 250 | + key += newChar; |
| 251 | + } |
| 252 | + }; |
152 | 253 |
|
153 |
| - s.split(/\s*\.\s*/).filter(function(part) { |
154 |
| - return part; |
155 |
| - }).forEach(function(part) { |
156 |
| - this.push(part); |
157 |
| - }, this); |
| 254 | + function maybeUnescapeQuote() { |
| 255 | + if (index >= path.length) |
| 256 | + return; |
| 257 | + |
| 258 | + var nextChar = path[index + 1]; |
| 259 | + if ((mode == 'inSingleQuote' && nextChar == "'") || |
| 260 | + (mode == 'inDoubleQuote' && nextChar == '"')) { |
| 261 | + index++; |
| 262 | + newChar = nextChar; |
| 263 | + actions.append(); |
| 264 | + return true; |
| 265 | + } |
| 266 | + } |
| 267 | + |
| 268 | + while (mode) { |
| 269 | + index++; |
| 270 | + c = path[index]; |
| 271 | + |
| 272 | + if (c == '\\' && maybeUnescapeQuote(mode)) |
| 273 | + continue; |
| 274 | + |
| 275 | + type = getPathCharType(c); |
| 276 | + typeMap = pathStateMachine[mode]; |
| 277 | + transition = typeMap[type] || typeMap['else'] || 'error'; |
| 278 | + |
| 279 | + if (transition == 'error') |
| 280 | + return; // parse error; |
| 281 | + |
| 282 | + mode = transition[0]; |
| 283 | + action = actions[transition[1]] || noop; |
| 284 | + newChar = transition[2] === undefined ? c : transition[2]; |
| 285 | + action(); |
| 286 | + |
| 287 | + if (mode === 'afterPath') { |
| 288 | + return keys; |
| 289 | + } |
| 290 | + } |
| 291 | + |
| 292 | + return; // parse error |
| 293 | + } |
| 294 | + |
| 295 | + function isIdent(s) { |
| 296 | + return identRegExp.test(s); |
| 297 | + } |
| 298 | + |
| 299 | + var constructorIsPrivate = {}; |
| 300 | + |
| 301 | + function Path(parts, privateToken) { |
| 302 | + if (privateToken !== constructorIsPrivate) |
| 303 | + throw Error('Use Path.get to retrieve path objects'); |
| 304 | + |
| 305 | + if (parts.length) |
| 306 | + Array.prototype.push.apply(this, parts.slice()); |
158 | 307 |
|
159 | 308 | if (hasEval && this.length) {
|
160 | 309 | this.getValueFrom = this.compiledGetValueFromFn();
|
|
168 | 317 | if (pathString instanceof Path)
|
169 | 318 | return pathString;
|
170 | 319 |
|
171 |
| - if (pathString == null) |
| 320 | + if (pathString == null || pathString.length == 0) |
172 | 321 | pathString = '';
|
173 | 322 |
|
174 |
| - if (typeof pathString !== 'string') |
| 323 | + if (typeof pathString != 'string') { |
| 324 | + if (isIndex(pathString.length)) { |
| 325 | + // Constructed with array-like (pre-parsed) keys |
| 326 | + return new Path(pathString, constructorIsPrivate); |
| 327 | + } |
| 328 | + |
175 | 329 | pathString = String(pathString);
|
| 330 | + } |
176 | 331 |
|
177 | 332 | var path = pathCache[pathString];
|
178 | 333 | if (path)
|
179 | 334 | return path;
|
180 |
| - if (!isPathValid(pathString)) |
| 335 | + |
| 336 | + var parts = parsePath(pathString); |
| 337 | + if (!parts) |
181 | 338 | return invalidPath;
|
182 |
| - var path = new Path(pathString, constructorIsPrivate); |
| 339 | + |
| 340 | + var path = new Path(parts, constructorIsPrivate); |
183 | 341 | pathCache[pathString] = path;
|
184 | 342 | return path;
|
185 | 343 | }
|
186 | 344 |
|
187 | 345 | Path.get = getPath;
|
188 | 346 |
|
| 347 | + function formatAccessor(key) { |
| 348 | + if (isIndex(key)) { |
| 349 | + return '[' + key + ']'; |
| 350 | + } else { |
| 351 | + return '["' + key.replace(/"/g, '\\"') + '"]'; |
| 352 | + } |
| 353 | + } |
| 354 | + |
189 | 355 | Path.prototype = createObject({
|
190 | 356 | __proto__: [],
|
191 | 357 | valid: true,
|
192 | 358 |
|
193 | 359 | toString: function() {
|
194 |
| - return this.join('.'); |
| 360 | + var pathString = ''; |
| 361 | + for (var i = 0; i < this.length; i++) { |
| 362 | + var key = this[i]; |
| 363 | + if (isIdent(key)) { |
| 364 | + pathString += i ? '.' + key : key; |
| 365 | + } else { |
| 366 | + pathString += formatAccessor(key); |
| 367 | + } |
| 368 | + } |
| 369 | + |
| 370 | + return pathString; |
195 | 371 | },
|
196 | 372 |
|
197 | 373 | getValueFrom: function(obj, directObserver) {
|
|
214 | 390 | },
|
215 | 391 |
|
216 | 392 | compiledGetValueFromFn: function() {
|
217 |
| - var accessors = this.map(function(ident) { |
218 |
| - return isIndex(ident) ? '["' + ident + '"]' : '.' + ident; |
219 |
| - }); |
220 |
| - |
221 | 393 | var str = '';
|
222 | 394 | var pathString = 'obj';
|
223 | 395 | str += 'if (obj != null';
|
224 | 396 | var i = 0;
|
| 397 | + var key; |
225 | 398 | for (; i < (this.length - 1); i++) {
|
226 |
| - var ident = this[i]; |
227 |
| - pathString += accessors[i]; |
| 399 | + key = this[i]; |
| 400 | + pathString += isIdent(key) ? '.' + key : formatAccessor(key); |
228 | 401 | str += ' &&\n ' + pathString + ' != null';
|
229 | 402 | }
|
230 | 403 | str += ')\n';
|
231 | 404 |
|
232 |
| - pathString += accessors[i]; |
| 405 | + var key = this[i]; |
| 406 | + pathString += isIdent(key) ? '.' + key : formatAccessor(key); |
233 | 407 |
|
234 | 408 | str += ' return ' + pathString + ';\nelse\n return undefined;';
|
235 | 409 | return new Function('obj', str);
|
|
0 commit comments