Skip to content

Commit c4c6d38

Browse files
committed
feat(JSON Viewer): add unescape JSON string
Fix JSON.parseBigNum edge cases Inspired by CorentinTh#1673 from @iyuq
1 parent 95590f1 commit c4c6d38

6 files changed

Lines changed: 150 additions & 10 deletions

File tree

locales/en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5798,6 +5798,7 @@ tools:
57985798
label-json-schema: 'JSON schema:'
57995799
placeholder-paste-your-json-schema-here: Paste your JSON Schema here...
58005800
title-schema-validation-errors: Schema Validation Errors
5801+
label-unescape-json-string: 'Unescape JSON string:'
58015802
text:
58025803
provided-json-is-not-valid: Provided JSON is not valid.
58035804
try-again-with-repairjsonlabel: Try again with "{0}"

src/tools/json-viewer/json-viewer.vue

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script setup lang="ts">
22
import { useI18n } from 'vue-i18n';
3-
import { jsonrepair } from 'jsonrepair';
43
import {
54
get,
65
} from '@vueuse/core';
@@ -21,20 +20,21 @@ const schemaData = useITStorage('json-prettify:schema-data', '');
2120
const indentSize = useITStorage('json-prettify:indent-size', 3);
2221
const sortKeys = useITStorage('json-prettify:sort-keys', true);
2322
const unescapeUnicode = useITStorage('json-prettify:unescape-unicode', false);
23+
const unescapeJsonString = useITStorage('json-prettify:unescape-json', false);
2424
const repairJson = useITStorage('json-prettify:repair-json', true);
25-
const cleanJson = computed(() => withDefaultOnError(() => formatJson({ rawJson, indentSize, sortKeys, unescapeUnicode, repairJson }), ''));
25+
const cleanJson = computed(() => withDefaultOnError(() => formatJson({ rawJson, indentSize, sortKeys, unescapeUnicode, unescapeJsonString, repairJson }), ''));
2626
2727
const rawJsonValidation = useValidation({
2828
source: rawJson,
2929
rules: [
3030
{
31-
validator: v => v === '' || (get(repairJson) ? jsonrepair(v) : JSON.parseBigNum(v)),
31+
validator: v => v === '' || formatJson({ rawJson, indentSize: 0, sortKeys: false, unescapeUnicode, unescapeJsonString, repairJson }),
3232
get message() {
3333
return t('tools.json-viewer.text.provided-json-is-not-valid') + (!get(repairJson) ? t('tools.json-viewer.text.try-again-with-repairjsonlabel', [repairJsonLabel]) : '');
3434
},
3535
},
3636
],
37-
watch: [repairJson],
37+
watch: [repairJson, unescapeUnicode, unescapeJsonString],
3838
});
3939
4040
const schemaUrl = useQueryParamOrStorage<string>({ name: 'schema', storageName: 'json-prettify:schema', defaultValue: '' });
@@ -49,6 +49,9 @@ const { schemas, errors: validationErrors } = useJsonSchemaValidation({ json: ra
4949
<n-form-item :label="t('tools.json-viewer.texts.label-unescape-unicode')" label-placement="left" label-width="150">
5050
<n-switch v-model:value="unescapeUnicode" />
5151
</n-form-item>
52+
<n-form-item :label="t('tools.json-viewer.texts.label-unescape-json-string')" label-placement="left" label-width="180">
53+
<n-switch v-model:value="unescapeJsonString" />
54+
</n-form-item>
5255
<n-form-item :label="t('tools.json-viewer.texts.label-indent-size')" label-placement="left" label-width="100" :show-feedback="false">
5356
<n-input-number-i18n v-model:value="indentSize" min="0" max="10" style="width: 100px" />
5457
</n-form-item>

src/tools/json-viewer/json.models.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,104 @@ describe('json models', () => {
2727
expect(() => formatJson({ rawJson, sortKeys: false, repairJson: false })).to.throw();
2828
});
2929
});
30+
31+
const testJson = '{"b": 2, "a": 1}';
32+
const expectedSorted = '{\n "a": 1,\n "b": 2\n}';
33+
const expectedUnsorted = '{\n "b": 2,\n "a": 1\n}';
34+
35+
it('formats JSON with default options (sorted keys, 3 spaces)', () => {
36+
const result = formatJson({ rawJson: testJson });
37+
expect(result).toBe(expectedSorted);
38+
});
39+
40+
it('formats JSON without sorting keys when sortKeys is false', () => {
41+
const result = formatJson({ rawJson: testJson, sortKeys: false });
42+
expect(result).toBe(expectedUnsorted);
43+
});
44+
45+
it('formats JSON with custom indent size', () => {
46+
const result = formatJson({ rawJson: testJson, indentSize: 2 });
47+
const expected = '{\n "a": 1,\n "b": 2\n}';
48+
expect(result).toBe(expected);
49+
});
50+
51+
it('works with reactive refs', () => {
52+
const rawJsonRef = ref(testJson);
53+
const sortKeysRef = ref(true);
54+
const indentSizeRef = ref(3);
55+
56+
const result = formatJson({
57+
rawJson: rawJsonRef,
58+
sortKeys: sortKeysRef,
59+
indentSize: indentSizeRef,
60+
});
61+
expect(result).toBe(expectedSorted);
62+
});
63+
64+
describe('autoUnescape functionality', () => {
65+
it('unescapes escaped JSON strings when autoUnescape is true', () => {
66+
const escapedJson = '"{\\\"id\\\":\\\"123\\\",\\\"name\\\":\\\"test\\\"}"';
67+
const result = formatJson({ rawJson: escapedJson, unescapeJsonString: true, indentSize: 2 });
68+
const expected = '{\n "id": "123",\n "name": "test"\n}';
69+
expect(result).toBe(expected);
70+
});
71+
72+
it('handles escaped JSON without outer quotes', () => {
73+
const escapedJson = '{\\\"id\\\":\\\"123\\\",\\\"name\\\":\\\"test\\\"}';
74+
const result = formatJson({ rawJson: escapedJson, unescapeJsonString: true, indentSize: 2 });
75+
const expected = '{\n "id": "123",\n "name": "test"\n}';
76+
expect(result).toBe(expected);
77+
});
78+
79+
it('unescapes various escape sequences', () => {
80+
const escapedJson = '{\\\"text\\\":\\\"Hello\\\\\\\\World\\\",\\\"path\\\":\\\"/api\\\\/test\\\"}';
81+
const result = formatJson({ rawJson: escapedJson, unescapeJsonString: true, indentSize: 2 });
82+
const expected = '{\n "path": "/api/test",\n "text": "Hello\\\\World"\n}';
83+
expect(result).toBe(expected);
84+
});
85+
86+
it('handles single-quoted outer strings', () => {
87+
const escapedJson = '\'{\\\"id\\\":\\\"123\\\"}\'';
88+
const result = formatJson({ rawJson: escapedJson, unescapeJsonString: true, indentSize: 2 });
89+
const expected = '{\n "id": "123"\n}';
90+
expect(result).toBe(expected);
91+
});
92+
93+
it('processes regular JSON normally when autoUnescape is false', () => {
94+
const normalJson = '{"id":"123","name":"test"}';
95+
const result = formatJson({ rawJson: normalJson, unescapeJsonString: false, indentSize: 2 });
96+
const expected = '{\n "id": "123",\n "name": "test"\n}';
97+
expect(result).toBe(expected);
98+
});
99+
100+
it('handles malformed escaped JSON gracefully', () => {
101+
const malformedJson = '"{\\\"incomplete';
102+
// Should fall back to original string and fail parsing
103+
expect(() => formatJson({ rawJson: malformedJson, unescapeJsonString: true })).toThrow();
104+
});
105+
106+
it('works with complex nested objects', () => {
107+
const complexEscaped = '"{\\\"users\\\":[{\\\"id\\\":\\\"1\\\",\\\"data\\\":{\\\"active\\\":true}}],\\\"meta\\\":{\\\"total\\\":1}}"';
108+
const result = formatJson({ rawJson: complexEscaped, unescapeJsonString: true, indentSize: 2 });
109+
const expected = '{\n "meta": {\n "total": 1\n },\n "users": [\n {\n "data": {\n "active": true\n },\n "id": "1"\n }\n ]\n}';
110+
expect(result).toBe(expected);
111+
});
112+
113+
it('works with reactive autoUnescape ref', () => {
114+
const escapedJson = '"{\\\"test\\\":\\\"value\\\"}"';
115+
const autoUnescapeRef = ref(true);
116+
const result = formatJson({ rawJson: escapedJson, unescapeJsonString: autoUnescapeRef, indentSize: 2 });
117+
const expected = '{\n "test": "value"\n}';
118+
expect(result).toBe(expected);
119+
});
120+
});
121+
122+
it('handles empty string input', () => {
123+
expect(() => formatJson({ rawJson: '' })).toThrow();
124+
});
125+
126+
it('handles invalid JSON input', () => {
127+
expect(() => formatJson({ rawJson: 'invalid json' })).toThrow();
128+
});
30129
});
31130
});

