Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) 2013 kaelzhang <>, contributors
http://kael.me/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+600
View File
@@ -0,0 +1,600 @@
[![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)
[![Coverage](https://codecov.io/gh/kaelzhang/node-comment-json/branch/master/graph/badge.svg)](https://codecov.io/gh/kaelzhang/node-comment-json)
[![npm module downloads per month](http://img.shields.io/npm/dm/comment-json.svg)](https://www.npmjs.org/package/comment-json)
<!-- optional appveyor tst
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/kaelzhang/node-comment-json?branch=master&svg=true)](https://ci.appveyor.com/project/kaelzhang/node-comment-json)
-->
<!-- optional npm version
[![NPM version](https://badge.fury.io/js/comment-json.svg)](http://badge.fury.io/js/comment-json)
-->
<!-- optional dependency status
[![Dependency Status](https://david-dm.org/kaelzhang/node-comment-json.svg)](https://david-dm.org/kaelzhang/node-comment-json)
-->
# comment-json
Parse and stringify JSON with comments. It will retain comments even after saved!
- [Parse](#parse) JSON strings with comments into JavaScript objects and MAINTAIN comments
- supports comments everywhere, yes, **EVERYWHERE** in a JSON file, eventually 😆
- fixes the known issue about comments inside arrays.
- [Stringify](#stringify) the objects into JSON strings with comments if there are
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.
## Table of Contents
- [Why](#why) and [How](#how)
- [Usage and examples](#usage)
- API reference
- [parse](#parse)
- [stringify](#stringify)
- [assign](#assigntarget-object-source-object-keys-array)
- [CommentArray](#commentarray)
- [Change Logs](https://github.com/kaelzhang/node-comment-json/releases)
## Why?
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.
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.
So, **if you want to parse a JSON string with comments, modify it, then save it back**, `comment-json` is your must choice!
## How?
`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.
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.
## Install
```sh
$ npm i comment-json
```
~~For TypeScript developers, [`@types/comment-json`](https://www.npmjs.com/package/@types/comment-json) could be used~~
Since `2.4.1`, `comment-json` contains typescript declarations, so you might as well remove `@types/comment-json`.
## Usage
package.json:
```js
{
// package name
"name": "comment-json"
}
```
```js
const {
parse,
stringify,
assign
} = require('comment-json')
const fs = require('fs')
const obj = parse(fs.readFileSync('package.json').toString())
console.log(obj.name) // comment-json
stringify(obj, null, 2)
// Will be the same as package.json, Oh yeah! 😆
// which will be very useful if we use a json file to store configurations.
```
### Sort keys
It is a common use case to sort the keys of a JSON file
```js
const parsed = parse(`{
// b
"b": 2,
// a
"a": 1
}`)
// Copy the properties including comments from `parsed` to the new object `{}`
// according to the sequence of the given keys
const sorted = assign(
{},
parsed,
// You could also use your custom sorting function
Object.keys(parsed).sort()
)
console.log(stringify(sorted, null, 2))
// {
// // a
// "a": 1,
// // b
// "b": 2
// }
```
For details about `assign`, see [here](#assigntarget-object-source-object-keys-array).
## parse()
```ts
parse(text, reviver? = null, remove_comments? = false)
: object | string | number | boolean | null
```
- **text** `string` The string to parse as JSON. See the [JSON](http://json.org/) object for a description of JSON syntax.
- **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.
- **remove_comments?** `boolean = false` If true, the comments won't be maintained, which is often used when we want to get a clean object.
Returns `CommentJSONValue` (`object | string | number | boolean | null`) corresponding to the given JSON text.
If the `content` is:
```js
/**
before-all
*/
// before-all
{ // before:foo
// before:foo
/* before:foo */
"foo" /* after-prop:foo */: // after-colon:foo
1 // after-value:foo
// after-value:foo
, // after:foo
// before:bar
"bar": [ // before:0
// before:0
"baz" // after-value:0
// after-value:0
, // after:0
"quux"
// after:1
] // after:bar
// after:bar
}
// after-all
```
```js
const {inspect} = require('util')
const parsed = parse(content)
console.log(
inspect(parsed, {
// Since 4.0.0, symbol properties of comments are not enumerable,
// use `showHidden: true` to print them
showHidden: true
})
)
console.log(Object.keys(parsed))
// > ['foo', 'bar']
console.log(stringify(parsed, null, 2))
// 🚀 Exact as the content above! 🚀
```
And the value of `parsed` will be:
```js
{
// Comments before the JSON object
[Symbol.for('before-all')]: [{
type: 'BlockComment',
value: '\n before-all\n ',
inline: false,
loc: {
// The start location of `/**`
start: {
line: 1,
column: 0
},
// The end location of `*/`
end: {
line: 3,
column: 3
}
}
}, {
type: 'LineComment',
value: ' before-all',
inline: false,
loc: ...
}],
...
[Symbol.for('after-prop:foo')]: [{
type: 'BlockComment',
value: ' after-prop:foo ',
inline: true,
loc: ...
}],
// The real value
foo: 1,
bar: [
"baz",
"quux",
// The property of the array
[Symbol.for('after-value:0')]: [{
type: 'LineComment',
value: ' after-value:0',
inline: true,
loc: ...
}, ...],
...
]
}
```
There are **EIGHT** kinds of symbol properties:
```js
// Comments before everything
Symbol.for('before-all')
// If all things inside an object or an array are comments
Symbol.for('before')
// comment tokens before
// - a property of an object
// - an item of an array
// and after the previous comma(`,`) or the opening bracket(`{` or `[`)
Symbol.for(`before:${prop}`)
// comment tokens after property key `prop` and before colon(`:`)
Symbol.for(`after-prop:${prop}`)
// comment tokens after the colon(`:`) of property `prop` and before property value
Symbol.for(`after-colon:${prop}`)
// comment tokens after
// - the value of property `prop` inside an object
// - the item of index `prop` inside an array
// and before the next key-value/item delimiter(`,`)
// or the closing bracket(`}` or `]`)
Symbol.for(`after-value:${prop}`)
// comment tokens after
// - comma(`,`)
// - the value of property `prop` if it is the last property
Symbol.for(`after:${prop}`)
// Comments after everything
Symbol.for('after-all')
```
And the value of each symbol property is an **array** of `CommentToken`
```ts
interface CommentToken {
type: 'BlockComment' | 'LineComment'
// The content of the comment, including whitespaces and line breaks
value: string
// If the start location is the same line as the previous token,
// then `inline` is `true`
inline: boolean
// But pay attention that,
// locations will NOT be maintained when stringified
loc: CommentLocation
}
interface CommentLocation {
// The start location begins at the `//` or `/*` symbol
start: Location
// The end location of multi-line comment ends at the `*/` symbol
end: Location
}
interface Location {
line: number
column: number
}
```
### Query comments in TypeScript
`comment-json` provides a `symbol`-type called `CommentSymbol` which can be used for querying comments.
Furthermore, a type `CommentDescriptor` is provided for enforcing properly formatted symbol names:
```ts
import {
CommentDescriptor, CommentSymbol, parse, CommentArray
} from 'comment-json'
const parsed = parse(`{ /* test */ "foo": "bar" }`)
// typescript only allows properly formatted symbol names here
const symbolName: CommentDescriptor = 'before:foo'
console.log((parsed as CommentArray<string>)[Symbol.for(symbolName) as CommentSymbol][0].value)
```
In this example, casting to `Symbol.for(symbolName)` to `CommentSymbol` is mandatory.
Otherwise, TypeScript won't detect that you're trying to query comments.
### Parse into an object without comments
```js
console.log(parse(content, null, true))
```
And the result will be:
```js
{
foo: 1,
bar: [
"baz",
"quux"
]
}
```
### Special cases
```js
const parsed = parse(`
// comment
1
`)
console.log(parsed === 1)
// false
```
If we parse a JSON of primative type with `remove_comments:false`, then the return value of `parse()` will be of object type.
The value of `parsed` is equivalent to:
```js
const parsed = new Number(1)
parsed[Symbol.for('before-all')] = [{
type: 'LineComment',
value: ' comment',
inline: false,
loc: ...
}]
```
Which is similar for:
- `Boolean` type
- `String` type
For example
```js
const parsed = parse(`
"foo" /* comment */
`)
```
Which is equivalent to
```js
const parsed = new String('foo')
parsed[Symbol.for('after-all')] = [{
type: 'BlockComment',
value: ' comment ',
inline: true,
loc: ...
}]
```
But there is one exception:
```js
const parsed = parse(`
// comment
null
`)
console.log(parsed === null) // true
```
## stringify()
```ts
stringify(object: any, replacer?, space?): string
```
The arguments are the same as the vanilla [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify).
And it does the similar thing as the vanilla one, but also deal with extra properties and convert them into comments.
```js
console.log(stringify(parsed, null, 2))
// Exactly the same as `content`
```
#### space
If space is not specified, or the space is an empty string, the result of `stringify()` will have no comments.
For the case above:
```js
console.log(stringify(result)) // {"a":1}
console.log(stringify(result, null, 2)) // is the same as `code`
```
## assign(target: object, source?: object, keys?: Array<string>)
- **target** `object` the target object
- **source?** `object` the source object. This parameter is optional but it is silly to not pass this argument.
- **keys?** `Array<string>` If not specified, all enumerable own properties of `source` will be used.
This method is used to copy the enumerable own properties and their corresponding comment symbol properties to the target object.
```js
const parsed = parse(`// before all
{
// This is a comment
"foo": "bar"
}`)
const obj = assign({
bar: 'baz'
}, parsed)
stringify(obj, null, 2)
// // before all
// {
// "bar": "baz",
// // This is a comment
// "foo": "bar"
// }
```
### Special cases about `keys`
But if argument `keys` is specified and is not empty, then comment ` before all`, which belongs to no properties, will **NOT** be copied.
```js
const obj = assign({
bar: 'baz'
}, parsed, ['foo'])
stringify(obj, null, 2)
// {
// "bar": "baz",
// // This is a comment
// "foo": "bar"
// }
```
Specifying the argument `keys` as an empty array indicates that it will only copy non-property symbols of comments
```js
const obj = assign({
bar: 'baz'
}, parsed, [])
stringify(obj, null, 2)
// // before all
// {
// "bar": "baz",
// }
```
Non-property symbols include:
```js
Symbol.for('before-all')
Symbol.for('before')
Symbol.for('after-all')
```
## `CommentArray`
> Advanced Section
All arrays of the parsed object are `CommentArray`s.
The constructor of `CommentArray` could be accessed by:
```js
const {CommentArray} = require('comment-json')
```
If we modify a comment array, its comment symbol properties could be handled automatically.
```js
const parsed = parse(`{
"foo": [
// bar
"bar",
// baz,
"baz"
]
}`)
parsed.foo.unshift('qux')
stringify(parsed, null, 2)
// {
// "foo": [
// "qux",
// // bar
// "bar",
// // baz
// "baz"
// ]
// }
```
Oh yeah! 😆
But pay attention, if you reassign the property of a comment array with a normal array, all comments will be gone:
```js
parsed.foo = ['quux'].concat(parsed.foo)
stringify(parsed, null, 2)
// {
// "foo": [
// "quux",
// "qux",
// "bar",
// "baz"
// ]
// }
// Whoooops!! 😩 Comments are gone
```
Instead, we should:
```js
parsed.foo = new CommentArray('quux').concat(parsed.foo)
stringify(parsed, null, 2)
// {
// "foo": [
// "quux",
// "qux",
// // bar
// "bar",
// // baz
// "baz"
// ]
// }
```
## Special Cases about Trailing Comma
If we have a JSON string `str`
```js
{
"foo": "bar", // comment
}
```
```js
// When stringify, trailing commas will be eliminated
const stringified = stringify(parse(str), null, 2)
console.log(stringified)
```
And it will print:
```js
{
"foo": "bar" // comment
}
```
## License
[MIT](LICENSE)
## Change Logs
See [releases](https://github.com/kaelzhang/node-comment-json/releases)
+112
View File
@@ -0,0 +1,112 @@
// Original from DefinitelyTyped. Thanks a million
// Type definitions for comment-json 1.1
// Project: https://github.com/kaelzhang/node-comment-json
// Definitions by: Jason Dent <https://github.com/Jason3S>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
declare const commentSymbol: unique symbol
export type CommentPrefix = 'before'
| 'after-prop'
| 'after-colon'
| 'after-value'
| 'after'
export type CommentDescriptor = `${CommentPrefix}:${string}`
| 'before'
| 'before-all'
| 'after-all'
export type CommentSymbol = typeof commentSymbol
export class CommentArray<TValue> extends Array<TValue> {
[commentSymbol]: CommentToken[]
}
export type CommentJSONValue = number
| string
| null
| boolean
| CommentArray<CommentJSONValue>
| CommentObject
export interface CommentObject {
[key: string]: CommentJSONValue
[commentSymbol]: CommentToken[]
}
export interface CommentToken {
type: 'BlockComment' | 'LineComment'
// The content of the comment, including whitespaces and line breaks
value: string
// If the start location is the same line as the previous token,
// then `inline` is `true`
inline: boolean
// But pay attention that,
// locations will NOT be maintained when stringified
loc: CommentLocation
}
export interface CommentLocation {
// The start location begins at the `//` or `/*` symbol
start: Location
// The end location of multi-line comment ends at the `*/` symbol
end: Location
}
export interface Location {
line: number
column: number
}
export type Reviver = (k: number | string, v: unknown) => unknown
/**
* Converts a JavaScript Object Notation (JSON) string into an object.
* @param json A valid JSON string.
* @param reviver A function that transforms the results. This function is called for each member of the object.
* @param removesComments If true, the comments won't be maintained, which is often used when we want to get a clean object.
* If a member contains nested objects, the nested objects are transformed before the parent object is.
*/
export function parse(
json: string,
reviver?: Reviver | null,
removesComments?: boolean
): CommentJSONValue
/**
* Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
* @param value A JavaScript value, usually an object or array, to be converted.
* @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.
* @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
*/
export function stringify(
value: unknown,
replacer?: (
(key: string, value: unknown) => unknown
) | Array<number | string> | null,
space?: string | number
): string
export function tokenize(input: string, config?: TokenizeOptions): Token[]
export interface Token {
type: string
value: string
}
export interface TokenizeOptions {
tolerant?: boolean
range?: boolean
loc?: boolean
comment?: boolean
}
export function assign<TTarget, TSource>(
target: TTarget,
source: TSource,
// Although it actually accepts more key types and filters then`,
// we set the type of `keys` stricter
keys?: readonly (number | string)[]
): TTarget
+69
View File
@@ -0,0 +1,69 @@
{
"name": "comment-json",
"version": "4.2.5",
"description": "Parse and stringify JSON with comments. It will retain comments even after saved!",
"main": "src/index.js",
"types": "index.d.ts",
"scripts": {
"test": "npm run test:only",
"test:only": "npm run test:ts && npm run test:node",
"test:ts": "tsc -b test/ts/tsconfig.build.json && node test/ts/test-ts.js",
"test:node": "NODE_DEBUG=comment-json nyc ava --timeout=10s --verbose",
"test:dev": "npm run test:only && npm run report:dev",
"lint": "eslint .",
"fix": "eslint . --fix",
"posttest": "npm run report",
"report": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
"report:dev": "nyc report --reporter=html && npm run report:open",
"report:open": "open coverage/index.html"
},
"files": [
"src/",
"index.d.ts"
],
"repository": {
"type": "git",
"url": "git://github.com/kaelzhang/node-comment-json.git"
},
"keywords": [
"comment-json",
"comments",
"annotations",
"json",
"json-stringify",
"json-parse",
"parser",
"comments-json",
"json-comments"
],
"engines": {
"node": ">= 6"
},
"ava": {
"files": [
"test/*.test.js"
]
},
"author": "kaelzhang",
"license": "MIT",
"bugs": {
"url": "https://github.com/kaelzhang/node-comment-json/issues"
},
"devDependencies": {
"@ostai/eslint-config": "^3.6.0",
"ava": "^4.0.1",
"codecov": "^3.8.2",
"eslint": "^8.8.0",
"eslint-plugin-import": "^2.25.4",
"nyc": "^15.1.0",
"test-fixture": "^2.4.1",
"typescript": "^4.5.5"
},
"dependencies": {
"array-timsort": "^1.0.3",
"core-util-is": "^1.0.3",
"esprima": "^4.0.1",
"has-own-prop": "^2.0.0",
"repeat-string": "^1.6.1"
}
}
+288
View File
@@ -0,0 +1,288 @@
const {isArray} = require('core-util-is')
const {sort} = require('array-timsort')
const {
SYMBOL_PREFIXES,
UNDEFINED,
symbol,
copy_comments,
swap_comments
} = require('./common')
const reverse_comments = array => {
const {length} = array
let i = 0
const max = length / 2
for (; i < max; i ++) {
swap_comments(array, i, length - i - 1)
}
}
const move_comment = (target, source, i, offset, remove) => {
copy_comments(target, source, i + offset, i, remove)
}
const move_comments = (
// `Array` target array
target,
// `Array` source array
source,
// `number` start index
start,
// `number` number of indexes to move
count,
// `number` offset to move
offset,
// `boolean` whether should remove the comments from source
remove
) => {
if (offset > 0) {
let i = count
// | count | offset |
// source: -------------
// target: -------------
// | remove |
// => remove === offset
// From [count - 1, 0]
while (i -- > 0) {
move_comment(target, source, start + i, offset, remove)
}
return
}
let i = 0
// | remove | count |
// -------------
// -------------
// | offset |
// From [0, count - 1]
while (i < count) {
const ii = i ++
move_comment(target, source, start + ii, offset, remove)
}
}
const remove_comments = (array, key) => {
SYMBOL_PREFIXES.forEach(prefix => {
const prop = symbol(prefix, key)
delete array[prop]
})
}
const get_mapped = (map, key) => {
let mapped = key
while (mapped in map) {
mapped = map[mapped]
}
return mapped
}
class CommentArray extends Array {
// - deleteCount + items.length
// We should avoid `splice(begin, deleteCount, ...items)`,
// because `splice(0, undefined)` is not equivalent to `splice(0)`,
// as well as:
// - slice
splice (...args) {
const {length} = this
const ret = super.splice(...args)
// #16
// If no element removed, we might still need to move comments,
// because splice could add new items
// if (!ret.length) {
// return ret
// }
// JavaScript syntax is silly
// eslint-disable-next-line prefer-const
let [begin, deleteCount, ...items] = args
if (begin < 0) {
begin += length
}
if (arguments.length === 1) {
deleteCount = length - begin
} else {
deleteCount = Math.min(length - begin, deleteCount)
}
const {
length: item_length
} = items
// itemsToDelete: -
// itemsToAdd: +
// | dc | count |
// =======-------------============
// =======++++++============
// | il |
const offset = item_length - deleteCount
const start = begin + deleteCount
const count = length - start
move_comments(this, this, start, count, offset, true)
return ret
}
slice (...args) {
const {length} = this
const array = super.slice(...args)
if (!array.length) {
return new CommentArray()
}
let [begin, before] = args
// Ref:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
if (before === UNDEFINED) {
before = length
} else if (before < 0) {
before += length
}
if (begin < 0) {
begin += length
} else if (begin === UNDEFINED) {
begin = 0
}
move_comments(array, this, begin, before - begin, - begin)
return array
}
unshift (...items) {
const {length} = this
const ret = super.unshift(...items)
const {
length: items_length
} = items
if (items_length > 0) {
move_comments(this, this, 0, length, items_length, true)
}
return ret
}
shift () {
const ret = super.shift()
const {length} = this
remove_comments(this, 0)
move_comments(this, this, 1, length, - 1, true)
return ret
}
reverse () {
super.reverse()
reverse_comments(this)
return this
}
pop () {
const ret = super.pop()
// Removes comments
remove_comments(this, this.length)
return ret
}
concat (...items) {
let {length} = this
const ret = super.concat(...items)
if (!items.length) {
return ret
}
move_comments(ret, this, 0, this.length, 0)
items.forEach(item => {
const prev = length
length += isArray(item)
? item.length
: 1
if (!(item instanceof CommentArray)) {
return
}
move_comments(ret, item, 0, item.length, prev)
})
return ret
}
sort (...args) {
const result = sort(
this,
// Make sure there is no more than one argument
...args.slice(0, 1)
)
// For example,
// if we sort ['b', 'd', 'c', 'a'],
// then `result` will be [3, 0, 2, 1], and the array is ['a', 'b', 'c', 'd']
// First, we swap index 0 (b) and index 3 (a), then the array comments are
// ['a.comments', 'd.comments', 'c.comments', 'b.comments']
// index 0 is finalized
// index 3 is actually mapped to original index 0, we present as 0 -> 3
// Then swap index 1 (d) and index 0 (-> 3, b)
// 1 (index) -> 0 (new index) -> 3 (real_index)
// ['d.comments', 'b.comments', 'c.comments', 'd.comments']
// index 1 is finalized
// index 3 is contains the item of original index 1
// - we present as 1 -> 3
// - it is ok that we don't remove mapping 0 -> 3
// Then index 2 should be skipped
// Then swap index 3 (d) and index 1 (-> 3, b), skipped
const map = Object.create(null)
result.forEach((source_index, index) => {
if (source_index === index) {
return
}
const real_source_index = get_mapped(map, source_index)
if (real_source_index === index) {
return
}
// The item of index `index` gets the final value
// delete map[index]
map[index] = real_source_index
swap_comments(this, index, real_source_index)
})
return this
}
}
module.exports = {
CommentArray
}
+181
View File
@@ -0,0 +1,181 @@
const hasOwnProperty = require('has-own-prop')
const {
isObject,
isArray,
isString,
isNumber
} = require('core-util-is')
const PREFIX_BEFORE = 'before'
const PREFIX_AFTER_PROP = 'after-prop'
const PREFIX_AFTER_COLON = 'after-colon'
const PREFIX_AFTER_VALUE = 'after-value'
const PREFIX_AFTER = 'after'
const PREFIX_BEFORE_ALL = 'before-all'
const PREFIX_AFTER_ALL = 'after-all'
const BRACKET_OPEN = '['
const BRACKET_CLOSE = ']'
const CURLY_BRACKET_OPEN = '{'
const CURLY_BRACKET_CLOSE = '}'
const COMMA = ','
const EMPTY = ''
const MINUS = '-'
const SYMBOL_PREFIXES = [
PREFIX_BEFORE,
PREFIX_AFTER_PROP,
PREFIX_AFTER_COLON,
PREFIX_AFTER_VALUE,
PREFIX_AFTER
]
const NON_PROP_SYMBOL_KEYS = [
PREFIX_BEFORE,
PREFIX_BEFORE_ALL,
PREFIX_AFTER_ALL
].map(Symbol.for)
const COLON = ':'
const UNDEFINED = undefined
const symbol = (prefix, key) => Symbol.for(prefix + COLON + key)
const define = (target, key, value) => Object.defineProperty(target, key, {
value,
writable: true,
configurable: true
})
const copy_comments_by_kind = (
target, source, target_key, source_key, prefix, remove_source
) => {
const source_prop = symbol(prefix, source_key)
if (!hasOwnProperty(source, source_prop)) {
return
}
const target_prop = target_key === source_key
? source_prop
: symbol(prefix, target_key)
define(target, target_prop, source[source_prop])
if (remove_source) {
delete source[source_prop]
}
}
const copy_comments = (
target, source, target_key, source_key, remove_source
) => {
SYMBOL_PREFIXES.forEach(prefix => {
copy_comments_by_kind(
target, source, target_key, source_key, prefix, remove_source
)
})
}
const swap_comments = (array, from, to) => {
if (from === to) {
return
}
SYMBOL_PREFIXES.forEach(prefix => {
const target_prop = symbol(prefix, to)
if (!hasOwnProperty(array, target_prop)) {
copy_comments_by_kind(array, array, to, from, prefix, true)
return
}
const comments = array[target_prop]
delete array[target_prop]
copy_comments_by_kind(array, array, to, from, prefix, true)
define(array, symbol(prefix, from), comments)
})
}
const assign_non_prop_comments = (target, source) => {
NON_PROP_SYMBOL_KEYS.forEach(key => {
const comments = source[key]
if (comments) {
define(target, key, comments)
}
})
}
// Assign keys and comments
const assign = (target, source, keys) => {
keys.forEach(key => {
if (!isString(key) && !isNumber(key)) {
return
}
if (!hasOwnProperty(source, key)) {
return
}
target[key] = source[key]
copy_comments(target, source, key, key)
})
return target
}
module.exports = {
SYMBOL_PREFIXES,
PREFIX_BEFORE,
PREFIX_AFTER_PROP,
PREFIX_AFTER_COLON,
PREFIX_AFTER_VALUE,
PREFIX_AFTER,
PREFIX_BEFORE_ALL,
PREFIX_AFTER_ALL,
BRACKET_OPEN,
BRACKET_CLOSE,
CURLY_BRACKET_OPEN,
CURLY_BRACKET_CLOSE,
COLON,
COMMA,
MINUS,
EMPTY,
UNDEFINED,
symbol,
define,
copy_comments,
swap_comments,
assign_non_prop_comments,
assign (target, source, keys) {
if (!isObject(target)) {
throw new TypeError('Cannot convert undefined or null to object')
}
if (!isObject(source)) {
return target
}
if (keys === UNDEFINED) {
keys = Object.keys(source)
// We assign non-property comments
// if argument `keys` is not specified
assign_non_prop_comments(target, source)
} else if (!isArray(keys)) {
throw new TypeError('keys must be array or undefined')
} else if (keys.length === 0) {
// Or argument `keys` is an empty array
assign_non_prop_comments(target, source)
}
return assign(target, source, keys)
}
}
+13
View File
@@ -0,0 +1,13 @@
const {parse, tokenize} = require('./parse')
const stringify = require('./stringify')
const {CommentArray} = require('./array')
const {assign} = require('./common')
module.exports = {
parse,
stringify,
tokenize,
CommentArray,
assign
}
+451
View File
@@ -0,0 +1,451 @@
// JSON formatting
const esprima = require('esprima')
const {
CommentArray,
} = require('./array')
const {
PREFIX_BEFORE,
PREFIX_AFTER_PROP,
PREFIX_AFTER_COLON,
PREFIX_AFTER_VALUE,
PREFIX_AFTER,
PREFIX_BEFORE_ALL,
PREFIX_AFTER_ALL,
BRACKET_OPEN,
BRACKET_CLOSE,
CURLY_BRACKET_OPEN,
CURLY_BRACKET_CLOSE,
COLON,
COMMA,
MINUS,
EMPTY,
UNDEFINED,
define,
assign_non_prop_comments
} = require('./common')
const tokenize = code => esprima.tokenize(code, {
comment: true,
loc: true
})
const previous_hosts = []
let comments_host = null
let unassigned_comments = null
const previous_props = []
let last_prop
let remove_comments = false
let inline = false
let tokens = null
let last = null
let current = null
let index
let reviver = null
const clean = () => {
previous_props.length =
previous_hosts.length = 0
last = null
last_prop = UNDEFINED
}
const free = () => {
clean()
tokens.length = 0
unassigned_comments =
comments_host =
tokens =
last =
current =
reviver = null
}
const symbolFor = prefix => Symbol.for(
last_prop !== UNDEFINED
? prefix + COLON + last_prop
: prefix
)
const transform = (k, v) => reviver
? reviver(k, v)
: v
const unexpected = () => {
const error = new SyntaxError(`Unexpected token ${current.value.slice(0, 1)}`)
Object.assign(error, current.loc.start)
throw error
}
const unexpected_end = () => {
const error = new SyntaxError('Unexpected end of JSON input')
Object.assign(error, last
? last.loc.end
// Empty string
: {
line: 1,
column: 0
})
throw error
}
// Move the reader to the next
const next = () => {
const new_token = tokens[++ index]
inline = current
&& new_token
&& current.loc.end.line === new_token.loc.start.line
|| false
last = current
current = new_token
}
const type = () => {
if (!current) {
unexpected_end()
}
return current.type === 'Punctuator'
? current.value
: current.type
}
const is = t => type() === t
const expect = a => {
if (!is(a)) {
unexpected()
}
}
const set_comments_host = new_host => {
previous_hosts.push(comments_host)
comments_host = new_host
}
const restore_comments_host = () => {
comments_host = previous_hosts.pop()
}
const assign_after_comments = () => {
if (!unassigned_comments) {
return
}
const after_comments = []
for (const comment of unassigned_comments) {
// If the comment is inline, then it is an after-comma comment
if (comment.inline) {
after_comments.push(comment)
// Otherwise, all comments are before:<next-prop> comment
} else {
break
}
}
const {length} = after_comments
if (!length) {
return
}
if (length === unassigned_comments.length) {
// If unassigned_comments are all consumed
unassigned_comments = null
} else {
unassigned_comments.splice(0, length)
}
define(comments_host, symbolFor(PREFIX_AFTER), after_comments)
}
const assign_comments = prefix => {
if (!unassigned_comments) {
return
}
define(comments_host, symbolFor(prefix), unassigned_comments)
unassigned_comments = null
}
const parse_comments = prefix => {
const comments = []
while (
current
&& (
is('LineComment')
|| is('BlockComment')
)
) {
const comment = {
...current,
inline
}
// delete comment.loc
comments.push(comment)
next()
}
if (remove_comments) {
return
}
if (!comments.length) {
return
}
if (prefix) {
define(comments_host, symbolFor(prefix), comments)
return
}
unassigned_comments = comments
}
const set_prop = (prop, push) => {
if (push) {
previous_props.push(last_prop)
}
last_prop = prop
}
const restore_prop = () => {
last_prop = previous_props.pop()
}
const parse_object = () => {
const obj = {}
set_comments_host(obj)
set_prop(UNDEFINED, true)
let started = false
let name
parse_comments()
while (!is(CURLY_BRACKET_CLOSE)) {
if (started) {
assign_comments(PREFIX_AFTER_VALUE)
// key-value pair delimiter
expect(COMMA)
next()
parse_comments()
assign_after_comments()
// If there is a trailing comma, we might reach the end
// ```
// {
// "a": 1,
// }
// ```
if (is(CURLY_BRACKET_CLOSE)) {
break
}
}
started = true
expect('String')
name = JSON.parse(current.value)
set_prop(name)
assign_comments(PREFIX_BEFORE)
next()
parse_comments(PREFIX_AFTER_PROP)
expect(COLON)
next()
parse_comments(PREFIX_AFTER_COLON)
obj[name] = transform(name, walk())
parse_comments()
}
if (started) {
// If there are properties,
// then the unassigned comments are after comments
assign_comments(PREFIX_AFTER)
}
// bypass }
next()
last_prop = undefined
if (!started) {
// Otherwise, they are before comments
assign_comments(PREFIX_BEFORE)
}
restore_comments_host()
restore_prop()
return obj
}
const parse_array = () => {
const array = new CommentArray()
set_comments_host(array)
set_prop(UNDEFINED, true)
let started = false
let i = 0
parse_comments()
while (!is(BRACKET_CLOSE)) {
if (started) {
assign_comments(PREFIX_AFTER_VALUE)
expect(COMMA)
next()
parse_comments()
assign_after_comments()
if (is(BRACKET_CLOSE)) {
break
}
}
started = true
set_prop(i)
assign_comments(PREFIX_BEFORE)
array[i] = transform(i, walk())
i ++
parse_comments()
}
if (started) {
assign_comments(PREFIX_AFTER)
}
next()
last_prop = undefined
if (!started) {
assign_comments(PREFIX_BEFORE)
}
restore_comments_host()
restore_prop()
return array
}
function walk () {
let tt = type()
if (tt === CURLY_BRACKET_OPEN) {
next()
return parse_object()
}
if (tt === BRACKET_OPEN) {
next()
return parse_array()
}
let negative = EMPTY
// -1
if (tt === MINUS) {
next()
tt = type()
negative = MINUS
}
let v
switch (tt) {
case 'String':
case 'Boolean':
case 'Null':
case 'Numeric':
v = current.value
next()
return JSON.parse(negative + v)
default:
}
}
const isObject = subject => Object(subject) === subject
const parse = (code, rev, no_comments) => {
// Clean variables in closure
clean()
tokens = tokenize(code)
reviver = rev
remove_comments = no_comments
if (!tokens.length) {
unexpected_end()
}
index = - 1
next()
set_comments_host({})
parse_comments(PREFIX_BEFORE_ALL)
let result = walk()
parse_comments(PREFIX_AFTER_ALL)
if (current) {
unexpected()
}
if (!no_comments && result !== null) {
if (!isObject(result)) {
// 1 -> new Number(1)
// true -> new Boolean(1)
// "foo" -> new String("foo")
// eslint-disable-next-line no-new-object
result = new Object(result)
}
assign_non_prop_comments(result, comments_host)
}
restore_comments_host()
// reviver
result = transform('', result)
free()
return result
}
module.exports = {
parse,
tokenize
}
+366
View File
@@ -0,0 +1,366 @@
const {
isArray, isObject, isFunction, isNumber, isString
} = require('core-util-is')
const repeat = require('repeat-string')
const {
PREFIX_BEFORE_ALL,
PREFIX_BEFORE,
PREFIX_AFTER_PROP,
PREFIX_AFTER_COLON,
PREFIX_AFTER_VALUE,
PREFIX_AFTER,
PREFIX_AFTER_ALL,
BRACKET_OPEN,
BRACKET_CLOSE,
CURLY_BRACKET_OPEN,
CURLY_BRACKET_CLOSE,
COLON,
COMMA,
EMPTY,
UNDEFINED
} = require('./common')
// eslint-disable-next-line no-control-regex, no-misleading-character-class
const ESCAPABLE = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g
// String constants
const SPACE = ' '
const LF = '\n'
const STR_NULL = 'null'
// Symbol tags
const BEFORE = prop => `${PREFIX_BEFORE}:${prop}`
const AFTER_PROP = prop => `${PREFIX_AFTER_PROP}:${prop}`
const AFTER_COLON = prop => `${PREFIX_AFTER_COLON}:${prop}`
const AFTER_VALUE = prop => `${PREFIX_AFTER_VALUE}:${prop}`
const AFTER = prop => `${PREFIX_AFTER}:${prop}`
// table of character substitutions
const meta = {
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"': '\\"',
'\\': '\\\\'
}
const escape = string => {
ESCAPABLE.lastIndex = 0
if (!ESCAPABLE.test(string)) {
return string
}
return string.replace(ESCAPABLE, a => {
const c = meta[a]
return typeof c === 'string'
? c
: a
})
}
// Escape no control characters, no quote characters,
// and no backslash characters,
// then we can safely slap some quotes around it.
const quote = string => `"${escape(string)}"`
const comment_stringify = (value, line) => line
? `//${value}`
: `/*${value}*/`
// display_block `boolean` whether the
// WHOLE block of comments is always a block group
const process_comments = (host, symbol_tag, deeper_gap, display_block) => {
const comments = host[Symbol.for(symbol_tag)]
if (!comments || !comments.length) {
return EMPTY
}
let is_line_comment = false
const str = comments.reduce((prev, {
inline,
type,
value
}) => {
const delimiter = inline
? SPACE
: LF + deeper_gap
is_line_comment = type === 'LineComment'
return prev + delimiter + comment_stringify(value, is_line_comment)
}, EMPTY)
return display_block
// line comment should always end with a LF
|| is_line_comment
? str + LF + deeper_gap
: str
}
let replacer = null
let indent = EMPTY
const clean = () => {
replacer = null
indent = EMPTY
}
const join = (one, two, gap) =>
one
? two
// Symbol.for('before') and Symbol.for('before:prop')
// might both exist if user mannually add comments to the object
// and make a mistake.
// SO, we are not to only trimRight but trim for both sides
? one + two.trim() + LF + gap
: one.trimRight() + LF + gap
: two
? two.trimRight() + LF + gap
: EMPTY
const join_content = (inside, value, gap) => {
const comment = process_comments(value, PREFIX_BEFORE, gap + indent, true)
return join(comment, inside, gap)
}
// | deeper_gap |
// | gap | indent |
// [
// "foo",
// "bar"
// ]
const array_stringify = (value, gap) => {
const deeper_gap = gap + indent
const {length} = value
// From the item to before close
let inside = EMPTY
let after_comma = EMPTY
// Never use Array.prototype.forEach,
// that we should iterate all items
for (let i = 0; i < length; i ++) {
if (i !== 0) {
inside += COMMA
}
const before = join(
after_comma,
process_comments(value, BEFORE(i), deeper_gap),
deeper_gap
)
inside += before || (LF + deeper_gap)
// JSON.stringify([undefined]) => [null]
inside += stringify(i, value, deeper_gap) || STR_NULL
inside += process_comments(value, AFTER_VALUE(i), deeper_gap)
after_comma = process_comments(value, AFTER(i), deeper_gap)
}
inside += join(
after_comma,
process_comments(value, PREFIX_AFTER, deeper_gap),
deeper_gap
)
return BRACKET_OPEN
+ join_content(inside, value, gap)
+ BRACKET_CLOSE
}
// | deeper_gap |
// | gap | indent |
// {
// "foo": 1,
// "bar": 2
// }
const object_stringify = (value, gap) => {
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null'
}
const deeper_gap = gap + indent
// From the first element to before close
let inside = EMPTY
let after_comma = EMPTY
let first = true
const keys = isArray(replacer)
? replacer
: Object.keys(value)
const iteratee = key => {
// Stringified value
const sv = stringify(key, value, deeper_gap)
// If a value is undefined, then the key-value pair should be ignored
if (sv === UNDEFINED) {
return
}
// The treat ment
if (!first) {
inside += COMMA
}
first = false
const before = join(
after_comma,
process_comments(value, BEFORE(key), deeper_gap),
deeper_gap
)
inside += before || (LF + deeper_gap)
inside += quote(key)
+ process_comments(value, AFTER_PROP(key), deeper_gap)
+ COLON
+ process_comments(value, AFTER_COLON(key), deeper_gap)
+ SPACE
+ sv
+ process_comments(value, AFTER_VALUE(key), deeper_gap)
after_comma = process_comments(value, AFTER(key), deeper_gap)
}
keys.forEach(iteratee)
// if (after_comma) {
// inside += COMMA
// }
inside += join(
after_comma,
process_comments(value, PREFIX_AFTER, deeper_gap),
deeper_gap
)
return CURLY_BRACKET_OPEN
+ join_content(inside, value, gap)
+ CURLY_BRACKET_CLOSE
}
// @param {string} key
// @param {Object} holder
// @param {function()|Array} replacer
// @param {string} indent
// @param {string} gap
function stringify (key, holder, gap) {
let value = holder[key]
// If the value has a toJSON method, call it to obtain a replacement value.
if (isObject(value) && isFunction(value.toJSON)) {
value = value.toJSON(key)
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (isFunction(replacer)) {
value = replacer.call(holder, key, value)
}
switch (typeof value) {
case 'string':
return quote(value)
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return Number.isFinite(value) ? String(value) : STR_NULL
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value)
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
return isArray(value)
? array_stringify(value, gap)
: object_stringify(value, gap)
// undefined
default:
// JSON.stringify(undefined) === undefined
// JSON.stringify('foo', () => undefined) === undefined
}
}
const get_indent = space => isString(space)
// If the space parameter is a string, it will be used as the indent string.
? space
: isNumber(space)
? repeat(SPACE, space)
: EMPTY
const {toString} = Object.prototype
const PRIMITIVE_OBJECT_TYPES = [
'[object Number]',
'[object String]',
'[object Boolean]'
]
const is_primitive_object = subject => {
if (typeof subject !== 'object') {
return false
}
const str = toString.call(subject)
return PRIMITIVE_OBJECT_TYPES.includes(str)
}
// @param {function()|Array} replacer
// @param {string|number} space
module.exports = (value, replacer_, space) => {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
// If the space parameter is a number, make an indent string containing that
// many spaces.
const indent_ = get_indent(space)
if (!indent_) {
return JSON.stringify(value, replacer_)
}
// vanilla `JSON.parse` allow invalid replacer
if (!isFunction(replacer_) && !isArray(replacer_)) {
replacer_ = null
}
replacer = replacer_
indent = indent_
const str = is_primitive_object(value)
? JSON.stringify(value)
: stringify('', {'': value}, EMPTY)
clean()
return isObject(value)
? process_comments(value, PREFIX_BEFORE_ALL, EMPTY).trimLeft()
+ str
+ process_comments(value, PREFIX_AFTER_ALL, EMPTY).trimRight()
: str
}