@nexkit/json-repair 1.0.0

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/dist/index.cjs ADDED
@@ -0,0 +1,2400 @@
1
+ 'use strict';
2
+
3
+ // src/scanner/tokens.ts
4
+ var CH_OPEN_BRACE = 123;
5
+ var CH_CLOSE_BRACE = 125;
6
+ var CH_OPEN_BRACKET = 91;
7
+ var CH_CLOSE_BRACKET = 93;
8
+ var CH_COLON = 58;
9
+ var CH_COMMA = 44;
10
+ var CH_DOUBLE_QUOTE = 34;
11
+ var CH_SINGLE_QUOTE = 39;
12
+ var CH_BACKTICK = 96;
13
+ var CH_LEFT_SINGLE_QUOTE = 8216;
14
+ var CH_RIGHT_SINGLE_QUOTE = 8217;
15
+ var CH_SINGLE_LOW_QUOTE = 8218;
16
+ var CH_SINGLE_HIGH_REVERSED = 8219;
17
+ var CH_LEFT_DOUBLE_QUOTE = 8220;
18
+ var CH_RIGHT_DOUBLE_QUOTE = 8221;
19
+ var CH_DOUBLE_LOW_QUOTE = 8222;
20
+ var CH_DOUBLE_HIGH_REVERSED = 8223;
21
+ var CH_BACKSLASH = 92;
22
+ var CH_SLASH = 47;
23
+ var CH_ASTERISK = 42;
24
+ var CH_HASH = 35;
25
+ var CH_MINUS = 45;
26
+ var CH_PLUS = 43;
27
+ var CH_DOT = 46;
28
+ var CH_UNDERSCORE = 95;
29
+ var CH_DOLLAR = 36;
30
+ var CH_TAB = 9;
31
+ var CH_LINE_FEED = 10;
32
+ var CH_VERTICAL_TAB = 11;
33
+ var CH_FORM_FEED = 12;
34
+ var CH_CARRIAGE_RETURN = 13;
35
+ var CH_SPACE = 32;
36
+ var CH_NBSP = 160;
37
+ var CH_BOM = 65279;
38
+ var CH_LOWER_A = 97;
39
+ var CH_LOWER_B = 98;
40
+ var CH_LOWER_E = 101;
41
+ var CH_LOWER_F = 102;
42
+ var CH_LOWER_N = 110;
43
+ var CH_LOWER_R = 114;
44
+ var CH_LOWER_T = 116;
45
+ var CH_LOWER_U = 117;
46
+ var CH_LOWER_X = 120;
47
+ var CH_LOWER_Z = 122;
48
+ var CH_UPPER_A = 65;
49
+ var CH_UPPER_E = 69;
50
+ var CH_UPPER_F = 70;
51
+ var CH_UPPER_X = 88;
52
+ var CH_UPPER_Z = 90;
53
+ var CH_ZERO = 48;
54
+ var CH_NINE = 57;
55
+ var CH_HIGH_SURROGATE_START = 55296;
56
+ var CH_HIGH_SURROGATE_END = 56319;
57
+ var CH_LOW_SURROGATE_START = 56320;
58
+ var CH_LOW_SURROGATE_END = 57343;
59
+ var CH_EOF = -1;
60
+ function isWhitespace(code) {
61
+ switch (code) {
62
+ case CH_SPACE:
63
+ case CH_TAB:
64
+ case CH_LINE_FEED:
65
+ case CH_CARRIAGE_RETURN:
66
+ case CH_VERTICAL_TAB:
67
+ case CH_FORM_FEED:
68
+ case CH_NBSP:
69
+ case CH_BOM:
70
+ case 5760:
71
+ /* OGHAM SPACE MARK */
72
+ case 8232:
73
+ /* LINE SEPARATOR */
74
+ case 8233:
75
+ /* PARAGRAPH SEPARATOR */
76
+ case 8239:
77
+ /* NARROW NO-BREAK SPACE */
78
+ case 8287:
79
+ /* MEDIUM MATHEMATICAL SPACE */
80
+ case 12288:
81
+ return true;
82
+ default:
83
+ return code >= 8192 && code <= 8202;
84
+ }
85
+ }
86
+ function isJsonWhitespace(code) {
87
+ return code === CH_SPACE || code === CH_TAB || code === CH_LINE_FEED || code === CH_CARRIAGE_RETURN;
88
+ }
89
+ function isLineBreak(code) {
90
+ return code === CH_LINE_FEED || code === CH_CARRIAGE_RETURN;
91
+ }
92
+ function isDigit(code) {
93
+ return code >= CH_ZERO && code <= CH_NINE;
94
+ }
95
+ function isHexDigit(code) {
96
+ return code >= CH_ZERO && code <= CH_NINE || code >= CH_UPPER_A && code <= CH_UPPER_F || code >= CH_LOWER_A && code <= CH_LOWER_F;
97
+ }
98
+ function isQuote(code) {
99
+ switch (code) {
100
+ case CH_DOUBLE_QUOTE:
101
+ case CH_SINGLE_QUOTE:
102
+ case CH_BACKTICK:
103
+ case CH_LEFT_SINGLE_QUOTE:
104
+ case CH_RIGHT_SINGLE_QUOTE:
105
+ case CH_SINGLE_LOW_QUOTE:
106
+ case CH_SINGLE_HIGH_REVERSED:
107
+ case CH_LEFT_DOUBLE_QUOTE:
108
+ case CH_RIGHT_DOUBLE_QUOTE:
109
+ case CH_DOUBLE_LOW_QUOTE:
110
+ case CH_DOUBLE_HIGH_REVERSED:
111
+ return true;
112
+ default:
113
+ return false;
114
+ }
115
+ }
116
+ function closersFor(code) {
117
+ switch (code) {
118
+ case CH_LEFT_DOUBLE_QUOTE:
119
+ case CH_DOUBLE_LOW_QUOTE:
120
+ case CH_DOUBLE_HIGH_REVERSED:
121
+ return [CH_RIGHT_DOUBLE_QUOTE, code];
122
+ case CH_RIGHT_DOUBLE_QUOTE:
123
+ return [CH_RIGHT_DOUBLE_QUOTE, CH_LEFT_DOUBLE_QUOTE];
124
+ case CH_LEFT_SINGLE_QUOTE:
125
+ case CH_SINGLE_LOW_QUOTE:
126
+ case CH_SINGLE_HIGH_REVERSED:
127
+ return [CH_RIGHT_SINGLE_QUOTE, code];
128
+ case CH_RIGHT_SINGLE_QUOTE:
129
+ return [CH_RIGHT_SINGLE_QUOTE, CH_LEFT_SINGLE_QUOTE];
130
+ default:
131
+ return [code, -1];
132
+ }
133
+ }
134
+ function isStandardQuote(code) {
135
+ return code === CH_DOUBLE_QUOTE;
136
+ }
137
+ function isIdentifierStart(code) {
138
+ return code >= CH_LOWER_A && code <= CH_LOWER_Z || code >= CH_UPPER_A && code <= CH_UPPER_Z || code === CH_UNDERSCORE || code === CH_DOLLAR || code >= 192;
139
+ }
140
+ function isIdentifierPart(code) {
141
+ return isIdentifierStart(code) || isDigit(code) || code === CH_MINUS || code === CH_DOT;
142
+ }
143
+ function isStructural(code) {
144
+ switch (code) {
145
+ case CH_OPEN_BRACE:
146
+ case CH_CLOSE_BRACE:
147
+ case CH_OPEN_BRACKET:
148
+ case CH_CLOSE_BRACKET:
149
+ case CH_COLON:
150
+ case CH_COMMA:
151
+ case CH_EOF:
152
+ return true;
153
+ default:
154
+ return false;
155
+ }
156
+ }
157
+ function isHighSurrogate(code) {
158
+ return code >= CH_HIGH_SURROGATE_START && code <= CH_HIGH_SURROGATE_END;
159
+ }
160
+ function isLowSurrogate(code) {
161
+ return code >= CH_LOW_SURROGATE_START && code <= CH_LOW_SURROGATE_END;
162
+ }
163
+ function toLowerAscii(code) {
164
+ return code >= CH_UPPER_A && code <= CH_UPPER_Z ? code + 32 : code;
165
+ }
166
+
167
+ // src/scanner/scanner.ts
168
+ var Scanner = class {
169
+ /** The original, uncut input. */
170
+ source;
171
+ /** First offset of the region being parsed. */
172
+ start;
173
+ /** Offset one past the last character of the region. */
174
+ end;
175
+ /** Current offset. Always within `[start, end]`. */
176
+ index;
177
+ /** Offsets at which each line begins; built lazily by {@link positionAt}. */
178
+ lineStarts;
179
+ constructor(source, start = 0, end = source.length) {
180
+ this.source = source;
181
+ this.start = start;
182
+ this.end = end;
183
+ this.index = start;
184
+ }
185
+ /** Reports whether the cursor has reached the end of the region. */
186
+ eof() {
187
+ return this.index >= this.end;
188
+ }
189
+ /** Returns the code unit at the cursor, or {@link CH_EOF} at the end. */
190
+ peek() {
191
+ return this.index < this.end ? this.source.charCodeAt(this.index) : CH_EOF;
192
+ }
193
+ /** Returns the text between two absolute offsets. */
194
+ slice(from, to) {
195
+ return this.source.slice(from, to);
196
+ }
197
+ /**
198
+ * Resolves an absolute offset to a one-based line and column.
199
+ *
200
+ * `\n`, `\r\n` and a lone `\r` all end a line. U+2028 and U+2029 do not:
201
+ * JSON treats them as ordinary string content, and counting them would make
202
+ * positions disagree with what an editor shows.
203
+ */
204
+ positionAt(offset) {
205
+ const starts = this.lineStarts ?? (this.lineStarts = buildLineStarts(this.source));
206
+ let low = 0;
207
+ let high = starts.length - 1;
208
+ while (low < high) {
209
+ const mid = low + high + 1 >>> 1;
210
+ if (starts[mid] <= offset) {
211
+ low = mid;
212
+ } else {
213
+ high = mid - 1;
214
+ }
215
+ }
216
+ return { line: low + 1, column: offset - starts[low] + 1 };
217
+ }
218
+ };
219
+ function buildLineStarts(source) {
220
+ const starts = [0];
221
+ const length = source.length;
222
+ for (let index = 0; index < length; index += 1) {
223
+ const code = source.charCodeAt(index);
224
+ if (code === CH_CARRIAGE_RETURN) {
225
+ if (index + 1 < length && source.charCodeAt(index + 1) === CH_LINE_FEED) {
226
+ index += 1;
227
+ }
228
+ starts.push(index + 1);
229
+ } else if (code === CH_LINE_FEED) {
230
+ starts.push(index + 1);
231
+ }
232
+ }
233
+ return starts;
234
+ }
235
+
236
+ // src/errors/JsonRepairError.ts
237
+ var ERROR_BRAND = /* @__PURE__ */ Symbol.for("json-repair.error");
238
+ var SNIPPET_RADIUS = 24;
239
+ var JsonRepairError = class extends Error {
240
+ /** Always `'JsonRepairError'`, so the name survives minification. */
241
+ name = "JsonRepairError";
242
+ /** Stable, machine-readable failure reason. */
243
+ code;
244
+ /** Zero-based offset into the original input, in UTF-16 code units. */
245
+ position;
246
+ /** One-based line number in the original input. */
247
+ line;
248
+ /** One-based column number in the original input. */
249
+ column;
250
+ /** A short excerpt of the input around {@link JsonRepairError.position}. */
251
+ snippet;
252
+ /** Cross-realm brand; see {@link isJsonRepairError}. */
253
+ [ERROR_BRAND] = true;
254
+ constructor(code, message, details = {}) {
255
+ super(message);
256
+ this.code = code;
257
+ this.position = details.position;
258
+ this.line = details.line;
259
+ this.column = details.column;
260
+ this.snippet = details.snippet;
261
+ Object.setPrototypeOf(this, new.target.prototype);
262
+ }
263
+ /**
264
+ * Reports whether a value is a {@link JsonRepairError}, including instances
265
+ * created by a different copy of this package.
266
+ *
267
+ * Prefer this over `instanceof` — see {@link ERROR_BRAND}.
268
+ */
269
+ static isJsonRepairError(value) {
270
+ return typeof value === "object" && value !== null && value[ERROR_BRAND] === true;
271
+ }
272
+ };
273
+ function isJsonRepairError(value) {
274
+ return JsonRepairError.isJsonRepairError(value);
275
+ }
276
+ function createError(code, message, source, position) {
277
+ const clamped = position < 0 ? 0 : position > source.length ? source.length : position;
278
+ const { line, column } = new Scanner(source).positionAt(clamped);
279
+ return new JsonRepairError(code, message, {
280
+ position: clamped,
281
+ line,
282
+ column,
283
+ snippet: buildSnippet(source, clamped)
284
+ });
285
+ }
286
+ function buildSnippet(source, position) {
287
+ const clamped = position < 0 ? 0 : position > source.length ? source.length : position;
288
+ const start = clamped - SNIPPET_RADIUS < 0 ? 0 : clamped - SNIPPET_RADIUS;
289
+ const end = clamped + SNIPPET_RADIUS > source.length ? source.length : clamped + SNIPPET_RADIUS;
290
+ let excerpt = "";
291
+ for (let index = start; index < end; index += 1) {
292
+ excerpt += escapeForDisplay(source.charCodeAt(index));
293
+ }
294
+ return `${start > 0 ? "\u2026" : ""}${excerpt}${end < source.length ? "\u2026" : ""}`;
295
+ }
296
+ function escapeForDisplay(code) {
297
+ switch (code) {
298
+ case 8:
299
+ return "\\b";
300
+ case 9:
301
+ return "\\t";
302
+ case 10:
303
+ return "\\n";
304
+ case 12:
305
+ return "\\f";
306
+ case 13:
307
+ return "\\r";
308
+ }
309
+ if (code < 32 || code === 127) {
310
+ return `\\u${code.toString(16).padStart(4, "0")}`;
311
+ }
312
+ return String.fromCharCode(code);
313
+ }
314
+
315
+ // src/limits.ts
316
+ function assertString(input, parameter) {
317
+ if (typeof input !== "string") {
318
+ throw new JsonRepairError(
319
+ "INVALID_INPUT",
320
+ `Parameter "${parameter}" must be a string, received ${input === null ? "null" : typeof input}.`
321
+ );
322
+ }
323
+ }
324
+ function assertInputLength(input, maxLength) {
325
+ if (input.length > maxLength) {
326
+ throw new JsonRepairError(
327
+ "MAX_LENGTH_EXCEEDED",
328
+ `Input is ${input.length} characters, which exceeds the maximum of ${maxLength}.`,
329
+ { position: maxLength }
330
+ );
331
+ }
332
+ }
333
+ function assertStructuralDepth(source, start, end, maxDepth) {
334
+ let depth = 0;
335
+ let index = start;
336
+ while (index < end) {
337
+ const code = source.charCodeAt(index);
338
+ if (code === CH_DOUBLE_QUOTE) {
339
+ index = skipValidString(source, index + 1, end);
340
+ continue;
341
+ }
342
+ if (code === CH_OPEN_BRACE || code === CH_OPEN_BRACKET) {
343
+ depth += 1;
344
+ if (depth > maxDepth) {
345
+ throw createError(
346
+ "MAX_DEPTH_EXCEEDED",
347
+ `Nesting depth exceeds the maximum of ${maxDepth}.`,
348
+ source,
349
+ index
350
+ );
351
+ }
352
+ } else if (code === CH_CLOSE_BRACE || code === CH_CLOSE_BRACKET) {
353
+ depth -= 1;
354
+ }
355
+ index += 1;
356
+ }
357
+ }
358
+ function skipValidString(source, from, end) {
359
+ let index = from;
360
+ while (index < end) {
361
+ const code = source.charCodeAt(index);
362
+ if (code === CH_BACKSLASH) {
363
+ index += 2;
364
+ continue;
365
+ }
366
+ if (code === CH_DOUBLE_QUOTE) {
367
+ return index + 1;
368
+ }
369
+ index += 1;
370
+ }
371
+ return end;
372
+ }
373
+
374
+ // src/native.ts
375
+ var parseJsonStrict = JSON.parse;
376
+
377
+ // src/types.ts
378
+ var REPAIR_TYPES = [
379
+ "removed-byte-order-mark",
380
+ "extracted-json",
381
+ "removed-comment",
382
+ "normalized-whitespace",
383
+ "removed-trailing-comma",
384
+ "removed-extra-comma",
385
+ "removed-stray-token",
386
+ "removed-trailing-content",
387
+ "removed-incomplete-member",
388
+ "added-missing-comma",
389
+ "added-missing-colon",
390
+ "added-missing-value",
391
+ "added-closing-brace",
392
+ "added-closing-bracket",
393
+ "normalized-quotes",
394
+ "normalized-literal",
395
+ "normalized-number",
396
+ "quoted-key",
397
+ "quoted-value",
398
+ "escaped-character",
399
+ "fixed-escape",
400
+ "terminated-string-at-eof",
401
+ "terminated-string-at-newline"
402
+ ];
403
+ var MAX_SUPPORTED_DEPTH = 1024;
404
+
405
+ // src/options.ts
406
+ var DEFAULT_MAX_LENGTH = 1e7;
407
+ var DEFAULT_MAX_DEPTH = 512;
408
+ function resolveOptions(options) {
409
+ const source = asOptionBag(options);
410
+ const mode = resolveMode(source.mode);
411
+ return {
412
+ mode,
413
+ aggressive: mode === "aggressive",
414
+ extract: resolveFlag(source.extract, "extract", true),
415
+ allowComments: resolveFlag(source.allowComments, "allowComments", true),
416
+ allowSingleQuotes: resolveFlag(source.allowSingleQuotes, "allowSingleQuotes", true),
417
+ allowUnquotedKeys: resolveFlag(source.allowUnquotedKeys, "allowUnquotedKeys", true),
418
+ fixTrailingCommas: resolveFlag(source.fixTrailingCommas, "fixTrailingCommas", true),
419
+ fixMissingBrackets: resolveFlag(source.fixMissingBrackets, "fixMissingBrackets", true),
420
+ maxLength: resolveLimit(source.maxLength, "maxLength", DEFAULT_MAX_LENGTH, 1, Infinity),
421
+ maxDepth: resolveLimit(source.maxDepth, "maxDepth", DEFAULT_MAX_DEPTH, 1, MAX_SUPPORTED_DEPTH),
422
+ maxRepairs: resolveLimit(source.maxRepairs, "maxRepairs", Infinity, 0, Infinity),
423
+ returnMetadata: resolveFlag(source.returnMetadata, "returnMetadata", false)
424
+ };
425
+ }
426
+ function asOptionBag(options) {
427
+ if (options === void 0) {
428
+ return {};
429
+ }
430
+ if (typeof options !== "object" || options === null) {
431
+ throw new JsonRepairError(
432
+ "INVALID_INPUT",
433
+ `Options must be an object, received ${describe(options)}.`
434
+ );
435
+ }
436
+ return options;
437
+ }
438
+ function resolveExtractOptions(options) {
439
+ const source = asOptionBag(options);
440
+ const select = resolveSelect(source.select);
441
+ return { ...resolveOptions({ ...source, extract: true }), select };
442
+ }
443
+ function resolveSelect(select) {
444
+ if (select === void 0) {
445
+ return "best";
446
+ }
447
+ if (select !== "best" && select !== "first" && select !== "last" && select !== "largest") {
448
+ throw new JsonRepairError(
449
+ "INVALID_INPUT",
450
+ `Option "select" must be "best", "first", "last" or "largest", received ${describe(select)}.`
451
+ );
452
+ }
453
+ return select;
454
+ }
455
+ function withMode(options, mode) {
456
+ return { ...options, mode, aggressive: mode === "aggressive" };
457
+ }
458
+ function resolveMode(mode) {
459
+ if (mode === void 0) {
460
+ return "safe";
461
+ }
462
+ if (mode !== "safe" && mode !== "aggressive") {
463
+ throw new JsonRepairError(
464
+ "INVALID_INPUT",
465
+ `Option "mode" must be "safe" or "aggressive", received ${describe(mode)}.`
466
+ );
467
+ }
468
+ return mode;
469
+ }
470
+ function resolveFlag(value, name, fallback) {
471
+ if (value === void 0) {
472
+ return fallback;
473
+ }
474
+ if (typeof value !== "boolean") {
475
+ throw new JsonRepairError(
476
+ "INVALID_INPUT",
477
+ `Option "${name}" must be a boolean, received ${describe(value)}.`
478
+ );
479
+ }
480
+ return value;
481
+ }
482
+ function resolveLimit(value, name, fallback, min, max) {
483
+ if (value === void 0) {
484
+ return fallback;
485
+ }
486
+ if (typeof value !== "number" || Number.isNaN(value)) {
487
+ throw new JsonRepairError(
488
+ "INVALID_INPUT",
489
+ `Option "${name}" must be a number, received ${describe(value)}.`
490
+ );
491
+ }
492
+ if (value !== Infinity && !Number.isInteger(value)) {
493
+ throw new JsonRepairError(
494
+ "INVALID_INPUT",
495
+ `Option "${name}" must be an integer or Infinity, received ${value}.`
496
+ );
497
+ }
498
+ if (value < min || value > max) {
499
+ const ceiling = max === Infinity ? "Infinity" : String(max);
500
+ throw new JsonRepairError(
501
+ "INVALID_INPUT",
502
+ `Option "${name}" must be between ${min} and ${ceiling}, received ${value}.`
503
+ );
504
+ }
505
+ return value;
506
+ }
507
+ function describe(value) {
508
+ if (value === null) {
509
+ return "null";
510
+ }
511
+ switch (typeof value) {
512
+ case "string":
513
+ return JSON.stringify(value);
514
+ case "number":
515
+ case "boolean":
516
+ case "undefined":
517
+ return String(value);
518
+ case "bigint":
519
+ return `${String(value)}n`;
520
+ default:
521
+ return typeof value;
522
+ }
523
+ }
524
+
525
+ // src/repair/brackets.ts
526
+ function closeUnterminated(ctx, kind, position) {
527
+ if (!ctx.options.fixMissingBrackets) {
528
+ ctx.fail(kind === "object" ? "Object is never closed" : "Array is never closed", position);
529
+ }
530
+ ctx.record(kind === "object" ? "added-closing-brace" : "added-closing-bracket", position);
531
+ }
532
+ function dropStrayCloser(ctx) {
533
+ const position = ctx.scanner.index;
534
+ const atEnd = position === ctx.lastContentIndex;
535
+ if (!atEnd && !ctx.options.aggressive) {
536
+ const character = ctx.scanner.peek() === CH_CLOSE_BRACE ? "}" : "]";
537
+ ctx.refuse(`Unexpected "${character}" with no matching opening bracket`, position);
538
+ }
539
+ ctx.record("removed-stray-token", position);
540
+ ctx.scanner.index = position + 1;
541
+ }
542
+
543
+ // src/repair/comments.ts
544
+ function skipTrivia(ctx) {
545
+ const { scanner } = ctx;
546
+ const { source, end } = scanner;
547
+ const allowComments = ctx.options.allowComments;
548
+ let index = scanner.index;
549
+ for (; ; ) {
550
+ let exotic = -1;
551
+ while (index < end) {
552
+ const code2 = source.charCodeAt(index);
553
+ if (!isWhitespace(code2)) {
554
+ break;
555
+ }
556
+ if (exotic < 0 && !isJsonWhitespace(code2)) {
557
+ exotic = index;
558
+ }
559
+ index += 1;
560
+ }
561
+ if (exotic >= 0) {
562
+ scanner.index = index;
563
+ ctx.record("normalized-whitespace", exotic);
564
+ }
565
+ if (index >= end || !allowComments) {
566
+ break;
567
+ }
568
+ const commentStart = index;
569
+ const code = source.charCodeAt(index);
570
+ if (code === CH_HASH) {
571
+ index = skipToLineEnd(source, index + 1, end);
572
+ } else if (code === CH_SLASH && index + 1 < end) {
573
+ const nextCode = source.charCodeAt(index + 1);
574
+ if (nextCode === CH_SLASH) {
575
+ index = skipToLineEnd(source, index + 2, end);
576
+ } else if (nextCode === CH_ASTERISK) {
577
+ index = skipBlockComment(source, index + 2, end);
578
+ } else {
579
+ break;
580
+ }
581
+ } else {
582
+ break;
583
+ }
584
+ scanner.index = index;
585
+ ctx.record("removed-comment", commentStart);
586
+ }
587
+ scanner.index = index;
588
+ }
589
+ function scanTrivia(source, from, end, allowComments) {
590
+ let index = from;
591
+ for (; ; ) {
592
+ while (index < end && isWhitespace(source.charCodeAt(index))) {
593
+ index += 1;
594
+ }
595
+ if (index >= end || !allowComments) {
596
+ return index;
597
+ }
598
+ const code = source.charCodeAt(index);
599
+ if (code === CH_HASH) {
600
+ index = skipToLineEnd(source, index + 1, end);
601
+ } else if (code === CH_SLASH && index + 1 < end) {
602
+ const nextCode = source.charCodeAt(index + 1);
603
+ if (nextCode === CH_SLASH) {
604
+ index = skipToLineEnd(source, index + 2, end);
605
+ } else if (nextCode === CH_ASTERISK) {
606
+ index = skipBlockComment(source, index + 2, end);
607
+ } else {
608
+ return index;
609
+ }
610
+ } else {
611
+ return index;
612
+ }
613
+ }
614
+ }
615
+ function skipToLineEnd(source, from, end) {
616
+ let index = from;
617
+ while (index < end && !isLineBreak(source.charCodeAt(index))) {
618
+ index += 1;
619
+ }
620
+ return index;
621
+ }
622
+ function skipBlockComment(source, from, end) {
623
+ let index = from;
624
+ while (index < end) {
625
+ if (source.charCodeAt(index) === CH_ASTERISK && index + 1 < end && source.charCodeAt(index + 1) === CH_SLASH) {
626
+ return index + 2;
627
+ }
628
+ index += 1;
629
+ }
630
+ return end;
631
+ }
632
+
633
+ // src/repair/literals.ts
634
+ function readLiteral(ctx, start, end) {
635
+ const { source } = ctx.scanner;
636
+ const length = end - start;
637
+ if (length < 3 || length > 9) {
638
+ return "not-a-literal";
639
+ }
640
+ const canonical = matchJsonLiteral(source, start, end, length);
641
+ if (canonical !== void 0) {
642
+ if (matchesExactly(source, start, canonical)) {
643
+ ctx.writeSpan(start, end);
644
+ } else {
645
+ ctx.write(canonical);
646
+ ctx.record("normalized-literal", start, `Rewrote a literal as ${canonical}`);
647
+ }
648
+ return "emitted";
649
+ }
650
+ const nullish = matchNullish(source, start, end, length);
651
+ if (nullish === void 0) {
652
+ return "not-a-literal";
653
+ }
654
+ if (nullish === "lossy" && !ctx.options.aggressive) {
655
+ return "needs-aggressive";
656
+ }
657
+ ctx.write("null");
658
+ ctx.record("normalized-literal", start, "Rewrote a non-JSON literal as null");
659
+ return "emitted";
660
+ }
661
+ function matchJsonLiteral(source, start, end, length) {
662
+ if (length === 4) {
663
+ if (matchesWord(source, start, end, "true")) {
664
+ return "true";
665
+ }
666
+ if (matchesWord(source, start, end, "null")) {
667
+ return "null";
668
+ }
669
+ return void 0;
670
+ }
671
+ if (length === 5 && matchesWord(source, start, end, "false")) {
672
+ return "false";
673
+ }
674
+ return void 0;
675
+ }
676
+ function matchNullish(source, start, end, length) {
677
+ switch (length) {
678
+ case 3:
679
+ return matchesWord(source, start, end, "nan") ? "lossy" : void 0;
680
+ case 4:
681
+ return matchesWord(source, start, end, "none") ? "equivalent" : void 0;
682
+ case 8:
683
+ return matchesWord(source, start, end, "infinity") ? "lossy" : void 0;
684
+ case 9:
685
+ return matchesWord(source, start, end, "undefined") || matchesWord(source, start, end, "-infinity") ? "lossy" : void 0;
686
+ default:
687
+ return void 0;
688
+ }
689
+ }
690
+ function matchesWord(source, start, end, word) {
691
+ if (end - start !== word.length) {
692
+ return false;
693
+ }
694
+ for (let offset = 0; offset < word.length; offset += 1) {
695
+ if (toLowerAscii(source.charCodeAt(start + offset)) !== word.charCodeAt(offset)) {
696
+ return false;
697
+ }
698
+ }
699
+ return true;
700
+ }
701
+ function matchesExactly(source, start, word) {
702
+ for (let offset = 0; offset < word.length; offset += 1) {
703
+ if (source.charCodeAt(start + offset) !== word.charCodeAt(offset)) {
704
+ return false;
705
+ }
706
+ }
707
+ return true;
708
+ }
709
+
710
+ // src/repair/numbers.ts
711
+ function readNumber(ctx, start, end) {
712
+ const { source } = ctx.scanner;
713
+ if (isValidJsonNumber(source, start, end)) {
714
+ ctx.writeSpan(start, end);
715
+ return true;
716
+ }
717
+ const aggressive = ctx.options.aggressive;
718
+ let index = start;
719
+ let sign = "";
720
+ let normalized = false;
721
+ const first = source.charCodeAt(index);
722
+ if (first === CH_PLUS) {
723
+ index += 1;
724
+ normalized = true;
725
+ } else if (first === CH_MINUS) {
726
+ sign = "-";
727
+ index += 1;
728
+ }
729
+ if (aggressive && isHexPrefix(source, index, end)) {
730
+ return readHex(ctx, start, index, end, sign);
731
+ }
732
+ let usedUnderscore = false;
733
+ let integerDigits = "";
734
+ while (index < end) {
735
+ const code = source.charCodeAt(index);
736
+ if (isDigit(code)) {
737
+ integerDigits += source.charAt(index);
738
+ } else if (code === CH_UNDERSCORE) {
739
+ usedUnderscore = true;
740
+ } else {
741
+ break;
742
+ }
743
+ index += 1;
744
+ }
745
+ let fractionDigits = "";
746
+ let sawDot = false;
747
+ if (index < end && source.charCodeAt(index) === CH_DOT) {
748
+ sawDot = true;
749
+ index += 1;
750
+ while (index < end) {
751
+ const code = source.charCodeAt(index);
752
+ if (isDigit(code)) {
753
+ fractionDigits += source.charAt(index);
754
+ } else if (code === CH_UNDERSCORE) {
755
+ usedUnderscore = true;
756
+ } else {
757
+ break;
758
+ }
759
+ index += 1;
760
+ }
761
+ }
762
+ let exponent = "";
763
+ if (index < end) {
764
+ const code = source.charCodeAt(index);
765
+ if (code === CH_LOWER_E || code === CH_UPPER_E) {
766
+ let cursor = index + 1;
767
+ let exponentSign = "";
768
+ if (cursor < end) {
769
+ const signCode = source.charCodeAt(cursor);
770
+ if (signCode === CH_PLUS || signCode === CH_MINUS) {
771
+ exponentSign = signCode === CH_MINUS ? "-" : "";
772
+ cursor += 1;
773
+ }
774
+ }
775
+ const digitsStart = cursor;
776
+ while (cursor < end && isDigit(source.charCodeAt(cursor))) {
777
+ cursor += 1;
778
+ }
779
+ if (cursor > digitsStart) {
780
+ exponent = `e${exponentSign}${source.slice(digitsStart, cursor)}`;
781
+ } else {
782
+ normalized = true;
783
+ }
784
+ index = cursor;
785
+ }
786
+ }
787
+ if (index !== end || integerDigits.length === 0 && fractionDigits.length === 0) {
788
+ return false;
789
+ }
790
+ if (usedUnderscore) {
791
+ if (!aggressive) {
792
+ return false;
793
+ }
794
+ normalized = true;
795
+ }
796
+ let integerPart = integerDigits;
797
+ if (integerPart.length === 0) {
798
+ integerPart = "0";
799
+ normalized = true;
800
+ } else if (integerPart.length > 1 && integerPart.charCodeAt(0) === CH_ZERO) {
801
+ if (!aggressive) {
802
+ return false;
803
+ }
804
+ let cut = 0;
805
+ while (cut < integerPart.length - 1 && integerPart.charCodeAt(cut) === CH_ZERO) {
806
+ cut += 1;
807
+ }
808
+ integerPart = integerPart.slice(cut);
809
+ normalized = true;
810
+ }
811
+ if (sawDot && fractionDigits.length === 0) {
812
+ normalized = true;
813
+ }
814
+ if (!normalized) {
815
+ return false;
816
+ }
817
+ ctx.write(sign);
818
+ ctx.write(integerPart);
819
+ if (fractionDigits.length > 0) {
820
+ ctx.write(".");
821
+ ctx.write(fractionDigits);
822
+ }
823
+ ctx.write(exponent);
824
+ ctx.record("normalized-number", start);
825
+ return true;
826
+ }
827
+ function isValidJsonNumber(source, start, end) {
828
+ let index = start;
829
+ if (index < end && source.charCodeAt(index) === CH_MINUS) {
830
+ index += 1;
831
+ }
832
+ if (index >= end) {
833
+ return false;
834
+ }
835
+ if (source.charCodeAt(index) === CH_ZERO) {
836
+ index += 1;
837
+ } else {
838
+ if (!isDigit(source.charCodeAt(index))) {
839
+ return false;
840
+ }
841
+ while (index < end && isDigit(source.charCodeAt(index))) {
842
+ index += 1;
843
+ }
844
+ }
845
+ if (index < end && source.charCodeAt(index) === CH_DOT) {
846
+ index += 1;
847
+ const digitsStart = index;
848
+ while (index < end && isDigit(source.charCodeAt(index))) {
849
+ index += 1;
850
+ }
851
+ if (index === digitsStart) {
852
+ return false;
853
+ }
854
+ }
855
+ if (index < end) {
856
+ const code = source.charCodeAt(index);
857
+ if (code !== CH_LOWER_E && code !== CH_UPPER_E) {
858
+ return false;
859
+ }
860
+ index += 1;
861
+ if (index < end) {
862
+ const signCode = source.charCodeAt(index);
863
+ if (signCode === CH_PLUS || signCode === CH_MINUS) {
864
+ index += 1;
865
+ }
866
+ }
867
+ const digitsStart = index;
868
+ while (index < end && isDigit(source.charCodeAt(index))) {
869
+ index += 1;
870
+ }
871
+ if (index === digitsStart) {
872
+ return false;
873
+ }
874
+ }
875
+ return index === end;
876
+ }
877
+ function isHexPrefix(source, index, end) {
878
+ if (index + 2 >= end || source.charCodeAt(index) !== CH_ZERO) {
879
+ return false;
880
+ }
881
+ const marker = source.charCodeAt(index + 1);
882
+ return marker === CH_LOWER_X || marker === CH_UPPER_X;
883
+ }
884
+ function readHex(ctx, start, prefixStart, end, sign) {
885
+ const { source } = ctx.scanner;
886
+ const digitsStart = prefixStart + 2;
887
+ let index = digitsStart;
888
+ while (index < end && isHexDigit(source.charCodeAt(index))) {
889
+ index += 1;
890
+ }
891
+ if (index !== end || index === digitsStart) {
892
+ return false;
893
+ }
894
+ const value = BigInt(source.slice(prefixStart, end));
895
+ ctx.write(sign);
896
+ ctx.write(value.toString());
897
+ ctx.record("normalized-number", start);
898
+ return true;
899
+ }
900
+
901
+ // src/parser/probe.ts
902
+ var PROBE_WINDOW = 256;
903
+ var SCAN_UNTERMINATED = -1;
904
+ var SCAN_BUDGET = -2;
905
+ function probeContinuation(ctx, from, slot) {
906
+ if (ctx.probeBudget <= 0) {
907
+ return true;
908
+ }
909
+ const { source, end } = ctx.scanner;
910
+ const window = end < from + PROBE_WINDOW ? end : from + PROBE_WINDOW;
911
+ const allowComments = ctx.options.allowComments;
912
+ const aggressive = ctx.options.aggressive;
913
+ const index = scanTrivia(source, from, window, allowComments);
914
+ ctx.probeBudget -= index - from + 1;
915
+ if (index >= end) {
916
+ return true;
917
+ }
918
+ if (index >= window) {
919
+ return true;
920
+ }
921
+ const code = source.charCodeAt(index);
922
+ switch (slot) {
923
+ case "object-key":
924
+ if (code === CH_COLON) {
925
+ return true;
926
+ }
927
+ return aggressive && (code === CH_COMMA || code === CH_CLOSE_BRACE);
928
+ case "object-value":
929
+ if (code === CH_CLOSE_BRACE || code === CH_CLOSE_BRACKET) {
930
+ return true;
931
+ }
932
+ if (code === CH_COMMA) {
933
+ return looksLikeMember(ctx, index + 1, window, allowComments);
934
+ }
935
+ if (isQuote(code) || isIdentifierStart(code)) {
936
+ return looksLikeMember(ctx, index, window, allowComments);
937
+ }
938
+ return false;
939
+ case "array-element":
940
+ case "document":
941
+ if (code === CH_CLOSE_BRACKET || code === CH_CLOSE_BRACE) {
942
+ return true;
943
+ }
944
+ if (code === CH_COMMA) {
945
+ return looksLikeElement(ctx, index + 1, window, allowComments);
946
+ }
947
+ if (isQuote(code)) {
948
+ return looksLikeElement(ctx, index, window, allowComments);
949
+ }
950
+ return false;
951
+ default:
952
+ return true;
953
+ }
954
+ }
955
+ function looksLikeMember(ctx, from, window, allowComments) {
956
+ const { source, end } = ctx.scanner;
957
+ const keyStart = scanTrivia(source, from, window, allowComments);
958
+ ctx.probeBudget -= keyStart - from + 1;
959
+ if (keyStart >= end || keyStart >= window) {
960
+ return true;
961
+ }
962
+ const code = source.charCodeAt(keyStart);
963
+ if (code === CH_CLOSE_BRACE || code === CH_COMMA) {
964
+ return true;
965
+ }
966
+ const keyEnd = scanKey(source, keyStart, window, end);
967
+ ctx.probeBudget -= (keyEnd < 0 ? window - keyStart : keyEnd - keyStart) + 1;
968
+ if (keyEnd === SCAN_BUDGET) {
969
+ return true;
970
+ }
971
+ if (keyEnd < 0) {
972
+ return false;
973
+ }
974
+ const afterKey = scanTrivia(source, keyEnd, window, allowComments);
975
+ if (afterKey >= end || afterKey >= window) {
976
+ return true;
977
+ }
978
+ return source.charCodeAt(afterKey) === CH_COLON;
979
+ }
980
+ function looksLikeElement(ctx, from, window, allowComments) {
981
+ const { source, end } = ctx.scanner;
982
+ const valueStart = scanTrivia(source, from, window, allowComments);
983
+ ctx.probeBudget -= valueStart - from + 1;
984
+ if (valueStart >= end || valueStart >= window) {
985
+ return true;
986
+ }
987
+ const code = source.charCodeAt(valueStart);
988
+ if (code === CH_CLOSE_BRACKET || code === CH_COMMA) {
989
+ return true;
990
+ }
991
+ const valueEnd = scanValueToken(source, valueStart, window, end);
992
+ ctx.probeBudget -= (valueEnd < 0 ? window - valueStart : valueEnd - valueStart) + 1;
993
+ if (valueEnd === SCAN_BUDGET) {
994
+ return true;
995
+ }
996
+ if (valueEnd < 0) {
997
+ return false;
998
+ }
999
+ const afterValue = scanTrivia(source, valueEnd, window, allowComments);
1000
+ if (afterValue >= end || afterValue >= window) {
1001
+ return true;
1002
+ }
1003
+ const next = source.charCodeAt(afterValue);
1004
+ return next === CH_COMMA || next === CH_CLOSE_BRACKET;
1005
+ }
1006
+ function scanKey(source, from, window, end) {
1007
+ const code = source.charCodeAt(from);
1008
+ if (isQuote(code)) {
1009
+ return scanString(source, from, window, end);
1010
+ }
1011
+ if (!isIdentifierStart(code)) {
1012
+ return SCAN_UNTERMINATED;
1013
+ }
1014
+ let index = from + 1;
1015
+ while (index < window && isIdentifierPart(source.charCodeAt(index))) {
1016
+ index += 1;
1017
+ }
1018
+ return index >= window && index < end ? SCAN_BUDGET : index;
1019
+ }
1020
+ function scanValueToken(source, from, window, end) {
1021
+ const code = source.charCodeAt(from);
1022
+ if (isQuote(code)) {
1023
+ return scanString(source, from, window, end);
1024
+ }
1025
+ if (code === CH_OPEN_BRACE || code === CH_OPEN_BRACKET) {
1026
+ return SCAN_BUDGET;
1027
+ }
1028
+ if (!isDigit(code) && !isIdentifierStart(code) && code !== CH_MINUS && code !== CH_PLUS) {
1029
+ return SCAN_UNTERMINATED;
1030
+ }
1031
+ let index = from + 1;
1032
+ while (index < window) {
1033
+ const next = source.charCodeAt(index);
1034
+ if (!isIdentifierPart(next) && !isDigit(next) && next !== CH_PLUS && next !== CH_DOT) {
1035
+ break;
1036
+ }
1037
+ index += 1;
1038
+ }
1039
+ return index >= window && index < end ? SCAN_BUDGET : index;
1040
+ }
1041
+ function scanString(source, from, window, end) {
1042
+ const opener = source.charCodeAt(from);
1043
+ const closers = closersFor(opener);
1044
+ let index = from + 1;
1045
+ while (index < window) {
1046
+ const code = source.charCodeAt(index);
1047
+ if (code === CH_BACKSLASH) {
1048
+ index += 2;
1049
+ continue;
1050
+ }
1051
+ if (code === closers[0] || code === closers[1]) {
1052
+ return index + 1;
1053
+ }
1054
+ index += 1;
1055
+ }
1056
+ return index >= end ? SCAN_UNTERMINATED : SCAN_BUDGET;
1057
+ }
1058
+
1059
+ // src/repair/quotes.ts
1060
+ var ESC_SHORT = 2;
1061
+ var ESC_UNICODE = 6;
1062
+ var ESC_DANGLING = -1;
1063
+ var ESC_TRUNCATED_UNICODE = -2;
1064
+ var ESC_BAD_UNICODE = -3;
1065
+ var ESC_QUOTE = -4;
1066
+ var ESC_CONTROL = -5;
1067
+ var ESC_UNKNOWN = -6;
1068
+ function readString(ctx, slot) {
1069
+ const { scanner } = ctx;
1070
+ const { source, end } = scanner;
1071
+ const start = scanner.index;
1072
+ const opener = source.charCodeAt(start);
1073
+ const standard = isStandardQuote(opener);
1074
+ const closers = closersFor(opener);
1075
+ const closerA = closers[0];
1076
+ const closerB = closers[1];
1077
+ if (!standard) {
1078
+ ctx.record("normalized-quotes", start);
1079
+ }
1080
+ ctx.write('"');
1081
+ let index = start + 1;
1082
+ let spanStart = index;
1083
+ let checkpoint;
1084
+ let reportedControlEnd = -1;
1085
+ while (index < end) {
1086
+ const code = source.charCodeAt(index);
1087
+ if (code === CH_BACKSLASH) {
1088
+ const kind = classifyEscape(source, index, end);
1089
+ if (kind >= 0) {
1090
+ index += kind;
1091
+ continue;
1092
+ }
1093
+ ctx.writeSpan(spanStart, index);
1094
+ switch (kind) {
1095
+ case ESC_DANGLING:
1096
+ ctx.record("fixed-escape", index, "Removed a trailing backslash with nothing to escape");
1097
+ index = end;
1098
+ break;
1099
+ case ESC_TRUNCATED_UNICODE:
1100
+ ctx.record("fixed-escape", index, "Removed a truncated \\u escape at end of input");
1101
+ index = end;
1102
+ break;
1103
+ case ESC_BAD_UNICODE:
1104
+ ctx.write("\\\\u");
1105
+ ctx.record("fixed-escape", index, "Escaped an incomplete \\u sequence");
1106
+ index += 2;
1107
+ break;
1108
+ case ESC_QUOTE: {
1109
+ const quoted = source.charCodeAt(index + 1);
1110
+ ctx.write(quoted === CH_DOUBLE_QUOTE ? '\\"' : source[index + 1]);
1111
+ ctx.record("fixed-escape", index, "Unescaped a quote that JSON does not escape");
1112
+ index += 2;
1113
+ break;
1114
+ }
1115
+ case ESC_CONTROL:
1116
+ ctx.write(escapeControl(source.charCodeAt(index + 1)));
1117
+ ctx.record("fixed-escape", index, "Replaced a backslash before a control character");
1118
+ index += 2;
1119
+ break;
1120
+ default:
1121
+ ctx.write("\\\\");
1122
+ ctx.write(source[index + 1]);
1123
+ ctx.record("fixed-escape", index, "Escaped a backslash that began an unknown sequence");
1124
+ index += 2;
1125
+ break;
1126
+ }
1127
+ spanStart = index;
1128
+ continue;
1129
+ }
1130
+ if (code === closerA || code === closerB) {
1131
+ if (probeContinuation(ctx, index + 1, slot)) {
1132
+ ctx.writeSpan(spanStart, index);
1133
+ ctx.write('"');
1134
+ scanner.index = index + 1;
1135
+ return;
1136
+ }
1137
+ if (standard && !ctx.options.aggressive) {
1138
+ if (checkpoint !== void 0 && ctx.canRewindTo(checkpoint)) {
1139
+ closeAtCheckpoint(ctx, checkpoint);
1140
+ return;
1141
+ }
1142
+ ctx.writeSpan(spanStart, index);
1143
+ ctx.write('"');
1144
+ scanner.index = index + 1;
1145
+ return;
1146
+ }
1147
+ ctx.writeSpan(spanStart, index);
1148
+ if (standard) {
1149
+ ctx.write('\\"');
1150
+ ctx.record("escaped-character", index, "Escaped a quote inside a string");
1151
+ } else {
1152
+ ctx.write(source[index]);
1153
+ }
1154
+ index += 1;
1155
+ spanStart = index;
1156
+ continue;
1157
+ }
1158
+ if (code < 32) {
1159
+ ctx.writeSpan(spanStart, index);
1160
+ if (code === CH_LINE_FEED || code === CH_CARRIAGE_RETURN) {
1161
+ checkpoint = ctx.checkpoint(index);
1162
+ }
1163
+ if (index !== reportedControlEnd) {
1164
+ ctx.record("escaped-character", index, "Escaped a raw control character in a string");
1165
+ }
1166
+ reportedControlEnd = index + 1;
1167
+ ctx.write(escapeControl(code));
1168
+ index += 1;
1169
+ spanStart = index;
1170
+ continue;
1171
+ }
1172
+ if (code === CH_DOUBLE_QUOTE) {
1173
+ ctx.writeSpan(spanStart, index);
1174
+ ctx.write('\\"');
1175
+ index += 1;
1176
+ spanStart = index;
1177
+ continue;
1178
+ }
1179
+ if (isHighSurrogate(code)) {
1180
+ if (index + 1 < end && isLowSurrogate(source.charCodeAt(index + 1))) {
1181
+ index += 2;
1182
+ continue;
1183
+ }
1184
+ index = escapeStrayUnit(ctx, spanStart, index, code);
1185
+ spanStart = index;
1186
+ continue;
1187
+ }
1188
+ if (isLowSurrogate(code)) {
1189
+ index = escapeStrayUnit(ctx, spanStart, index, code);
1190
+ spanStart = index;
1191
+ continue;
1192
+ }
1193
+ index += 1;
1194
+ }
1195
+ ctx.writeSpan(spanStart, index);
1196
+ if (checkpoint !== void 0 && ctx.canRewindTo(checkpoint) && tailIsStructural(source, checkpoint.index, end)) {
1197
+ closeAtCheckpoint(ctx, checkpoint);
1198
+ return;
1199
+ }
1200
+ ctx.write('"');
1201
+ ctx.record("terminated-string-at-eof", end);
1202
+ scanner.index = end;
1203
+ }
1204
+ function writeQuotedSpan(ctx, from, to) {
1205
+ const { source } = ctx.scanner;
1206
+ ctx.write('"');
1207
+ let spanStart = from;
1208
+ for (let index = from; index < to; index += 1) {
1209
+ const code = source.charCodeAt(index);
1210
+ let replacement;
1211
+ if (code === CH_DOUBLE_QUOTE) {
1212
+ replacement = '\\"';
1213
+ } else if (code === CH_BACKSLASH) {
1214
+ replacement = "\\\\";
1215
+ } else if (code < 32) {
1216
+ replacement = escapeControl(code);
1217
+ } else if (isHighSurrogate(code)) {
1218
+ if (index + 1 < to && isLowSurrogate(source.charCodeAt(index + 1))) {
1219
+ index += 1;
1220
+ continue;
1221
+ }
1222
+ replacement = escapeUnit(code);
1223
+ } else if (isLowSurrogate(code)) {
1224
+ replacement = escapeUnit(code);
1225
+ }
1226
+ if (replacement !== void 0) {
1227
+ ctx.writeSpan(spanStart, index);
1228
+ ctx.write(replacement);
1229
+ spanStart = index + 1;
1230
+ }
1231
+ }
1232
+ ctx.writeSpan(spanStart, to);
1233
+ ctx.write('"');
1234
+ }
1235
+ function escapeStrayUnit(ctx, spanStart, index, code) {
1236
+ ctx.writeSpan(spanStart, index);
1237
+ ctx.write(escapeUnit(code));
1238
+ ctx.record("escaped-character", index, "Escaped an unpaired surrogate");
1239
+ return index + 1;
1240
+ }
1241
+ function closeAtCheckpoint(ctx, checkpoint) {
1242
+ ctx.rewindTo(checkpoint);
1243
+ ctx.write('"');
1244
+ ctx.record("terminated-string-at-newline", checkpoint.index);
1245
+ }
1246
+ function classifyEscape(source, index, end) {
1247
+ if (index + 1 >= end) {
1248
+ return ESC_DANGLING;
1249
+ }
1250
+ const escaped = source.charCodeAt(index + 1);
1251
+ switch (escaped) {
1252
+ case CH_DOUBLE_QUOTE:
1253
+ case CH_BACKSLASH:
1254
+ case CH_SLASH:
1255
+ case CH_LOWER_B:
1256
+ case CH_LOWER_F:
1257
+ case CH_LOWER_N:
1258
+ case CH_LOWER_R:
1259
+ case CH_LOWER_T:
1260
+ return ESC_SHORT;
1261
+ case CH_LOWER_U:
1262
+ if (index + 5 < end && isHexDigit(source.charCodeAt(index + 2)) && isHexDigit(source.charCodeAt(index + 3)) && isHexDigit(source.charCodeAt(index + 4)) && isHexDigit(source.charCodeAt(index + 5))) {
1263
+ return ESC_UNICODE;
1264
+ }
1265
+ return index + 6 > end ? ESC_TRUNCATED_UNICODE : ESC_BAD_UNICODE;
1266
+ }
1267
+ if (isQuote(escaped)) {
1268
+ return ESC_QUOTE;
1269
+ }
1270
+ return escaped < 32 ? ESC_CONTROL : ESC_UNKNOWN;
1271
+ }
1272
+ function tailIsStructural(source, from, end) {
1273
+ for (let index = from; index < end; index += 1) {
1274
+ const code = source.charCodeAt(index);
1275
+ if (code === CH_CLOSE_BRACE || code === CH_CLOSE_BRACKET || code === CH_COMMA || isWhitespace(code)) {
1276
+ continue;
1277
+ }
1278
+ return false;
1279
+ }
1280
+ return true;
1281
+ }
1282
+ function escapeControl(code) {
1283
+ switch (code) {
1284
+ case 8:
1285
+ return "\\b";
1286
+ case 9:
1287
+ return "\\t";
1288
+ case 10:
1289
+ return "\\n";
1290
+ case 12:
1291
+ return "\\f";
1292
+ case 13:
1293
+ return "\\r";
1294
+ default:
1295
+ return escapeUnit(code);
1296
+ }
1297
+ }
1298
+ function escapeUnit(code) {
1299
+ return `\\u${code.toString(16).padStart(4, "0")}`;
1300
+ }
1301
+
1302
+ // src/repair/trailing-comma.ts
1303
+ function readSeparator(ctx) {
1304
+ skipTrivia(ctx);
1305
+ const code = ctx.scanner.peek();
1306
+ if (code === CH_COMMA) {
1307
+ const commaPosition = ctx.scanner.index;
1308
+ ctx.scanner.index += 1;
1309
+ skipTrivia(ctx);
1310
+ const next = ctx.scanner.peek();
1311
+ if (next === CH_CLOSE_BRACE || next === CH_CLOSE_BRACKET || next === CH_EOF) {
1312
+ if (!ctx.options.fixTrailingCommas) {
1313
+ ctx.fail("Trailing comma before the end of the container", commaPosition);
1314
+ }
1315
+ ctx.record("removed-trailing-comma", commaPosition);
1316
+ return "trailing-comma";
1317
+ }
1318
+ return "comma";
1319
+ }
1320
+ if (code === CH_CLOSE_BRACE || code === CH_CLOSE_BRACKET || code === CH_EOF) {
1321
+ return "end";
1322
+ }
1323
+ ctx.record("added-missing-comma", ctx.scanner.index);
1324
+ return "missing";
1325
+ }
1326
+
1327
+ // src/parser/parser.ts
1328
+ function parseDocument(ctx) {
1329
+ skipTrivia(ctx);
1330
+ if (ctx.scanner.eof()) {
1331
+ throw ctx.error("NO_JSON_FOUND", "No JSON value found", ctx.scanner.index);
1332
+ }
1333
+ if (parseValue(ctx, "document", false) === "dropped") {
1334
+ throw ctx.error("NO_JSON_FOUND", "No JSON value found", ctx.scanner.index);
1335
+ }
1336
+ }
1337
+ function parseValue(ctx, slot, mayDrop) {
1338
+ const { scanner } = ctx;
1339
+ for (; ; ) {
1340
+ skipTrivia(ctx);
1341
+ const start = scanner.index;
1342
+ const code = scanner.peek();
1343
+ if (code === CH_EOF) {
1344
+ return "dropped";
1345
+ }
1346
+ if (code === CH_OPEN_BRACE) {
1347
+ return parseObject(ctx, mayDrop);
1348
+ }
1349
+ if (code === CH_OPEN_BRACKET) {
1350
+ return parseArray(ctx, mayDrop);
1351
+ }
1352
+ if (isQuote(code)) {
1353
+ if (code !== CH_DOUBLE_QUOTE && !ctx.options.allowSingleQuotes) {
1354
+ ctx.fail("Non-standard string delimiter, and allowSingleQuotes is disabled", start);
1355
+ }
1356
+ readString(ctx, slot);
1357
+ return "ok";
1358
+ }
1359
+ if (code === CH_CLOSE_BRACE || code === CH_CLOSE_BRACKET || code === CH_COMMA || code === CH_COLON) {
1360
+ if (!ctx.options.aggressive) {
1361
+ ctx.refuse("Expected a value", start);
1362
+ }
1363
+ ctx.write("null");
1364
+ ctx.record("added-missing-value", start);
1365
+ return "ok";
1366
+ }
1367
+ const coreEnd = scanCoreToken(ctx, start);
1368
+ if (coreEnd > start) {
1369
+ return parseBareValue(ctx, start, coreEnd);
1370
+ }
1371
+ if (!ctx.options.aggressive) {
1372
+ ctx.fail(`Unexpected character ${describeChar(scanner.source, start)}`, start);
1373
+ }
1374
+ ctx.record("removed-stray-token", start);
1375
+ scanner.index = start + 1;
1376
+ }
1377
+ }
1378
+ function parseObject(ctx, mayDrop) {
1379
+ const { scanner } = ctx;
1380
+ const openPosition = scanner.index;
1381
+ const entry = ctx.checkpoint(openPosition);
1382
+ scanner.index += 1;
1383
+ ctx.enter(openPosition);
1384
+ ctx.openBraces += 1;
1385
+ ctx.write("{");
1386
+ let members = 0;
1387
+ let closed = false;
1388
+ let truncated = false;
1389
+ let previousIndex = -1;
1390
+ for (; ; ) {
1391
+ skipTrivia(ctx);
1392
+ if (scanner.index === previousIndex) {
1393
+ ctx.fail("Parser made no progress", scanner.index);
1394
+ }
1395
+ previousIndex = scanner.index;
1396
+ const code = scanner.peek();
1397
+ if (code === CH_EOF) {
1398
+ closeUnterminated(ctx, "object", scanner.index);
1399
+ truncated = true;
1400
+ break;
1401
+ }
1402
+ if (code === CH_CLOSE_BRACE) {
1403
+ scanner.index += 1;
1404
+ closed = true;
1405
+ break;
1406
+ }
1407
+ if (code === CH_CLOSE_BRACKET) {
1408
+ if (ctx.openBrackets > 0) {
1409
+ closeUnterminated(ctx, "object", scanner.index);
1410
+ break;
1411
+ }
1412
+ dropStrayCloser(ctx);
1413
+ continue;
1414
+ }
1415
+ if (code === CH_COMMA) {
1416
+ ctx.record("removed-extra-comma", scanner.index);
1417
+ scanner.index += 1;
1418
+ continue;
1419
+ }
1420
+ if (!canStartKey(ctx, code)) {
1421
+ if (!ctx.options.aggressive) {
1422
+ ctx.fail(
1423
+ `Expected an object key, found ${describeChar(scanner.source, scanner.index)}`,
1424
+ scanner.index
1425
+ );
1426
+ }
1427
+ ctx.record("removed-stray-token", scanner.index);
1428
+ scanner.index += 1;
1429
+ continue;
1430
+ }
1431
+ const slot = ctx.checkpoint(scanner.index);
1432
+ if (members > 0) {
1433
+ ctx.write(",");
1434
+ }
1435
+ if (parseMember(ctx) === "dropped") {
1436
+ ctx.restore(slot);
1437
+ scanner.index = scanner.end;
1438
+ ctx.record("removed-incomplete-member", slot.index);
1439
+ closeUnterminated(ctx, "object", slot.index);
1440
+ truncated = true;
1441
+ break;
1442
+ }
1443
+ members += 1;
1444
+ readSeparator(ctx);
1445
+ }
1446
+ ctx.write("}");
1447
+ ctx.openBraces -= 1;
1448
+ ctx.leave();
1449
+ if (mayDrop && truncated && !closed && members === 0) {
1450
+ ctx.restore(entry);
1451
+ scanner.index = scanner.end;
1452
+ return "dropped";
1453
+ }
1454
+ return "ok";
1455
+ }
1456
+ function parseMember(ctx) {
1457
+ const { scanner } = ctx;
1458
+ parseKey(ctx);
1459
+ skipTrivia(ctx);
1460
+ const afterKey = scanner.index;
1461
+ const next = scanner.peek();
1462
+ if (next === CH_EOF) {
1463
+ return "dropped";
1464
+ }
1465
+ if (next === CH_COLON) {
1466
+ scanner.index += 1;
1467
+ } else if (next === CH_CLOSE_BRACE || next === CH_COMMA) {
1468
+ if (!ctx.options.aggressive) {
1469
+ ctx.refuse("Object key has no value", afterKey);
1470
+ }
1471
+ ctx.record("added-missing-colon", afterKey);
1472
+ ctx.write(":null");
1473
+ ctx.record("added-missing-value", afterKey);
1474
+ return "ok";
1475
+ } else if (isQuote(next)) {
1476
+ if (!ctx.options.aggressive) {
1477
+ ctx.refuse('Expected ":" between key and value', afterKey);
1478
+ }
1479
+ ctx.record("added-missing-colon", afterKey);
1480
+ } else {
1481
+ ctx.record("added-missing-colon", afterKey);
1482
+ }
1483
+ ctx.write(":");
1484
+ return parseValue(ctx, "object-value", true);
1485
+ }
1486
+ function parseKey(ctx) {
1487
+ const { scanner } = ctx;
1488
+ const { source, end } = scanner;
1489
+ const start = scanner.index;
1490
+ const code = scanner.peek();
1491
+ if (isQuote(code)) {
1492
+ readString(ctx, "object-key");
1493
+ return;
1494
+ }
1495
+ let index = start;
1496
+ if (isIdentifierStart(code)) {
1497
+ index += 1;
1498
+ while (index < end && isIdentifierPart(source.charCodeAt(index))) {
1499
+ index += 1;
1500
+ }
1501
+ } else {
1502
+ index = scanBareKey(ctx, start);
1503
+ }
1504
+ writeQuotedSpan(ctx, start, index);
1505
+ ctx.record("quoted-key", start);
1506
+ scanner.index = index;
1507
+ }
1508
+ function canStartKey(ctx, code) {
1509
+ if (isQuote(code)) {
1510
+ return code === CH_DOUBLE_QUOTE || ctx.options.allowSingleQuotes;
1511
+ }
1512
+ if (!ctx.options.allowUnquotedKeys) {
1513
+ return false;
1514
+ }
1515
+ if (isIdentifierStart(code)) {
1516
+ return true;
1517
+ }
1518
+ return ctx.options.aggressive && !isStructural(code);
1519
+ }
1520
+ function parseArray(ctx, mayDrop) {
1521
+ const { scanner } = ctx;
1522
+ const openPosition = scanner.index;
1523
+ const entry = ctx.checkpoint(openPosition);
1524
+ scanner.index += 1;
1525
+ ctx.enter(openPosition);
1526
+ ctx.openBrackets += 1;
1527
+ ctx.write("[");
1528
+ let elements = 0;
1529
+ let closed = false;
1530
+ let truncated = false;
1531
+ let previousIndex = -1;
1532
+ for (; ; ) {
1533
+ skipTrivia(ctx);
1534
+ if (scanner.index === previousIndex) {
1535
+ ctx.fail("Parser made no progress", scanner.index);
1536
+ }
1537
+ previousIndex = scanner.index;
1538
+ const code = scanner.peek();
1539
+ if (code === CH_EOF) {
1540
+ closeUnterminated(ctx, "array", scanner.index);
1541
+ truncated = true;
1542
+ break;
1543
+ }
1544
+ if (code === CH_CLOSE_BRACKET) {
1545
+ scanner.index += 1;
1546
+ closed = true;
1547
+ break;
1548
+ }
1549
+ if (code === CH_CLOSE_BRACE) {
1550
+ if (ctx.openBraces > 0) {
1551
+ closeUnterminated(ctx, "array", scanner.index);
1552
+ break;
1553
+ }
1554
+ dropStrayCloser(ctx);
1555
+ continue;
1556
+ }
1557
+ if (code === CH_COMMA) {
1558
+ if (!ctx.options.aggressive) {
1559
+ ctx.refuse("Array element is missing", scanner.index);
1560
+ }
1561
+ if (elements > 0) {
1562
+ ctx.write(",");
1563
+ }
1564
+ ctx.write("null");
1565
+ ctx.record("added-missing-value", scanner.index);
1566
+ elements += 1;
1567
+ scanner.index += 1;
1568
+ continue;
1569
+ }
1570
+ const slot = ctx.checkpoint(scanner.index);
1571
+ if (elements > 0) {
1572
+ ctx.write(",");
1573
+ }
1574
+ if (parseValue(ctx, "array-element", true) === "dropped") {
1575
+ ctx.restore(slot);
1576
+ scanner.index = scanner.end;
1577
+ ctx.record("removed-incomplete-member", slot.index);
1578
+ closeUnterminated(ctx, "array", slot.index);
1579
+ truncated = true;
1580
+ break;
1581
+ }
1582
+ elements += 1;
1583
+ readSeparator(ctx);
1584
+ }
1585
+ ctx.write("]");
1586
+ ctx.openBrackets -= 1;
1587
+ ctx.leave();
1588
+ if (mayDrop && truncated && !closed && elements === 0) {
1589
+ ctx.restore(entry);
1590
+ scanner.index = scanner.end;
1591
+ return "dropped";
1592
+ }
1593
+ return "ok";
1594
+ }
1595
+ function parseBareValue(ctx, start, coreEnd) {
1596
+ const { scanner } = ctx;
1597
+ const literal = readLiteral(ctx, start, coreEnd);
1598
+ if (literal === "emitted") {
1599
+ scanner.index = coreEnd;
1600
+ return "ok";
1601
+ }
1602
+ if (literal === "needs-aggressive") {
1603
+ ctx.refuse(`"${scanner.slice(start, coreEnd)}" is not a JSON literal`, start);
1604
+ }
1605
+ if (readNumber(ctx, start, coreEnd)) {
1606
+ scanner.index = coreEnd;
1607
+ return "ok";
1608
+ }
1609
+ if (!ctx.options.aggressive) {
1610
+ ctx.refuse(`Unquoted value "${scanner.slice(start, coreEnd)}"`, start);
1611
+ }
1612
+ const valueEnd = extendBareValue(ctx, coreEnd);
1613
+ writeQuotedSpan(ctx, start, valueEnd);
1614
+ ctx.record("quoted-value", start);
1615
+ scanner.index = valueEnd;
1616
+ return "ok";
1617
+ }
1618
+ function scanCoreToken(ctx, from) {
1619
+ const { source, end } = ctx.scanner;
1620
+ const allowComments = ctx.options.allowComments;
1621
+ let index = from;
1622
+ while (index < end) {
1623
+ const code = source.charCodeAt(index);
1624
+ if (isWhitespace(code) || isStructural(code) || isQuote(code) || startsComment(source, index, end, allowComments)) {
1625
+ break;
1626
+ }
1627
+ index += 1;
1628
+ }
1629
+ return index;
1630
+ }
1631
+ function extendBareValue(ctx, from) {
1632
+ const { source, end } = ctx.scanner;
1633
+ const allowComments = ctx.options.allowComments;
1634
+ let index = from;
1635
+ let lastContent = from;
1636
+ while (index < end) {
1637
+ const code = source.charCodeAt(index);
1638
+ if (isStructural(code) || startsComment(source, index, end, allowComments)) {
1639
+ break;
1640
+ }
1641
+ if (!isWhitespace(code)) {
1642
+ lastContent = index + 1;
1643
+ }
1644
+ index += 1;
1645
+ }
1646
+ return lastContent;
1647
+ }
1648
+ function scanBareKey(ctx, from) {
1649
+ const { source, end } = ctx.scanner;
1650
+ const allowComments = ctx.options.allowComments;
1651
+ let index = from;
1652
+ let lastContent = from + 1;
1653
+ while (index < end) {
1654
+ const code = source.charCodeAt(index);
1655
+ if (code === CH_COLON || isStructural(code) || startsComment(source, index, end, allowComments)) {
1656
+ break;
1657
+ }
1658
+ if (!isWhitespace(code)) {
1659
+ lastContent = index + 1;
1660
+ }
1661
+ index += 1;
1662
+ }
1663
+ return lastContent;
1664
+ }
1665
+ function startsComment(source, index, end, allowComments) {
1666
+ if (!allowComments) {
1667
+ return false;
1668
+ }
1669
+ const code = source.charCodeAt(index);
1670
+ if (code === CH_HASH) {
1671
+ return true;
1672
+ }
1673
+ if (code !== CH_SLASH || index + 1 >= end) {
1674
+ return false;
1675
+ }
1676
+ const next = source.charCodeAt(index + 1);
1677
+ return next === CH_SLASH || next === CH_ASTERISK;
1678
+ }
1679
+ function describeChar(source, index) {
1680
+ const code = source.charCodeAt(index);
1681
+ if (Number.isNaN(code)) {
1682
+ return "end of input";
1683
+ }
1684
+ if (code < 32 || code === 127) {
1685
+ return `"\\u${code.toString(16).padStart(4, "0")}"`;
1686
+ }
1687
+ return `"${source.charAt(index)}"`;
1688
+ }
1689
+
1690
+ // src/repair/messages.ts
1691
+ var MESSAGES = {
1692
+ "removed-byte-order-mark": "Removed a leading byte order mark",
1693
+ "extracted-json": "Extracted JSON from surrounding text",
1694
+ "removed-comment": "Removed a comment",
1695
+ "normalized-whitespace": "Replaced whitespace that JSON does not allow between tokens",
1696
+ "removed-trailing-comma": "Removed a trailing comma",
1697
+ "removed-extra-comma": "Removed a redundant comma",
1698
+ "removed-stray-token": "Removed a closing bracket that matched nothing",
1699
+ "removed-trailing-content": "Removed content after the end of the JSON value",
1700
+ "removed-incomplete-member": "Removed a truncated final member",
1701
+ "added-missing-comma": "Inserted a missing comma",
1702
+ "added-missing-colon": "Inserted a missing colon",
1703
+ "added-missing-value": "Inserted null for a missing value",
1704
+ "added-closing-brace": "Closed an unterminated object",
1705
+ "added-closing-bracket": "Closed an unterminated array",
1706
+ "normalized-quotes": "Converted non-standard quotes to double quotes",
1707
+ "normalized-literal": "Rewrote a non-JSON literal",
1708
+ "normalized-number": "Rewrote a non-JSON number",
1709
+ "quoted-key": "Quoted an unquoted key",
1710
+ "quoted-value": "Quoted an unquoted value",
1711
+ "escaped-character": "Escaped a character that JSON forbids raw",
1712
+ "fixed-escape": "Corrected an invalid escape sequence",
1713
+ "terminated-string-at-eof": "Closed a string left open at end of input",
1714
+ "terminated-string-at-newline": "Closed a string at a line break, discarding the rest of the line"
1715
+ };
1716
+ function describeRepair(type) {
1717
+ return MESSAGES[type];
1718
+ }
1719
+
1720
+ // src/parser/state.ts
1721
+ var PROBE_BUDGET_FACTOR = 4;
1722
+ var RepairContext = class {
1723
+ scanner;
1724
+ options;
1725
+ /** Repairs applied so far, in the order they were discovered. */
1726
+ repairs = [];
1727
+ /** Pieces of the output, joined once at the end. */
1728
+ chunks = [];
1729
+ /** Current nesting depth. */
1730
+ depth = 0;
1731
+ /** Open `{` containers, used for O(1) stray-closer classification. */
1732
+ openBraces = 0;
1733
+ /** Open `[` containers, used for O(1) stray-closer classification. */
1734
+ openBrackets = 0;
1735
+ /** Remaining lookahead budget; see {@link PROBE_BUDGET_FACTOR}. */
1736
+ probeBudget;
1737
+ /**
1738
+ * Highest offset the parser has already rewound to.
1739
+ *
1740
+ * Rewinding is only ever allowed to move forward, which guarantees the parse
1741
+ * cannot loop between two readings of the same unterminated string.
1742
+ */
1743
+ lastRewind = -1;
1744
+ constructor(scanner, options) {
1745
+ this.scanner = scanner;
1746
+ this.options = options;
1747
+ this.probeBudget = (scanner.end - scanner.start) * PROBE_BUDGET_FACTOR;
1748
+ }
1749
+ /* ── Output ───────────────────────────────────────────────────────────── */
1750
+ /** Appends literal text to the output. */
1751
+ write(text) {
1752
+ this.chunks.push(text);
1753
+ }
1754
+ /**
1755
+ * Appends a span of the original input verbatim.
1756
+ *
1757
+ * Copying spans rather than characters keeps the number of output chunks
1758
+ * proportional to the number of tokens, not to the length of the input.
1759
+ */
1760
+ writeSpan(from, to) {
1761
+ if (to > from) {
1762
+ this.chunks.push(this.scanner.slice(from, to));
1763
+ }
1764
+ }
1765
+ /** Concatenates everything written so far. */
1766
+ output() {
1767
+ return this.chunks.join("");
1768
+ }
1769
+ /* ── Checkpoints ──────────────────────────────────────────────────────── */
1770
+ /** Captures the current output, repair log and cursor for a later rewind. */
1771
+ checkpoint(index) {
1772
+ return { index, chunkCount: this.chunks.length, repairCount: this.repairs.length };
1773
+ }
1774
+ /**
1775
+ * Reports whether the parser may rewind to `checkpoint`.
1776
+ *
1777
+ * Refusing a backwards rewind is what makes the unterminated-string heuristic
1778
+ * terminating: every rewind strictly advances the furthest point already
1779
+ * reconsidered.
1780
+ */
1781
+ canRewindTo(checkpoint) {
1782
+ return checkpoint.index > this.lastRewind;
1783
+ }
1784
+ /**
1785
+ * Returns the parser to `checkpoint`, discarding the output written and the
1786
+ * repairs recorded since it was taken, and marks the point as reconsidered.
1787
+ *
1788
+ * Truncating the repair log matters: without it, a rewound attempt would
1789
+ * report repairs that are not present in the text it finally emits.
1790
+ */
1791
+ rewindTo(checkpoint) {
1792
+ this.restore(checkpoint);
1793
+ this.lastRewind = checkpoint.index;
1794
+ }
1795
+ /**
1796
+ * Returns the parser to `checkpoint` without marking it as reconsidered.
1797
+ *
1798
+ * Used to un-write a construct the parser decided to drop rather than re-read
1799
+ * — a member truncated by a cut-off stream, for instance. Unlike
1800
+ * {@link rewindTo} these restores never re-scan the same text, so they cannot
1801
+ * loop and must not consume the rewind budget.
1802
+ */
1803
+ restore(checkpoint) {
1804
+ this.chunks.length = checkpoint.chunkCount;
1805
+ this.repairs.length = checkpoint.repairCount;
1806
+ this.scanner.index = checkpoint.index;
1807
+ }
1808
+ /**
1809
+ * Absolute offset of the last non-whitespace character in the region, or
1810
+ * `-1` if there is none.
1811
+ *
1812
+ * Computed once per parse. Recomputing it per token would make input such as
1813
+ * half a megabyte of `}` quadratic, which is precisely the shape an attacker
1814
+ * would send.
1815
+ */
1816
+ get lastContentIndex() {
1817
+ if (this.lastContentCache === -2) {
1818
+ this.lastContentCache = findLastContent(
1819
+ this.scanner.source,
1820
+ this.scanner.start,
1821
+ this.scanner.end
1822
+ );
1823
+ }
1824
+ return this.lastContentCache;
1825
+ }
1826
+ lastContentCache = -2;
1827
+ /* ── Repair log ───────────────────────────────────────────────────────── */
1828
+ /**
1829
+ * Records a repair at an absolute offset in the original input.
1830
+ *
1831
+ * @throws {JsonRepairError} `MAX_REPAIRS_EXCEEDED` once the budget is spent.
1832
+ */
1833
+ record(type, position, message) {
1834
+ if (this.repairs.length >= this.options.maxRepairs) {
1835
+ throw this.error(
1836
+ "MAX_REPAIRS_EXCEEDED",
1837
+ `Input needs more than ${this.options.maxRepairs} repairs.`,
1838
+ position
1839
+ );
1840
+ }
1841
+ const { line, column } = this.scanner.positionAt(position);
1842
+ this.repairs.push({
1843
+ type,
1844
+ position,
1845
+ line,
1846
+ column,
1847
+ message: message ?? describeRepair(type)
1848
+ });
1849
+ }
1850
+ /* ── Depth ────────────────────────────────────────────────────────────── */
1851
+ /**
1852
+ * Enters a nested container.
1853
+ *
1854
+ * @throws {JsonRepairError} `MAX_DEPTH_EXCEEDED` beyond the configured depth.
1855
+ */
1856
+ enter(position) {
1857
+ this.depth += 1;
1858
+ if (this.depth > this.options.maxDepth) {
1859
+ throw this.error(
1860
+ "MAX_DEPTH_EXCEEDED",
1861
+ `Input nests deeper than the configured maximum of ${this.options.maxDepth}.`,
1862
+ position
1863
+ );
1864
+ }
1865
+ }
1866
+ /** Leaves a nested container. */
1867
+ leave() {
1868
+ this.depth -= 1;
1869
+ }
1870
+ /* ── Failure ──────────────────────────────────────────────────────────── */
1871
+ /** Builds a positioned error without throwing it. */
1872
+ error(code, message, position) {
1873
+ const { line, column } = this.scanner.positionAt(position);
1874
+ return new JsonRepairError(code, `${message} (line ${line}, column ${column})`, {
1875
+ position,
1876
+ line,
1877
+ column,
1878
+ snippet: buildSnippet(this.scanner.source, position)
1879
+ });
1880
+ }
1881
+ /**
1882
+ * Aborts the attempt because the input cannot be read as JSON at all.
1883
+ *
1884
+ * @throws {JsonRepairError} `UNREPAIRABLE_JSON`.
1885
+ */
1886
+ fail(message, position) {
1887
+ throw this.error("UNREPAIRABLE_JSON", message, position);
1888
+ }
1889
+ /**
1890
+ * Aborts the attempt because safe mode declines to choose between readings.
1891
+ *
1892
+ * Callers reach this only when `mode: 'aggressive'` has a defined answer, so
1893
+ * the message always points at the escape hatch.
1894
+ *
1895
+ * @throws {JsonRepairError} `AMBIGUOUS_REPAIR`.
1896
+ */
1897
+ refuse(message, position) {
1898
+ throw this.error(
1899
+ "AMBIGUOUS_REPAIR",
1900
+ `${message}; retry with mode: "aggressive" to apply a best-effort repair.`,
1901
+ position
1902
+ );
1903
+ }
1904
+ };
1905
+ function findLastContent(source, start, end) {
1906
+ for (let index = end - 1; index >= start; index -= 1) {
1907
+ if (!isWhitespace(source.charCodeAt(index))) {
1908
+ return index;
1909
+ }
1910
+ }
1911
+ return -1;
1912
+ }
1913
+
1914
+ // src/repair/markdown.ts
1915
+ var CH_TILDE = 126;
1916
+ var MIN_FENCE_LENGTH = 3;
1917
+ var MAX_FENCE_INDENT = 3;
1918
+ function scanFences(source, start, end) {
1919
+ const fences = [];
1920
+ let lineStart = start;
1921
+ while (lineStart < end) {
1922
+ const lineEnd = findLineEnd(source, lineStart, end);
1923
+ const marker = readFenceMarker(source, lineStart, lineEnd);
1924
+ if (marker === void 0) {
1925
+ lineStart = nextLineStart(source, lineEnd, end);
1926
+ continue;
1927
+ }
1928
+ const bodyStart = nextLineStart(source, lineEnd, end);
1929
+ const closing = findClosingFence(source, bodyStart, end, marker.char, marker.length);
1930
+ fences.push({
1931
+ bodyStart,
1932
+ bodyEnd: closing.bodyEnd,
1933
+ jsonTagged: isJsonInfoString(source, marker.infoStart, lineEnd)
1934
+ });
1935
+ lineStart = closing.next;
1936
+ }
1937
+ return fences;
1938
+ }
1939
+ function findValueStarts(source, start, end, limit) {
1940
+ const starts = [];
1941
+ for (let index = start; index < end && starts.length < limit; index += 1) {
1942
+ const code = source.charCodeAt(index);
1943
+ if (code === CH_OPEN_BRACE || code === CH_OPEN_BRACKET) {
1944
+ starts.push(index);
1945
+ }
1946
+ }
1947
+ return starts;
1948
+ }
1949
+ function skipLeadingWhitespace(source, start, end) {
1950
+ let index = start;
1951
+ while (index < end && isWhitespace(source.charCodeAt(index))) {
1952
+ index += 1;
1953
+ }
1954
+ return index;
1955
+ }
1956
+ function trimTrailingWhitespace(source, start, end) {
1957
+ let index = end;
1958
+ while (index > start && isWhitespace(source.charCodeAt(index - 1))) {
1959
+ index -= 1;
1960
+ }
1961
+ return index;
1962
+ }
1963
+ function readFenceMarker(source, lineStart, lineEnd) {
1964
+ let index = lineStart;
1965
+ let indent = 0;
1966
+ while (index < lineEnd && indent <= MAX_FENCE_INDENT) {
1967
+ const code = source.charCodeAt(index);
1968
+ if (code !== CH_SPACE && code !== CH_TAB) {
1969
+ break;
1970
+ }
1971
+ indent += 1;
1972
+ index += 1;
1973
+ }
1974
+ if (index >= lineEnd || indent > MAX_FENCE_INDENT) {
1975
+ return void 0;
1976
+ }
1977
+ const char = source.charCodeAt(index);
1978
+ if (char !== CH_BACKTICK && char !== CH_TILDE) {
1979
+ return void 0;
1980
+ }
1981
+ const runStart = index;
1982
+ while (index < lineEnd && source.charCodeAt(index) === char) {
1983
+ index += 1;
1984
+ }
1985
+ const length = index - runStart;
1986
+ return length >= MIN_FENCE_LENGTH ? { char, length, infoStart: index } : void 0;
1987
+ }
1988
+ function findClosingFence(source, bodyStart, end, char, length) {
1989
+ let lineStart = bodyStart;
1990
+ while (lineStart < end) {
1991
+ const lineEnd = findLineEnd(source, lineStart, end);
1992
+ const marker = readFenceMarker(source, lineStart, lineEnd);
1993
+ if (marker?.char === char && marker.length >= length) {
1994
+ return { bodyEnd: lineStart, next: nextLineStart(source, lineEnd, end) };
1995
+ }
1996
+ lineStart = nextLineStart(source, lineEnd, end);
1997
+ }
1998
+ return { bodyEnd: end, next: end };
1999
+ }
2000
+ function isJsonInfoString(source, from, lineEnd) {
2001
+ let index = from;
2002
+ while (index < lineEnd && isWhitespace(source.charCodeAt(index))) {
2003
+ index += 1;
2004
+ }
2005
+ const wordStart = index;
2006
+ while (index < lineEnd) {
2007
+ const code = toLowerAscii(source.charCodeAt(index));
2008
+ if (code < 48 || code > 57 && code < 97 || code > 122) {
2009
+ break;
2010
+ }
2011
+ index += 1;
2012
+ }
2013
+ const length = index - wordStart;
2014
+ if (length < 4 || length > 5) {
2015
+ return false;
2016
+ }
2017
+ if (toLowerAscii(source.charCodeAt(wordStart)) !== 106 || toLowerAscii(source.charCodeAt(wordStart + 1)) !== 115 || toLowerAscii(source.charCodeAt(wordStart + 2)) !== 111 || toLowerAscii(source.charCodeAt(wordStart + 3)) !== 110) {
2018
+ return false;
2019
+ }
2020
+ if (length === 4) {
2021
+ return true;
2022
+ }
2023
+ const suffix = toLowerAscii(source.charCodeAt(wordStart + 4));
2024
+ return suffix === 99 || suffix === 53;
2025
+ }
2026
+ function findLineEnd(source, from, end) {
2027
+ let index = from;
2028
+ while (index < end && !isLineBreak(source.charCodeAt(index))) {
2029
+ index += 1;
2030
+ }
2031
+ return index;
2032
+ }
2033
+ function nextLineStart(source, lineEnd, end) {
2034
+ if (lineEnd >= end) {
2035
+ return end;
2036
+ }
2037
+ let index = lineEnd + 1;
2038
+ if (source.charCodeAt(lineEnd) !== CH_LINE_FEED && index < end && source.charCodeAt(index) === CH_LINE_FEED) {
2039
+ index += 1;
2040
+ }
2041
+ return index;
2042
+ }
2043
+
2044
+ // src/extract.ts
2045
+ var MAX_CANDIDATES = 256;
2046
+ var MAX_FAILED_ATTEMPTS = 32;
2047
+ function extractJson(input, options) {
2048
+ const resolved = resolveExtractOptions(options);
2049
+ const region = prepare(input, resolved);
2050
+ const attempts = solve(input, region.start, region.end, resolved);
2051
+ const chosen = rankAttempts(attempts, resolved.select);
2052
+ return input.slice(chosen.start, chosen.end);
2053
+ }
2054
+ function extractAllJson(input, options) {
2055
+ const resolved = resolveExtractOptions(options);
2056
+ const region = prepare(input, resolved);
2057
+ let attempts;
2058
+ try {
2059
+ attempts = solve(input, region.start, region.end, resolved);
2060
+ } catch (error) {
2061
+ if (isJsonRepairError(error) && error.code === "NO_JSON_FOUND") {
2062
+ return [];
2063
+ }
2064
+ throw error;
2065
+ }
2066
+ return attempts.map((attempt) => input.slice(attempt.start, attempt.end));
2067
+ }
2068
+ function prepare(input, options) {
2069
+ assertString(input, "input");
2070
+ assertInputLength(input, options.maxLength);
2071
+ return {
2072
+ start: input.length > 0 && input.charCodeAt(0) === CH_BOM ? 1 : 0,
2073
+ end: input.length
2074
+ };
2075
+ }
2076
+ function solve(source, start, end, options) {
2077
+ const tiers = buildCandidateTiers(source, start, end, options);
2078
+ const safe = collectAttempts(source, tiers, withMode(options, "safe"));
2079
+ if (safe.attempts.length > 0) {
2080
+ return safe.attempts;
2081
+ }
2082
+ if (!options.aggressive) {
2083
+ throw safe.error ?? noJsonFound(source, start);
2084
+ }
2085
+ const loose = collectAttempts(source, tiers, options);
2086
+ if (loose.attempts.length > 0) {
2087
+ return loose.attempts;
2088
+ }
2089
+ throw loose.error ?? safe.error ?? noJsonFound(source, start);
2090
+ }
2091
+ function buildCandidateTiers(source, start, end, options) {
2092
+ const whole = wholeRegionCandidate(source, start, end);
2093
+ if (!options.extract) {
2094
+ return whole === void 0 ? [] : [[whole]];
2095
+ }
2096
+ const tagged = [];
2097
+ const fenced = [];
2098
+ for (const fence of scanFences(source, start, end)) {
2099
+ const target = fence.jsonTagged ? tagged : fenced;
2100
+ const starts = findValueStarts(source, fence.bodyStart, fence.bodyEnd, MAX_CANDIDATES);
2101
+ for (const offset of starts) {
2102
+ target.push({ start: offset, limit: fence.bodyEnd });
2103
+ }
2104
+ if (starts.length === 0) {
2105
+ const body = wholeRegionCandidate(source, fence.bodyStart, fence.bodyEnd);
2106
+ if (body !== void 0) {
2107
+ target.push(body);
2108
+ }
2109
+ }
2110
+ }
2111
+ const bare = findValueStarts(source, start, end, MAX_CANDIDATES).map((offset) => ({
2112
+ start: offset,
2113
+ limit: end
2114
+ }));
2115
+ const tiers = [];
2116
+ for (const tier of [tagged, fenced, bare]) {
2117
+ if (tier.length > 0) {
2118
+ tiers.push(tier);
2119
+ }
2120
+ }
2121
+ if (whole !== void 0 && startsJsonValue(source, whole.start, end)) {
2122
+ tiers.push([whole]);
2123
+ }
2124
+ return tiers;
2125
+ }
2126
+ var VALUE_WORDS = ["true", "false", "null", "nan", "none", "infinity", "undefined"];
2127
+ function startsJsonValue(source, from, end) {
2128
+ const code = source.charCodeAt(from);
2129
+ if (code === CH_OPEN_BRACE || code === CH_OPEN_BRACKET || code === CH_MINUS || code === CH_PLUS || code === CH_DOT || isDigit(code) || isQuote(code)) {
2130
+ return true;
2131
+ }
2132
+ return VALUE_WORDS.some((word) => matchesWord2(source, from, end, word));
2133
+ }
2134
+ function matchesWord2(source, from, end, word) {
2135
+ if (from + word.length > end) {
2136
+ return false;
2137
+ }
2138
+ for (let index = 0; index < word.length; index += 1) {
2139
+ if (toLowerAscii(source.charCodeAt(from + index)) !== word.charCodeAt(index)) {
2140
+ return false;
2141
+ }
2142
+ }
2143
+ return true;
2144
+ }
2145
+ function collectAttempts(source, tiers, options) {
2146
+ let firstError;
2147
+ let failures = 0;
2148
+ for (const tier of tiers) {
2149
+ const attempts = [];
2150
+ let barrier = -1;
2151
+ for (const candidate of tier) {
2152
+ if (candidate.start < barrier) {
2153
+ continue;
2154
+ }
2155
+ if (failures >= MAX_FAILED_ATTEMPTS) {
2156
+ break;
2157
+ }
2158
+ let attempt;
2159
+ try {
2160
+ attempt = tryCandidate(source, candidate, options);
2161
+ } catch (error) {
2162
+ if (!isJsonRepairError(error)) {
2163
+ throw error;
2164
+ }
2165
+ failures += 1;
2166
+ firstError ??= error;
2167
+ barrier = Math.max(barrier, findStructuralEnd(source, candidate.start, candidate.limit));
2168
+ continue;
2169
+ }
2170
+ attempts.push(attempt);
2171
+ barrier = Math.max(barrier, attempt.end);
2172
+ }
2173
+ if (attempts.length > 0) {
2174
+ return { attempts, error: void 0 };
2175
+ }
2176
+ }
2177
+ return { attempts: [], error: firstError };
2178
+ }
2179
+ function findStructuralEnd(source, start, end) {
2180
+ const open = [];
2181
+ for (let index = start; index < end; index += 1) {
2182
+ const code = source.charCodeAt(index);
2183
+ if (isQuote(code)) {
2184
+ index += 1;
2185
+ while (index < end) {
2186
+ const inner = source.charCodeAt(index);
2187
+ if (inner === CH_BACKSLASH) {
2188
+ index += 1;
2189
+ } else if (inner === code) {
2190
+ break;
2191
+ }
2192
+ index += 1;
2193
+ }
2194
+ continue;
2195
+ }
2196
+ if (code === CH_OPEN_BRACE || code === CH_OPEN_BRACKET) {
2197
+ open.push(code);
2198
+ continue;
2199
+ }
2200
+ if (code !== CH_CLOSE_BRACE && code !== CH_CLOSE_BRACKET) {
2201
+ continue;
2202
+ }
2203
+ const opener = code === CH_CLOSE_BRACE ? CH_OPEN_BRACE : CH_OPEN_BRACKET;
2204
+ const frame = open.lastIndexOf(opener);
2205
+ if (frame < 0) {
2206
+ continue;
2207
+ }
2208
+ open.length = frame;
2209
+ if (open.length === 0) {
2210
+ return index + 1;
2211
+ }
2212
+ }
2213
+ return end;
2214
+ }
2215
+ function tryCandidate(source, candidate, options) {
2216
+ const scanner = new Scanner(source, candidate.start, candidate.limit);
2217
+ const context = new RepairContext(scanner, options);
2218
+ parseDocument(context);
2219
+ const end = scanner.index;
2220
+ const { repairs } = context;
2221
+ if (repairs.length === 0) {
2222
+ const slice = source.slice(candidate.start, end);
2223
+ const parsed2 = tryParse(slice);
2224
+ if (parsed2 !== void 0) {
2225
+ return { start: candidate.start, end, json: slice, value: parsed2.value, repairs };
2226
+ }
2227
+ }
2228
+ const json = context.output();
2229
+ const parsed = tryParse(json);
2230
+ if (parsed === void 0) {
2231
+ throw createError(
2232
+ "UNREPAIRABLE_JSON",
2233
+ "The repaired output is not valid JSON. This is a defect in @nexkit/json-repair; please report it along with the input that triggered it.",
2234
+ source,
2235
+ candidate.start
2236
+ );
2237
+ }
2238
+ return { start: candidate.start, end, json, value: parsed.value, repairs };
2239
+ }
2240
+ function rankAttempts(attempts, select) {
2241
+ const first = attempts[0];
2242
+ if (first === void 0) {
2243
+ throw new Error("rankAttempts requires at least one attempt");
2244
+ }
2245
+ if (select === "first") {
2246
+ return first;
2247
+ }
2248
+ if (select === "last") {
2249
+ return attempts[attempts.length - 1];
2250
+ }
2251
+ let best = first;
2252
+ for (let index = 1; index < attempts.length; index += 1) {
2253
+ const attempt = attempts[index];
2254
+ if (select === "largest" ? isLarger(attempt, best) : isBetter(attempt, best)) {
2255
+ best = attempt;
2256
+ }
2257
+ }
2258
+ return best;
2259
+ }
2260
+ function isBetter(attempt, best) {
2261
+ const span = attempt.end - attempt.start;
2262
+ const bestSpan = best.end - best.start;
2263
+ if (span !== bestSpan) {
2264
+ return span > bestSpan;
2265
+ }
2266
+ return attempt.repairs.length < best.repairs.length;
2267
+ }
2268
+ function isLarger(attempt, best) {
2269
+ return attempt.end - attempt.start > best.end - best.start;
2270
+ }
2271
+ function wholeRegionCandidate(source, start, end) {
2272
+ const from = skipLeadingWhitespace(source, start, end);
2273
+ const to = trimTrailingWhitespace(source, from, end);
2274
+ return from < to ? { start: from, limit: end } : void 0;
2275
+ }
2276
+ function tryParse(text) {
2277
+ try {
2278
+ return { value: parseJsonStrict(text) };
2279
+ } catch {
2280
+ return void 0;
2281
+ }
2282
+ }
2283
+ function noJsonFound(source, position) {
2284
+ return createError("NO_JSON_FOUND", "No JSON value was found in the input.", source, position);
2285
+ }
2286
+
2287
+ // src/repair.ts
2288
+ function repairJson(input, options) {
2289
+ const resolved = resolveOptions(options);
2290
+ const outcome = repairInternal(input, resolved);
2291
+ if (!resolved.returnMetadata) {
2292
+ return outcome.json;
2293
+ }
2294
+ return { json: outcome.json, changed: outcome.changed, repairs: outcome.repairs };
2295
+ }
2296
+ function repairInternal(input, options) {
2297
+ const region = prepare(input, options);
2298
+ const fast = tryFastPath(input, region.start, options);
2299
+ if (fast !== void 0) {
2300
+ return fast;
2301
+ }
2302
+ const attempts = solve(input, region.start, region.end, options);
2303
+ const chosen = rankAttempts(attempts, "best");
2304
+ const repairs = buildRepairLog(input, region, chosen);
2305
+ if (repairs.length > options.maxRepairs) {
2306
+ throw createError(
2307
+ "MAX_REPAIRS_EXCEEDED",
2308
+ `Repairing this input required ${repairs.length} changes, which exceeds the maximum of ${options.maxRepairs}.`,
2309
+ input,
2310
+ chosen.start
2311
+ );
2312
+ }
2313
+ return {
2314
+ json: chosen.json,
2315
+ changed: chosen.json !== input,
2316
+ repairs,
2317
+ value: chosen.value
2318
+ };
2319
+ }
2320
+ function tryFastPath(input, regionStart, options) {
2321
+ const text = regionStart > 0 ? input.slice(regionStart) : input;
2322
+ let value;
2323
+ try {
2324
+ value = parseJsonStrict(text);
2325
+ } catch {
2326
+ return void 0;
2327
+ }
2328
+ assertStructuralDepth(input, regionStart, input.length, options.maxDepth);
2329
+ const repairs = regionStart > 0 ? [operation(new Scanner(input), "removed-byte-order-mark", 0)] : [];
2330
+ if (repairs.length > options.maxRepairs) {
2331
+ throw createError(
2332
+ "MAX_REPAIRS_EXCEEDED",
2333
+ `Removing the byte order mark exceeds the maximum of ${options.maxRepairs} repairs.`,
2334
+ input,
2335
+ 0
2336
+ );
2337
+ }
2338
+ return { json: text, changed: regionStart > 0, repairs, value };
2339
+ }
2340
+ function buildRepairLog(input, region, chosen) {
2341
+ const scanner = new Scanner(input);
2342
+ const repairs = [];
2343
+ if (region.start > 0) {
2344
+ repairs.push(operation(scanner, "removed-byte-order-mark", 0));
2345
+ }
2346
+ const before = classifyGap(input, region.start, chosen.start);
2347
+ if (before === "content") {
2348
+ repairs.push(operation(scanner, "extracted-json", chosen.start));
2349
+ } else if (before === "exotic") {
2350
+ repairs.push(operation(scanner, "normalized-whitespace", region.start));
2351
+ }
2352
+ for (const repair of chosen.repairs) {
2353
+ repairs.push(repair);
2354
+ }
2355
+ const after = classifyGap(input, chosen.end, region.end);
2356
+ if (after === "content") {
2357
+ repairs.push(operation(scanner, "removed-trailing-content", chosen.end));
2358
+ } else if (after === "exotic") {
2359
+ repairs.push(operation(scanner, "normalized-whitespace", chosen.end));
2360
+ }
2361
+ return repairs;
2362
+ }
2363
+ function classifyGap(source, from, to) {
2364
+ if (from >= to) {
2365
+ return "empty";
2366
+ }
2367
+ let exotic = false;
2368
+ for (let index = from; index < to; index += 1) {
2369
+ const code = source.charCodeAt(index);
2370
+ if (!isWhitespace(code)) {
2371
+ return "content";
2372
+ }
2373
+ if (!isJsonWhitespace(code)) {
2374
+ exotic = true;
2375
+ }
2376
+ }
2377
+ return exotic ? "exotic" : "whitespace";
2378
+ }
2379
+ function operation(scanner, type, position) {
2380
+ const { line, column } = scanner.positionAt(position);
2381
+ return { type, position, line, column, message: describeRepair(type) };
2382
+ }
2383
+
2384
+ // src/parse.ts
2385
+ function parseJson(input, options) {
2386
+ return repairInternal(input, resolveOptions(options)).value;
2387
+ }
2388
+
2389
+ exports.DEFAULT_MAX_DEPTH = DEFAULT_MAX_DEPTH;
2390
+ exports.DEFAULT_MAX_LENGTH = DEFAULT_MAX_LENGTH;
2391
+ exports.JsonRepairError = JsonRepairError;
2392
+ exports.MAX_SUPPORTED_DEPTH = MAX_SUPPORTED_DEPTH;
2393
+ exports.REPAIR_TYPES = REPAIR_TYPES;
2394
+ exports.extractAllJson = extractAllJson;
2395
+ exports.extractJson = extractJson;
2396
+ exports.isJsonRepairError = isJsonRepairError;
2397
+ exports.parseJson = parseJson;
2398
+ exports.repairJson = repairJson;
2399
+ //# sourceMappingURL=index.cjs.map
2400
+ //# sourceMappingURL=index.cjs.map