Skip to content
This repository was archived by the owner on Mar 13, 2018. It is now read-only.

Commit d99eff1

Browse files
committed
implement full path parsing, including element accessors & literals
R=arv BUG= Review URL: https://codereview.appspot.com/98450048
1 parent 9de5e77 commit d99eff1

File tree

2 files changed

+274
-62
lines changed

2 files changed

+274
-62
lines changed

src/observe.js

+215-41
Original file line numberDiff line numberDiff line change
@@ -116,45 +116,194 @@
116116

117117
var identStart = '[\$_a-zA-Z]';
118118
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+
}
129150

130-
if (s == '')
131-
return true;
151+
// a-z, A-Z
152+
if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A))
153+
return 'ident';
132154

133-
if (s[0] == '.')
134-
return false;
155+
// 1-9
156+
if (0x31 <= code && code <= 0x39)
157+
return 'number';
135158

136-
return pathRegExp.test(s);
159+
return 'else';
137160
}
138161

139-
var constructorIsPrivate = {};
162+
var pathStateMachine = {
163+
'beforePath': {
164+
'ws': ['beforePath'],
165+
'ident': ['inIdent', 'append'],
166+
'[': ['beforeElement'],
167+
'eof': ['afterPath']
168+
},
140169

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+
},
144176

145-
if (s.trim() == '')
146-
return this;
177+
'beforeIdent': {
178+
'ws': ['beforeIdent'],
179+
'ident': ['inIdent', 'append']
180+
},
147181

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']
151227
}
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+
};
152253

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());
158307

159308
if (hasEval && this.length) {
160309
this.getValueFrom = this.compiledGetValueFromFn();
@@ -168,30 +317,57 @@
168317
if (pathString instanceof Path)
169318
return pathString;
170319

171-
if (pathString == null)
320+
if (pathString == null || pathString.length == 0)
172321
pathString = '';
173322

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+
175329
pathString = String(pathString);
330+
}
176331

177332
var path = pathCache[pathString];
178333
if (path)
179334
return path;
180-
if (!isPathValid(pathString))
335+
336+
var parts = parsePath(pathString);
337+
if (!parts)
181338
return invalidPath;
182-
var path = new Path(pathString, constructorIsPrivate);
339+
340+
var path = new Path(parts, constructorIsPrivate);
183341
pathCache[pathString] = path;
184342
return path;
185343
}
186344

187345
Path.get = getPath;
188346

347+
function formatAccessor(key) {
348+
if (isIndex(key)) {
349+
return '[' + key + ']';
350+
} else {
351+
return '["' + key.replace(/"/g, '\\"') + '"]';
352+
}
353+
}
354+
189355
Path.prototype = createObject({
190356
__proto__: [],
191357
valid: true,
192358

193359
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;
195371
},
196372

197373
getValueFrom: function(obj, directObserver) {
@@ -214,22 +390,20 @@
214390
},
215391

216392
compiledGetValueFromFn: function() {
217-
var accessors = this.map(function(ident) {
218-
return isIndex(ident) ? '["' + ident + '"]' : '.' + ident;
219-
});
220-
221393
var str = '';
222394
var pathString = 'obj';
223395
str += 'if (obj != null';
224396
var i = 0;
397+
var key;
225398
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);
228401
str += ' &&\n ' + pathString + ' != null';
229402
}
230403
str += ')\n';
231404

232-
pathString += accessors[i];
405+
var key = this[i];
406+
pathString += isIdent(key) ? '.' + key : formatAccessor(key);
233407

234408
str += ' return ' + pathString + ';\nelse\n return undefined;';
235409
return new Function('obj', str);

0 commit comments

Comments
 (0)