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 |
|
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 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
|
521 | JSON5.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 | };
|