Projekt

Obecné

Profil

Stáhnout (24 KB) Statistiky
| Větev: | Revize:
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
    "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
        lineNumber,   // The current line number
22
        columnNumber, // The current column number
23
        ch,           // The current character
24
        escapee = {
25
            "'":  "'",
26
            '"':  '"',
27
            '\\': '\\',
28
            '/':  '/',
29
            '\n': '',       // Replace escaped newlines in strings w/ empty string
30
            b:    '\b',
31
            f:    '\f',
32
            n:    '\n',
33
            r:    '\r',
34
            t:    '\t'
35
        },
36
        ws = [
37
            ' ',
38
            '\t',
39
            '\r',
40
            '\n',
41
            '\v',
42
            '\f',
43
            '\xA0',
44
            '\uFEFF'
45
        ],
46
        text,
47

    
48
        renderChar = function (chr) {
49
            return chr === '' ? 'EOF' : "'" + chr + "'";
50
        },
51

    
52
        error = function (m) {
53

    
54
// Call error when something is wrong.
55

    
56
            var error = new SyntaxError();
57
            // beginning of message suffix to agree with that provided by Gecko - see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
58
            error.message = m + " at line " + lineNumber + " column " + columnNumber + " of the JSON5 data. Still to read: " + JSON.stringify(text.substring(at - 1, at + 19));
59
            error.at = at;
60
            // These two property names have been chosen to agree with the ones in Gecko, the only popular
61
            // environment which seems to supply this info on JSON.parse
62
            error.lineNumber = lineNumber;
63
            error.columnNumber = columnNumber;
64
            throw error;
65
        },
66

    
67
        next = function (c) {
68

    
69
// If a c parameter is provided, verify that it matches the current character.
70

    
71
            if (c && c !== ch) {
72
                error("Expected " + renderChar(c) + " instead of " + renderChar(ch));
73
            }
74

    
75
// Get the next character. When there are no more characters,
76
// return the empty string.
77

    
78
            ch = text.charAt(at);
79
            at++;
80
            columnNumber++;
81
            if (ch === '\n' || ch === '\r' && peek() !== '\n') {
82
                lineNumber++;
83
                columnNumber = 0;
84
            }
85
            return ch;
86
        },
87

    
88
        peek = function () {
89

    
90
// Get the next character without consuming it or
91
// assigning it to the ch varaible.
92

    
93
            return text.charAt(at);
94
        },
95

    
96
        identifier = function () {
97

    
98
// Parse an identifier. Normally, reserved words are disallowed here, but we
99
// only use this for unquoted object keys, where reserved words are allowed,
100
// so we don't check for those here. References:
101
// - http://es5.github.com/#x7.6
102
// - https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Variables
103
// - http://docstore.mik.ua/orelly/webprog/jscript/ch02_07.htm
104
// TODO Identifiers can have Unicode "letters" in them; add support for those.
105

    
106
            var key = ch;
107

    
108
            // Identifiers must start with a letter, _ or $.
109
            if ((ch !== '_' && ch !== '$') &&
110
                    (ch < 'a' || ch > 'z') &&
111
                    (ch < 'A' || ch > 'Z')) {
112
                error("Bad identifier as unquoted key");
113
            }
114

    
115
            // Subsequent characters can contain digits.
116
            while (next() && (
117
                    ch === '_' || ch === '$' ||
118
                    (ch >= 'a' && ch <= 'z') ||
119
                    (ch >= 'A' && ch <= 'Z') ||
120
                    (ch >= '0' && ch <= '9'))) {
121
                key += ch;
122
            }
123

    
124
            return key;
125
        },
126

    
127
        number = function () {
128

    
129
// Parse a number value.
130

    
131
            var number,
132
                sign = '',
133
                string = '',
134
                base = 10;
135

    
136
            if (ch === '-' || ch === '+') {
137
                sign = ch;
138
                next(ch);
139
            }
140

    
141
            // support for Infinity (could tweak to allow other words):
142
            if (ch === 'I') {
143
                number = word();
144
                if (typeof number !== 'number' || isNaN(number)) {
145
                    error('Unexpected word for number');
146
                }
147
                return (sign === '-') ? -number : number;
148
            }
149

    
150
            // support for NaN
151
            if (ch === 'N' ) {
152
              number = word();
153
              if (!isNaN(number)) {
154
                error('expected word to be NaN');
155
              }
156
              // ignore sign as -NaN also is NaN
157
              return number;
158
            }
159

    
160
            if (ch === '0') {
161
                string += ch;
162
                next();
163
                if (ch === 'x' || ch === 'X') {
164
                    string += ch;
165
                    next();
166
                    base = 16;
167
                } else if (ch >= '0' && ch <= '9') {
168
                    error('Octal literal');
169
                }
170
            }
171

    
172
            switch (base) {
173
            case 10:
174
                while (ch >= '0' && ch <= '9' ) {
175
                    string += ch;
176
                    next();
177
                }
178
                if (ch === '.') {
179
                    string += '.';
180
                    while (next() && ch >= '0' && ch <= '9') {
181
                        string += ch;
182
                    }
183
                }
184
                if (ch === 'e' || ch === 'E') {
185
                    string += ch;
186
                    next();
187
                    if (ch === '-' || ch === '+') {
188
                        string += ch;
189
                        next();
190
                    }
191
                    while (ch >= '0' && ch <= '9') {
192
                        string += ch;
193
                        next();
194
                    }
195
                }
196
                break;
197
            case 16:
198
                while (ch >= '0' && ch <= '9' || ch >= 'A' && ch <= 'F' || ch >= 'a' && ch <= 'f') {
199
                    string += ch;
200
                    next();
201
                }
202
                break;
203
            }
204

    
205
            if(sign === '-') {
206
                number = -string;
207
            } else {
208
                number = +string;
209
            }
210

    
211
            if (!isFinite(number)) {
212
                error("Bad number");
213
            } else {
214
                return number;
215
            }
216
        },
217

    
218
        string = function () {
219

    
220
// Parse a string value.
221

    
222
            var hex,
223
                i,
224
                string = '',
225
                delim,      // double quote or single quote
226
                uffff;
227

    
228
// When parsing for string values, we must look for ' or " and \ characters.
229

    
230
            if (ch === '"' || ch === "'") {
231
                delim = ch;
232
                while (next()) {
233
                    if (ch === delim) {
234
                        next();
235
                        return string;
236
                    } else if (ch === '\\') {
237
                        next();
238
                        if (ch === 'u') {
239
                            uffff = 0;
240
                            for (i = 0; i < 4; i += 1) {
241
                                hex = parseInt(next(), 16);
242
                                if (!isFinite(hex)) {
243
                                    break;
244
                                }
245
                                uffff = uffff * 16 + hex;
246
                            }
247
                            string += String.fromCharCode(uffff);
248
                        } else if (ch === '\r') {
249
                            if (peek() === '\n') {
250
                                next();
251
                            }
252
                        } else if (typeof escapee[ch] === 'string') {
253
                            string += escapee[ch];
254
                        } else {
255
                            break;
256
                        }
257
                    } else if (ch === '\n') {
258
                        // unescaped newlines are invalid; see:
259
                        // https://github.com/aseemk/json5/issues/24
260
                        // TODO this feels special-cased; are there other
261
                        // invalid unescaped chars?
262
                        break;
263
                    } else {
264
                        string += ch;
265
                    }
266
                }
267
            }
268
            error("Bad string");
269
        },
270

    
271
        inlineComment = function () {
272

    
273
// Skip an inline comment, assuming this is one. The current character should
274
// be the second / character in the // pair that begins this inline comment.
275
// To finish the inline comment, we look for a newline or the end of the text.
276

    
277
            if (ch !== '/') {
278
                error("Not an inline comment");
279
            }
280

    
281
            do {
282
                next();
283
                if (ch === '\n' || ch === '\r') {
284
                    next();
285
                    return;
286
                }
287
            } while (ch);
288
        },
289

    
290
        blockComment = function () {
291

    
292
// Skip a block comment, assuming this is one. The current character should be
293
// the * character in the /* pair that begins this block comment.
294
// To finish the block comment, we look for an ending */ pair of characters,
295
// but we also watch for the end of text before the comment is terminated.
296

    
297
            if (ch !== '*') {
298
                error("Not a block comment");
299
            }
300

    
301
            do {
302
                next();
303
                while (ch === '*') {
304
                    next('*');
305
                    if (ch === '/') {
306
                        next('/');
307
                        return;
308
                    }
309
                }
310
            } while (ch);
311

    
312
            error("Unterminated block comment");
313
        },