src/tools/json-viewer/json.models.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,56 @@ function unescapeUnicodeJSON(str: string) {
2727
);
2828
}
2929

30+
function unescapeJson(jsonString: string): string {
31+
try {
32+
// First, try to handle double-escaped scenarios
33+
let result = jsonString.trim();
34+
35+
// If the string starts and ends with quotes, and contains escaped quotes inside,
36+
// it might be a JSON string that needs to be unescaped
37+
if ((result.startsWith('"') && result.endsWith('"'))
38+
|| (result.startsWith('\'') && result.endsWith('\''))) {
39+
// Remove outer quotes first
40+
result = result.slice(1, -1);
41+
}
42+
43+
// Handle common escape sequences
44+
result = result
45+
.replace(/\\"/g, '"') // Unescape quotes
46+
.replace(/\\\\/g, '\\') // Unescape backslashes (do this after quotes!)
47+
.replace(/\\n/g, '\n') // Unescape newlines
48+
.replace(/\\r/g, '\r') // Unescape carriage returns
49+
.replace(/\\t/g, '\t') // Unescape tabs
50+
.replace(/\\f/g, '\f') // Unescape form feeds
51+
.replace(/\\b/g, '\b') // Unescape backspaces
52+
.replace(/\\\//g, '/'); // Unescape forward slashes
53+
54+
return result;
55+
}
56+
catch {
57+
return jsonString;
58+
}
59+
}
60+
3061
function formatJson({
3162
rawJson,
3263
sortKeys = true,
3364
indentSize = 3,
3465
unescapeUnicode = false,
66+
unescapeJsonString = false,
3567
repairJson = false,
3668
}: {
3769
rawJson: MaybeRef<string>
3870
sortKeys?: MaybeRef<boolean>
3971
indentSize?: MaybeRef<number>
4072
unescapeUnicode?: MaybeRef<boolean>
73+
unescapeJsonString?: MaybeRef<boolean>
4174
repairJson?: MaybeRef<boolean>
4275
}) {
43-
const unwrappedJson = get(rawJson);
76+
let unwrappedJson = get(rawJson)?.trim();
77+
if (get(unescapeJsonString)) {
78+
unwrappedJson = unescapeJson(unwrappedJson);
79+
}
4480
const jsonString = get(repairJson) ? jsonrepair(unwrappedJson) : unwrappedJson;
4581
const parsedObject = JSON.parseBigNum(get(unescapeUnicode) ? unescapeUnicodeJSON(jsonString) : jsonString);
4682

src/utils/json5-bigint.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ JSON.parseBigInt = function (jsonStr: string, options?: { minDigits?: number }):
3030
return JSON5.parse(safeStr, (_key, value) => {
3131
if (typeof value === 'string') {
3232
if (bigintRegex.test(value)) {
33+
const bigintValue = value.slice(1, -1);
3334
try {
3435
if (typeof BigInt !== 'undefined') {
3536
// take only part xxx between ¤xxx¤
36-
return BigInt(value.substring(1, value.length - 1));
37+
return BigInt(bigintValue);
3738
}
3839
}
3940
catch {
40-
return value;
41+
return bigintValue;
4142
}
4243
}
4344
}

src/utils/json5-bignum.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ JSON.parseBigNum = function (jsonStr: string): unknown {
2929
return JSON5.parse(safeStr, (_key, value) => {
3030
if (typeof value === 'string') {
3131
if (bignumRegex.test(value)) {
32+
// take only part xxx between ¤xxx¤
33+
const number_string = value.slice(1, -1);
3234
try {
33-
// take only part xxx between ¤xxx¤
34-
const number_string = value.substring(1, value.length - 1);
3535
const full_precision = new Decimal(number_string);
3636
const json_number = Number(number_string);
3737
if (json_number.toString() === full_precision.toString()) {
@@ -42,7 +42,7 @@ JSON.parseBigNum = function (jsonStr: string): unknown {
4242
}
4343
}
4444
catch {
45-
return value;
45+
return number_string;
4646
}
4747
}
4848
}

0 commit comments

Comments
 (0)