Created
February 23, 2020 04:31
-
-
Save danneu/0ebf4d52aa58e145d1c4b306756dbed4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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. | |
if (!Array.prototype.indexOf){ | |
Array.prototype.indexOf = function (ch) { | |
for(var i = 0; i < this.length; i++){ | |
if (this[i] === ch){ | |
return i; | |
} | |
} | |
return -1; | |
} | |
} | |
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: " + JSON5.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(); | |
buffer += makeIndent(indentStr, objStack.length, true) + "]"; | |
} 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); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment