├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md └── workflows │ └── nodejs.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── appveyor.yml ├── index.d.ts ├── package.json ├── src ├── array.js ├── common.js ├── index.js ├── parse.js └── stringify.js └── test ├── array.test.js ├── fixtures ├── deep-null-2.json ├── deep-null-3.json ├── deep-null-null.json ├── duplex-null-2.json ├── duplex-null-3.json ├── duplex-null-null.json ├── indent-null-2.json ├── indent-null-3.json ├── indent-null-null.json ├── simple-null-2.json ├── simple-null-3.json ├── simple-null-null.json ├── single-right-null-2.json ├── single-right-null-3.json ├── single-right-null-null.json ├── single-top-null-2.json ├── single-top-null-3.json └── single-top-null-null.json ├── others.test.js ├── parse.test.js ├── stringify.test.js └── ts ├── test-ts.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css,md}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /* 2 | !src 3 | !test 4 | test/ts 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: require.resolve('@ostai/eslint-config'), 3 | rules: { 4 | 'no-underscore-dangle': 0, 5 | 'no-cond-assign': 0, 6 | 'no-loop-func': 0, 7 | 'operator-linebreak': 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Bug report" 3 | about: Report an issue with comment-json 4 | title: '' 5 | labels: bug, triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Tell us about your environment** 11 | 12 | * **Node Version:** 13 | * **comment-json Version** 14 | 15 | **Please show your use case / code slices / code link that could reproduce the issue** 16 | 17 | 18 | ```js 19 | 20 | ``` 21 | 22 | **What did you expect to happen?** 23 | 24 | **Are you willing to submit a pull request to fix this bug?** 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Propose a new feature to be added to comment-json 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Why this feature is better to be included in comment-json? Please describe the scenario** 11 | 12 | 13 | **Provide 1-2 code examples that the usage of the new feature:** 14 | 15 | 16 | ```js 17 | 18 | ``` 19 | 20 | **Are you willing to submit a pull request to implement this rule?** 21 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [18.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm install 23 | npm run build --if-present 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node, Coverage, NPM 2 | /.nyc_output 3 | /coverage 4 | /coverage.lcov 5 | /package-lock.json 6 | /*.tgz 7 | /test/ts/*.js 8 | 9 | 10 | # Numerous always-ignore extensions 11 | *.bak 12 | *.patch 13 | *.diff 14 | *.err 15 | *.orig 16 | *.log 17 | *.rej 18 | *.swo 19 | *.swp 20 | *.zip 21 | *.vi 22 | *~ 23 | *.sass-cache 24 | *.lock 25 | *.rdb 26 | *.db 27 | nohup.out 28 | 29 | # OS or Editor folders 30 | .DS_Store 31 | ._* 32 | .cache 33 | .project 34 | .settings 35 | .tmproj 36 | *.esproj 37 | *.*-project 38 | *.*-workspace 39 | nbproject 40 | thumbs.db 41 | 42 | # Folders to ignore 43 | .hg 44 | .svn 45 | .CVS 46 | .idea 47 | node_modules 48 | old/ 49 | *-old/ 50 | *-notrack/ 51 | no-track/ 52 | build/ 53 | combo/ 54 | reference/ 55 | jscoverage_lib/ 56 | temp/ 57 | tmp/ 58 | 59 | # Java 60 | .mvn 61 | .gradle 62 | .vscode 63 | .project 64 | .classpath 65 | /gradle 66 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 kaelzhang <>, contributors 2 | http://kael.me/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/kaelzhang/node-comment-json/actions/workflows/nodejs.yml/badge.svg)](https://github.com/kaelzhang/node-comment-json/actions/workflows/nodejs.yml) 2 | [![Coverage](https://codecov.io/gh/kaelzhang/node-comment-json/branch/master/graph/badge.svg)](https://codecov.io/gh/kaelzhang/node-comment-json) 3 | [![npm module downloads per month](http://img.shields.io/npm/dm/comment-json.svg)](https://www.npmjs.org/package/comment-json) 4 | 7 | 10 | 13 | 14 | # comment-json 15 | 16 | Parse and stringify JSON with comments. It will retain comments even after saved! 17 | 18 | - [Parse](#parse) JSON strings with comments into JavaScript objects and MAINTAIN comments 19 | - supports comments everywhere, yes, **EVERYWHERE** in a JSON file, eventually 😆 20 | - fixes the known issue about comments inside arrays. 21 | - [Stringify](#stringify) the objects into JSON strings with comments if there are 22 | 23 | The usage of `comment-json` is exactly the same as the vanilla [`JSON`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) object. 24 | 25 | ## Table of Contents 26 | 27 | - [Why](#why) and [How](#how) 28 | - [Usage and examples](#usage) 29 | - API reference 30 | - [parse](#parse) 31 | - [stringify](#stringify) 32 | - [assign](#assigntarget-object-source-object-keys-array) 33 | - [CommentArray](#commentarray) 34 | - [Change Logs](https://github.com/kaelzhang/node-comment-json/releases) 35 | 36 | ## Why? 37 | 38 | There are many other libraries that can deal with JSON with comments, such as [json5](https://npmjs.org/package/json5), or [strip-json-comments](https://npmjs.org/package/strip-json-comments), but none of them can stringify the parsed object and return back a JSON string the same as the original content. 39 | 40 | Imagine that if the user settings are saved in `${library}.json`, and the user has written a lot of comments to improve readability. If the library `library` need to modify the user setting, such as modifying some property values and adding new fields, and if the library uses `json5` to read the settings, all comments will disappear after modified which will drive people insane. 41 | 42 | So, **if you want to parse a JSON string with comments, modify it, then save it back**, `comment-json` is your must choice! 43 | 44 | ## How? 45 | 46 | `comment-json` parse JSON strings with comments and save comment tokens into [symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) properties. 47 | 48 | For JSON array with comments, `comment-json` extends the vanilla `Array` object into [`CommentArray`](#commentarray) whose instances could handle comments changes even after a comment array is modified. 49 | 50 | ## Install 51 | 52 | ```sh 53 | $ npm i comment-json 54 | ``` 55 | 56 | ~~For TypeScript developers, [`@types/comment-json`](https://www.npmjs.com/package/@types/comment-json) could be used~~ 57 | 58 | Since `2.4.1`, `comment-json` contains typescript declarations, so you might as well remove `@types/comment-json`. 59 | 60 | ## Usage 61 | 62 | package.json: 63 | 64 | ```js 65 | { 66 | // package name 67 | "name": "comment-json" 68 | } 69 | ``` 70 | 71 | ```js 72 | const { 73 | parse, 74 | stringify, 75 | assign 76 | } = require('comment-json') 77 | const fs = require('fs') 78 | 79 | const obj = parse(fs.readFileSync('package.json').toString()) 80 | 81 | console.log(obj.name) // comment-json 82 | 83 | stringify(obj, null, 2) 84 | // Will be the same as package.json, Oh yeah! 😆 85 | // which will be very useful if we use a json file to store configurations. 86 | ``` 87 | 88 | ### Sort keys 89 | 90 | It is a common use case to sort the keys of a JSON file 91 | 92 | ```js 93 | const parsed = parse(`{ 94 | // b 95 | "b": 2, 96 | // a 97 | "a": 1 98 | }`) 99 | 100 | // Copy the properties including comments from `parsed` to the new object `{}` 101 | // according to the sequence of the given keys 102 | const sorted = assign( 103 | {}, 104 | parsed, 105 | // You could also use your custom sorting function 106 | Object.keys(parsed).sort() 107 | ) 108 | 109 | console.log(stringify(sorted, null, 2)) 110 | // { 111 | // // a 112 | // "a": 1, 113 | // // b 114 | // "b": 2 115 | // } 116 | ``` 117 | 118 | For details about `assign`, see [here](#assigntarget-object-source-object-keys-array). 119 | 120 | ## parse() 121 | 122 | ```ts 123 | parse(text, reviver? = null, remove_comments? = false) 124 | : object | string | number | boolean | null 125 | ``` 126 | 127 | - **text** `string` The string to parse as JSON. See the [JSON](http://json.org/) object for a description of JSON syntax. 128 | - **reviver?** `Function() | null` Default to `null`. It acts the same as the second parameter of [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse). If a function, prescribes how the value originally produced by parsing is transformed, before being returned. 129 | - **remove_comments?** `boolean = false` If true, the comments won't be maintained, which is often used when we want to get a clean object. 130 | 131 | Returns `CommentJSONValue` (`object | string | number | boolean | null`) corresponding to the given JSON text. 132 | 133 | If the `content` is: 134 | 135 | ```js 136 | /** 137 | before-all 138 | */ 139 | // before-all 140 | { // before:foo 141 | // before:foo 142 | /* before:foo */ 143 | "foo" /* after-prop:foo */: // after-colon:foo 144 | 1 // after-value:foo 145 | // after-value:foo 146 | , // after:foo 147 | // before:bar 148 | "bar": [ // before:0 149 | // before:0 150 | "baz" // after-value:0 151 | // after-value:0 152 | , // after:0 153 | "quux" 154 | // after:1 155 | ] // after:bar 156 | // after:bar 157 | } 158 | // after-all 159 | ``` 160 | 161 | ```js 162 | const {inspect} = require('util') 163 | 164 | const parsed = parse(content) 165 | 166 | console.log( 167 | inspect(parsed, { 168 | // Since 4.0.0, symbol properties of comments are not enumerable, 169 | // use `showHidden: true` to print them 170 | showHidden: true 171 | }) 172 | ) 173 | 174 | console.log(Object.keys(parsed)) 175 | // > ['foo', 'bar'] 176 | 177 | console.log(stringify(parsed, null, 2)) 178 | // 🚀 Exact as the content above! 🚀 179 | ``` 180 | 181 | And the value of `parsed` will be: 182 | 183 | ```js 184 | { 185 | // Comments before the JSON object 186 | [Symbol.for('before-all')]: [{ 187 | type: 'BlockComment', 188 | value: '\n before-all\n ', 189 | inline: false, 190 | loc: { 191 | // The start location of `/**` 192 | start: { 193 | line: 1, 194 | column: 0 195 | }, 196 | // The end location of `*/` 197 | end: { 198 | line: 3, 199 | column: 3 200 | } 201 | } 202 | }, { 203 | type: 'LineComment', 204 | value: ' before-all', 205 | inline: false, 206 | loc: ... 207 | }], 208 | ... 209 | 210 | [Symbol.for('after-prop:foo')]: [{ 211 | type: 'BlockComment', 212 | value: ' after-prop:foo ', 213 | inline: true, 214 | loc: ... 215 | }], 216 | 217 | // The real value 218 | foo: 1, 219 | bar: [ 220 | "baz", 221 | "quux", 222 | 223 | // The property of the array 224 | [Symbol.for('after-value:0')]: [{ 225 | type: 'LineComment', 226 | value: ' after-value:0', 227 | inline: true, 228 | loc: ... 229 | }, ...], 230 | ... 231 | ] 232 | } 233 | ``` 234 | 235 | There are **EIGHT** kinds of symbol properties: 236 | 237 | ```js 238 | // Comments before everything 239 | Symbol.for('before-all') 240 | 241 | // If all things inside an object or an array are comments 242 | Symbol.for('before') 243 | 244 | // comment tokens before 245 | // - a property of an object 246 | // - an item of an array 247 | // and after the previous comma(`,`) or the opening bracket(`{` or `[`) 248 | Symbol.for(`before:${prop}`) 249 | 250 | // comment tokens after property key `prop` and before colon(`:`) 251 | Symbol.for(`after-prop:${prop}`) 252 | 253 | // comment tokens after the colon(`:`) of property `prop` and before property value 254 | Symbol.for(`after-colon:${prop}`) 255 | 256 | // comment tokens after 257 | // - the value of property `prop` inside an object 258 | // - the item of index `prop` inside an array 259 | // and before the next key-value/item delimiter(`,`) 260 | // or the closing bracket(`}` or `]`) 261 | Symbol.for(`after-value:${prop}`) 262 | 263 | // comment tokens after 264 | // - comma(`,`) 265 | // - the value of property `prop` if it is the last property 266 | Symbol.for(`after:${prop}`) 267 | 268 | // Comments after everything 269 | Symbol.for('after-all') 270 | ``` 271 | 272 | And the value of each symbol property is an **array** of `CommentToken` 273 | 274 | ```ts 275 | interface CommentToken { 276 | type: 'BlockComment' | 'LineComment' 277 | // The content of the comment, including whitespaces and line breaks 278 | value: string 279 | // If the start location is the same line as the previous token, 280 | // then `inline` is `true` 281 | inline: boolean 282 | 283 | // But pay attention that, 284 | // locations will NOT be maintained when stringified 285 | loc: CommentLocation 286 | } 287 | 288 | interface CommentLocation { 289 | // The start location begins at the `//` or `/*` symbol 290 | start: Location 291 | // The end location of multi-line comment ends at the `*/` symbol 292 | end: Location 293 | } 294 | 295 | interface Location { 296 | line: number 297 | column: number 298 | } 299 | ``` 300 | 301 | ### Query comments in TypeScript 302 | 303 | `comment-json` provides a `symbol`-type called `CommentSymbol` which can be used for querying comments. 304 | Furthermore, a type `CommentDescriptor` is provided for enforcing properly formatted symbol names: 305 | 306 | ```ts 307 | import { 308 | CommentDescriptor, CommentSymbol, parse, CommentArray 309 | } from 'comment-json' 310 | 311 | const parsed = parse(`{ /* test */ "foo": "bar" }`) 312 | // typescript only allows properly formatted symbol names here 313 | const symbolName: CommentDescriptor = 'before:foo' 314 | 315 | console.log((parsed as CommentArray)[Symbol.for(symbolName) as CommentSymbol][0].value) 316 | ``` 317 | 318 | In this example, casting to `Symbol.for(symbolName)` to `CommentSymbol` is mandatory. 319 | Otherwise, TypeScript won't detect that you're trying to query comments. 320 | 321 | ### Parse into an object without comments 322 | 323 | ```js 324 | console.log(parse(content, null, true)) 325 | ``` 326 | 327 | And the result will be: 328 | 329 | ```js 330 | { 331 | foo: 1, 332 | bar: [ 333 | "baz", 334 | "quux" 335 | ] 336 | } 337 | ``` 338 | 339 | ### Special cases 340 | 341 | ```js 342 | const parsed = parse(` 343 | // comment 344 | 1 345 | `) 346 | 347 | console.log(parsed === 1) 348 | // false 349 | ``` 350 | 351 | If we parse a JSON of primative type with `remove_comments:false`, then the return value of `parse()` will be of object type. 352 | 353 | The value of `parsed` is equivalent to: 354 | 355 | ```js 356 | const parsed = new Number(1) 357 | 358 | parsed[Symbol.for('before-all')] = [{ 359 | type: 'LineComment', 360 | value: ' comment', 361 | inline: false, 362 | loc: ... 363 | }] 364 | ``` 365 | 366 | Which is similar for: 367 | 368 | - `Boolean` type 369 | - `String` type 370 | 371 | For example 372 | 373 | ```js 374 | const parsed = parse(` 375 | "foo" /* comment */ 376 | `) 377 | ``` 378 | 379 | Which is equivalent to 380 | 381 | ```js 382 | const parsed = new String('foo') 383 | 384 | parsed[Symbol.for('after-all')] = [{ 385 | type: 'BlockComment', 386 | value: ' comment ', 387 | inline: true, 388 | loc: ... 389 | }] 390 | ``` 391 | 392 | But there is one exception: 393 | 394 | ```js 395 | const parsed = parse(` 396 | // comment 397 | null 398 | `) 399 | 400 | console.log(parsed === null) // true 401 | ``` 402 | 403 | ## stringify() 404 | 405 | ```ts 406 | stringify(object: any, replacer?, space?): string 407 | ``` 408 | 409 | The arguments are the same as the vanilla [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify). 410 | 411 | And it does the similar thing as the vanilla one, but also deal with extra properties and convert them into comments. 412 | 413 | ```js 414 | console.log(stringify(parsed, null, 2)) 415 | // Exactly the same as `content` 416 | ``` 417 | 418 | #### space 419 | 420 | If space is not specified, or the space is an empty string, the result of `stringify()` will have no comments. 421 | 422 | For the case above: 423 | 424 | ```js 425 | console.log(stringify(result)) // {"a":1} 426 | console.log(stringify(result, null, 2)) // is the same as `code` 427 | ``` 428 | 429 | ## assign(target: object, source?: object, keys?: Array) 430 | 431 | - **target** `object` the target object 432 | - **source?** `object` the source object. This parameter is optional but it is silly to not pass this argument. 433 | - **keys?** `Array` If not specified, all enumerable own properties of `source` will be used. 434 | 435 | This method is used to copy the enumerable own properties and their corresponding comment symbol properties to the target object. 436 | 437 | ```js 438 | const parsed = parse(`// before all 439 | { 440 | // This is a comment 441 | "foo": "bar" 442 | }`) 443 | 444 | const obj = assign({ 445 | bar: 'baz' 446 | }, parsed) 447 | 448 | stringify(obj, null, 2) 449 | // // before all 450 | // { 451 | // "bar": "baz", 452 | // // This is a comment 453 | // "foo": "bar" 454 | // } 455 | ``` 456 | 457 | ### Special cases about `keys` 458 | 459 | But if argument `keys` is specified and is not empty, then comment ` before all`, which belongs to no properties, will **NOT** be copied. 460 | 461 | ```js 462 | const obj = assign({ 463 | bar: 'baz' 464 | }, parsed, ['foo']) 465 | 466 | stringify(obj, null, 2) 467 | // { 468 | // "bar": "baz", 469 | // // This is a comment 470 | // "foo": "bar" 471 | // } 472 | ``` 473 | 474 | Specifying the argument `keys` as an empty array indicates that it will only copy non-property symbols of comments 475 | 476 | ```js 477 | const obj = assign({ 478 | bar: 'baz' 479 | }, parsed, []) 480 | 481 | stringify(obj, null, 2) 482 | // // before all 483 | // { 484 | // "bar": "baz", 485 | // } 486 | ``` 487 | 488 | Non-property symbols include: 489 | 490 | ```js 491 | Symbol.for('before-all') 492 | Symbol.for('before') 493 | Symbol.for('after-all') 494 | ``` 495 | 496 | ## `CommentArray` 497 | 498 | > Advanced Section 499 | 500 | All arrays of the parsed object are `CommentArray`s. 501 | 502 | The constructor of `CommentArray` could be accessed by: 503 | 504 | ```js 505 | const {CommentArray} = require('comment-json') 506 | ``` 507 | 508 | If we modify a comment array, its comment symbol properties could be handled automatically. 509 | 510 | ```js 511 | const parsed = parse(`{ 512 | "foo": [ 513 | // bar 514 | "bar", 515 | // baz, 516 | "baz" 517 | ] 518 | }`) 519 | 520 | parsed.foo.unshift('qux') 521 | 522 | stringify(parsed, null, 2) 523 | // { 524 | // "foo": [ 525 | // "qux", 526 | // // bar 527 | // "bar", 528 | // // baz 529 | // "baz" 530 | // ] 531 | // } 532 | ``` 533 | 534 | Oh yeah! 😆 535 | 536 | But pay attention, if you reassign the property of a comment array with a normal array, all comments will be gone: 537 | 538 | ```js 539 | parsed.foo = ['quux'].concat(parsed.foo) 540 | stringify(parsed, null, 2) 541 | // { 542 | // "foo": [ 543 | // "quux", 544 | // "qux", 545 | // "bar", 546 | // "baz" 547 | // ] 548 | // } 549 | 550 | // Whoooops!! 😩 Comments are gone 551 | ``` 552 | 553 | Instead, we should: 554 | 555 | ```js 556 | parsed.foo = new CommentArray('quux').concat(parsed.foo) 557 | stringify(parsed, null, 2) 558 | // { 559 | // "foo": [ 560 | // "quux", 561 | // "qux", 562 | // // bar 563 | // "bar", 564 | // // baz 565 | // "baz" 566 | // ] 567 | // } 568 | ``` 569 | 570 | ## Special Cases about Trailing Comma 571 | 572 | If we have a JSON string `str` 573 | 574 | ```js 575 | { 576 | "foo": "bar", // comment 577 | } 578 | ``` 579 | 580 | ```js 581 | // When stringify, trailing commas will be eliminated 582 | const stringified = stringify(parse(str), null, 2) 583 | console.log(stringified) 584 | ``` 585 | 586 | And it will print: 587 | 588 | ```js 589 | { 590 | "foo": "bar" // comment 591 | } 592 | ``` 593 | 594 | ## License 595 | 596 | [MIT](LICENSE) 597 | 598 | ## Change Logs 599 | 600 | See [releases](https://github.com/kaelzhang/node-comment-json/releases) 601 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against this version of Node.js 2 | environment: 3 | matrix: 4 | - nodejs_version: "8" 5 | - nodejs_version: "10" 6 | 7 | # Install scripts. (runs after repo cloning) 8 | install: 9 | # Get the latest stable version of Node.js or io.js 10 | - ps: Install-Product node $env:nodejs_version 11 | # install modules 12 | - npm install 13 | 14 | # Post-install test scripts. 15 | test_script: 16 | # Output useful info for debugging. 17 | - node --version 18 | - npm --version 19 | - npm test 20 | 21 | # Don't actually build. 22 | build: off 23 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Original from DefinitelyTyped. Thanks a million 2 | // Type definitions for comment-json 1.1 3 | // Project: https://github.com/kaelzhang/node-comment-json 4 | // Definitions by: Jason Dent 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | 7 | declare const commentSymbol: unique symbol 8 | 9 | export type CommentPrefix = 'before' 10 | | 'after-prop' 11 | | 'after-colon' 12 | | 'after-value' 13 | | 'after' 14 | 15 | export type CommentDescriptor = `${CommentPrefix}:${string}` 16 | | 'before' 17 | | 'before-all' 18 | | 'after-all' 19 | 20 | export type CommentSymbol = typeof commentSymbol 21 | 22 | export class CommentArray extends Array { 23 | [commentSymbol]: CommentToken[] 24 | } 25 | 26 | export type CommentJSONValue = number 27 | | string 28 | | null 29 | | boolean 30 | | CommentArray 31 | | CommentObject 32 | 33 | export interface CommentObject { 34 | [key: string]: CommentJSONValue 35 | [commentSymbol]: CommentToken[] 36 | } 37 | 38 | export interface CommentToken { 39 | type: 'BlockComment' | 'LineComment' 40 | // The content of the comment, including whitespaces and line breaks 41 | value: string 42 | // If the start location is the same line as the previous token, 43 | // then `inline` is `true` 44 | inline: boolean 45 | // But pay attention that, 46 | // locations will NOT be maintained when stringified 47 | loc: CommentLocation 48 | } 49 | 50 | export interface CommentLocation { 51 | // The start location begins at the `//` or `/*` symbol 52 | start: Location 53 | // The end location of multi-line comment ends at the `*/` symbol 54 | end: Location 55 | } 56 | 57 | export interface Location { 58 | line: number 59 | column: number 60 | } 61 | 62 | export type Reviver = (k: number | string, v: unknown) => unknown 63 | 64 | /** 65 | * Converts a JavaScript Object Notation (JSON) string into an object. 66 | * @param json A valid JSON string. 67 | * @param reviver A function that transforms the results. This function is called for each member of the object. 68 | * @param removesComments If true, the comments won't be maintained, which is often used when we want to get a clean object. 69 | * If a member contains nested objects, the nested objects are transformed before the parent object is. 70 | */ 71 | export function parse( 72 | json: string, 73 | reviver?: Reviver | null, 74 | removesComments?: boolean 75 | ): CommentJSONValue 76 | 77 | /** 78 | * Converts a JavaScript value to a JavaScript Object Notation (JSON) string. 79 | * @param value A JavaScript value, usually an object or array, to be converted. 80 | * @param replacer A function that transforms the results or an array of strings and numbers that acts as a approved list for selecting the object properties that will be stringified. 81 | * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. 82 | */ 83 | export function stringify( 84 | value: unknown, 85 | replacer?: ( 86 | (key: string, value: unknown) => unknown 87 | ) | Array | null, 88 | space?: string | number 89 | ): string 90 | 91 | 92 | export function tokenize(input: string, config?: TokenizeOptions): Token[] 93 | 94 | export interface Token { 95 | type: string 96 | value: string 97 | } 98 | 99 | export interface TokenizeOptions { 100 | tolerant?: boolean 101 | range?: boolean 102 | loc?: boolean 103 | comment?: boolean 104 | } 105 | 106 | export function assign( 107 | target: TTarget, 108 | source: TSource, 109 | // Although it actually accepts more key types and filters then`, 110 | // we set the type of `keys` stricter 111 | keys?: readonly (number | string)[] 112 | ): TTarget 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "comment-json", 3 | "version": "4.2.5", 4 | "description": "Parse and stringify JSON with comments. It will retain comments even after saved!", 5 | "main": "src/index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "test": "npm run test:only", 9 | "test:only": "npm run test:ts && npm run test:node", 10 | "test:ts": "tsc -b test/ts/tsconfig.build.json && node test/ts/test-ts.js", 11 | "test:node": "NODE_DEBUG=comment-json nyc ava --timeout=10s --verbose", 12 | "test:dev": "npm run test:only && npm run report:dev", 13 | "lint": "eslint .", 14 | "fix": "eslint . --fix", 15 | "posttest": "npm run report", 16 | "report": "nyc report --reporter=text-lcov > coverage.lcov && codecov", 17 | "report:dev": "nyc report --reporter=html && npm run report:open", 18 | "report:open": "open coverage/index.html" 19 | }, 20 | "files": [ 21 | "src/", 22 | "index.d.ts" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/kaelzhang/node-comment-json.git" 27 | }, 28 | "keywords": [ 29 | "comment-json", 30 | "comments", 31 | "annotations", 32 | "json", 33 | "json-stringify", 34 | "json-parse", 35 | "parser", 36 | "comments-json", 37 | "json-comments" 38 | ], 39 | "engines": { 40 | "node": ">= 6" 41 | }, 42 | "ava": { 43 | "files": [ 44 | "test/*.test.js" 45 | ] 46 | }, 47 | "author": "kaelzhang", 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/kaelzhang/node-comment-json/issues" 51 | }, 52 | "devDependencies": { 53 | "@ostai/eslint-config": "^3.6.0", 54 | "ava": "^4.0.1", 55 | "codecov": "^3.8.2", 56 | "eslint": "^8.8.0", 57 | "eslint-plugin-import": "^2.25.4", 58 | "nyc": "^15.1.0", 59 | "test-fixture": "^2.4.1", 60 | "typescript": "^4.5.5" 61 | }, 62 | "dependencies": { 63 | "array-timsort": "^1.0.3", 64 | "core-util-is": "^1.0.3", 65 | "esprima": "^4.0.1", 66 | "has-own-prop": "^2.0.0", 67 | "repeat-string": "^1.6.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/array.js: -------------------------------------------------------------------------------- 1 | const {isArray} = require('core-util-is') 2 | const {sort} = require('array-timsort') 3 | 4 | const { 5 | SYMBOL_PREFIXES, 6 | 7 | UNDEFINED, 8 | 9 | symbol, 10 | copy_comments, 11 | swap_comments 12 | } = require('./common') 13 | 14 | const reverse_comments = array => { 15 | const {length} = array 16 | let i = 0 17 | const max = length / 2 18 | 19 | for (; i < max; i ++) { 20 | swap_comments(array, i, length - i - 1) 21 | } 22 | } 23 | 24 | const move_comment = (target, source, i, offset, remove) => { 25 | copy_comments(target, source, i + offset, i, remove) 26 | } 27 | 28 | const move_comments = ( 29 | // `Array` target array 30 | target, 31 | // `Array` source array 32 | source, 33 | // `number` start index 34 | start, 35 | // `number` number of indexes to move 36 | count, 37 | // `number` offset to move 38 | offset, 39 | // `boolean` whether should remove the comments from source 40 | remove 41 | ) => { 42 | if (offset > 0) { 43 | let i = count 44 | // | count | offset | 45 | // source: ------------- 46 | // target: ------------- 47 | // | remove | 48 | // => remove === offset 49 | 50 | // From [count - 1, 0] 51 | while (i -- > 0) { 52 | move_comment(target, source, start + i, offset, remove) 53 | } 54 | return 55 | } 56 | 57 | let i = 0 58 | // | remove | count | 59 | // ------------- 60 | // ------------- 61 | // | offset | 62 | 63 | // From [0, count - 1] 64 | while (i < count) { 65 | const ii = i ++ 66 | move_comment(target, source, start + ii, offset, remove) 67 | } 68 | } 69 | 70 | const remove_comments = (array, key) => { 71 | SYMBOL_PREFIXES.forEach(prefix => { 72 | const prop = symbol(prefix, key) 73 | delete array[prop] 74 | }) 75 | } 76 | 77 | const get_mapped = (map, key) => { 78 | let mapped = key 79 | 80 | while (mapped in map) { 81 | mapped = map[mapped] 82 | } 83 | 84 | return mapped 85 | } 86 | 87 | class CommentArray extends Array { 88 | // - deleteCount + items.length 89 | 90 | // We should avoid `splice(begin, deleteCount, ...items)`, 91 | // because `splice(0, undefined)` is not equivalent to `splice(0)`, 92 | // as well as: 93 | // - slice 94 | splice (...args) { 95 | const {length} = this 96 | const ret = super.splice(...args) 97 | 98 | // #16 99 | // If no element removed, we might still need to move comments, 100 | // because splice could add new items 101 | 102 | // if (!ret.length) { 103 | // return ret 104 | // } 105 | 106 | // JavaScript syntax is silly 107 | // eslint-disable-next-line prefer-const 108 | let [begin, deleteCount, ...items] = args 109 | 110 | if (begin < 0) { 111 | begin += length 112 | } 113 | 114 | if (arguments.length === 1) { 115 | deleteCount = length - begin 116 | } else { 117 | deleteCount = Math.min(length - begin, deleteCount) 118 | } 119 | 120 | const { 121 | length: item_length 122 | } = items 123 | 124 | // itemsToDelete: - 125 | // itemsToAdd: + 126 | // | dc | count | 127 | // =======-------------============ 128 | // =======++++++============ 129 | // | il | 130 | const offset = item_length - deleteCount 131 | const start = begin + deleteCount 132 | const count = length - start 133 | 134 | move_comments(this, this, start, count, offset, true) 135 | 136 | return ret 137 | } 138 | 139 | slice (...args) { 140 | const {length} = this 141 | const array = super.slice(...args) 142 | if (!array.length) { 143 | return new CommentArray() 144 | } 145 | 146 | let [begin, before] = args 147 | 148 | // Ref: 149 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice 150 | if (before === UNDEFINED) { 151 | before = length 152 | } else if (before < 0) { 153 | before += length 154 | } 155 | 156 | if (begin < 0) { 157 | begin += length 158 | } else if (begin === UNDEFINED) { 159 | begin = 0 160 | } 161 | 162 | move_comments(array, this, begin, before - begin, - begin) 163 | 164 | return array 165 | } 166 | 167 | unshift (...items) { 168 | const {length} = this 169 | const ret = super.unshift(...items) 170 | const { 171 | length: items_length 172 | } = items 173 | 174 | if (items_length > 0) { 175 | move_comments(this, this, 0, length, items_length, true) 176 | } 177 | 178 | return ret 179 | } 180 | 181 | shift () { 182 | const ret = super.shift() 183 | const {length} = this 184 | 185 | remove_comments(this, 0) 186 | move_comments(this, this, 1, length, - 1, true) 187 | 188 | return ret 189 | } 190 | 191 | reverse () { 192 | super.reverse() 193 | 194 | reverse_comments(this) 195 | 196 | return this 197 | } 198 | 199 | pop () { 200 | const ret = super.pop() 201 | 202 | // Removes comments 203 | remove_comments(this, this.length) 204 | 205 | return ret 206 | } 207 | 208 | concat (...items) { 209 | let {length} = this 210 | const ret = super.concat(...items) 211 | 212 | if (!items.length) { 213 | return ret 214 | } 215 | 216 | move_comments(ret, this, 0, this.length, 0) 217 | 218 | items.forEach(item => { 219 | const prev = length 220 | length += isArray(item) 221 | ? item.length 222 | : 1 223 | 224 | if (!(item instanceof CommentArray)) { 225 | return 226 | } 227 | 228 | move_comments(ret, item, 0, item.length, prev) 229 | }) 230 | 231 | return ret 232 | } 233 | 234 | sort (...args) { 235 | const result = sort( 236 | this, 237 | // Make sure there is no more than one argument 238 | ...args.slice(0, 1) 239 | ) 240 | 241 | // For example, 242 | // if we sort ['b', 'd', 'c', 'a'], 243 | // then `result` will be [3, 0, 2, 1], and the array is ['a', 'b', 'c', 'd'] 244 | 245 | // First, we swap index 0 (b) and index 3 (a), then the array comments are 246 | // ['a.comments', 'd.comments', 'c.comments', 'b.comments'] 247 | // index 0 is finalized 248 | // index 3 is actually mapped to original index 0, we present as 0 -> 3 249 | 250 | // Then swap index 1 (d) and index 0 (-> 3, b) 251 | // 1 (index) -> 0 (new index) -> 3 (real_index) 252 | // ['d.comments', 'b.comments', 'c.comments', 'd.comments'] 253 | // index 1 is finalized 254 | // index 3 is contains the item of original index 1 255 | // - we present as 1 -> 3 256 | // - it is ok that we don't remove mapping 0 -> 3 257 | 258 | // Then index 2 should be skipped 259 | 260 | // Then swap index 3 (d) and index 1 (-> 3, b), skipped 261 | 262 | const map = Object.create(null) 263 | 264 | result.forEach((source_index, index) => { 265 | if (source_index === index) { 266 | return 267 | } 268 | 269 | const real_source_index = get_mapped(map, source_index) 270 | 271 | if (real_source_index === index) { 272 | return 273 | } 274 | 275 | // The item of index `index` gets the final value 276 | // delete map[index] 277 | map[index] = real_source_index 278 | 279 | swap_comments(this, index, real_source_index) 280 | }) 281 | 282 | return this 283 | } 284 | } 285 | 286 | module.exports = { 287 | CommentArray 288 | } 289 | -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | const hasOwnProperty = require('has-own-prop') 2 | const { 3 | isObject, 4 | isArray, 5 | isString, 6 | isNumber 7 | } = require('core-util-is') 8 | 9 | const PREFIX_BEFORE = 'before' 10 | const PREFIX_AFTER_PROP = 'after-prop' 11 | const PREFIX_AFTER_COLON = 'after-colon' 12 | const PREFIX_AFTER_VALUE = 'after-value' 13 | const PREFIX_AFTER = 'after' 14 | 15 | const PREFIX_BEFORE_ALL = 'before-all' 16 | const PREFIX_AFTER_ALL = 'after-all' 17 | 18 | const BRACKET_OPEN = '[' 19 | const BRACKET_CLOSE = ']' 20 | const CURLY_BRACKET_OPEN = '{' 21 | const CURLY_BRACKET_CLOSE = '}' 22 | const COMMA = ',' 23 | const EMPTY = '' 24 | const MINUS = '-' 25 | 26 | const SYMBOL_PREFIXES = [ 27 | PREFIX_BEFORE, 28 | PREFIX_AFTER_PROP, 29 | PREFIX_AFTER_COLON, 30 | PREFIX_AFTER_VALUE, 31 | PREFIX_AFTER 32 | ] 33 | 34 | const NON_PROP_SYMBOL_KEYS = [ 35 | PREFIX_BEFORE, 36 | PREFIX_BEFORE_ALL, 37 | PREFIX_AFTER_ALL 38 | ].map(Symbol.for) 39 | 40 | const COLON = ':' 41 | const UNDEFINED = undefined 42 | 43 | const symbol = (prefix, key) => Symbol.for(prefix + COLON + key) 44 | 45 | const define = (target, key, value) => Object.defineProperty(target, key, { 46 | value, 47 | writable: true, 48 | configurable: true 49 | }) 50 | 51 | const copy_comments_by_kind = ( 52 | target, source, target_key, source_key, prefix, remove_source 53 | ) => { 54 | const source_prop = symbol(prefix, source_key) 55 | if (!hasOwnProperty(source, source_prop)) { 56 | return 57 | } 58 | 59 | const target_prop = target_key === source_key 60 | ? source_prop 61 | : symbol(prefix, target_key) 62 | 63 | define(target, target_prop, source[source_prop]) 64 | 65 | if (remove_source) { 66 | delete source[source_prop] 67 | } 68 | } 69 | 70 | const copy_comments = ( 71 | target, source, target_key, source_key, remove_source 72 | ) => { 73 | SYMBOL_PREFIXES.forEach(prefix => { 74 | copy_comments_by_kind( 75 | target, source, target_key, source_key, prefix, remove_source 76 | ) 77 | }) 78 | } 79 | 80 | const swap_comments = (array, from, to) => { 81 | if (from === to) { 82 | return 83 | } 84 | 85 | SYMBOL_PREFIXES.forEach(prefix => { 86 | const target_prop = symbol(prefix, to) 87 | if (!hasOwnProperty(array, target_prop)) { 88 | copy_comments_by_kind(array, array, to, from, prefix, true) 89 | return 90 | } 91 | 92 | const comments = array[target_prop] 93 | delete array[target_prop] 94 | 95 | copy_comments_by_kind(array, array, to, from, prefix, true) 96 | define(array, symbol(prefix, from), comments) 97 | }) 98 | } 99 | 100 | const assign_non_prop_comments = (target, source) => { 101 | NON_PROP_SYMBOL_KEYS.forEach(key => { 102 | const comments = source[key] 103 | 104 | if (comments) { 105 | define(target, key, comments) 106 | } 107 | }) 108 | } 109 | 110 | // Assign keys and comments 111 | const assign = (target, source, keys) => { 112 | keys.forEach(key => { 113 | if (!isString(key) && !isNumber(key)) { 114 | return 115 | } 116 | 117 | if (!hasOwnProperty(source, key)) { 118 | return 119 | } 120 | 121 | target[key] = source[key] 122 | copy_comments(target, source, key, key) 123 | }) 124 | 125 | return target 126 | } 127 | 128 | module.exports = { 129 | SYMBOL_PREFIXES, 130 | 131 | PREFIX_BEFORE, 132 | PREFIX_AFTER_PROP, 133 | PREFIX_AFTER_COLON, 134 | PREFIX_AFTER_VALUE, 135 | PREFIX_AFTER, 136 | 137 | PREFIX_BEFORE_ALL, 138 | PREFIX_AFTER_ALL, 139 | 140 | BRACKET_OPEN, 141 | BRACKET_CLOSE, 142 | CURLY_BRACKET_OPEN, 143 | CURLY_BRACKET_CLOSE, 144 | 145 | COLON, 146 | COMMA, 147 | MINUS, 148 | EMPTY, 149 | 150 | UNDEFINED, 151 | 152 | symbol, 153 | define, 154 | copy_comments, 155 | swap_comments, 156 | assign_non_prop_comments, 157 | 158 | assign (target, source, keys) { 159 | if (!isObject(target)) { 160 | throw new TypeError('Cannot convert undefined or null to object') 161 | } 162 | 163 | if (!isObject(source)) { 164 | return target 165 | } 166 | 167 | if (keys === UNDEFINED) { 168 | keys = Object.keys(source) 169 | // We assign non-property comments 170 | // if argument `keys` is not specified 171 | assign_non_prop_comments(target, source) 172 | } else if (!isArray(keys)) { 173 | throw new TypeError('keys must be array or undefined') 174 | } else if (keys.length === 0) { 175 | // Or argument `keys` is an empty array 176 | assign_non_prop_comments(target, source) 177 | } 178 | 179 | return assign(target, source, keys) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const {parse, tokenize} = require('./parse') 2 | const stringify = require('./stringify') 3 | const {CommentArray} = require('./array') 4 | const {assign} = require('./common') 5 | 6 | module.exports = { 7 | parse, 8 | stringify, 9 | tokenize, 10 | 11 | CommentArray, 12 | assign 13 | } 14 | -------------------------------------------------------------------------------- /src/parse.js: -------------------------------------------------------------------------------- 1 | // JSON formatting 2 | 3 | const esprima = require('esprima') 4 | 5 | const { 6 | CommentArray, 7 | } = require('./array') 8 | 9 | const { 10 | PREFIX_BEFORE, 11 | PREFIX_AFTER_PROP, 12 | PREFIX_AFTER_COLON, 13 | PREFIX_AFTER_VALUE, 14 | PREFIX_AFTER, 15 | 16 | PREFIX_BEFORE_ALL, 17 | PREFIX_AFTER_ALL, 18 | 19 | BRACKET_OPEN, 20 | BRACKET_CLOSE, 21 | CURLY_BRACKET_OPEN, 22 | CURLY_BRACKET_CLOSE, 23 | 24 | COLON, 25 | COMMA, 26 | MINUS, 27 | EMPTY, 28 | 29 | UNDEFINED, 30 | 31 | define, 32 | assign_non_prop_comments 33 | } = require('./common') 34 | 35 | const tokenize = code => esprima.tokenize(code, { 36 | comment: true, 37 | loc: true 38 | }) 39 | 40 | const previous_hosts = [] 41 | let comments_host = null 42 | let unassigned_comments = null 43 | 44 | const previous_props = [] 45 | let last_prop 46 | 47 | let remove_comments = false 48 | let inline = false 49 | let tokens = null 50 | let last = null 51 | let current = null 52 | let index 53 | let reviver = null 54 | 55 | const clean = () => { 56 | previous_props.length = 57 | previous_hosts.length = 0 58 | 59 | last = null 60 | last_prop = UNDEFINED 61 | } 62 | 63 | const free = () => { 64 | clean() 65 | 66 | tokens.length = 0 67 | 68 | unassigned_comments = 69 | comments_host = 70 | tokens = 71 | last = 72 | current = 73 | reviver = null 74 | } 75 | 76 | const symbolFor = prefix => Symbol.for( 77 | last_prop !== UNDEFINED 78 | ? prefix + COLON + last_prop 79 | : prefix 80 | ) 81 | 82 | const transform = (k, v) => reviver 83 | ? reviver(k, v) 84 | : v 85 | 86 | const unexpected = () => { 87 | const error = new SyntaxError(`Unexpected token ${current.value.slice(0, 1)}`) 88 | Object.assign(error, current.loc.start) 89 | 90 | throw error 91 | } 92 | 93 | const unexpected_end = () => { 94 | const error = new SyntaxError('Unexpected end of JSON input') 95 | Object.assign(error, last 96 | ? last.loc.end 97 | // Empty string 98 | : { 99 | line: 1, 100 | column: 0 101 | }) 102 | 103 | throw error 104 | } 105 | 106 | // Move the reader to the next 107 | const next = () => { 108 | const new_token = tokens[++ index] 109 | inline = current 110 | && new_token 111 | && current.loc.end.line === new_token.loc.start.line 112 | || false 113 | 114 | last = current 115 | current = new_token 116 | } 117 | 118 | const type = () => { 119 | if (!current) { 120 | unexpected_end() 121 | } 122 | 123 | return current.type === 'Punctuator' 124 | ? current.value 125 | : current.type 126 | } 127 | 128 | const is = t => type() === t 129 | 130 | const expect = a => { 131 | if (!is(a)) { 132 | unexpected() 133 | } 134 | } 135 | 136 | const set_comments_host = new_host => { 137 | previous_hosts.push(comments_host) 138 | comments_host = new_host 139 | } 140 | 141 | const restore_comments_host = () => { 142 | comments_host = previous_hosts.pop() 143 | } 144 | 145 | const assign_after_comments = () => { 146 | if (!unassigned_comments) { 147 | return 148 | } 149 | 150 | const after_comments = [] 151 | 152 | for (const comment of unassigned_comments) { 153 | // If the comment is inline, then it is an after-comma comment 154 | if (comment.inline) { 155 | after_comments.push(comment) 156 | // Otherwise, all comments are before: comment 157 | } else { 158 | break 159 | } 160 | } 161 | 162 | const {length} = after_comments 163 | if (!length) { 164 | return 165 | } 166 | 167 | if (length === unassigned_comments.length) { 168 | // If unassigned_comments are all consumed 169 | unassigned_comments = null 170 | } else { 171 | unassigned_comments.splice(0, length) 172 | } 173 | 174 | define(comments_host, symbolFor(PREFIX_AFTER), after_comments) 175 | } 176 | 177 | const assign_comments = prefix => { 178 | if (!unassigned_comments) { 179 | return 180 | } 181 | 182 | define(comments_host, symbolFor(prefix), unassigned_comments) 183 | 184 | unassigned_comments = null 185 | } 186 | 187 | const parse_comments = prefix => { 188 | const comments = [] 189 | 190 | while ( 191 | current 192 | && ( 193 | is('LineComment') 194 | || is('BlockComment') 195 | ) 196 | ) { 197 | const comment = { 198 | ...current, 199 | inline 200 | } 201 | 202 | // delete comment.loc 203 | comments.push(comment) 204 | 205 | next() 206 | } 207 | 208 | if (remove_comments) { 209 | return 210 | } 211 | 212 | if (!comments.length) { 213 | return 214 | } 215 | 216 | if (prefix) { 217 | define(comments_host, symbolFor(prefix), comments) 218 | return 219 | } 220 | 221 | unassigned_comments = comments 222 | } 223 | 224 | const set_prop = (prop, push) => { 225 | if (push) { 226 | previous_props.push(last_prop) 227 | } 228 | 229 | last_prop = prop 230 | } 231 | 232 | const restore_prop = () => { 233 | last_prop = previous_props.pop() 234 | } 235 | 236 | const parse_object = () => { 237 | const obj = {} 238 | set_comments_host(obj) 239 | set_prop(UNDEFINED, true) 240 | 241 | let started = false 242 | let name 243 | 244 | parse_comments() 245 | 246 | while (!is(CURLY_BRACKET_CLOSE)) { 247 | if (started) { 248 | assign_comments(PREFIX_AFTER_VALUE) 249 | 250 | // key-value pair delimiter 251 | expect(COMMA) 252 | next() 253 | parse_comments() 254 | 255 | assign_after_comments() 256 | 257 | // If there is a trailing comma, we might reach the end 258 | // ``` 259 | // { 260 | // "a": 1, 261 | // } 262 | // ``` 263 | if (is(CURLY_BRACKET_CLOSE)) { 264 | break 265 | } 266 | } 267 | 268 | started = true 269 | expect('String') 270 | name = JSON.parse(current.value) 271 | 272 | set_prop(name) 273 | assign_comments(PREFIX_BEFORE) 274 | 275 | next() 276 | parse_comments(PREFIX_AFTER_PROP) 277 | 278 | expect(COLON) 279 | 280 | next() 281 | parse_comments(PREFIX_AFTER_COLON) 282 | 283 | obj[name] = transform(name, walk()) 284 | parse_comments() 285 | } 286 | 287 | if (started) { 288 | // If there are properties, 289 | // then the unassigned comments are after comments 290 | assign_comments(PREFIX_AFTER) 291 | } 292 | 293 | // bypass } 294 | next() 295 | last_prop = undefined 296 | 297 | if (!started) { 298 | // Otherwise, they are before comments 299 | assign_comments(PREFIX_BEFORE) 300 | } 301 | 302 | restore_comments_host() 303 | restore_prop() 304 | 305 | return obj 306 | } 307 | 308 | const parse_array = () => { 309 | const array = new CommentArray() 310 | set_comments_host(array) 311 | set_prop(UNDEFINED, true) 312 | 313 | let started = false 314 | let i = 0 315 | 316 | parse_comments() 317 | 318 | while (!is(BRACKET_CLOSE)) { 319 | if (started) { 320 | assign_comments(PREFIX_AFTER_VALUE) 321 | expect(COMMA) 322 | next() 323 | parse_comments() 324 | 325 | assign_after_comments() 326 | 327 | if (is(BRACKET_CLOSE)) { 328 | break 329 | } 330 | } 331 | 332 | started = true 333 | 334 | set_prop(i) 335 | assign_comments(PREFIX_BEFORE) 336 | 337 | array[i] = transform(i, walk()) 338 | i ++ 339 | 340 | parse_comments() 341 | } 342 | 343 | if (started) { 344 | assign_comments(PREFIX_AFTER) 345 | } 346 | 347 | next() 348 | last_prop = undefined 349 | 350 | if (!started) { 351 | assign_comments(PREFIX_BEFORE) 352 | } 353 | 354 | restore_comments_host() 355 | restore_prop() 356 | 357 | return array 358 | } 359 | 360 | function walk () { 361 | let tt = type() 362 | 363 | if (tt === CURLY_BRACKET_OPEN) { 364 | next() 365 | return parse_object() 366 | } 367 | 368 | if (tt === BRACKET_OPEN) { 369 | next() 370 | return parse_array() 371 | } 372 | 373 | let negative = EMPTY 374 | 375 | // -1 376 | if (tt === MINUS) { 377 | next() 378 | tt = type() 379 | negative = MINUS 380 | } 381 | 382 | let v 383 | 384 | switch (tt) { 385 | case 'String': 386 | case 'Boolean': 387 | case 'Null': 388 | case 'Numeric': 389 | v = current.value 390 | next() 391 | return JSON.parse(negative + v) 392 | default: 393 | } 394 | } 395 | 396 | const isObject = subject => Object(subject) === subject 397 | 398 | const parse = (code, rev, no_comments) => { 399 | // Clean variables in closure 400 | clean() 401 | 402 | tokens = tokenize(code) 403 | reviver = rev 404 | remove_comments = no_comments 405 | 406 | if (!tokens.length) { 407 | unexpected_end() 408 | } 409 | 410 | index = - 1 411 | next() 412 | 413 | set_comments_host({}) 414 | 415 | parse_comments(PREFIX_BEFORE_ALL) 416 | 417 | let result = walk() 418 | 419 | parse_comments(PREFIX_AFTER_ALL) 420 | 421 | if (current) { 422 | unexpected() 423 | } 424 | 425 | if (!no_comments && result !== null) { 426 | if (!isObject(result)) { 427 | // 1 -> new Number(1) 428 | // true -> new Boolean(1) 429 | // "foo" -> new String("foo") 430 | 431 | // eslint-disable-next-line no-new-object 432 | result = new Object(result) 433 | } 434 | 435 | assign_non_prop_comments(result, comments_host) 436 | } 437 | 438 | restore_comments_host() 439 | 440 | // reviver 441 | result = transform('', result) 442 | 443 | free() 444 | 445 | return result 446 | } 447 | 448 | module.exports = { 449 | parse, 450 | tokenize 451 | } 452 | -------------------------------------------------------------------------------- /src/stringify.js: -------------------------------------------------------------------------------- 1 | const { 2 | isArray, isObject, isFunction, isNumber, isString 3 | } = require('core-util-is') 4 | const repeat = require('repeat-string') 5 | 6 | const { 7 | PREFIX_BEFORE_ALL, 8 | PREFIX_BEFORE, 9 | PREFIX_AFTER_PROP, 10 | PREFIX_AFTER_COLON, 11 | PREFIX_AFTER_VALUE, 12 | PREFIX_AFTER, 13 | PREFIX_AFTER_ALL, 14 | 15 | BRACKET_OPEN, 16 | BRACKET_CLOSE, 17 | CURLY_BRACKET_OPEN, 18 | CURLY_BRACKET_CLOSE, 19 | COLON, 20 | COMMA, 21 | EMPTY, 22 | 23 | UNDEFINED 24 | } = require('./common') 25 | 26 | // eslint-disable-next-line no-control-regex, no-misleading-character-class 27 | const ESCAPABLE = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g 28 | 29 | // String constants 30 | const SPACE = ' ' 31 | const LF = '\n' 32 | const STR_NULL = 'null' 33 | 34 | // Symbol tags 35 | const BEFORE = prop => `${PREFIX_BEFORE}:${prop}` 36 | const AFTER_PROP = prop => `${PREFIX_AFTER_PROP}:${prop}` 37 | const AFTER_COLON = prop => `${PREFIX_AFTER_COLON}:${prop}` 38 | const AFTER_VALUE = prop => `${PREFIX_AFTER_VALUE}:${prop}` 39 | const AFTER = prop => `${PREFIX_AFTER}:${prop}` 40 | 41 | // table of character substitutions 42 | const meta = { 43 | '\b': '\\b', 44 | '\t': '\\t', 45 | '\n': '\\n', 46 | '\f': '\\f', 47 | '\r': '\\r', 48 | '"': '\\"', 49 | '\\': '\\\\' 50 | } 51 | 52 | const escape = string => { 53 | ESCAPABLE.lastIndex = 0 54 | 55 | if (!ESCAPABLE.test(string)) { 56 | return string 57 | } 58 | 59 | return string.replace(ESCAPABLE, a => { 60 | const c = meta[a] 61 | return typeof c === 'string' 62 | ? c 63 | : a 64 | }) 65 | } 66 | 67 | // Escape no control characters, no quote characters, 68 | // and no backslash characters, 69 | // then we can safely slap some quotes around it. 70 | const quote = string => `"${escape(string)}"` 71 | const comment_stringify = (value, line) => line 72 | ? `//${value}` 73 | : `/*${value}*/` 74 | 75 | // display_block `boolean` whether the 76 | // WHOLE block of comments is always a block group 77 | const process_comments = (host, symbol_tag, deeper_gap, display_block) => { 78 | const comments = host[Symbol.for(symbol_tag)] 79 | if (!comments || !comments.length) { 80 | return EMPTY 81 | } 82 | 83 | let is_line_comment = false 84 | 85 | const str = comments.reduce((prev, { 86 | inline, 87 | type, 88 | value 89 | }) => { 90 | const delimiter = inline 91 | ? SPACE 92 | : LF + deeper_gap 93 | 94 | is_line_comment = type === 'LineComment' 95 | 96 | return prev + delimiter + comment_stringify(value, is_line_comment) 97 | }, EMPTY) 98 | 99 | return display_block 100 | // line comment should always end with a LF 101 | || is_line_comment 102 | ? str + LF + deeper_gap 103 | : str 104 | } 105 | 106 | let replacer = null 107 | let indent = EMPTY 108 | 109 | const clean = () => { 110 | replacer = null 111 | indent = EMPTY 112 | } 113 | 114 | const join = (one, two, gap) => 115 | one 116 | ? two 117 | // Symbol.for('before') and Symbol.for('before:prop') 118 | // might both exist if user mannually add comments to the object 119 | // and make a mistake. 120 | // SO, we are not to only trimRight but trim for both sides 121 | ? one + two.trim() + LF + gap 122 | : one.trimRight() + LF + gap 123 | : two 124 | ? two.trimRight() + LF + gap 125 | : EMPTY 126 | 127 | const join_content = (inside, value, gap) => { 128 | const comment = process_comments(value, PREFIX_BEFORE, gap + indent, true) 129 | 130 | return join(comment, inside, gap) 131 | } 132 | 133 | // | deeper_gap | 134 | // | gap | indent | 135 | // [ 136 | // "foo", 137 | // "bar" 138 | // ] 139 | const array_stringify = (value, gap) => { 140 | const deeper_gap = gap + indent 141 | 142 | const {length} = value 143 | 144 | // From the item to before close 145 | let inside = EMPTY 146 | let after_comma = EMPTY 147 | 148 | // Never use Array.prototype.forEach, 149 | // that we should iterate all items 150 | for (let i = 0; i < length; i ++) { 151 | if (i !== 0) { 152 | inside += COMMA 153 | } 154 | 155 | const before = join( 156 | after_comma, 157 | process_comments(value, BEFORE(i), deeper_gap), 158 | deeper_gap 159 | ) 160 | 161 | inside += before || (LF + deeper_gap) 162 | 163 | // JSON.stringify([undefined]) => [null] 164 | inside += stringify(i, value, deeper_gap) || STR_NULL 165 | 166 | inside += process_comments(value, AFTER_VALUE(i), deeper_gap) 167 | 168 | after_comma = process_comments(value, AFTER(i), deeper_gap) 169 | } 170 | 171 | inside += join( 172 | after_comma, 173 | process_comments(value, PREFIX_AFTER, deeper_gap), 174 | deeper_gap 175 | ) 176 | 177 | return BRACKET_OPEN 178 | + join_content(inside, value, gap) 179 | + BRACKET_CLOSE 180 | } 181 | 182 | // | deeper_gap | 183 | // | gap | indent | 184 | // { 185 | // "foo": 1, 186 | // "bar": 2 187 | // } 188 | const object_stringify = (value, gap) => { 189 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 190 | // so watch out for that case. 191 | if (!value) { 192 | return 'null' 193 | } 194 | 195 | const deeper_gap = gap + indent 196 | 197 | // From the first element to before close 198 | let inside = EMPTY 199 | let after_comma = EMPTY 200 | let first = true 201 | 202 | const keys = isArray(replacer) 203 | ? replacer 204 | : Object.keys(value) 205 | 206 | const iteratee = key => { 207 | // Stringified value 208 | const sv = stringify(key, value, deeper_gap) 209 | 210 | // If a value is undefined, then the key-value pair should be ignored 211 | if (sv === UNDEFINED) { 212 | return 213 | } 214 | 215 | // The treat ment 216 | if (!first) { 217 | inside += COMMA 218 | } 219 | 220 | first = false 221 | 222 | const before = join( 223 | after_comma, 224 | process_comments(value, BEFORE(key), deeper_gap), 225 | deeper_gap 226 | ) 227 | 228 | inside += before || (LF + deeper_gap) 229 | 230 | inside += quote(key) 231 | + process_comments(value, AFTER_PROP(key), deeper_gap) 232 | + COLON 233 | + process_comments(value, AFTER_COLON(key), deeper_gap) 234 | + SPACE 235 | + sv 236 | + process_comments(value, AFTER_VALUE(key), deeper_gap) 237 | 238 | after_comma = process_comments(value, AFTER(key), deeper_gap) 239 | } 240 | 241 | keys.forEach(iteratee) 242 | 243 | // if (after_comma) { 244 | // inside += COMMA 245 | // } 246 | 247 | inside += join( 248 | after_comma, 249 | process_comments(value, PREFIX_AFTER, deeper_gap), 250 | deeper_gap 251 | ) 252 | 253 | return CURLY_BRACKET_OPEN 254 | + join_content(inside, value, gap) 255 | + CURLY_BRACKET_CLOSE 256 | } 257 | 258 | // @param {string} key 259 | // @param {Object} holder 260 | // @param {function()|Array} replacer 261 | // @param {string} indent 262 | // @param {string} gap 263 | function stringify (key, holder, gap) { 264 | let value = holder[key] 265 | 266 | // If the value has a toJSON method, call it to obtain a replacement value. 267 | if (isObject(value) && isFunction(value.toJSON)) { 268 | value = value.toJSON(key) 269 | } 270 | 271 | // If we were called with a replacer function, then call the replacer to 272 | // obtain a replacement value. 273 | if (isFunction(replacer)) { 274 | value = replacer.call(holder, key, value) 275 | } 276 | 277 | switch (typeof value) { 278 | case 'string': 279 | return quote(value) 280 | 281 | case 'number': 282 | // JSON numbers must be finite. Encode non-finite numbers as null. 283 | return Number.isFinite(value) ? String(value) : STR_NULL 284 | 285 | case 'boolean': 286 | case 'null': 287 | 288 | // If the value is a boolean or null, convert it to a string. Note: 289 | // typeof null does not produce 'null'. The case is included here in 290 | // the remote chance that this gets fixed someday. 291 | return String(value) 292 | 293 | // If the type is 'object', we might be dealing with an object or an array or 294 | // null. 295 | case 'object': 296 | return isArray(value) 297 | ? array_stringify(value, gap) 298 | : object_stringify(value, gap) 299 | 300 | // undefined 301 | default: 302 | // JSON.stringify(undefined) === undefined 303 | // JSON.stringify('foo', () => undefined) === undefined 304 | } 305 | } 306 | 307 | const get_indent = space => isString(space) 308 | // If the space parameter is a string, it will be used as the indent string. 309 | ? space 310 | : isNumber(space) 311 | ? repeat(SPACE, space) 312 | : EMPTY 313 | 314 | const {toString} = Object.prototype 315 | const PRIMITIVE_OBJECT_TYPES = [ 316 | '[object Number]', 317 | '[object String]', 318 | '[object Boolean]' 319 | ] 320 | 321 | const is_primitive_object = subject => { 322 | if (typeof subject !== 'object') { 323 | return false 324 | } 325 | 326 | const str = toString.call(subject) 327 | return PRIMITIVE_OBJECT_TYPES.includes(str) 328 | } 329 | 330 | // @param {function()|Array} replacer 331 | // @param {string|number} space 332 | module.exports = (value, replacer_, space) => { 333 | // The stringify method takes a value and an optional replacer, and an optional 334 | // space parameter, and returns a JSON text. The replacer can be a function 335 | // that can replace values, or an array of strings that will select the keys. 336 | // A default replacer method can be provided. Use of the space parameter can 337 | // produce text that is more easily readable. 338 | 339 | // If the space parameter is a number, make an indent string containing that 340 | // many spaces. 341 | const indent_ = get_indent(space) 342 | 343 | if (!indent_) { 344 | return JSON.stringify(value, replacer_) 345 | } 346 | 347 | // vanilla `JSON.parse` allow invalid replacer 348 | if (!isFunction(replacer_) && !isArray(replacer_)) { 349 | replacer_ = null 350 | } 351 | 352 | replacer = replacer_ 353 | indent = indent_ 354 | 355 | const str = is_primitive_object(value) 356 | ? JSON.stringify(value) 357 | : stringify('', {'': value}, EMPTY) 358 | 359 | clean() 360 | 361 | return isObject(value) 362 | ? process_comments(value, PREFIX_BEFORE_ALL, EMPTY).trimLeft() 363 | + str 364 | + process_comments(value, PREFIX_AFTER_ALL, EMPTY).trimRight() 365 | : str 366 | } 367 | -------------------------------------------------------------------------------- /test/array.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { 3 | isFunction, isObject, isString, isArray 4 | } = require('core-util-is') 5 | const { 6 | parse, stringify, assign, CommentArray 7 | } = require('..') 8 | 9 | const st = o => stringify(o, null, 2) 10 | 11 | const a1 = `[ 12 | // 0 13 | 0, 14 | // 1 15 | 1, 16 | // 2 17 | 2 18 | ]` 19 | 20 | const a2 = `[ 21 | // 1 22 | 1, 23 | // 2 24 | 2 25 | ]` 26 | 27 | const a3 = `[ 28 | // 0 29 | 0 30 | ]` 31 | 32 | const texpect = ( 33 | t, 34 | // cleaned parsed 35 | ret, 36 | // stringified value from parsed 37 | str, 38 | // parsed value 39 | r, 40 | // real return value 41 | rr 42 | ) => { 43 | if (isObject(ret)) { 44 | t.deepEqual(r, ret) 45 | } else { 46 | t.is(r, ret) 47 | } 48 | 49 | if (isString(rr)) { 50 | t.is(rr, str) 51 | } else { 52 | t.is(st(rr), str) 53 | } 54 | } 55 | 56 | const slice = (ret, str) => 57 | (t, r, _, __, rr) => texpect(t, ret, str, r, rr) 58 | 59 | const unshift = (ret, str) => 60 | (t, r, s) => texpect(t, ret, str, r, s) 61 | 62 | const CASES = [ 63 | [ 64 | // title 65 | 'splice(0, 1)', 66 | // input array 67 | a1, 68 | // run 69 | array => array.splice(0, 1), 70 | // expect function or expected value of the result array 71 | [0], 72 | // expected stringified string 73 | a2 74 | ], 75 | [ 76 | 'splice(0)', 77 | a1, 78 | array => array.splice(0), 79 | [0, 1, 2], 80 | '[]' 81 | ], 82 | [ 83 | 'splice(- 3, 1)', 84 | a1, 85 | array => array.splice(- 3, 1), 86 | [0], 87 | a2 88 | ], 89 | [ 90 | '#16: splice(1, 0, 3)', 91 | a1, 92 | array => array.splice(1, 0, 3), 93 | [], 94 | `[ 95 | // 0 96 | 0, 97 | 3, 98 | // 1 99 | 1, 100 | // 2 101 | 2 102 | ]` 103 | ], 104 | [ 105 | 'invalid: splice(0, undefined)', 106 | a1, 107 | array => array.splice(0, undefined), 108 | [], 109 | a1 110 | ], 111 | [ 112 | 'slice(0)', 113 | a1, 114 | array => array.slice(0), 115 | [0, 1, 2], 116 | a1 117 | ], 118 | [ 119 | 'slice(-1)', 120 | a1, 121 | array => array.slice(- 1), 122 | slice( 123 | // cleaned object after parsed and sliced 124 | [2], 125 | // parse, slice and then stringify 126 | `[ 127 | // 2 128 | 2 129 | ]`) 130 | ], 131 | [ 132 | 'slice(3)', 133 | a1, 134 | array => array.slice(3), 135 | slice([], '[]') 136 | ], 137 | [ 138 | 'slice(undefined, undefined)', 139 | a1, 140 | array => array.slice(), 141 | slice([0, 1, 2], a1) 142 | ], 143 | [ 144 | 'slice(0, - 2)', 145 | a1, 146 | array => array.slice(0, - 2), 147 | slice([0], a3) 148 | ], 149 | [ 150 | 'slice(0, 1)', 151 | a1, 152 | array => array.slice(0, 1), 153 | slice([0], a3) 154 | ], 155 | [ 156 | 'slice(0, 1), no mess', 157 | `[ 158 | // 0 159 | 0, 160 | 1 // 1 161 | ]`, 162 | array => array.slice(0, 1), 163 | slice([0], `[ 164 | // 0 165 | 0 166 | ]`) 167 | ], 168 | [ 169 | 'unshift()', 170 | a1, 171 | array => array.unshift(), 172 | unshift(3, a1) 173 | ], 174 | [ 175 | 'unshift(- 1)', 176 | a1, 177 | array => array.unshift(- 1), 178 | unshift(4, `[ 179 | -1, 180 | // 0 181 | 0, 182 | // 1 183 | 1, 184 | // 2 185 | 2 186 | ]`) 187 | ], 188 | [ 189 | 'shift', 190 | a1, 191 | array => array.shift(), 192 | unshift(0, `[ 193 | // 1 194 | 1, 195 | // 2 196 | 2 197 | ]`) 198 | ], 199 | [ 200 | 'shift, no mess', 201 | `[ 202 | // 0 203 | 0, 204 | 1 /* 1 */, 205 | 2 // 2 206 | ]`, 207 | array => array.shift(), 208 | unshift(0, `[ 209 | 1 /* 1 */, 210 | 2 // 2 211 | ]`) 212 | ], 213 | [ 214 | 'reverse', 215 | a1, 216 | array => array.reverse(), 217 | unshift([2, 1, 0], `[ 218 | // 2 219 | 2, 220 | // 1 221 | 1, 222 | // 0 223 | 0 224 | ]`) 225 | ], 226 | [ 227 | 'reverse, no mess', 228 | `[ 229 | 0, // 0 230 | // 1 231 | 1, 232 | // 2 233 | 2 234 | ]`, 235 | array => array.reverse(), 236 | unshift([2, 1, 0], `[ 237 | // 2 238 | 2, 239 | // 1 240 | 1, 241 | 0 // 0 242 | ]`) 243 | ], 244 | [ 245 | 'pop', 246 | a1, 247 | array => array.pop(), 248 | unshift(2, `[ 249 | // 0 250 | 0, 251 | // 1 252 | 1 253 | ]`) 254 | ], 255 | [ 256 | 'sort, default lex sort', 257 | `[ 258 | // before c 259 | "c", 260 | "a", // after a 261 | "b" /* after b value */, 262 | "d" 263 | ]`, 264 | array => array.sort(), 265 | unshift(['a', 'b', 'c', 'd'], `[ 266 | "a", // after a 267 | "b" /* after b value */, 268 | // before c 269 | "c", 270 | "d" 271 | ]`) 272 | ], 273 | [ 274 | 'sort, with `compareFunction`', 275 | `[ 276 | // before c 277 | "c", 278 | "a", // after a 279 | "b" /* after b value */, 280 | // before d 281 | "d" 282 | ]`, 283 | array => { 284 | array.sort( 285 | (a, b) => a > b 286 | ? - 1 287 | : 1 288 | ) 289 | return array 290 | }, 291 | unshift(['d', 'c', 'b', 'a'], `[ 292 | // before d 293 | "d", 294 | // before c 295 | "c", 296 | "b" /* after b value */, 297 | "a" // after a 298 | ]`) 299 | ], 300 | ] 301 | 302 | CASES.forEach(([d, a, run, e, s]) => { 303 | test(d, t => { 304 | const parsed = parse(a) 305 | const ret = run(parsed) 306 | 307 | const expect = isFunction(e) 308 | ? e 309 | : (tt, r, str) => { 310 | tt.deepEqual(r, e) 311 | tt.is(str, s) 312 | } 313 | 314 | expect( 315 | t, 316 | // Cleaned return value 317 | isArray(ret) 318 | // clean ret 319 | ? [...ret] 320 | : ret, 321 | // Stringified 322 | st(parsed), 323 | parsed, 324 | ret 325 | ) 326 | }) 327 | }) 328 | 329 | test('assign', t => { 330 | const str = `{ 331 | // a 332 | "a": 1, 333 | // b 334 | "b": 2 335 | }` 336 | 337 | const parsed = parse(str) 338 | 339 | t.is(st(assign({}, parsed)), str) 340 | t.is(st(assign({})), '{}') 341 | 342 | t.is(st(assign({}, parsed, ['a', 'c'])), `{ 343 | // a 344 | "a": 1 345 | }`) 346 | 347 | t.throws(() => assign({}, parsed, false), { 348 | message: /keys/ 349 | }) 350 | t.throws(() => assign(), { 351 | message: /convert/ 352 | }) 353 | }) 354 | 355 | test('concat', t => { 356 | const parsed1 = parse(`[ 357 | // foo 358 | "foo" 359 | ]`) 360 | 361 | const parsed2 = parse(`[ 362 | // bar 363 | "bar", 364 | // baz, 365 | "baz" 366 | ]`) 367 | 368 | const concated = parsed1.concat( 369 | parsed2, 370 | 'qux' 371 | ) 372 | 373 | t.is(stringify(concated, null, 2), `[ 374 | // foo 375 | "foo", 376 | // bar 377 | "bar", 378 | // baz, 379 | "baz", 380 | "qux" 381 | ]`) 382 | 383 | t.is(stringify(new CommentArray().concat()), '[]') 384 | }) 385 | -------------------------------------------------------------------------------- /test/fixtures/deep-null-2.json: -------------------------------------------------------------------------------- 1 | { 2 | // comment a 3 | "a": 1, 4 | "c": { 5 | // comment deep c 6 | "c": 2, 7 | // comment deep a 8 | "a": 1 9 | } 10 | } -------------------------------------------------------------------------------- /test/fixtures/deep-null-3.json: -------------------------------------------------------------------------------- 1 | { 2 | // comment a 3 | "a": 1, 4 | "c": { 5 | // comment deep c 6 | "c": 2, 7 | // comment deep a 8 | "a": 1 9 | } 10 | } -------------------------------------------------------------------------------- /test/fixtures/deep-null-null.json: -------------------------------------------------------------------------------- 1 | {"a":1,"c":{"c":2,"a":1}} -------------------------------------------------------------------------------- /test/fixtures/duplex-null-2.json: -------------------------------------------------------------------------------- 1 | { 2 | // comment top a 3 | "a": 1, // comment right a 4 | /* comment top b */ 5 | "b": 2, 6 | "// c": "// comment c" 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/duplex-null-3.json: -------------------------------------------------------------------------------- 1 | { 2 | // comment top a 3 | "a": 1, // comment right a 4 | /* comment top b */ 5 | "b": 2, 6 | "// c": "// comment c" 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/duplex-null-null.json: -------------------------------------------------------------------------------- 1 | {"a":1,"b":2,"// c":"// comment c"} -------------------------------------------------------------------------------- /test/fixtures/indent-null-2.json: -------------------------------------------------------------------------------- 1 | /** 2 | block comment at the top 3 | */ 4 | // comment at the top 5 | { 6 | // comment for a 7 | // comment line 2 for a 8 | /* block comment */ 9 | "a": 1 // comment at right 10 | } 11 | // comment at the bottom 12 | -------------------------------------------------------------------------------- /test/fixtures/indent-null-3.json: -------------------------------------------------------------------------------- 1 | /** 2 | block comment at the top 3 | */ 4 | // comment at the top 5 | { 6 | // comment for a 7 | // comment line 2 for a 8 | /* block comment */ 9 | "a": 1 // comment at right 10 | } 11 | // comment at the bottom -------------------------------------------------------------------------------- /test/fixtures/indent-null-null.json: -------------------------------------------------------------------------------- 1 | {"a":1} -------------------------------------------------------------------------------- /test/fixtures/simple-null-2.json: -------------------------------------------------------------------------------- 1 | { 2 | // a 3 | "a": 1 4 | } -------------------------------------------------------------------------------- /test/fixtures/simple-null-3.json: -------------------------------------------------------------------------------- 1 | { 2 | // a 3 | "a": 1 4 | } -------------------------------------------------------------------------------- /test/fixtures/simple-null-null.json: -------------------------------------------------------------------------------- 1 | {"a":1} -------------------------------------------------------------------------------- /test/fixtures/single-right-null-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": 1 // comment a 3 | } -------------------------------------------------------------------------------- /test/fixtures/single-right-null-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": 1 // comment a 3 | } -------------------------------------------------------------------------------- /test/fixtures/single-right-null-null.json: -------------------------------------------------------------------------------- 1 | {"a":1} -------------------------------------------------------------------------------- /test/fixtures/single-top-null-2.json: -------------------------------------------------------------------------------- 1 | { 2 | // comment a 3 | "a": 1 4 | } -------------------------------------------------------------------------------- /test/fixtures/single-top-null-3.json: -------------------------------------------------------------------------------- 1 | { 2 | // comment a 3 | "a": 1 4 | } -------------------------------------------------------------------------------- /test/fixtures/single-top-null-null.json: -------------------------------------------------------------------------------- 1 | {"a":1} -------------------------------------------------------------------------------- /test/others.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const hasOwnProperty = require('has-own-prop') 3 | 4 | const {parse, stringify, assign} = require('..') 5 | 6 | test('#33: assign: should ignore symbol keys', t => { 7 | const str = `{ 8 | // a 9 | "a": 1 10 | }` 11 | 12 | const parsed = parse(str) 13 | 14 | const obj = {} 15 | const key = Symbol.for('before:a') 16 | 17 | t.is(hasOwnProperty(parsed, key), true) 18 | 19 | assign(obj, parsed, [key]) 20 | 21 | t.is(obj[key]) 22 | }) 23 | 24 | test('#17: has trailing comma and comment after comma', t => { 25 | const str = `{ 26 | "b": [ 27 | 1, 28 | ], 29 | "a": 1, // a 30 | }` 31 | 32 | t.is(stringify(parse(str), null, 2), `{ 33 | "b": [ 34 | 1 35 | ], 36 | "a": 1 // a 37 | }`) 38 | }) 39 | 40 | test('#17: insert key between a and b', t => { 41 | const str = `{ 42 | "a": 1, // a 43 | "b": 2, // b 44 | }` 45 | const parsed = parse(str) 46 | const obj = {} 47 | assign(obj, parsed, ['a']) 48 | obj.c = 3 49 | assign(obj, parsed, ['b']) 50 | 51 | t.is(stringify(obj, null, 2), `{ 52 | "a": 1, // a 53 | "c": 3, 54 | "b": 2 // b 55 | }`) 56 | }) 57 | 58 | test('#22: stringify parsed primitive', t => { 59 | const CASES = [ 60 | ['1 /* comment */', '1'], 61 | ['// comment\n1', '1'], 62 | ['true // comment', 'true'], 63 | ['"1" // comment', '"1"'] 64 | ] 65 | 66 | for (const [str, str2] of CASES) { 67 | t.is( 68 | stringify(parse(str), null, 2), 69 | str, 70 | `${str} with space` 71 | ) 72 | 73 | t.is( 74 | stringify(parse(str)), 75 | str2, 76 | `${str} with no space` 77 | ) 78 | } 79 | }) 80 | 81 | test('#21: comma placement', t => { 82 | const parsed = parse(`{ 83 | "foo": "bar" // comment 84 | }` 85 | ) 86 | 87 | parsed.bar = 'baz' 88 | 89 | t.is(stringify(parsed, null, 2), `{ 90 | "foo": "bar", // comment 91 | "bar": "baz" 92 | }`) 93 | }) 94 | 95 | test('#18', t => { 96 | const parsed = parse(`{ 97 | // b 98 | "b": 2, 99 | // a 100 | "a": 1 101 | }` 102 | ) 103 | 104 | const sorted = assign({}, parsed, Object.keys(parsed).sort()) 105 | 106 | t.is(stringify(sorted, null, 2), `{ 107 | // a 108 | "a": 1, 109 | // b 110 | "b": 2 111 | }`) 112 | }) 113 | 114 | test('#26: non-property comments', t => { 115 | const str = `// before all 116 | { 117 | // a 118 | "a": 1 119 | } 120 | // after all` 121 | 122 | const parsed = parse(str) 123 | 124 | t.is( 125 | stringify( 126 | assign( 127 | {}, 128 | parsed, 129 | ), 130 | null, 2 131 | ), 132 | str, 133 | 'should assign non-property comments if no keys' 134 | ) 135 | 136 | t.is( 137 | stringify( 138 | assign( 139 | { 140 | a: 1 141 | }, 142 | parsed, 143 | [] 144 | ), 145 | null, 2 146 | ), 147 | `// before all 148 | { 149 | "a": 1 150 | } 151 | // after all`, 152 | 'should assign non-property comments if keys is an empty array' 153 | ) 154 | 155 | t.is( 156 | stringify( 157 | assign( 158 | {}, 159 | parsed, 160 | ['a'] 161 | ), 162 | null, 163 | 2 164 | ), 165 | `{ 166 | // a 167 | "a": 1 168 | }`) 169 | }) 170 | -------------------------------------------------------------------------------- /test/parse.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | 3 | const parser = require('..') 4 | 5 | const cases = [ 6 | { 7 | d: '#21: introduce `after:prop` symbol', 8 | s: `{ 9 | "foo": "bar" /* after value:foo */, // after:foo 10 | "bar": "baz" // after:baz 11 | }`, 12 | o: '{"foo":"bar","bar":"baz"}', 13 | e (t, obj) { 14 | t.is(obj[Symbol.for('after-value:foo')][0].value, ' after value:foo ') 15 | t.is(obj[Symbol.for('after:foo')][0].value, ' after:foo') 16 | t.is(obj[Symbol.for('after:bar')][0].value, ' after:baz') 17 | } 18 | }, 19 | { 20 | d: '#21: introduce `after:prop` symbol: array', 21 | s: `[ 22 | 1 /* after value:0 */, // after:0 23 | 2 // after:1 24 | ]`, 25 | o: '[1,2]', 26 | e (t, obj) { 27 | t.is(obj[Symbol.for('after-value:0')][0].value, ' after value:0 ') 28 | t.is(obj[Symbol.for('after:0')][0].value, ' after:0') 29 | t.is(obj[Symbol.for('after:1')][0].value, ' after:1') 30 | } 31 | }, 32 | { 33 | d: 'empty object', 34 | s: ` 35 | { 36 | // comment 37 | } 38 | `, 39 | o: '{}', 40 | e (t, obj) { 41 | t.is(obj[Symbol.for('before')][0].value, ' comment') 42 | } 43 | }, 44 | { 45 | d: 'empty array', 46 | s: ` 47 | [ 48 | // comment 49 | ] 50 | `, 51 | o: '[]', 52 | e (t, obj) { 53 | t.is(obj[Symbol.for('before')][0].value, ' comment') 54 | } 55 | }, 56 | { 57 | d: '#17: after-comma comment, with trailing comma', 58 | s: `//top 59 | { 60 | "foo": "bar", // after comma foo 61 | "bar": "baz", // after comma bar 62 | }`, 63 | o: '{"foo":"bar","bar":"baz"}', 64 | e (t, obj) { 65 | t.is(obj.foo, 'bar') 66 | t.is(obj[Symbol.for('after:foo')][0].value, ' after comma foo') 67 | t.is(obj[Symbol.for('after:bar')][0].value, ' after comma bar') 68 | } 69 | }, 70 | { 71 | d: '#8: object with trailing comma', 72 | s: `//top 73 | { 74 | "foo": "bar", 75 | // after 76 | }`, 77 | o: '{"foo":"bar"}', 78 | e (t, obj) { 79 | t.is(obj.foo, 'bar') 80 | t.is(obj[Symbol.for('after:foo')][0].value, ' after') 81 | } 82 | }, 83 | { 84 | d: '#8: array with trailing comma', 85 | s: `//top 86 | [1,]`, 87 | o: '[1]', 88 | e (t, obj) { 89 | t.is(Number(obj[0]), 1) 90 | t.is(obj[Symbol.for('before-all')][0].value, 'top') 91 | } 92 | }, 93 | { 94 | d: 'comment at the top', 95 | s: '//top\n{"a":1}', 96 | o: '{"a":1}', 97 | e (t, obj) { 98 | t.is(obj.a, 1) 99 | t.is(obj[Symbol.for('before-all')][0].value, 'top') 100 | } 101 | }, 102 | { 103 | d: 'multiple comments at the top, both line and block', 104 | s: '//top\n/*abc*/{"a":1}', 105 | o: '{"a":1}', 106 | e (t, obj) { 107 | t.is(obj.a, 1) 108 | 109 | const [c1, c2] = obj[Symbol.for('before-all')] 110 | t.is(c1.value, 'top') 111 | t.is(c1.type, 'LineComment') 112 | t.is(c2.value, 'abc') 113 | t.is(c2.type, 'BlockComment') 114 | } 115 | }, 116 | { 117 | d: 'comment at the bottom', 118 | s: '{"a":1}\n//bot', 119 | o: '{"a":1}', 120 | e (t, obj) { 121 | t.is(obj.a, 1) 122 | const [c] = obj[Symbol.for('after-all')] 123 | t.is(c.value, 'bot') 124 | } 125 | }, 126 | { 127 | d: 'multiple comments at the bottom, both line and block', 128 | s: '{"a":1}\n//top\n/*abc*/', 129 | o: '{"a":1}', 130 | e (t, obj) { 131 | t.is(obj.a, 1) 132 | const [c1, c2] = obj[Symbol.for('after-all')] 133 | t.is(c1.value, 'top') 134 | t.is(c2.value, 'abc') 135 | } 136 | }, 137 | { 138 | d: 'comment for properties', 139 | s: '{//a\n"a":1}', 140 | o: '{"a":1}', 141 | e (t, obj) { 142 | t.is(obj.a, 1) 143 | const [c] = obj[Symbol.for('before:a')] 144 | t.is(c.value, 'a') 145 | t.is(c.inline, true) 146 | } 147 | }, 148 | { 149 | d: 'comment for properties, multiple at the top', 150 | s: '{//a\n/*b*/"a":1}', 151 | o: '{"a":1}', 152 | e (t, obj) { 153 | t.is(obj.a, 1) 154 | const [c1, c2] = obj[Symbol.for('before:a')] 155 | t.is(c1.value, 'a') 156 | t.is(c1.inline, true) 157 | t.is(c2.value, 'b') 158 | t.is(c2.inline, false) 159 | } 160 | }, 161 | { 162 | d: 'comment for properties, both top and right', 163 | s: '{//a\n"a":1//b\n}', 164 | o: '{"a":1}', 165 | e (t, obj) { 166 | t.is(obj.a, 1) 167 | const [c] = obj[Symbol.for('after:a')] 168 | t.is(c.value, 'b') 169 | t.is(c.inline, true) 170 | } 171 | }, 172 | { 173 | // #8 174 | d: 'support negative numbers', 175 | s: '{//a\n"a": -1}', 176 | o: '{"a": -1}', 177 | e (t, obj) { 178 | t.is(obj.a, - 1) 179 | } 180 | }, 181 | { 182 | d: 'inline comment after prop', 183 | s: `{ 184 | "a" /* a */: 1 185 | }`, 186 | o: '{"a":1}', 187 | e (t, obj) { 188 | const [c] = obj[Symbol.for('after-prop:a')] 189 | t.is(c.value, ' a ') 190 | t.is(c.inline, true) 191 | } 192 | }, 193 | { 194 | d: 'inline comment after comma', 195 | s: `{ 196 | "a": 1, // a 197 | "b": 2 198 | }`, 199 | o: '{"a":1,"b":2}', 200 | e (t, obj) { 201 | t.is(obj.a, 1) 202 | t.is(obj.b, 2) 203 | const [c] = obj[Symbol.for('after:a')] 204 | t.is(c.value, ' a') 205 | t.is(c.inline, true) 206 | } 207 | }, 208 | { 209 | d: 'array', 210 | s: `{ 211 | "a": /*a*/ [ // b 212 | //c 213 | 1 /*m*/ , // d 214 | // e 215 | 2 216 | // n 217 | ] /* 218 | g*/ //g2 219 | //h 220 | , 221 | "b" /*i*/ 222 | // j 223 | : 224 | // k 225 | 1 226 | } // f 227 | //l`, 228 | o: `{ 229 | "a": [1, 2], 230 | "b": 1 231 | }`, 232 | e (t, obj) { 233 | t.is(obj.a[0], 1) 234 | t.is(obj.a[1], 2) 235 | 236 | const [g, g2, h] = obj[Symbol.for('after-value:a')] 237 | 238 | t.deepEqual(g, { 239 | type: 'BlockComment', 240 | value: '\ng', 241 | inline: true, 242 | loc: { 243 | start: { 244 | line: 8, 245 | column: 8 246 | }, 247 | end: { 248 | line: 9, 249 | column: 3 250 | } 251 | } 252 | }) 253 | t.deepEqual(g2, { 254 | type: 'LineComment', 255 | value: 'g2', 256 | inline: true, 257 | loc: { 258 | start: { 259 | line: 9, 260 | column: 4 261 | }, 262 | end: { 263 | line: 9, 264 | column: 8 265 | } 266 | } 267 | }) 268 | t.deepEqual(h, { 269 | type: 'LineComment', 270 | value: 'h', 271 | inline: false, 272 | loc: { 273 | start: { 274 | line: 10, 275 | column: 6 276 | }, 277 | end: { 278 | line: 10, 279 | column: 9 280 | } 281 | }, 282 | }) 283 | 284 | const [i, j] = obj[Symbol.for('after-prop:b')] 285 | t.is(i.value, 'i') 286 | t.is(i.inline, true) 287 | t.is(j.value, ' j') 288 | 289 | const [k] = obj[Symbol.for('after-colon:b')] 290 | t.is(k.value, ' k') 291 | 292 | const [b, c] = obj.a[Symbol.for('before:0')] 293 | t.is(b.value, ' b') 294 | t.is(c.value, 'c') 295 | 296 | const [d] = obj.a[Symbol.for('after:0')] 297 | t.is(d.value, ' d') 298 | 299 | const [e] = obj.a[Symbol.for('before:1')] 300 | t.is(e.value, ' e') 301 | 302 | const [n] = obj.a[Symbol.for('after:1')] 303 | t.is(n.value, ' n') 304 | 305 | const [m] = obj.a[Symbol.for('after-value:0')] 306 | t.is(m.value, 'm') 307 | 308 | const [f, l] = obj[Symbol.for('after-all')] 309 | t.is(f.value, ' f') 310 | t.is(f.inline, true) 311 | t.is(l.value, 'l') 312 | t.is(l.inline, false) 313 | } 314 | } 315 | ] 316 | 317 | cases.forEach(c => { 318 | const tt = c.only 319 | ? test.only 320 | : test 321 | 322 | tt(c.d, t => { 323 | c.e(t, parser.parse(c.s)) 324 | }) 325 | 326 | tt(`${c.d}, removes comments`, t => { 327 | t.deepEqual(parser.parse(c.s, null, true), parser.parse(c.o)) 328 | }) 329 | }) 330 | 331 | const invalid = [ 332 | ['{', 1, 1], 333 | ['}', 1, 0], 334 | ['[', 1, 1], 335 | ['', 1, 0], 336 | ['{a:1}', 1, 1], 337 | ['{"a":a}', 1, 5], 338 | ['{"a":undefined}', 1, 5] 339 | ] 340 | 341 | const removes_position = s => s.replace(/\s+in JSON at position.+$/, '') 342 | 343 | // ECMA262 does not define the standard of error messages. 344 | // However, we throw error messages the same as JSON.parse() 345 | invalid.forEach(([i, line, col]) => { 346 | test(`error message:${i}`, t => { 347 | let error 348 | let err 349 | 350 | try { 351 | parser.parse(i) 352 | } catch (e) { 353 | error = e 354 | } 355 | 356 | try { 357 | JSON.parse(i) 358 | } catch (e) { 359 | err = e 360 | } 361 | 362 | t.is(!!(err && error), true) 363 | t.is(error.message, removes_position(err.message)) 364 | 365 | if (line !== undefined && col !== undefined) { 366 | t.is(error.line, line) 367 | t.is(error.column, col) 368 | } 369 | }) 370 | }) 371 | 372 | test('reviver', t => { 373 | t.is( 374 | parser.parse('{"p": 5}', (key, value) => 375 | typeof value === 'number' 376 | ? value * 2 // return value * 2 for numbers 377 | : value // return everything else unchanged 378 | ).p, 379 | 10 380 | ) 381 | }) 382 | 383 | test('special: null', t => { 384 | t.is(parser.parse(`// abc\nnull`), null) 385 | }) 386 | 387 | test('special: 1', t => { 388 | const result = parser.parse(`//abc\n1`) 389 | 390 | t.is(Number(result), 1) 391 | t.is(result[Symbol.for('before-all')][0].value, 'abc') 392 | }) 393 | 394 | test('special: "foo"', t => { 395 | const result = parser.parse(`//abc\n"foo"`) 396 | 397 | t.is(String(result), 'foo') 398 | t.is(result[Symbol.for('before-all')][0].value, 'abc') 399 | }) 400 | 401 | test('special: true', t => { 402 | const result = parser.parse(`//abc\ntrue`) 403 | 404 | t.true(Boolean(result)) 405 | t.is(result[Symbol.for('before-all')][0].value, 'abc') 406 | }) 407 | 408 | test('#20: Object.keys', t => { 409 | const content = `{ 410 | // comment 411 | "foo": "bar" 412 | }` 413 | 414 | const parsed = parser.parse(content) 415 | 416 | t.deepEqual(Object.keys(parsed), ['foo']) 417 | }) 418 | -------------------------------------------------------------------------------- /test/stringify.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const {resolve} = require('test-fixture')() 3 | const fs = require('fs') 4 | const {isFunction, isString} = require('core-util-is') 5 | 6 | const {parse, stringify} = require('..') 7 | 8 | 9 | const SUBJECTS = [ 10 | 'abc', 11 | '\u00ad\u0600', 12 | 1, 13 | true, 14 | false, 15 | null, 16 | undefined, 17 | [], 18 | {}, 19 | {a: 1, b: null}, 20 | ['abc', 1, {a: 1, b: undefined}], 21 | [undefined, 1, 'abc'], 22 | { 23 | a: undefined, 24 | b: false, 25 | c: [1, '1'], 26 | d: 'bar', 27 | e: { 28 | f: 2 29 | } 30 | }, 31 | Number.POSITIVE_INFINITY, 32 | Number.NEGATIVE_INFINITY, 33 | { 34 | toJSON () { 35 | return { 36 | foo: 1 37 | } 38 | } 39 | }, 40 | '"', 41 | { 42 | foo: '"', 43 | bar: '\b' 44 | } 45 | ] 46 | 47 | const REPLACERS = [ 48 | null, 49 | ['a'], 50 | (key, value) => { 51 | if (typeof value === 'string') { 52 | return undefined 53 | } 54 | 55 | return value 56 | } 57 | ] 58 | 59 | const SPACES = [ 60 | 1, 61 | 2, 62 | ' ', 63 | '1' 64 | ] 65 | 66 | const each = (subjects, replacers, spaces, iterator) => { 67 | subjects.forEach((subject, i) => { 68 | replacers.forEach((replacer, ii) => { 69 | spaces.forEach((space, iii) => { 70 | const desc = [subject, replacer, space] 71 | .map(s => 72 | isFunction(s) 73 | ? 'replacer' 74 | : JSON.stringify(s) 75 | ) 76 | .join(', ') 77 | 78 | iterator(subject, replacer, space, desc, 79 | // prevent title duplication 80 | `${i}+${ii}+${iii}`) 81 | }) 82 | }) 83 | }) 84 | } 85 | 86 | each(SUBJECTS, REPLACERS, SPACES, (subject, replacer, space, desc, i) => { 87 | test(`${i}: stringify: ${desc}`, t => { 88 | const compare = [ 89 | JSON.stringify(subject, replacer, space), 90 | stringify(subject, replacer, space) 91 | ] 92 | 93 | t.is(...compare) 94 | }) 95 | }) 96 | 97 | const OLD_CASES = [ 98 | 'deep', 99 | 'duplex', 100 | 'indent', 101 | 'simple', 102 | 'single-right', 103 | 'single-top' 104 | ] 105 | 106 | OLD_CASES.forEach(name => { 107 | [ 108 | ' ', 109 | 2, 110 | 3, 111 | null 112 | ].forEach(space => { 113 | const s = isString(space) 114 | ? space.length 115 | : space 116 | 117 | const filename = resolve(`${name}-null-${s}.json`) 118 | 119 | test(`${name}, space: ${s} (${space}): ${filename}`, t => { 120 | const file = resolve(filename) 121 | const content = fs.readFileSync(file).toString().trim() 122 | const parsed = parse(content) 123 | const str = stringify(parsed, null, space) 124 | 125 | t.is(str, content) 126 | }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /test/ts/test-ts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parse, 3 | stringify, 4 | tokenize, 5 | 6 | CommentArray, 7 | CommentObject, 8 | assign, 9 | CommentDescriptor, 10 | CommentSymbol 11 | } from '../..' 12 | 13 | const assert = (test: boolean, message: string): void => { 14 | if (!test) { 15 | throw new Error(message) 16 | } 17 | } 18 | 19 | assert((parse('{"a":1}') as CommentObject).a === 1, 'basic parse') 20 | 21 | const str = `{ 22 | // This is a comment 23 | "foo": "bar" 24 | }` 25 | const parsed = parse(str) 26 | 27 | const obj = assign({ 28 | bar: 'baz' 29 | }, parsed) 30 | 31 | assert(stringify(obj, null, 2) === `{ 32 | "bar": "baz", 33 | // This is a comment 34 | "foo": "bar" 35 | }`, 'assign') 36 | 37 | assert(Array.isArray(tokenize(str)), 'tokenize') 38 | 39 | const comment = "this is a comment" 40 | let commentDescriptor: CommentDescriptor = `before:0` 41 | 42 | const commentSrc = `[ 43 | //${comment} 44 | "bar" 45 | ]` 46 | 47 | assert((parse(commentSrc) as CommentArray)[Symbol.for(commentDescriptor) as CommentSymbol][0].value === comment, 'comment parse') 48 | commentDescriptor = "before"; 49 | -------------------------------------------------------------------------------- /test/ts/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [], 3 | "references": [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /test/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2015" 5 | }, 6 | "include": [ 7 | "./test-ts.ts" 8 | ] 9 | } 10 | --------------------------------------------------------------------------------