bestjsonformatter 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -25
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +429 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/lib/json-check.d.ts +32 -0
- package/dist/lib/json-check.js +132 -0
- package/dist/lib/json-schema.d.ts +19 -0
- package/dist/lib/json-schema.js +195 -0
- package/dist/lib/json-tools.d.ts +41 -0
- package/dist/lib/json-tools.js +543 -0
- package/dist/lib/lossless-json.d.ts +83 -0
- package/dist/lib/lossless-json.js +508 -0
- package/package.json +48 -12
- package/dist/cli.mjs +0 -248
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
export class JsonSyntaxError extends Error {
|
|
2
|
+
offset;
|
|
3
|
+
line;
|
|
4
|
+
column;
|
|
5
|
+
constructor(message, input, offset) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "JsonSyntaxError";
|
|
8
|
+
this.offset = Math.max(0, Math.min(offset, input.length));
|
|
9
|
+
const before = input.slice(0, this.offset);
|
|
10
|
+
this.line = 1;
|
|
11
|
+
this.column = 1;
|
|
12
|
+
for (let index = 0; index < before.length; index += 1) {
|
|
13
|
+
if (before.charCodeAt(index) === 10) {
|
|
14
|
+
this.line += 1;
|
|
15
|
+
this.column = 1;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
this.column += 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const MAX_DEPTH = 4_096;
|
|
24
|
+
class LosslessParser {
|
|
25
|
+
input;
|
|
26
|
+
buildTree;
|
|
27
|
+
maxTreeValues;
|
|
28
|
+
indent;
|
|
29
|
+
output;
|
|
30
|
+
position = 0;
|
|
31
|
+
depth = 0;
|
|
32
|
+
duplicateKeys = 0;
|
|
33
|
+
unsafeNumbers = 0;
|
|
34
|
+
values = 0;
|
|
35
|
+
treeTruncated = false;
|
|
36
|
+
rootType = "null";
|
|
37
|
+
constructor(input, options) {
|
|
38
|
+
this.input = input;
|
|
39
|
+
this.buildTree = options.buildTree === true;
|
|
40
|
+
this.maxTreeValues = options.maxTreeValues ?? Number.POSITIVE_INFINITY;
|
|
41
|
+
this.indent = options.indent;
|
|
42
|
+
this.output = options.indent === undefined ? null : [];
|
|
43
|
+
}
|
|
44
|
+
parse() {
|
|
45
|
+
this.skipWhitespace();
|
|
46
|
+
if (this.position === this.input.length)
|
|
47
|
+
this.fail("Paste or upload JSON to begin.");
|
|
48
|
+
const first = this.input[this.position];
|
|
49
|
+
this.rootType =
|
|
50
|
+
first === "{"
|
|
51
|
+
? "object"
|
|
52
|
+
: first === "["
|
|
53
|
+
? "array"
|
|
54
|
+
: first === '"'
|
|
55
|
+
? "string"
|
|
56
|
+
: first === "t" || first === "f"
|
|
57
|
+
? "boolean"
|
|
58
|
+
: first === "n"
|
|
59
|
+
? "null"
|
|
60
|
+
: "number";
|
|
61
|
+
const root = this.parseValue();
|
|
62
|
+
this.skipWhitespace();
|
|
63
|
+
if (this.position !== this.input.length)
|
|
64
|
+
this.fail("Unexpected content after the JSON value.");
|
|
65
|
+
if (root)
|
|
66
|
+
this.rootType = root.kind;
|
|
67
|
+
return {
|
|
68
|
+
root,
|
|
69
|
+
formatted: this.output?.join(""),
|
|
70
|
+
metadata: {
|
|
71
|
+
type: this.rootType,
|
|
72
|
+
duplicateKeys: this.duplicateKeys,
|
|
73
|
+
unsafeNumbers: this.unsafeNumbers,
|
|
74
|
+
values: this.values,
|
|
75
|
+
treeTruncated: this.treeTruncated,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
fail(message, offset = this.position) {
|
|
80
|
+
throw new JsonSyntaxError(message, this.input, offset);
|
|
81
|
+
}
|
|
82
|
+
write(value) {
|
|
83
|
+
this.output?.push(value);
|
|
84
|
+
}
|
|
85
|
+
writeLine(depth = this.depth) {
|
|
86
|
+
if (this.indent)
|
|
87
|
+
this.write(`\n${this.indent.repeat(depth)}`);
|
|
88
|
+
}
|
|
89
|
+
skipWhitespace() {
|
|
90
|
+
while (this.position < this.input.length) {
|
|
91
|
+
const code = this.input.charCodeAt(this.position);
|
|
92
|
+
if (code !== 32 && code !== 9 && code !== 10 && code !== 13)
|
|
93
|
+
break;
|
|
94
|
+
this.position += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
parseValue() {
|
|
98
|
+
if (this.depth > MAX_DEPTH)
|
|
99
|
+
this.fail(`JSON nesting exceeds the supported depth of ${MAX_DEPTH}.`);
|
|
100
|
+
this.values += 1;
|
|
101
|
+
if (this.buildTree && this.values > this.maxTreeValues) {
|
|
102
|
+
this.buildTree = false;
|
|
103
|
+
this.treeTruncated = true;
|
|
104
|
+
}
|
|
105
|
+
const character = this.input[this.position];
|
|
106
|
+
let node;
|
|
107
|
+
if (character === "{")
|
|
108
|
+
node = this.parseObject();
|
|
109
|
+
else if (character === "[")
|
|
110
|
+
node = this.parseArray();
|
|
111
|
+
else if (character === '"')
|
|
112
|
+
node = this.parseString();
|
|
113
|
+
else if (character === "t")
|
|
114
|
+
node = this.parseLiteral("true", true);
|
|
115
|
+
else if (character === "f")
|
|
116
|
+
node = this.parseLiteral("false", false);
|
|
117
|
+
else if (character === "n")
|
|
118
|
+
node = this.parseNull();
|
|
119
|
+
else if (character === "-" || (character >= "0" && character <= "9"))
|
|
120
|
+
node = this.parseNumber();
|
|
121
|
+
else if (character === undefined)
|
|
122
|
+
this.fail("Expected a JSON value.");
|
|
123
|
+
else
|
|
124
|
+
this.fail(`Unexpected ${JSON.stringify(character)}; expected a JSON value.`);
|
|
125
|
+
return node;
|
|
126
|
+
}
|
|
127
|
+
parseObject() {
|
|
128
|
+
const start = this.position;
|
|
129
|
+
const entries = [];
|
|
130
|
+
const keys = new Set();
|
|
131
|
+
this.position += 1;
|
|
132
|
+
this.write("{");
|
|
133
|
+
this.skipWhitespace();
|
|
134
|
+
if (this.input[this.position] === "}") {
|
|
135
|
+
this.position += 1;
|
|
136
|
+
this.write("}");
|
|
137
|
+
return this.buildTree ? { kind: "object", start, end: this.position, entries } : undefined;
|
|
138
|
+
}
|
|
139
|
+
this.depth += 1;
|
|
140
|
+
let first = true;
|
|
141
|
+
while (this.position < this.input.length) {
|
|
142
|
+
if (!first) {
|
|
143
|
+
if (this.input[this.position] !== ",")
|
|
144
|
+
this.fail("Expected ',' or '}' after an object property.");
|
|
145
|
+
this.position += 1;
|
|
146
|
+
this.write(",");
|
|
147
|
+
this.skipWhitespace();
|
|
148
|
+
if (this.input[this.position] === "}")
|
|
149
|
+
this.fail("Trailing commas are not valid JSON.");
|
|
150
|
+
}
|
|
151
|
+
this.writeLine();
|
|
152
|
+
if (this.input[this.position] !== '"')
|
|
153
|
+
this.fail("Expected a property name enclosed in double quotes.");
|
|
154
|
+
const keyStart = this.position;
|
|
155
|
+
const keyNode = this.parseString();
|
|
156
|
+
const keyEnd = this.position;
|
|
157
|
+
const key = keyNode?.kind === "string"
|
|
158
|
+
? keyNode.value
|
|
159
|
+
: this.decodeString(this.input.slice(keyStart, keyEnd), keyStart);
|
|
160
|
+
const duplicate = keys.has(key);
|
|
161
|
+
if (duplicate)
|
|
162
|
+
this.duplicateKeys += 1;
|
|
163
|
+
else
|
|
164
|
+
keys.add(key);
|
|
165
|
+
this.skipWhitespace();
|
|
166
|
+
if (this.input[this.position] !== ":")
|
|
167
|
+
this.fail("Expected ':' after the property name.");
|
|
168
|
+
this.position += 1;
|
|
169
|
+
this.write(this.indent ? ": " : ":");
|
|
170
|
+
this.skipWhitespace();
|
|
171
|
+
const value = this.parseValue();
|
|
172
|
+
if (this.buildTree && value) {
|
|
173
|
+
entries.push({
|
|
174
|
+
key,
|
|
175
|
+
keyRaw: this.input.slice(keyStart, keyEnd),
|
|
176
|
+
keyStart,
|
|
177
|
+
keyEnd,
|
|
178
|
+
duplicate,
|
|
179
|
+
value,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
this.skipWhitespace();
|
|
183
|
+
first = false;
|
|
184
|
+
if (this.input[this.position] === "}")
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
this.depth -= 1;
|
|
188
|
+
if (this.input[this.position] !== "}")
|
|
189
|
+
this.fail("Expected '}' to close the object.");
|
|
190
|
+
if (!first)
|
|
191
|
+
this.writeLine();
|
|
192
|
+
this.position += 1;
|
|
193
|
+
this.write("}");
|
|
194
|
+
return this.buildTree ? { kind: "object", start, end: this.position, entries } : undefined;
|
|
195
|
+
}
|
|
196
|
+
parseArray() {
|
|
197
|
+
const start = this.position;
|
|
198
|
+
const items = [];
|
|
199
|
+
this.position += 1;
|
|
200
|
+
this.write("[");
|
|
201
|
+
this.skipWhitespace();
|
|
202
|
+
if (this.input[this.position] === "]") {
|
|
203
|
+
this.position += 1;
|
|
204
|
+
this.write("]");
|
|
205
|
+
return this.buildTree ? { kind: "array", start, end: this.position, items } : undefined;
|
|
206
|
+
}
|
|
207
|
+
this.depth += 1;
|
|
208
|
+
let first = true;
|
|
209
|
+
while (this.position < this.input.length) {
|
|
210
|
+
if (!first) {
|
|
211
|
+
if (this.input[this.position] !== ",")
|
|
212
|
+
this.fail("Expected ',' or ']' after an array item.");
|
|
213
|
+
this.position += 1;
|
|
214
|
+
this.write(",");
|
|
215
|
+
this.skipWhitespace();
|
|
216
|
+
if (this.input[this.position] === "]")
|
|
217
|
+
this.fail("Trailing commas are not valid JSON.");
|
|
218
|
+
}
|
|
219
|
+
this.writeLine();
|
|
220
|
+
const value = this.parseValue();
|
|
221
|
+
if (this.buildTree && value)
|
|
222
|
+
items.push(value);
|
|
223
|
+
this.skipWhitespace();
|
|
224
|
+
first = false;
|
|
225
|
+
if (this.input[this.position] === "]")
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
this.depth -= 1;
|
|
229
|
+
if (this.input[this.position] !== "]")
|
|
230
|
+
this.fail("Expected ']' to close the array.");
|
|
231
|
+
if (!first)
|
|
232
|
+
this.writeLine();
|
|
233
|
+
this.position += 1;
|
|
234
|
+
this.write("]");
|
|
235
|
+
return this.buildTree ? { kind: "array", start, end: this.position, items } : undefined;
|
|
236
|
+
}
|
|
237
|
+
parseString() {
|
|
238
|
+
const start = this.position;
|
|
239
|
+
this.position += 1;
|
|
240
|
+
while (this.position < this.input.length) {
|
|
241
|
+
const code = this.input.charCodeAt(this.position);
|
|
242
|
+
if (code === 34) {
|
|
243
|
+
this.position += 1;
|
|
244
|
+
const raw = this.input.slice(start, this.position);
|
|
245
|
+
this.write(raw);
|
|
246
|
+
return this.buildTree
|
|
247
|
+
? { kind: "string", start, end: this.position, raw, value: this.decodeString(raw, start) }
|
|
248
|
+
: undefined;
|
|
249
|
+
}
|
|
250
|
+
if (code < 32)
|
|
251
|
+
this.fail("Unescaped control character in string.");
|
|
252
|
+
if (code === 92) {
|
|
253
|
+
this.position += 1;
|
|
254
|
+
const escapeCharacter = this.input[this.position];
|
|
255
|
+
if (escapeCharacter === "u") {
|
|
256
|
+
const digits = this.input.slice(this.position + 1, this.position + 5);
|
|
257
|
+
if (!/^[0-9a-fA-F]{4}$/.test(digits))
|
|
258
|
+
this.fail("Invalid Unicode escape sequence.");
|
|
259
|
+
this.position += 5;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (!escapeCharacter || !'"\\/bfnrt'.includes(escapeCharacter))
|
|
263
|
+
this.fail("Invalid escape sequence.");
|
|
264
|
+
}
|
|
265
|
+
this.position += 1;
|
|
266
|
+
}
|
|
267
|
+
this.fail("Unterminated string.", start);
|
|
268
|
+
}
|
|
269
|
+
decodeString(raw, offset) {
|
|
270
|
+
try {
|
|
271
|
+
return JSON.parse(raw);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
this.fail("Invalid string escape sequence.", offset);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
parseNumber() {
|
|
278
|
+
const start = this.position;
|
|
279
|
+
if (this.input[this.position] === "-")
|
|
280
|
+
this.position += 1;
|
|
281
|
+
if (this.input[this.position] === "0") {
|
|
282
|
+
this.position += 1;
|
|
283
|
+
if (this.isDigit(this.input[this.position]))
|
|
284
|
+
this.fail("Leading zeroes are not valid JSON numbers.");
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
if (!this.isDigitOneToNine(this.input[this.position]))
|
|
288
|
+
this.fail("Expected a digit after '-'.");
|
|
289
|
+
while (this.isDigit(this.input[this.position]))
|
|
290
|
+
this.position += 1;
|
|
291
|
+
}
|
|
292
|
+
if (this.input[this.position] === ".") {
|
|
293
|
+
this.position += 1;
|
|
294
|
+
if (!this.isDigit(this.input[this.position]))
|
|
295
|
+
this.fail("Expected a digit after the decimal point.");
|
|
296
|
+
while (this.isDigit(this.input[this.position]))
|
|
297
|
+
this.position += 1;
|
|
298
|
+
}
|
|
299
|
+
if (this.input[this.position] === "e" || this.input[this.position] === "E") {
|
|
300
|
+
this.position += 1;
|
|
301
|
+
if (this.input[this.position] === "+" || this.input[this.position] === "-")
|
|
302
|
+
this.position += 1;
|
|
303
|
+
if (!this.isDigit(this.input[this.position]))
|
|
304
|
+
this.fail("Expected an exponent digit.");
|
|
305
|
+
while (this.isDigit(this.input[this.position]))
|
|
306
|
+
this.position += 1;
|
|
307
|
+
}
|
|
308
|
+
const raw = this.input.slice(start, this.position);
|
|
309
|
+
if (!raw.includes(".") && !/[eE]/.test(raw)) {
|
|
310
|
+
try {
|
|
311
|
+
const integer = BigInt(raw);
|
|
312
|
+
if (integer > BigInt(Number.MAX_SAFE_INTEGER) ||
|
|
313
|
+
integer < BigInt(Number.MIN_SAFE_INTEGER)) {
|
|
314
|
+
this.unsafeNumbers += 1;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
this.fail("Invalid JSON number.", start);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.write(raw);
|
|
322
|
+
return this.buildTree ? { kind: "number", start, end: this.position, raw } : undefined;
|
|
323
|
+
}
|
|
324
|
+
parseLiteral(raw, value) {
|
|
325
|
+
const start = this.position;
|
|
326
|
+
if (this.input.slice(start, start + raw.length) !== raw)
|
|
327
|
+
this.fail(`Expected '${raw}'.`);
|
|
328
|
+
this.position += raw.length;
|
|
329
|
+
this.write(raw);
|
|
330
|
+
return this.buildTree ? { kind: "boolean", start, end: this.position, raw, value } : undefined;
|
|
331
|
+
}
|
|
332
|
+
parseNull() {
|
|
333
|
+
const start = this.position;
|
|
334
|
+
if (this.input.slice(start, start + 4) !== "null")
|
|
335
|
+
this.fail("Expected 'null'.");
|
|
336
|
+
this.position += 4;
|
|
337
|
+
this.write("null");
|
|
338
|
+
return this.buildTree ? { kind: "null", start, end: this.position, raw: "null" } : undefined;
|
|
339
|
+
}
|
|
340
|
+
isDigit(value) {
|
|
341
|
+
return value !== undefined && value >= "0" && value <= "9";
|
|
342
|
+
}
|
|
343
|
+
isDigitOneToNine(value) {
|
|
344
|
+
return value !== undefined && value >= "1" && value <= "9";
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
export function parseLosslessJson(input, options = {}) {
|
|
348
|
+
return new LosslessParser(input, options).parse();
|
|
349
|
+
}
|
|
350
|
+
export function indentToken(indent) {
|
|
351
|
+
if (indent === 0)
|
|
352
|
+
return "";
|
|
353
|
+
if (indent === "tab")
|
|
354
|
+
return "\t";
|
|
355
|
+
return " ".repeat(indent ?? 2);
|
|
356
|
+
}
|
|
357
|
+
export function formatLosslessJson(input, indent) {
|
|
358
|
+
return parseLosslessJson(input, { indent: indentToken(indent) });
|
|
359
|
+
}
|
|
360
|
+
export function serializeJsonNode(node, indent = 2, sortKeys = false, canonicalNumbers = false) {
|
|
361
|
+
const token = indentToken(indent);
|
|
362
|
+
const render = (value, depth) => {
|
|
363
|
+
if (value.kind === "number" && canonicalNumbers)
|
|
364
|
+
return canonicalJsonNumber(value.raw);
|
|
365
|
+
if (value.kind === "string" ||
|
|
366
|
+
value.kind === "number" ||
|
|
367
|
+
value.kind === "boolean" ||
|
|
368
|
+
value.kind === "null") {
|
|
369
|
+
return value.raw;
|
|
370
|
+
}
|
|
371
|
+
const nextDepth = depth + 1;
|
|
372
|
+
const padding = token ? `\n${token.repeat(nextDepth)}` : "";
|
|
373
|
+
const closingPadding = token ? `\n${token.repeat(depth)}` : "";
|
|
374
|
+
if (value.kind === "array") {
|
|
375
|
+
if (!value.items.length)
|
|
376
|
+
return "[]";
|
|
377
|
+
return `[${padding}${value.items.map((item) => render(item, nextDepth)).join(`,${padding}`)}${closingPadding}]`;
|
|
378
|
+
}
|
|
379
|
+
if (!value.entries.length)
|
|
380
|
+
return "{}";
|
|
381
|
+
const entries = sortKeys
|
|
382
|
+
? value.entries
|
|
383
|
+
.map((entry, index) => ({ entry, index }))
|
|
384
|
+
.sort((left, right) => left.entry.key < right.entry.key
|
|
385
|
+
? -1
|
|
386
|
+
: left.entry.key > right.entry.key
|
|
387
|
+
? 1
|
|
388
|
+
: left.index - right.index)
|
|
389
|
+
: value.entries.map((entry, index) => ({ entry, index }));
|
|
390
|
+
const separator = token ? ": " : ":";
|
|
391
|
+
return `{${padding}${entries
|
|
392
|
+
.map(({ entry }) => `${entry.keyRaw}${separator}${render(entry.value, nextDepth)}`)
|
|
393
|
+
.join(`,${padding}`)}${closingPadding}}`;
|
|
394
|
+
};
|
|
395
|
+
return render(node, 0);
|
|
396
|
+
}
|
|
397
|
+
export function canonicalJsonNumber(raw) {
|
|
398
|
+
const negative = raw.startsWith("-");
|
|
399
|
+
const unsigned = negative ? raw.slice(1) : raw;
|
|
400
|
+
const [mantissa, exponentRaw = "0"] = unsigned.toLowerCase().split("e");
|
|
401
|
+
const decimal = mantissa.indexOf(".");
|
|
402
|
+
let digits = mantissa.replace(".", "").replace(/^0+/, "");
|
|
403
|
+
let scale = Number(exponentRaw) - (decimal === -1 ? 0 : mantissa.length - decimal - 1);
|
|
404
|
+
if (!digits)
|
|
405
|
+
return "0";
|
|
406
|
+
while (digits.endsWith("0")) {
|
|
407
|
+
digits = digits.slice(0, -1);
|
|
408
|
+
scale += 1;
|
|
409
|
+
}
|
|
410
|
+
return `${negative ? "-" : ""}${digits}e${scale}`;
|
|
411
|
+
}
|
|
412
|
+
export class LosslessNumber {
|
|
413
|
+
raw;
|
|
414
|
+
constructor(raw) {
|
|
415
|
+
this.raw = raw;
|
|
416
|
+
}
|
|
417
|
+
valueOf() {
|
|
418
|
+
return Number(this.raw);
|
|
419
|
+
}
|
|
420
|
+
toString() {
|
|
421
|
+
return this.raw;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
export function jsonNodeToNative(node, preserveNumbers = false) {
|
|
425
|
+
switch (node.kind) {
|
|
426
|
+
case "null":
|
|
427
|
+
return null;
|
|
428
|
+
case "boolean":
|
|
429
|
+
return node.value;
|
|
430
|
+
case "string":
|
|
431
|
+
return node.value;
|
|
432
|
+
case "number": {
|
|
433
|
+
if (!preserveNumbers)
|
|
434
|
+
return Number(node.raw);
|
|
435
|
+
if (!node.raw.includes(".") && !/[eE]/.test(node.raw)) {
|
|
436
|
+
const value = BigInt(node.raw);
|
|
437
|
+
if (value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(Number.MIN_SAFE_INTEGER))
|
|
438
|
+
return Number(node.raw);
|
|
439
|
+
}
|
|
440
|
+
return new LosslessNumber(node.raw);
|
|
441
|
+
}
|
|
442
|
+
case "array":
|
|
443
|
+
return node.items.map((item) => jsonNodeToNative(item, preserveNumbers));
|
|
444
|
+
case "object": {
|
|
445
|
+
const result = {};
|
|
446
|
+
for (const entry of node.entries)
|
|
447
|
+
result[entry.key] = jsonNodeToNative(entry.value, preserveNumbers);
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
export function stringifyNativeLosslessly(value, indent = 2) {
|
|
453
|
+
const token = indentToken(indent);
|
|
454
|
+
const seen = new Set();
|
|
455
|
+
const render = (item, depth) => {
|
|
456
|
+
if (item instanceof LosslessNumber)
|
|
457
|
+
return item.raw;
|
|
458
|
+
if (item === null || typeof item === "boolean" || typeof item === "number")
|
|
459
|
+
return JSON.stringify(item);
|
|
460
|
+
if (typeof item === "string")
|
|
461
|
+
return JSON.stringify(item);
|
|
462
|
+
if (typeof item === "bigint")
|
|
463
|
+
return item.toString();
|
|
464
|
+
if (item === undefined)
|
|
465
|
+
return "null";
|
|
466
|
+
if (typeof item !== "object")
|
|
467
|
+
return JSON.stringify(String(item));
|
|
468
|
+
if (seen.has(item))
|
|
469
|
+
throw new TypeError("Cannot serialize a circular JSONPath result.");
|
|
470
|
+
seen.add(item);
|
|
471
|
+
const nextDepth = depth + 1;
|
|
472
|
+
const padding = token ? `\n${token.repeat(nextDepth)}` : "";
|
|
473
|
+
const closingPadding = token ? `\n${token.repeat(depth)}` : "";
|
|
474
|
+
let result;
|
|
475
|
+
if (Array.isArray(item)) {
|
|
476
|
+
result = item.length
|
|
477
|
+
? `[${padding}${item.map((child) => render(child, nextDepth)).join(`,${padding}`)}${closingPadding}]`
|
|
478
|
+
: "[]";
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
const entries = Object.entries(item);
|
|
482
|
+
result = entries.length
|
|
483
|
+
? `{${padding}${entries
|
|
484
|
+
.map(([key, child]) => `${JSON.stringify(key)}${token ? ": " : ":"}${render(child, nextDepth)}`)
|
|
485
|
+
.join(`,${padding}`)}${closingPadding}}`
|
|
486
|
+
: "{}";
|
|
487
|
+
}
|
|
488
|
+
seen.delete(item);
|
|
489
|
+
return result;
|
|
490
|
+
};
|
|
491
|
+
return render(value, 0);
|
|
492
|
+
}
|
|
493
|
+
export function findDeepestNodeAtOffset(node, offset) {
|
|
494
|
+
if (node.kind === "array") {
|
|
495
|
+
for (const item of node.items) {
|
|
496
|
+
if (offset >= item.start && offset <= item.end)
|
|
497
|
+
return findDeepestNodeAtOffset(item, offset);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else if (node.kind === "object") {
|
|
501
|
+
for (const entry of node.entries) {
|
|
502
|
+
if (offset >= entry.keyStart && offset <= entry.value.end) {
|
|
503
|
+
return offset >= entry.value.start ? findDeepestNodeAtOffset(entry.value, offset) : node;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return node;
|
|
508
|
+
}
|
package/package.json
CHANGED
|
@@ -1,35 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bestjsonformatter",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Fast, local, lossless JSON formatting and correctness checks for developers, CI, and coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"bestjsonformatter": "dist/cli.
|
|
7
|
+
"bestjsonformatter": "dist/cli.js",
|
|
8
|
+
"bjf": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
8
16
|
},
|
|
9
17
|
"files": [
|
|
10
18
|
"dist",
|
|
11
|
-
"README.md"
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
12
21
|
],
|
|
13
22
|
"engines": {
|
|
14
23
|
"node": ">=20"
|
|
15
24
|
},
|
|
16
|
-
"
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "node scripts/build.mjs",
|
|
27
|
+
"lint": "biome check .",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"smoke": "node scripts/smoke-test.mjs",
|
|
31
|
+
"check": "npm run lint && npm run typecheck && npm run test && npm run build && npm run smoke && npm pack --dry-run",
|
|
32
|
+
"benchmark": "npm run build && node benchmarks/cli.mjs",
|
|
33
|
+
"prepublishOnly": "npm run check"
|
|
34
|
+
},
|
|
35
|
+
"sideEffects": false,
|
|
17
36
|
"repository": {
|
|
18
37
|
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/skrcode/bestjsonformatter.git"
|
|
38
|
+
"url": "git+https://github.com/skrcode/bestjsonformatter-cli.git"
|
|
20
39
|
},
|
|
21
|
-
"homepage": "https://bestjsonformatter.org/
|
|
40
|
+
"homepage": "https://bestjsonformatter.org/cli",
|
|
22
41
|
"bugs": {
|
|
23
|
-
"url": "https://
|
|
42
|
+
"url": "https://github.com/skrcode/bestjsonformatter-cli/issues"
|
|
24
43
|
},
|
|
25
44
|
"keywords": [
|
|
26
45
|
"json",
|
|
27
46
|
"formatter",
|
|
28
47
|
"validator",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
48
|
+
"cli",
|
|
49
|
+
"lossless-json",
|
|
31
50
|
"json-schema",
|
|
32
|
-
"
|
|
51
|
+
"coding-agents",
|
|
52
|
+
"ci"
|
|
33
53
|
],
|
|
34
|
-
"license": "
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"ajv": "^8.20.0",
|
|
57
|
+
"ajv-formats": "^3.0.1",
|
|
58
|
+
"diff": "^9.0.0",
|
|
59
|
+
"jsonpath-plus": "^10.4.0",
|
|
60
|
+
"jsonrepair": "^3.15.0",
|
|
61
|
+
"yaml": "^2.9.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@biomejs/biome": "2.5.6",
|
|
65
|
+
"@types/node": "^20.19.24",
|
|
66
|
+
"jsonc-parser": "3.3.1",
|
|
67
|
+
"prettier": "3.9.6",
|
|
68
|
+
"typescript": "^5.9.3",
|
|
69
|
+
"vitest": "4.1.10"
|
|
70
|
+
}
|
|
35
71
|
}
|