[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs\n# http://editorconfig.org\n\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\n\n# We recommend you to keep these unchanged\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.xml]\nindent_style = space\nindent_size = 4\n\n[*.json]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".gitignore",
    "content": "\nnode_modules/\npackage-lock.json\npnpm-lock.yaml\npnpm-debug.log\ndist/\n.cache/\n*.tgz\n.env\n.nyc_output\ncoverage/\n"
  },
  {
    "path": ".idea/.gitignore",
    "content": "*\n!modules.xml\n!*.iml\n!dictionaries/\n!.gitignore\n\n"
  },
  {
    "path": ".idea/best-effort-json-parser.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"JAVA_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\" inherit-compiler-output=\"true\">\n    <exclude-output />\n    <content url=\"file://$MODULE_DIR$\">\n      <sourceFolder url=\"file://$MODULE_DIR$/src\" isTestSource=\"false\" />\n      <sourceFolder url=\"file://$MODULE_DIR$/test\" isTestSource=\"true\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/dist\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/data\" />\n      <excludeFolder url=\"file://$MODULE_DIR$/res\" />\n    </content>\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>\n"
  },
  {
    "path": ".idea/modules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n      <module fileurl=\"file://$PROJECT_DIR$/.idea/best-effort-json-parser.iml\" filepath=\"$PROJECT_DIR$/.idea/.idea/best-effort-json-parser.iml\" />\n    </modules>\n  </component>\n</project>\n"
  },
  {
    "path": ".nycrc.json",
    "content": "{\n  \"include\": \"src/**/*.ts\",\n  \"exclude\": \"src/**/*.{test,spec}.ts\"\n}\n"
  },
  {
    "path": ".prettierignore",
    "content": "*.macro.ts\nsrc/assets/*\nsrc/components.d.ts\nsrc/polyfill-array.ts\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": false,\n  \"quoteProps\": \"consistent\",\n  \"overrides\": [\n    {\n      \"files\": [\n        \"*.scss\",\n        \"*.css\"\n      ],\n      \"options\": {\n        \"singleQuote\": false\n      }\n    }\n  ],\n  \"semi\": false,\n  \"arrowParens\": \"avoid\",\n  \"trailingComma\": \"all\"\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 2-Clause License\n\nCopyright (c) [2020], [Beeno Tung (Tung Cheung Leong)]\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# best-effort-json-parser\n\nParse 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.\n\n[![npm Package Version](https://img.shields.io/npm/v/best-effort-json-parser)](https://www.npmjs.com/package/best-effort-json-parser)\n[![Minified Package Size](https://img.shields.io/bundlephobia/min/best-effort-json-parser)](https://bundlephobia.com/package/best-effort-json-parser)\n[![Minified and Gzipped Package Size](https://img.shields.io/bundlephobia/minzip/best-effort-json-parser)](https://bundlephobia.com/package/best-effort-json-parser)\n[![npm Package Downloads](https://img.shields.io/npm/dm/best-effort-json-parser)](https://www.npmtrends.com/best-effort-json-parser)\n\n## Features\n\n- Typescript support\n- Isomorphic package: works in Node.js and browsers\n- Comment support: `// inline`, `/* multi-line */`, and `<!-- HTML-style -->` comments\n\n## Installation\n\n```bash\nnpm install best-effort-json-parser\n```\n\nYou can also install `best-effort-json-parser` with [pnpm](https://pnpm.io/), [yarn](https://yarnpkg.com/), or [slnpm](https://github.com/beenotung/slnpm)\n\n## Usage Example\n\n### Parsing incomplete JSON text\n\nIf the string, object, or array is not complete, the parser will return the partial data.\n\n```typescript\nimport { parse } from 'best-effort-json-parser'\n\nlet data = parse(`[1, 2, {\"a\": \"apple`)\nconsole.log(data) // [1, 2, { a: 'apple' }]\n```\n\n### Parsing json with comments\n\nMultiple types of comments are supported:\n\n```typescript\nimport { parse } from 'best-effort-json-parser'\n\nlet config = parse(`{\n  \"database\": {\n    \"host\": \"localhost\", // database server\n    \"port\": 5432, /* default port */\n    \"ssl\": true\n  },\n  \"features\": [\"auth\", \"api\"] <!-- injected by LLM -->\n}`)\n```\n\nComments inside strings are preserved and not treated as comments:\n\n```typescript\nlet data = parse(`{\n  \"inline_comment\": \"// this is not a comment\",\n  \"block_comment\": \"/* neither is this */\",\n  \"html_comment\": \\`<!-- \\${variable} -->\\`,\n  \"value\": 42\n}`)\n```\n\n**Note:** The parser also supports template literals with backticks (\\`) for strings, in addition to single and double quotes.\n\n## Error Logging\n\nBy default, the parser logs errors to `console.error`. You can control error logging behavior:\n\n```typescript\nimport {\n  disableErrorLogging,\n  enableErrorLogging,\n  setErrorLogger,\n} from 'best-effort-json-parser'\n\n// Disable error logging completely\ndisableErrorLogging()\n\n// Re-enable error logging (default behavior)\nenableErrorLogging()\n\n// Set a custom error logger\nsetErrorLogger((message, data) => {\n  // Your custom logging logic here\n  console.log('Custom error:', message, data)\n\n  // Common destinations for error data:\n  // - Database storage for analysis\n  // - File system logging\n  // - Third-party services (Sentry, LogRocket, etc.)\n  // - Monitoring and alerting systems\n})\n```\n\n## Typescript Signature\n\n```typescript\n// Main parse function\nfunction parse(s: string | undefined | null): any\n\n// Parse namespace with additional properties\nnamespace parse {\n  lastParseReminding: string | undefined\n  onExtraToken: (text: string, data: any, reminding: string) => void | undefined\n}\n\n// Error logging functions\nfunction setErrorLogger(logger: (message: string, data?: any) => void): void\nfunction disableErrorLogging(): void\nfunction enableErrorLogging(): void\n```\n\nMore examples see [parse.spec.ts](./src/parse.spec.ts)\n\n## License\n\nThis is free and open-source software (FOSS) with\n[BSD-2-Clause License](./LICENSE)\n"
  },
  {
    "path": "example/index.ts",
    "content": "import { parse } from 'best-effort-json-parser'\n\n// Incomplete string, object, array\nlet data = parse(`[1,2,{\"a\":\"apple`)\nconsole.log(data) // [1, 2, { a: 'apple' }]\n\n// Example with comments\nlet dataWithComments = parse(`{\n  \"users\": [\n    {\n      \"name\": \"Alice\", // admin user\n      \"role\": \"admin\"\n    },\n    {\n      \"name\": \"Bob\", /* regular user\n      with multi-line comment */\n      \"role\": \"user\"\n    }\n  ],\n  \"version\": 1.2.3\n}`)\nconsole.log(dataWithComments)\n/*\n{\n  users: [\n    { name: 'Alice', role: 'admin' },\n    { name: 'Bob', role: 'user' },\n  ],\n  version: '1.2.3'\n}\n*/\n"
  },
  {
    "path": "example/package.json",
    "content": "{\n  \"name\": \"example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"ts-node index.ts\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"best-effort-json-parser\": \"file:..\"\n  },\n  \"devDependencies\": {\n    \"ts-node\": \"^9.1.1\",\n    \"typescript\": \"^5.9.3\"\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"best-effort-json-parser\",\n  \"version\": \"1.4.0\",\n  \"description\": \"Parse incomplete json text in best-effort manner\",\n  \"keywords\": [\n    \"json\",\n    \"parser\",\n    \"auto-fix\",\n    \"auto-repair\",\n    \"best-effort\"\n  ],\n  \"author\": \"Beeno Tung <aabbcc1241@yahoo.com.hk> (https://beeno-tung.surge.sh)\",\n  \"license\": \"BSD-2-Clause\",\n  \"main\": \"dist/parse.js\",\n  \"types\": \"dist/parse.d.ts\",\n  \"scripts\": {\n    \"test\": \"run-s format tsc mocha\",\n    \"clean\": \"rimraf dist\",\n    \"format\": \"prettier --write \\\"src/**/*.ts\\\"\",\n    \"postformat\": \"tslint -p . --fix && format-json-cli\",\n    \"build\": \"run-s clean tsc\",\n    \"tsc\": \"tsc -p .\",\n    \"mocha\": \"ts-mocha \\\"src/**/*.spec.ts\\\"\",\n    \"coverage\": \"nyc npm run mocha -- --reporter=progress\",\n    \"report:update\": \"nyc --reporter=lcov npm run mocha -- --reporter=progress\",\n    \"report:open\": \"open-cli coverage/lcov-report/index.html\",\n    \"report\": \"run-s report:update report:open\",\n    \"prepublishOnly\": \"run-s test build\"\n  },\n  \"directories\": {\n    \"example\": \"example\",\n    \"test\": \"test\"\n  },\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"devDependencies\": {\n    \"@types/chai\": \"^4.2.14\",\n    \"@types/mocha\": \"^8.2.0\",\n    \"@types/node\": \"*\",\n    \"@types/sinon\": \"^9.0.9\",\n    \"chai\": \"^4.2.0\",\n    \"format-json-cli\": \"^1.0.1\",\n    \"mocha\": \"^8.2.1\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"nyc\": \"^15.1.0\",\n    \"open-cli\": \"^6.0.1\",\n    \"prettier\": \"^2.2.1\",\n    \"rimraf\": \"^3.0.2\",\n    \"sinon\": \"^9.2.2\",\n    \"ts-mocha\": \"^8.0.0\",\n    \"ts-node\": \"^9.1.1\",\n    \"tslint\": \"^6.1.3\",\n    \"tslint-config-prettier\": \"^1.18.0\",\n    \"tslint-eslint-rules\": \"^5.4.0\",\n    \"tslint-etc\": \"^1.13.9\",\n    \"typescript\": \"^4.8.3\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/beenotung/best-effort-json-parser.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/beenotung/best-effort-json-parser/issues\"\n  },\n  \"homepage\": \"https://github.com/beenotung/best-effort-json-parser#readme\"\n}\n"
  },
  {
    "path": "src/parse.spec.ts",
    "content": "import sinon from 'sinon'\nimport { expect } from 'chai'\nimport { parse, setErrorLogger } from './parse'\n\nlet onExtraTokenSpy: sinon.SinonSpy\nlet originalOnExtraToken: typeof parse.onExtraToken\nlet muteLog = true\nbeforeEach(() => {\n  if (muteLog) {\n    onExtraTokenSpy = sinon.fake()\n    originalOnExtraToken = parse.onExtraToken\n    parse.onExtraToken = onExtraTokenSpy\n  } else {\n    onExtraTokenSpy = sinon.spy(parse, 'onExtraToken')\n  }\n})\nafterEach(() => {\n  if (muteLog) {\n    parse.onExtraToken = originalOnExtraToken\n  } else {\n    sinon.restore()\n  }\n})\n\ndescribe('parser TestSuit', function () {\n  context('number', () => {\n    it('should parse positive integer', function () {\n      expect(parse(`42`)).equals(42)\n    })\n    it('should parse negative integer', function () {\n      expect(parse(`-42`)).equals(-42)\n    })\n\n    it('should parse positive float', function () {\n      expect(parse(`12.34`)).equals(12.34)\n    })\n    it('should parse negative float', function () {\n      expect(parse(`-12.34`)).equals(-12.34)\n    })\n\n    it('should parse incomplete positive float', function () {\n      expect(parse(`12.`)).equals(12)\n    })\n    it('should parse incomplete negative float', function () {\n      expect(parse(`-12.`)).equals(-12)\n    })\n    it('should parse incomplete negative integer', function () {\n      expect(parse(`-`)).equals(-0)\n    })\n\n    it('should preserve invalid number', function () {\n      expect(parse(`1.2.3.4`)).equals('1.2.3.4')\n    })\n  })\n\n  context('string', () => {\n    it('should parse string', function () {\n      expect(parse(`\"I am text\"`)).equals('I am text')\n      expect(parse(`\"I'm text\"`)).equals(\"I'm text\")\n      expect(parse(`\"I\\\\\"m text\"`)).equals('I\"m text')\n    })\n    it('should parse incomplete string', function () {\n      expect(parse(`\"I am text`)).equals('I am text')\n      expect(parse(`\"I'm text`)).equals(\"I'm text\")\n      expect(parse(`\"I\\\\\"m text`)).equals('I\"m text')\n    })\n  })\n\n  context('boolean', () => {\n    it('should parse boolean', function () {\n      expect(parse(`true`)).equals(true)\n      expect(parse(`false`)).equals(false)\n    })\n\n    function testIncomplete(str: string, val: boolean) {\n      for (let i = str.length; i >= 1; i--) {\n        expect(parse(str.slice(0, i))).equals(val)\n      }\n    }\n\n    it('should parse incomplete true', function () {\n      testIncomplete(`true`, true)\n    })\n    it('should parse incomplete false', function () {\n      testIncomplete(`false`, false)\n    })\n  })\n\n  context('array', () => {\n    it('should parse empty array', function () {\n      expect(parse(`[]`)).deep.equals([])\n    })\n    it('should parse number array', function () {\n      expect(parse(`[1,2,3]`)).deep.equals([1, 2, 3])\n    })\n    it('should parse incomplete array', function () {\n      expect(parse(`[1,2,3`)).deep.equals([1, 2, 3])\n      expect(parse(`[1,2,`)).deep.equals([1, 2])\n      expect(parse(`[1,2`)).deep.equals([1, 2])\n      expect(parse(`[1,`)).deep.equals([1])\n      expect(parse(`[1`)).deep.equals([1])\n      expect(parse(`[`)).deep.equals([])\n    })\n  })\n\n  context('object', () => {\n    it('should parse simple object', function () {\n      let o = { a: 'apple', b: 'banana' }\n      expect(parse(JSON.stringify(o))).deep.equals(o)\n      expect(parse(JSON.stringify(o, null, 2))).deep.equals(o)\n      expect(parse(`{\"a\":\"apple\",\"b\":\"banana\"}`)).deep.equals({\n        a: 'apple',\n        b: 'banana',\n      })\n      expect(parse(`{\"a\": \"apple\",\"b\": \"banana\"}`)).deep.equals({\n        a: 'apple',\n        b: 'banana',\n      })\n      expect(parse(`{\"a\": \"apple\", \"b\": \"banana\"}`)).deep.equals({\n        a: 'apple',\n        b: 'banana',\n      })\n      expect(parse(`{\"a\" : \"apple\", \"b\" : \"banana\"}`)).deep.equals({\n        a: 'apple',\n        b: 'banana',\n      })\n      expect(parse(`{ \"a\" : \"apple\", \"b\" : \"banana\" }`)).deep.equals({\n        a: 'apple',\n        b: 'banana',\n      })\n      expect(parse(`{ \"a\" : \"apple\" , \"b\" : \"banana\" }`)).deep.equals({\n        a: 'apple',\n        b: 'banana',\n      })\n    })\n    it('should parse incomplete simple object', function () {\n      expect(parse(`{\"a\":\"apple\",\"b\":\"banana\"`)).deep.equals({\n        a: 'apple',\n        b: 'banana',\n      })\n      expect(parse(`{\"a\":\"apple\",\"b\":\"banana`)).deep.equals({\n        a: 'apple',\n        b: 'banana',\n      })\n      expect(parse(`{\"a\":\"apple\",\"b\":\"b`)).deep.equals({ a: 'apple', b: 'b' })\n      expect(parse(`{\"a\":\"apple\",\"b\":\"`)).deep.equals({ a: 'apple', b: '' })\n      expect(parse(`{\"a\":\"apple\",\"b\":`)).deep.equals({\n        a: 'apple',\n        b: undefined,\n      })\n      expect(parse(`{\"a\":\"apple\",\"b\"`)).deep.equals({\n        a: 'apple',\n        b: undefined,\n      })\n      expect(parse(`{\"a\":\"apple\",\"b`)).deep.equals({ a: 'apple', b: undefined })\n      expect(parse(`{\"a\":\"apple\",\"`)).deep.equals({\n        'a': 'apple',\n        '': undefined,\n      })\n      expect(parse(`{\"a\":\"apple\",`)).deep.equals({ a: 'apple' })\n      expect(parse(`{\"a\":\"apple\"`)).deep.equals({ a: 'apple' })\n      expect(parse(`{\"a\":\"apple`)).deep.equals({ a: 'apple' })\n      expect(parse(`{\"a\":\"a`)).deep.equals({ a: 'a' })\n      expect(parse(`{\"a\":\"`)).deep.equals({ a: '' })\n      expect(parse(`{\"a\":`)).deep.equals({ a: undefined })\n      expect(parse(`{\"a\"`)).deep.equals({ a: undefined })\n      expect(parse(`{\"a`)).deep.equals({ a: undefined })\n      expect(parse(`{\"`)).deep.equals({ '': undefined })\n      expect(parse(`{`)).deep.equals({})\n    })\n  })\n\n  context('complex object', () => {\n    it('should parse complete complex object', function () {\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n        \"obj\": {\n          \"int\": 42,\n          \"float\": 12.34\n        }\n      }\n}`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n          obj: {\n            int: 42,\n            float: 12.34,\n          },\n        },\n      })\n    })\n    it('should parse incomplete complex object', function () {\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n        \"obj\": {\n          \"int\": 42,\n          \"float\": 12.34\n        }\n      }`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n          obj: {\n            int: 42,\n            float: 12.34,\n          },\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n        \"obj\": {\n          \"int\": 42,\n          \"float\": 12.34\n        }`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n          obj: {\n            int: 42,\n            float: 12.34,\n          },\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n        \"obj\": {\n          \"int\": 42,\n          \"float\": 12.34`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n          obj: {\n            int: 42,\n            float: 12.34,\n          },\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n        \"obj\": {\n          \"int\": 42,\n          \"float\": 12.`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n          obj: {\n            int: 42,\n            float: 12,\n          },\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n        \"obj\": {\n          \"int\": 42,\n          \"float\":`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n          obj: {\n            int: 42,\n            float: undefined,\n          },\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n        \"obj\": {\n          \"int\": 42,`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n          obj: {\n            int: 42,\n          },\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n        \"obj\": {`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n          obj: {},\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n        \"obj\":`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n          obj: undefined,\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"flo`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], { int: 42, flo: undefined }],\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 12.34], {`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 12.34], {}],\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [42, 12.34, [42, 1`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [42, 12.34, [42, 1]],\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\": 12.34,\n        \"arr\": [`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: 12.34,\n          arr: [],\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"float\": 12.34 }],\n      \"obj\": {\n        \"int\": 42,\n        \"float\"`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, float: 12.34 }],\n        obj: {\n          int: 42,\n          float: undefined,\n        },\n      })\n\n      expect(\n        parse(`{\n      \"int\": 42,\n      \"float\": 12.34,\n      \"arr\": [42, 12.34, [42, 12.34], { \"int\": 42, \"flo`),\n      ).deep.equals({\n        int: 42,\n        float: 12.34,\n        arr: [42, 12.34, [42, 12.34], { int: 42, flo: undefined }],\n      })\n    })\n  })\n\n  context('invalid inputs', () => {\n    it('should throw error on invalid (not incomplete) json text', function () {\n      // spy the error logger\n      let spy = sinon.fake()\n      setErrorLogger(spy)\n\n      expect(() => parse(`:atom`)).to.throws()\n      expect(spy.called).be.true\n      expect(spy.firstCall.firstArg).is.string('no parser registered for \":\"')\n\n      // restore the error logger\n      setErrorLogger(console.error)\n    })\n    it('should complaint on extra tokens', function () {\n      expect(parse(`[1] 2`)).deep.equals([1])\n      expect(onExtraTokenSpy.called).be.true\n      expect(parse.lastParseReminding).equals(' 2')\n    })\n  })\n\n  context('extra space', () => {\n    it('should parse complete json with extra space', function () {\n      expect(parse(` [1] `)).deep.equals([1])\n    })\n    it('should parse incomplete json with extra space', function () {\n      expect(parse(` [1 `)).deep.equals([1])\n    })\n  })\n\n  context('invalid but understandable json', () => {\n    context('string newline', () => {\n      it('should parse escaped newline', function () {\n        expect(parse(`\"line1\\\\nline2\"`)).equals('line1\\nline2')\n        expect(\n          parse(/* javascript */ `\n            {\n              \"essay\": \"global health.\\n\\nDuring my tenure ...\"\n            }\n          `),\n        ).deep.equals({\n          essay: 'global health.\\n\\nDuring my tenure ...',\n        })\n      })\n      it('should parse non-escaped newline', function () {\n        expect(parse(`\"line1\\nline2\"`)).equals('line1\\nline2')\n      })\n      it('should parse non-escaped newline inside string value', function () {\n        expect(parse(`{\"key\":\"line1\\\\nline2`)).deep.equals({\n          key: 'line1\\nline2',\n        })\n        expect(parse(`{\"key\":\"line1\\nline2`)).deep.equals({\n          key: 'line1\\nline2',\n        })\n        expect(parse(`{\"key\":\"line1\\n`)).deep.equals({ key: 'line1\\n' })\n        expect(parse(`{\\n\\t\"key\":\"line1\\n`)).deep.equals({ key: 'line1\\n' })\n        expect(parse(`{\\n\\t\"key\":\"line1\\nline2\"\\n}`)).deep.equals({\n          key: 'line1\\nline2',\n        })\n      })\n    })\n    context('string non-escaped characters', function () {\n      it('should parse \\\\t', function () {\n        expect(parse(`\"text\\t\"`)).equals(`text\\t`)\n        expect(parse(`\"text\\\\t\"`)).equals(`text\\t`)\n      })\n      it('should parse \\\\r', function () {\n        expect(parse(`\"text\\r\"`)).equals(`text\\r`)\n        expect(parse(`\"text\\\\r\"`)).equals(`text\\r`)\n      })\n    })\n    context('string quote', () => {\n      it('should parse string with double quote', function () {\n        expect(parse(`\"str\"`)).equals('str')\n      })\n      it('should parse string with single quote', function () {\n        expect(parse(`'str'`)).equals('str')\n      })\n      it('should parse string without double quote', function () {\n        expect(parse(`str`)).equals('str')\n      })\n      it('should parse array of string without double quote', function () {\n        expect(parse(`[a,b,c]`)).deep.equals(['a', 'b', 'c'])\n      })\n      it('should parse string with backticks', function () {\n        expect(parse(`\\`\"Alice's\"\\``)).equals(`\"Alice's\"`)\n        expect(\n          parse(`[\n            \\`double quote: \"\\`,\n            \\`single quote: '\\`,\n            ${'`backtick: \\\\``'}\n          ]`),\n        ).deep.equals([`double quote: \"`, `single quote: '`, 'backtick: `'])\n      })\n    })\n    context('object key', () => {\n      it('should parse object key with double quote', function () {\n        expect(parse(`{ \"int\" : 42 }`)).deep.equals({ int: 42 })\n      })\n      it('should parse object key with single quote', function () {\n        expect(parse(`{ 'int' : 42 }`)).deep.equals({ int: 42 })\n      })\n      it('should parse object key without double quote', function () {\n        expect(parse(`{ int : 42 }`)).deep.equals({ int: 42 })\n      })\n    })\n  })\n\n  context('falsy values', () => {\n    it('should parse empty string', function () {\n      expect(parse('')).equals('')\n    })\n    it('should parse undefined', function () {\n      expect(parse(undefined)).to.be.undefined\n    })\n    it('should parse null', function () {\n      expect(parse(null)).to.be.null\n    })\n  })\n\n  context('incomplete escaped characters', function () {\n    it('should ignore an incomplete escaped character (such as control character \\\\n)', function () {\n      expect(parse(`\"the newline\\n`)).equals('the newline\\n')\n      expect(parse(`\"the newline\\\\n`)).equals('the newline\\n')\n      expect(parse(`\"the newline\\\\`)).equals('the newline')\n      expect(parse(`\"the newline\\n\\\\`)).equals('the newline\\n')\n      expect(parse(`\"the newline\\\\n\\\\`)).equals('the newline\\n')\n      expect(parse(`\"the newline\\\\\\\\`)).equals('the newline\\\\')\n    })\n    it('should ignore incomplete escape character in object value', function () {\n      expect(parse('{\"a\":\"\\n\"')).deep.equals({ a: '\\n' })\n      expect(parse('{\"a\":\"\\n')).deep.equals({ a: '\\n' })\n      expect(parse('{\"a\":\"\\\\n\"')).deep.equals({ a: '\\n' })\n      expect(parse('{\"a\":\"\\\\')).deep.equals({ a: '' })\n    })\n  })\n\n  context('comment in json', () => {\n    it('should ignore inline comment', function () {\n      // test with //\n      let text = `{\n        \"a\": 1, // comment\n        \"b\": 2\n      }`\n      expect(parse(text)).deep.equals({ a: 1, b: 2 })\n    })\n    it('should ignore multi-line comment', function () {\n      // test with /* */\n      let text = `{\n        \"a\": 1, /* line 1\n        line 2\n        line 3 */\n        \"b\": 2\n      }`\n      expect(parse(text)).deep.equals({ a: 1, b: 2 })\n    })\n    it('should not strip comments inside strings', function () {\n      let text = `{\n        \"comment\": \"// this is not a comment\",\n        \"block\": \"/* neither is this */\",\n        \"value\": 42\n      }`\n      expect(parse(text)).deep.equals({\n        comment: '// this is not a comment',\n        block: '/* neither is this */',\n        value: 42,\n      })\n    })\n    it('should handle comments at beginning and end', function () {\n      let text = `// start comment\n      {\n        \"a\": 1,\n        \"b\": 2\n      } // end comment`\n      expect(parse(text)).deep.equals({ a: 1, b: 2 })\n    })\n    it('should handle mixed comment types', function () {\n      let text = `{\n        \"a\": 1, // inline comment\n        /* multi-line\n           comment */\n        \"b\": 2\n      }`\n      expect(parse(text)).deep.equals({ a: 1, b: 2 })\n    })\n    it('should handle empty comments', function () {\n      let text = `{\n        \"a\": 1, //\n        \"b\": 2, /**/\n        \"c\": 3\n      }`\n      expect(parse(text)).deep.equals({ a: 1, b: 2, c: 3 })\n    })\n    it('should handle comments with special characters', function () {\n      let text = `{\n        \"a\": 1, // comment with \"quotes\" and 'single quotes'\n        \"b\": 2 /* comment with { } [ ] */\n      }`\n      expect(parse(text)).deep.equals({ a: 1, b: 2 })\n    })\n    it('should handle html style of comments', function () {\n      let text = `[\n        \"line 1\", <!-- comment -->\n        \"line 2\"\n      ]`\n      expect(parse(text)).deep.equals(['line 1', 'line 2'])\n    })\n  })\n})\n"
  },
  {
    "path": "src/parse.ts",
    "content": "type Error = unknown\n\nlet logError = console.error\n\n// for testing (spy/mock)\nexport function setErrorLogger(\n  logger: (message: string, data?: any) => void,\n): void {\n  logError = logger\n}\n\nexport function disableErrorLogging(): void {\n  logError = () => {\n    /* do not output to console */\n  }\n}\n\nexport function enableErrorLogging(): void {\n  logError = console.error\n}\n\nexport function stripComments(text: string): string {\n  const buffer: string[] = []\n\n  let in_string = false\n  let string_char = ''\n  let string_escaped = false\n\n  let in_inline_comment = false\n\n  let in_block_comment = false\n  let saw_star = false\n\n  let in_html_comment = false\n  let saw_hyphen = 0\n\n  for (const char of text) {\n    // handle string content\n    if (in_string) {\n      // handle escaped sequence payload\n      if (string_escaped) {\n        string_escaped = false\n        buffer.push(char)\n        continue\n      }\n\n      // handle start of escape sequence\n      if (char === '\\\\') {\n        string_escaped = true\n        buffer.push(char)\n        continue\n      }\n\n      // handle end of string\n      if (char === string_char) {\n        in_string = false\n        buffer.push(char)\n        continue\n      }\n\n      // otherwise take the content of string\n      buffer.push(char)\n      continue\n    }\n\n    // handle inline comment content\n    if (in_inline_comment) {\n      // handle end of inline comment\n      if (char === '\\n') {\n        in_inline_comment = false\n        continue\n      }\n\n      // otherwise ignore the content of comment\n      continue\n    }\n\n    const buffer_length = buffer.length\n    const last_char = buffer_length === 0 ? '' : buffer[buffer_length - 1]\n\n    // handle block comment content\n    if (in_block_comment) {\n      // handle end of block comment\n      if (char === '*') {\n        saw_star = true\n        continue\n      }\n      if (saw_star && char === '/') {\n        in_block_comment = false\n        continue\n      }\n\n      // otherwise ignore the content of comment\n      saw_star = false\n      continue\n    }\n\n    // handle html comment content\n    if (in_html_comment) {\n      // handle end of html comment\n      if (char === '-') {\n        saw_hyphen++\n        continue\n      }\n      if (saw_hyphen >= 2 && char === '>') {\n        in_html_comment = false\n        continue\n      }\n\n      // otherwise ignore the content of comment\n      saw_hyphen = 0\n      continue\n    }\n\n    // handle start of inline comment\n    if (last_char === '/' && char === '/') {\n      buffer.pop()\n      in_inline_comment = true\n      continue\n    }\n\n    // handle start of block comment\n    if (last_char === '/' && char === '*') {\n      buffer.pop()\n      in_block_comment = true\n      saw_star = false\n      continue\n    }\n\n    // handle start of string\n    if (char === '\"' || char === \"'\" || char === '`') {\n      in_string = true\n      string_char = char\n      string_escaped = false\n      buffer.push(char)\n      continue\n    }\n\n    // handle start of html comment \"<!--\"\n    if (\n      buffer_length >= 4 &&\n      char === '-' &&\n      buffer[buffer_length - 1] === '-' &&\n      buffer[buffer_length - 2] === '!' &&\n      buffer[buffer_length - 3] === '<'\n    ) {\n      in_html_comment = true\n      buffer.pop()\n      buffer.pop()\n      buffer.pop()\n      continue\n    }\n\n    // otherwise take the content of the text\n    buffer.push(char)\n  }\n  return buffer.join('')\n}\n\nexport function parse(s: string | undefined | null): any {\n  if (s === undefined) {\n    return undefined\n  }\n  if (s === null) {\n    return null\n  }\n  if (s === '') {\n    return ''\n  }\n  // strip comments first\n  s = stripComments(s)\n  // remove incomplete escaped characters at the end of the string\n  s = s.replace(/\\\\+$/, match =>\n    match.length % 2 === 0 ? match : match.slice(0, -1),\n  )\n  try {\n    return JSON.parse(s)\n  } catch (e) {\n    const [data, reminding] =\n      s.trimLeft()[0] === ':'\n        ? parseAny(s, e)\n        : parseAny(s, e, parseStringWithoutQuote)\n    parse.lastParseReminding = reminding\n    if (parse.onExtraToken && reminding.length > 0) {\n      const trimmedReminding = reminding.trimRight()\n      parse.lastParseReminding = trimmedReminding\n      if (trimmedReminding.length > 0) {\n        parse.onExtraToken(s, data, trimmedReminding)\n      }\n    }\n    return data\n  }\n}\n\nexport namespace parse {\n  export let lastParseReminding: string | undefined\n  export let onExtraToken: (\n    text: string,\n    data: any,\n    reminding: string,\n  ) => void | undefined = (text, data, reminding) => {\n    logError('parsed json with extra tokens:', {\n      text,\n      data,\n      reminding,\n    })\n  }\n}\n\nfunction parseAny(\n  s: string,\n  e: Error,\n  fallback?: Parser<any>,\n): ParseResult<any> {\n  const parser = parsers[s[0]] || fallback\n  if (!parser) {\n    logError(`no parser registered for ${JSON.stringify(s[0])}:`, { s })\n    throw e\n  }\n  return parser(s, e)\n}\n\nfunction parseStringCasual(\n  s: string,\n  e: Error,\n  delimiters?: string[],\n): ParseResult<string> {\n  if (s[0] === '\"') {\n    return parseString(s)\n  }\n  if (s[0] === \"'\") {\n    return parseSingleQuoteString(s)\n  }\n  if (s[0] === '`') {\n    return parseBacktickString(s)\n  }\n  return parseStringWithoutQuote(s, e, delimiters)\n}\n\ntype Code = string\ntype Parser<T> = (s: Code, e: Error) => ParseResult<T>\ntype ParseResult<T> = [T, Code]\n\nconst parsers: Record<string, Parser<any>> = {}\n\nfunction skipSpace(s: string): string {\n  return s.trimLeft()\n}\n\nparsers[' '] = parseSpace\nparsers['\\r'] = parseSpace\nparsers['\\n'] = parseSpace\nparsers['\\t'] = parseSpace\n\nfunction parseSpace(s: string, e: Error) {\n  s = skipSpace(s)\n  return parseAny(s, e)\n}\n\nparsers['['] = parseArray\n\nfunction parseArray(s: string, e: Error): ParseResult<any[]> {\n  s = s.substr(1) // skip starting '['\n  const acc: any[] = []\n  s = skipSpace(s)\n  for (; s.length > 0; ) {\n    if (s[0] === ']') {\n      s = s.substr(1) // skip ending ']'\n      break\n    }\n    const res = parseAny(s, e, (s, e) =>\n      parseStringWithoutQuote(s, e, [',', ']']),\n    )\n    acc.push(res[0])\n    s = res[1]\n    s = skipSpace(s)\n    if (s[0] === ',') {\n      s = s.substring(1)\n      s = skipSpace(s)\n    }\n  }\n  return [acc, s]\n}\n\nfor (const c of '0123456789.-'.slice()) {\n  parsers[c] = parseNumber\n}\n\nfunction parseNumber(s: string): ParseResult<number | string> {\n  for (let i = 0; i < s.length; i++) {\n    const c = s[i]\n    if (parsers[c] === parseNumber) {\n      continue\n    }\n    const num = s.substring(0, i)\n    s = s.substring(i)\n    return [numToStr(num), s]\n  }\n  return [numToStr(s), '']\n}\n\nfunction numToStr(s: string) {\n  if (s === '-') {\n    return -0\n  }\n  const num = +s\n  if (Number.isNaN(num)) {\n    return s\n  }\n  return num\n}\n\nparsers['\"'] = parseString\n\nfunction parseString(s: string): ParseResult<string> {\n  for (let i = 1; i < s.length; i++) {\n    const c = s[i]\n    if (c === '\\\\') {\n      i++\n      continue\n    }\n    if (c === '\"') {\n      const str = fixEscapedCharacters(s.substring(0, i + 1))\n      s = s.substring(i + 1)\n      return [JSON.parse(str), s]\n    }\n  }\n  return [JSON.parse(fixEscapedCharacters(s) + '\"'), '']\n}\n\nfunction fixEscapedCharacters(s: string): string {\n  return s.replace(/\\n/g, '\\\\n').replace(/\\t/g, '\\\\t').replace(/\\r/g, '\\\\r')\n}\n\nparsers[\"'\"] = parseSingleQuoteString\n\nfunction parseSingleQuoteString(s: string): ParseResult<string> {\n  for (let i = 1; i < s.length; i++) {\n    const c = s[i]\n    if (c === '\\\\') {\n      i++\n      continue\n    }\n    if (c === \"'\") {\n      const str = fixEscapedCharacters(s.substring(0, i + 1))\n      s = s.substring(i + 1)\n      return [JSON.parse('\"' + str.slice(1, -1) + '\"'), s]\n    }\n  }\n  return [JSON.parse('\"' + fixEscapedCharacters(s.slice(1)) + '\"'), '']\n}\n\nparsers['`'] = parseBacktickString\n\nfunction parseBacktickString(s: string): ParseResult<string> {\n  const buffer: string[] = []\n  let is_escaped = false\n  let escape_count = 0\n  for (let i = 1; i < s.length; i++) {\n    const c = s[i]\n    if (is_escaped) {\n      buffer.push(c)\n      is_escaped = false\n      continue\n    }\n    if (c === '\\\\') {\n      is_escaped = true\n      escape_count++\n      continue\n    }\n    if (c === '`') {\n      const str = buffer.join('')\n      s = s.substring(str.length + escape_count + 2)\n      return [str, s]\n    }\n    buffer.push(c)\n  }\n  return [buffer.join(''), s.slice(1)]\n}\n\nfunction parseStringWithoutQuote(\n  s: string,\n  e: Error,\n  delimiters: string[] = [' '],\n): ParseResult<string> {\n  const index = Math.min(\n    ...delimiters.map(delimiter => {\n      const index = s.indexOf(delimiter)\n      return index === -1 ? s.length : index\n    }),\n  )\n  const value = s.substring(0, index).trim()\n  const rest = s.substring(index)\n  return [value, rest]\n}\n\nparsers['{'] = parseObject\n\nfunction parseObject(s: string, e: Error): ParseResult<object> {\n  s = s.substr(1) // skip starting '{'\n  const acc: any = {}\n  s = skipSpace(s)\n  for (; s.length > 0; ) {\n    if (s[0] === '}') {\n      s = s.substr(1) // skip ending '}'\n      break\n    }\n\n    const keyRes = parseStringCasual(s, e, [':', '}'])\n    const key = keyRes[0]\n    s = keyRes[1]\n\n    s = skipSpace(s)\n    if (s[0] !== ':') {\n      acc[key] = undefined\n      break\n    }\n    s = s.substr(1) // skip ':'\n    s = skipSpace(s)\n\n    if (s.length === 0) {\n      acc[key] = undefined\n      break\n    }\n    const valueRes = parseAny(s, e)\n    acc[key] = valueRes[0]\n    s = valueRes[1]\n    s = skipSpace(s)\n\n    if (s[0] === ',') {\n      s = s.substr(1)\n      s = skipSpace(s)\n    }\n  }\n  return [acc, s]\n}\n\nparsers['t'] = parseTrue\n\nfunction parseTrue(s: string, e: Error): ParseResult<true> {\n  return parseToken(s, `true`, true, e)\n}\n\nparsers['f'] = parseFalse\n\nfunction parseFalse(s: string, e: Error): ParseResult<false> {\n  return parseToken(s, `false`, false, e)\n}\n\nparsers['n'] = parseNull\n\nfunction parseNull(s: string, e: Error): ParseResult<null> {\n  return parseToken(s, `null`, null, e)\n}\n\nfunction parseToken<T>(\n  s: string,\n  tokenStr: string,\n  tokenVal: T,\n  e: Error,\n): ParseResult<T> {\n  for (let i = tokenStr.length; i >= 1; i--) {\n    if (s.startsWith(tokenStr.slice(0, i))) {\n      return [tokenVal, s.slice(i)]\n    }\n  }\n  /* istanbul ignore next */\n  {\n    const prefix = JSON.stringify(s.slice(0, tokenStr.length))\n    logError(`unknown token starting with ${prefix}:`, { s })\n    throw e\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"allowJs\": false,\n    \"sourceMap\": true,\n    \"declaration\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"downlevelIteration\": false,\n    \"importHelpers\": false,\n    \"lib\": [\n      \"dom\",\n      \"es2018\"\n    ],\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"node\",\n    \"target\": \"es2015\",\n    \"outDir\": \"dist\"\n  },\n  \"include\": [\n    \"src/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\",\n    \"src/**/*.spec.ts\"\n  ],\n  \"compileOnSave\": false,\n  \"atom\": {\n    \"rewriteTsconfig\": false\n  }\n}\n"
  },
  {
    "path": "tslint.json",
    "content": "{\n  \"extends\": [\n    \"tslint:recommended\",\n    \"tslint-eslint-rules\",\n    \"tslint-etc\",\n    \"tslint-config-prettier\"\n  ],\n  \"rules\": {\n    \"no-unused-declaration\": true,\n    \"array-type\": [\n      true,\n      \"array-simple\"\n    ],\n    \"interface-over-type-literal\": false,\n    \"no-reference\": true,\n    \"only-arrow-functions\": false,\n    \"max-classes-per-file\": false,\n    \"no-namespace\": false,\n    \"align\": [\n      false,\n      \"arguments\",\n      \"elements\",\n      \"members\",\n      \"parameters\",\n      \"statements\"\n    ],\n    \"arrow-parens\": [\n      true,\n      \"ban-single-arg-parens\"\n    ],\n    \"jsdoc-format\": false,\n    \"space-before-function-paren\": false,\n    \"no-invalid-this\": true,\n    \"callable-types\": true,\n    \"class-name\": true,\n    \"comment-format\": [\n      true,\n      \"check-space\"\n    ],\n    \"curly\": true,\n    \"eofline\": true,\n    \"forin\": true,\n    \"import-blacklist\": [\n      true\n    ],\n    \"import-spacing\": true,\n    \"indent\": [\n      true,\n      \"spaces\"\n    ],\n    \"label-position\": true,\n    \"max-line-length\": [\n      true,\n      140\n    ],\n    \"member-access\": false,\n    \"member-ordering\": [\n      true,\n      {\n        \"order\": \"instance-sandwich\"\n      }\n    ],\n    \"no-arg\": true,\n    \"no-bitwise\": true,\n    \"no-console\": [\n      true,\n      \"debug\",\n      \"info\",\n      \"time\",\n      \"timeEnd\",\n      \"trace\"\n    ],\n    \"no-construct\": true,\n    \"no-debugger\": true,\n    \"no-duplicate-variable\": true,\n    \"no-empty\": true,\n    \"no-empty-interface\": true,\n    \"no-eval\": true,\n    \"no-inferrable-types\": false,\n    \"no-shadowed-variable\": false,\n    \"no-string-literal\": false,\n    \"no-string-throw\": true,\n    \"no-switch-case-fall-through\": true,\n    \"no-trailing-whitespace\": true,\n    \"no-unused-expression\": [\n      true,\n      \"allow-fast-null-checks\"\n    ],\n    \"no-var-keyword\": true,\n    \"object-literal-sort-keys\": false,\n    \"one-line\": [\n      true,\n      \"check-open-brace\",\n      \"check-catch\",\n      \"check-else\",\n      \"check-whitespace\"\n    ],\n    \"prefer-const\": true,\n    \"quotemark\": [\n      false,\n      \"single\",\n      \"avoid-escape\"\n    ],\n    \"radix\": true,\n    \"semicolon\": [\n      false,\n      \"never\",\n      \"ignore-interfaces\"\n    ],\n    \"triple-equals\": true,\n    \"typedef-whitespace\": [\n      true,\n      {\n        \"call-signature\": \"nospace\",\n        \"index-signature\": \"nospace\",\n        \"parameter\": \"nospace\",\n        \"property-declaration\": \"nospace\",\n        \"variable-declaration\": \"nospace\"\n      }\n    ],\n    \"unified-signatures\": false,\n    \"variable-name\": false,\n    \"whitespace\": [\n      true,\n      \"check-branch\",\n      \"check-decl\",\n      \"check-operator\",\n      \"check-separator\",\n      \"check-type\"\n    ],\n    \"interface-name\": false,\n    \"ordered-imports\": true,\n    \"trailing-comma\": [\n      true,\n      {\n        \"multiline\": {\n          \"arrays\": \"always\",\n          \"exports\": \"always\",\n          \"functions\": \"ignore\",\n          \"imports\": \"always\",\n          \"objects\": \"always\",\n          \"typeLiterals\": \"never\"\n        },\n        \"singleline\": \"never\"\n      }\n    ]\n  }\n}\n"
  }
]