Skip to content

Commit d5eaa93

Browse files
committed
feat: Add support for pretty-printing of the JSON input
Comments can be retained in the consistently formatted output. Quotes around object keys can be stripped if the key is an identifier name (JSON5). BREAKING CHANGE: The option for pretty-printing *invalid input* has been renamed: -p (--pretty-print) ==> -P (--pretty-print-invalid) The option `-p (--pretty-print)` will newly prettify the raw (text) input instead of formatting the parsed JSON object.
1 parent 7a6f44d commit d5eaa93

File tree

13 files changed

+1489
-433
lines changed

13 files changed

+1489
-433
lines changed

README.md

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This is a fork of the original package with the following enhancements:
1717
* Optionally recognizes JavaScript-style comments and single quoted strings.
1818
* Optionally ignores trailing commas and reports duplicate object keys as an error.
1919
* Supports [JSON Schema] drafts 04, 06 and 07.
20+
* Offers pretty-printing including comment-stripping and object keys without quotes (JSON5).
2021
* Prefers the native JSON parser if possible to run [7x faster than the custom parser].
2122
* Reports errors with rich additional information. From the schema validation too.
2223
* Implements JavaScript modules using [UMD] to work everywhere.
@@ -76,25 +77,32 @@ By default, `jsonlint` will either report a syntax error with details or pretty-
7677

7778
Usage: jsonlint [options] [<file or directory> ...]
7879

79-
JSON parser and validator - checks syntax and semantics of JSON data.
80+
JSON parser, syntax and schema validator and pretty-printer.
8081

8182
Options:
82-
-s, --sort-keys sort object keys
83+
-s, --sort-keys sort object keys (not when prettifying)
8384
-E, --extensions [ext] file extensions to process for directory walk
8485
(default: ["json","JSON"])
8586
-i, --in-place overwrite the input files
86-
-t, --indent [char] characters to use for indentation (default: " ")
87+
-t, --indent [char] characters to use for indentation
88+
(default: " ")
8789
-c, --compact compact error display
88-
-M, --mode set other parsing flags according to a format type
90+
-M, --mode [mode] set other parsing flags according to a format
91+
type (default: "json")
8992
-C, --comments recognize and ignore JavaScript-style comments
9093
-S, --single-quoted-strings support single quotes as string delimiters
91-
-T, --trailing-commas' ignore trailing commas in objects and arrays
94+
-T, --trailing-commas ignore trailing commas in objects and arrays
9295
-D, --no-duplicate-keys report duplicate object keys as an error
9396
-V, --validate [file] JSON schema file to use for validation
94-
-e, --environment [env] which specification of JSON Schema
95-
the validation file uses
97+
-e, --environment [env] which specification of JSON Schema the
98+
validation file uses
9699
-q, --quiet do not print the parsed json to stdin
97-
-p, --pretty-print force pretty-printing even for invalid input
100+
-p, --pretty-print prettify the input instead of stringifying
101+
the parsed object
102+
-P, --pretty-print-invalid force pretty-printing even for invalid input
103+
--prune-comments omit comments from the prettified output
104+
--strip-object-keys strip quotes from object keys if possible
105+
(JSON5)
98106
-v, --version output the version number
99107
-h, --help output usage information
100108

@@ -138,21 +146,21 @@ The exported `parse` method is compatible with the native `JSON.parse` method. T
138146

