OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 goog.provide('cvox.ChromeVoxJSON'); | 5 goog.provide('cvox.ChromeVoxJSON'); |
6 | 6 |
7 | 7 |
8 /** | 8 /** |
9 * @fileoverview A simple wrapper around the JSON APIs. | 9 * @fileoverview A simple wrapper around the JSON APIs. |
10 * If it is possible to use the browser's built in native JSON, then | 10 * If it is possible to use the browser's built in native JSON, then |
(...skipping 25 matching lines...) Expand all Loading... |
36 * | 36 * |
37 * See http://www.JSON.org/js.html | 37 * See http://www.JSON.org/js.html |
38 */ | 38 */ |
39 (function() { | 39 (function() { |
40 function f(n) { | 40 function f(n) { |
41 // Format integers to have at least two digits. | 41 // Format integers to have at least two digits. |
42 return n < 10 ? '0' + n : n; | 42 return n < 10 ? '0' + n : n; |
43 } | 43 } |
44 | 44 |
45 if (typeof Date.prototype.toJSON !== 'function') { | 45 if (typeof Date.prototype.toJSON !== 'function') { |
46 | |
47 Date.prototype.toJSON = function(key) { | 46 Date.prototype.toJSON = function(key) { |
48 | 47 |
49 return isFinite(this.valueOf()) ? | 48 return isFinite(this.valueOf()) ? |
50 this.getUTCFullYear() + '-' + | 49 this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + |
51 f(this.getUTCMonth() + 1) + '-' + | 50 f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + |
52 f(this.getUTCDate()) + 'T' + | 51 f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : |
53 f(this.getUTCHours()) + ':' + | 52 'null'; |
54 f(this.getUTCMinutes()) + ':' + | |
55 f(this.getUTCSeconds()) + 'Z' : 'null'; | |
56 }; | 53 }; |
57 | 54 |
58 Boolean.prototype.toJSON = | 55 Boolean.prototype.toJSON = Number.prototype.toJSON = |
59 Number.prototype.toJSON = | 56 String.prototype.toJSON = function(key) { |
60 String.prototype.toJSON = function(key) { | 57 return /** @type {string} */ (this.valueOf()); |
61 return /** @type {string} */ (this.valueOf()); | 58 }; |
62 }; | |
63 } | 59 } |
64 | 60 |
65 var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u
202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, | 61 var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u
202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, |
66 escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b
5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, | 62 escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b
5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, |
67 gap, | 63 gap, |
68 indent, | 64 indent, |
69 meta = { // table of character substitutions | 65 meta = { // table of character substitutions |
70 '\b': '\\b', | 66 '\b': '\\b', |
71 '\t': '\\t', | 67 '\t': '\\t', |
72 '\n': '\\n', | 68 '\n': '\\n', |
73 '\f': '\\f', | 69 '\f': '\\f', |
74 '\r': '\\r', | 70 '\r': '\\r', |
75 '"' : '\\"', | 71 '"' : '\\"', |
76 '\\': '\\\\' | 72 '\\': '\\\\' |
77 }, | 73 }, |
78 rep; | 74 rep; |
79 | 75 |
80 | 76 |
81 function quote(string) { | 77 function quote(string) { |
82 | |
83 // If the string contains no control characters, no quote characters, and | 78 // If the string contains no control characters, no quote characters, and |
84 // no backslash characters, then we can safely slap some quotes around it. | 79 // no backslash characters, then we can safely slap some quotes around it. |
85 // Otherwise we must also replace the offending characters with safe | 80 // Otherwise we must also replace the offending characters with safe |
86 // escape sequences. | 81 // escape sequences. |
87 | 82 |
88 escapable.lastIndex = 0; | 83 escapable.lastIndex = 0; |
89 return escapable.test(string) ? | 84 return escapable.test(string) ? '"' + |
90 '"' + string.replace(escapable, function(a) { | 85 string.replace( |
91 var c = meta[a]; | 86 escapable, |
92 return typeof c === 'string' ? c : | 87 function(a) { |
93 '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); | 88 var c = meta[a]; |
94 }) + '"' : | 89 return typeof c === 'string' ? c : |
95 '"' + string + '"'; | 90 '\\u' + |
| 91 ('0000' + a.charCodeAt(0).toString(16)).slice(-4); |
| 92 }) + |
| 93 '"' : |
| 94 '"' + string + '"'; |
96 } | 95 } |
97 | 96 |
98 | 97 |
99 function str(key, holder) { | 98 function str(key, holder) { |
100 | |
101 // Produce a string from holder[key]. | 99 // Produce a string from holder[key]. |
102 | 100 |
103 var i, // The loop counter. | 101 var i, // The loop counter. |
104 k, // The member key. | 102 k, // The member key. |
105 v, // The member value. | 103 v, // The member value. |
106 length, | 104 length, mind = gap, partial, value = holder[key]; |
107 mind = gap, | |
108 partial, | |
109 value = holder[key]; | |
110 | 105 |
111 // If the value has a toJSON method, call it to obtain a replacement | 106 // If the value has a toJSON method, call it to obtain a replacement |
112 // value. | 107 // value. |
113 | 108 |
114 if (value && typeof value === 'object' && | 109 if (value && typeof value === 'object' && |
115 typeof value.toJSON === 'function') { | 110 typeof value.toJSON === 'function') { |
116 value = value.toJSON(key); | 111 value = value.toJSON(key); |
117 } | 112 } |
118 | 113 |
119 // If we were called with a replacer function, then call the replacer to | 114 // If we were called with a replacer function, then call the replacer to |
120 // obtain a replacement value. | 115 // obtain a replacement value. |
121 | 116 |
122 if (typeof rep === 'function') { | 117 if (typeof rep === 'function') { |
123 value = rep.call(holder, key, value); | 118 value = rep.call(holder, key, value); |
124 } | 119 } |
125 | 120 |
126 // What happens next depends on the value's type. | 121 // What happens next depends on the value's type. |
127 | 122 |
128 switch (typeof value) { | 123 switch (typeof value) { |
129 case 'string': | 124 case 'string': |
130 return quote(value); | 125 return quote(value); |
131 | 126 |
132 case 'number': | 127 case 'number': |
133 // JSON numbers must be finite. Encode non-finite numbers as null. | 128 // JSON numbers must be finite. Encode non-finite numbers as null. |
134 return isFinite(value) ? String(value) : 'null'; | 129 return isFinite(value) ? String(value) : 'null'; |
135 | 130 |
136 case 'boolean': | 131 case 'boolean': |
137 case 'null': | 132 case 'null': |
138 // If the value is a boolean or null, convert it to a string. Note: | 133 // If the value is a boolean or null, convert it to a string. Note: |
139 // typeof null does not produce 'null'. The case is included here in | 134 // typeof null does not produce 'null'. The case is included here in |
140 // the remote chance that this gets fixed someday. | 135 // the remote chance that this gets fixed someday. |
141 return String(value); | 136 return String(value); |
142 | 137 |
143 // If the type is 'object', we might be dealing with an object or an | 138 // If the type is 'object', we might be dealing with an object or an |
144 // array or null. | 139 // array or null. |
145 | 140 |
146 case 'object': | 141 case 'object': |
147 | 142 |
148 // Due to a specification blunder in ECMAScript, typeof null is | 143 // Due to a specification blunder in ECMAScript, typeof null is |
149 // 'object', so watch out for that case. | 144 // 'object', so watch out for that case. |
150 | 145 |
151 if (!value) { | 146 if (!value) { |
152 return 'null'; | 147 return 'null'; |
153 } | 148 } |
154 | 149 |
155 // Make an array to hold the partial results of stringifying this | 150 // Make an array to hold the partial results of stringifying this |
156 // object value. | 151 // object value. |
157 | 152 |
158 gap += indent; | 153 gap += indent; |
159 partial = []; | 154 partial = []; |
160 | 155 |
161 // Is the value an array? | 156 // Is the value an array? |
162 | 157 |
163 if (Object.prototype.toString.apply(value) === '[object Array]') { | 158 if (Object.prototype.toString.apply(value) === '[object Array]') { |
164 | |
165 // The value is an array. Stringify every element. Use null as a | 159 // The value is an array. Stringify every element. Use null as a |
166 // placeholder for non-JSON values. | 160 // placeholder for non-JSON values. |
167 | 161 |
168 length = value.length; | 162 length = value.length; |
169 for (i = 0; i < length; i += 1) { | 163 for (i = 0; i < length; i += 1) { |
170 partial[i] = str(i, value) || 'null'; | 164 partial[i] = str(i, value) || 'null'; |
171 } | 165 } |
172 | 166 |
173 // Join all of the elements together, separated with commas, and | 167 // Join all of the elements together, separated with commas, and |
174 // wrap them in brackets. | 168 // wrap them in brackets. |
175 | 169 |
176 v = partial.length === 0 ? '[]' : | 170 v = partial.length === 0 ? |
177 gap ? '[\n' + gap + | 171 '[]' : |
178 partial.join(',\n' + gap) + '\n' + | 172 gap ? |
179 mind + ']' : | 173 '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : |
180 '[' + partial.join(',') + ']'; | 174 '[' + partial.join(',') + ']'; |
181 gap = mind; | 175 gap = mind; |
182 return v; | 176 return v; |
183 } | 177 } |
184 | 178 |
185 // If the replacer is an array, use it to select the members to be | 179 // If the replacer is an array, use it to select the members to be |
186 // stringified. | 180 // stringified. |
187 | 181 |
188 if (rep && typeof rep === 'object') { | 182 if (rep && typeof rep === 'object') { |
189 length = rep.length; | 183 length = rep.length; |
190 for (i = 0; i < length; i += 1) { | 184 for (i = 0; i < length; i += 1) { |
191 k = rep[i]; | 185 k = rep[i]; |
192 if (typeof k === 'string') { | 186 if (typeof k === 'string') { |
193 v = str(k, value); | 187 v = str(k, value); |
194 if (v) { | 188 if (v) { |
195 partial.push(quote(k) + (gap ? ': ' : ':') + v); | 189 partial.push(quote(k) + (gap ? ': ' : ':') + v); |
196 } | 190 } |
197 } | 191 } |
198 } | 192 } |
199 } else { | 193 } else { |
200 | |
201 // Otherwise, iterate through all of the keys in the object. | 194 // Otherwise, iterate through all of the keys in the object. |
202 for (k in value) { | 195 for (k in value) { |
203 if (Object.hasOwnProperty.call(value, k)) { | 196 if (Object.hasOwnProperty.call(value, k)) { |
204 v = str(k, value); | 197 v = str(k, value); |
205 if (v) { | 198 if (v) { |
206 partial.push(quote(k) + (gap ? ': ' : ':') + v); | 199 partial.push(quote(k) + (gap ? ': ' : ':') + v); |
207 } | 200 } |
208 } | 201 } |
209 } | 202 } |
210 } | 203 } |
211 | 204 |
212 // Join all of the member texts together, separated with commas, | 205 // Join all of the member texts together, separated with commas, |
213 // and wrap them in braces. | 206 // and wrap them in braces. |
214 | 207 |
215 v = partial.length === 0 ? '{}' : | 208 v = partial.length === 0 ? |
216 gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + | 209 '{}' : |
217 mind + '}' : '{' + partial.join(',') + '}'; | 210 gap ? |
| 211 '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : |
| 212 '{' + partial.join(',') + '}'; |
218 gap = mind; | 213 gap = mind; |
219 return v; | 214 return v; |
220 } | 215 } |
221 } | 216 } |
222 | 217 |
223 // If the JSON object does not yet have a stringify method, give it one. | 218 // If the JSON object does not yet have a stringify method, give it one. |
224 | 219 |
225 if (typeof cvox.ChromeVoxJSON.stringify !== 'function') { | 220 if (typeof cvox.ChromeVoxJSON.stringify !== 'function') { |
226 /** | 221 /** |
227 * @param {*} value Input object. | 222 * @param {*} value Input object. |
(...skipping 28 matching lines...) Expand all Loading... |
256 } else if (typeof space === 'string') { | 251 } else if (typeof space === 'string') { |
257 indent = space; | 252 indent = space; |
258 } | 253 } |
259 | 254 |
260 // If there is a replacer, it must be a function or an array. | 255 // If there is a replacer, it must be a function or an array. |
261 // Otherwise, throw an error. | 256 // Otherwise, throw an error. |
262 | 257 |
263 rep = replacer; | 258 rep = replacer; |
264 if (replacer && typeof replacer !== 'function' && | 259 if (replacer && typeof replacer !== 'function' && |
265 (typeof replacer !== 'object' || | 260 (typeof replacer !== 'object' || |
266 typeof replacer.length !== 'number')) { | 261 typeof replacer.length !== 'number')) { |
267 throw new Error('JSON.stringify'); | 262 throw new Error('JSON.stringify'); |
268 } | 263 } |
269 | 264 |
270 // Make a fake root object containing our value under the key of ''. | 265 // Make a fake root object containing our value under the key of ''. |
271 // Return the result of stringifying the value. | 266 // Return the result of stringifying the value. |
272 | 267 |
273 return str('', {'': value}); | 268 return str('', {'': value}); |
274 }; | 269 }; |
275 } | 270 } |
276 | 271 |
277 | 272 |
278 // If the JSON object does not yet have a parse method, give it one. | 273 // If the JSON object does not yet have a parse method, give it one. |
279 | 274 |
280 if (typeof cvox.ChromeVoxJSON.parse !== 'function') { | 275 if (typeof cvox.ChromeVoxJSON.parse !== 'function') { |
281 /** | 276 /** |
282 * @param {string} text The string to parse. | 277 * @param {string} text The string to parse. |
283 * @param {(function(string, *) : *|null)=} reviver Reviver function. | 278 * @param {(function(string, *) : *|null)=} reviver Reviver function. |
284 * @return {*} The JSON object. | 279 * @return {*} The JSON object. |
285 */ | 280 */ |
286 cvox.ChromeVoxJSON.parse = function(text, reviver) { | 281 cvox.ChromeVoxJSON.parse = function(text, reviver) { |
287 | 282 |
288 // The parse method takes a text and an optional reviver function, and | 283 // The parse method takes a text and an optional reviver function, and |
289 // returns a JavaScript value if the text is a valid JSON text. | 284 // returns a JavaScript value if the text is a valid JSON text. |
290 | 285 |
291 var j; | 286 var j; |
292 | 287 |
293 function walk(holder, key) { | 288 function walk(holder, key) { |
294 | |
295 // The walk method is used to recursively walk the resulting structure | 289 // The walk method is used to recursively walk the resulting structure |
296 // so that modifications can be made. | 290 // so that modifications can be made. |
297 | 291 |
298 var k, v, value = holder[key]; | 292 var k, v, value = holder[key]; |
299 if (value && typeof value === 'object') { | 293 if (value && typeof value === 'object') { |
300 for (k in value) { | 294 for (k in value) { |
301 if (Object.hasOwnProperty.call(value, k)) { | 295 if (Object.hasOwnProperty.call(value, k)) { |
302 v = walk(value, k); | 296 v = walk(value, k); |
303 if (v !== undefined) { | 297 if (v !== undefined) { |
304 value[k] = v; | 298 value[k] = v; |
305 } else { | 299 } else { |
306 delete value[k]; | 300 delete value[k]; |
307 } | 301 } |
308 } | 302 } |
309 } | 303 } |
310 } | 304 } |
311 return reviver.call(holder, key, value); | 305 return reviver.call(holder, key, value); |
312 } | 306 } |
313 | 307 |
314 | 308 |
315 // Parsing happens in four stages. In the first stage, we replace | 309 // Parsing happens in four stages. In the first stage, we replace |
316 // certain Unicode characters with escape sequences. JavaScript handles | 310 // certain Unicode characters with escape sequences. JavaScript handles |
317 // many characters incorrectly, either silently deleting them, or | 311 // many characters incorrectly, either silently deleting them, or |
318 // treating them as line endings. | 312 // treating them as line endings. |
319 | 313 |
320 text = String(text); | 314 text = String(text); |
321 cx.lastIndex = 0; | 315 cx.lastIndex = 0; |
322 if (cx.test(text)) { | 316 if (cx.test(text)) { |
323 text = text.replace(cx, function(a) { | 317 text = text.replace(cx, function(a) { |
324 return '\\u' + | 318 return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); |
325 ('0000' + a.charCodeAt(0).toString(16)).slice(-4); | |
326 }); | 319 }); |
327 } | 320 } |
328 | 321 |
329 // In the second stage, we run the text against regular expressions that | 322 // In the second stage, we run the text against regular expressions that |
330 // look for non-JSON patterns. We are especially concerned with '()' and | 323 // look for non-JSON patterns. We are especially concerned with '()' and |
331 // 'new' because they can cause invocation, and '=' because it can cause | 324 // 'new' because they can cause invocation, and '=' because it can cause |
332 // mutation. But just to be safe, we want to reject all unexpected | 325 // mutation. But just to be safe, we want to reject all unexpected |
333 // forms. | 326 // forms. |
334 // We split the second stage into 4 regexp operations in order to work | 327 // We split the second stage into 4 regexp operations in order to work |
335 // around crippling inefficiencies in IE's and Safari's regexp engines. | 328 // around crippling inefficiencies in IE's and Safari's regexp engines. |
336 // First we replace the JSON backslash pairs with '@' (a non-JSON | 329 // First we replace the JSON backslash pairs with '@' (a non-JSON |
337 // character). Second, we replace all simple value tokens with ']' | 330 // character). Second, we replace all simple value tokens with ']' |
338 // characters. Third, we delete all open brackets that follow a colon or | 331 // characters. Third, we delete all open brackets that follow a colon or |
339 // comma or that begin the text. Finally, we look to see that the | 332 // comma or that begin the text. Finally, we look to see that the |
340 // remaining characters are only whitespace or ']' or ',' or ':' or '{' | 333 // remaining characters are only whitespace or ']' or ',' or ':' or '{' |
341 // or '}'. If that is so, then the text is safe for eval. | 334 // or '}'. If that is so, then the text is safe for eval. |
342 | 335 |
343 if (/^[\],:{}\s]*$/. | 336 if (/^[\],:{}\s]*$/.test( |
344 test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). | 337 text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') |
345 replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?
/g, ']'). | 338 .replace( |
346 replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { | 339 /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\
-]?\d+)?/g, |
347 | 340 ']') |
| 341 .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { |
348 // In the third stage we use the eval function to compile the text | 342 // In the third stage we use the eval function to compile the text |
349 // into a JavaScript structure. The '{' operator is subject to a | 343 // into a JavaScript structure. The '{' operator is subject to a |
350 // syntactic ambiguity in JavaScript: it can begin a block or an | 344 // syntactic ambiguity in JavaScript: it can begin a block or an |
351 // object literal. We wrap the text in parens to eliminate the | 345 // object literal. We wrap the text in parens to eliminate the |
352 // ambiguity. | 346 // ambiguity. |
353 | 347 |
354 j = eval('(' + text + ')'); | 348 j = eval('(' + text + ')'); |
355 | 349 |
356 // In the optional fourth stage, we recursively walk the new | 350 // In the optional fourth stage, we recursively walk the new |
357 // structure, passing each name/value pair to a reviver function for | 351 // structure, passing each name/value pair to a reviver function for |
358 // possible transformation. | 352 // possible transformation. |
359 return typeof reviver === 'function' ? walk({'': j}, '') : j; | 353 return typeof reviver === 'function' ? walk({'': j}, '') : j; |
360 } | 354 } |
361 | 355 |
362 // If the text is not JSON parseable, then a SyntaxError is thrown. | 356 // If the text is not JSON parseable, then a SyntaxError is thrown. |
363 | 357 |
364 throw new SyntaxError('JSON.parse'); | 358 throw new SyntaxError('JSON.parse'); |
365 }; | 359 }; |
366 } | 360 } |
367 }()); | 361 }()); |
368 } | 362 } |
OLD | NEW |