@asymmetric-effort/jsonlint 0.0.1 → 0.0.3

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/bin/jsonlint ADDED
Binary file
package/build/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
package/build/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(args?: string[]): void;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Character-by-character JSON formatter.
3
+ * Works even on invalid JSON — used for best-effort pretty-printing.
4
+ */
5
+ export declare function formatJson(json: string, indent?: string): string;
@@ -0,0 +1,14 @@
1
+ export { JsonParser, parse, ParseError } from "./parser.js";
2
+ export { Lexer, LexerError, TokenType } from "./lexer.js";
3
+ export type { Token, SourceLocation } from "./lexer.js";
4
+ export type { ParseErrorHash } from "./parser.js";
5
+ export { formatJson } from "./formatter.js";
6
+ export { SchemaValidator } from "./schema.js";
7
+ export type { JsonSchema, SchemaError } from "./schema.js";
8
+ export { main } from "./cli.js";
9
+ import { JsonParser } from "./parser.js";
10
+ export declare const parser: JsonParser;
11
+ import { formatJson } from "./formatter.js";
12
+ export declare const formatter: {
13
+ formatJson: typeof formatJson;
14
+ };
package/build/index.js ADDED
@@ -0,0 +1,1129 @@
1
+ // src/lexer.ts
2
+ var TokenType;
3
+ ((TokenType2) => {
4
+ TokenType2["STRING"] = "STRING";
5
+ TokenType2["NUMBER"] = "NUMBER";
6
+ TokenType2["TRUE"] = "TRUE";
7
+ TokenType2["FALSE"] = "FALSE";
8
+ TokenType2["NULL"] = "NULL";
9
+ TokenType2["LBRACE"] = "{";
10
+ TokenType2["RBRACE"] = "}";
11
+ TokenType2["LBRACKET"] = "[";
12
+ TokenType2["RBRACKET"] = "]";
13
+ TokenType2["COMMA"] = ",";
14
+ TokenType2["COLON"] = ":";
15
+ TokenType2["EOF"] = "EOF";
16
+ TokenType2["INVALID"] = "INVALID";
17
+ })(TokenType ||= {});
18
+
19
+ class LexerError extends Error {
20
+ line;
21
+ column;
22
+ position;
23
+ input;
24
+ constructor(message, line, column, position, input) {
25
+ super(message);
26
+ this.name = "LexerError";
27
+ this.line = line;
28
+ this.column = column;
29
+ this.position = position;
30
+ this.input = input;
31
+ }
32
+ showPosition() {
33
+ const lines = this.input.split(`
34
+ `);
35
+ const errorLine = lines[this.line - 1] || "";
36
+ const before = errorLine.substring(Math.max(0, this.column - 20), this.column);
37
+ const after = errorLine.substring(this.column, this.column + 20);
38
+ const pad = new Array(before.length + 1).join("-");
39
+ return before + after + `
40
+ ` + pad + "^";
41
+ }
42
+ }
43
+
44
+ class Lexer {
45
+ input = "";
46
+ pos = 0;
47
+ line = 1;
48
+ column = 0;
49
+ setInput(input) {
50
+ this.input = input;
51
+ this.pos = 0;
52
+ this.line = 1;
53
+ this.column = 0;
54
+ }
55
+ peek() {
56
+ return this.input[this.pos] ?? "";
57
+ }
58
+ advance() {
59
+ const ch = this.input[this.pos];
60
+ this.pos++;
61
+ if (ch === `
62
+ `) {
63
+ this.line++;
64
+ this.column = 0;
65
+ } else {
66
+ this.column++;
67
+ }
68
+ return ch ?? "";
69
+ }
70
+ skipWhitespace() {
71
+ while (this.pos < this.input.length) {
72
+ const ch = this.input[this.pos];
73
+ if (ch === " " || ch === "\t" || ch === `
74
+ ` || ch === "\r") {
75
+ this.advance();
76
+ } else {
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ readString() {
82
+ const startLine = this.line;
83
+ const startCol = this.column;
84
+ this.advance();
85
+ let value = "";
86
+ while (this.pos < this.input.length) {
87
+ const ch = this.input[this.pos];
88
+ if (ch.charCodeAt(0) >= 0 && ch.charCodeAt(0) <= 31) {
89
+ if (ch === `
90
+ ` || ch === "\r") {
91
+ throw new LexerError(`Lexical error on line ${this.line}. Bad string: unterminated string.
92
+ ${this.showPositionAt(startLine, startCol)}`, this.line, this.column, this.pos, this.input);
93
+ }
94
+ throw new LexerError(`Lexical error on line ${this.line}. Bad string: control character in string.
95
+ ${this.showPositionAt(this.line, this.column)}`, this.line, this.column, this.pos, this.input);
96
+ }
97
+ if (ch === '"') {
98
+ this.advance();
99
+ return {
100
+ type: "STRING" /* STRING */,
101
+ value,
102
+ loc: {
103
+ first_line: startLine,
104
+ last_line: this.line,
105
+ first_column: startCol,
106
+ last_column: this.column
107
+ }
108
+ };
109
+ }
110
+ if (ch === "\\") {
111
+ this.advance();
112
+ const escaped = this.input[this.pos];
113
+ if (escaped === undefined) {
114
+ throw new LexerError(`Lexical error on line ${this.line}. Bad string: unterminated escape sequence.
115
+ ${this.showPositionAt(this.line, this.column)}`, this.line, this.column, this.pos, this.input);
116
+ }
117
+ switch (escaped) {
118
+ case '"':
119
+ value += '"';
120
+ this.advance();
121
+ break;
122
+ case "\\":
123
+ value += "\\";
124
+ this.advance();
125
+ break;
126
+ case "/":
127
+ value += "/";
128
+ this.advance();
129
+ break;
130
+ case "b":
131
+ value += "\b";
132
+ this.advance();
133
+ break;
134
+ case "f":
135
+ value += "\f";
136
+ this.advance();
137
+ break;
138
+ case "n":
139
+ value += `
140
+ `;
141
+ this.advance();
142
+ break;
143
+ case "r":
144
+ value += "\r";
145
+ this.advance();
146
+ break;
147
+ case "t":
148
+ value += "\t";
149
+ this.advance();
150
+ break;
151
+ case "u": {
152
+ this.advance();
153
+ let hex = "";
154
+ for (let i = 0;i < 4; i++) {
155
+ const h = this.input[this.pos];
156
+ if (h === undefined || !/[0-9a-fA-F]/.test(h)) {
157
+ throw new LexerError(`Lexical error on line ${this.line}. Bad string: invalid unicode escape.
158
+ ${this.showPositionAt(this.line, this.column)}`, this.line, this.column, this.pos, this.input);
159
+ }
160
+ hex += h;
161
+ this.advance();
162
+ }
163
+ value += String.fromCharCode(parseInt(hex, 16));
164
+ break;
165
+ }
166
+ default:
167
+ throw new LexerError(`Lexical error on line ${this.line}. Bad string: invalid escape character '\\${escaped}'.
168
+ ${this.showPositionAt(this.line, this.column)}`, this.line, this.column, this.pos, this.input);
169
+ }
170
+ continue;
171
+ }
172
+ value += ch;
173
+ this.advance();
174
+ }
175
+ throw new LexerError(`Lexical error on line ${this.line}. Bad string: unterminated string.
176
+ ${this.showPositionAt(startLine, startCol)}`, this.line, this.column, this.pos, this.input);
177
+ }
178
+ readNumber() {
179
+ const startLine = this.line;
180
+ const startCol = this.column;
181
+ let numStr = "";
182
+ if (this.peek() === "-") {
183
+ numStr += this.advance();
184
+ }
185
+ if (this.peek() === "0") {
186
+ numStr += this.advance();
187
+ if (this.pos < this.input.length && /[0-9]/.test(this.peek())) {
188
+ throw new LexerError(`Lexical error on line ${this.line}. Bad number: leading zeros are not allowed.
189
+ ${this.showPositionAt(this.line, this.column)}`, this.line, this.column, this.pos, this.input);
190
+ }
191
+ } else if (/[1-9]/.test(this.peek())) {
192
+ numStr += this.advance();
193
+ while (this.pos < this.input.length && /[0-9]/.test(this.peek())) {
194
+ numStr += this.advance();
195
+ }
196
+ } else {
197
+ throw new LexerError(`Lexical error on line ${this.line}. Bad number: expected digit.
198
+ ${this.showPositionAt(this.line, this.column)}`, this.line, this.column, this.pos, this.input);
199
+ }
200
+ if (this.peek() === ".") {
201
+ numStr += this.advance();
202
+ if (!/[0-9]/.test(this.peek())) {
203
+ throw new LexerError(`Lexical error on line ${this.line}. Bad number: expected digit after decimal point.
204
+ ${this.showPositionAt(this.line, this.column)}`, this.line, this.column, this.pos, this.input);
205
+ }
206
+ while (this.pos < this.input.length && /[0-9]/.test(this.peek())) {
207
+ numStr += this.advance();
208
+ }
209
+ }
210
+ if (this.peek() === "e" || this.peek() === "E") {
211
+ numStr += this.advance();
212
+ if (this.peek() === "+" || this.peek() === "-") {
213
+ numStr += this.advance();
214
+ }
215
+ if (!/[0-9]/.test(this.peek())) {
216
+ throw new LexerError(`Lexical error on line ${this.line}. Bad number: expected digit in exponent.
217
+ ${this.showPositionAt(this.line, this.column)}`, this.line, this.column, this.pos, this.input);
218
+ }
219
+ while (this.pos < this.input.length && /[0-9]/.test(this.peek())) {
220
+ numStr += this.advance();
221
+ }
222
+ }
223
+ return {
224
+ type: "NUMBER" /* NUMBER */,
225
+ value: numStr,
226
+ loc: {
227
+ first_line: startLine,
228
+ last_line: this.line,
229
+ first_column: startCol,
230
+ last_column: this.column
231
+ }
232
+ };
233
+ }
234
+ readKeyword(expected, tokenType) {
235
+ const startLine = this.line;
236
+ const startCol = this.column;
237
+ for (let i = 0;i < expected.length; i++) {
238
+ if (this.pos >= this.input.length || this.input[this.pos] !== expected[i]) {
239
+ throw new LexerError(`Lexical error on line ${this.line}. Unrecognized text.
240
+ ${this.showPositionAt(this.line, this.column)}`, this.line, this.column, this.pos, this.input);
241
+ }
242
+ this.advance();
243
+ }
244
+ return {
245
+ type: tokenType,
246
+ value: expected,
247
+ loc: {
248
+ first_line: startLine,
249
+ last_line: this.line,
250
+ first_column: startCol,
251
+ last_column: this.column
252
+ }
253
+ };
254
+ }
255
+ showPositionAt(line, col) {
256
+ const lines = this.input.split(`
257
+ `);
258
+ const errorLine = lines[line - 1] || "";
259
+ const before = errorLine.substring(Math.max(0, col - 20), col);
260
+ const after = errorLine.substring(col, col + 20);
261
+ const pad = new Array(before.length + 1).join("-");
262
+ return before + after + `
263
+ ` + pad + "^";
264
+ }
265
+ nextToken() {
266
+ this.skipWhitespace();
267
+ if (this.pos >= this.input.length) {
268
+ return {
269
+ type: "EOF" /* EOF */,
270
+ value: "",
271
+ loc: {
272
+ first_line: this.line,
273
+ last_line: this.line,
274
+ first_column: this.column,
275
+ last_column: this.column
276
+ }
277
+ };
278
+ }
279
+ const ch = this.peek();
280
+ const startLine = this.line;
281
+ const startCol = this.column;
282
+ switch (ch) {
283
+ case "{":
284
+ this.advance();
285
+ return {
286
+ type: "{" /* LBRACE */,
287
+ value: "{",
288
+ loc: {
289
+ first_line: startLine,
290
+ last_line: this.line,
291
+ first_column: startCol,
292
+ last_column: this.column
293
+ }
294
+ };
295
+ case "}":
296
+ this.advance();
297
+ return {
298
+ type: "}" /* RBRACE */,
299
+ value: "}",
300
+ loc: {
301
+ first_line: startLine,
302
+ last_line: this.line,
303
+ first_column: startCol,
304
+ last_column: this.column
305
+ }
306
+ };
307
+ case "[":
308
+ this.advance();
309
+ return {
310
+ type: "[" /* LBRACKET */,
311
+ value: "[",
312
+ loc: {
313
+ first_line: startLine,
314
+ last_line: this.line,
315
+ first_column: startCol,
316
+ last_column: this.column
317
+ }
318
+ };
319
+ case "]":
320
+ this.advance();
321
+ return {
322
+ type: "]" /* RBRACKET */,
323
+ value: "]",
324
+ loc: {
325
+ first_line: startLine,
326
+ last_line: this.line,
327
+ first_column: startCol,
328
+ last_column: this.column
329
+ }
330
+ };
331
+ case ",":
332
+ this.advance();
333
+ return {
334
+ type: "," /* COMMA */,
335
+ value: ",",
336
+ loc: {
337
+ first_line: startLine,
338
+ last_line: this.line,
339
+ first_column: startCol,
340
+ last_column: this.column
341
+ }
342
+ };
343
+ case ":":
344
+ this.advance();
345
+ return {
346
+ type: ":" /* COLON */,
347
+ value: ":",
348
+ loc: {
349
+ first_line: startLine,
350
+ last_line: this.line,
351
+ first_column: startCol,
352
+ last_column: this.column
353
+ }
354
+ };
355
+ case '"':
356
+ return this.readString();
357
+ case "t":
358
+ return this.readKeyword("true", "TRUE" /* TRUE */);
359
+ case "f":
360
+ return this.readKeyword("false", "FALSE" /* FALSE */);
361
+ case "n":
362
+ return this.readKeyword("null", "NULL" /* NULL */);
363
+ case "-":
364
+ return this.readNumber();
365
+ default:
366
+ if (/[0-9]/.test(ch)) {
367
+ return this.readNumber();
368
+ }
369
+ throw new LexerError(`Lexical error on line ${this.line}. Unrecognized text.
370
+ ${this.showPositionAt(this.line, this.column)}`, this.line, this.column, this.pos, this.input);
371
+ }
372
+ }
373
+ }
374
+
375
+ // src/parser.ts
376
+ class ParseError extends Error {
377
+ hash;
378
+ constructor(message, hash) {
379
+ super(message);
380
+ this.name = "ParseError";
381
+ this.hash = hash;
382
+ }
383
+ }
384
+
385
+ class JsonParser {
386
+ lexer;
387
+ currentToken;
388
+ previousToken = null;
389
+ input = "";
390
+ yy = {};
391
+ parseError = null;
392
+ constructor() {
393
+ this.lexer = new Lexer;
394
+ }
395
+ parse(input) {
396
+ this.input = input;
397
+ this.lexer.setInput(input);
398
+ this.currentToken = this.lexer.nextToken();
399
+ const result = this.parseValue();
400
+ this.expect("EOF" /* EOF */);
401
+ return result;
402
+ }
403
+ advance() {
404
+ this.previousToken = this.currentToken;
405
+ this.currentToken = this.lexer.nextToken();
406
+ return this.previousToken;
407
+ }
408
+ expect(type) {
409
+ if (this.currentToken.type !== type) {
410
+ this.throwParseError(type);
411
+ }
412
+ return this.advance();
413
+ }
414
+ throwParseError(...expected) {
415
+ const token = this.currentToken;
416
+ const hash = {
417
+ text: token.value,
418
+ token: token.type,
419
+ line: token.loc.first_line,
420
+ loc: token.loc,
421
+ expected: expected.map((t) => `'${t}'`)
422
+ };
423
+ if (this.parseError) {
424
+ this.parseError(this.formatError(token, expected), hash);
425
+ }
426
+ const msg = this.formatError(token, expected);
427
+ throw new ParseError(msg, hash);
428
+ }
429
+ formatError(token, expected) {
430
+ const line = token.loc.first_line;
431
+ const position = this.showPosition(token);
432
+ const expectedStr = expected.map((t) => `'${t}'`).join(", ");
433
+ const got = token.type === "EOF" /* EOF */ ? "'EOF'" : `'${token.value || token.type}'`;
434
+ return `Parse error on line ${line}:
435
+ ${position}
436
+ Expecting ${expectedStr}, got ${got}`;
437
+ }
438
+ showPosition(token) {
439
+ const lines = this.input.split(`
440
+ `);
441
+ const lineIdx = token.loc.first_line - 1;
442
+ const errorLine = lines[lineIdx] || "";
443
+ const col = token.loc.first_column;
444
+ const before = errorLine.substring(Math.max(0, col - 20), col);
445
+ const after = errorLine.substring(col, col + 20);
446
+ const pad = new Array(before.length + 1).join("-");
447
+ return before + after + `
448
+ ` + pad + "^";
449
+ }
450
+ parseValue() {
451
+ switch (this.currentToken.type) {
452
+ case "STRING" /* STRING */:
453
+ return this.advance().value;
454
+ case "NUMBER" /* NUMBER */:
455
+ return Number(this.advance().value);
456
+ case "TRUE" /* TRUE */:
457
+ this.advance();
458
+ return true;
459
+ case "FALSE" /* FALSE */:
460
+ this.advance();
461
+ return false;
462
+ case "NULL" /* NULL */:
463
+ this.advance();
464
+ return null;
465
+ case "{" /* LBRACE */:
466
+ return this.parseObject();
467
+ case "[" /* LBRACKET */:
468
+ return this.parseArray();
469
+ default:
470
+ this.throwParseError("STRING" /* STRING */, "NUMBER" /* NUMBER */, "NULL" /* NULL */, "TRUE" /* TRUE */, "FALSE" /* FALSE */, "{" /* LBRACE */, "[" /* LBRACKET */);
471
+ }
472
+ }
473
+ parseObject() {
474
+ this.expect("{" /* LBRACE */);
475
+ const obj = {};
476
+ if (this.currentToken.type === "}" /* RBRACE */) {
477
+ this.advance();
478
+ return obj;
479
+ }
480
+ this.parseMember(obj);
481
+ while (this.currentToken.type === "," /* COMMA */) {
482
+ this.advance();
483
+ this.parseMember(obj);
484
+ }
485
+ this.expect("}" /* RBRACE */);
486
+ return obj;
487
+ }
488
+ parseMember(obj) {
489
+ if (this.currentToken.type !== "STRING" /* STRING */) {
490
+ this.throwParseError("STRING" /* STRING */);
491
+ }
492
+ const key = this.advance().value;
493
+ this.expect(":" /* COLON */);
494
+ obj[key] = this.parseValue();
495
+ }
496
+ parseArray() {
497
+ this.expect("[" /* LBRACKET */);
498
+ const arr = [];
499
+ if (this.currentToken.type === "]" /* RBRACKET */) {
500
+ this.advance();
501
+ return arr;
502
+ }
503
+ arr.push(this.parseValue());
504
+ while (this.currentToken.type === "," /* COMMA */) {
505
+ this.advance();
506
+ arr.push(this.parseValue());
507
+ }
508
+ this.expect("]" /* RBRACKET */);
509
+ return arr;
510
+ }
511
+ }
512
+ var defaultParser = new JsonParser;
513
+ function parse(input) {
514
+ return defaultParser.parse(input);
515
+ }
516
+ // src/formatter.ts
517
+ function formatJson(json, indent = " ") {
518
+ let result = "";
519
+ let depth = 0;
520
+ let inString = false;
521
+ let escaped = false;
522
+ let i = 0;
523
+ const newline = () => {
524
+ return `
525
+ ` + indent.repeat(depth);
526
+ };
527
+ while (i < json.length) {
528
+ const ch = json[i];
529
+ if (escaped) {
530
+ result += ch;
531
+ escaped = false;
532
+ i++;
533
+ continue;
534
+ }
535
+ if (ch === "\\" && inString) {
536
+ result += ch;
537
+ escaped = true;
538
+ i++;
539
+ continue;
540
+ }
541
+ if (ch === '"' && !escaped) {
542
+ inString = !inString;
543
+ result += ch;
544
+ i++;
545
+ continue;
546
+ }
547
+ if (inString) {
548
+ result += ch;
549
+ i++;
550
+ continue;
551
+ }
552
+ switch (ch) {
553
+ case "{":
554
+ case "[":
555
+ result += ch;
556
+ depth++;
557
+ if (!isEmptyBracket(json, i)) {
558
+ result += newline();
559
+ }
560
+ break;
561
+ case "}":
562
+ case "]":
563
+ depth = Math.max(0, depth - 1);
564
+ if (!isPrecededByOpen(result, ch)) {
565
+ result += newline();
566
+ }
567
+ result += ch;
568
+ break;
569
+ case ",":
570
+ result += ch;
571
+ result += newline();
572
+ break;
573
+ case ":":
574
+ result += ": ";
575
+ break;
576
+ case " ":
577
+ case "\t":
578
+ case `
579
+ `:
580
+ case "\r":
581
+ break;
582
+ default:
583
+ result += ch;
584
+ break;
585
+ }
586
+ i++;
587
+ }
588
+ return result;
589
+ }
590
+ function isEmptyBracket(json, openPos) {
591
+ const openChar = json[openPos];
592
+ const closeChar = openChar === "{" ? "}" : "]";
593
+ let j = openPos + 1;
594
+ while (j < json.length) {
595
+ const ch = json[j];
596
+ if (ch === " " || ch === "\t" || ch === `
597
+ ` || ch === "\r") {
598
+ j++;
599
+ continue;
600
+ }
601
+ return ch === closeChar;
602
+ }
603
+ return false;
604
+ }
605
+ function isPrecededByOpen(result, closeChar) {
606
+ const openChar = closeChar === "}" ? "{" : "[";
607
+ for (let i = result.length - 1;i >= 0; i--) {
608
+ const ch = result[i];
609
+ if (ch === " " || ch === "\t" || ch === `
610
+ ` || ch === "\r") {
611
+ continue;
612
+ }
613
+ return ch === openChar;
614
+ }
615
+ return false;
616
+ }
617
+ // src/schema.ts
618
+ function getType(value) {
619
+ if (value === null)
620
+ return "null";
621
+ if (Array.isArray(value))
622
+ return "array";
623
+ return typeof value;
624
+ }
625
+ function deepEqual(a, b) {
626
+ if (a === b)
627
+ return true;
628
+ if (a === null || b === null)
629
+ return false;
630
+ if (typeof a !== typeof b)
631
+ return false;
632
+ if (Array.isArray(a) && Array.isArray(b)) {
633
+ if (a.length !== b.length)
634
+ return false;
635
+ for (let i = 0;i < a.length; i++) {
636
+ if (!deepEqual(a[i], b[i]))
637
+ return false;
638
+ }
639
+ return true;
640
+ }
641
+ if (typeof a === "object" && typeof b === "object") {
642
+ const aObj = a;
643
+ const bObj = b;
644
+ const aKeys = Object.keys(aObj);
645
+ const bKeys = Object.keys(bObj);
646
+ if (aKeys.length !== bKeys.length)
647
+ return false;
648
+ for (const key of aKeys) {
649
+ if (!deepEqual(aObj[key], bObj[key]))
650
+ return false;
651
+ }
652
+ return true;
653
+ }
654
+ return false;
655
+ }
656
+
657
+ class SchemaValidator {
658
+ schemas = new Map;
659
+ errors = [];
660
+ validate(instance, schema, path = "") {
661
+ this.errors = [];
662
+ this.collectSchemas(schema, "");
663
+ this.doValidate(instance, schema, path);
664
+ return this.errors;
665
+ }
666
+ collectSchemas(schema, basePath) {
667
+ if (schema.id) {
668
+ this.schemas.set(schema.id, schema);
669
+ }
670
+ if (schema.properties) {
671
+ for (const [key, prop] of Object.entries(schema.properties)) {
672
+ this.collectSchemas(prop, `${basePath}/properties/${key}`);
673
+ }
674
+ }
675
+ if (schema.items && !Array.isArray(schema.items)) {
676
+ this.collectSchemas(schema.items, `${basePath}/items`);
677
+ }
678
+ }
679
+ resolveRef(ref) {
680
+ return this.schemas.get(ref) || null;
681
+ }
682
+ addError(path, message) {
683
+ this.errors.push({ property: path, message });
684
+ }
685
+ doValidate(instance, schema, path) {
686
+ if (schema.$ref) {
687
+ const resolved = this.resolveRef(schema.$ref);
688
+ if (resolved) {
689
+ this.doValidate(instance, resolved, path);
690
+ }
691
+ return;
692
+ }
693
+ if (schema.extends) {
694
+ const extList = Array.isArray(schema.extends) ? schema.extends : [schema.extends];
695
+ for (const ext of extList) {
696
+ this.doValidate(instance, ext, path);
697
+ }
698
+ }
699
+ if (schema.type !== undefined) {
700
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
701
+ const actualType = getType(instance);
702
+ const typeMatch = types.some((t) => {
703
+ if (t === "any")
704
+ return true;
705
+ if (t === "integer")
706
+ return actualType === "number" && Number.isInteger(instance);
707
+ if (t === "number")
708
+ return actualType === "number";
709
+ return t === actualType;
710
+ });
711
+ if (!typeMatch) {
712
+ this.addError(path, `Expected type ${types.join(" or ")} but found type ${actualType}`);
713
+ return;
714
+ }
715
+ }
716
+ if (schema.disallow !== undefined) {
717
+ const disallowed = Array.isArray(schema.disallow) ? schema.disallow : [schema.disallow];
718
+ const actualType = getType(instance);
719
+ if (disallowed.some((t) => {
720
+ if (t === "integer")
721
+ return actualType === "number" && Number.isInteger(instance);
722
+ return t === actualType;
723
+ })) {
724
+ this.addError(path, `Type ${actualType} is disallowed`);
725
+ }
726
+ }
727
+ if (schema.enum !== undefined) {
728
+ if (!schema.enum.some((e) => deepEqual(e, instance))) {
729
+ this.addError(path, `Value ${JSON.stringify(instance)} is not one of the allowed enum values`);
730
+ }
731
+ }
732
+ if (typeof instance === "string") {
733
+ if (schema.minLength !== undefined && instance.length < schema.minLength) {
734
+ this.addError(path, `String is too short (${instance.length} < ${schema.minLength})`);
735
+ }
736
+ if (schema.maxLength !== undefined && instance.length > schema.maxLength) {
737
+ this.addError(path, `String is too long (${instance.length} > ${schema.maxLength})`);
738
+ }
739
+ if (schema.pattern !== undefined) {
740
+ const re = new RegExp(schema.pattern);
741
+ if (!re.test(instance)) {
742
+ this.addError(path, `String does not match pattern: ${schema.pattern}`);
743
+ }
744
+ }
745
+ }
746
+ if (typeof instance === "number") {
747
+ if (schema.minimum !== undefined) {
748
+ if (schema.exclusiveMinimum) {
749
+ if (instance <= schema.minimum) {
750
+ this.addError(path, `Value ${instance} must be greater than ${schema.minimum}`);
751
+ }
752
+ } else {
753
+ if (instance < schema.minimum) {
754
+ this.addError(path, `Value ${instance} must be at least ${schema.minimum}`);
755
+ }
756
+ }
757
+ }
758
+ if (schema.maximum !== undefined) {
759
+ if (schema.exclusiveMaximum) {
760
+ if (instance >= schema.maximum) {
761
+ this.addError(path, `Value ${instance} must be less than ${schema.maximum}`);
762
+ }
763
+ } else {
764
+ if (instance > schema.maximum) {
765
+ this.addError(path, `Value ${instance} must be at most ${schema.maximum}`);
766
+ }
767
+ }
768
+ }
769
+ if (schema.divisibleBy !== undefined && schema.divisibleBy !== 0) {
770
+ if (instance % schema.divisibleBy !== 0) {
771
+ this.addError(path, `Value ${instance} is not divisible by ${schema.divisibleBy}`);
772
+ }
773
+ }
774
+ }
775
+ if (typeof instance === "object" && instance !== null && !Array.isArray(instance)) {
776
+ const obj = instance;
777
+ const objKeys = Object.keys(obj);
778
+ if (schema.properties) {
779
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
780
+ const propPath = path ? `${path}.${key}` : key;
781
+ if (key in obj) {
782
+ this.doValidate(obj[key], propSchema, propPath);
783
+ } else if (propSchema.required === true) {
784
+ this.addError(propPath, `Property is required`);
785
+ }
786
+ }
787
+ }
788
+ if (schema.patternProperties) {
789
+ for (const [pattern, propSchema] of Object.entries(schema.patternProperties)) {
790
+ const re = new RegExp(pattern);
791
+ for (const key of objKeys) {
792
+ if (re.test(key)) {
793
+ const propPath = path ? `${path}.${key}` : key;
794
+ this.doValidate(obj[key], propSchema, propPath);
795
+ }
796
+ }
797
+ }
798
+ }
799
+ if (schema.additionalProperties !== undefined) {
800
+ const knownKeys = new Set;
801
+ if (schema.properties) {
802
+ for (const key of Object.keys(schema.properties)) {
803
+ knownKeys.add(key);
804
+ }
805
+ }
806
+ if (schema.patternProperties) {
807
+ for (const pattern of Object.keys(schema.patternProperties)) {
808
+ const re = new RegExp(pattern);
809
+ for (const key of objKeys) {
810
+ if (re.test(key))
811
+ knownKeys.add(key);
812
+ }
813
+ }
814
+ }
815
+ for (const key of objKeys) {
816
+ if (!knownKeys.has(key)) {
817
+ if (schema.additionalProperties === false) {
818
+ const propPath = path ? `${path}.${key}` : key;
819
+ this.addError(propPath, `Additional property not allowed`);
820
+ } else if (typeof schema.additionalProperties === "object") {
821
+ const propPath = path ? `${path}.${key}` : key;
822
+ this.doValidate(obj[key], schema.additionalProperties, propPath);
823
+ }
824
+ }
825
+ }
826
+ }
827
+ if (schema.dependencies) {
828
+ for (const [key, dep] of Object.entries(schema.dependencies)) {
829
+ if (key in obj) {
830
+ if (typeof dep === "string") {
831
+ if (!(dep in obj)) {
832
+ this.addError(path, `Property ${key} requires ${dep}`);
833
+ }
834
+ } else if (Array.isArray(dep)) {
835
+ for (const d of dep) {
836
+ if (!(d in obj)) {
837
+ this.addError(path, `Property ${key} requires ${d}`);
838
+ }
839
+ }
840
+ } else {
841
+ this.doValidate(instance, dep, path);
842
+ }
843
+ }
844
+ }
845
+ }
846
+ }
847
+ if (Array.isArray(instance)) {
848
+ if (schema.minItems !== undefined && instance.length < schema.minItems) {
849
+ this.addError(path, `Array has too few items (${instance.length} < ${schema.minItems})`);
850
+ }
851
+ if (schema.maxItems !== undefined && instance.length > schema.maxItems) {
852
+ this.addError(path, `Array has too many items (${instance.length} > ${schema.maxItems})`);
853
+ }
854
+ if (schema.uniqueItems === true) {
855
+ for (let i = 0;i < instance.length; i++) {
856
+ for (let j = i + 1;j < instance.length; j++) {
857
+ if (deepEqual(instance[i], instance[j])) {
858
+ this.addError(path, `Array items must be unique (duplicate at index ${j})`);
859
+ }
860
+ }
861
+ }
862
+ }
863
+ if (schema.items) {
864
+ if (Array.isArray(schema.items)) {
865
+ for (let i = 0;i < instance.length; i++) {
866
+ if (i < schema.items.length) {
867
+ this.doValidate(instance[i], schema.items[i], `${path}[${i}]`);
868
+ }
869
+ }
870
+ } else {
871
+ for (let i = 0;i < instance.length; i++) {
872
+ this.doValidate(instance[i], schema.items, `${path}[${i}]`);
873
+ }
874
+ }
875
+ }
876
+ }
877
+ }
878
+ }
879
+ // src/cli.ts
880
+ import { readFileSync, writeFileSync } from "fs";
881
+
882
+ // src/version.ts
883
+ var VERSION = "0.0.3";
884
+
885
+ // src/cli.ts
886
+ function printVersion() {
887
+ process.stdout.write(`${VERSION}
888
+ `);
889
+ }
890
+ function printUsage() {
891
+ process.stdout.write(`Usage: jsonlint [OPTIONS] [FILE]
892
+
893
+ Options:
894
+ -v, --version Print version and exit
895
+ -s, --sort-keys Sort object keys in output
896
+ -i, --in-place Overwrite the input file with formatted output
897
+ -t, --indent CHAR Character(s) to use for indentation (default: " ")
898
+ -c, --compact Compact error display
899
+ -V, --validate FILE Validate against a JSON Schema file
900
+ -e, --environment ENV JSON Schema spec version (default: json-schema-draft-03)
901
+ -q, --quiet Do not print parsed JSON to stdout
902
+ -p, --pretty-print Force pretty printing (even if invalid)
903
+ -h, --help Show this help message
904
+
905
+ If FILE is omitted, reads from stdin.
906
+ `);
907
+ }
908
+ function parseArgs(args) {
909
+ const opts = {
910
+ file: null,
911
+ sortKeys: false,
912
+ inPlace: false,
913
+ indent: " ",
914
+ compact: false,
915
+ validate: null,
916
+ environment: "json-schema-draft-03",
917
+ quiet: false,
918
+ prettyPrint: false,
919
+ version: false
920
+ };
921
+ let i = 0;
922
+ while (i < args.length) {
923
+ const arg = args[i];
924
+ switch (arg) {
925
+ case "-v":
926
+ case "--version":
927
+ opts.version = true;
928
+ break;
929
+ case "-s":
930
+ case "--sort-keys":
931
+ opts.sortKeys = true;
932
+ break;
933
+ case "-i":
934
+ case "--in-place":
935
+ opts.inPlace = true;
936
+ break;
937
+ case "-t":
938
+ case "--indent":
939
+ i++;
940
+ opts.indent = args[i] ?? " ";
941
+ break;
942
+ case "-c":
943
+ case "--compact":
944
+ opts.compact = true;
945
+ break;
946
+ case "-V":
947
+ case "--validate":
948
+ i++;
949
+ opts.validate = args[i] ?? null;
950
+ break;
951
+ case "-e":
952
+ case "--environment":
953
+ i++;
954
+ opts.environment = args[i] ?? "json-schema-draft-03";
955
+ break;
956
+ case "-q":
957
+ case "--quiet":
958
+ opts.quiet = true;
959
+ break;
960
+ case "-p":
961
+ case "--pretty-print":
962
+ opts.prettyPrint = true;
963
+ break;
964
+ case "-h":
965
+ case "--help":
966
+ printUsage();
967
+ process.exit(0);
968
+ break;
969
+ default:
970
+ if (arg.startsWith("-")) {
971
+ process.stderr.write(`Unknown option: ${arg}
972
+ `);
973
+ process.exit(1);
974
+ }
975
+ opts.file = arg;
976
+ break;
977
+ }
978
+ i++;
979
+ }
980
+ return opts;
981
+ }
982
+ function sortObject(obj) {
983
+ if (Array.isArray(obj)) {
984
+ return obj.map(sortObject);
985
+ }
986
+ if (obj !== null && typeof obj === "object") {
987
+ const sorted = {};
988
+ const keys = Object.keys(obj).sort();
989
+ for (const key of keys) {
990
+ sorted[key] = sortObject(obj[key]);
991
+ }
992
+ return sorted;
993
+ }
994
+ return obj;
995
+ }
996
+ function readStdin() {
997
+ try {
998
+ return readFileSync("/dev/stdin", "utf-8");
999
+ } catch {
1000
+ process.stderr.write(`Error reading from stdin
1001
+ `);
1002
+ process.exit(1);
1003
+ }
1004
+ }
1005
+ function main(args) {
1006
+ const cliArgs = args ?? process.argv.slice(2);
1007
+ const opts = parseArgs(cliArgs);
1008
+ if (opts.version) {
1009
+ printVersion();
1010
+ return;
1011
+ }
1012
+ let input;
1013
+ let filename;
1014
+ if (opts.file) {
1015
+ try {
1016
+ input = readFileSync(opts.file, "utf-8");
1017
+ } catch {
1018
+ process.stderr.write(`Error: could not open file '${opts.file}'
1019
+ `);
1020
+ process.exit(1);
1021
+ return;
1022
+ }
1023
+ filename = opts.file;
1024
+ } else {
1025
+ input = readStdin();
1026
+ filename = "<stdin>";
1027
+ }
1028
+ const parser = new JsonParser;
1029
+ if (opts.compact) {
1030
+ parser.parseError = (str, hash) => {
1031
+ const line = hash.line;
1032
+ const col = hash.loc.last_column;
1033
+ const found = hash.token === "EOF" ? "EOF" : hash.text || hash.token;
1034
+ const expected = hash.expected.join(", ");
1035
+ const msg = `${filename}: line ${line}, col ${col}, found: '${found}' - expected: ${expected}.`;
1036
+ throw new ParseError(msg, hash);
1037
+ };
1038
+ }
1039
+ function formatCompactError(e) {
1040
+ if (opts.compact && e instanceof LexerError) {
1041
+ return `${filename}: line ${e.line}, col ${e.column}, ${e.message.split(`
1042
+ `)[0]}`;
1043
+ }
1044
+ return e instanceof Error ? e.message : String(e);
1045
+ }
1046
+ let parsed;
1047
+ try {
1048
+ parsed = parser.parse(input);
1049
+ } catch (e) {
1050
+ if (opts.prettyPrint) {
1051
+ const formatted = formatJson(input, opts.indent);
1052
+ process.stdout.write(formatted + `
1053
+ `);
1054
+ try {
1055
+ const reformatParser = new JsonParser;
1056
+ if (opts.compact) {
1057
+ reformatParser.parseError = parser.parseError;
1058
+ }
1059
+ reformatParser.parse(formatted);
1060
+ } catch (e2) {
1061
+ process.stderr.write(formatCompactError(e2) + `
1062
+ `);
1063
+ }
1064
+ process.exit(1);
1065
+ return;
1066
+ }
1067
+ process.stderr.write(formatCompactError(e) + `
1068
+ `);
1069
+ process.exit(1);
1070
+ return;
1071
+ }
1072
+ if (opts.validate) {
1073
+ let schemaInput;
1074
+ try {
1075
+ schemaInput = readFileSync(opts.validate, "utf-8");
1076
+ } catch {
1077
+ process.stderr.write(`Error: could not open schema file '${opts.validate}'
1078
+ `);
1079
+ process.exit(1);
1080
+ return;
1081
+ }
1082
+ let schema;
1083
+ try {
1084
+ schema = JSON.parse(schemaInput);
1085
+ } catch {
1086
+ process.stderr.write(`Error: invalid JSON in schema file '${opts.validate}'
1087
+ `);
1088
+ process.exit(1);
1089
+ return;
1090
+ }
1091
+ const validator = new SchemaValidator;
1092
+ const errors = validator.validate(parsed, schema);
1093
+ if (errors.length > 0) {
1094
+ for (const err of errors) {
1095
+ process.stderr.write(`Schema validation error: property '${err.property}': ${err.message}
1096
+ `);
1097
+ }
1098
+ process.exit(1);
1099
+ return;
1100
+ }
1101
+ }
1102
+ if (opts.sortKeys) {
1103
+ parsed = sortObject(parsed);
1104
+ }
1105
+ const output = JSON.stringify(parsed, null, opts.indent);
1106
+ if (opts.inPlace && opts.file) {
1107
+ writeFileSync(opts.file, output + `
1108
+ `, "utf-8");
1109
+ } else if (!opts.quiet) {
1110
+ process.stdout.write(output + `
1111
+ `);
1112
+ }
1113
+ }
1114
+ // src/index.ts
1115
+ var parser = new JsonParser;
1116
+ var formatter = { formatJson };
1117
+ export {
1118
+ parser,
1119
+ parse,
1120
+ main,
1121
+ formatter,
1122
+ formatJson,
1123
+ TokenType,
1124
+ SchemaValidator,
1125
+ ParseError,
1126
+ LexerError,
1127
+ Lexer,
1128
+ JsonParser
1129
+ };
@@ -0,0 +1,49 @@
1
+ export declare enum TokenType {
2
+ STRING = "STRING",
3
+ NUMBER = "NUMBER",
4
+ TRUE = "TRUE",
5
+ FALSE = "FALSE",
6
+ NULL = "NULL",
7
+ LBRACE = "{",
8
+ RBRACE = "}",
9
+ LBRACKET = "[",
10
+ RBRACKET = "]",
11
+ COMMA = ",",
12
+ COLON = ":",
13
+ EOF = "EOF",
14
+ INVALID = "INVALID"
15
+ }
16
+ export interface SourceLocation {
17
+ first_line: number;
18
+ last_line: number;
19
+ first_column: number;
20
+ last_column: number;
21
+ }
22
+ export interface Token {
23
+ type: TokenType;
24
+ value: string;
25
+ loc: SourceLocation;
26
+ }
27
+ export declare class LexerError extends Error {
28
+ line: number;
29
+ column: number;
30
+ position: number;
31
+ input: string;
32
+ constructor(message: string, line: number, column: number, position: number, input: string);
33
+ showPosition(): string;
34
+ }
35
+ export declare class Lexer {
36
+ private input;
37
+ private pos;
38
+ private line;
39
+ private column;
40
+ setInput(input: string): void;
41
+ private peek;
42
+ private advance;
43
+ private skipWhitespace;
44
+ private readString;
45
+ private readNumber;
46
+ private readKeyword;
47
+ private showPositionAt;
48
+ nextToken(): Token;
49
+ }
@@ -0,0 +1,34 @@
1
+ import { Lexer, LexerError } from "./lexer.js";
2
+ import type { SourceLocation } from "./lexer.js";
3
+ export { LexerError };
4
+ export interface ParseErrorHash {
5
+ text: string;
6
+ token: string;
7
+ line: number;
8
+ loc: SourceLocation;
9
+ expected: string[];
10
+ }
11
+ export declare class ParseError extends Error {
12
+ hash: ParseErrorHash;
13
+ constructor(message: string, hash: ParseErrorHash);
14
+ }
15
+ export declare class JsonParser {
16
+ lexer: Lexer;
17
+ private currentToken;
18
+ private previousToken;
19
+ private input;
20
+ yy: Record<string, unknown>;
21
+ parseError: ((str: string, hash: ParseErrorHash) => never) | null;
22
+ constructor();
23
+ parse(input: string): unknown;
24
+ private advance;
25
+ private expect;
26
+ private throwParseError;
27
+ private formatError;
28
+ private showPosition;
29
+ private parseValue;
30
+ private parseObject;
31
+ private parseMember;
32
+ private parseArray;
33
+ }
34
+ export declare function parse(input: string): unknown;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Minimal JSON Schema Draft-03 validator.
3
+ * Zero dependencies — implements the subset needed for feature parity with jsonlint.
4
+ */
5
+ export interface SchemaError {
6
+ property: string;
7
+ message: string;
8
+ }
9
+ export interface JsonSchema {
10
+ type?: string | string[];
11
+ properties?: Record<string, JsonSchema>;
12
+ items?: JsonSchema | JsonSchema[];
13
+ required?: boolean;
14
+ additionalProperties?: boolean | JsonSchema;
15
+ pattern?: string;
16
+ minLength?: number;
17
+ maxLength?: number;
18
+ minimum?: number;
19
+ maximum?: number;
20
+ exclusiveMinimum?: boolean;
21
+ exclusiveMaximum?: boolean;
22
+ minItems?: number;
23
+ maxItems?: number;
24
+ uniqueItems?: boolean;
25
+ enum?: unknown[];
26
+ default?: unknown;
27
+ description?: string;
28
+ title?: string;
29
+ $ref?: string;
30
+ id?: string;
31
+ extends?: JsonSchema | JsonSchema[];
32
+ disallow?: string | string[];
33
+ divisibleBy?: number;
34
+ format?: string;
35
+ patternProperties?: Record<string, JsonSchema>;
36
+ dependencies?: Record<string, string | string[] | JsonSchema>;
37
+ [key: string]: unknown;
38
+ }
39
+ export declare class SchemaValidator {
40
+ private schemas;
41
+ private errors;
42
+ validate(instance: unknown, schema: JsonSchema, path?: string): SchemaError[];
43
+ private collectSchemas;
44
+ private resolveRef;
45
+ private addError;
46
+ private doValidate;
47
+ }
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.0.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asymmetric-effort/jsonlint",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "A pure JavaScript/TypeScript JSON parser and validator with detailed error reporting — zero dependencies",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -20,6 +20,9 @@
20
20
  "LICENSE.txt",
21
21
  "README.md"
22
22
  ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
23
26
  "scripts": {
24
27
  "clean": "make clean",
25
28
  "lint": "make lint",