// json5.js // Modern JSON. See README.md for details. // // This file is based directly off of Douglas Crockford's json_parse.js: // https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js var JSON5 = (typeof exports === 'object' ? exports : {}); JSON5.parse = (function () { "use strict"; // This is a function that can parse a JSON5 text, producing a JavaScript // data structure. It is a simple, recursive descent parser. It does not use // eval or regular expressions, so it can be used as a model for implementing // a JSON5 parser in other languages. // We are defining the function inside of another function to avoid creating // global variables. var at, // The index of the current character lineNumber, // The current line number columnNumber, // The current column number ch, // The current character escapee = { "'": "'", '"': '"', '\\': '\\', '/': '/', '\n': '', // Replace escaped newlines in strings w/ empty string b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' }, ws = [ ' ', '\t', '\r', '\n', '\v', '\f', '\xA0', '\uFEFF' ], text, renderChar = function (chr) { return chr === '' ? 'EOF' : "'" + chr + "'"; }, error = function (m) { // Call error when something is wrong. var error = new SyntaxError(); // 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 error.message = m + " at line " + lineNumber + " column " + columnNumber + " of the JSON5 data. Still to read: " + JSON.stringify(text.substring(at - 1, at + 19)); error.at = at; // These two property names have been chosen to agree with the ones in Gecko, the only popular // environment which seems to supply this info on JSON.parse error.lineNumber = lineNumber; error.columnNumber = columnNumber; throw error; }, next = function (c) { // If a c parameter is provided, verify that it matches the current character. if (c && c !== ch) { error("Expected " + renderChar(c) + " instead of " + renderChar(ch)); } // Get the next character. When there are no more characters, // return the empty string. ch = text.charAt(at); at++; columnNumber++; if (ch === '\n' || ch === '\r' && peek() !== '\n') { lineNumber++; columnNumber = 0; } return ch; }, peek = function () { // Get the next character without consuming it or // assigning it to the ch varaible. return text.charAt(at); }, identifier = function () { // Parse an identifier. Normally, reserved words are disallowed here, but we // only use this for unquoted object keys, where reserved words are allowed, // so we don't check for those here. References: // - http://es5.github.com/#x7.6 // - https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Variables // - http://docstore.mik.ua/orelly/webprog/jscript/ch02_07.htm // TODO Identifiers can have Unicode "letters" in them; add support for those. var key = ch; // Identifiers must start with a letter, _ or $. if ((ch !== '_' && ch !== '$') && (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z')) { error("Bad identifier as unquoted key"); } // Subsequent characters can contain digits. while (next() && ( ch === '_' || ch === '$' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'))) { key += ch; } return key; }, number = function () { // Parse a number value. var number, sign = '', string = '', base = 10; if (ch === '-' || ch === '+') { sign = ch; next(ch); } // support for Infinity (could tweak to allow other words): if (ch === 'I') { number = word(); if (typeof number !== 'number' || isNaN(number)) { error('Unexpected word for number'); } return (sign === '-') ? -number : number; } // support for NaN if (ch === 'N' ) { number = word(); if (!isNaN(number)) { error('expected word to be NaN'); } // ignore sign as -NaN also is NaN return number; } if (ch === '0') { string += ch; next(); if (ch === 'x' || ch === 'X') { string += ch; next(); base = 16; } else if (ch >= '0' && ch <= '9') { error('Octal literal'); } } switch (base) { case 10: while (ch >= '0' && ch <= '9' ) { string += ch; next(); } if (ch === '.') { string += '.'; while (next() && ch >= '0' && ch <= '9') { string += ch; } } if (ch === 'e' || ch === 'E') { string += ch; next(); if (ch === '-' || ch === '+') { string += ch; next(); } while (ch >= '0' && ch <= '9') { string += ch; next(); } } break; case 16: while (ch >= '0' && ch <= '9' || ch >= 'A' && ch <= 'F' || ch >= 'a' && ch <= 'f') { string += ch; next(); } break; } if(sign === '-') { number = -string; } else { number = +string; } if (!isFinite(number)) { error("Bad number"); } else { return number; } }, string = function () { // Parse a string value. var hex, i, string = '', delim, // double quote or single quote uffff; // When parsing for string values, we must look for ' or " and \ characters. if (ch === '"' || ch === "'") { delim = ch; while (next()) { if (ch === delim) { next(); return string; } else if (ch === '\\') { next(); if (ch === 'u') { uffff = 0; for (i = 0; i < 4; i += 1) { hex = parseInt(next(), 16); if (!isFinite(hex)) { break; } uffff = uffff * 16 + hex; } string += String.fromCharCode(uffff); } else if (ch === '\r') { if (peek() === '\n') { next(); } } else if (typeof escapee[ch] === 'string') { string += escapee[ch]; } else { break; } } else if (ch === '\n') { // unescaped newlines are invalid; see: // https://github.com/aseemk/json5/issues/24 // TODO this feels special-cased; are there other // invalid unescaped chars? break; } else { string += ch; } } } error("Bad string"); }, inlineComment = function () { // Skip an inline comment, assuming this is one. The current character should // be the second / character in the // pair that begins this inline comment. // To finish the inline comment, we look for a newline or the end of the text. if (ch !== '/') { error("Not an inline comment"); } do { next(); if (ch === '\n' || ch === '\r') { next(); return; } } while (ch); }, blockComment = function () { // Skip a block comment, assuming this is one. The current character should be // the * character in the /* pair that begins this block comment. // To finish the block comment, we look for an ending */ pair of characters, // but we also watch for the end of text before the comment is terminated. if (ch !== '*') { error("Not a block comment"); } do { next(); while (ch === '*') { next('*'); if (ch === '/') { next('/'); return; } } } while (ch); error("Unterminated block comment"); }, comment = function () { // Skip a comment, whether inline or block-level, assuming this is one. // Comments always begin with a / character. if (ch !== '/') { error("Not a comment"); } next('/'); if (ch === '/') { inlineComment(); } else if (ch === '*') { blockComment(); } else { error("Unrecognized comment"); } }, white = function () { // Skip whitespace and comments. // Note that we're detecting comments by only a single / character. // This works since regular expressions are not valid JSON(5), but this will // break if there are other valid values that begin with a / character! while (ch) { if (ch === '/') { comment(); } else if (ws.indexOf(ch) >= 0) { next(); } else { return; } } }, word = function () { // true, false, or null. switch (ch) { case 't': next('t'); next('r'); next('u'); next('e'); return true; case 'f': next('f'); next('a'); next('l'); next('s'); next('e'); return false; case 'n': next('n'); next('u'); next('l'); next('l'); return null; case 'I': next('I'); next('n'); next('f'); next('i'); next('n'); next('i'); next('t'); next('y'); return Infinity; case 'N': next( 'N' ); next( 'a' ); next( 'N' ); return NaN; } error("Unexpected " + renderChar(ch)); }, value, // Place holder for the value function. array = function () { // Parse an array value. var array = []; if (ch === '[') { next('['); white(); while (ch) { if (ch === ']') { next(']'); return array; // Potentially empty array } // ES5 allows omitting elements in arrays, e.g. [,] and // [,null]. We don't allow this in JSON5. if (ch === ',') { error("Missing array element"); } else { array.push(value()); } white(); // If there's no comma after this value, this needs to // be the end of the array. if (ch !== ',') { next(']'); return array; } next(','); white(); } } error("Bad array"); }, object = function () { // Parse an object value. var key, object = {}; if (ch === '{') { next('{'); white(); while (ch) { if (ch === '}') { next('}'); return object; // Potentially empty object } // Keys can be unquoted. If they are, they need to be // valid JS identifiers. if (ch === '"' || ch === "'") { key = string(); } else { key = identifier(); } white(); next(':'); object[key] = value(); white(); // If there's no comma after this pair, this needs to be // the end of the object. if (ch !== ',') { next('}'); return object; } next(','); white(); } } error("Bad object"); }; value = function () { // Parse a JSON value. It could be an object, an array, a string, a number, // or a word. white(); switch (ch) { case '{': return object(); case '[': return array(); case '"': case "'": return string(); case '-': case '+': case '.': return number(); default: return ch >= '0' && ch <= '9' ? number() : word(); } }; // Return the json_parse function. It will have access to all of the above // functions and variables. return function (source, reviver) { var result; text = String(source); at = 0; lineNumber = 1; columnNumber = 1; ch = ' '; result = value(); white(); if (ch) { error("Syntax error"); } // If there is a reviver function, we recursively walk the new structure, // passing each name/value pair to the reviver function for possible // transformation, starting with a temporary root object that holds the result // in an empty key. If there is not a reviver function, we simply return the // result. return typeof reviver === 'function' ? (function walk(holder, key) { var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); }({'': result}, '')) : result; }; }()); // JSON5 stringify will not quote keys where appropriate JSON5.stringify = function (obj, replacer, space) { if (replacer && (typeof(replacer) !== "function" && !isArray(replacer))) { throw new Error('Replacer must be a function or an array'); } var getReplacedValueOrUndefined = function(holder, key, isTopLevel) { var value = holder[key]; // Replace the value with its toJSON value first, if possible if (value && value.toJSON && typeof value.toJSON === "function") { value = value.toJSON(); } // If the user-supplied replacer if a function, call it. If it's an array, check objects' string keys for // presence in the array (removing the key/value pair from the resulting JSON if the key is missing). if (typeof(replacer) === "function") { return replacer.call(holder, key, value); } else if(replacer) { if (isTopLevel || isArray(holder) || replacer.indexOf(key) >= 0) { return value; } else { return undefined; } } else { return value; } }; function isWordChar(c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '_' || c === '$'; } function isWordStart(c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_' || c === '$'; } function isWord(key) { if (typeof key !== 'string') { return false; } if (!isWordStart(key[0])) { return false; } var i = 1, length = key.length; while (i < length) { if (!isWordChar(key[i])) { return false; } i++; } return true; } // export for use in tests JSON5.isWord = isWord; // polyfills function isArray(obj) { if (Array.isArray) { return Array.isArray(obj); } else { return Object.prototype.toString.call(obj) === '[object Array]'; } } function isDate(obj) { return Object.prototype.toString.call(obj) === '[object Date]'; } var objStack = []; function checkForCircular(obj) { for (var i = 0; i < objStack.length; i++) { if (objStack[i] === obj) { throw new TypeError("Converting circular structure to JSON"); } } } function makeIndent(str, num, noNewLine) { if (!str) { return ""; } // indentation no more than 10 chars if (str.length > 10) { str = str.substring(0, 10); } var indent = noNewLine ? "" : "\n"; for (var i = 0; i < num; i++) { indent += str; } return indent; } var indentStr; if (space) { if (typeof space === "string") { indentStr = space; } else if (typeof space === "number" && space >= 0) { indentStr = makeIndent(" ", space, true); } else { // ignore space parameter } } // Copied from Crokford's implementation of JSON // See https://github.com/douglascrockford/JSON-js/blob/e39db4b7e6249f04a195e7dd0840e610cc9e941e/json2.js#L195 // Begin var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }; function escapeString(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } // End function internalStringify(holder, key, isTopLevel) { var buffer, res; // Replace the value, if necessary var obj_part = getReplacedValueOrUndefined(holder, key, isTopLevel); if (obj_part && !isDate(obj_part)) { // unbox objects // don't unbox dates, since will turn it into number obj_part = obj_part.valueOf(); } switch(typeof obj_part) { case "boolean": return obj_part.toString(); case "number": if (isNaN(obj_part) || !isFinite(obj_part)) { return "null"; } return obj_part.toString(); case "string": return escapeString(obj_part.toString()); case "object": if (obj_part === null) { return "null"; } else if (isArray(obj_part)) { checkForCircular(obj_part); buffer = "["; objStack.push(obj_part); for (var i = 0; i < obj_part.length; i++) { res = internalStringify(obj_part, i, false); buffer += makeIndent(indentStr, objStack.length); if (res === null || typeof res === "undefined") { buffer += "null"; } else { buffer += res; } if (i < obj_part.length-1) { buffer += ","; } else if (indentStr) { buffer += "\n"; } } objStack.pop(); if (obj_part.length) { buffer += makeIndent(indentStr, objStack.length, true) } buffer += "]"; } else { checkForCircular(obj_part); buffer = "{"; var nonEmpty = false; objStack.push(obj_part); for (var prop in obj_part) { if (obj_part.hasOwnProperty(prop)) { var value = internalStringify(obj_part, prop, false); isTopLevel = false; if (typeof value !== "undefined" && value !== null) { buffer += makeIndent(indentStr, objStack.length); nonEmpty = true; key = isWord(prop) ? prop : escapeString(prop); buffer += key + ":" + (indentStr ? ' ' : '') + value + ","; } } } objStack.pop(); if (nonEmpty) { buffer = buffer.substring(0, buffer.length-1) + makeIndent(indentStr, objStack.length) + "}"; } else { buffer = '{}'; } } return buffer; default: // functions and undefined should be ignored return undefined; } } // special case...when undefined is used inside of // a compound object/array, return null. // but when top-level, return undefined var topLevelHolder = {"":obj}; if (obj === undefined) { return getReplacedValueOrUndefined(topLevelHolder, '', true); } return internalStringify(topLevelHolder, '', true); };