'use strict'; var escapedChars = { 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', '"': '"', '/': '/', '\\': '\\' }; var A_CODE = 'a'.charCodeAt(); function parse(source) { var pointers = {}; var line = 0; var column = 0; var pos = 0; return { data: _parse('', true), pointers: pointers }; function _parse(ptr, topLevel) { whitespace(); var data; map(ptr, 'value'); var char = getChar(); switch (char) { case 't': read('rue'); data = true; break; case 'f': read('alse'); data = false; break; case 'n': read('ull'); data = null; break; case '"': data = parseString(); break; case '[': data = parseArray(ptr); break; case '{': data = parseObject(ptr); break; default: backChar(); if ('-0123456789'.indexOf(char) >= 0) data = parseNumber(); else unexpectedToken(); } map(ptr, 'valueEnd'); whitespace(); if (topLevel && pos < source.length) unexpectedToken(); return data; } function whitespace() { loop: while (pos < source.length) { switch (source[pos]) { case ' ': column++; break; case '\t': column += 4; break; case '\r': column = 0; break; case '\n': column = 0; line++; break; default: break loop; } pos++; } } function parseString() { var str = ''; var char; while (true) { char = getChar(); if (char == '"') { break; } else if (char == '\\') { char = getChar(); if (char in escapedChars) str += escapedChars[char]; else if (char == 'u') str += getCharCode(); else wasUnexpectedToken(); } else { str += char; } } return str; } function parseNumber() { var numStr = ''; if (source[pos] == '-') numStr += getChar(); numStr += source[pos] == '0' ? getChar() : getDigits(); if (source[pos] == '.') numStr += getChar() + getDigits(); if (source[pos] == 'e' || source[pos] == 'E') { numStr += getChar(); if (source[pos] == '+' || source[pos] == '-') numStr += getChar(); numStr += getDigits(); } return +numStr; } function parseArray(ptr) { whitespace(); var arr = []; var i = 0; if (getChar() == ']') return arr; backChar(); while (true) { var itemPtr = ptr + '/' + i; arr.push(_parse(itemPtr)); whitespace(); var char = getChar(); if (char == ']') break; if (char != ',') wasUnexpectedToken(); whitespace(); i++; } return arr; } function parseObject(ptr) { whitespace(); var obj = {}; if (getChar() == '}') return obj; backChar(); while (true) { var loc = getLoc(); if (getChar() != '"') wasUnexpectedToken(); var key = parseString(); var propPtr = ptr + '/' + escapeJsonPointer(key); mapLoc(propPtr, 'key', loc); map(propPtr, 'keyEnd'); whitespace(); if (getChar() != ':') wasUnexpectedToken(); whitespace(); obj[key] = _parse(propPtr); whitespace(); var char = getChar(); if (char == '}') break; if (char != ',') wasUnexpectedToken(); whitespace(); } return obj; } function read(str) { for (var i = 0; i < str.length; i++) if (getChar() !== str[i]) wasUnexpectedToken(); } function getChar() { checkUnexpectedEnd(); var char = source[pos]; pos++; column++; // new line? return char; } function backChar() { pos--; column--; } function getCharCode() { var count = 4; var code = 0; while (count--) { code <<= 4; var char = getChar().toLowerCase(); if (char >= 'a' && char <= 'f') code += char.charCodeAt() - A_CODE + 10; else if (char >= '0' && char <= '9') code += +char; else wasUnexpectedToken(); } return String.fromCharCode(code); } function getDigits() { var digits = ''; while (source[pos] >= '0' && source[pos] <= '9') digits += getChar(); if (digits.length) return digits; checkUnexpectedEnd(); unexpectedToken(); } function map(ptr, prop) { mapLoc(ptr, prop, getLoc()); } function mapLoc(ptr, prop, loc) { pointers[ptr] = pointers[ptr] || {}; pointers[ptr][prop] = loc; } function getLoc() { return { line: line, column: column, pos: pos }; } function unexpectedToken() { throw new SyntaxError('Unexpected token ' + source[pos] + ' in JSON at position ' + pos); } function wasUnexpectedToken() { backChar(); unexpectedToken(); } function checkUnexpectedEnd() { if (pos >= source.length) throw new SyntaxError('Unexpected end of JSON input'); } }; function stringify(data, _, whitespace) { if (!validType(data)) return; var wsLine = 0; var wsPos, wsColumn; switch (typeof whitespace) { case 'number': var len = whitespace > 10 ? 10 : whitespace < 0 ? 0 : Math.floor(whitespace); whitespace = len && repeat(len, ' '); wsPos = len; wsColumn = len; break; case 'string': whitespace = whitespace.slice(0, 10); wsPos = 0; wsColumn = 0; for (var j = 0; j < whitespace.length; j++) { var char = whitespace[j]; switch (char) { case ' ': wsColumn++; break; case '\t': wsColumn += 4; break; case '\r': wsColumn = 0; break; case '\n': wsColumn = 0; wsLine++; break; default: throw new Error('whitespace characters not allowed in JSON'); } wsPos++; } break; default: whitespace = undefined; } var json = ''; var pointers = {}; var line = 0; var column = 0; var pos = 0; _stringify(data, 0, ''); return { json: json, pointers: pointers }; function _stringify(_data, lvl, ptr) { map(ptr, 'value'); switch (typeof _data) { case 'number': case 'boolean': out('' + _data); break; case 'string': out(quoted(_data)); break; case 'object': if (_data === null) out('null'); else if (typeof _data.toJSON == 'function') out(quoted(_data.toJSON())); else if (Array.isArray(_data)) stringifyArray(); else stringifyObject(); } map(ptr, 'valueEnd'); function stringifyArray() { if (_data.length) { out('['); var itemLvl = lvl + 1; for (var i = 0; i < _data.length; i++) { if (i) out(','); indent(itemLvl); var item = validType(_data[i]) ? _data[i] : null; var itemPtr = ptr + '/' + i; _stringify(item, itemLvl, itemPtr); } indent(lvl); out(']'); } else { out('[]'); } } function stringifyObject() { var keys = Object.keys(_data); if (keys.length) { out('{'); var propLvl = lvl + 1; for (var i = 0; i < keys.length; i++) { var key = keys[i]; var value = _data[key]; if (validType(value)) { if (i) out(','); var propPtr = ptr + '/' + escapeJsonPointer(key); indent(propLvl); map(propPtr, 'key'); out(quoted(key)); map(propPtr, 'keyEnd'); out(':'); if (whitespace) out(' '); _stringify(value, propLvl, propPtr); } } indent(lvl); out('}'); } else { out('{}'); } } } function out(str) { column += str.length; pos += str.length; json += str; } function indent(lvl) { if (whitespace) { json += '\n' + repeat(lvl, whitespace); line++; column = 0; while (lvl--) { if (wsLine) { line += wsLine; column = wsColumn; } else { column += wsColumn; } pos += wsPos; } pos += 1; // \n character } } function map(ptr, prop) { pointers[ptr] = pointers[ptr] || {}; pointers[ptr][prop] = { line: line, column: column, pos: pos }; } function repeat(n, str) { return Array(n + 1).join(str); } }; var VALID_TYPES = ['number', 'boolean', 'string', 'object']; function validType(data) { return VALID_TYPES.indexOf(typeof data) >= 0; } var ESC_QUOTE = /"|\\/g; var ESC_B = /[\b]/g; var ESC_F = /\f/g; var ESC_N = /\n/g; var ESC_R = /\r/g; var ESC_T = /\t/g; function quoted(str) { str = str.replace(ESC_QUOTE, '\\$&') .replace(ESC_F, '\\f') .replace(ESC_B, '\\b') .replace(ESC_N, '\\n') .replace(ESC_R, '\\r') .replace(ESC_T, '\\t'); return '"' + str + '"'; } var ESC_0 = /~/g; var ESC_1 = /\//g; function escapeJsonPointer(str) { return str.replace(ESC_0, '~0') .replace(ESC_1, '~1'); }