Repository: beenotung/best-effort-json-parser
Branch: master
Commit: 7de73f1e06f7
Files: 17
Total size: 44.7 KB
Directory structure:
gitextract_ujoa3yj2/
├── .editorconfig
├── .gitignore
├── .idea/
│ ├── .gitignore
│ ├── best-effort-json-parser.iml
│ └── modules.xml
├── .nycrc.json
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── example/
│ ├── index.ts
│ └── package.json
├── package.json
├── src/
│ ├── parse.spec.ts
│ └── parse.ts
├── tsconfig.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.xml]
indent_style = space
indent_size = 4
[*.json]
indent_style = space
indent_size = 2
================================================
FILE: .gitignore
================================================
node_modules/
package-lock.json
pnpm-lock.yaml
pnpm-debug.log
dist/
.cache/
*.tgz
.env
.nyc_output
coverage/
================================================
FILE: .idea/.gitignore
================================================
*
!modules.xml
!*.iml
!dictionaries/
!.gitignore
================================================
FILE: .idea/best-effort-json-parser.iml
================================================
================================================
FILE: .idea/modules.xml
================================================
================================================
FILE: .nycrc.json
================================================
{
"include": "src/**/*.ts",
"exclude": "src/**/*.{test,spec}.ts"
}
================================================
FILE: .prettierignore
================================================
*.macro.ts
src/assets/*
src/components.d.ts
src/polyfill-array.ts
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true,
"jsxSingleQuote": false,
"quoteProps": "consistent",
"overrides": [
{
"files": [
"*.scss",
"*.css"
],
"options": {
"singleQuote": false
}
}
],
"semi": false,
"arrowParens": "avoid",
"trailingComma": "all"
}
================================================
FILE: LICENSE
================================================
BSD 2-Clause License
Copyright (c) [2020], [Beeno Tung (Tung Cheung Leong)]
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
# best-effort-json-parser
Parse incomplete JSON text in best-effort manner with support for comments. Useful for partial JSON responses, broken network packages, or LLM responses exceeding tokens. It can also read configuration files with comments.
[](https://www.npmjs.com/package/best-effort-json-parser)
[](https://bundlephobia.com/package/best-effort-json-parser)
[](https://bundlephobia.com/package/best-effort-json-parser)
[](https://www.npmtrends.com/best-effort-json-parser)
## Features
- Typescript support
- Isomorphic package: works in Node.js and browsers
- Comment support: `// inline`, `/* multi-line */`, and `` comments
## Installation
```bash
npm install best-effort-json-parser
```
You can also install `best-effort-json-parser` with [pnpm](https://pnpm.io/), [yarn](https://yarnpkg.com/), or [slnpm](https://github.com/beenotung/slnpm)
## Usage Example
### Parsing incomplete JSON text
If the string, object, or array is not complete, the parser will return the partial data.
```typescript
import { parse } from 'best-effort-json-parser'
let data = parse(`[1, 2, {"a": "apple`)
console.log(data) // [1, 2, { a: 'apple' }]
```
### Parsing json with comments
Multiple types of comments are supported:
```typescript
import { parse } from 'best-effort-json-parser'
let config = parse(`{
"database": {
"host": "localhost", // database server
"port": 5432, /* default port */
"ssl": true
},
"features": ["auth", "api"]
}`)
```
Comments inside strings are preserved and not treated as comments:
```typescript
let data = parse(`{
"inline_comment": "// this is not a comment",
"block_comment": "/* neither is this */",
"html_comment": \`\`,
"value": 42
}`)
```
**Note:** The parser also supports template literals with backticks (\`) for strings, in addition to single and double quotes.
## Error Logging
By default, the parser logs errors to `console.error`. You can control error logging behavior:
```typescript
import {
disableErrorLogging,
enableErrorLogging,
setErrorLogger,
} from 'best-effort-json-parser'
// Disable error logging completely
disableErrorLogging()
// Re-enable error logging (default behavior)
enableErrorLogging()
// Set a custom error logger
setErrorLogger((message, data) => {
// Your custom logging logic here
console.log('Custom error:', message, data)
// Common destinations for error data:
// - Database storage for analysis
// - File system logging
// - Third-party services (Sentry, LogRocket, etc.)
// - Monitoring and alerting systems
})
```
## Typescript Signature
```typescript
// Main parse function
function parse(s: string | undefined | null): any
// Parse namespace with additional properties
namespace parse {
lastParseReminding: string | undefined
onExtraToken: (text: string, data: any, reminding: string) => void | undefined
}
// Error logging functions
function setErrorLogger(logger: (message: string, data?: any) => void): void
function disableErrorLogging(): void
function enableErrorLogging(): void
```
More examples see [parse.spec.ts](./src/parse.spec.ts)
## License
This is free and open-source software (FOSS) with
[BSD-2-Clause License](./LICENSE)
================================================
FILE: example/index.ts
================================================
import { parse } from 'best-effort-json-parser'
// Incomplete string, object, array
let data = parse(`[1,2,{"a":"apple`)
console.log(data) // [1, 2, { a: 'apple' }]
// Example with comments
let dataWithComments = parse(`{
"users": [
{
"name": "Alice", // admin user
"role": "admin"
},
{
"name": "Bob", /* regular user
with multi-line comment */
"role": "user"
}
],
"version": 1.2.3
}`)
console.log(dataWithComments)
/*
{
users: [
{ name: 'Alice', role: 'admin' },
{ name: 'Bob', role: 'user' },
],
version: '1.2.3'
}
*/
================================================
FILE: example/package.json
================================================
{
"name": "example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "ts-node index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"best-effort-json-parser": "file:.."
},
"devDependencies": {
"ts-node": "^9.1.1",
"typescript": "^5.9.3"
}
}
================================================
FILE: package.json
================================================
{
"name": "best-effort-json-parser",
"version": "1.4.0",
"description": "Parse incomplete json text in best-effort manner",
"keywords": [
"json",
"parser",
"auto-fix",
"auto-repair",
"best-effort"
],
"author": "Beeno Tung (https://beeno-tung.surge.sh)",
"license": "BSD-2-Clause",
"main": "dist/parse.js",
"types": "dist/parse.d.ts",
"scripts": {
"test": "run-s format tsc mocha",
"clean": "rimraf dist",
"format": "prettier --write \"src/**/*.ts\"",
"postformat": "tslint -p . --fix && format-json-cli",
"build": "run-s clean tsc",
"tsc": "tsc -p .",
"mocha": "ts-mocha \"src/**/*.spec.ts\"",
"coverage": "nyc npm run mocha -- --reporter=progress",
"report:update": "nyc --reporter=lcov npm run mocha -- --reporter=progress",
"report:open": "open-cli coverage/lcov-report/index.html",
"report": "run-s report:update report:open",
"prepublishOnly": "run-s test build"
},
"directories": {
"example": "example",
"test": "test"
},
"files": [
"dist",
"src"
],
"devDependencies": {
"@types/chai": "^4.2.14",
"@types/mocha": "^8.2.0",
"@types/node": "*",
"@types/sinon": "^9.0.9",
"chai": "^4.2.0",
"format-json-cli": "^1.0.1",
"mocha": "^8.2.1",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"open-cli": "^6.0.1",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"sinon": "^9.2.2",
"ts-mocha": "^8.0.0",
"ts-node": "^9.1.1",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"tslint-eslint-rules": "^5.4.0",
"tslint-etc": "^1.13.9",
"typescript": "^4.8.3"
},
"repository": {
"type": "git",
"url": "git+https://github.com/beenotung/best-effort-json-parser.git"
},
"bugs": {
"url": "https://github.com/beenotung/best-effort-json-parser/issues"
},
"homepage": "https://github.com/beenotung/best-effort-json-parser#readme"
}
================================================
FILE: src/parse.spec.ts
================================================
import sinon from 'sinon'
import { expect } from 'chai'
import { parse, setErrorLogger } from './parse'
let onExtraTokenSpy: sinon.SinonSpy
let originalOnExtraToken: typeof parse.onExtraToken
let muteLog = true
beforeEach(() => {
if (muteLog) {
onExtraTokenSpy = sinon.fake()
originalOnExtraToken = parse.onExtraToken
parse.onExtraToken = onExtraTokenSpy
} else {
onExtraTokenSpy = sinon.spy(parse, 'onExtraToken')
}
})
afterEach(() => {
if (muteLog) {
parse.onExtraToken = originalOnExtraToken
} else {
sinon.restore()
}
})
describe('parser TestSuit', function () {
context('number', () => {
it('should parse positive integer', function () {
expect(parse(`42`)).equals(42)
})
it('should parse negative integer', function () {
expect(parse(`-42`)).equals(-42)
})
it('should parse positive float', function () {
expect(parse(`12.34`)).equals(12.34)
})
it('should parse negative float', function () {
expect(parse(`-12.34`)).equals(-12.34)
})
it('should parse incomplete positive float', function () {
expect(parse(`12.`)).equals(12)
})
it('should parse incomplete negative float', function () {
expect(parse(`-12.`)).equals(-12)
})
it('should parse incomplete negative integer', function () {
expect(parse(`-`)).equals(-0)
})
it('should preserve invalid number', function () {
expect(parse(`1.2.3.4`)).equals('1.2.3.4')
})
})
context('string', () => {
it('should parse string', function () {
expect(parse(`"I am text"`)).equals('I am text')
expect(parse(`"I'm text"`)).equals("I'm text")
expect(parse(`"I\\"m text"`)).equals('I"m text')
})
it('should parse incomplete string', function () {
expect(parse(`"I am text`)).equals('I am text')
expect(parse(`"I'm text`)).equals("I'm text")
expect(parse(`"I\\"m text`)).equals('I"m text')
})
})
context('boolean', () => {
it('should parse boolean', function () {
expect(parse(`true`)).equals(true)
expect(parse(`false`)).equals(false)
})
function testIncomplete(str: string, val: boolean) {
for (let i = str.length; i >= 1; i--) {
expect(parse(str.slice(0, i))).equals(val)
}
}
it('should parse incomplete true', function () {
testIncomplete(`true`, true)
})
it('should parse incomplete false', function () {
testIncomplete(`false`, false)
})
})
context('array', () => {
it('should parse empty array', function () {
expect(parse(`[]`)).deep.equals([])
})
it('should parse number array', function () {
expect(parse(`[1,2,3]`)).deep.equals([1, 2, 3])
})
it('should parse incomplete array', function () {
expect(parse(`[1,2,3`)).deep.equals([1, 2, 3])
expect(parse(`[1,2,`)).deep.equals([1, 2])
expect(parse(`[1,2`)).deep.equals([1, 2])
expect(parse(`[1,`)).deep.equals([1])
expect(parse(`[1`)).deep.equals([1])
expect(parse(`[`)).deep.equals([])
})
})
context('object', () => {
it('should parse simple object', function () {
let o = { a: 'apple', b: 'banana' }
expect(parse(JSON.stringify(o))).deep.equals(o)
expect(parse(JSON.stringify(o, null, 2))).deep.equals(o)
expect(parse(`{"a":"apple","b":"banana"}`)).deep.equals({
a: 'apple',
b: 'banana',
})
expect(parse(`{"a": "apple","b": "banana"}`)).deep.equals({
a: 'apple',
b: 'banana',
})
expect(parse(`{"a": "apple", "b": "banana"}`)).deep.equals({
a: 'apple',
b: 'banana',
})
expect(parse(`{"a" : "apple", "b" : "banana"}`)).deep.equals({
a: 'apple',
b: 'banana',
})
expect(parse(`{ "a" : "apple", "b" : "banana" }`)).deep.equals({
a: 'apple',
b: 'banana',
})
expect(parse(`{ "a" : "apple" , "b" : "banana" }`)).deep.equals({
a: 'apple',
b: 'banana',
})
})
it('should parse incomplete simple object', function () {
expect(parse(`{"a":"apple","b":"banana"`)).deep.equals({
a: 'apple',
b: 'banana',
})
expect(parse(`{"a":"apple","b":"banana`)).deep.equals({
a: 'apple',
b: 'banana',
})
expect(parse(`{"a":"apple","b":"b`)).deep.equals({ a: 'apple', b: 'b' })
expect(parse(`{"a":"apple","b":"`)).deep.equals({ a: 'apple', b: '' })
expect(parse(`{"a":"apple","b":`)).deep.equals({
a: 'apple',
b: undefined,
})
expect(parse(`{"a":"apple","b"`)).deep.equals({
a: 'apple',
b: undefined,
})
expect(parse(`{"a":"apple","b`)).deep.equals({ a: 'apple', b: undefined })
expect(parse(`{"a":"apple","`)).deep.equals({
'a': 'apple',
'': undefined,
})
expect(parse(`{"a":"apple",`)).deep.equals({ a: 'apple' })
expect(parse(`{"a":"apple"`)).deep.equals({ a: 'apple' })
expect(parse(`{"a":"apple`)).deep.equals({ a: 'apple' })
expect(parse(`{"a":"a`)).deep.equals({ a: 'a' })
expect(parse(`{"a":"`)).deep.equals({ a: '' })
expect(parse(`{"a":`)).deep.equals({ a: undefined })
expect(parse(`{"a"`)).deep.equals({ a: undefined })
expect(parse(`{"a`)).deep.equals({ a: undefined })
expect(parse(`{"`)).deep.equals({ '': undefined })
expect(parse(`{`)).deep.equals({})
})
})
context('complex object', () => {
it('should parse complete complex object', function () {
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34
}
}
}`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
},
},
})
})
it('should parse incomplete complex object', function () {
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34
}
}`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
},
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34
}`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
},
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
},
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12,
},
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float":`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: undefined,
},
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
},
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {},
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj":`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: undefined,
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "flo`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, flo: undefined }],
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], {`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], {}],
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 1`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 1]],
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float": 12.34,
"arr": [`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: 12.34,
arr: [],
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "float": 12.34 }],
"obj": {
"int": 42,
"float"`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],
obj: {
int: 42,
float: undefined,
},
})
expect(
parse(`{
"int": 42,
"float": 12.34,
"arr": [42, 12.34, [42, 12.34], { "int": 42, "flo`),
).deep.equals({
int: 42,
float: 12.34,
arr: [42, 12.34, [42, 12.34], { int: 42, flo: undefined }],
})
})
})
context('invalid inputs', () => {
it('should throw error on invalid (not incomplete) json text', function () {
// spy the error logger
let spy = sinon.fake()
setErrorLogger(spy)
expect(() => parse(`:atom`)).to.throws()
expect(spy.called).be.true
expect(spy.firstCall.firstArg).is.string('no parser registered for ":"')
// restore the error logger
setErrorLogger(console.error)
})
it('should complaint on extra tokens', function () {
expect(parse(`[1] 2`)).deep.equals([1])
expect(onExtraTokenSpy.called).be.true
expect(parse.lastParseReminding).equals(' 2')
})
})
context('extra space', () => {
it('should parse complete json with extra space', function () {
expect(parse(` [1] `)).deep.equals([1])
})
it('should parse incomplete json with extra space', function () {
expect(parse(` [1 `)).deep.equals([1])
})
})
context('invalid but understandable json', () => {
context('string newline', () => {
it('should parse escaped newline', function () {
expect(parse(`"line1\\nline2"`)).equals('line1\nline2')
expect(
parse(/* javascript */ `
{
"essay": "global health.\n\nDuring my tenure ..."
}
`),
).deep.equals({
essay: 'global health.\n\nDuring my tenure ...',
})
})
it('should parse non-escaped newline', function () {
expect(parse(`"line1\nline2"`)).equals('line1\nline2')
})
it('should parse non-escaped newline inside string value', function () {
expect(parse(`{"key":"line1\\nline2`)).deep.equals({
key: 'line1\nline2',
})
expect(parse(`{"key":"line1\nline2`)).deep.equals({
key: 'line1\nline2',
})
expect(parse(`{"key":"line1\n`)).deep.equals({ key: 'line1\n' })
expect(parse(`{\n\t"key":"line1\n`)).deep.equals({ key: 'line1\n' })
expect(parse(`{\n\t"key":"line1\nline2"\n}`)).deep.equals({
key: 'line1\nline2',
})
})
})
context('string non-escaped characters', function () {
it('should parse \\t', function () {
expect(parse(`"text\t"`)).equals(`text\t`)
expect(parse(`"text\\t"`)).equals(`text\t`)
})
it('should parse \\r', function () {
expect(parse(`"text\r"`)).equals(`text\r`)
expect(parse(`"text\\r"`)).equals(`text\r`)
})
})
context('string quote', () => {
it('should parse string with double quote', function () {
expect(parse(`"str"`)).equals('str')
})
it('should parse string with single quote', function () {
expect(parse(`'str'`)).equals('str')
})
it('should parse string without double quote', function () {
expect(parse(`str`)).equals('str')
})
it('should parse array of string without double quote', function () {
expect(parse(`[a,b,c]`)).deep.equals(['a', 'b', 'c'])
})
it('should parse string with backticks', function () {
expect(parse(`\`"Alice's"\``)).equals(`"Alice's"`)
expect(
parse(`[
\`double quote: "\`,
\`single quote: '\`,
${'`backtick: \\``'}
]`),
).deep.equals([`double quote: "`, `single quote: '`, 'backtick: `'])
})
})
context('object key', () => {
it('should parse object key with double quote', function () {
expect(parse(`{ "int" : 42 }`)).deep.equals({ int: 42 })
})
it('should parse object key with single quote', function () {
expect(parse(`{ 'int' : 42 }`)).deep.equals({ int: 42 })
})
it('should parse object key without double quote', function () {
expect(parse(`{ int : 42 }`)).deep.equals({ int: 42 })
})
})
})
context('falsy values', () => {
it('should parse empty string', function () {
expect(parse('')).equals('')
})
it('should parse undefined', function () {
expect(parse(undefined)).to.be.undefined
})
it('should parse null', function () {
expect(parse(null)).to.be.null
})
})
context('incomplete escaped characters', function () {
it('should ignore an incomplete escaped character (such as control character \\n)', function () {
expect(parse(`"the newline\n`)).equals('the newline\n')
expect(parse(`"the newline\\n`)).equals('the newline\n')
expect(parse(`"the newline\\`)).equals('the newline')
expect(parse(`"the newline\n\\`)).equals('the newline\n')
expect(parse(`"the newline\\n\\`)).equals('the newline\n')
expect(parse(`"the newline\\\\`)).equals('the newline\\')
})
it('should ignore incomplete escape character in object value', function () {
expect(parse('{"a":"\n"')).deep.equals({ a: '\n' })
expect(parse('{"a":"\n')).deep.equals({ a: '\n' })
expect(parse('{"a":"\\n"')).deep.equals({ a: '\n' })
expect(parse('{"a":"\\')).deep.equals({ a: '' })
})
})
context('comment in json', () => {
it('should ignore inline comment', function () {
// test with //
let text = `{
"a": 1, // comment
"b": 2
}`
expect(parse(text)).deep.equals({ a: 1, b: 2 })
})
it('should ignore multi-line comment', function () {
// test with /* */
let text = `{
"a": 1, /* line 1
line 2
line 3 */
"b": 2
}`
expect(parse(text)).deep.equals({ a: 1, b: 2 })
})
it('should not strip comments inside strings', function () {
let text = `{
"comment": "// this is not a comment",
"block": "/* neither is this */",
"value": 42
}`
expect(parse(text)).deep.equals({
comment: '// this is not a comment',
block: '/* neither is this */',
value: 42,
})
})
it('should handle comments at beginning and end', function () {
let text = `// start comment
{
"a": 1,
"b": 2
} // end comment`
expect(parse(text)).deep.equals({ a: 1, b: 2 })
})
it('should handle mixed comment types', function () {
let text = `{
"a": 1, // inline comment
/* multi-line
comment */
"b": 2
}`
expect(parse(text)).deep.equals({ a: 1, b: 2 })
})
it('should handle empty comments', function () {
let text = `{
"a": 1, //
"b": 2, /**/
"c": 3
}`
expect(parse(text)).deep.equals({ a: 1, b: 2, c: 3 })
})
it('should handle comments with special characters', function () {
let text = `{
"a": 1, // comment with "quotes" and 'single quotes'
"b": 2 /* comment with { } [ ] */
}`
expect(parse(text)).deep.equals({ a: 1, b: 2 })
})
it('should handle html style of comments', function () {
let text = `[
"line 1",
"line 2"
]`
expect(parse(text)).deep.equals(['line 1', 'line 2'])
})
})
})
================================================
FILE: src/parse.ts
================================================
type Error = unknown
let logError = console.error
// for testing (spy/mock)
export function setErrorLogger(
logger: (message: string, data?: any) => void,
): void {
logError = logger
}
export function disableErrorLogging(): void {
logError = () => {
/* do not output to console */
}
}
export function enableErrorLogging(): void {
logError = console.error
}
export function stripComments(text: string): string {
const buffer: string[] = []
let in_string = false
let string_char = ''
let string_escaped = false
let in_inline_comment = false
let in_block_comment = false
let saw_star = false
let in_html_comment = false
let saw_hyphen = 0
for (const char of text) {
// handle string content
if (in_string) {
// handle escaped sequence payload
if (string_escaped) {
string_escaped = false
buffer.push(char)
continue
}
// handle start of escape sequence
if (char === '\\') {
string_escaped = true
buffer.push(char)
continue
}
// handle end of string
if (char === string_char) {
in_string = false
buffer.push(char)
continue
}
// otherwise take the content of string
buffer.push(char)
continue
}
// handle inline comment content
if (in_inline_comment) {
// handle end of inline comment
if (char === '\n') {
in_inline_comment = false
continue
}
// otherwise ignore the content of comment
continue
}
const buffer_length = buffer.length
const last_char = buffer_length === 0 ? '' : buffer[buffer_length - 1]
// handle block comment content
if (in_block_comment) {
// handle end of block comment
if (char === '*') {
saw_star = true
continue
}
if (saw_star && char === '/') {
in_block_comment = false
continue
}
// otherwise ignore the content of comment
saw_star = false
continue
}
// handle html comment content
if (in_html_comment) {
// handle end of html comment
if (char === '-') {
saw_hyphen++
continue
}
if (saw_hyphen >= 2 && char === '>') {
in_html_comment = false
continue
}
// otherwise ignore the content of comment
saw_hyphen = 0
continue
}
// handle start of inline comment
if (last_char === '/' && char === '/') {
buffer.pop()
in_inline_comment = true
continue
}
// handle start of block comment
if (last_char === '/' && char === '*') {
buffer.pop()
in_block_comment = true
saw_star = false
continue
}
// handle start of string
if (char === '"' || char === "'" || char === '`') {
in_string = true
string_char = char
string_escaped = false
buffer.push(char)
continue
}
// handle start of html comment "