Repair button is now capable of turning MongoDB documents into valid JSON

This commit is contained in:
jos 2019-07-27 14:16:59 +02:00
parent cec16b9de1
commit 3f182a1f04
3 changed files with 118 additions and 20 deletions

View File

@ -6,6 +6,7 @@ https://github.com/josdejong/jsoneditor
- Implemented new mode `preview`, capable of working with large JSON documents - Implemented new mode `preview`, capable of working with large JSON documents
up to 500 MiB. 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 - Fixed #730: in `code` mode, there was an initial undo action which clears
the content. the content.
- Upgraded dependencies `vanilla-picker@2.9.1`, `mobius1-selectr@2.4.13`, - Upgraded dependencies `vanilla-picker@2.9.1`, `mobius1-selectr@2.4.13`,

View File

@ -36,6 +36,8 @@ exports.parse = function parse(jsonString) {
* @returns {string} json * @returns {string} json
*/ */
exports.repair = function (jsString) { exports.repair = function (jsString) {
// TODO: refactor this function, it's too large and complicated now
// escape all single and double quotes inside strings // escape all single and double quotes inside strings
var chars = []; var chars = [];
var i = 0; 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) { function parseString(endQuote) {
chars.push('"'); var string = '';
string += '"';
i++; i++;
var c = curr(); var c = curr();
while (i < jsString.length && c !== endQuote) { while (i < jsString.length && c !== endQuote) {
if (c === '"' && prev() !== '\\') { if (c === '"' && prev() !== '\\') {
// unescaped double quote, escape it // unescaped double quote, escape it
chars.push('\\"'); string += '\\"';
} }
else if (controlChars.hasOwnProperty(c)) { else if (controlChars.hasOwnProperty(c)) {
// replace unescaped control characters with escaped ones // replace unescaped control characters with escaped ones
chars.push(controlChars[c]) string += controlChars[c]
} }
else if (c === '\\') { else if (c === '\\') {
// remove the escape character when followed by a single quote ', not needed // remove the escape character when followed by a single quote ', not needed
i++; i++;
c = curr(); c = curr();
if (c !== '\'') { if (c !== '\'') {
chars.push('\\'); string += '\\';
} }
chars.push(c); string += c;
} }
else { else {
// regular character // regular character
chars.push(c); string += c;
} }
i++; i++;
c = curr(); c = curr();
} }
if (c === endQuote) { if (c === endQuote) {
chars.push('"'); string += '"';
i++; i++;
} }
return string;
} }
// parse an unquoted key // parse an unquoted key
@ -167,13 +177,69 @@ exports.repair = function (jsString) {
} }
if (specialValues.indexOf(key) === -1) { if (specialValues.indexOf(key) === -1) {
chars.push('"' + key + '"'); return '"' + key + '"';
} }
else { 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) { while(i < jsString.length) {
var c = curr(); var c = curr();
@ -183,25 +249,25 @@ exports.repair = function (jsString) {
else if (c === '/' && next() === '/') { else if (c === '/' && next() === '/') {
skipComment(); 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) // special white spaces (like non breaking space)
chars.push(' '); chars.push(' ');
i++ i++
} }
else if (c === quote) { else if (c === quote) {
parseString(quote); chars.push(parseString(c));
} }
else if (c === quoteDbl) { else if (c === quoteDbl) {
parseString(quoteDbl); chars.push(parseString(quoteDbl));
} }
else if (c === graveAccent) { else if (c === graveAccent) {
parseString(acuteAccent); chars.push(parseString(acuteAccent));
} }
else if (c === quoteLeft) { else if (c === quoteLeft) {
parseString(quoteRight); chars.push(parseString(quoteRight));
} }
else if (c === quoteDblLeft) { else if (c === quoteDblLeft) {
parseString(quoteDblRight); chars.push(parseString(quoteDblRight));
} }
else if (c === ',' && [']', '}'].indexOf(nextNonWhiteSpace()) !== -1) { else if (c === ',' && [']', '}'].indexOf(nextNonWhiteSpace()) !== -1) {
// skip trailing commas // skip trailing commas
@ -209,11 +275,16 @@ exports.repair = function (jsString) {
} }
else if (/[a-zA-Z_$]/.test(c) && ['{', ','].indexOf(lastNonWhitespace()) !== -1) { else if (/[a-zA-Z_$]/.test(c) && ['{', ','].indexOf(lastNonWhitespace()) !== -1) {
// an unquoted object key (like a in '{a:2}') // an unquoted object key (like a in '{a:2}')
parseKey(); chars.push(parseKey());
} }
else { else {
chars.push(c); if (/[a-zA-Z_$]/.test(c)) {
i++; chars.push(parseMongoDataType());
}
else {
chars.push(c);
i++;
}
} }
} }

View File

@ -76,7 +76,6 @@ describe('util', function () {
assert.strictEqual(util.repair('\n/* foo\nbar */\ncallback_123 ({});\n\n'), '{}'); assert.strictEqual(util.repair('\n/* foo\nbar */\ncallback_123 ({});\n\n'), '{}');
// non-matching // non-matching
assert.strictEqual(util.repair('callback abc({});'), 'callback abc({});');
assert.strictEqual(util.repair('callback {}'), 'callback {}'); assert.strictEqual(util.repair('callback {}'), 'callback {}');
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,}"'); 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 () { describe('jsonPath', function () {