This commit is contained in:
Tutur33
2023-11-24 22:35:41 +01:00
parent 3c0b507a93
commit 7644b2a0f7
45165 changed files with 4803356 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
# History
+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.
+456
View File
@@ -0,0 +1,456 @@
[![Build Status](https://travis-ci.org/kaelzhang/node-comment-json.svg?branch=master)](https://travis-ci.org/kaelzhang/node-comment-json)
[![Coverage](https://codecov.io/gh/kaelzhang/node-comment-json/branch/master/graph/badge.svg)](https://codecov.io/gh/kaelzhang/node-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 npm downloads
[![npm module downloads per month](http://img.shields.io/npm/dm/comment-json.svg)](https://www.npmjs.org/package/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.
## 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.
## 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.
```
## 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 `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-comma:foo
1 // after-value:foo
// after-value:foo
, // before:bar
// before:bar
"bar": [ // before:0
// before:0
"baz" // after-value:0
// after-value:0
, // before:1
"quux"
// after-value:1
] // after-value:bar
// after-value:bar
}
// after-all
```
```js
const parsed = parse(content)
console.log(parsed)
console.log(stringify(parsed, null, 2))
// 🚀 Exact as the content above! 🚀
```
And the result 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 before 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}`)
// if comments after
// - the last key-value:pair of an object
// - the last item of an array
Symbol.for('after')
// 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
}
```
### 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(`{
// This is a comment
"foo": "bar"
}`)
const obj = assign({
bar: 'baz'
}, parsed)
stringify(obj, null, 2)
// {
// "bar": "baz",
// // This is a comment
// "foo": "bar"
// }
```
## `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"
// ]
// }
```
## License
[MIT](LICENSE)
+85
View File
@@ -0,0 +1,85 @@
// 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
// For now, typescript does not support symbol as object/array index key,
// just use any to clean the mess
// https://github.com/microsoft/TypeScript/issues/1863
export type CommentJSONValue = any;
export type CommentArray = any;
// export type CommentJSONValue = number | string | null | boolean | CommentJSONArray<CommentJSONValue> | CommentJSONObject
// // export type CommentJSONArray = Array<CommentJSONValue>
// export class CommentJSONArray<CommentJSONValue> extends Array<CommentJSONValue> {
// [key: symbol]: CommentToken
// }
// export interface CommentJSONObject {
// [key: string]: CommentJSONValue
// [key: symbol]: 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: any) => any;
/**
* 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 removes_comments 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, removes_comments?: 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: any, replacer?: ((key: string, value: any) => any) | 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(target: any, source: any, keys?: Array<string>): any;
+68
View File
@@ -0,0 +1,68 @@
{
"name": "comment-json",
"version": "2.4.2",
"description": "Parse and stringify JSON with comments. It will retain comments even after saved!",
"main": "src/index.js",
"scripts": {
"test": "npm run test:only",
"test:only": "npm run test:ts && npm run test:node",
"test:ts": "tsc test/ts/test-ts.ts && 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": {
"babel": false,
"files": [
"test/*.test.js"
]
},
"author": "kaelzhang",
"license": "MIT",
"bugs": {
"url": "https://github.com/kaelzhang/node-comment-json/issues"
},
"devDependencies": {
"@ostai/eslint-config": "^3.5.0",
"ava": "^2.4.0",
"codecov": "^3.6.1",
"eslint": "^6.6.0",
"eslint-plugin-import": "^2.18.2",
"nyc": "^14.1.1",
"test-fixture": "^2.4.1",
"typescript": "^3.7.4"
},
"dependencies": {
"core-util-is": "^1.0.2",
"esprima": "^4.0.1",
"has-own-prop": "^2.0.0",
"repeat-string": "^1.6.1"
}
}
+309
View File
@@ -0,0 +1,309 @@
const hasOwnProperty = require('has-own-prop')
const {isObject, isArray} = 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 SYMBOL_PREFIXES = [
PREFIX_BEFORE,
PREFIX_AFTER_PROP,
PREFIX_AFTER_COLON,
PREFIX_AFTER_VALUE
]
const COLON = ':'
const UNDEFINED = undefined
const symbol = (prefix, key) => Symbol.for(prefix + COLON + key)
const assign_comments = (
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)
target[target_prop] = source[source_prop]
if (remove_source) {
delete source[source_prop]
}
}
// Assign keys and comments
const assign = (target, source, keys) => {
keys.forEach(key => {
if (!hasOwnProperty(source, key)) {
return
}
target[key] = source[key]
SYMBOL_PREFIXES.forEach(prefix => {
assign_comments(target, source, key, key, prefix)
})
})
return target
}
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)) {
assign_comments(array, array, to, from, prefix)
return
}
const comments = array[target_prop]
assign_comments(array, array, to, from, prefix)
array[symbol(prefix, from)] = comments
})
}
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) => {
SYMBOL_PREFIXES.forEach(prefix => {
assign_comments(target, source, i + offset, i, prefix, 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 && i < offset)
}
return
}
let i = 0
const min_remove = count + offset
// | remove | count |
// -------------
// -------------
// | offset |
// From [0, count - 1]
while (i < count) {
const ii = i ++
move_comment(target, source, start + ii, offset, remove && i >= min_remove)
}
}
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
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
const {length} = this
SYMBOL_PREFIXES.forEach(prefix => {
const prop = symbol(prefix, length)
delete this[prop]
})
return ret
}
concat (...items) {
let {length} = this
const ret = super.concat(...items)
if (!items.length) {
return ret
}
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
}
}
module.exports = {
CommentArray,
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)
} else if (!isArray(keys)) {
throw new TypeError('keys must be array or undefined')
}
return assign(target, source, keys)
},
PREFIX_BEFORE,
PREFIX_AFTER_PROP,
PREFIX_AFTER_COLON,
PREFIX_AFTER_VALUE,
COLON,
UNDEFINED
}
+12
View File
@@ -0,0 +1,12 @@
const {parse, tokenize} = require('./parse')
const stringify = require('./stringify')
const {CommentArray, assign} = require('./array')
module.exports = {
parse,
stringify,
tokenize,
CommentArray,
assign
}
+407
View File
@@ -0,0 +1,407 @@
// JSON formatting
const esprima = require('esprima')
const {
CommentArray,
PREFIX_BEFORE,
PREFIX_AFTER_PROP,
PREFIX_AFTER_COLON,
PREFIX_AFTER_VALUE,
COLON,
UNDEFINED
} = require('./array')
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 PREFIX_BEFORE_ALL = 'before-all'
const PREFIX_AFTER = 'after'
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 symbolFor = prefix => Symbol.for(
last_prop !== UNDEFINED
? `${prefix}:${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_comments = prefix => {
if (!unassigned_comments) {
return
}
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) {
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) {
// key-value pair delimiter
expect(COMMA)
next()
parse_comments()
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(PREFIX_AFTER_VALUE)
}
// bypass }
next()
last_prop = undefined
// If there is no properties in the object,
// try to save unassigned comments
assign_comments(
started
? PREFIX_AFTER
: 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) {
expect(COMMA)
next()
parse_comments()
if (is(BRACKET_CLOSE)) {
break
}
}
started = true
set_prop(i)
assign_comments(PREFIX_BEFORE)
array[i] = transform(i, walk())
parse_comments(PREFIX_AFTER_VALUE)
i ++
}
next()
last_prop = undefined
assign_comments(
started
? PREFIX_AFTER
: 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)
}
Object.assign(result, comments_host)
}
restore_comments_host()
// reviver
result = transform('', result)
free()
return result
}
module.exports = {
parse,
tokenize,
PREFIX_BEFORE,
PREFIX_BEFORE_ALL,
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
}
+324
View File
@@ -0,0 +1,324 @@
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('./parse')
// 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}`
// 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
: `\\u${(`0000${a.charCodeAt(0).toString(16)}`).slice(- 4)}`
})
}
// 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_content = (inside, value, gap) => {
const comment = process_comments(value, PREFIX_BEFORE, gap + indent, true)
return comment
? inside
// Symbol.for('before') and Symbol.for('before:prop')
// might both exist if user mannually add comments to the object
// and make a mistake.
// We trim to make sure the layout
? comment + inside.trim() + LF + gap
: comment.trimRight() + LF + gap
: inside
? inside.trimRight() + LF + gap
: EMPTY
}
// | 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
// Never use Array.prototype.forEach,
// that we should iterate all items
for (let i = 0; i < length; i ++) {
const before = process_comments(value, BEFORE(i), deeper_gap, true)
if (i !== 0) {
inside += COMMA
}
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)
}
inside += process_comments(value, PREFIX_AFTER, 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
const colon_value_gap = indent
? SPACE
: EMPTY
// From the first element to before close
let inside = EMPTY
let first = true
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
}
if (!first) {
inside += COMMA
}
first = false
const before = process_comments(value, BEFORE(key), deeper_gap, true)
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)
+ colon_value_gap
+ sv
+ process_comments(value, AFTER_VALUE(key), deeper_gap)
}
const keys = isArray(replacer)
? replacer
: Object.keys(value)
keys.forEach(iteratee)
inside += process_comments(value, PREFIX_AFTER, 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
// @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_)
}
// ~~If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.~~
// vanilla `JSON.parse` allow invalid replacer
if (!isFunction(replacer_) && !isArray(replacer_)) {
replacer_ = null
}
replacer = replacer_
indent = indent_
const str = 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
}