314

    
315
        comment = function () {
316

    
317
// Skip a comment, whether inline or block-level, assuming this is one.
318
// Comments always begin with a / character.
319

    
320
            if (ch !== '/') {
321
                error("Not a comment");
322
            }
323

    
324
            next('/');
325

    
326
            if (ch === '/') {
327
                inlineComment();
328
            } else if (ch === '*') {
329
                blockComment();
330
            } else {
331
                error("Unrecognized comment");
332
            }
333
        },
334

    
335
        white = function () {
336

    
337
// Skip whitespace and comments.
338
// Note that we're detecting comments by only a single / character.
339
// This works since regular expressions are not valid JSON(5), but this will
340
// break if there are other valid values that begin with a / character!
341

    
342
            while (ch) {
343
                if (ch === '/') {
344
                    comment();
345
                } else if (ws.indexOf(ch) >= 0) {
346
                    next();
347
                } else {
348
                    return;
349
                }
350
            }
351
        },
352

    
353
        word = function () {
354

    
355
// true, false, or null.
356

    
357
            switch (ch) {
358
            case 't':
359
                next('t');
360
                next('r');
361
                next('u');
362
                next('e');
363
                return true;
364
            case 'f':
365
                next('f');
366
                next('a');
367
                next('l');
368
                next('s');
369
                next('e');
370
                return false;
371
            case 'n':
372
                next('n');
373
                next('u');
374
                next('l');
375
                next('l');
376
                return null;
377
            case 'I':
378
                next('I');
379
                next('n');
380
                next('f');
381
                next('i');
382
                next('n');
383
                next('i');
384
                next('t');
385
                next('y');
386
                return Infinity;
387
            case 'N':
388
              next( 'N' );
389
              next( 'a' );
390
              next( 'N' );
391
              return NaN;
392
            }
393
            error("Unexpected " + renderChar(ch));
394
        },
395

    
396
        value,  // Place holder for the value function.
397

    
398
        array = function () {
399

    
400
// Parse an array value.
401

    
402
            var array = [];
403

    
404
            if (ch === '[') {
405
                next('[');
406
                white();
407
                while (ch) {
408
                    if (ch === ']') {
409
                        next(']');
410
                        return array;   // Potentially empty array
411
                    }
412
                    // ES5 allows omitting elements in arrays, e.g. [,] and
413
                    // [,null]. We don't allow this in JSON5.
414
                    if (ch === ',') {
415
                        error("Missing array element");
416
                    } else {
417
                        array.push(value());
418
                    }
419
                    white();
420
                    // If there's no comma after this value, this needs to
421
                    // be the end of the array.
422
                    if (ch !== ',') {
423
                        next(']');
424
                        return array;
425
                    }
426
                    next(',');
427
                    white();
428
                }
429
            }
430
            error("Bad array");
431
        },
432

    
433
        object = function () {
434

    
435
// Parse an object value.
436

    
437
            var key,
438
                object = {};
439

    
440
            if (ch === '{') {
441
                next('{');
442
                white();
443
                while (ch) {
444
                    if (ch === '}') {
445
                        next('}');
446
                        return object;   // Potentially empty object
447
                    }
448

    
449
                    // Keys can be unquoted. If they are, they need to be
450
                    // valid JS identifiers.
451
                    if (ch === '"' || ch === "'") {
452
                        key = string();
453
                    } else {
454
                        key = identifier();
455
                    }
456

    
457
                    white();
458
                    next(':');
459
                    object[key] = value();
460
                    white();
461
                    // If there's no comma after this pair, this needs to be
462
                    // the end of the object.
463
                    if (ch !== ',') {
464
                        next('}');
465
                        return object;
466
                    }
467
                    next(',');
468
                    white();
469
                }
470
            }
471
            error("Bad object");
472
        };
473

    
474
    value = function () {
475

    
476
// Parse a JSON value. It could be an object, an array, a string, a number,
477
// or a word.
478

    
479
        white();
480
        switch (ch) {
481
        case '{':
482
            return object();
483
        case '[':
484
            return array();
485
        case '"':
486
        case "'":
487
            return string();
488
        case '-':
489
        case '+':
490
        case '.':
491
            return number();
492
        default:
493
            return ch >= '0' && ch <= '9' ? number() : word();
494
        }
495
    };
