diff --git a/HISTORY.md b/HISTORY.md index a045243..143658f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,7 @@ https://github.com/josdejong/jsoneditor - Implemented new mode `preview`, capable of working with large JSON documents up to 500 MiB. +- Repair button is now capable of turning MongoDB documents into valid JSON. - Fixed #730: in `code` mode, there was an initial undo action which clears the content. - Upgraded dependencies `vanilla-picker@2.9.1`, `mobius1-selectr@2.4.13`, diff --git a/src/js/util.js b/src/js/util.js index 40c9d32..8876065 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -36,6 +36,8 @@ exports.parse = function parse(jsonString) { * @returns {string} json */ exports.repair = function (jsString) { + // TODO: refactor this function, it's too large and complicated now + // escape all single and double quotes inside strings var chars = []; var i = 0; @@ -116,41 +118,49 @@ exports.repair = function (jsString) { } } - // parse single or double quoted string + /** + * parse single or double quoted string. Returns the parsed string + * @param {string} endQuote + * @return {string} + */ function parseString(endQuote) { - chars.push('"'); + var string = ''; + + string += '"'; i++; var c = curr(); while (i < jsString.length && c !== endQuote) { if (c === '"' && prev() !== '\\') { // unescaped double quote, escape it - chars.push('\\"'); + string += '\\"'; } else if (controlChars.hasOwnProperty(c)) { // replace unescaped control characters with escaped ones - chars.push(controlChars[c]) + string += controlChars[c] } else if (c === '\\') { // remove the escape character when followed by a single quote ', not needed i++; c = curr(); if (c !== '\'') { - chars.push('\\'); + string += '\\'; } - chars.push(c); + string += c; } else { // regular character - chars.push(c); + string += c; } i++; c = curr(); } if (c === endQuote) { - chars.push('"'); + string += '"'; i++; } + + return string; } // parse an unquoted key @@ -167,13 +177,69 @@ exports.repair = function (jsString) { } if (specialValues.indexOf(key) === -1) { - chars.push('"' + key + '"'); + return '"' + key + '"'; } else { - chars.push(key); + return key; } } + function parseMongoDataType () { + var c = curr(); + var value; + var dataType = ''; + while (/[a-zA-Z_$]/.test(c)) { + dataType += c + i++; + c = curr(); + } + + if (dataType.length > 0 && c === '(') { + // This is an MongoDB data type like {"_id": ObjectId("123")} + i++; + c = curr(); + if (c === '"') { + // a data type containing a string, like ISODate("2012-12-19T06:01:17.171Z") + value = parseString(c); + c = curr(); + } + else { + // a data type containing a value, like 'NumberLong(2)' + value = ''; + while(c !== ')' && c !== '') { + value += c; + i++; + c = curr(); + } + } + + if (c === ')') { + // skip the closing bracket at the end + i++; + + // return the value (strip the data type object) + return value; + } + else { + // huh? that's unexpected. don't touch it + return dataType + '(' + value + c; + } + } + else { + // hm, no Mongo data type after all + return dataType; + } + } + + function isSpecialWhiteSpace (c) { + return ( + c === '\u00A0' || + (c >= '\u2000' && c <= '\u200A') || + c === '\u202F' || + c === '\u205F' || + c === '\u3000') + } + while(i < jsString.length) { var c = curr(); @@ -183,25 +249,25 @@ exports.repair = function (jsString) { else if (c === '/' && next() === '/') { skipComment(); } - else if (c === '\u00A0' || (c >= '\u2000' && c <= '\u200A') || c === '\u202F' || c === '\u205F' || c === '\u3000') { + else if (isSpecialWhiteSpace(c)) { // special white spaces (like non breaking space) chars.push(' '); i++ } else if (c === quote) { - parseString(quote); + chars.push(parseString(c)); } else if (c === quoteDbl) { - parseString(quoteDbl); + chars.push(parseString(quoteDbl)); } else if (c === graveAccent) { - parseString(acuteAccent); + chars.push(parseString(acuteAccent)); } else if (c === quoteLeft) { - parseString(quoteRight); + chars.push(parseString(quoteRight)); } else if (c === quoteDblLeft) { - parseString(quoteDblRight); + chars.push(parseString(quoteDblRight)); } else if (c === ',' && [']', '}'].indexOf(nextNonWhiteSpace()) !== -1) { // skip trailing commas @@ -209,11 +275,16 @@ exports.repair = function (jsString) { } else if (/[a-zA-Z_$]/.test(c) && ['{', ','].indexOf(lastNonWhitespace()) !== -1) { // an unquoted object key (like a in '{a:2}') - parseKey(); + chars.push(parseKey()); } else { - chars.push(c); - i++; + if (/[a-zA-Z_$]/.test(c)) { + chars.push(parseMongoDataType()); + } + else { + chars.push(c); + i++; + } } } diff --git a/test/util.test.js b/test/util.test.js index 3e88be5..9116338 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -76,7 +76,6 @@ describe('util', function () { assert.strictEqual(util.repair('\n/* foo\nbar */\ncallback_123 ({});\n\n'), '{}'); // non-matching - assert.strictEqual(util.repair('callback abc({});'), 'callback abc({});'); assert.strictEqual(util.repair('callback {}'), 'callback {}'); assert.strictEqual(util.repair('callback({}'), 'callback({}'); }); @@ -93,6 +92,33 @@ describe('util', function () { assert.strictEqual(util.repair('"{a:2,}"'), '"{a:2,}"'); }); + it('should strip MongoDB data types', function () { + const mongoDocument = '{\n' + + ' "_id" : ObjectId("123"),\n' + + ' "isoDate" : ISODate("2012-12-19T06:01:17.171Z"),\n' + + ' "regularNumber" : 67,\n' + + ' "long" : NumberLong("2"),\n' + + ' "long2" : NumberLong(2),\n' + + ' "int" : NumberInt("3"),\n' + + ' "int2" : NumberInt(3),\n' + + ' "decimal" : NumberDecimal("4"),\n' + + ' "decimal2" : NumberDecimal(4)\n' + + '}'; + + const expectedJson = '{\n' + + ' "_id" : "123",\n' + + ' "isoDate" : "2012-12-19T06:01:17.171Z",\n' + + ' "regularNumber" : 67,\n' + + ' "long" : "2",\n' + + ' "long2" : 2,\n' + + ' "int" : "3",\n' + + ' "int2" : 3,\n' + + ' "decimal" : "4",\n' + + ' "decimal2" : 4\n' + + '}'; + + assert.strictEqual(util.repair(mongoDocument), expectedJson); + }); }); describe('jsonPath', function () {