Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/ jsonc #11

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Location object has properties (zero-based numbers):

Options:
- _bigint_: parse large integers as [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt).
- _jsonc_: parse as [jsonc](https://code.visualstudio.com/docs/languages/json#_json-with-comments).

Whitespace:
- the only character that increases line number in mappings is line feed ('\n'), so if your JSON string has '\r\n' sequence, it will still be counted as one line,
Expand Down
99 changes: 91 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ exports.parse = function (source, _, options) {
var line = 0;
var column = 0;
var pos = 0;
var jsonc = !!(options && options.jsonc);
var bigint = options && options.bigint && typeof BigInt != 'undefined';
return {
data: _parse('', true),
Expand Down Expand Up @@ -58,12 +59,60 @@ exports.parse = function (source, _, options) {
case '\t': column += 4; break;
case '\r': column = 0; break;
case '\n': column = 0; line++; break;
case '/':
if (!jsonc) break loop;
column++; pos++;
parseComment();
continue loop;
default: break loop;
}
pos++;
}
}

function parseComment() {
var nextChar = getChar();
var singleLineComment = nextChar === '/';
var multiLineComment = nextChar === '*';

if (!singleLineComment && !multiLineComment)
wasUnexpectedToken();

var commentStr = '/' + nextChar;

readComment: {
while (true) {
nextChar = getChar();

switch (nextChar) {
case '\t': column += 3; break;
case '\r': column = 0; break;
case '\n':
column = 0;
line++;

if (singleLineComment) break readComment;
break;
case '*':
if (multiLineComment) {
commentStr += nextChar;
nextChar = getChar();
commentStr += nextChar;

if (nextChar === '/')
break readComment;
}
break;
default: break;
}

commentStr += nextChar;
}
}

return commentStr;
}

function parseString() {
var str = '';
var char;
Expand Down Expand Up @@ -117,17 +166,34 @@ exports.parse = function (source, _, options) {
whitespace();
var arr = [];
var i = 0;
if (getChar() == ']') return arr;
var char = getChar();
if (char == ']') return arr;
backChar();
whitespace();

while (true) {
readEarlyCommas: while (jsonc) {
switch (getChar()) {
case ']': return arr;
case ',': whitespace(); continue readEarlyCommas;
default: backChar(); break readEarlyCommas;
}
}

readArray: while (true) {
var itemPtr = ptr + '/' + i;
arr.push(_parse(itemPtr));
whitespace();
var char = getChar();
if (char == ']') break;
char = getChar();
if (char == ']') break readArray;
if (char != ',') wasUnexpectedToken();
whitespace();
readTrailingCommas: while (jsonc) {
switch (getChar()) {
case ']': break readArray;
case ',': whitespace(); continue readTrailingCommas;
default: backChar(); break readTrailingCommas;
}
}
i++;
}
return arr;
Expand All @@ -136,10 +202,20 @@ exports.parse = function (source, _, options) {
function parseObject(ptr) {
whitespace();
var obj = {};
if (getChar() == '}') return obj;
var char = getChar();
if (char == '}') return obj;
backChar();
whitespace();

while (true) {
readEarlyCommas: while (jsonc) {
switch (getChar()) {
case '}': return obj;
case ',': whitespace(); continue readEarlyCommas;
default: backChar(); break readEarlyCommas;
}
}

readObject: while (true) {
var loc = getLoc();
if (getChar() != '"') wasUnexpectedToken();
var key = parseString();
Expand All @@ -151,10 +227,17 @@ exports.parse = function (source, _, options) {
whitespace();
obj[key] = _parse(propPtr);
whitespace();
var char = getChar();
if (char == '}') break;
char = getChar();
if (char == '}') break readObject;
if (char != ',') wasUnexpectedToken();
whitespace();
readTrailingCommas: while (jsonc) {
switch (getChar()) {
case '}': break readObject;
case ',': whitespace(); continue readTrailingCommas;
default: backChar(); break readTrailingCommas;
}
}
}
return obj;
}
Expand Down
173 changes: 173 additions & 0 deletions spec/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,179 @@ describe('parse', function() {
});
});

describe("jsonc", () => {
const jsonc =
`{
// Hello World
"prop1": "test",
// Test
"prop2": "test2"
/* Hello World */,
"prop3": [ 123, /* Hello World */ "456",,, /* Hello World */ 789 ] /* Hello World */,
"prop4" /* Hello World ,,, * */ : "test3",,,,
"prop5": [ , , , , \r \r \r
// Hello World!
/// Header \r \r \n \n \r \n
// /* */
0,
1, /* Hello World!
,,,,,,,,,,
*/
2, 3, 4,
],
// /**********
/**
*
* Multi line Big Comment
*/
/// Header
/// Subtext
,,,,,,,,,,,,,,
}`;
const expectedJsonc = { prop1: "test", prop2: "test2", prop3: [123, "456", 789], prop4: "test3", prop5: [0, 1, 2, 3, 4] };
const expectedPointers = {
'': {
value: { line: 0, column: 0, pos: 0 },
valueEnd: { line: 27, column: 1, pos: 503 }
},
'/prop1': {
key: { line: 2, column: 2, pos: 21 },
keyEnd: { line: 2, column: 9, pos: 28 },
value: { line: 2, column: 11, pos: 30 },
valueEnd: { line: 2, column: 17, pos: 36 }
},
'/prop2': {
key: { line: 4, column: 2, pos: 50 },
keyEnd: { line: 4, column: 9, pos: 57 },
value: { line: 4, column: 11, pos: 59 },
valueEnd: { line: 4, column: 18, pos: 66 }
},
'/prop3': {
key: { line: 6, column: 2, pos: 90 },
keyEnd: { line: 6, column: 9, pos: 97 },
value: { line: 6, column: 11, pos: 99 },
valueEnd: { line: 6, column: 68, pos: 156 }
},
'/prop3/0': {
value: { line: 6, column: 13, pos: 101 },
valueEnd: { line: 6, column: 16, pos: 104 }
},
'/prop3/1': {
value: { line: 6, column: 36, pos: 124 },
valueEnd: { line: 6, column: 41, pos: 129 }
},
'/prop3/2': {
value: { line: 6, column: 63, pos: 151 },
valueEnd: { line: 6, column: 66, pos: 154 }
},
'/prop4': {
key: { line: 7, column: 2, pos: 178 },
keyEnd: { line: 7, column: 9, pos: 185 },
value: { line: 7, column: 36, pos: 212 },
valueEnd: { line: 7, column: 43, pos: 219 }
},
'/prop5': {
key: { line: 8, column: 2, pos: 226 },
keyEnd: { line: 8, column: 9, pos: 233 },
value: { line: 8, column: 11, pos: 235 },
valueEnd: { line: 20, column: 3, pos: 394 }
},
'/prop5/0': {
value: { line: 15, column: 4, pos: 332 },
valueEnd: { line: 15, column: 5, pos: 333 }
},
'/prop5/1': {
value: { line: 16, column: 4, pos: 339 },
valueEnd: { line: 16, column: 5, pos: 340 }
},
'/prop5/2': {
value: { line: 19, column: 2, pos: 382 },
valueEnd: { line: 19, column: 3, pos: 383 }
},
'/prop5/3': {
value: { line: 19, column: 5, pos: 385 },
valueEnd: { line: 19, column: 6, pos: 386 }
},
'/prop5/4': {
value: { line: 19, column: 8, pos: 388 },
valueEnd: { line: 19, column: 9, pos: 389 }
}
};

const simpleJsonc =
`/* Simple jsonc*/{ "prop1": "test1" }`;
const expectedSimpleJsonc = { prop1: "test1" };
const pointersSimpleJsonc = {
"": {
"value": {
"column": 17,
"line": 0,
"pos": 17,
},
"valueEnd": {
"column": 37,
"line": 0,
"pos": 37,
},
},
"/prop1": {
"key": {
"column": 19,
"line": 0,
"pos": 19
},
"keyEnd": {
"column": 26,
"line": 0,
"pos": 26
},
"value": {
"column": 28,
"line": 0,
"pos": 28
},
"valueEnd": {
"column": 35,
"line": 0,
"pos": 35
}
}
};

const badJsonc = `{ "prop1": "test" / }`;
const badJsoncArr = `[ "test" / ]`;
const tooManyExits = `{ "prop1": "test" /* */ */ }`;
const tooManyExitsArr = `{ "test" /* */ */ }`;
const unopenedComment = `{ Hello World! }`;
const unopenedCommentArr = `[ Hello World! ]`;

const jsonWithTrailingComma = `{ "prop1": "test1", "prop2": [1, 2, 3,] }`;
const jsonObjWithTrailingComma = `{ "prop1": "test1", "prop2": [1, 2, 3] , }`;

it("Should parse json with comments and trailing commas as whitespace and execute as normal if jsonc true", () => {
assert.deepStrictEqual(jsonMap.parse(jsonc, null, { jsonc: true }).data, expectedJsonc);
assert.deepStrictEqual(jsonMap.parse(jsonc, null, { jsonc: true }).pointers, expectedPointers);
assert.deepStrictEqual(jsonMap.parse(simpleJsonc, null, { jsonc: true }).data, expectedSimpleJsonc);
assert.deepStrictEqual(jsonMap.parse(simpleJsonc, null, { jsonc: true }).pointers, pointersSimpleJsonc);
});

it("Should throw errors on a / not followed by a * or another /", () => {
assert.throws(() => jsonMap.parse(badJsonc, null, { jsonc: true }), /Unexpected token[ ]{3}in JSON at position 19/);
assert.throws(() => jsonMap.parse(badJsoncArr, null, { jsonc: true }), /Unexpected token[ ]{3}in JSON at position 10/);
assert.throws(() => jsonMap.parse(tooManyExits, null, { jsonc: true }), /Unexpected token \* in JSON at position 24/);
assert.throws(() => jsonMap.parse(tooManyExitsArr, null, { jsonc: true }), /Unexpected token \* in JSON at position 15/);
assert.throws(() => jsonMap.parse(unopenedComment, null, { jsonc: true }), /Unexpected token H in JSON at position 2/);
assert.throws(() => jsonMap.parse(unopenedCommentArr, null, { jsonc: true }), /Unexpected token H in JSON at position 2/);
});

it("Should throw errors for invalid json if jsonc option false or not given and json contains comments or trailing commas", () => {
assert.throws(() => jsonMap.parse(jsonc, null, { jsonc: false }), /Unexpected token \/ in JSON at position 4/);
assert.throws(() => jsonMap.parse(jsonc, null, {}), /Unexpected token \/ in JSON at position 4/);
assert.throws(() => jsonMap.parse(jsonc), /Unexpected token \/ in JSON at position 4/);
assert.throws(() => jsonMap.parse(jsonWithTrailingComma), /Unexpected token ] in JSON at position 38/);
assert.throws(() => jsonMap.parse(jsonObjWithTrailingComma), /Unexpected token } in JSON at position 45/);
});
});

function testParse(json, expectedData, skipReverseCheck, whitespace) {
var result = jsonMap.parse(json);
Expand Down