496

    
497
// Return the json_parse function. It will have access to all of the above
498
// functions and variables.
499

    
500
    return function (source, reviver) {
501
        var result;
502

    
503
        text = String(source);
504
        at = 0;
505
        lineNumber = 1;
506
        columnNumber = 1;
507
        ch = ' ';
508
        result = value();
509
        white();
510
        if (ch) {
511
            error("Syntax error");
512
        }
513

    
514
// If there is a reviver function, we recursively walk the new structure,
515
// passing each name/value pair to the reviver function for possible
516
// transformation, starting with a temporary root object that holds the result
517
// in an empty key. If there is not a reviver function, we simply return the
518
// result.
519

    
520
        return typeof reviver === 'function' ? (function walk(holder, key) {
521
            var k, v, value = holder[key];
522
            if (value && typeof value === 'object') {
523
                for (k in value) {
524
                    if (Object.prototype.hasOwnProperty.call(value, k)) {
525
                        v = walk(value, k);
526
                        if (v !== undefined) {
527
                            value[k] = v;
528
                        } else {
529
                            delete value[k];
530
                        }
531
                    }
532
                }
533
            }
534
            return reviver.call(holder, key, value);
535
        }({'': result}, '')) : result;
536
    };
537
}());
538

    
539
// JSON5 stringify will not quote keys where appropriate
540
JSON5.stringify = function (obj, replacer, space) {
541
    if (replacer && (typeof(replacer) !== "function" && !isArray(replacer))) {
542
        throw new Error('Replacer must be a function or an array');
543
    }
544
    var getReplacedValueOrUndefined = function(holder, key, isTopLevel) {
545
        var value = holder[key];
546

    
547
        // Replace the value with its toJSON value first, if possible
548
        if (value && value.toJSON && typeof value.toJSON === "function") {
549
            value = value.toJSON();
550
        }
551

    
552
        // If the user-supplied replacer if a function, call it. If it's an array, check objects' string keys for
553
        // presence in the array (removing the key/value pair from the resulting JSON if the key is missing).
554
        if (typeof(replacer) === "function") {
555
            return replacer.call(holder, key, value);
556
        } else if(replacer) {
557
            if (isTopLevel || isArray(holder) || replacer.indexOf(key) >= 0) {
558
                return value;
559
            } else {
560
                return undefined;
561
            }
562
        } else {
563
            return value;
564
        }
565
    };
566

    
567
    function isWordChar(c) {
568
        return (c >= 'a' && c <= 'z') ||
569
            (c >= 'A' && c <= 'Z') ||
570
            (c >= '0' && c <= '9') ||
571
            c === '_' || c === '$';
572
    }
573

    
574
    function isWordStart(c) {
575
        return (c >= 'a' && c <= 'z') ||
576
            (c >= 'A' && c <= 'Z') ||
577
            c === '_' || c === '$';
578
    }
579

    
580
    function isWord(key) {
581
        if (typeof key !== 'string') {
582
            return false;
583
        }
584
        if (!isWordStart(key[0])) {
585
            return false;
586
        }
587
        var i = 1, length = key.length;
588
        while (i < length) {
589
            if (!isWordChar(key[i])) {
590
                return false;
591
            }
592
            i++;
593
        }
594
        return true;
595
    }
596

    
597
    // export for use in tests
598
    JSON5.isWord = isWord;
599

    
600
    // polyfills
601
    function isArray(obj) {
602
        if (Array.isArray) {
603
            return Array.isArray(obj);
604
        } else {
605
            return Object.prototype.toString.call(obj) === '[object Array]';
606
        }
607
    }
608

    
609
    function isDate(obj) {
610
        return Object.prototype.toString.call(obj) === '[object Date]';
611
    }
612

    
613
    var objStack = [];
614
    function checkForCircular(obj) {
615
        for (var i = 0; i < objStack.length; i++) {
616
            if (objStack[i] === obj) {
617
                throw new TypeError("Converting circular structure to JSON");
618
            }
619
        }
620
    }
621

    
622
    function makeIndent(str, num, noNewLine) {
623
        if (!str) {
624
            return "";
625
        }
626
        // indentation no more than 10 chars
627
        if (str.length > 10) {
628
            str = str.substring(0, 10);
629
        }
630

    
631
        var indent = noNewLine ? "" : "\n";
632
        for (var i = 0; i < num; i++) {
633
            indent += str;
634
        }
635

    
636
        return indent;
637
    }
638

    
639
    var indentStr;
640
    if (space) {
641
        if (typeof space === "string") {
642
            indentStr = space;
643
        } else if (typeof space === "number" && space >= 0) {
644
            indentStr = makeIndent(" ", space, true);
645
        } else {
646
            // ignore space parameter
647
        }
648
    }
649

    
650
    // Copied from Crokford's implementation of JSON
651
    // See https://github.com/douglascrockford/JSON-js/blob/e39db4b7e6249f04a195e7dd0840e610cc9e941e/json2.js#L195
652
    // Begin
653
    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
654
        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
655
        meta = { // table of character substitutions
656
        '\b': '\\b',
657
        '\t': '\\t',
658
        '\n': '\\n',
659
        '\f': '\\f',
660
        '\r': '\\r',
661
        '"' : '\\"',
662
        '\\': '\\\\'
663
    };
664
    function escapeString(string) {
665

    
666
// If the string contains no control characters, no quote characters, and no
667
// backslash characters, then we can safely slap some quotes around it.
668
// Otherwise we must also replace the offending characters with safe escape
669
// sequences.
670
        escapable.lastIndex = 0;
671
        return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
672
            var c = meta[a];
673
            return typeof c === 'string' ?
674
                c :
675
                '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
676
        }) + '"' : '"' + string + '"';
