1 | // json5.js
|
2 | // Modern JSON. See README.md for details.
|
3 | //
|
4 | // This file is modeled directly off of Douglas Crockford's json_parse.js:
|
5 | // https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js
|
6 |
|
7 | var JSON5 = (typeof exports === 'object' ? exports : {});
|
8 |
|
9 | JSON5.parse = (function () {
|
10 | ;
|
11 |
|
12 | // This is a function that can parse a JSON5 text, producing a JavaScript
|
13 | // data structure. It is a simple, recursive descent parser. It does not use
|
14 | // eval or regular expressions, so it can be used as a model for implementing
|
15 | // a JSON5 parser in other languages.
|
16 |
|
17 | // We are defining the function inside of another function to avoid creating
|
18 | // global variables.
|
19 |
|
20 | var at, // The index of the current character
|
21 | ch, // The current character
|
22 | escapee = {
|
23 | "'": "'",
|
24 | '"': '"',
|
25 | '\\': '\\',
|
26 | '/': '/',
|
27 | '\n': '', // Replace newlines in strings w/ empty string
|
28 | b: '\b',
|
29 | f: '\f',
|
30 | n: '\n',
|
31 | r: '\r',
|
32 | t: '\t'
|
33 | },
|
34 | text,
|
35 |
|
36 | error = function (m) {
|
37 |
|
38 | // Call error when something is wrong.
|
39 |
|
40 | throw {
|
41 | name: 'SyntaxError',
|
42 | message: m,
|
43 | at: at,
|
44 | text: text
|
45 | };
|
46 | },
|
47 |
|
48 | next = function (c) {
|
49 |
|
50 | // If a c parameter is provided, verify that it matches the current character.
|
51 |
|
52 | if (c && c !== ch) {
|
53 | error("Expected '" + c + "' instead of '" + ch + "'");
|
54 | }
|
55 |
|
56 | // Get the next character. When there are no more characters,
|
57 | // return the empty string.
|
58 |
|
59 | ch = text.charAt(at);
|
60 | at += 1;
|
61 | return ch;
|
62 | },
|
63 |
|
64 | identifier = function () {
|
65 |
|
66 | // Parse an identifier. Normally, reserved words are disallowed here, but we
|
67 | // only use this for unquoted object keys, where reserved words are allowed,
|
68 | // so we don't check for those here. References:
|
69 | // - http://es5.github.com/#x7.6
|
70 | // - https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Variables
|
71 | // - http://docstore.mik.ua/orelly/webprog/jscript/ch02_07.htm
|
72 | // TODO Identifiers can have Unicode "letters" in them; add support for those.
|
73 |
|
74 | var key = ch;
|
75 |
|
76 | // Identifiers must start with a letter, _ or $.
|
77 | if (!ch.match(/^[a-zA-Z_$]$/)) {
|
78 | error("Bad identifier");
|
79 | }
|
80 |
|
81 | // Subsequent characters can contain digits.
|
82 | while (next() && ch.match(/^[\w$]$/)) {
|
83 | key += ch;
|
84 | }
|
85 |
|
86 | return key;
|
87 | },
|
88 |
|
89 | number = function () {
|
90 |
|
91 | // Parse a number value.
|
92 |
|
93 | var number,
|
94 | string = '';
|
95 |
|
96 | if (ch === '-') {
|
97 | string = '-';
|
98 | next('-');
|
99 | }
|
100 | while (ch >= '0' && ch <= '9') {
|
101 | string += ch;
|
102 | next();
|
103 | }
|
104 | if (ch === '.') {
|
105 | string += '.';
|
106 | while (next() && ch >= '0' && ch <= '9') {
|
107 | string += ch;
|
108 | }
|
109 | }
|
110 | if (ch === 'e' || ch === 'E') {
|
111 | string += ch;
|
112 | next();
|
113 | if (ch === '-' || ch === '+') {
|
114 | string += ch;
|
115 | next();
|
116 | }
|
117 | while (ch >= '0' && ch <= '9') {
|
118 | string += ch;
|
119 | next();
|
120 | }
|
121 | }
|
122 | number = +string;
|
123 | if (!isFinite(number)) {
|
124 | error("Bad number");
|
125 | } else {
|
126 | return number;
|
127 | }
|
128 | },
|
129 |
|
130 | string = function () {
|
131 |
|
132 | // Parse a string value.
|
133 |
|
134 | var hex,
|
135 | i,
|
136 | string = '',
|
137 | delim, // double quote or single quote
|
138 | uffff;
|
139 |
|
140 | // When parsing for string values, we must look for " and \ characters.
|
141 |
|
142 | if (ch === '"' || ch === "'") {
|
143 | delim = ch;
|
144 | while (next()) {
|
145 | if (ch === delim) {
|
146 | next();
|
147 | return string;
|
148 | } else if (ch === '\\') {
|
149 | next();
|
150 | if (ch === 'u') {
|
151 | uffff = 0;
|
152 | for (i = 0; i < 4; i += 1) {
|
153 | hex = parseInt(next(), 16);
|
154 | if (!isFinite(hex)) {
|
155 | break;
|
156 | }
|
157 | uffff = uffff * 16 + hex;
|
158 | }
|
159 | string += String.fromCharCode(uffff);
|
160 | } else if (typeof escapee[ch] === 'string') {
|
161 | string += escapee[ch];
|
162 | } else {
|
163 | break;
|
164 | }
|
165 | } else {
|
166 | string += ch;
|
167 | }
|
168 | }
|
169 | }
|
170 | error("Bad string");
|
171 | },
|
172 |
|
173 | inlineComment = function () {
|
174 |
|
175 | // Skip inline comments, assuming this is one. The current character should be
|
176 | // the second / character in the // pair that begins this inline comment.
|
177 | // When parsing inline comments, we look for a newline or the end of the text.
|
178 |
|
179 | if (ch !== '/') {
|
180 | error("Not an inline comment");
|
181 | }
|
182 |
|
183 | do {
|
184 | next();
|
185 | if (ch === '\n') {
|
186 | next('\n');
|
187 | return;
|
188 | }
|
189 | } while (ch);
|
190 | },
|
191 |
|
192 | blockComment = function () {
|
193 |
|
194 | // Skip block comments, assuming this is one. The current character should be
|
195 | // the * character in the /* pair that begins this block comment.
|
196 | // When parsing block comments, we look for an ending */ pair of characters,
|
197 | // but we also watch for the end of text before the comment is terminated.
|
198 |
|
199 | if (ch !== '*') {
|
200 | error("Not a block comment");
|
201 | }
|
202 |
|
203 | do {
|
204 | next();
|
205 | while (ch === '*') {
|
206 | next('*');
|
207 | if (ch === '/') {
|
208 | next('/');
|
209 | return;
|
210 | }
|
211 | }
|
212 | } while (ch);
|
213 |
|
214 | error("Unterminated block comment");
|
215 | },
|
216 |
|
217 | comment = function () {
|
218 |
|
219 | // Skip comments, both inline and block-level, assuming this is one. Comments
|
220 | // always begin with a / character.
|
221 |
|
222 | if (ch !== '/') {
|
223 | error("Not a comment");
|
224 | }
|
225 |
|
226 | next('/');
|
227 |
|
228 | if (ch === '/') {
|
229 | inlineComment();
|
230 | } else if (ch === '*') {
|
231 | blockComment();
|
232 | } else {
|
233 | error("Unrecognized comment");
|
234 | }
|
235 | },
|
236 |
|
237 | white = function () {
|
238 |
|
239 | // Skip whitespace and comments.
|
240 | // Note that we're detecting comments by only a single / character.
|
241 | // This works since regular expressions are not valid JSON(5), but this will
|
242 | // break if there are other valid values that begin with a / character!
|
243 |
|
244 | while (ch) {
|
245 | if (ch === '/') {
|
246 | comment();
|
247 | } else if (ch <= ' ') {
|
248 | next();
|
249 | } else {
|
250 | return;
|
251 | }
|
252 | }
|
253 | },
|
254 |
|
255 | word = function () {
|
256 |
|
257 | // true, false, or null.
|
258 |
|
259 | switch (ch) {
|
260 | case 't':
|
261 | next('t');
|
262 | next('r');
|
263 | next('u');
|
264 | next('e');
|
265 | return true;
|
266 | case 'f':
|
267 | next('f');
|
268 | next('a');
|
269 | next('l');
|
270 | next('s');
|
271 | next('e');
|
272 | return false;
|
273 | case 'n':
|
274 | next('n');
|
275 | next('u');
|
276 | next('l');
|
277 | next('l');
|
278 | return null;
|
279 | }
|
280 | error("Unexpected '" + ch + "'");
|
281 | },
|
282 |
|
283 | value, // Place holder for the value function.
|
284 |
|
285 | array = function () {
|
286 |
|
287 | // Parse an array value.
|
288 |
|
289 | var array = [];
|
290 |
|
291 | if (ch === '[') {
|
292 | next('[');
|
293 | white();
|
294 | while (ch) {
|
295 | if (ch === ']') {
|
296 | next(']');
|
297 | return array; // Potentially empty array
|
298 | }
|
299 | // Omitted values are allowed and detected by the presence
|
300 | // of a trailing comma. Whether the value was omitted or
|
301 | // not, the current character after this block should be
|
302 | // the character after the value.
|
303 | if (ch === ',') {
|
304 | // Pushing an undefined value isn't quite equivalent
|
305 | // to what ES5 does in practice, but we can emulate it
|
306 | // by incrementing the array's length.
|
307 | array.length += 1;
|
308 | // Don't go next; the comma is the character after the
|
309 | // omitted (undefined) value.
|
310 | } else {
|
311 | array.push(value());
|
312 | // The various value methods call next(); the current
|
313 | // character here is now the one after the value.
|
314 | }
|
315 | white();
|
316 | // If there's no comma after this value, this needs to
|
317 | // be the end of the array.
|
318 | if (ch !== ',') {
|
319 | next(']');
|
320 | return array;
|
321 | }
|
322 | next(',');
|
323 | white();
|
324 | }
|
325 | }
|
326 | error("Bad array");
|
327 | },
|
328 |
|
329 | object = function () {
|
330 |
|
331 | // Parse an object value.
|
332 | // TODO Update to support unquoted keys.
|
333 |
|
334 | var key,
|
335 | object = {};
|
336 |
|
337 | if (ch === '{') {
|
338 | next('{');
|
339 | white();
|
340 | while (ch) {
|
341 | if (ch === '}') {
|
342 | next('}');
|
343 | return object; // Potentially empty object
|
344 | }
|
345 |
|
346 | // Keys can be unquoted. If they are, they need to be
|
347 | // valid JS identifiers.
|
348 | if (ch === '"' || ch === "'") {
|
349 | key = string();
|
350 | } else {
|
351 | key = identifier();
|
352 | }
|
353 |
|
354 | white();
|
355 | next(':');
|
356 | if (Object.hasOwnProperty.call(object, key)) {
|
357 | error('Duplicate key "' + key + '"');
|
358 | }
|
359 | object[key] = value();
|
360 | white();
|
361 | // If there's no comma after this pair, this needs to be
|
362 | // the end of the object.
|
363 | if (ch !== ',') {
|
364 | next('}');
|
365 | return object;
|
366 | }
|
367 | next(',');
|
368 | white();
|
369 | }
|
370 | }
|
371 | error("Bad object");
|
372 | };
|
373 |
|
374 | value = function () {
|
375 |
|
376 | // Parse a JSON value. It could be an object, an array, a string, a number,
|
377 | // or a word.
|
378 |
|
379 | white();
|
380 | switch (ch) {
|
381 | case '{':
|
382 | return object();
|
383 | case '[':
|
384 | return array();
|
385 | case '"':
|
386 | case "'":
|
387 | return string();
|
388 | case '-':
|
389 | return number();
|
390 | default:
|
391 | return ch >= '0' && ch <= '9' ? number() : word();
|
392 | }
|
393 | };
|
394 |
|
395 | // Return the json_parse function. It will have access to all of the above
|
396 | // functions and variables.
|
397 |
|
398 | return function (source, reviver) {
|
399 | var result;
|
400 |
|
401 | text = source;
|
402 | at = 0;
|
403 | ch = ' ';
|
404 | result = value();
|
405 | white();
|
406 | if (ch) {
|
407 | error("Syntax error");
|
408 | }
|
409 |
|
410 | // If there is a reviver function, we recursively walk the new structure,
|
411 | // passing each name/value pair to the reviver function for possible
|
412 | // transformation, starting with a temporary root object that holds the result
|
413 | // in an empty key. If there is not a reviver function, we simply return the
|
414 | // result.
|
415 |
|
416 | return typeof reviver === 'function' ? (function walk(holder, key) {
|
417 | var k, v, value = holder[key];
|
418 | if (value && typeof value === 'object') {
|
419 | for (k in value) {
|
420 | if (Object.prototype.hasOwnProperty.call(value, k)) {
|
421 | v = walk(value, k);
|
422 | if (v !== undefined) {
|
423 | value[k] = v;
|
424 | } else {
|
425 | delete value[k];
|
426 | }
|
427 | }
|
428 | }
|
429 | }
|
430 | return reviver.call(holder, key, value);
|
431 | }({'': result}, '')) : result;
|
432 | };
|
433 | }());
|
434 |
|
435 | JSON5.stringify = function (obj, replacer, space) {
|
436 | // Since regular JSON is a strict subset of JSON5, we'll always output as
|
437 | // regular JSON to foster better interoperability. TODO Should we not?
|
438 | return JSON.stringify.apply(JSON, arguments);
|
439 | };
|