Skip to content

Commit f9a6283

Browse files
Add option to also strip trailing commas (#59)
Co-authored-by: Andrew Bradley <[email protected]>
1 parent ad70a18 commit f9a6283

File tree

6 files changed

+70
-8
lines changed

6 files changed

+70
-8
lines changed

index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ export interface Options {
55
@default true
66
*/
77
readonly whitespace?: boolean;
8+
9+
/**
10+
Strip trailing commas in addition to comments.
11+
12+
@default true
13+
*/
14+
readonly trailingCommas?: boolean;
815
}
916

1017
/**

index.js

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const isEscaped = (jsonString, quotePosition) => {
1616
return Boolean(backslashCount % 2);
1717
};
1818

19-
export default function stripJsonComments(jsonString, {whitespace = true} = {}) {
19+
export default function stripJsonComments(jsonString, {whitespace = true, trailingCommas = false} = {}) {
2020
if (typeof jsonString !== 'string') {
2121
throw new TypeError(`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``);
2222
}
@@ -26,13 +26,16 @@ export default function stripJsonComments(jsonString, {whitespace = true} = {})
2626
let isInsideString = false;
2727
let isInsideComment = false;
2828
let offset = 0;
29+
let buffer = '';
2930
let result = '';
31+
let commaIndex = -1;
3032

3133
for (let index = 0; index < jsonString.length; index++) {
3234
const currentCharacter = jsonString[index];
3335
const nextCharacter = jsonString[index + 1];
3436

3537
if (!isInsideComment && currentCharacter === '"') {
38+
// Enter or exit string
3639
const escaped = isEscaped(jsonString, index);
3740
if (!escaped) {
3841
isInsideString = !isInsideString;
@@ -44,34 +47,61 @@ export default function stripJsonComments(jsonString, {whitespace = true} = {})
4447
}
4548

4649
if (!isInsideComment && currentCharacter + nextCharacter === '//') {
47-
result += jsonString.slice(offset, index);
50+
// Enter single-line comment
51+
buffer += jsonString.slice(offset, index);
4852
offset = index;
4953
isInsideComment = singleComment;
5054
index++;
5155
} else if (isInsideComment === singleComment && currentCharacter + nextCharacter === '\r\n') {
56+
// Exit single-line comment via \r\n
5257
index++;
5358
isInsideComment = false;
54-
result += strip(jsonString, offset, index);
59+
buffer += strip(jsonString, offset, index);
5560
offset = index;
5661
continue;
5762
} else if (isInsideComment === singleComment && currentCharacter === '\n') {
63+
// Exit single-line comment via \n
5864
isInsideComment = false;
59-
result += strip(jsonString, offset, index);
65+
buffer += strip(jsonString, offset, index);
6066
offset = index;
6167
} else if (!isInsideComment && currentCharacter + nextCharacter === '/*') {
62-
result += jsonString.slice(offset, index);
68+
// Enter multiline comment
69+
buffer += jsonString.slice(offset, index);
6370
offset = index;
6471
isInsideComment = multiComment;
6572
index++;
6673
continue;
6774
} else if (isInsideComment === multiComment && currentCharacter + nextCharacter === '*/') {
75+
// Exit multiline comment
6876
index++;
6977
isInsideComment = false;
70-
result += strip(jsonString, offset, index + 1);
78+
buffer += strip(jsonString, offset, index + 1);
7179
offset = index + 1;
7280
continue;
81+
} else if (trailingCommas && !isInsideComment) {
82+
if (commaIndex !== -1) {
83+
if (currentCharacter === '}' || currentCharacter === ']') {
84+
// Strip trailing comma
85+
buffer += jsonString.slice(offset, index);
86+
result += strip(buffer, 0, 1) + buffer.slice(1);
87+
buffer = '';
88+
offset = index;
89+
commaIndex = -1;
90+
} else if (currentCharacter !== ' ' && currentCharacter !== '\t' && currentCharacter !== '\r' && currentCharacter !== '\n') {
91+
// Hit non-whitespace following a comma; comma is not trailing
92+
buffer += jsonString.slice(offset, index);
93+
offset = index;
94+
commaIndex = -1;
95+
}
96+
} else if (currentCharacter === ',') {
97+
// Flush buffer prior to this point, and save new comma index
98+
result += buffer + jsonString.slice(offset, index);
99+
buffer = '';
100+
offset = index;
101+
commaIndex = index;
102+
}
73103
}
74104
}
75105

76-
return result + (isInsideComment ? strip(jsonString.slice(offset)) : jsonString.slice(offset));
106+
return result + buffer + (isInsideComment ? strip(jsonString.slice(offset)) : jsonString.slice(offset));
77107
}

index.test-d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ const json = '{/*rainbows*/"unicorn":"cake"}';
55

66
expectType<string>(stripJsonComments(json));
77
expectType<string>(stripJsonComments(json, {whitespace: true}));
8+
expectType<string>(stripJsonComments(json, {trailingCommas: true}));
9+
expectType<string>(stripJsonComments(json, {whitespace: true, trailingCommas: true}));

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,10 @@
4545
"matcha": "^0.7.0",
4646
"tsd": "^0.17.0",
4747
"xo": "^0.44.0"
48+
},
49+
"xo": {
50+
"rules": {
51+
"complexity": "off"
52+
}
4853
}
4954
}

readme.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,20 @@ Accepts a string with JSON and returns a string without comments.
4949

5050
Type: `object`
5151

52+
53+
##### trailingCommas
54+
55+
Type: `boolean`\
56+
Default: `false`
57+
58+
Strip trailing commas in addition to comments.
59+
5260
##### whitespace
5361

5462
Type: `boolean`\
5563
Default: `true`
5664

57-
Replace comments with whitespace instead of stripping them entirely.
65+
Replace comments and trailing commas with whitespace instead of stripping them entirely.
5866

5967
## Benchmark
6068

test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ test('handles weird escaping', t => {
6464
t.is(stripJsonComments(String.raw`{"x":"x \"sed -e \\\"s/^.\\\\{46\\\\}T//\\\" -e \\\"s/#033/\\\\x1b/g\\\"\""}`), String.raw`{"x":"x \"sed -e \\\"s/^.\\\\{46\\\\}T//\\\" -e \\\"s/#033/\\\\x1b/g\\\"\""}`);
6565
});
6666

67+
test('strips trailing commas', t => {
68+
t.is(stripJsonComments('{"x":true,}', {trailingCommas: true}), '{"x":true }');
69+
t.is(stripJsonComments('{"x":true,}', {trailingCommas: true, whitespace: false}), '{"x":true}');
70+
t.is(stripJsonComments('{"x":true,\n }', {trailingCommas: true}), '{"x":true \n }');
71+
t.is(stripJsonComments('[true, false,]', {trailingCommas: true}), '[true, false ]');
72+
t.is(stripJsonComments('[true, false,]', {trailingCommas: true, whitespace: false}), '[true, false]');
73+
t.is(stripJsonComments('{\n "array": [\n true,\n false,\n ],\n}', {trailingCommas: true, whitespace: false}), '{\n "array": [\n true,\n false\n ]\n}');
74+
t.is(stripJsonComments('{\n "array": [\n true,\n false /* comment */ ,\n /*comment*/ ],\n}', {trailingCommas: true, whitespace: false}), '{\n "array": [\n true,\n false \n ]\n}');
75+
});
76+
6777
test.failing('handles malformed block comments', t => {
6878
t.is(stripJsonComments('[] */'), '[] */');
6979
t.is(stripJsonComments('[] /*'), '[] /*'); // Fails

0 commit comments

Comments
 (0)