UNPKG

23.5 kBJavaScriptView Raw
1// json5.js
2// Modern JSON. See README.md for details.
3//
4// This file is based directly off of Douglas Crockford's json_parse.js:
5// https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js
6
7var JSON5 = (typeof exports === 'object' ? exports : {});
8
9JSON5.parse = (function () {
10 "use strict";
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 escaped newlines in strings w/ empty string
28 b: '\b',
29 f: '\f',
30 n: '\n',
31 r: '\r',
32 t: '\t'
33 },
34 ws = [
35 ' ',
36 '\t',
37 '\r',
38 '\n',
39 '\v',
40 '\f',
41 '\xA0',
42 '\uFEFF'
43 ],
44 text,
45
46 error = function (m) {
47
48// Call error when something is wrong.
49
50 var error = new SyntaxError();
51 error.message = m;
52 error.at = at;
53 error.text = text;
54 throw error;
55 },
56
57 next = function (c) {
58
59// If a c parameter is provided, verify that it matches the current character.
60
61 if (c && c !== ch) {
62 error("Expected '" + c + "' instead of '" + ch + "'");
63 }
64
65// Get the next character. When there are no more characters,
66// return the empty string.
67
68 ch = text.charAt(at);
69 at += 1;
70 return ch;
71 },
72
73 peek = function () {
74
75// Get the next character without consuming it or
76// assigning it to the ch varaible.
77
78 return text.charAt(at);
79 },
80
81 identifier = function () {
82
83// Parse an identifier. Normally, reserved words are disallowed here, but we
84// only use this for unquoted object keys, where reserved words are allowed,
85// so we don't check for those here. References:
86// - http://es5.github.com/#x7.6
87// - https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Variables
88// - http://docstore.mik.ua/orelly/webprog/jscript/ch02_07.htm
89
90 var key = ch;
91
92 // Identifiers must start with a letter, _ or $.
93 if ((ch !== '_' && ch !== '$') &&
94 (ch < 'a' || ch > 'z') &&
95 (ch < 'A' || ch > 'Z')) {
96 error("Bad identifier");
97 }
98
99 // Subsequent characters can contain digits.
100 while (next() && (
101 ch === '_' || ch === '$' ||
102 (ch >= 'a' && ch <= 'z') ||
103 (ch >= 'A' && ch <= 'Z') ||
104 (ch >= '0' && ch <= '9'))) {
105 key += ch;
106 }
107
108 return key;
109 },
110
111 number = function () {
112
113// Parse a number value.
114
115 var number,
116 sign = '',
117 string = '',
118 base = 10;
119
120 if (ch === '-' || ch === '+') {
121 sign = ch;
122 next(ch);
123 }
124
125 // support for Infinity (could tweak to allow other words):
126 if (ch === 'I') {
127 number = word();
128 if (typeof number !== 'number' || isNaN(number)) {
129 error('Unexpected word for number');
130 }
131 return (sign === '-') ? -number : number;
132 }
133
134 // support for NaN
135 if (ch === 'N' ) {
136 number = word();
137 if (!isNaN(number)) {
138 error('expected word to be NaN');
139 }
140 // ignore sign as -NaN also is NaN
141 return number;
142 }
143
144 if (ch === '0') {
145 string += ch;
146 next();
147 if (ch === 'x' || ch === 'X') {
148 string += ch;
149 next();
150 base = 16;
151 } else if (ch >= '0' && ch <= '9') {
152 error('Octal literal');
153 }
154 }
155
156 switch (base) {
157 case 10:
158 while (ch >= '0' && ch <= '9' ) {
159 string += ch;
160 next();
161 }
162 if (ch === '.') {
163 string += '.';
164 while (next() && ch >= '0' && ch <= '9') {
165 string += ch;
166 }
167 }
168 if (ch === 'e' || ch === 'E') {
169 string += ch;
170 next();
171 if (ch === '-' || ch === '+') {
172 string += ch;
173 next();
174 }
175 while (ch >= '0' && ch <= '9') {
176 string += ch;
177 next();
178 }
179 }
180 break;
181 case 16:
182 while (ch >= '0' && ch <= '9' || ch >= 'A' && ch <= 'F' || ch >= 'a' && ch <= 'f') {
183 string += ch;
184 next();
185 }
186 break;
187 }
188
189 if(sign === '-') {
190 number = -string;
191 } else {
192 number = +string;
193 }
194
195 if (!isFinite(number)) {
196 error("Bad number");
197 } else {
198 return number;
199 }
200 },
201
202 string = function () {
203
204// Parse a string value.
205
206 var hex,
207 i,
208 string = '',
209 delim, // double quote or single quote
210 uffff;
211
212// When parsing for string values, we must look for ' or " and \ characters.
213
214 if (ch === '"' || ch === "'") {
215 delim = ch;
216 while (next()) {
217 if (ch === delim) {
218 next();
219 return string;
220 } else if (ch === '\\') {
221 next();
222 if (ch === 'u') {
223 uffff = 0;
224 for (i = 0; i < 4; i += 1) {
225 hex = parseInt(next(), 16);
226 if (!isFinite(hex)) {
227 break;
228 }
229 uffff = uffff * 16 + hex;
230 }
231 string += String.fromCharCode(uffff);
232 } else if (ch === '\r') {
233 if (peek() === '\n') {
234 next();
235 }
236 } else if (typeof escapee[ch] === 'string') {
237 string += escapee[ch];
238 } else {
239 break;
240 }
241 } else if (ch === '\n') {
242 // unescaped newlines are invalid; see:
243 // https://github.com/aseemk/json5/issues/24
244 // invalid unescaped chars?
245 break;
246 } else {
247 string += ch;
248 }
249 }
250 }
251 error("Bad string");
252 },
253
254 inlineComment = function () {
255
256// Skip an inline comment, assuming this is one. The current character should
257// be the second / character in the // pair that begins this inline comment.
258// To finish the inline comment, we look for a newline or the end of the text.
259
260 if (ch !== '/') {
261 error("Not an inline comment");
262 }
263
264 do {
265 next();
266 if (ch === '\n' || ch === '\r') {
267 next();
268 return;
269 }
270 } while (ch);
271 },
272
273 blockComment = function () {
274
275// Skip a block comment, assuming this is one. The current character should be
276// the * character in the /* pair that begins this block comment.
277// To finish the block comment, we look for an ending */ pair of characters,
278// but we also watch for the end of text before the comment is terminated.
279
280 if (ch !== '*') {
281 error("Not a block comment");
282 }
283
284 do {
285 next();
286 while (ch === '*') {
287 next('*');
288 if (ch === '/') {
289 next('/');
290 return;
291 }
292 }
293 } while (ch);
294
295 error("Unterminated block comment");
296 },
297
298 comment = function () {
299
300// Skip a comment, whether inline or block-level, assuming this is one.
301// Comments always begin with a / character.
302
303 if (ch !== '/') {
304 error("Not a comment");
305 }
306
307 next('/');
308
309 if (ch === '/') {
310 inlineComment();
311 } else if (ch === '*') {
312 blockComment();
313 } else {
314 error("Unrecognized comment");
315 }
316 },
317
318 white = function () {
319
320// Skip whitespace and comments.
321// Note that we're detecting comments by only a single / character.
322// This works since regular expressions are not valid JSON(5), but this will
323// break if there are other valid values that begin with a / character!
324
325 while (ch) {
326 if (ch === '/') {
327 comment();
328 } else if (ws.indexOf(ch) >= 0) {
329 next();
330 } else {
331 return;
332 }
333 }
334 },
335
336 word = function () {
337
338// true, false, or null.
339
340 switch (ch) {
341 case 't':
342 next('t');
343 next('r');
344 next('u');
345 next('e');
346 return true;
347 case 'f':
348 next('f');
349 next('a');
350 next('l');
351 next('s');
352 next('e');
353 return false;
354 case 'n':
355 next('n');
356 next('u');
357 next('l');
358 next('l');
359 return null;
360 case 'I':
361 next('I');
362 next('n');
363 next('f');
364 next('i');
365 next('n');
366 next('i');
367 next('t');
368 next('y');
369 return Infinity;
370 case 'N':
371 next( 'N' );
372 next( 'a' );
373 next( 'N' );
374 return NaN;
375 }
376 error("Unexpected '" + ch + "'");
377 },
378
379 value, // Place holder for the value function.
380
381 array = function () {
382
383// Parse an array value.
384
385 var array = [];
386
387 if (ch === '[') {
388 next('[');
389 white();
390 while (ch) {
391 if (ch === ']') {
392 next(']');
393 return array; // Potentially empty array
394 }
395 // ES5 allows omitting elements in arrays, e.g. [,] and
396 // [,null]. We don't allow this in JSON5.
397 if (ch === ',') {
398 error("Missing array element");
399 } else {
400 array.push(value());
401 }
402 white();
403 // If there's no comma after this value, this needs to
404 // be the end of the array.
405 if (ch !== ',') {
406 next(']');
407 return array;
408 }
409 next(',');
410 white();
411 }
412 }
413 error("Bad array");
414 },
415
416 object = function () {
417
418// Parse an object value.
419
420 var key,
421 object = {};
422
423 if (ch === '{') {
424 next('{');
425 white();
426 while (ch) {
427 if (ch === '}') {
428 next('}');
429 return object; // Potentially empty object
430 }
431
432 // Keys can be unquoted. If they are, they need to be
433 // valid JS identifiers.
434 if (ch === '"' || ch === "'") {
435 key = string();
436 } else {
437 key = identifier();
438 }
439
440 white();
441 next(':');
442 object[key] = value();
443 white();
444 // If there's no comma after this pair, this needs to be
445 // the end of the object.
446 if (ch !== ',') {
447 next('}');
448 return object;
449 }
450 next(',');
451 white();
452 }
453 }
454 error("Bad object");
455 };
456
457 value = function () {
458
459// Parse a JSON value. It could be an object, an array, a string, a number,
460// or a word.
461
462 white();
463 switch (ch) {
464 case '{':
465 return object();
466 case '[':
467 return array();
468 case '"':
469 case "'":
470 return string();
471 case '-':
472 case '+':
473 case '.':
474 return number();
475 default:
476 return ch >= '0' && ch <= '9' ? number() : word();
477 }
478 };
479
480// Return the json_parse function. It will have access to all of the above
481// functions and variables.
482
483 return function (source, reviver) {
484 var result;
485
486 text = String(source);
487 at = 0;
488 ch = ' ';
489 result = value();
490 white();
491 if (ch) {
492 error("Syntax error");
493 }
494
495// If there is a reviver function, we recursively walk the new structure,
496// passing each name/value pair to the reviver function for possible
497// transformation, starting with a temporary root object that holds the result
498// in an empty key. If there is not a reviver function, we simply return the
499// result.
500
501 return typeof reviver === 'function' ? (function walk(holder, key) {
502 var k, v, value = holder[key];
503 if (value && typeof value === 'object') {
504 for (k in value) {
505 if (Object.prototype.hasOwnProperty.call(value, k)) {
506 v = walk(value, k);
507 if (v !== undefined) {
508 value[k] = v;
509 } else {
510 delete value[k];
511 }
512 }
513 }
514 }
515 return reviver.call(holder, key, value);
516 }({'': result}, '')) : result;
517 };
518}());
519
520// JSON5 stringify will not quote keys where appropriate
521JSON5.stringify = function (obj, replacer, space) {
522 if (replacer && (typeof(replacer) !== "function" && !isArray(replacer))) {
523 throw new Error('Replacer must be a function or an array');
524 }
525 var getReplacedValueOrUndefined = function(holder, key, isTopLevel) {
526 var value = holder[key];
527
528 // Replace the value with its toJSON value first, if possible
529 if (value && value.toJSON && typeof value.toJSON === "function") {
530 value = value.toJSON();
531 }
532
533 // If the user-supplied replacer if a function, call it. If it's an array, check objects' string keys for
534 // presence in the array (removing the key/value pair from the resulting JSON if the key is missing).
535 if (typeof(replacer) === "function") {
536 return replacer.call(holder, key, value);
537 } else if(replacer) {
538 if (isTopLevel || isArray(holder) || replacer.indexOf(key) >= 0) {
539 return value;
540 } else {
541 return undefined;
542 }
543 } else {
544 return value;
545 }
546 };
547
548 function isWordChar(char) {
549 return (char >= 'a' && char <= 'z') ||
550 (char >= 'A' && char <= 'Z') ||
551 (char >= '0' && char <= '9') ||
552 char === '_' || char === '$';
553 }
554
555 function isWordStart(char) {
556 return (char >= 'a' && char <= 'z') ||
557 (char >= 'A' && char <= 'Z') ||
558 char === '_' || char === '$';
559 }
560
561 function isWord(key) {
562 if (typeof key !== 'string') {
563 return false;
564 }
565 if (!isWordStart(key[0])) {
566 return false;
567 }
568 var i = 1, length = key.length;
569 while (i < length) {
570 if (!isWordChar(key[i])) {
571 return false;
572 }
573 i++;
574 }
575 return true;
576 }
577
578 // export for use in tests
579 JSON5.isWord = isWord;
580
581 // polyfills
582 function isArray(obj) {
583 if (Array.isArray) {
584 return Array.isArray(obj);
585 } else {
586 return Object.prototype.toString.call(obj) === '[object Array]';
587 }
588 }
589
590 function isDate(obj) {
591 return Object.prototype.toString.call(obj) === '[object Date]';
592 }
593
594 isNaN = isNaN || function(val) {
595 return typeof val === 'number' && val !== val;
596 };
597
598 var objStack = [];
599 function checkForCircular(obj) {
600 for (var i = 0; i < objStack.length; i++) {
601 if (objStack[i] === obj) {
602 throw new TypeError("Converting circular structure to JSON");
603 }
604 }
605 }
606
607 function makeIndent(str, num, noNewLine) {
608 if (!str) {
609 return "";
610 }
611 // indentation no more than 10 chars
612 if (str.length > 10) {
613 str = str.substring(0, 10);
614 }
615
616 var indent = noNewLine ? "" : "\n";
617 for (var i = 0; i < num; i++) {
618 indent += str;
619 }
620
621 return indent;
622 }
623
624 var indentStr;
625 if (space) {
626 if (typeof space === "string") {
627 indentStr = space;
628 } else if (typeof space === "number" && space >= 0) {
629 indentStr = makeIndent(" ", space, true);
630 } else {
631 // ignore space parameter
632 }
633 }
634
635 // Copied from Crokford's implementation of JSON
636 // See https://github.com/douglascrockford/JSON-js/blob/e39db4b7e6249f04a195e7dd0840e610cc9e941e/json2.js#L195
637 // Begin
638 var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
639 escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
640 meta = { // table of character substitutions
641 '\b': '\\b',
642 '\t': '\\t',
643 '\n': '\\n',
644 '\f': '\\f',
645 '\r': '\\r',
646 '"' : '\\"',
647 '\\': '\\\\'
648 };
649 function escapeString(string) {
650
651// If the string contains no control characters, no quote characters, and no
652// backslash characters, then we can safely slap some quotes around it.
653// Otherwise we must also replace the offending characters with safe escape
654// sequences.
655 escapable.lastIndex = 0;
656 return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
657 var c = meta[a];
658 return typeof c === 'string' ?
659 c :
660 '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
661 }) + '"' : '"' + string + '"';
662 }
663 // End
664
665 function internalStringify(holder, key, isTopLevel) {
666 var buffer, res;
667
668 // Replace the value, if necessary
669 var obj_part = getReplacedValueOrUndefined(holder, key, isTopLevel);
670
671 if (obj_part && !isDate(obj_part)) {
672 // unbox objects
673 // don't unbox dates, since will turn it into number
674 obj_part = obj_part.valueOf();
675 }
676 switch(typeof obj_part) {
677 case "boolean":
678 return obj_part.toString();
679
680 case "number":
681 if (isNaN(obj_part) || !isFinite(obj_part)) {
682 return "null";
683 }
684 return obj_part.toString();
685
686 case "string":
687 return escapeString(obj_part.toString());
688
689 case "object":
690 if (obj_part === null) {
691 return "null";
692 } else if (isArray(obj_part)) {
693 checkForCircular(obj_part);
694 buffer = "[";
695 objStack.push(obj_part);
696
697 for (var i = 0; i < obj_part.length; i++) {
698 res = internalStringify(obj_part, i, false);
699 buffer += makeIndent(indentStr, objStack.length);
700 if (res === null || typeof res === "undefined") {
701 buffer += "null";
702 } else {
703 buffer += res;
704 }
705 if (i < obj_part.length-1) {
706 buffer += ",";
707 } else if (indentStr) {
708 buffer += "\n";
709 }
710 }
711 objStack.pop();
712 buffer += makeIndent(indentStr, objStack.length, true) + "]";
713 } else {
714 checkForCircular(obj_part);
715 buffer = "{";
716 var nonEmpty = false;
717 objStack.push(obj_part);
718 for (var prop in obj_part) {
719 if (obj_part.hasOwnProperty(prop)) {
720 var value = internalStringify(obj_part, prop, false);
721 isTopLevel = false;
722 if (typeof value !== "undefined" && value !== null) {
723 buffer += makeIndent(indentStr, objStack.length);
724 nonEmpty = true;
725 var key = isWord(prop) ? prop : escapeString(prop);
726 buffer += key + ":" + (indentStr ? ' ' : '') + value + ",";
727 }
728 }
729 }
730 objStack.pop();
731 if (nonEmpty) {
732 buffer = buffer.substring(0, buffer.length-1) + makeIndent(indentStr, objStack.length) + "}";
733 } else {
734 buffer = '{}';
735 }
736 }
737 return buffer;
738 default:
739 // functions and undefined should be ignored
740 return undefined;
741 }
742 }
743
744 // special case...when undefined is used inside of
745 // a compound object/array, return null.
746 // but when top-level, return undefined
747 var topLevelHolder = {"":obj};
748 if (obj === undefined) {
749 return getReplacedValueOrUndefined(topLevelHolder, '', true);
750 }
751 return internalStringify(topLevelHolder, '', true);
752};