677
    }
678
    // End
679

    
680
    function internalStringify(holder, key, isTopLevel) {
681
        var buffer, res;
682

    
683
        // Replace the value, if necessary
684
        var obj_part = getReplacedValueOrUndefined(holder, key, isTopLevel);
685

    
686
        if (obj_part && !isDate(obj_part)) {
687
            // unbox objects
688
            // don't unbox dates, since will turn it into number
689
            obj_part = obj_part.valueOf();
690
        }
691
        switch(typeof obj_part) {
692
            case "boolean":
693
                return obj_part.toString();
694

    
695
            case "number":
696
                if (isNaN(obj_part) || !isFinite(obj_part)) {
697
                    return "null";
698
                }
699
                return obj_part.toString();
700

    
701
            case "string":
702
                return escapeString(obj_part.toString());
703

    
704
            case "object":
705
                if (obj_part === null) {
706
                    return "null";
707
                } else if (isArray(obj_part)) {
708
                    checkForCircular(obj_part);
709
                    buffer = "[";
710
                    objStack.push(obj_part);
711

    
712
                    for (var i = 0; i < obj_part.length; i++) {
713
                        res = internalStringify(obj_part, i, false);
714
                        buffer += makeIndent(indentStr, objStack.length);
715
                        if (res === null || typeof res === "undefined") {
716
                            buffer += "null";
717
                        } else {
718
                            buffer += res;
719
                        }
720
                        if (i < obj_part.length-1) {
721
                            buffer += ",";
722
                        } else if (indentStr) {
723
                            buffer += "\n";
724
                        }
725
                    }
726
                    objStack.pop();
727
                    if (obj_part.length) {
728
                        buffer += makeIndent(indentStr, objStack.length, true)
729
                    }
730
                    buffer += "]";
731
                } else {
732
                    checkForCircular(obj_part);
733
                    buffer = "{";
734
                    var nonEmpty = false;
735
                    objStack.push(obj_part);
736
                    for (var prop in obj_part) {
737
                        if (obj_part.hasOwnProperty(prop)) {
738
                            var value = internalStringify(obj_part, prop, false);
739
                            isTopLevel = false;
740
                            if (typeof value !== "undefined" && value !== null) {
741
                                buffer += makeIndent(indentStr, objStack.length);
742
                                nonEmpty = true;
743
                                key = isWord(prop) ? prop : escapeString(prop);
744
                                buffer += key + ":" + (indentStr ? ' ' : '') + value + ",";
745
                            }
746
                        }
747
                    }
748
                    objStack.pop();
749
                    if (nonEmpty) {
750
                        buffer = buffer.substring(0, buffer.length-1) + makeIndent(indentStr, objStack.length) + "}";
751
                    } else {
752
                        buffer = '{}';
753
                    }
754
                }
755
                return buffer;
756
            default:
757
                // functions and undefined should be ignored
758
                return undefined;
759
        }
760
    }
761

    
762
    // special case...when undefined is used inside of
763
    // a compound object/array, return null.
764
    // but when top-level, return undefined
765
    var topLevelHolder = {"":obj};
766
    if (obj === undefined) {
767
        return getReplacedValueOrUndefined(topLevelHolder, '', true);
768
    }
769
    return internalStringify(topLevelHolder, '', true);
770
};
(2-2/3)