comment-json
Advanced tools
+33
-3
@@ -64,3 +64,8 @@ // Original from DefinitelyTyped. Thanks a million | ||
| export interface CommentToken { | ||
| export interface BlankLineToken { | ||
| type: 'BlankLine' | ||
| inline: false | ||
| } | ||
| export interface CommentLineToken { | ||
| type: 'BlockComment' | 'LineComment' | ||
@@ -78,2 +83,4 @@ /** The content of the comment, including whitespaces and line breaks */ | ||
| export type CommentToken = BlankLineToken | CommentLineToken | ||
| export interface CommentLocation { | ||
@@ -97,2 +104,7 @@ /** The start location begins at the `//` or `/*` symbol */ | ||
| export interface ParseOptions { | ||
| no_comments?: boolean | ||
| no_blank_lines?: boolean | ||
| } | ||
| /** | ||
@@ -108,3 +120,3 @@ * Converts a JavaScript Object Notation (JSON) string into an object. | ||
| reviver?: Reviver | null, | ||
| removesComments?: boolean | ||
| removesComments?: boolean | ParseOptions | ||
| ): CommentJSONValue | ||
@@ -125,3 +137,3 @@ | ||
| reviver?: Reviver | null, | ||
| removesComments?: boolean | ||
| removesComments?: boolean | ParseOptions | ||
| ): T | ||
@@ -203,1 +215,19 @@ | ||
| ): void | ||
| /** | ||
| * Remove blank lines from all comment positions recursively. | ||
| * @param target The target object to remove blank lines from | ||
| */ | ||
| export function removeBlankLines( | ||
| target: CommentJSONValue | ||
| ): void | ||
| /** | ||
| * Remove blank lines from a specific location. | ||
| * @param target The target object to remove blank lines from | ||
| * @param location The comment location to remove blank lines from | ||
| */ | ||
| export function removeBlankLines( | ||
| target: CommentJSONValue, | ||
| location: CommentPosition | ||
| ): void |
+1
-1
| { | ||
| "name": "comment-json", | ||
| "version": "4.6.2", | ||
| "version": "5.0.0", | ||
| "description": "Parse and stringify JSON with comments. It will retain comments even after saved!", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
+85
-9
@@ -35,2 +35,3 @@ [](https://github.com/kaelzhang/node-comment-json/actions/workflows/nodejs.yml) | ||
| - [removeComments](#removecommentstarget-object-location-object) | ||
| - [removeBlankLines](#removeblanklinestarget-object-location-object) | ||
| - [CommentArray](#commentarray) | ||
@@ -80,3 +81,4 @@ - [Change Logs](https://github.com/kaelzhang/node-comment-json/releases) | ||
| moveComments, | ||
| removeComments | ||
| removeComments, | ||
| removeBlankLines | ||
| } = require('comment-json') | ||
@@ -129,3 +131,10 @@ const fs = require('fs') | ||
| ```ts | ||
| parse(text, reviver? = null, remove_comments? = false) | ||
| parse( | ||
| text, | ||
| reviver? = null, | ||
| options? = false | { | ||
| no_comments?: boolean, | ||
| no_blank_lines?: boolean | ||
| } | ||
| ) | ||
| : object | string | number | boolean | null | ||
@@ -137,3 +146,6 @@ ``` | ||
| - `comment-json` also passes the 3rd parameter `context` to the function `reviver`, as described in https://github.com/tc39/proposal-json-parse-with-source, which will be useful to parse a JSON string with `BigInt` values. | ||
| - **remove_comments?** `boolean = false` If true, the comments won't be maintained, which is often used when we want to get a clean object. | ||
| - **options?** `boolean | object = false` | ||
| - passing `true` is the backward-compatible shorthand of `{ no_comments: true }` | ||
| - **options.no_comments?** `boolean = false` If true, `LineComment` and `BlockComment` tokens won't be maintained. | ||
| - **options.no_blank_lines?** `boolean = false` If true, `BlankLine` tokens won't be generated. | ||
@@ -288,3 +300,10 @@ Returns `CommentJSONValue` (`object | string | number | boolean | null`) corresponding to the given JSON text. | ||
| ```ts | ||
| interface CommentToken { | ||
| type CommentToken = BlankLineToken | CommentLineToken | ||
| interface BlankLineToken { | ||
| type: 'BlankLine' | ||
| inline: false | ||
| } | ||
| interface CommentLineToken { | ||
| type: 'BlockComment' | 'LineComment' | ||
@@ -297,4 +316,5 @@ // The content of the comment, including whitespaces and line breaks | ||
| // But pay attention that, | ||
| // locations will NOT be maintained when stringified | ||
| // `loc` is kept for real comments only. | ||
| // It will NOT be maintained when stringified, and blank lines are rendered | ||
| // from explicit `BlankLine` tokens instead of inferred from `loc`. | ||
| loc: CommentLocation | ||
@@ -316,2 +336,5 @@ } | ||
| Each physical empty line is represented by an explicit `BlankLine` token, so | ||
| `stringify()` no longer infers empty lines from `loc`. | ||
| ### Query comments in TypeScript | ||
@@ -337,6 +360,9 @@ | ||
| ### Parse into an object without comments | ||
| ### Parse into an object without comments and/or blank lines | ||
| ```js | ||
| console.log(parse(content, null, true)) | ||
| console.log(parse(content, null, { | ||
| no_comments: true, | ||
| no_blank_lines: true | ||
| })) | ||
| ``` | ||
@@ -368,3 +394,3 @@ | ||
| If we parse a JSON of primative type with `remove_comments:false`, then the return value of `parse()` will be of object type. | ||
| If we parse a JSON of primative type with `no_comments:false`, then the return value of `parse()` will be of object type. | ||
@@ -679,2 +705,52 @@ The value of `parsed` is equivalent to: | ||
| ## removeBlankLines(target: object, location?: object) | ||
| - **target** `object` The target object to remove blank lines from. | ||
| - **location?** `object` Optional specific comment location to clean. | ||
| - **location.where** `CommentPrefix` The comment position (e.g., 'before', 'after', 'before-all', etc.). | ||
| - **location.key?** `string` The property key for property-specific comments. Omit for non-property comments. | ||
| This method removes only `BlankLine` tokens. Real comments stay in place. | ||
| ### Remove blank lines recursively | ||
| ```js | ||
| const {parse, stringify, removeBlankLines} = require('comment-json') | ||
| const obj = parse(`{ | ||
| // before foo | ||
| "foo": 1, | ||
| "bar": 2 | ||
| }`) | ||
| removeBlankLines(obj) | ||
| console.log(stringify(obj, null, 2)) | ||
| // { | ||
| // // before foo | ||
| // "foo": 1, | ||
| // "bar": 2 | ||
| // } | ||
| ``` | ||
| ### Remove blank lines from one location | ||
| ```js | ||
| const obj = parse(`{ | ||
| // before foo | ||
| "foo": 1 | ||
| }`) | ||
| removeBlankLines(obj, { where: 'before', key: 'foo' }) | ||
| console.log(stringify(obj, null, 2)) | ||
| // { | ||
| // // before foo | ||
| // "foo": 1 | ||
| // } | ||
| ``` | ||
| ## `CommentArray` | ||
@@ -681,0 +757,0 @@ |
+89
-18
@@ -38,4 +38,2 @@ const PREFIX_BEFORE = 'before' | ||
| const LINE_BREAKS_BEFORE = new WeakMap() | ||
| const LINE_BREAKS_AFTER = new WeakMap() | ||
| const RAW_STRING_LITERALS = new WeakMap() | ||
@@ -192,2 +190,59 @@ | ||
| const is_comment_symbol = subject => { | ||
| const key = Symbol.keyFor(subject) | ||
| return is_string(key) | ||
| && ( | ||
| NON_PROP_SYMBOL_PREFIXES.includes(key) | ||
| || PROP_SYMBOL_PREFIXES.some(prefix => key.indexOf(prefix + COLON) === 0) | ||
| ) | ||
| } | ||
| const remove_blank_line_tokens = comments => { | ||
| let write = 0 | ||
| let removed = false | ||
| comments.forEach(comment => { | ||
| if (comment && comment.type === 'BlankLine') { | ||
| removed = true | ||
| return | ||
| } | ||
| comments[write ++] = comment | ||
| }) | ||
| comments.length = write | ||
| return removed | ||
| } | ||
| const remove_blank_lines_from_prop = (target, prop) => { | ||
| if (!Object.hasOwn(target, prop)) { | ||
| return | ||
| } | ||
| const comments = target[prop] | ||
| if (!Array.isArray(comments) || !remove_blank_line_tokens(comments)) { | ||
| return | ||
| } | ||
| if (!comments.length) { | ||
| delete target[prop] | ||
| } | ||
| } | ||
| const remove_blank_lines_deep = target => { | ||
| Object.getOwnPropertySymbols(target).forEach(prop => { | ||
| if (is_comment_symbol(prop)) { | ||
| remove_blank_lines_from_prop(target, prop) | ||
| } | ||
| }) | ||
| Object.keys(target).forEach(key => { | ||
| const value = target[key] | ||
| if (is_object(value)) { | ||
| remove_blank_lines_deep(value) | ||
| } | ||
| }) | ||
| } | ||
| const is_raw_json = typeof JSON.isRawJSON === 'function' | ||
@@ -200,15 +255,2 @@ // For backward compatibility, | ||
| const set_comment_line_breaks = (comment, before, after) => { | ||
| if (is_number(before) && before >= 0) { | ||
| LINE_BREAKS_BEFORE.set(comment, before) | ||
| } | ||
| if (is_number(after) && after >= 0) { | ||
| LINE_BREAKS_AFTER.set(comment, after) | ||
| } | ||
| } | ||
| const get_comment_line_breaks_before = comment => LINE_BREAKS_BEFORE.get(comment) | ||
| const get_comment_line_breaks_after = comment => LINE_BREAKS_AFTER.get(comment) | ||
| module.exports = { | ||
@@ -251,5 +293,2 @@ PROP_SYMBOL_PREFIXES, | ||
| get_raw_string_literal, | ||
| set_comment_line_breaks, | ||
| get_comment_line_breaks_before, | ||
| get_comment_line_breaks_after, | ||
@@ -433,3 +472,35 @@ /** | ||
| delete target[prop] | ||
| }, | ||
| /** | ||
| * Remove blank lines from a specific location or recursively from an object. | ||
| * | ||
| * @param {Object} target The target object to remove blank lines from. | ||
| * @param {Object} [location] The comment location to remove blank lines from. | ||
| * @param {string} location.where The comment position (e.g., 'before', | ||
| * 'after', 'before-all', etc.). | ||
| * @param {string} [location.key] The property key for property-specific | ||
| * comments. Omit for non-property comments. | ||
| * | ||
| * @throws {TypeError} If target is not an object. | ||
| * @throws {RangeError} If where parameter is invalid or incompatible with key. | ||
| */ | ||
| removeBlankLines (target, location) { | ||
| if (!is_object(target)) { | ||
| throw new TypeError('target must be an object') | ||
| } | ||
| if (location === UNDEFINED) { | ||
| remove_blank_lines_deep(target) | ||
| return | ||
| } | ||
| const { | ||
| where, | ||
| key | ||
| } = location | ||
| const prop = symbol_checked(where, key) | ||
| remove_blank_lines_from_prop(target, prop) | ||
| } | ||
| } |
+4
-2
@@ -16,3 +16,4 @@ const {parse, tokenize} = require('./parse') | ||
| moveComments, | ||
| removeComments | ||
| removeComments, | ||
| removeBlankLines | ||
| } = require('./common') | ||
@@ -37,3 +38,4 @@ | ||
| moveComments, | ||
| removeComments | ||
| removeComments, | ||
| removeBlankLines | ||
| } |
+67
-28
@@ -34,3 +34,2 @@ // JSON formatting | ||
| set_raw_string_literal, | ||
| set_comment_line_breaks, | ||
| assign_non_prop_comments | ||
@@ -64,2 +63,3 @@ } = require('./common') | ||
| let remove_comments = false | ||
| let remove_blank_lines = false | ||
| let inline = false | ||
@@ -71,2 +71,3 @@ let tokens = null | ||
| let reviver = null | ||
| let source_line_count = 0 | ||
@@ -80,2 +81,3 @@ const clean = () => { | ||
| last_prop = UNDEFINED | ||
| remove_blank_lines = false | ||
| } | ||
@@ -96,2 +98,3 @@ | ||
| current_code = UNDEFINED | ||
| source_line_count = 0 | ||
| } | ||
@@ -172,2 +175,39 @@ | ||
| const create_blank_line = () => ({ | ||
| type: 'BlankLine', | ||
| inline: false | ||
| }) | ||
| const append_blank_lines = (comments, from_line, to_line) => { | ||
| if (remove_blank_lines) { | ||
| return | ||
| } | ||
| let blank_lines = Math.max(0, to_line - from_line - 1) | ||
| while (blank_lines -- > 0) { | ||
| comments.push(create_blank_line()) | ||
| } | ||
| } | ||
| const normalize_parse_options = options => { | ||
| if (typeof options === 'boolean') { | ||
| return { | ||
| no_comments: options, | ||
| no_blank_lines: false | ||
| } | ||
| } | ||
| if (!is_object(options)) { | ||
| return { | ||
| no_comments: false, | ||
| no_blank_lines: false | ||
| } | ||
| } | ||
| return { | ||
| no_comments: !!options.no_comments, | ||
| no_blank_lines: !!options.no_blank_lines | ||
| } | ||
| } | ||
| const assign_after_comments = () => { | ||
@@ -217,2 +257,5 @@ if (!unassigned_comments) { | ||
| const comments = [] | ||
| let previous_line = last | ||
| ? last.loc.end.line | ||
| : 0 | ||
@@ -226,2 +269,4 @@ while ( | ||
| ) { | ||
| append_blank_lines(comments, previous_line, current.loc.start.line) | ||
| const comment = { | ||
@@ -231,14 +276,4 @@ ...current, | ||
| } | ||
| const previous_line = last | ||
| ? last.loc.end.line | ||
| : 1 | ||
| set_comment_line_breaks( | ||
| comment, | ||
| Math.max(0, comment.loc.start.line - previous_line) | ||
| ) | ||
| // delete comment.loc | ||
| comments.push(comment) | ||
| previous_line = comment.loc.end.line | ||
@@ -248,22 +283,19 @@ next() | ||
| const {length} = comments | ||
| if (length) { | ||
| const comment = comments[length - 1] | ||
| const current_line = current | ||
| append_blank_lines( | ||
| comments, | ||
| previous_line, | ||
| current | ||
| ? current.loc.start.line | ||
| : comment.loc.end.line | ||
| : source_line_count | ||
| ) | ||
| set_comment_line_breaks( | ||
| comment, | ||
| undefined, | ||
| Math.max(0, current_line - comment.loc.end.line) | ||
| ) | ||
| } | ||
| if (remove_comments) { | ||
| return | ||
| for (let i = comments.length - 1; i >= 0; i --) { | ||
| if (comments[i].type !== 'BlankLine') { | ||
| comments.splice(i, 1) | ||
| } | ||
| } | ||
| } | ||
| if (!length) { | ||
| if (!comments.length) { | ||
| return | ||
@@ -497,10 +529,17 @@ } | ||
| */ | ||
| const parse = (code, rev, no_comments) => { | ||
| const parse = (code, rev, options) => { | ||
| // Clean variables in closure | ||
| clean() | ||
| const { | ||
| no_comments, | ||
| no_blank_lines | ||
| } = normalize_parse_options(options) | ||
| current_code = code | ||
| source_line_count = code.split('\n').length | ||
| tokens = tokenize(code) | ||
| reviver = rev | ||
| remove_comments = no_comments | ||
| remove_blank_lines = no_blank_lines | ||
@@ -507,0 +546,0 @@ if (!tokens.length) { |
+42
-59
@@ -25,5 +25,2 @@ const { | ||
| get_raw_string_literal, | ||
| get_comment_line_breaks_before, | ||
| get_comment_line_breaks_after, | ||
| is_raw_json | ||
@@ -49,40 +46,17 @@ } = require('./common') | ||
| const repeat_line_breaks = (line_breaks, gap) => (LF + gap).repeat(line_breaks) | ||
| const read_line_breaks = line_breaks => is_number(line_breaks) && line_breaks >= 0 | ||
| ? line_breaks | ||
| : null | ||
| const read_line_breaks_from_loc = (previous_comment, comment) => { | ||
| if ( | ||
| !previous_comment | ||
| || !previous_comment.loc | ||
| || !comment.loc | ||
| ) { | ||
| return null | ||
| } | ||
| const is_inline_whitespace = char => char === SPACE || char === '\t' | ||
| const count_trailing_line_breaks = str => { | ||
| let i = str.length | ||
| let count = 0 | ||
| const {end} = previous_comment.loc | ||
| const {start} = comment.loc | ||
| while (i > 0) { | ||
| while (i > 0 && is_inline_whitespace(str[i - 1])) { | ||
| i -- | ||
| } | ||
| if ( | ||
| !end | ||
| || !start | ||
| || !is_number(end.line) | ||
| || !is_number(start.line) | ||
| ) { | ||
| return null | ||
| } | ||
| if (i === 0 || str[i - 1] !== LF) { | ||
| return count | ||
| } | ||
| const line_breaks = start.line - end.line | ||
| return line_breaks >= 0 | ||
| ? line_breaks | ||
| : null | ||
| } | ||
| const count_trailing_line_breaks = (str, gap) => { | ||
| const unit = LF + gap | ||
| const {length} = unit | ||
| let i = str.length | ||
| let count = 0 | ||
| while (i >= length && str.slice(i - length, i) === unit) { | ||
| i -= length | ||
| i -- | ||
| count ++ | ||
@@ -103,5 +77,11 @@ } | ||
| let str = EMPTY | ||
| let blank_lines = 0 | ||
| let last_comment = null | ||
| comments.forEach((comment, i) => { | ||
| if (comment.type === 'BlankLine') { | ||
| blank_lines ++ | ||
| return | ||
| } | ||
| const { | ||
@@ -113,16 +93,6 @@ inline, | ||
| let line_breaks_before = read_line_breaks( | ||
| get_comment_line_breaks_before(comment) | ||
| ) | ||
| const line_breaks_before = blank_lines > 0 | ||
| ? blank_lines + (inline ? 0 : 1) | ||
| : null | ||
| if (line_breaks_before === null) { | ||
| line_breaks_before = read_line_breaks_from_loc(last_comment, comment) | ||
| } | ||
| if (line_breaks_before === null) { | ||
| line_breaks_before = inline | ||
| ? 0 | ||
| : 1 | ||
| } | ||
| const delimiter = line_breaks_before > 0 | ||
@@ -133,3 +103,3 @@ ? repeat_line_breaks(line_breaks_before, deeper_gap) | ||
| // The first comment at the very beginning of the source. | ||
| : i === 0 | ||
| : i === 0 && symbol_tag === PREFIX_BEFORE_ALL && deeper_gap === EMPTY | ||
| ? EMPTY | ||
@@ -142,5 +112,12 @@ : LF + deeper_gap | ||
| blank_lines = 0 | ||
| last_comment = comment | ||
| }) | ||
| if (!last_comment) { | ||
| return blank_lines | ||
| ? repeat_line_breaks(blank_lines + 1, deeper_gap) | ||
| : EMPTY | ||
| } | ||
| const default_line_breaks_after = display_block | ||
@@ -152,6 +129,5 @@ // line comment should always end with a LF | ||
| const line_breaks_after = Math.max( | ||
| default_line_breaks_after, | ||
| read_line_breaks(get_comment_line_breaks_after(last_comment)) || 0 | ||
| ) | ||
| const line_breaks_after = blank_lines > 0 | ||
| ? blank_lines + 1 | ||
| : default_line_breaks_after | ||
@@ -178,3 +154,3 @@ return str + repeat_line_breaks(line_breaks_after, deeper_gap) | ||
| : one.trimRight() + repeat_line_breaks( | ||
| Math.max(1, count_trailing_line_breaks(one, gap)), | ||
| Math.max(1, count_trailing_line_breaks(one)), | ||
| gap | ||
@@ -184,3 +160,3 @@ ) | ||
| ? two.trimRight() + repeat_line_breaks( | ||
| Math.max(1, count_trailing_line_breaks(two, gap)), | ||
| Math.max(1, count_trailing_line_breaks(two)), | ||
| gap | ||
@@ -412,2 +388,7 @@ ) | ||
| const normalize_blank_lines = str => | ||
| typeof str === 'string' | ||
| ? str.replace(/\n[ \t]+(?=\n)/g, '\n') | ||
| : str | ||
| /** | ||
@@ -469,3 +450,3 @@ * Converts a JavaScript value to a JavaScript Object Notation (JSON) string | ||
| return is_object(value) | ||
| const output = is_object(value) | ||
| ? process_comments(value, PREFIX_BEFORE_ALL, EMPTY, true).trimLeft() | ||
@@ -475,2 +456,4 @@ + str | ||
| : str | ||
| return normalize_blank_lines(output) | ||
| } |
72700
6.94%1733
6.52%882
9.43%