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. [![npm Package Version](https://img.shields.io/npm/v/best-effort-json-parser)](https://www.npmjs.com/package/best-effort-json-parser) [![Minified Package Size](https://img.shields.io/bundlephobia/min/best-effort-json-parser)](https://bundlephobia.com/package/best-effort-json-parser) [![Minified and Gzipped Package Size](https://img.shields.io/bundlephobia/minzip/best-effort-json-parser)](https://bundlephobia.com/package/best-effort-json-parser) [![npm Package Downloads](https://img.shields.io/npm/dm/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 "