139147
The `parse` method offers more detailed [error information](#error-handling), than the native `JSON.parse` method and it supports additional parsing options:
140148

141-
| Option | Description |
142-
| -------------------------- | --------------------------- |
149+
| Option | Description |
150+
| -------------------------- | ------------------------------------------- |
143151
| `ignoreComments` | ignores single-line and multi-line JavaScript-style comments during parsing as another "whitespace" (boolean) |
144-
| `ignoreTrailingCommas` | ignores trailing commas in objects and arrays (boolean) |
145-
| `allowSingleQuotedStrings` | accepts strings delimited by single-quotes too (boolean) |
152+
| `ignoreTrailingCommas` | ignores trailing commas in objects and arrays (boolean) |
153+
| `allowSingleQuotedStrings` | accepts strings delimited by single-quotes too (boolean) |
146154
| `allowDuplicateObjectKeys` | allows reporting duplicate object keys as an error (boolean) |
147155
| `mode` | sets multiple options according to the type of input data (string) |
148156
| `reviver` | converts object and array values (function) |
149157

150158
The `mode` parameter (string) sets parsing options to match a common format of input data:
151159

152-
| Mode | Description |
153-
| ------- | --------------------------- |
160+
| Mode | Description |
161+
| ------- | --------------------------------------------------------- |
154162
| `json` | complies to the pure standard [JSON] (default if not set) |
155-
| `cjson` | JSON with comments (sets `ignoreComments`) |
163+
| `cjson` | JSON with comments (sets `ignoreComments`) |
156164
| `json5` | complies to [JSON5] (sets `ignoreComments`, `allowSingleQuotedStrings`, `ignoreTrailingCommas` and enables other JSON5 features) |
157165

158166
### Schema Validation
@@ -174,15 +182,84 @@ const validate = compile('string with JSON schema', {
174182
})
175183
```
176184

185+
### Pretty-Printing
186+
187+
You can parse a JSON string to an array of tokens and print it back to a string with some changes applied. It can be unification of whitespace or tripping comments, for example. (Raw token values must be enabled when tokenizing the JSON input.)
188+
189+
```js
190+
const { tokenize } = require('@prantlf/jsonlint')
191+
const tokens = tokenize('string with JSON data', { rawTokens: true })
192+
const { print } = require('@prantlf/jsonlint/lib/printer')
193+
const output = print(tokens, { indent: ' ' })
194+
```
195+
196+
The [`tokenize`](#tokenizing) method accepts options in the second optional parameter. See the [`tokenize`](#tokenizing) method above for more information.
197+
198+
The [`print`](#pretty-printing) method accepts an object `options` as the second optional parameter. The following properties will be recognized there:
199+
200+
| Option | Description |
201+
| --------------------------- | ------------------------------------------------------- |
202+
| `indent` | whitespace characters to be used as an indentation unit |
203+
| `pruneComments` | will omit all tokens with comments |
204+
| `stripObjectKeys` | will not print quotes around object keys which are JavaScript identifier names |
205+
206+
```js
207+
// Just concatenate the tokens to produce the same output as was the input.
208+
print(tokens)
209+
// Strip all whitespace. (Just like `JSON.stringify(json)` would do it,
210+
// but leaving comments in the output.)
211+
print(tokens, {})
212+
// Print to multiple lines without object and array indentation.
213+
// (Just introduce line breaks.)
214+
print(tokens, { indent: '' })
215+
// Print to multiple lines with object and array indentation. (Just like
216+
//`JSON.stringify(json, undefined, ' ')` would do it, but retaining comments.)
217+
print(tokens, { indent: ' ' })
218+
// Print to multiple lines with object and array indentation, omit comments.
219+
// (Just like `JSON.stringify(json, undefined, ' ')` would do it.)
220+
print(tokens, { indent: ' ', pruneComments: true })
221+
// Print to multiple lines with indentation enabled and JSON5 object keys.
222+
print(tokens, { indent: ' ', stripObjectKeys: true })
223+
```
224+
225+
### Tokenizing
226+
227+
The method `tokenize` has the same prototype as the method [`parse`](#module-interface), but returns an array of tokens instead of the JSON object.
228+
229+
```js
230+
const { tokenize } = require('@prantlf/jsonlint')
231+
const tokens = tokenize('{"flag":true /* default */}', { ignoreComments: true }))
232+
// Returns the following array:
233+
// [
234+
// { type: 'symbol', raw: '{', value: '{' },
235+
// { type: 'literal', raw: '"flag"', value: 'flag' },
236+
// { type: 'symbol', raw: ':', value: ':' },
237+
// { type: 'literal', raw: 'true', value: true },
238+
// { type: 'whitespace', raw: ' ' },
239+
// { type: 'comment', raw: '/* default */' },
240+
// { type: 'symbol', raw: '}', value: '}' }
241+
// ]
242+
```
243+
244+
The `tokenize` method accepts options in the second optional parameter. See the [`parse`](#module-interface) method above for the shared options. There are several additional options supported for the tokenization:
245+
246+
| Option | Description |
247+
| -----------------| ------------------------------------------------------------------ |
248+
| `rawTokens` | adds a `raw` property with the original string from the JSON input |
249+
| `tokenLocations` | adds a `location` property with start, end and length of the original string from the JSON input |
250+
| `tokenPaths` | adds a `path` property with an array of keys and array indexes "on the way to" the token's value |
251+
252+
If you want to retain comments or whitespace for pretty-printing, for example, set `rawTokens` to true. (The [`print`](#pretty-printing) method requires tokens produced with this flag enabled.)
253+
177254
### Performance
178255

179256
This is a part of an output from the [parser benchmark], when parsing a 4.2 KB formatted string ([package.json](./package.json)) with Node.js 10.15.3:
180257

181258
the built-in parser x 68,212 ops/sec ±0.86% (87 runs sampled)
182259
the pure jju parser x 10,234 ops/sec ±1.08% (89 runs sampled)
183260
the extended jju parser x 10,210 ops/sec ±1.26% (88 runs sampled)
184-
the tokenisable jju parser x 8,832 ops/sec ±0.92% (89 runs sampled)
185-
the tokenising jju parser x 7,911 ops/sec ±1.05% (86 runs sampled)
261+
the tokenizable jju parser x 8,832 ops/sec ±0.92% (89 runs sampled)
262+
the tokenizing jju parser x 7,911 ops/sec ±1.05% (86 runs sampled)
186263

187264
A custom JSON parser is [a lot slower] than the built-in one. However, it is more important to have a [clear error reporting] than the highest speed in scenarios like parsing configuration files. Extending the parser with the support for comments and single-quoted strings does not affect the performance. Making the parser collect tokens and their locations decreases the performance a bit.
188265

lib/cli.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var fs = require('fs')
44
var path = require('path')
55
var parser = require('./jsonlint')
66
var formatter = require('./formatter')
7+
var printer = require('./printer')
78
var sorter = require('./sorter')
89
var validator = require('./validator')
910
var pkg = require('../package')
@@ -16,7 +17,7 @@ var options = require('commander')
1617
.name('jsonlint')
1718
.usage('[options] [<file or directory> ...]')
1819
.description(pkg.description)
19-
.option('-s, --sort-keys', 'sort object keys')
20+
.option('-s, --sort-keys', 'sort object keys (not when prettifying)')
2021
.option('-E, --extensions [ext]', 'file extensions to process for directory walk', collectExtensions, ['json', 'JSON'])
2122
.option('-i, --in-place', 'overwrite the input files')
2223
.option('-t, --indent [char]', 'characters to use for indentation', ' ')
@@ -29,7 +30,10 @@ var options = require('commander')
2930
.option('-V, --validate [file]', 'JSON schema file to use for validation')
3031
.option('-e, --environment [env]', 'which specification of JSON Schema the validation file uses')
3132
.option('-q, --quiet', 'do not print the parsed json to stdin')
32-
.option('-p, --pretty-print', 'force pretty-printing even for invalid input')
33+
.option('-p, --pretty-print', 'prettify the input instead of stringifying the parsed object')
34+
.option('-P, --pretty-print-invalid', 'force pretty-printing even for invalid input')
35+
.option('--prune-comments', 'omit comments from the prettified output')
36+
.option('--strip-object-keys', 'strip quotes from object keys if possible (JSON5)')
3337
.version(pkg.version, '-v, --version')
3438
.on('--help', () => {
3539
console.log()
@@ -75,12 +79,22 @@ function parse (source, file) {
7579
} else {
7680
parsed = parser.parse(source, parserOptions)
7781
}
82+
if (options.prettyPrint) {
83+
parserOptions.rawTokens = true
84+
var tokens = parser.tokenize(source, parserOptions)
85+
// TODO: Support sorting tor the tokenized input too.
86+
return printer.print(tokens, {
87+
indent: options.indent,
88+
pruneComments: options.pruneComments,
89+
stripObjectKeys: options.stripObjectKeys
90+
})
91+
}
7892
if (options.sortKeys) {
7993
parsed = sorter.sortObject(parsed)
8094
}
8195
return JSON.stringify(parsed, null, options.indent)
8296
} catch (e) {
83-
if (options.prettyPrint) {
97+
if (options.prettyPrintInvalid) {
8498
/* From https://github.com/umbrae/jsonlintdotcom:
8599
* If we failed to validate, run our manual formatter and then re-validate so that we
86100
* can get a better line number. On a successful validate, we don't want to run our

lib/formatter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
function format (json, indentChars) {
2424
var i = 0
2525
var il = 0
26-
var tab = (typeof indentChars !== 'undefined') ? indentChars : ' '
26+
var tab = indentChars !== undefined ? indentChars : ' '
2727
var newJson = ''
2828
var indentLevel = 0
2929
var inString = false

lib/jsonlint.d.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ interface ParseOptions {
1010
}
1111

1212
/**
13-
* Parses a string formatted as JSON. It is compatible with the native
14-
* `JSON.parse` method.
13+
* Parses a string formatted as JSON to a JSON output (primitive type, object
14+
* or array). It is compatible with the native `JSON.parse` method.
1515
*
1616
* @example
1717
* ```ts
@@ -26,6 +26,34 @@ interface ParseOptions {
2626
*/
2727
declare function parse (input: string, reviverOrOptions?: Function | ParseOptions): object
2828

29+
interface TokenizeOptions {
30+
ignoreComments?: boolean
31+
ignoreTrailingCommas?: boolean
32+
allowSingleQuotedStrings?: boolean
33+
allowDuplicateObjectKeys?: boolean
34+
mode?: ParseMode
35+
reviver?: Function
36+
rawTokens?: boolean
37+
tokenLocations?: boolean
38+
tokenPaths?: boolean
39+
}
40+
41+
/**
42+
* Parses a string formatted as JSON to an array of JSON tokens.
43+
*
44+
* @example
45+
* ```ts
46+
* import { tokenize } from '@prantlf/jsonlint'
47+
* const tokens = tokenize('string with JSON data')
48+
* ```
49+
*
50+
* @param input - a string input to parse
51+
* @param reviverOrOptions - either a value reviver or an object
52+
* with multiple options
53+
* @returns an array with the tokens
54+
*/
55+
declare function tokenize (input: string, reviverOrOptions?: Function | TokenizeOptions): object
56+
2957
declare module '@prantlf/jsonlint/lib/validator' {
3058
type Environment = 'json-schema-draft-04' | 'json-schema-draft-06' | 'json-schema-draft-07'
3159

@@ -57,4 +85,29 @@ declare module '@prantlf/jsonlint/lib/validator' {
5785
function compile (schema: string, environmentOrOptions?: Environment | CompileOptions): Function
5886
}
5987

60-
export { parse }
88+
declare module '@prantlf/jsonlint/lib/printer' {
89+
interface PrintOptions {
90+
indent?: string
91+
pruneComments?: boolean
92+
stripObjectKeys?: boolean
93+
}
94+
95+
/**
96+
* Pretty-prints an array of JSON tokens parsed from a valid JSON string by `tokenize`.
97+
*
98+
* @example
99+
* ```ts
100+
* import { tokenize } from '@prantlf/jsonlint'
101+
* import { print } from '@prantlf/jsonlint/lib/printer'
102+
* const tokens = tokenize('string with JSON data', { rawTokens: true })
103+
* const outputString = print(tokens, { indent: ' ' })
104+
* ```
105+
*
106+
* @param tokens - an array of JSON tokens
107+
* @param options - an object with multiple options
108+
* @returns the output string
109+
*/
110+
function print (tokens: Array<object>, options?: PrintOptions): string
111+
}
112+
113+
export { parse, tokenize }

0 commit comments

Comments
 (0)