UNPKG

13.4 kBJavaScriptView Raw
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
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 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
435JSON5.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};