@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/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +365 -0
- package/dist/cli.js +2744 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +2400 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +343 -0
- package/dist/index.d.ts +343 -0
- package/dist/index.js +2389 -0
- package/dist/index.js.map +1 -0
- package/package.json +97 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public type contracts for `@nexkit/json-repair`.
|
|
3
|
+
*
|
|
4
|
+
* These shapes are frozen for the 1.x line: fields may be added, never removed
|
|
5
|
+
* or narrowed. New {@link RepairType} members may appear in minor releases, so
|
|
6
|
+
* consumers should not write exhaustive `switch` statements over them.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* How much interpretation the repair engine is allowed to apply.
|
|
10
|
+
*
|
|
11
|
+
* - `safe` (default) applies only repairs with a single plausible reading and
|
|
12
|
+
* never invents data. Anything ambiguous fails with `AMBIGUOUS_REPAIR`.
|
|
13
|
+
* - `aggressive` additionally applies heuristics that guess intent, such as
|
|
14
|
+
* quoting bare values or dropping trailing garbage. Aggressive mode always
|
|
15
|
+
* tries `safe` first, so whenever `safe` succeeds both modes agree byte for
|
|
16
|
+
* byte.
|
|
17
|
+
*/
|
|
18
|
+
type RepairMode = 'safe' | 'aggressive';
|
|
19
|
+
/**
|
|
20
|
+
* The kind of change the repair engine made. Every operation reported in
|
|
21
|
+
* {@link RepairResult.repairs} carries one of these.
|
|
22
|
+
*
|
|
23
|
+
* Repairs that discard input have deliberately distinct types
|
|
24
|
+
* (`removed-*`, `terminated-string-at-newline`) so that callers can audit
|
|
25
|
+
* whether any content was lost.
|
|
26
|
+
*/
|
|
27
|
+
type RepairType =
|
|
28
|
+
/** A leading U+FEFF byte order mark was removed. */
|
|
29
|
+
'removed-byte-order-mark'
|
|
30
|
+
/** JSON was isolated from Markdown fences or surrounding prose. */
|
|
31
|
+
| 'extracted-json'
|
|
32
|
+
/** A `//`, block or `#` comment was removed. */
|
|
33
|
+
| 'removed-comment'
|
|
34
|
+
/** Whitespace JSON does not allow between tokens, such as a non-breaking space, was dropped. */
|
|
35
|
+
| 'normalized-whitespace'
|
|
36
|
+
/** A comma directly before `}` or `]` was removed. */
|
|
37
|
+
| 'removed-trailing-comma'
|
|
38
|
+
/** A repeated or leading comma inside an object was removed. */
|
|
39
|
+
| 'removed-extra-comma'
|
|
40
|
+
/** A closing bracket that matched no open container was discarded. */
|
|
41
|
+
| 'removed-stray-token'
|
|
42
|
+
/** Content after the top-level value was discarded. */
|
|
43
|
+
| 'removed-trailing-content'
|
|
44
|
+
/** A truncated final key or member was discarded (streamed output). */
|
|
45
|
+
| 'removed-incomplete-member'
|
|
46
|
+
/** A missing separator between members or elements was inserted. */
|
|
47
|
+
| 'added-missing-comma'
|
|
48
|
+
/** A missing `:` between a key and its value was inserted. */
|
|
49
|
+
| 'added-missing-colon'
|
|
50
|
+
/** An absent or elided value was replaced by `null`. */
|
|
51
|
+
| 'added-missing-value'
|
|
52
|
+
/** An unclosed object was closed. */
|
|
53
|
+
| 'added-closing-brace'
|
|
54
|
+
/** An unclosed array was closed. */
|
|
55
|
+
| 'added-closing-bracket'
|
|
56
|
+
/** Single, backtick or typographic quotes were converted to `"`. */
|
|
57
|
+
| 'normalized-quotes'
|
|
58
|
+
/** A non-JSON literal was mapped to JSON (`True`, `None`, `NaN`). */
|
|
59
|
+
| 'normalized-literal'
|
|
60
|
+
/** A number literal was rewritten to valid JSON grammar (`+1`, `.5`, `0x1F`). */
|
|
61
|
+
| 'normalized-number'
|
|
62
|
+
/** A bare object key was quoted. */
|
|
63
|
+
| 'quoted-key'
|
|
64
|
+
/** A bare value was quoted as a string. */
|
|
65
|
+
| 'quoted-value'
|
|
66
|
+
/** A raw control character, inner quote or lone surrogate was escaped. */
|
|
67
|
+
| 'escaped-character'
|
|
68
|
+
/** An invalid escape sequence was corrected. */
|
|
69
|
+
| 'fixed-escape'
|
|
70
|
+
/** A string left open at end of input was closed. Lossless. */
|
|
71
|
+
| 'terminated-string-at-eof'
|
|
72
|
+
/** A string left open was closed at a line break. **Discards** the text after that line break. */
|
|
73
|
+
| 'terminated-string-at-newline';
|
|
74
|
+
/** Every {@link RepairType}, in a stable order. Useful for exhaustive UIs and tests. */
|
|
75
|
+
declare const REPAIR_TYPES: readonly ["removed-byte-order-mark", "extracted-json", "removed-comment", "normalized-whitespace", "removed-trailing-comma", "removed-extra-comma", "removed-stray-token", "removed-trailing-content", "removed-incomplete-member", "added-missing-comma", "added-missing-colon", "added-missing-value", "added-closing-brace", "added-closing-bracket", "normalized-quotes", "normalized-literal", "normalized-number", "quoted-key", "quoted-value", "escaped-character", "fixed-escape", "terminated-string-at-eof", "terminated-string-at-newline"];
|
|
76
|
+
/**
|
|
77
|
+
* A single change applied to the input.
|
|
78
|
+
*
|
|
79
|
+
* Positions locate the change in the **original** input string — they stay
|
|
80
|
+
* absolute even when the JSON was extracted from a larger document, and they
|
|
81
|
+
* include a leading byte order mark if one was present.
|
|
82
|
+
*/
|
|
83
|
+
interface RepairOperation {
|
|
84
|
+
/** What kind of change was made. */
|
|
85
|
+
type: RepairType;
|
|
86
|
+
/** Zero-based offset into the original input, in UTF-16 code units. */
|
|
87
|
+
position?: number;
|
|
88
|
+
/** One-based line number in the original input. */
|
|
89
|
+
line?: number;
|
|
90
|
+
/** One-based column number in the original input, in UTF-16 code units. */
|
|
91
|
+
column?: number;
|
|
92
|
+
/** Human-readable description, used by the CLI's `--explain` output. */
|
|
93
|
+
message?: string;
|
|
94
|
+
}
|
|
95
|
+
/** Result of {@link repairJson} when `returnMetadata: true` is set. */
|
|
96
|
+
interface RepairResult {
|
|
97
|
+
/** Repaired JSON text. Guaranteed to be accepted by `JSON.parse`. */
|
|
98
|
+
json: string;
|
|
99
|
+
/** `true` when {@link RepairResult.json} differs from the input. */
|
|
100
|
+
changed: boolean;
|
|
101
|
+
/** Ordered list of the changes that were applied, ascending by position. */
|
|
102
|
+
repairs: RepairOperation[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Options accepted by {@link repairJson} and {@link parseJson}.
|
|
106
|
+
*
|
|
107
|
+
* The boolean flags are tri-state: leaving one `undefined` uses the default for
|
|
108
|
+
* the selected {@link RepairMode}, while an explicit `true` or `false` always
|
|
109
|
+
* wins. Passing `false` therefore *disables* a repair — it does not mean
|
|
110
|
+
* "use the default".
|
|
111
|
+
*/
|
|
112
|
+
interface RepairOptions {
|
|
113
|
+
/**
|
|
114
|
+
* Repair strategy. Defaults to `'safe'`.
|
|
115
|
+
* @see RepairMode
|
|
116
|
+
*/
|
|
117
|
+
mode?: RepairMode;
|
|
118
|
+
/**
|
|
119
|
+
* Isolate JSON from Markdown code fences and surrounding prose before
|
|
120
|
+
* repairing. Defaults to `true`.
|
|
121
|
+
*/
|
|
122
|
+
extract?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Accept and strip line comments, block comments and `#` comments.
|
|
125
|
+
* Defaults to `true`.
|
|
126
|
+
* When `false`, a comment is a syntax error rather than trivia.
|
|
127
|
+
*/
|
|
128
|
+
allowComments?: boolean;
|
|
129
|
+
/** Accept single-quoted strings. Defaults to `true`. */
|
|
130
|
+
allowSingleQuotes?: boolean;
|
|
131
|
+
/** Accept and quote bare object keys. Defaults to `true`. */
|
|
132
|
+
allowUnquotedKeys?: boolean;
|
|
133
|
+
/** Remove commas that directly precede `}` or `]`. Defaults to `true`. */
|
|
134
|
+
fixTrailingCommas?: boolean;
|
|
135
|
+
/** Close containers that were never closed. Defaults to `true`. */
|
|
136
|
+
fixMissingBrackets?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Maximum accepted input length, in UTF-16 code units (not bytes).
|
|
139
|
+
* Defaults to `10_000_000`. Exceeding it throws `MAX_LENGTH_EXCEEDED`.
|
|
140
|
+
*/
|
|
141
|
+
maxLength?: number;
|
|
142
|
+
/**
|
|
143
|
+
* Maximum accepted nesting depth. Defaults to `512`, and may not exceed
|
|
144
|
+
* {@link MAX_SUPPORTED_DEPTH}. Exceeding it throws `MAX_DEPTH_EXCEEDED`,
|
|
145
|
+
* which also keeps the parser clear of host stack limits.
|
|
146
|
+
*/
|
|
147
|
+
maxDepth?: number;
|
|
148
|
+
/**
|
|
149
|
+
* Maximum number of repair operations per attempt. Defaults to `Infinity`;
|
|
150
|
+
* the engine is single pass, so it terminates regardless of this value.
|
|
151
|
+
*/
|
|
152
|
+
maxRepairs?: number;
|
|
153
|
+
/** Return a {@link RepairResult} instead of a plain string. Defaults to `false`. */
|
|
154
|
+
returnMetadata?: boolean;
|
|
155
|
+
}
|
|
156
|
+
/** Options accepted by {@link extractJson} and {@link extractAllJson}. */
|
|
157
|
+
interface ExtractOptions extends Pick<RepairOptions, 'mode' | 'allowComments' | 'allowSingleQuotes' | 'allowUnquotedKeys' | 'fixTrailingCommas' | 'fixMissingBrackets' | 'maxLength' | 'maxDepth' | 'maxRepairs'> {
|
|
158
|
+
/**
|
|
159
|
+
* Which candidate {@link extractJson} returns when the document contains
|
|
160
|
+
* several JSON blocks. Defaults to `'best'`, matching what
|
|
161
|
+
* {@link repairJson} picks.
|
|
162
|
+
*
|
|
163
|
+
* - `best` — fewest repairs, then longest, then earliest. Prose decoys such
|
|
164
|
+
* as the `[1]` in "see [1] for details" lose to a real payload beside them.
|
|
165
|
+
* - `first` — the earliest repairable candidate, in document order.
|
|
166
|
+
* - `last` — the last repairable candidate, in document order.
|
|
167
|
+
* - `largest` — the longest repairable candidate; ties resolve to the earliest.
|
|
168
|
+
*/
|
|
169
|
+
select?: 'best' | 'first' | 'last' | 'largest';
|
|
170
|
+
}
|
|
171
|
+
/** Machine-readable reason a repair failed. */
|
|
172
|
+
type JsonRepairErrorCode =
|
|
173
|
+
/** Input was not a string, was empty/whitespace-only, or an option was invalid. */
|
|
174
|
+
'INVALID_INPUT'
|
|
175
|
+
/** No repairable JSON value could be located in the input. */
|
|
176
|
+
| 'NO_JSON_FOUND'
|
|
177
|
+
/** No safe repair sequence exists for this input. */
|
|
178
|
+
| 'UNREPAIRABLE_JSON'
|
|
179
|
+
/** Input exceeded `maxLength`. */
|
|
180
|
+
| 'MAX_LENGTH_EXCEEDED'
|
|
181
|
+
/** Nesting exceeded `maxDepth`. */
|
|
182
|
+
| 'MAX_DEPTH_EXCEEDED'
|
|
183
|
+
/** More repairs were required than `maxRepairs` allows. */
|
|
184
|
+
| 'MAX_REPAIRS_EXCEEDED'
|
|
185
|
+
/** Several readings are possible and `safe` mode refuses to choose. */
|
|
186
|
+
| 'AMBIGUOUS_REPAIR';
|
|
187
|
+
/**
|
|
188
|
+
* Hard ceiling for {@link RepairOptions.maxDepth}. Requesting more throws
|
|
189
|
+
* `INVALID_INPUT`. The cap keeps the recursive parser well clear of host stack
|
|
190
|
+
* limits on every supported runtime, so deep input always produces a
|
|
191
|
+
* `MAX_DEPTH_EXCEEDED` error rather than a stack overflow.
|
|
192
|
+
*/
|
|
193
|
+
declare const MAX_SUPPORTED_DEPTH = 1024;
|
|
194
|
+
|
|
195
|
+
/** Default maximum input length, in UTF-16 code units. */
|
|
196
|
+
declare const DEFAULT_MAX_LENGTH = 10000000;
|
|
197
|
+
/** Default maximum nesting depth. */
|
|
198
|
+
declare const DEFAULT_MAX_DEPTH = 512;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Repairs malformed JSON and returns valid JSON text.
|
|
202
|
+
*
|
|
203
|
+
* Input that is already valid JSON is returned byte for byte, so this is safe to
|
|
204
|
+
* call unconditionally on data that is usually fine.
|
|
205
|
+
*
|
|
206
|
+
* @throws {JsonRepairError} `INVALID_INPUT`, `NO_JSON_FOUND`, `UNREPAIRABLE_JSON`,
|
|
207
|
+
* `AMBIGUOUS_REPAIR`, `MAX_LENGTH_EXCEEDED`, `MAX_DEPTH_EXCEEDED` or
|
|
208
|
+
* `MAX_REPAIRS_EXCEEDED`.
|
|
209
|
+
*/
|
|
210
|
+
declare function repairJson(input: string, options?: RepairOptions & {
|
|
211
|
+
returnMetadata?: false;
|
|
212
|
+
}): string;
|
|
213
|
+
/**
|
|
214
|
+
* Repairs malformed JSON and reports what was changed.
|
|
215
|
+
*
|
|
216
|
+
* @throws {JsonRepairError} See the string-returning overload.
|
|
217
|
+
*/
|
|
218
|
+
declare function repairJson(input: string, options: RepairOptions & {
|
|
219
|
+
returnMetadata: true;
|
|
220
|
+
}): RepairResult;
|
|
221
|
+
declare function repairJson(input: string, options?: RepairOptions): string | RepairResult;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Repairs malformed JSON and returns the parsed value.
|
|
225
|
+
*
|
|
226
|
+
* Equivalent to `JSON.parse(repairJson(input, options))`, but the value comes
|
|
227
|
+
* from the validation stage that {@link repairJson} already runs, so the text is
|
|
228
|
+
* never parsed twice.
|
|
229
|
+
*
|
|
230
|
+
* `T` is an unchecked assertion, exactly as with `JSON.parse`. Nothing here
|
|
231
|
+
* validates the shape of the result against it — pair this with a schema
|
|
232
|
+
* validator when the input is untrusted.
|
|
233
|
+
*
|
|
234
|
+
* @throws {JsonRepairError} `INVALID_INPUT`, `NO_JSON_FOUND`, `UNREPAIRABLE_JSON`,
|
|
235
|
+
* `AMBIGUOUS_REPAIR`, `MAX_LENGTH_EXCEEDED`, `MAX_DEPTH_EXCEEDED` or
|
|
236
|
+
* `MAX_REPAIRS_EXCEEDED`.
|
|
237
|
+
*/
|
|
238
|
+
declare function parseJson<T = unknown>(input: string, options?: RepairOptions): T;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Extracts the first — by default the best — JSON value embedded in a larger
|
|
242
|
+
* document, and returns it **exactly as it appears in the input**.
|
|
243
|
+
*
|
|
244
|
+
* The text is returned verbatim rather than repaired, which keeps extraction
|
|
245
|
+
* lossless: whitespace, key order and number formatting all survive. A
|
|
246
|
+
* candidate still has to be repairable to be selected, and the default `best`
|
|
247
|
+
* strategy prefers the longest one, so a decoy such as the `[1]` in "see [1]
|
|
248
|
+
* for details" loses to a real payload beside it even though the decoy is
|
|
249
|
+
* already valid and the payload needs work. The returned string may itself need
|
|
250
|
+
* repairing before `JSON.parse` will take it.
|
|
251
|
+
*
|
|
252
|
+
* @throws {JsonRepairError} `INVALID_INPUT`, `MAX_LENGTH_EXCEEDED` or
|
|
253
|
+
* `NO_JSON_FOUND` when no candidate could be repaired.
|
|
254
|
+
*/
|
|
255
|
+
declare function extractJson(input: string, options?: ExtractOptions): string;
|
|
256
|
+
/**
|
|
257
|
+
* Extracts every JSON value embedded in a document, in document order, each
|
|
258
|
+
* returned verbatim as it appears in the input.
|
|
259
|
+
*
|
|
260
|
+
* Values nested inside an already-extracted value are not reported separately,
|
|
261
|
+
* so a document containing `[{"a":1}]` yields the array, not the array and then
|
|
262
|
+
* its element.
|
|
263
|
+
*
|
|
264
|
+
* Returns an empty array when the document holds no repairable JSON. Unlike
|
|
265
|
+
* {@link extractJson} this never throws `NO_JSON_FOUND`, because "nothing here"
|
|
266
|
+
* is an ordinary answer when the caller asked for everything.
|
|
267
|
+
*
|
|
268
|
+
* @throws {JsonRepairError} `INVALID_INPUT` or `MAX_LENGTH_EXCEEDED`.
|
|
269
|
+
*/
|
|
270
|
+
declare function extractAllJson(input: string, options?: ExtractOptions): string[];
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Brand used to identify errors from this package across module realms.
|
|
274
|
+
*
|
|
275
|
+
* A dual ESM/CJS package can be loaded twice in the same process (once through
|
|
276
|
+
* `import`, once through `require`), which gives two distinct class objects and
|
|
277
|
+
* makes `instanceof` unreliable. `Symbol.for` uses the cross-realm global
|
|
278
|
+
* registry, so the brand stays identical for every copy.
|
|
279
|
+
*/
|
|
280
|
+
declare const ERROR_BRAND: unique symbol;
|
|
281
|
+
/** Extra fields accepted when constructing a {@link JsonRepairError}. */
|
|
282
|
+
interface JsonRepairErrorDetails {
|
|
283
|
+
/** Zero-based offset into the original input, in UTF-16 code units. */
|
|
284
|
+
position?: number;
|
|
285
|
+
/** One-based line number in the original input. */
|
|
286
|
+
line?: number;
|
|
287
|
+
/** One-based column number in the original input. */
|
|
288
|
+
column?: number;
|
|
289
|
+
/** A short excerpt of the input around {@link JsonRepairErrorDetails.position}. */
|
|
290
|
+
snippet?: string;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The only error type thrown by this package.
|
|
294
|
+
*
|
|
295
|
+
* Every failure carries a stable {@link JsonRepairError.code} so callers can
|
|
296
|
+
* branch on the reason without matching on message text, which is not part of
|
|
297
|
+
* the public contract and may be reworded in any release.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* import { repairJson, isJsonRepairError } from '@nexkit/json-repair';
|
|
302
|
+
*
|
|
303
|
+
* try {
|
|
304
|
+
* repairJson(input);
|
|
305
|
+
* } catch (error) {
|
|
306
|
+
* if (isJsonRepairError(error) && error.code === 'AMBIGUOUS_REPAIR') {
|
|
307
|
+
* return repairJson(input, { mode: 'aggressive' });
|
|
308
|
+
* }
|
|
309
|
+
* throw error;
|
|
310
|
+
* }
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
declare class JsonRepairError extends Error {
|
|
314
|
+
/** Always `'JsonRepairError'`, so the name survives minification. */
|
|
315
|
+
readonly name = "JsonRepairError";
|
|
316
|
+
/** Stable, machine-readable failure reason. */
|
|
317
|
+
readonly code: JsonRepairErrorCode;
|
|
318
|
+
/** Zero-based offset into the original input, in UTF-16 code units. */
|
|
319
|
+
readonly position: number | undefined;
|
|
320
|
+
/** One-based line number in the original input. */
|
|
321
|
+
readonly line: number | undefined;
|
|
322
|
+
/** One-based column number in the original input. */
|
|
323
|
+
readonly column: number | undefined;
|
|
324
|
+
/** A short excerpt of the input around {@link JsonRepairError.position}. */
|
|
325
|
+
readonly snippet: string | undefined;
|
|
326
|
+
/** Cross-realm brand; see {@link isJsonRepairError}. */
|
|
327
|
+
readonly [ERROR_BRAND] = true;
|
|
328
|
+
constructor(code: JsonRepairErrorCode, message: string, details?: JsonRepairErrorDetails);
|
|
329
|
+
/**
|
|
330
|
+
* Reports whether a value is a {@link JsonRepairError}, including instances
|
|
331
|
+
* created by a different copy of this package.
|
|
332
|
+
*
|
|
333
|
+
* Prefer this over `instanceof` — see {@link ERROR_BRAND}.
|
|
334
|
+
*/
|
|
335
|
+
static isJsonRepairError(value: unknown): value is JsonRepairError;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Reports whether a value is a {@link JsonRepairError}, including instances
|
|
339
|
+
* created by a different copy of this package (dual ESM/CJS loading).
|
|
340
|
+
*/
|
|
341
|
+
declare function isJsonRepairError(value: unknown): value is JsonRepairError;
|
|
342
|
+
|
|
343
|
+
export { DEFAULT_MAX_DEPTH, DEFAULT_MAX_LENGTH, type ExtractOptions, JsonRepairError, type JsonRepairErrorCode, MAX_SUPPORTED_DEPTH, REPAIR_TYPES, type RepairMode, type RepairOperation, type RepairOptions, type RepairResult, type RepairType, extractAllJson, extractJson, isJsonRepairError, parseJson, repairJson };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public type contracts for `@nexkit/json-repair`.
|
|
3
|
+
*
|
|
4
|
+
* These shapes are frozen for the 1.x line: fields may be added, never removed
|
|
5
|
+
* or narrowed. New {@link RepairType} members may appear in minor releases, so
|
|
6
|
+
* consumers should not write exhaustive `switch` statements over them.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* How much interpretation the repair engine is allowed to apply.
|
|
10
|
+
*
|
|
11
|
+
* - `safe` (default) applies only repairs with a single plausible reading and
|
|
12
|
+
* never invents data. Anything ambiguous fails with `AMBIGUOUS_REPAIR`.
|
|
13
|
+
* - `aggressive` additionally applies heuristics that guess intent, such as
|
|
14
|
+
* quoting bare values or dropping trailing garbage. Aggressive mode always
|
|
15
|
+
* tries `safe` first, so whenever `safe` succeeds both modes agree byte for
|
|
16
|
+
* byte.
|
|
17
|
+
*/
|
|
18
|
+
type RepairMode = 'safe' | 'aggressive';
|
|
19
|
+
/**
|
|
20
|
+
* The kind of change the repair engine made. Every operation reported in
|
|
21
|
+
* {@link RepairResult.repairs} carries one of these.
|
|
22
|
+
*
|
|
23
|
+
* Repairs that discard input have deliberately distinct types
|
|
24
|
+
* (`removed-*`, `terminated-string-at-newline`) so that callers can audit
|
|
25
|
+
* whether any content was lost.
|
|
26
|
+
*/
|
|
27
|
+
type RepairType =
|
|
28
|
+
/** A leading U+FEFF byte order mark was removed. */
|
|
29
|
+
'removed-byte-order-mark'
|
|
30
|
+
/** JSON was isolated from Markdown fences or surrounding prose. */
|
|
31
|
+
| 'extracted-json'
|
|
32
|
+
/** A `//`, block or `#` comment was removed. */
|
|
33
|
+
| 'removed-comment'
|
|
34
|
+
/** Whitespace JSON does not allow between tokens, such as a non-breaking space, was dropped. */
|
|
35
|
+
| 'normalized-whitespace'
|
|
36
|
+
/** A comma directly before `}` or `]` was removed. */
|
|
37
|
+
| 'removed-trailing-comma'
|
|
38
|
+
/** A repeated or leading comma inside an object was removed. */
|
|
39
|
+
| 'removed-extra-comma'
|
|
40
|
+
/** A closing bracket that matched no open container was discarded. */
|
|
41
|
+
| 'removed-stray-token'
|
|
42
|
+
/** Content after the top-level value was discarded. */
|
|
43
|
+
| 'removed-trailing-content'
|
|
44
|
+
/** A truncated final key or member was discarded (streamed output). */
|
|
45
|
+
| 'removed-incomplete-member'
|
|
46
|
+
/** A missing separator between members or elements was inserted. */
|
|
47
|
+
| 'added-missing-comma'
|
|
48
|
+
/** A missing `:` between a key and its value was inserted. */
|
|
49
|
+
| 'added-missing-colon'
|
|
50
|
+
/** An absent or elided value was replaced by `null`. */
|
|
51
|
+
| 'added-missing-value'
|
|
52
|
+
/** An unclosed object was closed. */
|
|
53
|
+
| 'added-closing-brace'
|
|
54
|
+
/** An unclosed array was closed. */
|
|
55
|
+
| 'added-closing-bracket'
|
|
56
|
+
/** Single, backtick or typographic quotes were converted to `"`. */
|
|
57
|
+
| 'normalized-quotes'
|
|
58
|
+
/** A non-JSON literal was mapped to JSON (`True`, `None`, `NaN`). */
|
|
59
|
+
| 'normalized-literal'
|
|
60
|
+
/** A number literal was rewritten to valid JSON grammar (`+1`, `.5`, `0x1F`). */
|
|
61
|
+
| 'normalized-number'
|
|
62
|
+
/** A bare object key was quoted. */
|
|
63
|
+
| 'quoted-key'
|
|
64
|
+
/** A bare value was quoted as a string. */
|
|
65
|
+
| 'quoted-value'
|
|
66
|
+
/** A raw control character, inner quote or lone surrogate was escaped. */
|
|
67
|
+
| 'escaped-character'
|
|
68
|
+
/** An invalid escape sequence was corrected. */
|
|
69
|
+
| 'fixed-escape'
|
|
70
|
+
/** A string left open at end of input was closed. Lossless. */
|
|
71
|
+
| 'terminated-string-at-eof'
|
|
72
|
+
/** A string left open was closed at a line break. **Discards** the text after that line break. */
|
|
73
|
+
| 'terminated-string-at-newline';
|
|
74
|
+
/** Every {@link RepairType}, in a stable order. Useful for exhaustive UIs and tests. */
|
|
75
|
+
declare const REPAIR_TYPES: readonly ["removed-byte-order-mark", "extracted-json", "removed-comment", "normalized-whitespace", "removed-trailing-comma", "removed-extra-comma", "removed-stray-token", "removed-trailing-content", "removed-incomplete-member", "added-missing-comma", "added-missing-colon", "added-missing-value", "added-closing-brace", "added-closing-bracket", "normalized-quotes", "normalized-literal", "normalized-number", "quoted-key", "quoted-value", "escaped-character", "fixed-escape", "terminated-string-at-eof", "terminated-string-at-newline"];
|
|
76
|
+
/**
|
|
77
|
+
* A single change applied to the input.
|
|
78
|
+
*
|
|
79
|
+
* Positions locate the change in the **original** input string — they stay
|
|
80
|
+
* absolute even when the JSON was extracted from a larger document, and they
|
|
81
|
+
* include a leading byte order mark if one was present.
|
|
82
|
+
*/
|
|
83
|
+
interface RepairOperation {
|
|
84
|
+
/** What kind of change was made. */
|
|
85
|
+
type: RepairType;
|
|
86
|
+
/** Zero-based offset into the original input, in UTF-16 code units. */
|
|
87
|
+
position?: number;
|
|
88
|
+
/** One-based line number in the original input. */
|
|
89
|
+
line?: number;
|
|
90
|
+
/** One-based column number in the original input, in UTF-16 code units. */
|
|
91
|
+
column?: number;
|
|
92
|
+
/** Human-readable description, used by the CLI's `--explain` output. */
|
|
93
|
+
message?: string;
|
|
94
|
+
}
|
|
95
|
+
/** Result of {@link repairJson} when `returnMetadata: true` is set. */
|
|
96
|
+
interface RepairResult {
|
|
97
|
+
/** Repaired JSON text. Guaranteed to be accepted by `JSON.parse`. */
|
|
98
|
+
json: string;
|
|
99
|
+
/** `true` when {@link RepairResult.json} differs from the input. */
|
|
100
|
+
changed: boolean;
|
|
101
|
+
/** Ordered list of the changes that were applied, ascending by position. */
|
|
102
|
+
repairs: RepairOperation[];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Options accepted by {@link repairJson} and {@link parseJson}.
|
|
106
|
+
*
|
|
107
|
+
* The boolean flags are tri-state: leaving one `undefined` uses the default for
|
|
108
|
+
* the selected {@link RepairMode}, while an explicit `true` or `false` always
|
|
109
|
+
* wins. Passing `false` therefore *disables* a repair — it does not mean
|
|
110
|
+
* "use the default".
|
|
111
|
+
*/
|
|
112
|
+
interface RepairOptions {
|
|
113
|
+
/**
|
|
114
|
+
* Repair strategy. Defaults to `'safe'`.
|
|
115
|
+
* @see RepairMode
|
|
116
|
+
*/
|
|
117
|
+
mode?: RepairMode;
|
|
118
|
+
/**
|
|
119
|
+
* Isolate JSON from Markdown code fences and surrounding prose before
|
|
120
|
+
* repairing. Defaults to `true`.
|
|
121
|
+
*/
|
|
122
|
+
extract?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Accept and strip line comments, block comments and `#` comments.
|
|
125
|
+
* Defaults to `true`.
|
|
126
|
+
* When `false`, a comment is a syntax error rather than trivia.
|
|
127
|
+
*/
|
|
128
|
+
allowComments?: boolean;
|
|
129
|
+
/** Accept single-quoted strings. Defaults to `true`. */
|
|
130
|
+
allowSingleQuotes?: boolean;
|
|
131
|
+
/** Accept and quote bare object keys. Defaults to `true`. */
|
|
132
|
+
allowUnquotedKeys?: boolean;
|
|
133
|
+
/** Remove commas that directly precede `}` or `]`. Defaults to `true`. */
|
|
134
|
+
fixTrailingCommas?: boolean;
|
|
135
|
+
/** Close containers that were never closed. Defaults to `true`. */
|
|
136
|
+
fixMissingBrackets?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Maximum accepted input length, in UTF-16 code units (not bytes).
|
|
139
|
+
* Defaults to `10_000_000`. Exceeding it throws `MAX_LENGTH_EXCEEDED`.
|
|
140
|
+
*/
|
|
141
|
+
maxLength?: number;
|
|
142
|
+
/**
|
|
143
|
+
* Maximum accepted nesting depth. Defaults to `512`, and may not exceed
|
|
144
|
+
* {@link MAX_SUPPORTED_DEPTH}. Exceeding it throws `MAX_DEPTH_EXCEEDED`,
|
|
145
|
+
* which also keeps the parser clear of host stack limits.
|
|
146
|
+
*/
|
|
147
|
+
maxDepth?: number;
|
|
148
|
+
/**
|
|
149
|
+
* Maximum number of repair operations per attempt. Defaults to `Infinity`;
|
|
150
|
+
* the engine is single pass, so it terminates regardless of this value.
|
|
151
|
+
*/
|
|
152
|
+
maxRepairs?: number;
|
|
153
|
+
/** Return a {@link RepairResult} instead of a plain string. Defaults to `false`. */
|
|
154
|
+
returnMetadata?: boolean;
|
|
155
|
+
}
|
|
156
|
+
/** Options accepted by {@link extractJson} and {@link extractAllJson}. */
|
|
157
|
+
interface ExtractOptions extends Pick<RepairOptions, 'mode' | 'allowComments' | 'allowSingleQuotes' | 'allowUnquotedKeys' | 'fixTrailingCommas' | 'fixMissingBrackets' | 'maxLength' | 'maxDepth' | 'maxRepairs'> {
|
|
158
|
+
/**
|
|
159
|
+
* Which candidate {@link extractJson} returns when the document contains
|
|
160
|
+
* several JSON blocks. Defaults to `'best'`, matching what
|
|
161
|
+
* {@link repairJson} picks.
|
|
162
|
+
*
|
|
163
|
+
* - `best` — fewest repairs, then longest, then earliest. Prose decoys such
|
|
164
|
+
* as the `[1]` in "see [1] for details" lose to a real payload beside them.
|
|
165
|
+
* - `first` — the earliest repairable candidate, in document order.
|
|
166
|
+
* - `last` — the last repairable candidate, in document order.
|
|
167
|
+
* - `largest` — the longest repairable candidate; ties resolve to the earliest.
|
|
168
|
+
*/
|
|
169
|
+
select?: 'best' | 'first' | 'last' | 'largest';
|
|
170
|
+
}
|
|
171
|
+
/** Machine-readable reason a repair failed. */
|
|
172
|
+
type JsonRepairErrorCode =
|
|
173
|
+
/** Input was not a string, was empty/whitespace-only, or an option was invalid. */
|
|
174
|
+
'INVALID_INPUT'
|
|
175
|
+
/** No repairable JSON value could be located in the input. */
|
|
176
|
+
| 'NO_JSON_FOUND'
|
|
177
|
+
/** No safe repair sequence exists for this input. */
|
|
178
|
+
| 'UNREPAIRABLE_JSON'
|
|
179
|
+
/** Input exceeded `maxLength`. */
|
|
180
|
+
| 'MAX_LENGTH_EXCEEDED'
|
|
181
|
+
/** Nesting exceeded `maxDepth`. */
|
|
182
|
+
| 'MAX_DEPTH_EXCEEDED'
|
|
183
|
+
/** More repairs were required than `maxRepairs` allows. */
|
|
184
|
+
| 'MAX_REPAIRS_EXCEEDED'
|
|
185
|
+
/** Several readings are possible and `safe` mode refuses to choose. */
|
|
186
|
+
| 'AMBIGUOUS_REPAIR';
|
|
187
|
+
/**
|
|
188
|
+
* Hard ceiling for {@link RepairOptions.maxDepth}. Requesting more throws
|
|
189
|
+
* `INVALID_INPUT`. The cap keeps the recursive parser well clear of host stack
|
|
190
|
+
* limits on every supported runtime, so deep input always produces a
|
|
191
|
+
* `MAX_DEPTH_EXCEEDED` error rather than a stack overflow.
|
|
192
|
+
*/
|
|
193
|
+
declare const MAX_SUPPORTED_DEPTH = 1024;
|
|
194
|
+
|
|
195
|
+
/** Default maximum input length, in UTF-16 code units. */
|
|
196
|
+
declare const DEFAULT_MAX_LENGTH = 10000000;
|
|
197
|
+
/** Default maximum nesting depth. */
|
|
198
|
+
declare const DEFAULT_MAX_DEPTH = 512;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Repairs malformed JSON and returns valid JSON text.
|
|
202
|
+
*
|
|
203
|
+
* Input that is already valid JSON is returned byte for byte, so this is safe to
|
|
204
|
+
* call unconditionally on data that is usually fine.
|
|
205
|
+
*
|
|
206
|
+
* @throws {JsonRepairError} `INVALID_INPUT`, `NO_JSON_FOUND`, `UNREPAIRABLE_JSON`,
|
|
207
|
+
* `AMBIGUOUS_REPAIR`, `MAX_LENGTH_EXCEEDED`, `MAX_DEPTH_EXCEEDED` or
|
|
208
|
+
* `MAX_REPAIRS_EXCEEDED`.
|
|
209
|
+
*/
|
|
210
|
+
declare function repairJson(input: string, options?: RepairOptions & {
|
|
211
|
+
returnMetadata?: false;
|
|
212
|
+
}): string;
|
|
213
|
+
/**
|
|
214
|
+
* Repairs malformed JSON and reports what was changed.
|
|
215
|
+
*
|
|
216
|
+
* @throws {JsonRepairError} See the string-returning overload.
|
|
217
|
+
*/
|
|
218
|
+
declare function repairJson(input: string, options: RepairOptions & {
|
|
219
|
+
returnMetadata: true;
|
|
220
|
+
}): RepairResult;
|
|
221
|
+
declare function repairJson(input: string, options?: RepairOptions): string | RepairResult;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Repairs malformed JSON and returns the parsed value.
|
|
225
|
+
*
|
|
226
|
+
* Equivalent to `JSON.parse(repairJson(input, options))`, but the value comes
|
|
227
|
+
* from the validation stage that {@link repairJson} already runs, so the text is
|
|
228
|
+
* never parsed twice.
|
|
229
|
+
*
|
|
230
|
+
* `T` is an unchecked assertion, exactly as with `JSON.parse`. Nothing here
|
|
231
|
+
* validates the shape of the result against it — pair this with a schema
|
|
232
|
+
* validator when the input is untrusted.
|
|
233
|
+
*
|
|
234
|
+
* @throws {JsonRepairError} `INVALID_INPUT`, `NO_JSON_FOUND`, `UNREPAIRABLE_JSON`,
|
|
235
|
+
* `AMBIGUOUS_REPAIR`, `MAX_LENGTH_EXCEEDED`, `MAX_DEPTH_EXCEEDED` or
|
|
236
|
+
* `MAX_REPAIRS_EXCEEDED`.
|
|
237
|
+
*/
|
|
238
|
+
declare function parseJson<T = unknown>(input: string, options?: RepairOptions): T;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Extracts the first — by default the best — JSON value embedded in a larger
|
|
242
|
+
* document, and returns it **exactly as it appears in the input**.
|
|
243
|
+
*
|
|
244
|
+
* The text is returned verbatim rather than repaired, which keeps extraction
|
|
245
|
+
* lossless: whitespace, key order and number formatting all survive. A
|
|
246
|
+
* candidate still has to be repairable to be selected, and the default `best`
|
|
247
|
+
* strategy prefers the longest one, so a decoy such as the `[1]` in "see [1]
|
|
248
|
+
* for details" loses to a real payload beside it even though the decoy is
|
|
249
|
+
* already valid and the payload needs work. The returned string may itself need
|
|
250
|
+
* repairing before `JSON.parse` will take it.
|
|
251
|
+
*
|
|
252
|
+
* @throws {JsonRepairError} `INVALID_INPUT`, `MAX_LENGTH_EXCEEDED` or
|
|
253
|
+
* `NO_JSON_FOUND` when no candidate could be repaired.
|
|
254
|
+
*/
|
|
255
|
+
declare function extractJson(input: string, options?: ExtractOptions): string;
|
|
256
|
+
/**
|
|
257
|
+
* Extracts every JSON value embedded in a document, in document order, each
|
|
258
|
+
* returned verbatim as it appears in the input.
|
|
259
|
+
*
|
|
260
|
+
* Values nested inside an already-extracted value are not reported separately,
|
|
261
|
+
* so a document containing `[{"a":1}]` yields the array, not the array and then
|
|
262
|
+
* its element.
|
|
263
|
+
*
|
|
264
|
+
* Returns an empty array when the document holds no repairable JSON. Unlike
|
|
265
|
+
* {@link extractJson} this never throws `NO_JSON_FOUND`, because "nothing here"
|
|
266
|
+
* is an ordinary answer when the caller asked for everything.
|
|
267
|
+
*
|
|
268
|
+
* @throws {JsonRepairError} `INVALID_INPUT` or `MAX_LENGTH_EXCEEDED`.
|
|
269
|
+
*/
|
|
270
|
+
declare function extractAllJson(input: string, options?: ExtractOptions): string[];
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Brand used to identify errors from this package across module realms.
|
|
274
|
+
*
|
|
275
|
+
* A dual ESM/CJS package can be loaded twice in the same process (once through
|
|
276
|
+
* `import`, once through `require`), which gives two distinct class objects and
|
|
277
|
+
* makes `instanceof` unreliable. `Symbol.for` uses the cross-realm global
|
|
278
|
+
* registry, so the brand stays identical for every copy.
|
|
279
|
+
*/
|
|
280
|
+
declare const ERROR_BRAND: unique symbol;
|
|
281
|
+
/** Extra fields accepted when constructing a {@link JsonRepairError}. */
|
|
282
|
+
interface JsonRepairErrorDetails {
|
|
283
|
+
/** Zero-based offset into the original input, in UTF-16 code units. */
|
|
284
|
+
position?: number;
|
|
285
|
+
/** One-based line number in the original input. */
|
|
286
|
+
line?: number;
|
|
287
|
+
/** One-based column number in the original input. */
|
|
288
|
+
column?: number;
|
|
289
|
+
/** A short excerpt of the input around {@link JsonRepairErrorDetails.position}. */
|
|
290
|
+
snippet?: string;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The only error type thrown by this package.
|
|
294
|
+
*
|
|
295
|
+
* Every failure carries a stable {@link JsonRepairError.code} so callers can
|
|
296
|
+
* branch on the reason without matching on message text, which is not part of
|
|
297
|
+
* the public contract and may be reworded in any release.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* import { repairJson, isJsonRepairError } from '@nexkit/json-repair';
|
|
302
|
+
*
|
|
303
|
+
* try {
|
|
304
|
+
* repairJson(input);
|
|
305
|
+
* } catch (error) {
|
|
306
|
+
* if (isJsonRepairError(error) && error.code === 'AMBIGUOUS_REPAIR') {
|
|
307
|
+
* return repairJson(input, { mode: 'aggressive' });
|
|
308
|
+
* }
|
|
309
|
+
* throw error;
|
|
310
|
+
* }
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
declare class JsonRepairError extends Error {
|
|
314
|
+
/** Always `'JsonRepairError'`, so the name survives minification. */
|
|
315
|
+
readonly name = "JsonRepairError";
|
|
316
|
+
/** Stable, machine-readable failure reason. */
|
|
317
|
+
readonly code: JsonRepairErrorCode;
|
|
318
|
+
/** Zero-based offset into the original input, in UTF-16 code units. */
|
|
319
|
+
readonly position: number | undefined;
|
|
320
|
+
/** One-based line number in the original input. */
|
|
321
|
+
readonly line: number | undefined;
|
|
322
|
+
/** One-based column number in the original input. */
|
|
323
|
+
readonly column: number | undefined;
|
|
324
|
+
/** A short excerpt of the input around {@link JsonRepairError.position}. */
|
|
325
|
+
readonly snippet: string | undefined;
|
|
326
|
+
/** Cross-realm brand; see {@link isJsonRepairError}. */
|
|
327
|
+
readonly [ERROR_BRAND] = true;
|
|
328
|
+
constructor(code: JsonRepairErrorCode, message: string, details?: JsonRepairErrorDetails);
|
|
329
|
+
/**
|
|
330
|
+
* Reports whether a value is a {@link JsonRepairError}, including instances
|
|
331
|
+
* created by a different copy of this package.
|
|
332
|
+
*
|
|
333
|
+
* Prefer this over `instanceof` — see {@link ERROR_BRAND}.
|
|
334
|
+
*/
|
|
335
|
+
static isJsonRepairError(value: unknown): value is JsonRepairError;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Reports whether a value is a {@link JsonRepairError}, including instances
|
|
339
|
+
* created by a different copy of this package (dual ESM/CJS loading).
|
|
340
|
+
*/
|
|
341
|
+
declare function isJsonRepairError(value: unknown): value is JsonRepairError;
|
|
342
|
+
|
|
343
|
+
export { DEFAULT_MAX_DEPTH, DEFAULT_MAX_LENGTH, type ExtractOptions, JsonRepairError, type JsonRepairErrorCode, MAX_SUPPORTED_DEPTH, REPAIR_TYPES, type RepairMode, type RepairOperation, type RepairOptions, type RepairResult, type RepairType, extractAllJson, extractJson, isJsonRepairError, parseJson, repairJson };
|