Skip to content

Commit

Permalink
Strip comments before parsing JSON config files
Browse files Browse the repository at this point in the history
Also contains minor tweaks to analyzeArgv() and adds comments to the
JSON files under test/fixtures/analyzeArgv. Added a new "Development"
section to README.md including instructions on configuring Vim, Visual
Studio Code, and IntelliJ IDEA to recognize JSON comments.

From the function comment for the new stripJsonComments():

Replaces rather than removes characters so that any JSON.parse() errors
line up with the original. Preserves all existing whitespace as is,
including newlines, carriage returns, and horizontal tabs.

This function is necessary because the `jsdoc` command depends upon the
extremely popular strip-json-comments npm. Otherwise analyzeArgs() would
choke on config.json files containing comments.

This implementation was inspired by strip-json-comments, but is a
completely original implementation to avoid adding any dependencies. It
may become its own separate package one day, likely scoped to avoid
conflicts with strip-json-comments.
  • Loading branch information
mbland committed Dec 30, 2023
1 parent c340770 commit ea0fbec
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 8 deletions.
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,70 @@ $ pnpm jsdoc
../../../build/jsdoc/tomcat-servlet-testing-example-frontend/0.0.0/index.html
```

## Development

Uses [pnpm][] and [Vitest][] for building and testing.

### JSON with comments

You may want to configure your editor to recognize comments in JSON files, since
this project and [JSDoc][] both support them.

#### [Vim][]

Add to your `~/.vimrc`, based on advice from [Stack Overflow: Why does Vim
highlight all my JSON comments in red?][so-vim]:

```vim
" With a little help from:
" - https://stackoverflow.com/questions/55669954/why-does-vim-highlight-all-my-json-comments-in-red
autocmd FileType json syntax match Comment "//.*"
autocmd FileType json syn region jsonBlockComment start="/\*" end="\*/" fold
autocmd FileType json hi def link jsonBlockComment Comment
```

#### [Visual Studio Code][]

[VS Code supports JSON with Comments][vsc-jsonc]. Following the good advice from
[Stack Overflow: In VS Code, disable error "Comments are not permitted in
JSON"][so-vsc]:

##### Method 1, verbatim from <https://stackoverflow.com/a/47834826>

1. Click on the letters JSON in the bottom right corner. (A drop-down will
appear to "Select the Language Mode.")
2. Select "Configure File Association for '.json'..."
3. Type "jsonc" and press Enter.

##### Method 2, nearly verbatim from <https://stackoverflow.com/a/48773989>

Add this to your User Settings:

```json
"files.associations": {
"*.json": "jsonc"
},
```

If you don't already have a user settings file, you can create one. Hit
**&#8984;, or CTRL-,** (that's a comma) to open your settings, then hit
the Open Settings (JSON) button in the upper right. (It looks like a page with a
little curved arrow over it.)

- Or invoke the **[Preferences: Open User Settings (JSON)][vsc-settings]**
command.

#### [IntelliJ IDEA][]

You can effectively enable comments by [extending the JSON5 syntax to all JSON
files][idea-json5]:

1. In the Settings dialog (**&#8984;,** or **CTRL-,**), go to **Editor | File
Types**.
2. In the **Recognized File Types** list, select **JSON5**.
3. In the **File Name Patterns** area, click **&#65291; (Add)** and type `.json`
in the **Add Wildcard** dialog that opens.

## Background

I developed this while experimenting with JSDoc on
Expand All @@ -150,6 +214,15 @@ Node.js, JSDoc, and [npm packaging][] exercise as well.
[pnpm]: https://pnpm.io/
[mbland/tomcat-servlet-testing-example]: https://github.com/mbland/tomcat-servlet-testing-example
[Gradle]: https://gradle.org/
[Vitest]: https://vitest.dev/
[Vim]: https://www.vim.org/
[so-vim]: https://stackoverflow.com/questions/55669954/why-does-vim-highlight-all-my-json-comments-in-red
[Visual Studio Code]: https://code.visualstudio.com/
[vsc-jsonc]: https://code.visualstudio.com/Docs/languages/json#_json-with-comments
[so-vsc]: https://stackoverflow.com/questions/47834825/in-vs-code-disable-error-comments-are-not-permitted-in-json
[vsc-settings]: https://code.visualstudio.com/docs/getstarted/settings#_settingsjson
[IntelliJ IDEA]: https://www.jetbrains.com/idea/
[idea-json5]: https://www.jetbrains.com/help/idea/json.html#ws_json_choose_version_procedure
[Bash]: https://www.gnu.org/software/bash/
[Node.js]: https://nodejs.org/
[npm packaging]: https://docs.npmjs.com/packages-and-modules
86 changes: 80 additions & 6 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export async function getPath(cmdName, env, platform) {

/**
* Analyzes JSDoc CLI args to determine if JSDoc will generate docs and where
*
* Expects any JSON config files specified via -c or --configure to be UTF-8
* encoded.
* @param {string[]} argv - JSDoc command line interface arguments
* @returns {Promise<ArgvResults>} analysis results
*/
Expand All @@ -108,22 +111,20 @@ export async function analyzeArgv(argv) {
for (let i = 0; i !== argv.length; ++i) {
const arg = argv[i]
const nextArg = argv[i+1]
let config = null

switch (arg) {
case '-c':
case '--configure':
if (!cmdLineDest && validArg(nextArg)) {
config = JSON.parse(await readFile(nextArg))
if (config.opts !== undefined) {
destination = config.opts.destination
}
const jsonSrc = await readFile(nextArg, {encoding: 'utf8'})
const config = JSON.parse(stripJsonComments(jsonSrc))
if (config.opts !== undefined) destination = config.opts.destination
}
break

case '-d':
case '--destination':
if (nextArg !== undefined && validArg(nextArg)) {
if (validArg(nextArg)) {
destination = nextArg
cmdLineDest = true
}
Expand All @@ -143,6 +144,79 @@ export async function analyzeArgv(argv) {
return {willGenerate, destination}
}

/**
* Replaces all comments and trailing commas in a JSON string with spaces
*
* Replaces rather than removes characters so that any JSON.parse() errors line
* up with the original. Preserves all existing whitespace as is, including
* newlines, carriage returns, and horizontal tabs.
*
* Details to be aware of:
*
* - Replaces trailing commas before the next ']' or '}' with a space.
* - "/* /" (without the space) is a complete block comment. (Since this
* documentation is in a block comment, the space is necessary here.)
* - If the next character after the end of a block comment ("* /" without the
* space) is:
* - '*': reopens the block comment
* - '/': opens a line comment
*
* If you really want to strip all the extra whitespace out:
*
* ```js
* JSON.stringify(JSON.parse(stripJsonComments(jsonStr)))
* ```
*
* If you want to reformat it to your liking, e.g., using two space indents:
*
* ```js
* JSON.stringify(JSON.parse(stripJsonComments(jsonStr)), null, 2)
* ```
*
* This function is necessary because the `jsdoc` command depends upon the
* extremely popular strip-json-comments npm. Otherwise analyzeArgs() would
* choke on config.json files containing comments.
*
* This implementation was inspired by strip-json-comments, but is a completely
* original implementation to avoid adding any dependencies. It may become its
* own separate package one day, likely scoped to avoid conflicts with
* strip-json-comments.
* @param {string} jsonStr - JSON text to strip
* @returns {string} jsonStr with comments, trailing commas replaced by space
*/
export function stripJsonComments(jsonStr) {
let inString = false
let escaped = false
let inComment = null
let result = ''

for (let i = 0; i !== jsonStr.length; ++i) {
const prevChar = i !== 0 ? jsonStr[i-1] : null
let curChar = jsonStr[i]

if (inString) {
inString = curChar !== '"' || escaped
escaped = curChar === '\\' && !escaped
} else if (inComment) {
if ((inComment === 'line' && curChar === '\n') ||
(inComment === 'block' && prevChar === '*' && curChar === '/')) {
inComment = null
}
if (curChar.trimStart() !== '') curChar = ' '
} else if (curChar === '"') {
inString = true
} else if (prevChar === '/') {
if (curChar === '/') inComment = 'line'
else if (curChar === '*') inComment = 'block'
if (inComment) curChar = ' ' // otherwise prevChar closed a block comment
} else if (curChar === '/') {
curChar = ' ' // opening a line or block comment
}
result += curChar
}
return result.replaceAll(/,(\s*)([\]}])/g, ' $1$2')
}

/**
* Searches for filename within a directory tree via breadth-first search
* @param {string} dirname - current directory to search
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/analyzeArgv/conf-bar.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
{
"opts": {
"destination": "bar"
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/analyzeArgv/conf-foo.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
{
"opts": {
"destination": "foo"
Expand Down
7 changes: 6 additions & 1 deletion test/fixtures/analyzeArgv/conf-undef-dest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
{
"opts": {}
"opts": {} // This will override opts.destination, since opts is defined.
}
7 changes: 6 additions & 1 deletion test/fixtures/analyzeArgv/conf-undef-opts.json
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
{}
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
{/* This won't override opts.destination, since opts is undefined. */}
Loading

0 comments on commit ea0fbec

Please sign in to comment.