[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ninsert_final_newline = true\ncharset = utf-8\ntrim_trailing_whitespace = true\nend_of_line = lf\n\n[*.{ts,js,json}]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".eslintignore",
    "content": "dist/\nnode_modules/\ntypings/\n"
  },
  {
    "path": ".eslintrc.cjs",
    "content": "module.exports = {\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    project: true,\n    tsconfigRootDir: __dirname,\n  },\n  extends: [\n    'plugin:rexskz/default',\n  ],\n  globals: {\n    __VERSION__: 'readonly',\n  },\n};\n"
  },
  {
    "path": ".github/workflows/pages.yml",
    "content": "name: GitHub Pages\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v1\n      - run: |\n          git config user.name \"Rex Zeng\"\n          git config user.email \"rex@rexskz.info\"\n          git checkout -b gh-pages\n          npm install -g pnpm\n          pnpm i\n          pnpm build:pages\n          echo \"json-diff-kit.js.org\" > docs/CNAME\n          git add docs -f\n          git commit -m Pages\n          git remote set-url origin \"https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git\"\n          git push -f origin gh-pages\n        env:\n          GITHUB_TOKEN: ${{ secrets.github_token }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Unit Test\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v1\n      - run: |\n          npm install -g pnpm\n          pnpm i\n          pnpm test\n          curl -Os https://uploader.codecov.io/latest/linux/codecov\n          chmod +x codecov\n          ./codecov\n        env:\n          GITHUB_TOKEN: ${{ secrets.github_token }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\ncodecov\n.vscode\ndocs/\n"
  },
  {
    "path": ".npmignore",
    "content": ".cache\n.editorconfig\n.eslintrc.cjs\n.eslintignore\n.github\n.gitignore\n.npmignore\n.npmrc\n.stylelintrc.js\n.swcrc\n.travis.yml\ncoverage\ndocs\nesbuild.mjs\njest.config.js\nnode_modules\nplayground\npnpm-lock.yaml\npreview.png\npreview-cli.png\nrollup.*.ts\nsrc\ntsconfig.json\ntsconfig.*.json\n"
  },
  {
    "path": ".npmrc",
    "content": "use-lockfile-v6 = true\n"
  },
  {
    "path": ".stylelintrc.js",
    "content": "module.exports = {\n  extends: 'stylelint-plugin-rexskz',\n};\n"
  },
  {
    "path": ".swcrc",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/swcrc\",\n  \"jsc\": {\n    \"parser\": {\n      \"syntax\": \"typescript\"\n    },\n    \"target\": \"es2020\",\n    \"loose\": true,\n    \"keepClassNames\": true\n  },\n  \"minify\": false\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Rex Zeng\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# JSON Diff Kit\n\n[![NPM version][npm-image]][npm-url]\n[![Downloads][download-badge]][npm-url]\n[![Codecov](https://codecov.io/gh/RexSkz/json-diff-kit/branch/main/graph/badge.svg?token=8YRG3M4WTO)](https://codecov.io/gh/RexSkz/json-diff-kit)\n\nA better JSON differ & viewer library written in TypeScript. [Try it out in the playground!](https://json-diff-kit.js.org/)\n\n## Install\n\nYou can install `json-diff-kit` via various package managers.\n\n```sh\n# using npm\nnpm i json-diff-kit --save\n\n# using yarn\nyarn add json-diff-kit\n\n# using pnpm\npnpm add json-diff-kit\n```\n\n## Quick Start\n\nTo generate the diff data:\n\n```ts\nimport { Differ } from 'json-diff-kit';\n// or if you are using vue, you can import the differ only\nimport Differ from 'json-diff-kit/dist/differ';\n\n// the two JS objects\nconst before = {\n  a: 1,\n  b: 2,\n  d: [1, 5, 4],\n  e: ['1', 2, { f: 3, g: null, h: [5], i: [] }, 9],\n  m: [],\n  q: 'JSON diff can\\'t be possible',\n  r: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',\n  s: 1024,\n};\nconst after = {\n  b: 2,\n  c: 3,\n  d: [1, 3, 4, 6],\n  e: ['1', 2, 3, { f: 4, g: false, i: [7, 8] }, 10],\n  j: { k: 11, l: 12 },\n  m: [\n    { n: 1, o: 2 },\n    { p: 3 },\n  ],\n  q: 'JSON diff is possible!',\n  r: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed quasi architecto beatae incididunt ut labore et dolore magna aliqua.',\n  s: '1024',\n};\n\n// all configs are optional\nconst differ = new Differ({\n  detectCircular: true,    // default `true`\n  maxDepth: Infinity,      // default `Infinity`\n  showModifications: true, // default `true`\n  arrayDiffMethod: 'lcs',  // default `\"normal\"`, but `\"lcs\"` may be more useful\n});\n\n// you may want to use `useMemo` (for React) or `computed` (for Vue)\n// to avoid redundant computations\nconst diff = differ.diff(before, after);\nconsole.log(diff);\n```\n\nYou can use your own component to visualize the `diff` data, or use the built-in viewer:\n\n```tsx\nimport { Viewer } from 'json-diff-kit';\nimport type { DiffResult } from 'json-diff-kit';\n\nimport 'json-diff-kit/dist/viewer.css';\n\ninterface PageProps {\n  diff: [DiffResult[], DiffResult[]];\n}\n\nconst Page: React.FC<PageProps> = props => {\n  return (\n    <Viewer\n      diff={props.diff}          // required\n      indent={4}                 // default `2`\n      lineNumbers={true}         // default `false`\n      highlightInlineDiff={true} // default `false`\n      inlineDiffOptions={{\n        mode: 'word',            // default `\"char\"`, but `\"word\"` may be more useful\n        wordSeparator: ' ',      // default `\"\"`, but `\" \"` is more useful for sentences\n      }}\n    />\n  );\n};\n```\n\nThe result is here:\n\n![The result (using LCS array diff method).](./preview.png)\n\n## Other Version of Viewer\n\nHere is an experimental [Vue version](https://github.com/RexSkz/json-diff-kit-vue) of the `Viewer` component.\n\n## More Complex Usages\n\nPlease check the [playground page](https://json-diff-kit.js.org/), where you can adjust nearly all parameters and see the result.\n\n## CLI Tool\n\nYou can use the CLI tool to generate the diff data from two JSON files. Please install the package `terminal-kit` before using it.\n\n```bash\npnpm add terminal-kit # or make sure it's already installed in your project\n\n# Compare two JSON files, output the diff data to the terminal.\n# You can navigate it using keyboard like `less`.\njsondiff run path/to/before.json path/to/after.json\n\n# Output the diff data to a file.\n# Notice there will be no side-by-side view since it's not a TTY.\njsondiff run path/to/before.json path/to/after.json -o path/to/result.diff\n\n# Use a custom configuration file and output the diff data to a file.\njsondiff run path/to/before.json path/to/after.json -c path/to/config.json -o path/to/result.diff\n\n# Print the help message.\njsondiff --help\njsondiff run --help\n```\n\n![A screenshot when using CLI.](./preview-cli.png)\n\n## Algorithm Details\n\nPlease refer to the article [JSON Diff Kit: A Combination of Several Simple Algorithms](https://blog.rexskz.info/json-diff-kit-a-combination-of-several-simple-algorithms.html?cc_lang=en).\n\n## Features & Roadmap\n\n- [x] Provide a `Differ` class and a `Viewer` component\n- [x] Merge \"remove & add\" at the same place as a modification\n- [x] Support inline diffing by word instead of by character\n- [x] Generate code directly in the demo page (covered by playground)\n- [x] Optimise `Viewer` performance by adding virtual scrolling\n- [x] Add CLI tool\n- [x] Provide a Vue version of `Viewer`\n- [ ] Improve unit tests\n\n## License\n\nMIT\n\n[npm-url]: https://npmjs.org/package/json-diff-kit\n[npm-image]: https://img.shields.io/npm/v/json-diff-kit.svg\n\n[download-badge]: https://img.shields.io/npm/dm/json-diff-kit.svg\n"
  },
  {
    "path": "bin/examples/after.json",
    "content": "{\n  \"b\": 2,\n  \"c\": 3,\n  \"d\": [\n    1,\n    3,\n    4,\n    6\n  ],\n  \"e\": [\n    \"1\",\n    2,\n    3,\n    {\n      \"f\": 4,\n      \"g\": false,\n      \"i\": [\n        7,\n        8\n      ]\n    },\n    10\n  ],\n  \"j\": {\n    \"k\": 11,\n    \"l\": 12\n  },\n  \"m\": [\n    {\n      \"n\": 1,\n      \"o\": 2\n    },\n    {\n      \"p\": 3\n    }\n  ],\n  \"q\": \"JSON diff is possible!\",\n  \"r\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed quasi architecto beatae incididunt ut labore et dolore magna aliqua.\",\n  \"s\": \"1024\"\n}\n"
  },
  {
    "path": "bin/examples/before.json",
    "content": "{\n  \"a\": 1,\n  \"b\": 2,\n  \"d\": [\n    1,\n    5,\n    4\n  ],\n  \"e\": [\n    \"1\",\n    2,\n    {\n      \"f\": 3,\n      \"g\": null,\n      \"h\": [\n        5\n      ],\n      \"i\": []\n    },\n    9\n  ],\n  \"m\": [],\n  \"q\": \"JSON diff can't be possible\",\n  \"r\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\",\n  \"s\": 1024\n}\n"
  },
  {
    "path": "bin/examples/output.diff",
    "content": "  {\n-   \"a\": 1,\n    \"b\": 2,\n+   \"c\": 3,\n    \"d\": [\n      1,\n-     5,\n+     3,\n      4\n+     6\n    ],\n    \"e\": [\n      \"1\",\n      2,\n+     3,\n      {\n-       \"f\": 3,\n-       \"g\": null,\n-       \"h\": [\n-         5\n-       ],\n+       \"f\": 4,\n+       \"g\": false,\n        \"i\": [\n+         7,\n+         8\n        ]\n      },\n-     9\n+     10\n    ],\n+   \"j\": {\n+     \"k\": 11,\n+     \"l\": 12\n+   },\n    \"m\": [\n+     {\n+       \"n\": 1,\n+       \"o\": 2\n+     },\n+     {\n+       \"p\": 3\n+     }\n    ],\n-   \"q\": \"JSON diff can't be possible\",\n-   \"r\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\",\n-   \"s\": 1024\n+   \"q\": \"JSON diff is possible!\",\n+   \"r\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed quasi architecto beatae incididunt ut labore et dolore magna aliqua.\",\n+   \"s\": \"1024\"\n  }"
  },
  {
    "path": "bin/jsondiff.cjs",
    "content": "#!/usr/bin/env node\n\nrequire('../dist/cjs/cli/index.js')\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  transform: {\n    '^.+\\\\.(t|j)sx?$': [\n      '@swc/jest',\n    ],\n  },\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"json-diff-kit\",\n  \"version\": \"1.0.35\",\n  \"description\": \"A better JSON differ & viewer.\",\n  \"main\": \"dist/cjs/index.js\",\n  \"module\": \"dist/index.js\",\n  \"typings\": \"typings\",\n  \"bin\": {\n    \"jsondiff\": \"bin/jsondiff.cjs\"\n  },\n  \"sideEffects\": [\n    \"*.css\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/cjs/index.js\",\n      \"types\": \"./typings/index.d.ts\"\n    },\n    \"./*\": {\n      \"import\": \"./dist/*\",\n      \"require\": \"./dist/cjs/*\",\n      \"types\": \"./typings/*.d.ts\"\n    },\n    \"./dist/*\": {\n      \"import\": \"./dist/*\",\n      \"require\": \"./dist/cjs/*\",\n      \"types\": \"./typings/*.d.ts\"\n    }\n  },\n  \"scripts\": {\n    \"start\": \"cross-env rollup -c rollup.config.pages.mjs -w\",\n    \"dev\": \"cross-env pnpm start\",\n    \"lint:eslint\": \"eslint ./{src,playground}/**/*.{ts,tsx} --quiet\",\n    \"lint:stylelint\": \"stylelint '**/*.{css,less}' --fix\",\n    \"test\": \"cross-env jest --coverage\",\n    \"build\": \"cross-env pnpm build:ts && pnpm build:less && pnpm build:typings\",\n    \"build:ts\": \"cross-env rollup -c && rollup -c rollup.config.cli.mjs\",\n    \"build:typings\": \"cross-env tsc -p tsconfig.build.json\",\n    \"build:less\": \"cross-env lessc src/viewer.less dist/viewer.css && lessc src/viewer-monokai.less dist/viewer-monokai.css\",\n    \"build:pages\": \"cross-env NODE_ENV=production BASEDIR=docs rollup -c rollup.config.pages.mjs\",\n    \"prepublish\": \"cross-env pnpm build\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/RexSkz/json-diff-kit.git\"\n  },\n  \"keywords\": [\n    \"json\",\n    \"diff\",\n    \"view\",\n    \"kit\"\n  ],\n  \"author\": \"Rex Zeng <rex@rexskz.info>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/RexSkz/json-diff-kit/issues\"\n  },\n  \"homepage\": \"https://github.com/RexSkz/json-diff-kit#readme\",\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^21.0.1\",\n    \"@rollup/plugin-html\": \"^0.2.4\",\n    \"@rollup/plugin-node-resolve\": \"^13.1.3\",\n    \"@rollup/plugin-replace\": \"^5.0.2\",\n    \"@rollup/plugin-swc\": \"^0.4.0\",\n    \"@swc/cli\": \"^0.5.2\",\n    \"@swc/core\": \"^1.10.1\",\n    \"@swc/jest\": \"^0.2.37\",\n    \"@types/jest\": \"^29.5.14\",\n    \"@types/lodash\": \"^4.14.191\",\n    \"@types/node\": \"^20.11.16\",\n    \"@types/prismjs\": \"^1.26.0\",\n    \"@types/react\": \"^17.0.38\",\n    \"@types/react-dom\": \"^17.0.11\",\n    \"@types/terminal-kit\": \"^2.5.6\",\n    \"cross-env\": \"^7.0.3\",\n    \"eslint\": \"^8\",\n    \"eslint-plugin-rexskz\": \"1.0.0\",\n    \"fork-me-on-github\": \"^1.0.6\",\n    \"jest\": \"^29.7.0\",\n    \"less\": \"^4.1.3\",\n    \"prismjs\": \"^1.29.0\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"rollup\": \"^4.28.1\",\n    \"rollup-plugin-less\": \"^1.1.3\",\n    \"rollup-plugin-livereload\": \"^2.0.5\",\n    \"rollup-plugin-serve\": \"^1.1.1\",\n    \"rollup-plugin-styles\": \"^4.0.0\",\n    \"stylelint\": \"^15\",\n    \"stylelint-plugin-rexskz\": \"1.0.0-alpha.3\",\n    \"typescript\": \"^4.5.5\"\n  },\n  \"dependencies\": {\n    \"commander\": \"^11.1.0\",\n    \"fast-myers-diff\": \"^3.0.1\",\n    \"lodash\": \"^4.17.21\",\n    \"prompts\": \"^2.4.2\"\n  },\n  \"optionalDependencies\": {\n    \"terminal-kit\": \"^3.0.1\"\n  },\n  \"packageManager\": \"pnpm@8.15.7+sha256.50783dd0fa303852de2dd1557cd4b9f07cb5b018154a6e76d0f40635d6cee019\"\n}\n"
  },
  {
    "path": "playground/docs.less",
    "content": ".demo-root {\n  position: relative;\n  width: 100%;\n  max-width: 1200px;\n  box-sizing: border-box;\n  padding: 1em 2em;\n  margin: auto;\n  background: #fff;\n  box-shadow: 0 0 30px rgba(0, 0, 0, 0.01);\n\n  .statistics {\n    img {\n      margin-right: 8px;\n    }\n  }\n\n  .banner {\n    display: inline-block;\n    padding: 8px 16px;\n    border-radius: 4px;\n    background: #000;\n    color: #fff;\n    cursor: pointer;\n\n    &:hover {\n      background: #333;\n      text-decoration: underline;\n    }\n  }\n\n  blockquote {\n    margin: 0 0 1em;\n    line-height: 24px;\n  }\n\n  .diff-config,\n  .view-config {\n    form {\n      overflow: hidden;\n\n      & > label {\n        display: flex;\n        align-items: center;\n        padding: 0 4px;\n        border-radius: 4px;\n        margin-right: 8px;\n        background: #e5e6e9;\n        font-weight: 700;\n        user-select: none;\n\n        span {\n          margin-left: 1em;\n          font-weight: 400;\n        }\n\n        input,\n        select {\n          margin-left: 8px;\n        }\n\n        input[type=\"number\"] {\n          min-width: 8em;\n        }\n      }\n    }\n  }\n\n  .diff-result .json-diff-viewer {\n    box-sizing: border-box;\n    padding: 1em;\n    border: 1px solid;\n    border-radius: 4px;\n    margin-top: 1em;\n  }\n\n  .demo-footer {\n    padding: 2em 0;\n    border-top: 1px dashed;\n    margin: 4em 0 0;\n    text-align: center;\n  }\n}\n"
  },
  {
    "path": "playground/docs.tsx",
    "content": "/* eslint-disable max-len, react/no-unescaped-entities */\n\nimport React from 'react';\nimport _ForkMeOnGithub from 'fork-me-on-github';\n\nimport { Differ, Viewer } from '../src';\nimport type { DifferOptions } from '../src/differ';\nimport { InlineDiffOptions } from '../src/utils/get-inline-diff';\nimport type { ViewerProps } from '../src/viewer';\n\nimport './docs.less';\nimport { updateInitialValues } from './initial-values';\n\ninterface PropTypes {\n  onSwitch: () => void;\n}\n\nconst ForkMeOnGithub = _ForkMeOnGithub.default;\n\nconst Docs: React.FC<PropTypes> = props => {\n  // differ props\n  const [detectCircular] = React.useState(true);\n  const [maxDepth, setMaxDepth] = React.useState(Infinity);\n  const [showModifications, setShowModifications] = React.useState(true);\n  const [arrayDiffMethod, setArrayDiffMethod] = React.useState<DifferOptions['arrayDiffMethod']>('lcs');\n  const [ignoreCase, setIgnoreCase] = React.useState(false);\n  const [recursiveEqual, setRecursiveEqual] = React.useState(true);\n  const [preserveKeyOrder, setPreserveKeyOrder] = React.useState<DifferOptions['preserveKeyOrder']>(undefined);\n  const [compareKey, setCompareKey] = React.useState<string>('');\n\n  // viewer props\n  const [indent, setIndent] = React.useState(4);\n  const [lineNumbers, setLineNumbers] = React.useState(true);\n  const [highlightInlineDiff, setHighlightInlineDiff] = React.useState(true);\n  const [inlineDiffMode, setInlineDiffMode] = React.useState<InlineDiffOptions['mode']>('word');\n  const [inlineDiffSeparator, setInlineDiffSeparator] = React.useState(' ');\n  const [hideUnchangedLines, setHideUnchangedLines] = React.useState(true);\n  const [syntaxHighlight, setSyntaxHighlight] = React.useState(true);\n  const [useVirtual, setUseVirtual] = React.useState(false);\n\n  const differOptions = React.useMemo(() => ({\n    detectCircular,\n    maxDepth,\n    showModifications,\n    arrayDiffMethod,\n    ignoreCase,\n    recursiveEqual,\n    preserveKeyOrder,\n    compareKey,\n  }), [\n    detectCircular,\n    maxDepth,\n    showModifications,\n    arrayDiffMethod,\n    ignoreCase,\n    recursiveEqual,\n    preserveKeyOrder,\n    compareKey,\n  ]);\n  const differ = React.useMemo(() => new Differ(differOptions), [differOptions]);\n\n  const [before1] = React.useState({\n    a: 1,\n    b: 2,\n    d: [1, 5, 4],\n    e: ['1', 2, { f: 3, g: null, h: [5], i: [] }, 9],\n    m: [],\n    q: 'JSON diff can\\'t be possible',\n    r: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',\n    s: 1024,\n  });\n  const [after1] = React.useState({\n    b: 2,\n    c: 3,\n    d: [1, 3, 4, 6],\n    e: ['1', 2, 3, { f: 4, g: false, i: [7, 8] }, 10],\n    j: { k: 11, l: 12 },\n    m: [\n      { n: 1, o: 2 },\n      { p: 3 },\n    ],\n    q: 'JSON diff is possible!',\n    r: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed quasi architecto beatae incididunt ut labore et dolore magna aliqua.',\n    s: '1024',\n  });\n  const diff1 = React.useMemo(() => differ.diff(before1, after1), [differ, before1, after1]);\n\n  const [before2] = React.useState([2, 4, 3]);\n  const [after2] = React.useState([2, 5, 4, 3, 1]);\n  const diff2 = React.useMemo(() => differ.diff(before2, after2), [differ, before2, after2]);\n\n  const [before3] = React.useState({ a: 1, b: [2] });\n  const [after3] = React.useState(666);\n  const diff3 = React.useMemo(() => differ.diff(before3, after3), [differ, before3, after3]);\n\n  const [before4] = React.useState(233);\n  const [after4] = React.useState(666);\n  const diff4 = React.useMemo(() => differ.diff(before4, after4), [differ, before4, after4]);\n\n  const [before5] = React.useState([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]);\n  const [after5] = React.useState([0, 1, 2, 3, 4, 5, 6, 7, 8, 99, 10, 11, 12, 13, 14, 15, 16, 17, 1818, 1919, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]);\n  const diff5 = React.useMemo(() => differ.diff(before5, after5), [differ, before5, after5]);\n\n  const [before6] = React.useState([\n    { text: 'hello world' },\n  ]);\n  const [after6] = React.useState([\n    { text: 'above' },\n    { text: 'hello world' },\n    { text: 'below' },\n  ]);\n  const diff6 = React.useMemo(() => differ.diff(before6, after6), [differ, before6, after6]);\n\n  const [before7] = React.useState({\n    a: undefined,\n    b: 123n,\n    c: {\n      c1: Symbol('foo'),\n      c2: Symbol('bar'),\n    },\n    d: () => alert(1),\n    e: Infinity,\n    f: NaN,\n    h: [undefined, 123n, Symbol('foo'), Symbol('bar'), () => alert(1), Infinity, NaN, -Infinity],\n  });\n  const [after7] = React.useState({\n    a: undefined,\n    b: 123n,\n    c: {\n      c1: Symbol('foo'),\n      c3: Symbol('baz'),\n    },\n    d: () => alert(2),\n    e: Infinity,\n    f: NaN,\n    g: -Infinity,\n    h: [undefined, 123n, Symbol('foo'), Symbol('baz'), () => alert(2), Infinity, NaN, -Infinity],\n  });\n  const diff7 = React.useMemo(() => differ.diff(before7, after7), [differ, before7, after7]);\n\n  const openInPlayground = (l: unknown, r: unknown) => {\n    updateInitialValues(JSON.stringify(l, null, 2), JSON.stringify(r, null, 2));\n    props.onSwitch();\n  };\n\n  const viewerCommonProps: Partial<ViewerProps> = {\n    indent,\n    lineNumbers,\n    highlightInlineDiff,\n    inlineDiffOptions: {\n      mode: inlineDiffMode,\n      wordSeparator: inlineDiffSeparator || '',\n    },\n    hideUnchangedLines,\n    syntaxHighlight: syntaxHighlight ? { theme: 'monokai' } : false,\n    virtual: useVirtual ? {} : false,\n  };\n\n  return (\n    <div className=\"demo-root\">\n      <h1>JSON Diff Kit</h1>\n      <div className=\"statistics\">\n        <img src=\"https://img.shields.io/npm/v/json-diff-kit.svg\" />\n        <img src=\"https://img.shields.io/npm/dm/json-diff-kit.svg\" />\n        <img src=\"https://codecov.io/gh/RexSkz/json-diff-kit/branch/main/graph/badge.svg?token=8YRG3M4WTO\" />\n        <iframe\n          src=\"https://ghbtns.com/github-btn.html?user=rexskz&repo=json-diff-kit&type=star&count=true\"\n          frameBorder=\"0\"\n          scrolling=\"0\"\n          width=\"150\"\n          height=\"20\"\n          title=\"GitHub\"\n        />\n      </div>\n      <p>A better JSON differ & viewer library written in TypeScript.</p>\n      <div className=\"banner\" onClick={props.onSwitch}>\n        Click to try out the playground!\n      </div>\n      <h2>Differ Configuration</h2>\n      <div className=\"diff-config\">\n        <form>\n          <label htmlFor=\"detect-circular\">\n            Detect circular references:\n            <input\n              type=\"checkbox\"\n              id=\"detect-circular\"\n              checked={detectCircular}\n              disabled\n            />\n          </label>\n          <blockquote>Whether to detect circular reference in source objects before diff starts. Default is <code>true</code>. If you are confident about your data (maybe it's from <code>JSON.parse</code> or an API response), you can set it to <code>false</code> to improve performance, but the algorithm may not stop if circular reference does show up.</blockquote>\n          <label htmlFor=\"max-depth\">\n            Max depth:\n            <input\n              type=\"number\"\n              id=\"max-depth\"\n              value={maxDepth === Infinity ? undefined : maxDepth}\n              onChange={e => setMaxDepth(Number(e.target.value))}\n              min={0}\n              max={10}\n              step={1}\n              disabled={maxDepth === Infinity}\n              style={{ width: '3em' }}\n            />\n            <label htmlFor=\"max-depth-infinity\" style={{ margin: '0 0 0 4px' }}>\n              (\n              <input\n                type=\"checkbox\"\n                id=\"max-depth-infinity\"\n                checked={maxDepth === Infinity}\n                onChange={e => setMaxDepth(e.target.checked ? Infinity : 0)}\n              />\n              infinity)\n            </label>\n          </label>\n          <blockquote>Max depth, default <code>Infinity</code> means no depth limit.</blockquote>\n          <label htmlFor=\"show-modifications\">\n            Show modifications:\n            <input\n              type=\"checkbox\"\n              id=\"show-modifications\"\n              checked={showModifications}\n              onChange={e => setShowModifications(e.target.checked)}\n            />\n          </label>\n          <blockquote>Support recognizing modifications, default <code>true</code> means the differ will output the <code>* modified</code> sign apart from the basic <code>+ add</code> and <code>- remove</code> sign. If you prefer Git-style output, please set it to <code>false</code>.</blockquote>\n          <label htmlFor=\"array-diff-method\">\n            Array diff method:\n            <select\n              id=\"array-diff-method\"\n              value={arrayDiffMethod}\n              onChange={e => setArrayDiffMethod(e.target.value as DifferOptions['arrayDiffMethod'])}\n            >\n              <option value=\"normal\">normal (default)</option>\n              <option value=\"lcs\">lcs</option>\n              <option value=\"unorder-normal\">unorder-normal</option>\n              <option value=\"unorder-lcs\">unorder-lcs</option>\n              <option value=\"compare-key\">compare-key</option>\n            </select>\n          </label>\n          <blockquote>The way to diff arrays, default is <code>\"normal\"</code>, using <code>\"lcs\"</code> may get a better result but much slower. <code>\"unorder-normal\"</code> and <code>\"unorder-lcs\"</code> are for unordered arrays (the order of elements in the array doesn't matter). <code>\"compare-key\"</code> matches objects by a specific key property.</blockquote>\n          <label htmlFor=\"ignore-case\">\n            Ignore case:\n            <input\n              type=\"checkbox\"\n              id=\"ignore-case\"\n              checked={ignoreCase}\n              onChange={e => setIgnoreCase(e.target.checked)}\n            />\n          </label>\n          <blockquote>Whether to ignore case when comparing string values.</blockquote>\n          <label htmlFor=\"recursive-equal\">\n            Recursive equal:\n            <input\n              type=\"checkbox\"\n              id=\"recursive-equal\"\n              checked={recursiveEqual}\n              onChange={e => setRecursiveEqual(e.target.checked)}\n            />\n          </label>\n          <blockquote>Whether to use recursive equal to compare objects. This can provide a better output when there are unchanged object items in an array, but it may cause performance issues when the data is very large.</blockquote>\n          <label htmlFor=\"preserve-key-order\">\n            Preserve key order:\n            <select\n              id=\"preserve-key-order\"\n              value={preserveKeyOrder}\n              onChange={e => setPreserveKeyOrder((e.target.value === 'sort' ? undefined : e.target.value) as DifferOptions['preserveKeyOrder'])}\n            >\n              <option value=\"sort\">sort (default)</option>\n              <option value=\"before\">by \"before\"</option>\n              <option value=\"after\">by \"after\"</option>\n            </select>\n          </label>\n          <blockquote>Sometimes you do not want the keys in result be sorted, for example <code>start_time</code> and <code>end_time</code> will be swapped by default. You can set this option to let the differ preserve the key order according to <code>before</code> or <code>after</code>.</blockquote>\n        </form>\n      </div>\n      <h2>Viewer Configuration</h2>\n      <div className=\"view-config\">\n        <form>\n          <label htmlFor=\"indent\">\n            Indent:\n            <input\n              type=\"number\"\n              id=\"indent\"\n              min={1}\n              max={16}\n              value={indent}\n              onChange={e => setIndent(Number(e.target.value))}\n            />\n          </label>\n          <blockquote>Controls the indent in the <code>&lt;Viewer&gt;</code> component.</blockquote>\n          <label htmlFor=\"line-numbers\">\n            Line numbers:\n            <input\n              type=\"checkbox\"\n              id=\"line-numbers\"\n              checked={lineNumbers}\n              onChange={e => setLineNumbers(e.target.checked)}\n            />\n          </label>\n          <blockquote>Whether to show line numbers.</blockquote>\n          <label htmlFor=\"highlight-inline-diff\">\n            Highlight inline diff:\n            <input\n              type=\"checkbox\"\n              id=\"highlight-inline-diff\"\n              checked={highlightInlineDiff}\n              onChange={e => setHighlightInlineDiff(e.target.checked)}\n            />\n          </label>\n          <blockquote>Whether to show the inline diff highlight. For example, if the left value <code>\"JSON diff can't be possible\"</code> is changed to the right value <code>\"JSON diff is possible\"</code>, it will be recognized as we first remove <code>can't be</code> and then add <code>is</code>. This feature is powered by <a href=\"https://github.com/gliese1337/fast-myers-diff\" target=\"_blank\" rel=\"noreferrer\">gliese1337/fast-myers-diff</a>. Note: the <code>showModification</code> must be enabled, or you will not see modified lines.</blockquote>\n          <label htmlFor=\"inline-diff-options\">\n            Inline diff options:\n            <span>Diff method</span>\n            <select\n              id=\"inline-diff-mode\"\n              value={inlineDiffMode}\n              onChange={e => setInlineDiffMode(e.target.value as InlineDiffOptions['mode'])}\n            >\n              <option value=\"char\">char (default)</option>\n              <option value=\"word\">word</option>\n            </select>\n            <span>Word separator</span>\n            <input\n              id=\"inline-diff-separator\"\n              value={inlineDiffSeparator}\n              onChange={e => setInlineDiffSeparator(e.target.value)}\n              placeholder=\"Works when mode = char\"\n            />\n          </label>\n          <blockquote>You can control the inline diff behaviour. If the inline diff sources are sentences, we can diff them \"by word\" instead of \"by character\". For normal sentences, just set the method to <code>word</code> and the separator to <code>\" \"</code> (a half-width space) like this demo. But if you prefer the Git-style output, you can leave this props default, which is diffing \"by character\".</blockquote>\n          <label htmlFor=\"hide-unchanged-lines\">\n            Hide unchanged lines:\n            <input\n              type=\"checkbox\"\n              id=\"hide-unchanged-lines\"\n              checked={hideUnchangedLines}\n              onChange={e => setHideUnchangedLines(e.target.checked)}\n            />\n          </label>\n          <blockquote>Whether to hide the unchanged lines (like what GitHub and GitLab does).</blockquote>\n          <label htmlFor=\"syntax-highlight\">\n            Syntax highlight:\n            <input\n              type=\"checkbox\"\n              id=\"syntax-highlight\"\n              checked={syntaxHighlight}\n              onChange={e => setSyntaxHighlight(e.target.checked)}\n            />\n          </label>\n          <blockquote>Support syntax highlight. The viewer component will render like prismjs, and you can write your own style. Please don't forget to import the corresponding CSS file, e.g. <code>import 'json-diff-kit/viewer-monokai.less';</code></blockquote>\n          <label htmlFor=\"use-virtual\">\n            Use virtual:\n            <input\n              type=\"checkbox\"\n              id=\"use-virtual\"\n              checked={useVirtual}\n              onChange={e => setUseVirtual(e.target.checked)}\n            />\n          </label>\n          <blockquote>Whether to use virtual list to render the diff. This can improve the performance when the diff result is very large.</blockquote>\n        </form>\n      </div>\n      <div className=\"diff-result\">\n        <h2>Examples</h2>\n        <p>\n          <button onClick={() => openInPlayground(before1, after1)}>⬇️ Playground</button> An regular example with 2 objects.\n        </p>\n        <Viewer diff={diff1} {...viewerCommonProps} />\n        <p>\n          <button onClick={() => openInPlayground(before2, after2)}>⬇️ Playground</button> An example with 2 arrays.\n        </p>\n        <Viewer diff={diff2} {...viewerCommonProps} />\n        <p>\n          <button onClick={() => openInPlayground(before3, after3)}>⬇️ Playground</button> 2 variables with different types. The algorithm always returns the result \"left is removed, right is added\".\n        </p>\n        <Viewer diff={diff3} {...viewerCommonProps} />\n        <p>\n          <button onClick={() => openInPlayground(before4, after4)}>⬇️ Playground</button> 2 variables with the same primitive type. The algorithm always returns the result \"left is modified to right\" (if <code>showModification</code> is set to <code>false</code>, it will return the result \"left is removed, right is added\").\n        </p>\n        <Viewer diff={diff4} {...viewerCommonProps} />\n        <p>\n          <button onClick={() => openInPlayground(before5, after5)}>⬇️ Playground</button> Most of the lines are equal, only small amount of lines are different. You can use the <code>hideUnchangedLines</code> prop to hide the unchanged parts and make the result shorter. Notice: when the <code>diff</code> prop is changed, the expand status will be reset, it's your responsibility to keep the <code>diff</code> props unchanged (you may want to use <code>useState</code> or <code>useMemo</code>).\n        </p>\n        <Viewer diff={diff5} {...viewerCommonProps} />\n        <p>\n          <button onClick={() => openInPlayground(before6, after6)}>⬇️ Playground</button> An example for the recursive equal. If the differ option <code>recursiveEqual</code> is set to <code>true</code>, the object items should be treated as equal.\n        </p>\n        <Viewer diff={diff6} {...viewerCommonProps} />\n        <p>\n          <button disabled>⚠️ Playground not available</button> Sometimes there may be values that can't be serialized to JSON, like <code>undefined</code>, <code>BigInt</code>, <code>Symbol</code>, <code>function</code>, <code>Infinity</code>, <code>-Infinity</code> and <code>NaN</code>. The differ and viewer can handle them correctly.\n        </p>\n        <Viewer diff={diff7} {...viewerCommonProps} />\n      </div>\n      <div className=\"demo-footer\">\n        <p>Made with ♥ by Rex Zeng</p>\n      </div>\n      <ForkMeOnGithub repo=\"https://github.com/rexskz/json-diff-kit\" />\n    </div>\n  );\n};\n\nexport default Docs;\n"
  },
  {
    "path": "playground/generated-code.tsx",
    "content": "import React from 'react';\n\nimport Prism from 'prismjs';\nimport 'prismjs/components/prism-typescript';\nimport 'prismjs/components/prism-jsx';\nimport 'prismjs/components/prism-tsx';\nimport 'prismjs/themes/prism.css';\n\ninterface PropTypes {\n  code: string;\n}\n\nconst GeneratedCode: React.FC<PropTypes> = ({ code }) => {\n  const html = React.useMemo(() => {\n    return Prism.highlight(code, Prism.languages.tsx, 'tsx');\n  }, [code]);\n\n  return (\n    <pre className=\"language-tsx\" dangerouslySetInnerHTML={{ __html: html }} />\n  );\n};\n\nexport default GeneratedCode;\n"
  },
  {
    "path": "playground/index.less",
    "content": "html,\nbody {\n  padding: 0;\n  margin: 0;\n  background: #f2f4f6;\n  font-family: sans-serif;\n  font-size: 14px;\n}\n\ncode {\n  padding: 0 4px;\n  background: #f9f2f4;\n  color: #c7254e;\n}\n\na,\na:visited {\n  color: #2196f3;\n}\n\n[disabled] {\n  cursor: not-allowed;\n}\n"
  },
  {
    "path": "playground/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport Playground from './playground';\nimport Docs from './docs';\n\nimport '../src/viewer.less';\nimport './index.less';\n\nconst Index: React.FC = () => {\n  const [usePlayground, setUsePlayground] = React.useState(true);\n\n  return usePlayground\n    ? <Playground onSwitch={() => setUsePlayground(false)} />\n    : <Docs onSwitch={() => setUsePlayground(true)} />;\n};\n\nReactDOM.render(<React.StrictMode><Index /></React.StrictMode>, document.getElementById('root'));\n"
  },
  {
    "path": "playground/initial-values.ts",
    "content": "import React from 'react';\n\nconst getValue = () => {\n  const hash = window.location.hash ? window.location.hash.slice(1) : '';\n  const query = new URLSearchParams(hash);\n  return {\n    l: query.get('l') || '',\n    r: query.get('r') || '',\n  };\n};\n\nexport const useInitialValues = () => {\n  const [initialValues, setInitialValues] = React.useState(getValue());\n\n  React.useEffect(() => {\n    const hashChange = () => {\n      const newValue = getValue();\n      if (initialValues.l !== newValue.l || initialValues.r !== newValue.r) {\n        setInitialValues(newValue);\n      }\n    };\n    window.addEventListener('hashchange', hashChange);\n    return () => {\n      window.removeEventListener('hashchange', hashChange);\n    };\n  }, []);\n\n  return initialValues;\n};\n\nexport const updateInitialValues = (l: string, r: string) => {\n  const hash = window.location.hash ? window.location.hash.slice(1) : '';\n  const query = new URLSearchParams(hash);\n  query.set('l', l);\n  query.set('r', r);\n  window.location.hash = query.toString();\n};\n"
  },
  {
    "path": "playground/js-stringify.ts",
    "content": "const jsStringify = (obj: any) => {\n  const code = JSON.stringify(obj, null, 2);\n  return code\n    .replace(/^(\\s+)\"([^\"]+)\":/gm, '$1$2:')\n    .replace(/: \"([^'\"]+)\"(,?)\\n/g, ': \\'$1\\'$2\\n');\n};\n\nexport default jsStringify;\n"
  },
  {
    "path": "playground/label.less",
    "content": ".label {\n  &-wrapper {\n    position: relative;\n    margin-right: 4px;\n  }\n\n  &-question-mark {\n    display: inline-flex;\n    width: 16px;\n    height: 16px;\n    box-sizing: border-box;\n    align-items: center;\n    justify-content: center;\n    border: 1px solid;\n    border-radius: 50%;\n    cursor: pointer;\n    font-size: 12px;\n  }\n\n  &-tip {\n    display: none;\n  }\n\n  &-wrapper:hover &-question-mark {\n    border-color: #000;\n    background: #000;\n    color: #fff;\n  }\n\n  &-wrapper:hover &-tip {\n    position: absolute;\n    z-index: 1;\n    top: 100%;\n    display: block;\n    width: 268px;\n    box-sizing: border-box;\n    padding: 8px 16px;\n    border-radius: 4px;\n    background-color: #fff;\n    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);\n    color: #333;\n    font-size: 12px;\n    line-height: 1.5;\n  }\n}\n"
  },
  {
    "path": "playground/label.tsx",
    "content": "import React from 'react';\n\nimport './label.less';\n\ninterface PropTypes {\n  title: React.ReactNode;\n  tip?: React.ReactNode;\n}\n\nconst Label: React.FC<PropTypes> = ({ title, tip }) => {\n  return (\n    <span className=\"label\">\n      <span className=\"label-wrapper\">\n        <span className=\"label-question-mark\">?</span>\n        {!!tip && <div className=\"label-tip\">{tip}</div>}\n      </span>\n      {title}\n    </span>\n  );\n};\n\nexport default Label;\n"
  },
  {
    "path": "playground/playground.less",
    "content": ".playground {\n  display: flex;\n  height: 100vh;\n  background: #fff;\n\n  .layout-left,\n  .layout-right {\n    display: flex;\n    box-sizing: border-box;\n    flex-direction: column;\n  }\n\n  .layout-left {\n    flex: 0 0 300px;\n    padding: 0 16px;\n    border-right: 1px solid #ecf0f4;\n    overflow-y: auto;\n\n    &::-webkit-scrollbar {\n      display: none;\n    }\n\n    .logo {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 24px 0 8px;\n      font-size: 24px;\n      font-weight: 500;\n    }\n\n    .back {\n      padding: 8px 16px;\n      border-radius: 4px;\n      background: #000;\n      color: #fff;\n      cursor: pointer;\n      text-align: center;\n\n      &:hover {\n        background: #333;\n        text-decoration: underline;\n      }\n    }\n\n    .config {\n      margin-top: 16px;\n\n      legend {\n        margin: 16px 0 8px;\n        color: rgba(0, 0, 0, 0.5);\n      }\n\n      label {\n        display: flex;\n        align-items: center;\n        margin-bottom: 8px;\n\n        & > span:first-child {\n          margin-right: auto;\n        }\n\n        input,\n        select {\n          max-width: 120px;\n          height: 20px;\n          box-sizing: border-box;\n          font-size: 14px;\n        }\n\n        input[type=\"checkbox\"] {\n          margin: 0;\n        }\n      }\n    }\n\n    pre {\n      padding: 8px 16px;\n      margin: 0 -16px;\n      background: #f5f6f9;\n      font-size: 12px;\n    }\n\n    .statistics {\n      position: sticky;\n      bottom: 0;\n      display: flex;\n      padding: 16px;\n      margin: auto -16px 0;\n      background: #fff;\n    }\n  }\n\n  .layout-right {\n    flex: 1;\n\n    .title {\n      padding: 8px 16px;\n      color: rgba(0, 0, 0, 0.5);\n\n      .control-button {\n        padding: 0 4px;\n        margin-left: 4px;\n        cursor: pointer;\n\n        &:hover {\n          text-decoration: underline;\n        }\n      }\n    }\n\n    .loading,\n    .error {\n      margin-left: 8px;\n    }\n\n    .error {\n      color: red;\n    }\n\n    .inputs,\n    .results {\n      flex: 1;\n    }\n\n    .inputs {\n      display: flex;\n      border-top: 1px solid #ecf0f4;\n      border-bottom: 1px solid #ecf0f4;\n\n      textarea {\n        position: relative;\n        height: 100%;\n        box-sizing: border-box;\n        flex: 1;\n        padding: 16px;\n        border: none;\n        font-family: monospace;\n        font-size: 12px;\n        line-height: 1.5;\n        outline: none;\n        resize: none;\n        transition: all 0.3s;\n\n        &:first-child {\n          border-right: 1px solid #ecf0f4;\n        }\n\n        &:focus {\n          z-index: 1;\n          box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);\n        }\n      }\n\n      &:focus-within textarea {\n        border-color: transparent;\n      }\n    }\n\n    .results {\n      position: relative;\n      border-top: 1px solid #ecf0f4;\n      overflow-y: auto;\n    }\n\n    &.layout-right-fullscreen {\n      .title:first-child,\n      .inputs {\n        display: none;\n      }\n    }\n  }\n}\n\n.token.operator {\n  background: none;\n}\n"
  },
  {
    "path": "playground/playground.tsx",
    "content": "/* eslint-disable max-len, react/no-unescaped-entities */\n\nimport React from 'react';\nimport debounce from 'lodash/debounce';\n\nimport { Differ, Viewer, ViewerProps } from '../src';\nimport type { DifferOptions, InlineDiffOptions } from '../src';\n\nimport GeneratedCode from './generated-code';\nimport jsStringify from './js-stringify';\nimport Label from './label';\nimport { updateInitialValues, useInitialValues } from './initial-values';\n\nimport '../src/viewer-monokai.less';\nimport './playground.less';\n\ninterface PlaygroundProps {\n  onSwitch: () => void;\n}\n\nconst Playground: React.FC<PlaygroundProps> = props => {\n  // differ props\n  const [detectCircular] = React.useState(true);\n  const [maxDepth, setMaxDepth] = React.useState(Infinity);\n  const [showModifications, setShowModifications] = React.useState(true);\n  const [arrayDiffMethod, setArrayDiffMethod] = React.useState<DifferOptions['arrayDiffMethod']>('lcs');\n  const [ignoreCase, setIgnoreCase] = React.useState(false);\n  const [ignoreCaseForKey, setIgnoreCaseForKey] = React.useState(false);\n  const [recursiveEqual, setRecursiveEqual] = React.useState(true);\n  const [preserveKeyOrder, setPreserveKeyOrder] = React.useState<DifferOptions['preserveKeyOrder']>(undefined);\n  const [compareKey, setCompareKey] = React.useState<string>('');\n\n  // viewer props\n  const [indent, setIndent] = React.useState(4);\n  const [lineNumbers, setLineNumbers] = React.useState(true);\n  const [highlightInlineDiff, setHighlightInlineDiff] = React.useState(true);\n  const [inlineDiffMode, setInlineDiffMode] = React.useState<InlineDiffOptions['mode']>('word');\n  const [inlineDiffSeparator, setInlineDiffSeparator] = React.useState(' ');\n  const [hideUnchangedLines, setHideUnchangedLines] = React.useState(true);\n  const [syntaxHighlight, setSyntaxHighlight] = React.useState(false);\n  const [virtual, setVirtual] = React.useState(false);\n\n  const differOptions = React.useMemo(() => ({\n    detectCircular,\n    maxDepth,\n    showModifications,\n    arrayDiffMethod,\n    ignoreCase,\n    ignoreCaseForKey,\n    recursiveEqual,\n    preserveKeyOrder,\n    compareKey: compareKey || undefined,\n  }), [\n    detectCircular,\n    maxDepth,\n    showModifications,\n    arrayDiffMethod,\n    ignoreCase,\n    ignoreCaseForKey,\n    recursiveEqual,\n    preserveKeyOrder,\n    compareKey,\n  ]);\n  const differ = React.useMemo(() => new Differ(differOptions), [differOptions]);\n  const [diff, setDiff] = React.useState(differ.diff('', ''));\n  const [fullscreen, setFullscreen] = React.useState(false);\n  const [error, setError] = React.useState('');\n\n  const _triggerDiff = (before: string, after: string) => {\n    try {\n      const result = differ.diff(\n        JSON.parse(String(before || 'null')),\n        JSON.parse(String(after || 'null')),\n      );\n      setError('');\n      setDiff(result);\n    } catch (e) {\n      setError(e.message);\n      console.error(e); // eslint-disable-line no-console\n    }\n  };\n  const triggerDiff = React.useCallback(debounce(_triggerDiff, 500), [differ]);\n\n  const inlineDiffOptions = React.useMemo(() => ({\n    mode: inlineDiffMode,\n    wordSeparator: inlineDiffSeparator,\n  }), [inlineDiffMode, inlineDiffSeparator]);\n  const virtualOptions = React.useMemo(() => {\n    return virtual && {\n      scrollContainer: '.playground .layout-right .results',\n      itemHeight: 16,\n      expandLineHeight: 27,\n    };\n  }, [virtual]);\n  const viewerOptions: Omit<ViewerProps, 'diff'> = React.useMemo(() => ({\n    indent,\n    lineNumbers,\n    highlightInlineDiff,\n    inlineDiffOptions,\n    hideUnchangedLines,\n    syntaxHighlight: syntaxHighlight ? { theme: 'monokai' } : false,\n    virtual: virtualOptions,\n  }), [\n    indent,\n    lineNumbers,\n    highlightInlineDiff,\n    inlineDiffOptions,\n    hideUnchangedLines,\n    syntaxHighlight,\n    virtualOptions,\n  ]);\n\n  const code = `\nconst d = new Differ(${jsStringify(differOptions)});\nconst diff = d.diff(before, after);\n\nconst viewerProps = ${jsStringify(viewerOptions)};\nreturn (\n  <Viewer\n    diff={diff}\n    {...viewerProps}\n  />\n);\n`.trim();\n\n  // inputs\n  const { l, r } = useInitialValues();\n  const beforeRef = React.useRef(l || '');\n  const afterRef = React.useRef(r || '');\n  const beforeInputRef = React.useRef<HTMLTextAreaElement>(null);\n  const afterInputRef = React.useRef<HTMLTextAreaElement>(null);\n  const setBefore = (value: string, diff: boolean) => {\n    beforeRef.current = value;\n    updateInitialValues(beforeRef.current, afterRef.current);\n    if (diff) {\n      triggerDiff(beforeRef.current, afterRef.current);\n    }\n  };\n  const setAfter = (value: string, diff: boolean) => {\n    afterRef.current = value;\n    updateInitialValues(beforeRef.current, afterRef.current);\n    if (diff) {\n      triggerDiff(beforeRef.current, afterRef.current);\n    }\n  };\n  const clearAll = () => {\n    beforeRef.current = '';\n    afterRef.current = '';\n    updateInitialValues('', '');\n  };\n  const beautify = () => {\n    let before = '';\n    let after = '';\n    try {\n      if (beforeRef) {\n        before = JSON.stringify(JSON.parse(beforeRef.current || 'null'), null, 2);\n      }\n      if (afterRef) {\n        after = JSON.stringify(JSON.parse(afterRef.current || 'null'), null, 2);\n      }\n    } catch (e) {\n      setError(e.message);\n      console.error(e); // eslint-disable-line no-console\n    }\n    if (before || after) {\n      beforeInputRef.current!.value = before;\n      afterInputRef.current!.value = after;\n      setBefore(before, false);\n      setAfter(after, false);\n      updateInitialValues(before, after);\n    }\n  };\n  React.useEffect(() => {\n    if (l !== beforeRef.current || r !== afterRef.current) {\n      setBefore(l || '', false);\n      setAfter(r || '', false);\n      triggerDiff(l, r);\n    }\n  }, [l, r]);\n  React.useEffect(() => {\n    _triggerDiff(beforeRef.current, afterRef.current);\n  }, [differOptions]);\n\n  return (\n    <div className=\"playground\">\n      <div className=\"layout-left\">\n        <div className=\"logo\">JSON Diff Kit</div>\n        <div className=\"back\" onClick={props.onSwitch}>Go to docs & demo</div>\n        <div className=\"config\">\n          <form>\n            <legend>DIFFER CONFIGURATION</legend>\n            <label htmlFor=\"detect-circular\">\n              <Label\n                title=\"Detect circular references\"\n                tip={\n                  <>\n                    Whether to detect circular reference in source objects before diff starts. Default is <code>true</code>. If you are confident about your data (maybe it's from <code>JSON.parse</code> or an API response), you can set it to <code>false</code> to improve performance, but the algorithm may not stop if circular reference does show up.\n                  </>\n                }\n              />\n              <input\n                type=\"checkbox\"\n                id=\"detect-circular\"\n                checked={detectCircular}\n                disabled\n              />\n            </label>\n            <label htmlFor=\"max-depth\">\n              <Label\n                title=\"Max depth\"\n                tip={\n                  <>\n                    If there are nested objects in your data, you can set a max depth to limit the diff to a certain level. Default is <code>Infinity</code>. If you set it to <code>0</code>, only the top level will be diffed.\n                  </>\n                }\n              />\n              <label htmlFor=\"max-depth-infinity\" style={{ margin: '0 4px 0 0' }}>\n                ∞&nbsp;\n                <input\n                  type=\"checkbox\"\n                  id=\"max-depth-infinity\"\n                  checked={maxDepth === Infinity}\n                  onChange={e => setMaxDepth(e.target.checked ? Infinity : 0)}\n                />\n              </label>\n              <input\n                type=\"number\"\n                id=\"max-depth\"\n                value={maxDepth === Infinity ? undefined : maxDepth}\n                onChange={e => setMaxDepth(Number(e.target.value))}\n                min={0}\n                max={10}\n                step={1}\n                disabled={maxDepth === Infinity}\n                style={{ width: '3em' }}\n              />\n            </label>\n            <label htmlFor=\"show-modifications\">\n              <Label\n                title=\"Show modifications\"\n                tip={\n                  <>\n                    Support recognizing modifications, default <code>true</code> means the differ will output the <code>* modified</code> sign apart from the basic <code>+ add</code> and <code>- remove</code> sign. If you prefer Git-style output, please set it to <code>false</code>.\n                  </>\n                }\n              />\n              <input\n                type=\"checkbox\"\n                id=\"show-modifications\"\n                checked={showModifications}\n                onChange={e => setShowModifications(e.target.checked)}\n              />\n            </label>\n            <label htmlFor=\"array-diff-method\">\n              <Label\n                title=\"Array diff method\"\n                tip={\n                  <>\n                    The way to diff arrays, default is <code>\"normal\"</code>, using <code>\"lcs\"</code> may get a better result but much slower. <code>\"unorder-normal\"</code> and <code>\"unorder-lcs\"</code> are for unordered arrays (the order of elements in the array doesn't matter). <code>\"compare-key\"</code> matches objects by a specific key property.\n                  </>\n                }\n              />\n              <select\n                id=\"array-diff-method\"\n                value={arrayDiffMethod}\n                onChange={e => setArrayDiffMethod(e.target.value as DifferOptions['arrayDiffMethod'])}\n              >\n                <option value=\"normal\">normal (default)</option>\n                <option value=\"lcs\">lcs</option>\n                <option value=\"unorder-normal\">unorder-normal</option>\n                <option value=\"unorder-lcs\">unorder-lcs</option>\n                <option value=\"compare-key\">compare-key</option>\n              </select>\n            </label>\n            {arrayDiffMethod === 'compare-key' && (\n              <label htmlFor=\"compare-key\">\n                <Label\n                  title=\"Compare key\"\n                  tip=\"The key to use for matching objects in arrays. Objects with the same value for this key will be matched and compared, regardless of their position in the array.\"\n                />\n                <input\n                  type=\"text\"\n                  id=\"compare-key\"\n                  value={compareKey}\n                  onChange={e => setCompareKey(e.target.value)}\n                  placeholder=\"e.g., oid, userId, id\"\n                />\n              </label>\n            )}\n            <label htmlFor=\"ignore-case\">\n              <Label\n                title=\"Ignore case\"\n                tip=\"Whether to ignore case when comparing string values.\"\n              />\n              <input\n                type=\"checkbox\"\n                id=\"ignore-case\"\n                checked={ignoreCase}\n                onChange={e => setIgnoreCase(e.target.checked)}\n              />\n            </label>\n            <label htmlFor=\"ignore-case-for-key\">\n              <Label\n                title=\"Ignore case for key\"\n                tip=\"Whether to ignore case when comparing object keys.\"\n              />\n              <input\n                type=\"checkbox\"\n                id=\"ignore-case-for-key\"\n                checked={ignoreCaseForKey}\n                onChange={e => setIgnoreCaseForKey(e.target.checked)}\n              />\n            </label>\n            <label htmlFor=\"recursive-equal\">\n              <Label\n                title=\"Recursive equal\"\n                tip=\"Whether to use recursive equal to compare objects. This can provide a better output when there are unchanged object items in an array, but it may cause performance issues when the data is very large.\"\n              />\n              <input\n                type=\"checkbox\"\n                id=\"recursive-equal\"\n                checked={recursiveEqual}\n                onChange={e => setRecursiveEqual(e.target.checked)}\n              />\n            </label>\n            <label htmlFor=\"preserve-key-order\">\n              <Label\n                title=\"Preserve key order\"\n                tip={\n                  <>\n                    Sometimes you do not want the keys in result be sorted, for example <code>start_time</code> and <code>end_time</code> will be swapped by default. You can set this option to let the differ preserve the key order according to <code>before</code> or <code>after</code>.\n                  </>\n                }\n              />\n              <select\n                id=\"preserve-key-order\"\n                value={preserveKeyOrder}\n                onChange={e => setPreserveKeyOrder((e.target.value === 'sort' ? undefined : e.target.value) as DifferOptions['preserveKeyOrder'])}\n              >\n                <option value=\"sort\">sort (default)</option>\n                <option value=\"before\">by \"before\"</option>\n                <option value=\"after\">by \"after\"</option>\n              </select>\n            </label>\n          </form>\n        </div>\n        <div className=\"config\">\n          <form>\n            <legend>VIEWER CONFIGURATION</legend>\n            <label htmlFor=\"indent\">\n              <Label\n                title=\"Indent\"\n                tip={<>Controls the indent in the <code>&lt;Viewer&gt;</code> component.</>}\n              />\n              <input\n                type=\"number\"\n                id=\"indent\"\n                min={1}\n                max={16}\n                value={indent}\n                onChange={e => setIndent(Number(e.target.value))}\n              />\n            </label>\n            <label htmlFor=\"line-numbers\">\n              <Label\n                title=\"Line numbers\"\n                tip={<>Whether to show line numbers.</>}\n              />\n              <input\n                type=\"checkbox\"\n                id=\"line-numbers\"\n                checked={lineNumbers}\n                onChange={e => setLineNumbers(e.target.checked)}\n              />\n            </label>\n            <label htmlFor=\"highlight-inline-diff\">\n              <Label\n                title=\"Highlight inline diff\"\n                tip={\n                  <>\n                    Whether to show the inline diff highlight. For example, if the left value <code>\"JSON diff can't be possible\"</code> is changed to the right value <code>\"JSON diff is possible\"</code>, it will be recognized as we first remove <code>can't be</code> and then add <code>is</code>. This feature is powered by <a href=\"https://github.com/gliese1337/fast-myers-diff\" target=\"_blank\" rel=\"noreferrer\">gliese1337/fast-myers-diff</a>. Note: the <code>showModification</code> must be enabled, or you will not see modified lines.\n                  </>\n                }\n              />\n              <input\n                type=\"checkbox\"\n                id=\"highlight-inline-diff\"\n                checked={highlightInlineDiff}\n                onChange={e => setHighlightInlineDiff(e.target.checked)}\n              />\n            </label>\n            <label htmlFor=\"inline-diff-mode\">\n              <Label\n                title=\"Inline diff mode\"\n                tip={\n                  <>\n                    Control the inline diff behaviour. If the inline diff sources are sentences, we can diff them \"by word\" instead of \"by character\". For normal sentences, just set the method to <code>word</code> and the separator to <code>\" \"</code> (a half-width space) works like a charm. But if you prefer the Git-style output, you can leave this props default, which is diffing \"by character\".\n                  </>\n                }\n              />\n              <select\n                id=\"inline-diff-mode\"\n                disabled={!highlightInlineDiff}\n                value={inlineDiffMode}\n                onChange={e => setInlineDiffMode(e.target.value as InlineDiffOptions['mode'])}\n              >\n                <option value=\"char\">char (default)</option>\n                <option value=\"word\">word</option>\n              </select>\n            </label>\n            <label htmlFor=\"inline-diff-separator\">\n              <Label\n                title=\"Word separator\"\n                tip=\"The separator to split the inline diff sources, default is a half-width space.\"\n              />\n              <input\n                id=\"inline-diff-separator\"\n                disabled={!highlightInlineDiff}\n                value={inlineDiffSeparator}\n                onChange={e => setInlineDiffSeparator(e.target.value)}\n                placeholder=\"Works when mode = char\"\n              />\n            </label>\n            <label htmlFor=\"syntax-highlight\">\n              <Label\n                title=\"Syntax highlight\"\n                tip=\"Support syntax highlight. The viewer component will render like prismjs, and you can write your own style. Please don't forget to import the corresponding CSS file, e.g. import 'json-diff-kit/viewer-monokai.less';\"\n              />\n              <input\n                type=\"checkbox\"\n                id=\"syntax-highlight\"\n                checked={syntaxHighlight}\n                onChange={e => setSyntaxHighlight(e.target.checked)}\n              />\n            </label>\n            <label htmlFor=\"hide-unchanged-lines\">\n              <Label\n                title=\"Hide unchanged lines\"\n                tip=\"Whether to hide the unchanged lines (like what GitHub and GitLab does).\"\n              />\n              <input\n                type=\"checkbox\"\n                id=\"hide-unchanged-lines\"\n                checked={hideUnchangedLines}\n                onChange={e => setHideUnchangedLines(e.target.checked)}\n              />\n            </label>\n            <label htmlFor=\"use-virtual-scroll\">\n              <Label\n                title=\"Use virtual scroll (experimental)\"\n                tip=\"Whether to use virtual scroll. This can improve the rendering performance when the data is very large, but it's not well-tested.\"\n              />\n              <input\n                type=\"checkbox\"\n                id=\"use-virtual-scroll\"\n                checked={virtual}\n                onChange={e => setVirtual(e.target.checked)}\n              />\n            </label>\n          </form>\n        </div>\n        <div className=\"config\">\n          <form>\n            <legend>GENERATEDE CODE</legend>\n            <GeneratedCode code={code} />\n          </form>\n        </div>\n        <div className=\"statistics\">\n          <img src=\"https://img.shields.io/npm/v/json-diff-kit.svg?style=flat\" style={{ marginRight: 8 }} />\n          <iframe\n            src=\"https://ghbtns.com/github-btn.html?user=rexskz&repo=json-diff-kit&type=star&count=true\"\n            frameBorder=\"0\"\n            scrolling=\"0\"\n            width=\"90\"\n            height=\"20\"\n            title=\"GitHub\"\n          />\n        </div>\n      </div>\n      <div className={`layout-right${fullscreen ? ' layout-right-fullscreen' : ''}`}>\n        <div className=\"title\">\n          INPUTS\n          <span className=\"control-button\" onClick={clearAll}>[CLEAR ALL]</span>\n          <span className=\"control-button\" onClick={beautify}>[BEAUTIFY]</span>\n        </div>\n        <div className=\"inputs\">\n          <textarea\n            ref={beforeInputRef}\n            placeholder=\"before\"\n            defaultValue={beforeRef.current}\n            onChange={e => setBefore(e.target.value, true)}\n          />\n          <textarea\n            ref={afterInputRef}\n            placeholder=\"after\"\n            defaultValue={afterRef.current}\n            onChange={e => setAfter(e.target.value, true)}\n          />\n        </div>\n        <div className=\"title\">\n          DIFF RESULTS\n          <span className=\"control-button\" onClick={() => setFullscreen(pre => !pre)}>[{fullscreen ? 'EXIT ' : ''}PAGE FULLSCREEN]</span>\n          {!!error && <span className=\"error\">{error}</span>}\n        </div>\n        <div className=\"results\">\n          <Viewer diff={diff} {...viewerOptions} />\n        </div>\n      </div>\n    </div>\n  );\n};\n\nPlayground.displayName = 'Playground';\n\nexport default Playground;\n"
  },
  {
    "path": "rollup.config.cli.mjs",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport replace from '@rollup/plugin-replace';\nimport resolve from '@rollup/plugin-node-resolve';\nimport swc from '@rollup/plugin-swc';\n\nimport packageJson from './package.json' assert { type: 'json' };\n\nconst plugins = [\n  resolve({\n    preferBuiltins: true,\n    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],\n  }),\n  commonjs(),\n  replace({\n    values: {\n      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),\n      __VERSION__: JSON.stringify(packageJson.version),\n    },\n    preventAssignment: true,\n  }),\n  swc(),\n];\n\nexport default {\n  input: {\n    index: 'src/cli/index.ts',\n  },\n  output: {\n    dir: './dist/cjs/cli',\n    format: 'cjs',\n    exports: 'auto',\n  },\n  external: ['commander', 'prompts', 'terminal-kit'],\n  plugins,\n};\n"
  },
  {
    "path": "rollup.config.mjs",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport less from 'rollup-plugin-less';\nimport replace from '@rollup/plugin-replace';\nimport resolve from '@rollup/plugin-node-resolve';\nimport swc from '@rollup/plugin-swc';\n\nimport packageJson from './package.json' assert { type: 'json' };\n\nconst plugins = [\n  less({ output: './dist/viewer.css' }),\n  resolve({\n    preferBuiltins: true,\n    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],\n  }),\n  commonjs(),\n  replace({\n    values: {\n      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),\n      __VERSION__: JSON.stringify(packageJson.version),\n    },\n    preventAssignment: true,\n  }),\n  swc(),\n];\n\nconst globals = {\n  react: 'React',\n  'react-dom': 'ReactDOM',\n};\n\nexport default {\n  input: {\n    index: 'src/index.ts',\n    differ: 'src/differ.ts',\n    viewer: 'src/viewer.tsx',\n  },\n  output: [\n    { dir: './dist', format: 'esm', globals, exports: 'auto' },\n    { dir: './dist/cjs', format: 'cjs', globals, exports: 'auto' },\n  ],\n  external: ['react', 'react-dom'],\n  plugins,\n};\n"
  },
  {
    "path": "rollup.config.pages.mjs",
    "content": "import commonjs from '@rollup/plugin-commonjs';\nimport html from '@rollup/plugin-html';\nimport less from 'rollup-plugin-less';\nimport livereload from 'rollup-plugin-livereload';\nimport replace from '@rollup/plugin-replace';\nimport resolve from '@rollup/plugin-node-resolve';\nimport serve from 'rollup-plugin-serve';\nimport styles from 'rollup-plugin-styles';\nimport swc from '@rollup/plugin-swc';\n\nimport packageJson from './package.json' assert { type: 'json' };\n\nconst BASEDIR = process.env.BASEDIR || '.cache';\n\nconst plugins = [\n  less({\n    output: `${BASEDIR}/index.css`,\n    insert: true,\n  }),\n  styles(),\n  html({\n    template: options => {\n      return `<!DOCTYPE html>\n<html>\n<head>\n  <title>JSON Diff Kit Playground</title>\n  <meta charset=\"utf-8\" />\n  <script async src=\"https://www.googletagmanager.com/gtag/js?id=G-5D3V5T84WY\"></script>\n  <script>\n    window.dataLayer = window.dataLayer || [];\n    function gtag(){dataLayer.push(arguments);}\n    gtag('js', new Date());\n    gtag('config', 'G-5D3V5T84WY');\n  </script>\n  <link rel=\"stylesheet\" href=\"index.css\" />\n</head>\n<body>\n  <div id=\"root\"></div>\n  ${options?.files.js.map(({ fileName }) => `<script src=\"${fileName}\"></script>`)}\n</body>\n</html>\n`;\n    },\n  }),\n  resolve({\n    preferBuiltins: true,\n    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],\n  }),\n  commonjs(),\n  replace({\n    values: {\n      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),\n      __VERSION__: JSON.stringify(packageJson.version),\n    },\n    preventAssignment: true,\n  }),\n  swc({\n    minify: process.env.NODE_ENV === 'production',\n    sourceMaps: process.env.NODE_ENV !== 'production',\n  }),\n];\n\nif (process.env.NODE_ENV !== 'production') {\n  plugins.push(\n    serve({\n      contentBase: BASEDIR,\n      open: true,\n      openPage: '/index.html',\n      port: 3000,\n    }),\n    livereload({\n      watch: BASEDIR,\n      delay: 300,\n    }),\n  );\n}\n\nexport default {\n  input: 'playground/index.tsx',\n  output: {\n    file: `${BASEDIR}/index.js`,\n    format: 'umd',\n    name: 'JSONDiffKit',\n    globals: {\n      react: 'React',\n      'react-dom': 'ReactDOM',\n    },\n    sourcemap: process.env.NODE_ENV !== 'production',\n  },\n  plugins,\n};\n"
  },
  {
    "path": "src/cli/index.ts",
    "content": "/* eslint-disable no-console */\n\nimport fs from 'node:fs';\n\nimport { program } from 'commander';\nimport { prompts } from 'prompts';\n\nimport Differ, { type DifferOptions } from '../differ';\nimport showInTerminal from './show-in-terminal';\nimport writeToFile from './write-to-file';\n\nprogram\n  .name('jsondiff')\n  .description('A better JSON differ & viewer, support LCS diff for arrays and recognise some changes as \"modification\" apart from simple \"remove\"+\"add\".') // eslint-disable-line max-len\n  .version(__VERSION__);\n\nprogram\n  .command('run')\n  .description('Shows a difference between two JSON files.')\n  .argument('<before>', 'Path to the first JSON file')\n  .argument('<after>', 'Path to the second JSON file')\n  .option('-c, --config <path>', 'Path to the config file, will override the default config')\n  .option('-o --output <path>', 'Path to the output file, default to stdout')\n  .action(async(beforePath, afterPath, options) => {\n    const config: DifferOptions = {};\n    if (!fs.statSync(beforePath).isFile()) {\n      console.error(`Error: ${beforePath} is not a file.`);\n      process.exit(1);\n    }\n    if (!fs.statSync(afterPath).isFile()) {\n      console.error(`Error: ${afterPath} is not a file.`);\n      process.exit(1);\n    }\n    if (fs.existsSync(options.config)) {\n      if (!fs.statSync(options.config).isFile()) {\n        console.error(`Error: ${options.config} is not a file.`);\n        process.exit(1);\n      }\n      Object.assign(config, JSON.parse(fs.readFileSync(options.config, 'utf-8')));\n    }\n    if (fs.existsSync(options.output)) {\n      const resp = await prompts.confirm({\n        type: 'confirm',\n        message: `The output file \"${options.output}\" already exists, do you want to overwrite it?`,\n      });\n      if (!resp) {\n        console.error('File already exists, aborted.');\n        process.exit(0);\n      }\n    }\n    let beforeValue = null;\n    let afterValue = null;\n    try {\n      beforeValue = JSON.parse(fs.readFileSync(beforePath, 'utf-8'));\n    } catch (e) {\n      console.error(`Error: ${beforePath} is not a valid JSON file.`);\n      process.exit(1);\n    }\n    try {\n      afterValue = JSON.parse(fs.readFileSync(afterPath, 'utf-8'));\n    } catch (e) {\n      console.error(`Error: ${afterPath} is not a valid JSON file.`);\n      process.exit(1);\n    }\n    const differ = new Differ({\n      arrayDiffMethod: 'lcs',\n      ...config,\n      showModifications: options.output ? false : config.showModifications,\n    });\n    const result = differ.diff(beforeValue, afterValue);\n    if (options.output) {\n      writeToFile(options.output, result);\n    } else {\n      showInTerminal(result);\n    }\n  });\n\nprogram.parse();\n"
  },
  {
    "path": "src/cli/show-in-terminal.ts",
    "content": "import type { Terminal } from 'terminal-kit';\nimport type { DiffResult } from '../differ';\n\nconst DIVIDER = ' │ ';\nconst HINT_TEXT = 'Press q to quit, ↑/↓ to scroll';\n\nconst decorate = (line: DiffResult) => {\n  const indent = '  '.repeat(line.level);\n  const comma = line.comma ? ',' : '';\n  return `${indent}${line.text}${comma}`;\n};\n\nconst getOutputFunction = (terminal: Terminal, line: DiffResult) => {\n  if (line.type === 'add') return terminal.bgGreen;\n  if (line.type === 'remove') return terminal.bgRed;\n  if (line.type === 'modify') return terminal.bgYellow;\n  return terminal;\n};\n\nconst showContent = (\n  terminal: Terminal,\n  leftResult: DiffResult[],\n  rightResult: DiffResult[],\n  columns: number,\n  rows: number,\n  startLine: number,\n) => {\n  const lineNumberWidth = Math.max(\n    ...leftResult.map(line => String(line.lineNumber || '').length),\n    ...rightResult.map(line => String(line.lineNumber || '').length),\n  ) + DIVIDER.length;\n  const contentWidth = ((columns - 1) >> 1) - lineNumberWidth;\n\n  for (let _i = 0; _i < rows - 1; _i++) {\n    terminal.moveTo(1, _i + 1).eraseLine();\n\n    const i = _i + startLine;\n    const left = leftResult[i];\n    const right = rightResult[i];\n    if (!left && !right) {\n      continue;\n    }\n\n    const leftOutputFunction = getOutputFunction(terminal, left);\n    const rightOutputFunction = getOutputFunction(terminal, right);\n\n    leftOutputFunction((left.lineNumber || '').toString().padStart(lineNumberWidth - 1, ' ') + DIVIDER);\n    leftOutputFunction(decorate(left).slice(0, contentWidth).padEnd(contentWidth, ' '));\n    (left.type === 'modify' ? terminal.bgYellow : terminal)('│');\n    rightOutputFunction((right.lineNumber || '').toString().padStart(lineNumberWidth - 1, ' ') + DIVIDER);\n    rightOutputFunction(decorate(right).slice(0, contentWidth).padEnd(contentWidth, ' '));\n  }\n\n  terminal.moveTo(1, rows).eraseLine();\n  const lineRangeText = `${startLine + 1}-${Math.min(startLine + rows - 1, leftResult.length)}/${leftResult.length}`;\n  terminal(`${HINT_TEXT}${lineRangeText.padStart(columns - HINT_TEXT.length, ' ')}`);\n  terminal.moveTo(1, rows);\n};\n\nconst importTerminalKit = async() => {\n  try {\n    return await import('terminal-kit');\n  } catch {\n    // eslint-disable-next-line no-console\n    console.error('Please install the package \"terminal-kit\" to show diff in terminal.');\n    process.exit(1);\n  }\n};\n\nconst showInTerminal = async([leftResult, rightResult]: readonly [DiffResult[], DiffResult[]]) => {\n  const { terminal } = await importTerminalKit();\n  let startLine = 0;\n  let columns = terminal.width;\n  let rows = terminal.height;\n\n  // Swap to an alternate screen buffer\n  // https://github.com/vadimdemedes/ink/issues/263#issuecomment-600927688\n  const enterAltScreenCommand = '\\x1b[?1049h';\n  const leaveAltScreenCommand = '\\x1b[?1049l';\n  process.stdout.write(enterAltScreenCommand);\n  process.on('exit', () => {\n    process.stdout.write(leaveAltScreenCommand);\n  });\n\n  showContent(terminal, leftResult, rightResult, columns, rows, startLine);\n\n  terminal.on('resize', (newColumns: number, newRows: number) => {\n    columns = newColumns;\n    rows = newRows;\n    showContent(terminal, leftResult, rightResult, columns, rows, startLine);\n  });\n\n  terminal.grabInput(true);\n  terminal.on('key', (key: string) => {\n    switch (key) {\n      case 'UP':\n        if (startLine > 0) {\n          startLine--;\n          showContent(terminal, leftResult, rightResult, columns, rows, startLine);\n        }\n        break;\n      case 'DOWN':\n        if (startLine < leftResult.length - rows + 1) {\n          startLine++;\n          showContent(terminal, leftResult, rightResult, columns, rows, startLine);\n        }\n        break;\n      case 'PAGE_UP':\n        startLine = Math.max(0, startLine - rows + 1);\n        showContent(terminal, leftResult, rightResult, columns, rows, startLine);\n        break;\n      case 'PAGE_DOWN':\n      case 'SPACE':\n        startLine = Math.min(leftResult.length - rows + 1, startLine + rows - 1);\n        showContent(terminal, leftResult, rightResult, columns, rows, startLine);\n        break;\n      case 'q':\n      case 'CTRL_C':\n        process.exit(0);\n    }\n  });\n};\n\nexport default showInTerminal;\n"
  },
  {
    "path": "src/cli/write-to-file.ts",
    "content": "import fs from 'node:fs';\nimport type { DiffResult } from '../differ';\n\nconst decorate = (line: DiffResult) => {\n  const sign = line.type === 'equal' ? ' ' : line.type === 'remove' ? '-' : '+';\n  const indent = '  '.repeat(line.level);\n  const comma = line.comma ? ',' : '';\n  return `${sign} ${indent}${line.text}${comma}`;\n};\n\n/**\n * It's not able to write side-by-side diff to a file,\n * so we just use the Git-style output.\n */\nconst writeToFile = (path: string, content: readonly [DiffResult[], DiffResult[]]) => {\n  const [linesLeft, linesRight] = content;\n  const length = linesLeft.length;\n  const output: string[] = [];\n\n  for (let i = 0; i < length; i++) {\n    const left = linesLeft[i];\n    const right = linesRight[i];\n    if (left.type === 'equal' && right.type === 'equal') {\n      output.push(decorate(left));\n    } else {\n      if (left.text) output.push(decorate(left));\n      if (right.text) output.push(decorate(right));\n    }\n  }\n\n  const fileContent = output.join('\\n');\n  fs.writeFileSync(path, fileContent, 'utf-8');\n};\n\nexport default writeToFile;\n"
  },
  {
    "path": "src/declares.d.ts",
    "content": "declare const __VERSION__: string;\n"
  },
  {
    "path": "src/differ.spec.ts",
    "content": "import Differ from './differ';\n\ndescribe('object diff', () => {\n  it('should not infinite loop when an object has an empty string key', () => {\n    const l = { '': 'before', a: 1 };\n    const r = { '': 'after', b: 2 };\n    const d = new Differ();\n    // Should complete without hanging or throwing\n    expect(() => d.diff(l, r)).not.toThrow();\n    const result = d.diff(l, r);\n    // Both sides must have the same number of lines\n    expect(result[0].length).toBe(result[1].length);\n    // The empty-string key's value change should appear as a modification\n    const leftTypes = result[0].map(line => line.type);\n    const rightTypes = result[1].map(line => line.type);\n    expect(leftTypes).toContain('modify');\n    expect(rightTypes).toContain('modify');\n  });\n\n  it('preserve key order', () => {\n    const l = { a: 1, b: 2, c: 3 };\n    const r = { c: 3, b: 2, d: 4, a: 1 };\n    const d = new Differ();\n    const result = d.diff(l, r);\n    expect(result[0]).toEqual([\n      { lineNumber: 1, level: 0, type: 'equal', text: '{' },\n      { lineNumber: 2, level: 1, type: 'equal', text: '\"a\": 1', comma: true },\n      { lineNumber: 3, level: 1, type: 'equal', text: '\"b\": 2', comma: true },\n      { lineNumber: 4, level: 1, type: 'equal', text: '\"c\": 3' },\n      { level: 1, type: 'equal', text: '' },\n      { lineNumber: 5, level: 0, type: 'equal', text: '}' },\n    ]);\n    expect(result[1]).toEqual([\n      { lineNumber: 1, level: 0, type: 'equal', text: '{' },\n      { lineNumber: 2, level: 1, type: 'equal', text: '\"a\": 1', comma: true },\n      { lineNumber: 3, level: 1, type: 'equal', text: '\"b\": 2', comma: true },\n      { lineNumber: 4, level: 1, type: 'equal', text: '\"c\": 3', comma: true },\n      { lineNumber: 5, level: 1, type: 'add', text: '\"d\": 4' },\n      { lineNumber: 6, level: 0, type: 'equal', text: '}' },\n    ]);\n  });\n});\n\ndescribe('object array diff', () => {\n  it('recursive equal', () => {\n    const l = [\n      { id: '1', x: 'a' },\n      { id: '2', x: 'b' },\n    ];\n    const r = [\n      { id: '2', x: 'b' },\n      { id: '1', x: 'a' },\n    ];\n    const d = new Differ({ recursiveEqual: true, arrayDiffMethod: 'lcs' });\n    const result = d.diff(l, r);\n    expect(result[0]).toEqual([\n      { lineNumber: 1, level: 0, type: 'equal', text: '[' },\n      { level: 1, type: 'equal', text: '' },\n      { level: 1, type: 'equal', text: '' },\n      { level: 1, type: 'equal', text: '' },\n      { level: 1, type: 'equal', text: '' },\n      { lineNumber: 2, level: 1, type: 'equal', text: '{' },\n      { lineNumber: 3, level: 2, type: 'equal', text: '\"id\": \"1\"', comma: true },\n      { lineNumber: 4, level: 2, type: 'equal', text: '\"x\": \"a\"' },\n      { lineNumber: 5, level: 1, type: 'equal', text: '}', comma: true },\n      { lineNumber: 6, level: 1, type: 'remove', text: '{' },\n      { lineNumber: 7, level: 2, type: 'remove', text: '\"id\": \"2\"', comma: true },\n      { lineNumber: 8, level: 2, type: 'remove', text: '\"x\": \"b\"' },\n      { lineNumber: 9, level: 1, type: 'remove', text: '}' },\n      { lineNumber: 10, level: 0, type: 'equal', text: ']' },\n    ]);\n    expect(result[1]).toEqual([\n      { lineNumber: 1, level: 0, type: 'equal', text: '[' },\n      { lineNumber: 2, level: 1, type: 'add', text: '{' },\n      { lineNumber: 3, level: 2, type: 'add', text: '\"id\": \"2\"', comma: true },\n      { lineNumber: 4, level: 2, type: 'add', text: '\"x\": \"b\"' },\n      { lineNumber: 5, level: 1, type: 'add', text: '}', comma: true },\n      { lineNumber: 6, level: 1, type: 'equal', text: '{' },\n      { lineNumber: 7, level: 2, type: 'equal', text: '\"id\": \"1\"', comma: true },\n      { lineNumber: 8, level: 2, type: 'equal', text: '\"x\": \"a\"' },\n      { lineNumber: 9, level: 1, type: 'equal', text: '}' },\n      { level: 1, type: 'equal', text: '' },\n      { level: 1, type: 'equal', text: '' },\n      { level: 1, type: 'equal', text: '' },\n      { level: 1, type: 'equal', text: '' },\n      { lineNumber: 10, level: 0, type: 'equal', text: ']' },\n    ]);\n  });\n});\n\ndescribe('2-dimensional array diff', () => {\n  it('normal diff', () => {\n    const l = [[1, 2, 3, 4], [5, 6], [9]];\n    const r = [[1, 2, 4], [5, 9], [9]];\n    const d = new Differ({\n      arrayDiffMethod: 'normal',\n      showModifications: true,\n    });\n    const result = d.diff(l, r);\n    expect(result[0]).toEqual([\n      { lineNumber: 1, level: 0, type: 'equal', text: '[' },\n      { lineNumber: 2, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 3, level: 2, type: 'equal', text: '1', comma: true },\n      { lineNumber: 4, level: 2, type: 'equal', text: '2', comma: true },\n      { lineNumber: 5, level: 2, type: 'modify', text: '3', comma: true },\n      { lineNumber: 6, level: 2, type: 'remove', text: '4' },\n      { lineNumber: 7, level: 1, type: 'equal', text: ']', comma: true },\n      { lineNumber: 8, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 9, level: 2, type: 'equal', text: '5', comma: true },\n      { lineNumber: 10, level: 2, type: 'modify', text: '6' },\n      { lineNumber: 11, level: 1, type: 'equal', text: ']', comma: true },\n      { lineNumber: 12, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 13, level: 2, type: 'equal', text: '9' },\n      { lineNumber: 14, level: 1, type: 'equal', text: ']' },\n      { lineNumber: 15, level: 0, type: 'equal', text: ']' },\n    ]);\n    expect(result[1]).toEqual([\n      { lineNumber: 1, level: 0, type: 'equal', text: '[' },\n      { lineNumber: 2, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 3, level: 2, type: 'equal', text: '1', comma: true },\n      { lineNumber: 4, level: 2, type: 'equal', text: '2', comma: true },\n      { lineNumber: 5, level: 2, type: 'modify', text: '4' },\n      { level: 2, type: 'equal', text: '' },\n      { lineNumber: 6, level: 1, type: 'equal', text: ']', comma: true },\n      { lineNumber: 7, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 8, level: 2, type: 'equal', text: '5', comma: true },\n      { lineNumber: 9, level: 2, type: 'modify', text: '9' },\n      { lineNumber: 10, level: 1, type: 'equal', text: ']', comma: true },\n      { lineNumber: 11, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 12, level: 2, type: 'equal', text: '9' },\n      { lineNumber: 13, level: 1, type: 'equal', text: ']' },\n      { lineNumber: 14, level: 0, type: 'equal', text: ']' },\n    ]);\n  });\n\n  it('lcs diff', () => {\n    const l = [[1, 2, 3, 4], [5, 6], [9]];\n    const r = [[1, 2, 4], [5, 9], [9]];\n    const d = new Differ({\n      arrayDiffMethod: 'lcs',\n      showModifications: true,\n    });\n    const result = d.diff(l, r);\n    expect(result[0]).toEqual([\n      { lineNumber: 1, level: 0, type: 'equal', text: '[' },\n      { lineNumber: 2, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 3, level: 2, type: 'equal', text: '1', comma: true },\n      { lineNumber: 4, level: 2, type: 'equal', text: '2', comma: true },\n      { lineNumber: 5, level: 2, type: 'remove', text: '3', comma: true },\n      { lineNumber: 6, level: 2, type: 'equal', text: '4' },\n      { lineNumber: 7, level: 1, type: 'equal', text: ']', comma: true },\n      { lineNumber: 8, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 9, level: 2, type: 'equal', text: '5', comma: true },\n      { lineNumber: 10, level: 2, type: 'modify', text: '6' },\n      { lineNumber: 11, level: 1, type: 'equal', text: ']', comma: true },\n      { lineNumber: 12, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 13, level: 2, type: 'equal', text: '9' },\n      { lineNumber: 14, level: 1, type: 'equal', text: ']' },\n      { lineNumber: 15, level: 0, type: 'equal', text: ']' },\n    ]);\n    expect(result[1]).toEqual([\n      { lineNumber: 1, level: 0, type: 'equal', text: '[' },\n      { lineNumber: 2, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 3, level: 2, type: 'equal', text: '1', comma: true },\n      { lineNumber: 4, level: 2, type: 'equal', text: '2', comma: true },\n      { level: 2, type: 'equal', text: '' },\n      { lineNumber: 5, level: 2, type: 'equal', text: '4' },\n      { lineNumber: 6, level: 1, type: 'equal', text: ']', comma: true },\n      { lineNumber: 7, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 8, level: 2, type: 'equal', text: '5', comma: true },\n      { lineNumber: 9, level: 2, type: 'modify', text: '9' },\n      { lineNumber: 10, level: 1, type: 'equal', text: ']', comma: true },\n      { lineNumber: 11, level: 1, type: 'equal', text: '[' },\n      { lineNumber: 12, level: 2, type: 'equal', text: '9' },\n      { lineNumber: 13, level: 1, type: 'equal', text: ']' },\n      { lineNumber: 14, level: 0, type: 'equal', text: ']' },\n    ]);\n  });\n});\n"
  },
  {
    "path": "src/differ.ts",
    "content": "import cleanFields from './utils/clean-fields';\nimport concat from './utils/concat';\nimport detectCircular from './utils/detect-circular';\nimport diffArrayLCS from './utils/diff-array-lcs';\nimport diffArrayNormal from './utils/diff-array-normal';\nimport diffArrayCompareKey from './utils/diff-array-compare-key';\nimport diffObject from './utils/diff-object';\nimport getType from './utils/get-type';\nimport sortInnerArrays from './utils/sort-inner-arrays';\nimport stringify from './utils/stringify';\n\nexport interface DifferOptions {\n  /**\n   * Whether to detect circular reference in source objects before diff starts. Default\n   * is `true`. If you are confident for your data (e.g. from `JSON.parse` or an API\n   * response), you can set it to `false` to improve performance, but the algorithm may\n   * not stop if circular reference does show up.\n   */\n  detectCircular?: boolean;\n  /**\n   * Max depth, default `Infinity` means no depth limit.\n   */\n  maxDepth?: number;\n  /**\n   * Support recognizing modifications, default `true` means the differ will output the\n   * `* modified` sign apart from the basic `+ add` and `- remove` sign. If you prefer\n   * Git output, please set it to `false`.\n   */\n  showModifications?: boolean;\n  /**\n   * The way to diff arrays, default is `\"normal\"`.\n   *\n   * For example, if we got 2 arrays: `a =[1, 2, 3]` and `b = [2, 3, 1, 4, 0]`, and the\n   * `showModifications` is set to `true`.\n   *\n   * When using `normal`, the differ will compare the items in the same index one by one.\n   * The time complexity is faster (`O(LEN)`). The output will be:\n   *\n   * ```diff\n   *   a b\n   * * 1 2\n   * * 2 3\n   * * 3 1\n   * +   4\n   * +   0\n   * ```\n   *\n   * When using `lcs`, the differ will perform the LCS (Longest Common Subsequence) algorithm,\n   * assuming the items in the subsequence are unchanged. The time complexity for LCS is\n   * slower (`O(LEN^2)`). The output will be:\n   *\n   * ```diff\n   *   a b\n   * - 1\n   *   2 2\n   *   3 3\n   * +   1\n   * +   4\n   * +   0\n   * ```\n   *\n   * When using `unorder-normal`, the differ will first sort 2 arrays, then act like `normal`.\n   * The output will be:\n   *\n   * ```diff\n   *   a b\n   * * 1 0\n   * * 2 1\n   * * 3 2\n   * * 4 3\n   * +   4\n   * ```\n   *\n   * When using `unorder-lcs`, the differ will first sort 2 arrays, then act like `lcs`.\n   * The output will be:\n   *\n   * ```diff\n   *   a b\n   * +   0\n   *   1 1\n   *   2 2\n   *   3 3\n   * +   4\n   * ```\n   *\n   * When using `compare-key`, the differ will match objects in arrays by a specific key\n   * property (specified by `compareKey` option). This is useful when comparing arrays of\n   * objects where the order doesn't matter but you want to match related objects.\n   * The output will be:\n   *\n   * ```diff\n   *   a b\n   *   1 1\n   * - 2\n   * +   3\n   *   4 4\n   * ```\n   */\n  arrayDiffMethod?:\n    | 'normal'\n    | 'lcs'\n    | 'unorder-normal'\n    | 'unorder-lcs'\n    | 'compare-key';\n  /**\n   * Whether to ignore the case when comparing strings, default `false`.\n   */\n  ignoreCase?: boolean;\n  /**\n   * Whether to ignore the case when comparing keys, default `false`.\n   *\n   * Notice: if there are keys with different cases in the same object, the algorithm may fail\n   * since it's not able to tell which key is the correct one.\n   */\n  ignoreCaseForKey?: boolean;\n  /**\n   * Whether to use recursive equal to compare objects, default `false`.\n   *\n   * This will only applied to objects, not arrays.\n   *\n   * Two objects are considered equal if they have the same properties and values, for example:\n   *\n   * ```js\n   * const x = { 'a': 1, 'b': 2 };\n   * const y = { 'b': 2, 'a': 1 };\n   * ```\n   *\n   * The `x` and `y` here will be considered equal.\n   *\n   * This comparation process is slow in huge objects.\n  */\n  recursiveEqual?: boolean;\n  /**\n   * If the value is set, differ will make sure the key order of results is the same as inputs\n   * (\"before\" or \"after\"). Otherwise, differ will sort the keys of results.\n   */\n  preserveKeyOrder?: 'before' | 'after';\n  /**\n   * The key to use for matching objects in arrays when using `compare-key` array diff method.\n   * Objects with the same value for this key will be matched and compared, regardless of their\n   * position in the array.\n   */\n  compareKey?: string;\n  /**\n   * The behavior when encountering values that are not part of the JSON spec, e.g. `undefined`, `NaN`, `Infinity`, `123n`, `() => alert(1)`, `Symbol.iterator`.\n   *\n   * - `UndefinedBehavior.throw`: throw an error\n   * - `UndefinedBehavior.ignore`: ignore the key-value pair\n   * - `UndefinedBehavior.stringify`: try to stringify the value\n   *\n   * Default is `UndefinedBehavior.stringify`.\n   */\n  undefinedBehavior?: UndefinedBehavior;\n}\n\nexport enum UndefinedBehavior {\n  stringify = 'stringify',\n  ignore = 'ignore',\n  throw = 'throw',\n}\n\nexport interface DiffResult {\n  level: number;\n  type: 'modify' | 'add' | 'remove' | 'equal';\n  text: string;\n  comma?: boolean;\n  lineNumber?: number;\n}\n\nexport type ArrayDiffFunc = (\n  arrLeft: any[],\n  arrRight: any[],\n  keyLeft: string,\n  keyRight: string,\n  level: number,\n  options: DifferOptions,\n  ...args: any[]\n) => [DiffResult[], DiffResult[]];\n\nconst EQUAL_EMPTY_LINE: DiffResult = { level: 0, type: 'equal', text: '' };\nconst EQUAL_LEFT_BRACKET_LINE: DiffResult = { level: 0, type: 'equal', text: '{' };\nconst EQUAL_RIGHT_BRACKET_LINE: DiffResult = { level: 0, type: 'equal', text: '}' };\n\nclass Differ {\n  private options: DifferOptions;\n  private arrayDiffFunc: ArrayDiffFunc;\n\n  constructor({\n    detectCircular = true,\n    maxDepth = Infinity,\n    showModifications = true,\n    arrayDiffMethod = 'normal',\n    ignoreCase = false,\n    ignoreCaseForKey = false,\n    recursiveEqual = false,\n    preserveKeyOrder,\n    compareKey,\n    undefinedBehavior = UndefinedBehavior.stringify,\n  }: DifferOptions = {}) {\n    this.options = {\n      detectCircular,\n      maxDepth,\n      showModifications,\n      arrayDiffMethod,\n      ignoreCase,\n      ignoreCaseForKey,\n      recursiveEqual,\n      preserveKeyOrder,\n      compareKey,\n      undefinedBehavior,\n    };\n\n    if (arrayDiffMethod === 'compare-key') {\n      this.arrayDiffFunc = diffArrayCompareKey;\n    } else if (arrayDiffMethod === 'lcs' || arrayDiffMethod === 'unorder-lcs') {\n      this.arrayDiffFunc = diffArrayLCS;\n    } else {\n      this.arrayDiffFunc = diffArrayNormal;\n    }\n  }\n\n  private detectCircular(source: any) {\n    if (this.options.detectCircular) {\n      if (detectCircular(source)) {\n        throw new Error(\n          `Circular reference detected in object (with keys ${Object.keys(source).map(t => `\"${t}\"`).join(', ')})`,\n        );\n      }\n    }\n  }\n\n  private sortResultLines(left: DiffResult[], right: DiffResult[]) {\n    for (let k = 0; k < left.length; k++) {\n      let changed = false;\n      for (let i = 1; i < left.length; i++) {\n        if (\n          left[i].type === 'remove' &&\n          left[i - 1].type === 'equal' &&\n          right[i].type === 'equal' &&\n          right[i - 1].type === 'add'\n        ) {\n          const t1 = left[i - 1];\n          left[i - 1] = left[i];\n          left[i] = t1;\n          const t2 = right[i - 1];\n          right[i - 1] = right[i];\n          right[i] = t2;\n          changed = true;\n        }\n      }\n      if (!changed) {\n        break;\n      }\n    }\n  }\n\n  private calculateLineNumbers(result: DiffResult[]) {\n    let lineNumber = 0;\n    for (const item of result) {\n      if (!item.text) {\n        continue;\n      }\n      item.lineNumber = ++lineNumber;\n    }\n  }\n\n  private calculateCommas(result: DiffResult[]) {\n    const nextLine = Array(result.length).fill(0);\n    for (let i = result.length - 1; i > 0; i--) {\n      if (result[i].text) {\n        nextLine[i - 1] = i;\n      } else {\n        nextLine[i - 1] = nextLine[i];\n      }\n    }\n\n    for (let i = 0; i < result.length; i++) {\n      if (\n        !result[i].text.endsWith('{') &&\n        !result[i].text.endsWith('[') &&\n        result[i].text &&\n        nextLine[i] &&\n        result[i].level <= result[nextLine[i]].level\n      ) {\n        result[i].comma = true;\n      }\n    }\n  }\n\n  diff(sourceLeft: any, sourceRight: any) {\n    this.detectCircular(sourceLeft);\n    this.detectCircular(sourceRight);\n\n    if (\n      this.options.arrayDiffMethod === 'unorder-normal' ||\n      this.options.arrayDiffMethod === 'unorder-lcs'\n    ) {\n      sourceLeft = sortInnerArrays(sourceLeft, this.options);\n      sourceRight = sortInnerArrays(sourceRight, this.options);\n    }\n\n    if (this.options.undefinedBehavior === UndefinedBehavior.ignore) {\n      sourceLeft = cleanFields(sourceLeft) ?? null;\n      sourceRight = cleanFields(sourceRight) ?? null;\n    }\n\n    let resultLeft: DiffResult[] = [];\n    let resultRight: DiffResult[] = [];\n\n    const typeLeft = getType(sourceLeft);\n    const typeRight = getType(sourceRight);\n    if (typeLeft !== typeRight) {\n      const strLeft = stringify(sourceLeft, undefined, 1, this.options.maxDepth, this.options.undefinedBehavior);\n      resultLeft = strLeft.split('\\n').map(line => ({\n        level: line.match(/^\\s+/)?.[0]?.length || 0,\n        type: 'remove',\n        text: line.replace(/^\\s+/, '').replace(/,$/g, ''),\n        comma: line.endsWith(','),\n      }));\n      const strRight = stringify(sourceRight, undefined, 1, this.options.maxDepth, this.options.undefinedBehavior);\n      resultRight = strRight.split('\\n').map(line => ({\n        level: line.match(/^\\s+/)?.[0]?.length || 0,\n        type: 'add',\n        text: line.replace(/^\\s+/, '').replace(/,$/g, ''),\n        comma: line.endsWith(','),\n      }));\n      const lLength = resultLeft.length;\n      const rLength = resultRight.length;\n      resultLeft = concat(resultLeft, Array(rLength).fill(0).map(() => ({ ...EQUAL_EMPTY_LINE })));\n      resultRight = concat(resultRight, Array(lLength).fill(0).map(() => ({ ...EQUAL_EMPTY_LINE })), true);\n    } else if (typeLeft === 'object') {\n      [resultLeft, resultRight] = diffObject(sourceLeft, sourceRight, 1, this.options, this.arrayDiffFunc);\n      resultLeft.unshift({ ...EQUAL_LEFT_BRACKET_LINE });\n      resultLeft.push({ ...EQUAL_RIGHT_BRACKET_LINE });\n      resultRight.unshift({ ...EQUAL_LEFT_BRACKET_LINE });\n      resultRight.push({ ...EQUAL_RIGHT_BRACKET_LINE });\n    } else if (typeLeft === 'array') {\n      [resultLeft, resultRight] = this.arrayDiffFunc(sourceLeft, sourceRight, '', '', 0, this.options);\n    } else if (sourceLeft !== sourceRight) {\n      if (this.options.ignoreCase) {\n        if (\n          typeof sourceLeft === 'string' &&\n          typeof sourceRight === 'string' &&\n          sourceLeft.toLowerCase() === sourceRight.toLowerCase()\n        ) {\n          resultLeft = [{ level: 0, type: 'equal', text: sourceLeft }];\n          resultRight = [{ level: 0, type: 'equal', text: sourceRight }];\n        }\n      } else if (this.options.showModifications) {\n        resultLeft = [{\n          level: 0,\n          type: 'modify',\n          text: stringify(sourceLeft, undefined, undefined, this.options.maxDepth, this.options.undefinedBehavior),\n        }];\n        resultRight = [{\n          level: 0,\n          type: 'modify',\n          text: stringify(sourceRight, undefined, undefined, this.options.maxDepth, this.options.undefinedBehavior),\n        }];\n      } else {\n        resultLeft = [\n          {\n            level: 0,\n            type: 'remove',\n            text: stringify(sourceLeft, undefined, undefined, this.options.maxDepth, this.options.undefinedBehavior),\n          },\n          { ...EQUAL_EMPTY_LINE },\n        ];\n        resultRight = [\n          { ...EQUAL_EMPTY_LINE },\n          {\n            level: 0,\n            type: 'add',\n            text: stringify(sourceRight, undefined, undefined, this.options.maxDepth, this.options.undefinedBehavior),\n          },\n        ];\n      }\n    } else {\n      resultLeft = [{\n        level: 0,\n        type: 'equal',\n        text: stringify(sourceLeft, undefined, undefined, this.options.maxDepth, this.options.undefinedBehavior),\n      }];\n      resultRight = [{\n        level: 0,\n        type: 'equal',\n        text: stringify(sourceRight, undefined, undefined, this.options.maxDepth, this.options.undefinedBehavior),\n      }];\n    }\n\n    this.sortResultLines(resultLeft, resultRight);\n\n    this.calculateLineNumbers(resultLeft);\n    this.calculateLineNumbers(resultRight);\n\n    this.calculateCommas(resultLeft);\n    this.calculateCommas(resultRight);\n\n    return [resultLeft, resultRight] as const;\n  }\n}\n\nexport default Differ;\n"
  },
  {
    "path": "src/index.ts",
    "content": "import Differ from './differ';\nimport Viewer from './viewer';\n\nexport type {\n  InlineDiffOptions,\n  InlineDiffResult,\n} from './utils/get-inline-diff';\n\nexport type {\n  ArrayDiffFunc,\n  DifferOptions,\n  DiffResult,\n} from './differ';\n\nexport type {\n  ViewerProps,\n} from './viewer';\n\nexport { Differ, Viewer };\n"
  },
  {
    "path": "src/utils/array-bracket-utils.ts",
    "content": "import type { DiffResult } from '../differ';\n\n// Shared utility for array diff\nexport const addArrayOpeningBrackets = (\n  linesLeft: DiffResult[],\n  linesRight: DiffResult[],\n  keyLeft: string,\n  keyRight: string,\n  level: number\n) => {\n  if (keyLeft && keyRight) {\n    linesLeft.push({ level, type: 'equal', text: `\"${keyLeft}\": [` });\n    linesRight.push({ level, type: 'equal', text: `\"${keyRight}\": [` });\n  } else {\n    linesLeft.push({ level, type: 'equal', text: '[' });\n    linesRight.push({ level, type: 'equal', text: '[' });\n  }\n};\n\nexport const addArrayClosingBrackets = (\n  linesLeft: DiffResult[],\n  linesRight: DiffResult[],\n  level: number\n) => {\n  linesLeft.push({ level, type: 'equal', text: ']' });\n  linesRight.push({ level, type: 'equal', text: ']' });\n};\n\nexport const addMaxDepthPlaceholder = (\n  linesLeft: DiffResult[],\n  linesRight: DiffResult[],\n  level: number\n) => {\n  linesLeft.push({ level: level + 1, type: 'equal', text: '...' });\n  linesRight.push({ level: level + 1, type: 'equal', text: '...' });\n}; "
  },
  {
    "path": "src/utils/calculate-placeholder-height.ts",
    "content": "import type { SegmentItem, HiddenUnchangedLinesInfo } from './get-segments';\nimport { isExpandLine } from './segment-util';\n\nconst calculatePlaceholderHeight = (\n  segments: Array<SegmentItem | HiddenUnchangedLinesInfo>,\n  accTop: number[],\n  startSegment: number,\n  startLine: number,\n  endSegment: number,\n  endLine: number,\n  itemHeight: number,\n  expandLineHeight: number,\n  totalHeight: number,\n) => {\n  if (!accTop.length) {\n    return [0, 0];\n  }\n  let topHeight = 0;\n  let bottomHeight = 0;\n  const startSegmentItem = segments[startSegment];\n  if (isExpandLine(startSegmentItem)) {\n    topHeight = accTop[startSegment];\n  } else {\n    topHeight = accTop[startSegment] + (startLine - startSegmentItem.start) * itemHeight;\n  }\n  const endSegmentItem = segments[endSegment];\n  if (isExpandLine(endSegmentItem)) {\n    bottomHeight = totalHeight - accTop[endSegment] - expandLineHeight;\n  } else {\n    bottomHeight = totalHeight - accTop[endSegment] - (endLine - endSegmentItem.start) * itemHeight;\n  }\n  return [topHeight, bottomHeight];\n};\n\nexport default calculatePlaceholderHeight;\n"
  },
  {
    "path": "src/utils/clean-fields.ts",
    "content": "// Keep only the fields that are valid in JSON\nconst cleanFields = (obj: unknown) => {\n  if (\n    typeof obj === 'undefined' ||\n    obj === null ||\n    typeof obj === 'bigint' ||\n    Number.isNaN(obj) ||\n    obj === Infinity ||\n    obj === -Infinity\n  ) {\n    return undefined;\n  }\n  if (['string', 'number', 'boolean'].includes(typeof obj)) {\n    return obj;\n  }\n  if (Array.isArray(obj)) {\n    return obj.map(cleanFields).filter(t => typeof t !== 'undefined');\n  }\n  const result = {};\n  for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {\n    const cleaned = cleanFields(value);\n    if (typeof cleaned !== 'undefined') {\n      result[key] = cleaned;\n    }\n  }\n  return result;\n};\n\nexport default cleanFields;\n"
  },
  {
    "path": "src/utils/cmp.spec.ts",
    "content": "import cmp from './cmp';\n\ndescribe('Utility function: cmp', () => {\n  it('should respect the order', () => {\n    const arr = [true, false, 1, '1', null, [1, 2, 3], { a: 1, b: 2 }];\n    arr.sort(() => Math.random() > 0.5 ? 1 : -1);\n    arr.sort((x, y) => cmp(x, y, {}));\n    expect(arr).toEqual([false, true, 1, '1', null, [1, 2, 3], { a: 1, b: 2 }]);\n  });\n\n  it('should correctly handle `ignoreCase`', () => {\n    const arr = ['a', 'B', 'c', 'D', 'e', 'F'];\n\n    arr.sort(() => Math.random() > 0.5 ? 1 : -1);\n    arr.sort((x, y) => cmp(x, y, {}));\n    expect(arr).toEqual(['B', 'D', 'F', 'a', 'c', 'e']);\n\n    arr.sort(() => Math.random() > 0.5 ? 1 : -1);\n    arr.sort((x, y) => cmp(x, y, { ignoreCase: true }));\n    expect(arr).toEqual(['a', 'B', 'c', 'D', 'e', 'F']);\n  });\n});\n"
  },
  {
    "path": "src/utils/cmp.ts",
    "content": "interface CmpOptions {\n  ignoreCase?: boolean;\n  keyOrdersMap?: Map<string, number>;\n}\n\nconst getOrderByType = (value: any) => {\n  if (typeof value === 'boolean') {\n    return 0;\n  }\n  if (typeof value === 'number') {\n    return 1;\n  }\n  if (typeof value === 'string') {\n    return 2;\n  }\n  if (value === null) {\n    return 3;\n  }\n  if (Array.isArray(value)) {\n    return 4;\n  }\n  if (typeof value === 'object') {\n    return 5;\n  }\n  if (typeof value === 'symbol') {\n    return 6;\n  }\n  if (typeof value === 'function') {\n    return 7;\n  }\n  if (typeof value === 'bigint') {\n    return 8;\n  }\n  return -1;\n};\n\n/**\n * The compare function to correct the order for \"array\" or \"object\":\n * - The order for 2 values with different types are: boolean, number, string, null, array, object.\n * - The order for 2 values with the same type is according to the type:\n *   - For boolean, number, string: use the `<` sign.\n *   - For array and object: preserve the original order (or do we have a better idea?)\n */\nconst cmp = (a: any, b: any, options: CmpOptions) => {\n  const orderByMapA = options.keyOrdersMap?.get(a);\n  const orderByMapB = options.keyOrdersMap?.get(b);\n  if (orderByMapA !== undefined && orderByMapB !== undefined) {\n    return orderByMapA - orderByMapB;\n  }\n\n  const orderByTypeA = getOrderByType(a);\n  const orderByTypeB = getOrderByType(b);\n\n  if (orderByTypeA !== orderByTypeB) {\n    return orderByTypeA - orderByTypeB;\n  }\n\n  if (a === null && b === null || Array.isArray(a) && Array.isArray(b) || orderByTypeA === 5 && orderByTypeB === 5) {\n    return 0;\n  }\n\n  switch (typeof a) {\n    case 'number':\n      if (\n        Number.isNaN(a) && Number.isNaN(b) ||\n        a === Infinity && b === Infinity ||\n        a === -Infinity && b === -Infinity\n      ) {\n        return 0;\n      }\n      return a - b;\n    case 'string':\n      if (options.ignoreCase) {\n        a = a.toLowerCase();\n        b = b.toLowerCase();\n      }\n      return a < b ? -1 : a > b ? 1 : 0;\n    case 'boolean':\n      return (+a) - (+b);\n    case 'symbol':\n    case 'function':\n      return String(a).localeCompare(String(b));\n  }\n\n  if (typeof a === 'bigint' && typeof b === 'bigint') {\n    const result = BigInt(a) - BigInt(b);\n    return result < 0 ? -1 : result > 0 ? 1 : 0;\n  }\n\n  return String(a).localeCompare(String(b));\n};\n\nexport default cmp;\n"
  },
  {
    "path": "src/utils/concat.spec.ts",
    "content": "import concat from './concat';\n\ndescribe('Utility function: concat', () => {\n  it('should work for `append` mode', () => {\n    expect(concat([1, 2, 3], ['a', 'b', 'c'])).toEqual([1, 2, 3, 'a', 'b', 'c']);\n  });\n\n  it('should work for `prepend` mode (`unshift` mode)', () => {\n    expect(concat([1, 2, 3], ['a', 'b', 'c'], true)).toEqual(['c', 'b', 'a', 1, 2, 3]);\n  });\n\n  it('should throw error when parameter is not an array', () => {\n    expect(() => concat(1 as any, ['a', 'b', 'c'])).toThrowError();\n    expect(() => concat([1, 2, 3], 'abc' as any)).toThrowError();\n  });\n});\n"
  },
  {
    "path": "src/utils/concat.ts",
    "content": "/**\n * If we use `a.push(...b)`, it will result in `Maximum call stack size exceeded` error.\n * The reason is unclear, it may be a bug of V8, so we should implement a push method by ourselves.\n */\nconst concat = <T, U>(a: T[], b: U[], prependEach = false): (T | U)[] => {\n  if (!Array.isArray(a) || !Array.isArray(b)) {\n    throw new Error('Both arguments should be arrays.');\n  }\n  const lenA = a.length;\n  const lenB = b.length;\n  const len = lenA + lenB;\n  const result = new Array(len);\n  if (prependEach) {\n    for (let i = 0; i < lenB; i++) {\n      result[i] = b[lenB - i - 1];\n    }\n    for (let i = 0; i < lenA; i++) {\n      result[i + lenB] = a[i];\n    }\n    return result;\n  }\n  for (let i = 0; i < lenA; i++) {\n    result[i] = a[i];\n  }\n  for (let i = 0; i < lenB; i++) {\n    result[i + lenA] = b[i];\n  }\n  return result;\n};\n\nexport default concat;\n"
  },
  {
    "path": "src/utils/detect-circular.ts",
    "content": "const detectCircular = (value: any, map: Map<any, boolean> = new Map()) => {\n  // primitive types should not be checked\n  if (typeof value !== 'object' || value === null) {\n    return false;\n  }\n\n  // value has appeared\n  if (map.has(value)) {\n    return true;\n  }\n  map.set(value, true);\n\n  // value is an array\n  if (Array.isArray(value)) {\n    for (let i = 0; i < value.length; i++) {\n      if (detectCircular(value[i], map)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  // value is an object\n  for (const key in value) {\n    if (detectCircular(value[key], map)) {\n      return true;\n    }\n  }\n  return false;\n};\n\nexport default detectCircular;\n"
  },
  {
    "path": "src/utils/diff-array-compare-key.ts",
    "content": "import type { DiffResult, DifferOptions } from '../differ';\nimport concat from './concat';\nimport formatValue from './format-value';\nimport diffObject from './diff-object';\nimport getType from './get-type';\nimport isEqual from './is-equal';\nimport prettyAppendLines from './pretty-append-lines';\nimport stringify from './stringify';\nimport diffArrayNormal from './diff-array-normal';\nimport { addArrayClosingBrackets, addArrayOpeningBrackets, addMaxDepthPlaceholder } from './array-bracket-utils';\n\n// Recursively checks if all objects (including in nested arrays) have the compare key\nfunction allObjectsHaveCompareKey(arr: any[], compareKey: string): boolean {\n  for (const item of arr) {\n    const type = getType(item);\n    if (type === 'object') {\n      if (!(compareKey in item)) return false;\n      // Check nested arrays in object values\n      for (const value of Object.values(item)) {\n        if (Array.isArray(value) && !allObjectsHaveCompareKey(value, compareKey)) {\n          return false;\n        }\n      }\n    } else if (Array.isArray(item)) {\n      if (!allObjectsHaveCompareKey(item, compareKey)) return false;\n    }\n  }\n  return true;\n}\n\n// Recursively diff arrays, using compareKey if all elements have it, otherwise fallback to diffArrayNormal\nfunction diffArrayRecursive(\n  arrLeft: any[],\n  arrRight: any[],\n  keyLeft: string,\n  keyRight: string,\n  level: number,\n  options: DifferOptions,\n  linesLeft: DiffResult[] = [],\n  linesRight: DiffResult[] = [],\n): [DiffResult[], DiffResult[]] {\n  if (!options.compareKey) {\n    // Fallback to normal diff if no compare key is specified\n    return diffArrayNormal(arrLeft, arrRight, keyLeft, keyRight, level, options, linesLeft, linesRight);\n  }\n\n  // If arrays are not of objects, or not all objects have the compare key (including nested), fallback to unordered LCS diff\n  const isObjectArray = (arr: any[]) => arr.every(item => getType(item) === 'object');\n  if (!isObjectArray(arrLeft) || !isObjectArray(arrRight) ||\n      !allObjectsHaveCompareKey(arrLeft, options.compareKey) ||\n      !allObjectsHaveCompareKey(arrRight, options.compareKey)) {\n    // Use unordered LCS for arrays of primitives, mixed types, or missing compare key\n    return diffArrayNormal(arrLeft, arrRight, keyLeft, keyRight, level, options, linesLeft, linesRight);\n  }\n\n  addArrayOpeningBrackets(linesLeft, linesRight, keyLeft, keyRight, level);\n\n  if (level >= (options.maxDepth || Infinity)) {\n    addMaxDepthPlaceholder(linesLeft, linesRight, level);\n  } else {\n    const leftProcessed = new Set<number>();\n    const rightProcessed = new Set<number>();\n    \n    // First pass: find matching objects by compareKey\n    for (let i = 0; i < arrLeft.length; i++) {\n      const leftItem = arrLeft[i];\n      if (leftProcessed.has(i)) continue;\n      \n      // Skip if left item is not an object or doesn't have the compare key\n      if (getType(leftItem) !== 'object' || !(options.compareKey in leftItem)) {\n        continue;\n      }\n      \n      const leftKeyValue = leftItem[options.compareKey];\n      \n      // Find matching item in right array\n      let matchIndex = -1;\n      for (let j = 0; j < arrRight.length; j++) {\n        if (rightProcessed.has(j)) continue;\n        \n        const rightItem = arrRight[j];\n        if (getType(rightItem) !== 'object' || !(options.compareKey in rightItem)) {\n          continue;\n        }\n        \n        const rightKeyValue = rightItem[options.compareKey];\n        \n        // Compare key values\n        if (isEqual(leftKeyValue, rightKeyValue, options)) {\n          matchIndex = j;\n          break;\n        }\n      }\n      \n      if (matchIndex !== -1) {\n        // Found a match, compare the objects\n        const rightItem = arrRight[matchIndex];\n        const leftType = getType(leftItem);\n        const rightType = getType(rightItem);\n        \n        if (leftType !== rightType) {\n          prettyAppendLines(\n            linesLeft,\n            linesRight,\n            '',\n            '',\n            leftItem,\n            rightItem,\n            level + 1,\n            options,\n          );\n        } else if (leftType === 'object') {\n          // Always recurse into diffObject for aligned objects, regardless of recursiveEqual/isEqual\n          linesLeft.push({ level: level + 1, type: 'equal', text: '{' });\n          linesRight.push({ level: level + 1, type: 'equal', text: '{' });\n          // For each key, if value is array, apply recursive diff logic\n          const keys = Array.from(new Set([...Object.keys(leftItem), ...Object.keys(rightItem)]));\n          for (const key of keys) {\n            const lVal = leftItem[key];\n            const rVal = rightItem[key];\n            if (Array.isArray(lVal) && Array.isArray(rVal)) {\n              // Recursively diff arrays\n              const [arrL, arrR] = diffArrayRecursive(lVal, rVal, key, key, level + 2, options, [], []);\n              linesLeft = concat(linesLeft, arrL);\n              linesRight = concat(linesRight, arrR);\n            } else if (Array.isArray(lVal) || Array.isArray(rVal)) {\n              // If only one side is array, treat as modification\n              prettyAppendLines(\n                linesLeft,\n                linesRight,\n                key,\n                key,\n                lVal,\n                rVal,\n                level + 2,\n                options,\n              );\n            } else {\n              // Use diffObject for non-array values\n              const [leftLines, rightLines] = diffObject(\n                { [key]: lVal },\n                { [key]: rVal },\n                level + 2,\n                options,\n                diffArrayRecursive\n              );\n              linesLeft = concat(linesLeft, leftLines);\n              linesRight = concat(linesRight, rightLines);\n            }\n          }\n          linesLeft.push({ level: level + 1, type: 'equal', text: '}' });\n          linesRight.push({ level: level + 1, type: 'equal', text: '}' });\n        } else if (leftType === 'array') {\n          // For nested arrays, recursively apply the same logic\n          const [resLeft, resRight] = diffArrayRecursive(leftItem, rightItem, '', '', level + 1, options, [], []);\n          linesLeft = concat(linesLeft, resLeft);\n          linesRight = concat(linesRight, resRight);\n        } else if (isEqual(leftItem, rightItem, options)) {\n          linesLeft.push({\n            level: level + 1,\n            type: 'equal',\n            text: formatValue(leftItem, undefined, undefined, options.undefinedBehavior),\n          });\n          linesRight.push({\n            level: level + 1,\n            type: 'equal',\n            text: formatValue(rightItem, undefined, undefined, options.undefinedBehavior),\n          });\n        } else {\n          if (options.showModifications) {\n            linesLeft.push({\n              level: level + 1,\n              type: 'modify',\n              text: formatValue(leftItem, undefined, undefined, options.undefinedBehavior),\n            });\n            linesRight.push({\n              level: level + 1,\n              type: 'modify',\n              text: formatValue(rightItem, undefined, undefined, options.undefinedBehavior),\n            });\n          } else {\n            linesLeft.push({\n              level: level + 1,\n              type: 'remove',\n              text: formatValue(leftItem, undefined, undefined, options.undefinedBehavior),\n            });\n            linesLeft.push({ level: level + 1, type: 'equal', text: '' });\n            linesRight.push({ level: level + 1, type: 'equal', text: '' });\n            linesRight.push({\n              level: level + 1,\n              type: 'add',\n              text: formatValue(rightItem, undefined, undefined, options.undefinedBehavior),\n            });\n          }\n        }\n        \n        leftProcessed.add(i);\n        rightProcessed.add(matchIndex);\n      }\n    }\n    \n    // Second pass: handle remaining items (unmatched)\n    for (let i = 0; i < arrLeft.length; i++) {\n      if (leftProcessed.has(i)) continue;\n      \n      const leftItem = arrLeft[i];\n      const removedLines = stringify(leftItem, undefined, 1, undefined, options.undefinedBehavior).split('\\n');\n      for (let j = 0; j < removedLines.length; j++) {\n        linesLeft.push({\n          level: level + 1 + (removedLines[j].match(/^\\s+/)?.[0]?.length || 0),\n          type: 'remove',\n          text: removedLines[j].replace(/^\\s+/, '').replace(/,$/g, ''),\n        });\n        linesRight.push({ level: level + 1, type: 'equal', text: '' });\n      }\n    }\n    \n    for (let i = 0; i < arrRight.length; i++) {\n      if (rightProcessed.has(i)) continue;\n      \n      const rightItem = arrRight[i];\n      const addedLines = stringify(rightItem, undefined, 1, undefined, options.undefinedBehavior).split('\\n');\n      for (let j = 0; j < addedLines.length; j++) {\n        linesLeft.push({ level: level + 1, type: 'equal', text: '' });\n        linesRight.push({\n          level: level + 1 + (addedLines[j].match(/^\\s+/)?.[0]?.length || 0),\n          type: 'add',\n          text: addedLines[j].replace(/^\\s+/, '').replace(/,$/g, ''),\n        });\n      }\n    }\n  }\n\n  addArrayClosingBrackets(linesLeft, linesRight, level);\n  return [linesLeft, linesRight];\n}\n\nconst diffArrayCompareKey = diffArrayRecursive;\n\nexport default diffArrayCompareKey;\nexport { allObjectsHaveCompareKey }; "
  },
  {
    "path": "src/utils/diff-array-lcs.ts",
    "content": "import type { DifferOptions, DiffResult } from '../differ';\nimport formatValue from './format-value';\nimport diffObject from './diff-object';\nimport getType from './get-type';\nimport stringify from './stringify';\n\nimport isEqual from './is-equal';\nimport shallowSimilarity from './shallow-similarity';\nimport concat from './concat';\nimport prettyAppendLines from './pretty-append-lines';\nimport { addArrayClosingBrackets, addArrayOpeningBrackets, addMaxDepthPlaceholder } from './array-bracket-utils';\n\nconst lcs = (\n  arrLeft: any[],\n  arrRight: any[],\n  keyLeft: string,\n  keyRight: string,\n  level: number,\n  options: DifferOptions,\n): [DiffResult[], DiffResult[]] => {\n  const f = Array(arrLeft.length + 1).fill(0).map(() => Array(arrRight.length + 1).fill(0));\n  const backtrack = Array(arrLeft.length + 1).fill(0).map(() => Array(arrRight.length + 1).fill(0));\n\n  for (let i = 1; i <= arrLeft.length; i++) {\n    backtrack[i][0] = 'up';\n  }\n  for (let j = 1; j <= arrRight.length; j++) {\n    backtrack[0][j] = 'left';\n  }\n  for (let i = 1; i <= arrLeft.length; i++) {\n    for (let j = 1; j <= arrRight.length; j++) {\n      const typeI = getType(arrLeft[i - 1]);\n      const typeJ = getType(arrRight[j - 1]);\n      if (typeI === typeJ && (typeI === 'array' || typeI === 'object')) {\n        if (options.recursiveEqual) {\n          if (\n            isEqual(arrLeft[i - 1], arrRight[j - 1], options) ||\n            shallowSimilarity(arrLeft[i - 1], arrRight[j - 1]) > 0.5\n          ) {\n            f[i][j] = f[i - 1][j - 1] + 1;\n            backtrack[i][j] = 'diag';\n          } else if (f[i - 1][j] >= f[i][j - 1]) {\n            f[i][j] = f[i - 1][j];\n            backtrack[i][j] = 'up';\n          } else {\n            f[i][j] = f[i][j - 1];\n            backtrack[i][j] = 'left';\n          }\n        } else {\n          // this is a diff-specific logic, when 2 values are both arrays or both objects, the\n          // algorithm should assume they are equal in order to diff recursively later\n          f[i][j] = f[i - 1][j - 1] + 1;\n          backtrack[i][j] = 'diag';\n        }\n      } else if (isEqual(arrLeft[i - 1], arrRight[j - 1], options)) {\n        f[i][j] = f[i - 1][j - 1] + 1;\n        backtrack[i][j] = 'diag';\n      } else if (f[i - 1][j] >= f[i][j - 1]) {\n        f[i][j] = f[i - 1][j];\n        backtrack[i][j] = 'up';\n      } else {\n        f[i][j] = f[i][j - 1];\n        backtrack[i][j] = 'left';\n      }\n    }\n  }\n\n  let i = arrLeft.length;\n  let j = arrRight.length;\n  let tLeft: DiffResult[] = [];\n  let tRight: DiffResult[] = [];\n  // this is a backtracking process, all new lines should be unshifted to the result, not\n  // pushed to the result\n  while (i > 0 || j > 0) {\n    if (backtrack[i][j] === 'diag') {\n      const type = getType(arrLeft[i - 1]);\n      if (\n        options.recursiveEqual &&\n        (type === 'array' || type === 'object') &&\n        isEqual(arrLeft[i - 1], arrRight[j - 1], options)\n      ) {\n        const reversedLeft = [];\n        const reversedRight = [];\n        prettyAppendLines(\n          reversedLeft,\n          reversedRight,\n          '',\n          '',\n          arrLeft[i - 1],\n          arrRight[j - 1],\n          level + 1,\n          options,\n        );\n        tLeft = concat(tLeft, reversedLeft.reverse(), true);\n        tRight = concat(tRight, reversedRight.reverse(), true);\n      } else if (type === 'array') {\n        const [l, r] = diffArrayLCS(arrLeft[i - 1], arrRight[j - 1], keyLeft, keyRight, level + 1, options);\n        tLeft = concat(tLeft, l.reverse(), true);\n        tRight = concat(tRight, r.reverse(), true);\n      } else if (type === 'object') {\n        const [l, r] = diffObject(arrLeft[i - 1], arrRight[j - 1], level + 2, options, diffArrayLCS);\n        tLeft.unshift({ level: level + 1, type: 'equal', text: '}' });\n        tRight.unshift({ level: level + 1, type: 'equal', text: '}' });\n        tLeft = concat(tLeft, l.reverse(), true);\n        tRight = concat(tRight, r.reverse(), true);\n        tLeft.unshift({ level: level + 1, type: 'equal', text: '{' });\n        tRight.unshift({ level: level + 1, type: 'equal', text: '{' });\n      } else {\n        const reversedLeft = [];\n        const reversedRight = [];\n        prettyAppendLines(\n          reversedLeft,\n          reversedRight,\n          '',\n          '',\n          arrLeft[i - 1],\n          arrRight[j - 1],\n          level + 1,\n          options,\n        );\n        tLeft = concat(tLeft, reversedLeft.reverse(), true);\n        tRight = concat(tRight, reversedRight.reverse(), true);\n      }\n      i--;\n      j--;\n    } else if (backtrack[i][j] === 'up') {\n      if (options.showModifications && i > 1 && backtrack[i - 1][j] === 'left') {\n        const typeLeft = getType(arrLeft[i - 1]);\n        const typeRight = getType(arrRight[j - 1]);\n        if (typeLeft === typeRight) {\n          if (typeLeft === 'array') {\n            const [l, r] = diffArrayLCS(arrLeft[i - 1], arrRight[j - 1], keyLeft, keyRight, level + 1, options);\n            tLeft = concat(tLeft, l.reverse(), true);\n            tRight = concat(tRight, r.reverse(), true);\n          } else if (typeLeft === 'object') {\n            const [l, r] = diffObject(arrLeft[i - 1], arrRight[j - 1], level + 2, options, diffArrayLCS);\n            tLeft.unshift({ level: level + 1, type: 'equal', text: '}' });\n            tRight.unshift({ level: level + 1, type: 'equal', text: '}' });\n            tLeft = concat(tLeft, l.reverse(), true);\n            tRight = concat(tRight, r.reverse(), true);\n            tLeft.unshift({ level: level + 1, type: 'equal', text: '{' });\n            tRight.unshift({ level: level + 1, type: 'equal', text: '{' });\n          } else {\n            tLeft.unshift({\n              level: level + 1,\n              type: 'modify',\n              text: formatValue(arrLeft[i - 1], undefined, undefined, options.undefinedBehavior),\n            });\n            tRight.unshift({\n              level: level + 1,\n              type: 'modify',\n              text: formatValue(arrRight[j - 1], undefined, undefined, options.undefinedBehavior),\n            });\n          }\n        } else {\n          const reversedLeft = [];\n          const reversedRight = [];\n          prettyAppendLines(\n            reversedLeft,\n            reversedRight,\n            '',\n            '',\n            arrLeft[i - 1],\n            arrRight[j - 1],\n            level + 1,\n            options,\n          );\n          tLeft = concat(tLeft, reversedLeft.reverse(), true);\n          tRight = concat(tRight, reversedRight.reverse(), true);\n        }\n        i--;\n        j--;\n      } else {\n        const removedLines = stringify(arrLeft[i - 1], undefined, 1, undefined, options.undefinedBehavior).split('\\n');\n        for (let i = removedLines.length - 1; i >= 0; i--) {\n          tLeft.unshift({\n            level: level + 1 + (removedLines[i].match(/^\\s+/)?.[0]?.length || 0),\n            type: 'remove',\n            text: removedLines[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n          });\n          tRight.unshift({ level: level + 1, type: 'equal', text: '' });\n        }\n        i--;\n      }\n    } else {\n      const addedLines = stringify(arrRight[j - 1], undefined, 1, undefined, options.undefinedBehavior).split('\\n');\n      for (let i = addedLines.length - 1; i >= 0; i--) {\n        tLeft.unshift({ level: level + 1, type: 'equal', text: '' });\n        tRight.unshift({\n          level: level + 1 + (addedLines[i].match(/^\\s+/)?.[0]?.length || 0),\n          type: 'add',\n          text: addedLines[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n        });\n      }\n      j--;\n    }\n  }\n\n  return [tLeft, tRight];\n};\n\nconst diffArrayLCS = (\n  arrLeft: any[],\n  arrRight: any[],\n  keyLeft: string,\n  keyRight: string,\n  level: number,\n  options: DifferOptions,\n  linesLeft: DiffResult[] = [],\n  linesRight: DiffResult[] = [],\n): [DiffResult[], DiffResult[]] => {\n  addArrayOpeningBrackets(linesLeft, linesRight, keyLeft, keyRight, level)\n\n  if (level >= (options.maxDepth || Infinity)) {\n    addMaxDepthPlaceholder(linesLeft, linesRight, level);\n  } else {\n    const [tLeftReverse, tRightReverse] = lcs(arrLeft, arrRight, keyLeft, keyRight, level, options);\n    linesLeft = concat(linesLeft, tLeftReverse);\n    linesRight = concat(linesRight, tRightReverse);\n  }\n\n  addArrayClosingBrackets(linesLeft, linesRight, level)\n  return [linesLeft, linesRight];\n};\n\nexport default diffArrayLCS;\n"
  },
  {
    "path": "src/utils/diff-array-normal.ts",
    "content": "import type { DiffResult, DifferOptions } from '../differ';\n\nimport concat from './concat';\nimport formatValue from './format-value';\nimport diffObject from './diff-object';\nimport getType from './get-type';\nimport isEqual from './is-equal';\nimport prettyAppendLines from './pretty-append-lines';\nimport cmp from './cmp';\nimport { addArrayClosingBrackets, addArrayOpeningBrackets, addMaxDepthPlaceholder } from './array-bracket-utils';\nimport diffArrayCompareKey, { allObjectsHaveCompareKey } from './diff-array-compare-key';\nimport { diffObjectWithArraySupport } from './diff-object-with-array-support';\n\nconst diffArrayNormal = (\n  arrLeft: any[],\n  arrRight: any[],\n  keyLeft: string,\n  keyRight: string,\n  level: number,\n  options: DifferOptions,\n  linesLeft: DiffResult[] = [],\n  linesRight: DiffResult[] = [],\n): [DiffResult[], DiffResult[]] => {\n  arrLeft = [...arrLeft];\n  arrRight = [...arrRight];\n  addArrayOpeningBrackets(linesLeft, linesRight, keyLeft, keyRight, level)\n\n  if (level >= (options.maxDepth || Infinity)) {\n    addMaxDepthPlaceholder(linesLeft, linesRight, level);\n  } else {\n    while (arrLeft.length || arrRight.length) {\n      const itemLeft = arrLeft[0];\n      const itemRight = arrRight[0];\n      const leftType = getType(itemLeft);\n      const rightType = getType(itemRight);\n      if (arrLeft.length && arrRight.length) {\n        if (leftType !== rightType) {\n          prettyAppendLines(\n            linesLeft,\n            linesRight,\n            '',\n            '',\n            itemLeft,\n            itemRight,\n            level + 1,\n            options,\n          );\n        } else if (\n          options.recursiveEqual &&\n          ['object', 'array'].includes(leftType) &&\n          isEqual(itemLeft, itemRight, options)\n        ) {\n          prettyAppendLines(\n            linesLeft,\n            linesRight,\n            '',\n            '',\n            itemLeft,\n            itemRight,\n            level + 1,\n            options,\n          );\n        } else if (leftType === 'object') {\n          linesLeft.push({ level: level + 1, type: 'equal', text: '{' });\n          linesRight.push({ level: level + 1, type: 'equal', text: '{' });\n          let objLeft, objRight;\n          if (options.arrayDiffMethod === 'compare-key') {\n            [objLeft, objRight] = diffObjectWithArraySupport(\n              itemLeft,\n              itemRight,\n              level,\n              options,\n              diffArrayNormal,\n              diffArrayCompareKey,\n              allObjectsHaveCompareKey\n            );\n          } else {\n            [objLeft, objRight] = diffObject(\n              itemLeft,\n              itemRight,\n              level + 2,\n              options,\n              diffArrayNormal\n            );\n          }\n          linesLeft = concat(linesLeft, objLeft);\n          linesRight = concat(linesRight, objRight);\n          linesLeft.push({ level: level + 1, type: 'equal', text: '}' });\n          linesRight.push({ level: level + 1, type: 'equal', text: '}' });\n        } else if (leftType === 'array') {\n          // For nested arrays, check for compare-key logic\n          if (\n            options.compareKey &&\n            allObjectsHaveCompareKey(itemLeft, options.compareKey) &&\n            allObjectsHaveCompareKey(itemRight, options.compareKey)\n          ) {\n            const [resLeft, resRight] = diffArrayCompareKey(itemLeft, itemRight, '', '', level + 1, options, [], []);\n            linesLeft = concat(linesLeft, resLeft);\n            linesRight = concat(linesRight, resRight);\n          } else {\n            const [resLeft, resRight] = diffArrayNormal(itemLeft, itemRight, '', '', level + 1, options, [], []);\n            linesLeft = concat(linesLeft, resLeft);\n            linesRight = concat(linesRight, resRight);\n          }\n        } else if (cmp(itemLeft, itemRight, { ignoreCase: options.ignoreCase }) === 0) {\n          linesLeft.push({\n            level: level + 1,\n            type: 'equal',\n            text: formatValue(itemLeft, undefined, undefined, options.undefinedBehavior),\n          });\n          linesRight.push({\n            level: level + 1,\n            type: 'equal',\n            text: formatValue(itemRight, undefined, undefined, options.undefinedBehavior),\n          });\n        } else {\n          if (options.showModifications) {\n            linesLeft.push({\n              level: level + 1,\n              type: 'modify',\n              text: formatValue(itemLeft, undefined, undefined, options.undefinedBehavior),\n            });\n            linesRight.push({\n              level: level + 1,\n              type: 'modify',\n              text: formatValue(itemRight, undefined, undefined, options.undefinedBehavior),\n            });\n          } else {\n            linesLeft.push({\n              level: level + 1,\n              type: 'remove',\n              text: formatValue(itemLeft, undefined, undefined, options.undefinedBehavior),\n            });\n            linesLeft.push({ level: level + 1, type: 'equal', text: '' });\n            linesRight.push({ level: level + 1, type: 'equal', text: '' });\n            linesRight.push({\n              level: level + 1,\n              type: 'add',\n              text: formatValue(itemRight, undefined, undefined, options.undefinedBehavior),\n            });\n          }\n        }\n        arrLeft.shift();\n        arrRight.shift();\n      } else if (arrLeft.length) {\n        const removedLines = formatValue(itemLeft, undefined, true, options.undefinedBehavior).split('\\n');\n        for (let i = 0; i < removedLines.length; i++) {\n          linesLeft.push({\n            level: level + 1 + (removedLines[i].match(/^\\s+/)?.[0]?.length || 0),\n            type: 'remove',\n            text: removedLines[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n          });\n          linesRight.push({ level: level + 1, type: 'equal', text: '' });\n        }\n        arrLeft.shift();\n      } else if (arrRight.length) {\n        const addedLines = formatValue(itemRight, undefined, true, options.undefinedBehavior).split('\\n');\n        for (let i = 0; i < addedLines.length; i++) {\n          linesLeft.push({ level: level + 1, type: 'equal', text: '' });\n          linesRight.push({\n            level: level + 1 + (addedLines[i].match(/^\\s+/)?.[0]?.length || 0),\n            type: 'add',\n            text: addedLines[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n          });\n        }\n        arrRight.shift();\n      }\n    }\n  }\n\n  addArrayClosingBrackets(linesLeft, linesRight, level)\n  return [linesLeft, linesRight];\n};\n\nexport default diffArrayNormal;\n"
  },
  {
    "path": "src/utils/diff-object-with-array-support.ts",
    "content": "import type { DiffResult, DifferOptions } from '../differ';\nimport prettyAppendLines from './pretty-append-lines';\nimport diffObject from './diff-object';\nimport concat from './concat';\n\n/**\n * Diffs two objects, using compare-key logic for nested arrays if possible.\n *\n * @param leftObj The left object\n * @param rightObj The right object\n * @param level The current diff level\n * @param options The diff options\n * @param fallbackArrayDiff The fallback array diff function (e.g., diffArrayNormal or diffArrayLCS)\n * @param compareKeyArrayDiff The compare-key array diff function\n * @param allObjectsHaveCompareKey The function to check if all objects in an array have the compare key\n * @returns [DiffResult[], DiffResult[]]\n */\nexport function diffObjectWithArraySupport(\n  leftObj: any,\n  rightObj: any,\n  level: number,\n  options: DifferOptions,\n  fallbackArrayDiff: (\n    arrLeft: any[],\n    arrRight: any[],\n    keyLeft: string,\n    keyRight: string,\n    level: number,\n    options: DifferOptions,\n    linesLeft?: DiffResult[],\n    linesRight?: DiffResult[],\n  ) => [DiffResult[], DiffResult[]],\n  compareKeyArrayDiff: (\n    arrLeft: any[],\n    arrRight: any[],\n    keyLeft: string,\n    keyRight: string,\n    level: number,\n    options: DifferOptions,\n    linesLeft?: DiffResult[],\n    linesRight?: DiffResult[],\n  ) => [DiffResult[], DiffResult[]],\n  allObjectsHaveCompareKey: (arr: any[], compareKey: string) => boolean\n): [DiffResult[], DiffResult[]] {\n  let linesLeft: DiffResult[] = [];\n  let linesRight: DiffResult[] = [];\n  const keys = Array.from(new Set([\n    ...Object.keys(leftObj || {}),\n    ...Object.keys(rightObj || {}),\n  ]));\n  for (const key of keys) {\n    const lVal = leftObj ? leftObj[key] : undefined;\n    const rVal = rightObj ? rightObj[key] : undefined;\n    if (Array.isArray(lVal) && Array.isArray(rVal) && options.compareKey) {\n      if (\n        allObjectsHaveCompareKey(lVal, options.compareKey) &&\n        allObjectsHaveCompareKey(rVal, options.compareKey)\n      ) {\n        // Use compare-key diff for this property\n        const [arrL, arrR] = compareKeyArrayDiff(lVal, rVal, '', '', level + 2, options, [], []);\n        linesLeft = concat(linesLeft, arrL);\n        linesRight = concat(linesRight, arrR);\n        continue;\n      }\n    }\n    if (Array.isArray(lVal) && Array.isArray(rVal)) {\n      // Fallback to normal diff for arrays\n      const [arrL, arrR] = fallbackArrayDiff(lVal, rVal, '', '', level + 2, options, [], []);\n      linesLeft = concat(linesLeft, arrL);\n      linesRight = concat(linesRight, arrR);\n    } else if (Array.isArray(lVal) || Array.isArray(rVal)) {\n      // If only one side is array, treat as modification\n      prettyAppendLines(\n        linesLeft,\n        linesRight,\n        key,\n        key,\n        lVal,\n        rVal,\n        level + 2,\n        options,\n      );\n    } else {\n      // Use diffObject for non-array values\n      const [leftLines, rightLines] = diffObject(\n        { [key]: lVal },\n        { [key]: rVal },\n        level + 2,\n        options,\n        fallbackArrayDiff\n      );\n      linesLeft = concat(linesLeft, leftLines);\n      linesRight = concat(linesRight, rightLines);\n    }\n  }\n  return [linesLeft, linesRight];\n} "
  },
  {
    "path": "src/utils/diff-object.ts",
    "content": "import type { DifferOptions, DiffResult, ArrayDiffFunc } from '../differ';\nimport cmp from './cmp';\nimport concat from './concat';\nimport getType from './get-type';\nimport prettyAppendLines from './pretty-append-lines';\nimport sortKeys from './sort-keys';\nimport stringify from './stringify';\n\nconst diffObject = (\n  lhs: Record<string, any>,\n  rhs: Record<string, any>,\n  level = 1,\n  options: DifferOptions,\n  arrayDiffFunc: ArrayDiffFunc,\n): [DiffResult[], DiffResult[]] => {\n  if (level > (options.maxDepth || Infinity)) {\n    return [\n      [{ level, type: 'equal', text: '...' }],\n      [{ level, type: 'equal', text: '...' }],\n    ];\n  }\n\n  let linesLeft: DiffResult[] = [];\n  let linesRight: DiffResult[] = [];\n\n  if (lhs === null && rhs === null || lhs === undefined && rhs === undefined) {\n    return [linesLeft, linesRight];\n  } else if (lhs === null || lhs === undefined) {\n    const addedLines = stringify(rhs, undefined, 1, undefined, options.undefinedBehavior).split('\\n');\n    for (let i = 0; i < addedLines.length; i++) {\n      linesLeft.push({ level, type: 'equal', text: '' });\n      linesRight.push({\n        level: level + (addedLines[i].match(/^\\s+/)?.[0]?.length || 0),\n        type: 'add',\n        text: addedLines[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n      });\n    }\n    return [linesLeft, linesRight];\n  } else if (rhs === null || rhs === undefined) {\n    const addedLines = stringify(lhs, undefined, 1, undefined, options.undefinedBehavior).split('\\n');\n    for (let i = 0; i < addedLines.length; i++) {\n      linesLeft.push({\n        level: level + (addedLines[i].match(/^\\s+/)?.[0]?.length || 0),\n        type: 'remove',\n        text: addedLines[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n      });\n      linesRight.push({ level, type: 'equal', text: '' });\n    }\n    return [linesLeft, linesRight];\n  }\n\n  const keysLeft = Object.keys(lhs);\n  const keysRight = Object.keys(rhs);\n  const keyOrdersMap = new Map<string, number>();\n  if (!options.preserveKeyOrder) {\n    sortKeys(keysLeft, options);\n    sortKeys(keysRight, options);\n  } else if (options.preserveKeyOrder === 'before') {\n    for (let i = 0; i < keysLeft.length; i++) {\n      keyOrdersMap.set(keysLeft[i], i);\n    }\n    for (let i = 0; i < keysRight.length; i++) {\n      if (!keyOrdersMap.has(keysRight[i])) {\n        keyOrdersMap.set(keysRight[i], keysLeft.length + i);\n      }\n    }\n    keysRight.sort((a, b) => keyOrdersMap.get(a)! - keyOrdersMap.get(b)!);\n  } else if (options.preserveKeyOrder === 'after') {\n    for (let i = 0; i < keysRight.length; i++) {\n      keyOrdersMap.set(keysRight[i], i);\n    }\n    for (let i = 0; i < keysLeft.length; i++) {\n      if (!keyOrdersMap.has(keysLeft[i])) {\n        keyOrdersMap.set(keysLeft[i], keysRight.length + i);\n      }\n    }\n    keysLeft.sort((a, b) => keyOrdersMap.get(a)! - keyOrdersMap.get(b)!);\n  }\n\n  const keysCmpOptions = {\n    ignoreCase: options.ignoreCaseForKey,\n    keyOrdersMap,\n  };\n  while (keysLeft.length || keysRight.length) {\n    const keyLeft = keysLeft[0];\n    const keyRight = keysRight[0];\n    const keyCmpResult = cmp(keyLeft, keyRight, keysCmpOptions);\n\n    if (keyCmpResult === 0) {\n      if (getType(lhs[keyLeft]) !== getType(rhs[keyRight])) {\n        prettyAppendLines(\n          linesLeft,\n          linesRight,\n          keyLeft,\n          keyRight,\n          lhs[keyLeft],\n          rhs[keyRight],\n          level,\n          options,\n        );\n      } else if (Array.isArray(lhs[keyLeft])) {\n        const arrLeft = [...lhs[keyLeft]];\n        const arrRight = [...rhs[keyRight]];\n        const [resLeft, resRight] = arrayDiffFunc(arrLeft, arrRight, keyLeft, keyRight, level, options, [], []);\n        linesLeft = concat(linesLeft, resLeft);\n        linesRight = concat(linesRight, resRight);\n      } else if (lhs[keyLeft] === null) {\n        linesLeft.push({ level, type: 'equal', text: `\"${keyLeft}\": null` });\n        linesRight.push({ level, type: 'equal', text: `\"${keyRight}\": null` });\n      } else if (typeof lhs[keyLeft] === 'object') {\n        const result = diffObject(\n          lhs[keyLeft],\n          rhs[keyRight],\n          level + 1,\n          options,\n          arrayDiffFunc,\n        );\n        linesLeft.push({ level, type: 'equal', text: `\"${keyLeft}\": {` });\n        linesLeft = concat(linesLeft, result[0]);\n        linesLeft.push({ level, type: 'equal', text: '}' });\n        linesRight.push({ level, type: 'equal', text: `\"${keyRight}\": {` });\n        linesRight = concat(linesRight, result[1]);\n        linesRight.push({ level, type: 'equal', text: '}' });\n      } else {\n        prettyAppendLines(\n          linesLeft,\n          linesRight,\n          keyLeft,\n          keyRight,\n          lhs[keyLeft],\n          rhs[keyRight],\n          level,\n          options,\n        );\n      }\n    } else if (keysLeft.length && keysRight.length) {\n      if (keyCmpResult < 0) {\n        const addedLines = stringify(lhs[keyLeft], undefined, 1, undefined, options.undefinedBehavior).split('\\n');\n        for (let i = 0; i < addedLines.length; i++) {\n          const text = addedLines[i].replace(/^\\s+/, '').replace(/,$/g, '');\n          linesLeft.push({\n            level: level + (addedLines[i].match(/^\\s+/)?.[0]?.length || 0),\n            type: 'remove',\n            text: i ? text : `\"${keyLeft}\": ${text}`,\n          });\n          linesRight.push({ level, type: 'equal', text: '' });\n        }\n      } else {\n        const addedLines = stringify(rhs[keyRight], undefined, 1, undefined, options.undefinedBehavior).split('\\n');\n        for (let i = 0; i < addedLines.length; i++) {\n          const text = addedLines[i].replace(/^\\s+/, '').replace(/,$/g, '');\n          linesLeft.push({ level, type: 'equal', text: '' });\n          linesRight.push({\n            level: level + (addedLines[i].match(/^\\s+/)?.[0]?.length || 0),\n            type: 'add',\n            text: i ? text : `\"${keyRight}\": ${text}`,\n          });\n        }\n      }\n    } else if (keysLeft.length) {\n      const addedLines = stringify(lhs[keyLeft], undefined, 1, undefined, options.undefinedBehavior).split('\\n');\n      for (let i = 0; i < addedLines.length; i++) {\n        const text = addedLines[i].replace(/^\\s+/, '').replace(/,$/g, '');\n        linesLeft.push({\n          level: level + (addedLines[i].match(/^\\s+/)?.[0]?.length || 0),\n          type: 'remove',\n          text: i ? text : `\"${keyLeft}\": ${text}`,\n        });\n        linesRight.push({ level, type: 'equal', text: '' });\n      }\n    } else if (keysRight.length) {\n      const addedLines = stringify(rhs[keyRight], undefined, 1, undefined, options.undefinedBehavior).split('\\n');\n      for (let i = 0; i < addedLines.length; i++) {\n        const text = addedLines[i].replace(/^\\s+/, '').replace(/,$/g, '');\n        linesLeft.push({ level, type: 'equal', text: '' });\n        linesRight.push({\n          level: level + (addedLines[i].match(/^\\s+/)?.[0]?.length || 0),\n          type: 'add',\n          text: i ? text : `\"${keyRight}\": ${text}`,\n        });\n      }\n    }\n\n    if (keyLeft === undefined) {\n      keysRight.shift();\n    } else if (keyRight === undefined) {\n      keysLeft.shift();\n    } else if (keyCmpResult === 0) {\n      keysLeft.shift();\n      keysRight.shift();\n    } else if (keyCmpResult < 0) {\n      keysLeft.shift();\n    } else {\n      keysRight.shift();\n    }\n  }\n\n  if (linesLeft.length !== linesRight.length) {\n    throw new Error('Diff error: length not match for left & right, please report a bug with your data.');\n  }\n\n  return [linesLeft, linesRight];\n};\n\nexport default diffObject;\n"
  },
  {
    "path": "src/utils/find-visible-lines.ts",
    "content": "import type { SegmentItem, HiddenUnchangedLinesInfo } from './get-segments';\nimport { getSegmentHeight, isExpandLine } from './segment-util';\n\nconst findVisibleLines = (\n  segments: Array<SegmentItem | HiddenUnchangedLinesInfo>,\n  accTop: number[],\n  viewportTop: number,\n  viewportBottom: number,\n  itemHeight: number,\n  expandLineHeight: number,\n) => {\n  if (!accTop.length) {\n    return [0, 0, 0, 0];\n  }\n  let startSegment = 0;\n  let endSegment = 0;\n  let startLine = 0;\n  let endLine = 0;\n  let l = 0;\n  let r = segments.length - 1;\n  // start segment\n  while (1) {\n    const m = Math.floor((l + r) / 2);\n    const top = accTop[m];\n    const bottom = top + getSegmentHeight(segments[m], itemHeight, expandLineHeight);\n    if (bottom <= viewportTop) {\n      l = m + 1;\n    } else {\n      r = m;\n    }\n    if (l === r) {\n      startSegment = l;\n      break;\n    }\n  }\n  // start line\n  const startSegmentItem = segments[startSegment];\n  if (isExpandLine(startSegmentItem)) {\n    startLine = startSegmentItem.start;\n  } else {\n    startLine = startSegmentItem.start + Math.floor((viewportTop - accTop[startSegment]) / itemHeight);\n  }\n  // end segment\n  l = 0;\n  r = segments.length - 1;\n  while (1) {\n    const m = Math.floor((l + r + 1) / 2);\n    const top = accTop[m];\n    if (top >= viewportBottom) {\n      r = m - 1;\n    } else {\n      l = m;\n    }\n    if (l === r) {\n      endSegment = l;\n      break;\n    }\n  }\n  // end line\n  const endSegmentItem = segments[endSegment];\n  if (isExpandLine(endSegmentItem)) {\n    endLine = endSegmentItem.end;\n  } else {\n    endLine = endSegmentItem.start + Math.ceil((viewportBottom - accTop[endSegment]) / itemHeight);\n  }\n  return [\n    startSegment,\n    startLine,\n    endSegment,\n    endLine,\n  ];\n};\n\nexport default findVisibleLines;\n"
  },
  {
    "path": "src/utils/format-value.spec.ts",
    "content": "import formatValue from './format-value';\n\ndescribe('Utility function: formatValue', () => {\n  it('should work for primitives ', () => {\n    expect(formatValue('a')).toBe('\"a\"');\n    expect(formatValue(1)).toBe('1');\n    expect(formatValue(true)).toBe('true');\n  });\n\n  it('should handle invalid values correctly', () => {\n    expect(formatValue(null)).toBe('null');\n    expect(formatValue(NaN)).toBe('NaN');\n    expect(formatValue(Infinity)).toBe('Infinity');\n    expect(formatValue(-Infinity)).toBe('-Infinity');\n    expect(formatValue(undefined)).toBe('undefined');\n    expect(formatValue(Symbol.iterator)).toBe('Symbol(Symbol.iterator)');\n  });\n\n  it('should work for array & object', () => {\n    expect(formatValue([1, 2, '3'])).toBe('[1,2,\"3\"]');\n    expect(formatValue({ a: 1, b: ['2', true] })).toBe('{\"a\":1,\"b\":[\"2\",true]}');\n  });\n\n  it('should work for array & object with pretty', () => {\n    expect(formatValue([1, 2, '3'], undefined, true)).toBe('[\\n 1,\\n 2,\\n \"3\"\\n]');\n    expect(formatValue({ a: 1, b: ['2', true] }, undefined, true)).toBe('{\\n \"a\": 1,\\n \"b\": [\\n  \"2\",\\n  true\\n ]\\n}');\n  });\n\n  it('should escape the characters when necessary for better display', () => {\n    expect(formatValue('first line\\n\\tsecond line')).toBe('\"first line\\\\n\\\\tsecond line\"');\n    expect(formatValue('\"')).toBe('\"\\\\\"\"');\n  });\n});\n"
  },
  {
    "path": "src/utils/format-value.ts",
    "content": "import { UndefinedBehavior } from '../differ';\nimport stringify from './stringify';\n\nconst formatValue = (\n  value: any,\n  depth = Infinity,\n  pretty = false,\n  undefinedBehavior = UndefinedBehavior.stringify,\n) => {\n  if (value === null) {\n    return 'null';\n  }\n  if (Array.isArray(value) || typeof value === 'object') {\n    return stringify(value, undefined, pretty ? 1 : undefined, depth, undefinedBehavior);\n  }\n  return stringify(value, undefined, undefined, undefined, undefinedBehavior);\n};\n\nexport default formatValue;\n"
  },
  {
    "path": "src/utils/get-inline-diff.ts",
    "content": "import { lcs as myersDiff } from 'fast-myers-diff';\n\nexport interface InlineDiffOptions {\n  mode?: 'char' | 'word';\n  wordSeparator?: string;\n}\n\nexport interface InlineDiffResult {\n  type?: 'add' | 'remove';\n  start: number;\n  end: number;\n}\n\nconst getOriginalIndices = (arr: string[], separatorLength: number) => {\n  const result: number[] = [];\n  let index = 0;\n  for (const item of arr) {\n    result.push(index);\n    index += item.length + separatorLength;\n  }\n  result.push(index - separatorLength);\n  return result;\n};\n\nconst filterEmptyParts = (arr: InlineDiffResult[]) => {\n  return arr.filter(item => item.end > item.start);\n};\n\nconst getInlineDiff = (l: string, r: string, options: InlineDiffOptions): [\n  InlineDiffResult[],\n  InlineDiffResult[]\n] => {\n  let resultL: InlineDiffResult[] = [];\n  let resultR: InlineDiffResult[] = [];\n  let lastL = 0;\n  let lastR = 0;\n\n  if (options.mode === 'word') {\n    const wordSeparator = options.wordSeparator || ' ';\n    const lArr = l.split(wordSeparator);\n    const rArr = r.split(wordSeparator);\n\n    /**\n     * The iter array contains the information about replacement, which is an array of\n     * tuple `[startL, startR, length]`.\n     *\n     * e.g. `[1, 2, 3]` means replace `lArr[1...1+3]` to `rArr[2...2+3]` (include the end).\n     */\n    const iter = [...myersDiff(lArr, rArr)];\n\n    const separatorLength = wordSeparator.length;\n    const indicesL = getOriginalIndices(lArr, separatorLength);\n    const indicesR = getOriginalIndices(rArr, separatorLength);\n\n    for (const [sl, sr, length] of iter) {\n      if (sl > lastL) {\n        resultL.push({ type: 'remove', start: indicesL[lastL], end: indicesL[sl] });\n      }\n      if (sr > lastR) {\n        resultR.push({ type: 'add', start: indicesR[lastR], end: indicesR[sr] });\n      }\n      lastL = sl + length;\n      lastR = sr + length;\n      resultL.push({ start: indicesL[sl], end: indicesL[lastL] });\n      resultR.push({ start: indicesR[sr], end: indicesR[lastR] });\n    }\n    if (l.length > lastL) {\n      resultL.push({ type: 'remove', start: indicesL[lastL], end: l.length });\n    }\n    if (r.length > lastR) {\n      resultR.push({ type: 'add', start: indicesR[lastR], end: r.length });\n    }\n    resultL = filterEmptyParts(resultL);\n    resultR = filterEmptyParts(resultR);\n    return [resultL, resultR];\n  }\n\n  const iter = myersDiff(l, r);\n  for (const [sl, sr, length] of iter) {\n    if (sl > lastL) {\n      resultL.push({ type: 'remove', start: lastL, end: sl });\n    }\n    if (sr > lastR) {\n      resultR.push({ type: 'add', start: lastR, end: sr });\n    }\n    lastL = sl + length;\n    lastR = sr + length;\n    resultL.push({ start: sl, end: lastL });\n    resultR.push({ start: sr, end: lastR });\n  }\n  if (l.length > lastL) {\n    resultL.push({ type: 'remove', start: lastL, end: l.length });\n  }\n  if (r.length > lastR) {\n    resultR.push({ type: 'add', start: lastR, end: r.length });\n  }\n  resultL = filterEmptyParts(resultL);\n  resultR = filterEmptyParts(resultR);\n  return [resultL, resultR];\n};\n\nexport default getInlineDiff;\n"
  },
  {
    "path": "src/utils/get-inline-syntax-highlight.ts",
    "content": "export interface InlineHighlightResult {\n  start: number;\n  end: number;\n  token: 'plain' | 'number' | 'boolean' | 'null' | 'key' | 'punctuation' | 'string' | 'invalid';\n}\n\nconst syntaxHighlightLine = (enabled: boolean, text: string, offset: number): InlineHighlightResult[] => {\n  if (!enabled) {\n    return [{ token: 'plain', start: offset, end: text.length + offset }];\n  }\n  if (\n    text === 'undefined' ||\n    text === 'Infinity' ||\n    text === '-Infinity' ||\n    text === 'NaN' ||\n    /^\\d+n$/i.test(text) ||\n    text.startsWith('Symbol(') ||\n    text.startsWith('function') ||\n    text.startsWith('(')\n  ) {\n    return [{ token: 'invalid', start: offset, end: text.length + offset }];\n  }\n  if (!Number.isNaN(Number(text))) {\n    return [{ token: 'number', start: offset, end: text.length + offset }];\n  }\n  if (text === 'true' || text === 'false') {\n    return [{ token: 'boolean', start: offset, end: text.length + offset }];\n  }\n  if (text === 'null') {\n    return [{ token: 'null', start: offset, end: text.length + offset }];\n  }\n  if (text.startsWith('\"')) {\n    if (text.endsWith(': [') || text.endsWith(': {')) {\n      return [\n        { token: 'key', start: offset, end: text.length - 3 + offset },\n        { token: 'punctuation', start: text.length - 3, end: text.length - 2 + offset },\n        { token: 'plain', start: text.length - 2, end: text.length - 1 + offset },\n        { token: 'punctuation', start: text.length - 1, end: text.length + offset },\n      ];\n    }\n    let pairedQuoteIndex = 1;\n    while (pairedQuoteIndex < text.length) {\n      if (text[pairedQuoteIndex] === '\"') break;\n      if (text[pairedQuoteIndex] === '\\\\') ++pairedQuoteIndex;\n      ++pairedQuoteIndex;\n    }\n    if (pairedQuoteIndex === text.length - 1) {\n      return [{ token: 'string', start: offset, end: text.length + offset }];\n    }\n    return [\n      { token: 'key', start: offset, end: pairedQuoteIndex + 1 + offset },\n      { token: 'punctuation', start: pairedQuoteIndex + 1, end: pairedQuoteIndex + 2 + offset },\n      { token: 'plain', start: pairedQuoteIndex + 2, end: pairedQuoteIndex + 3 + offset },\n      ...syntaxHighlightLine(enabled, text.substring(pairedQuoteIndex + 3), offset + pairedQuoteIndex + 3),\n    ];\n  }\n  if (text === '{' || text === '}' || text === '[' || text === ']') {\n    return [{ token: 'punctuation', start: offset, end: text.length + offset }];\n  }\n  // should this be expected?\n  return [{ token: 'plain', start: offset, end: text.length + offset }];\n};\n\nexport default syntaxHighlightLine;\n"
  },
  {
    "path": "src/utils/get-segments.ts",
    "content": "import type { DiffResult } from '../differ';\nimport type { HideUnchangedLinesOptions } from '../viewer';\n\nconst defaultOptions = {\n  threshold: 8,\n  margin: 3,\n};\n\nexport interface SegmentItem {\n  start: number;\n  end: number;\n  isEqual: boolean;\n}\n\nexport interface HiddenUnchangedLinesInfo extends SegmentItem {\n  hasLinesBefore: boolean;\n  hasLinesAfter: boolean;\n}\n\nconst getSegments = (l: DiffResult[], r: DiffResult[], options: HideUnchangedLinesOptions, jsonsAreEqual: boolean) => {\n  if (!options || jsonsAreEqual) {\n    return [{ start: 0, end: l.length, isEqual: false }];\n  }\n\n  const segments: SegmentItem[] = [];\n  for (let i = 0; i < l.length; i++) {\n    if (l[i].type === 'equal' && r[i].type === 'equal') {\n      if (segments.length && segments[segments.length - 1].isEqual) {\n        segments[segments.length - 1].end++;\n      } else {\n        segments.push({ start: i, end: i + 1, isEqual: true });\n      }\n    } else {\n      if (segments.length && !segments[segments.length - 1].isEqual) {\n        segments[segments.length - 1].end++;\n      } else {\n        segments.push({ start: i, end: i + 1, isEqual: false });\n      }\n    }\n  }\n\n  const _options = options === true ? defaultOptions : { ...defaultOptions, ...options };\n  const { threshold, margin } = _options;\n  if (threshold < margin * 2 + 1) {\n    // eslint-disable-next-line no-console, max-len\n    console.warn(`Threshold (${threshold}) is no more than 2 margins + 1 \"expand\" line (${margin} * 2 + 1), it's not necessary to hide unchanged areas which have less than ${margin * 2 + 1} lines.`);\n  }\n\n  const result: Array<SegmentItem | HiddenUnchangedLinesInfo> = [];\n  for (let i = 0; i < segments.length; i++) {\n    const segment = segments[i];\n    if (\n      !segment.isEqual ||\n      segment.end - segment.start < threshold ||\n      segment.end - segment.start <= margin * 2 + 1\n    ) {\n      result.push(segment);\n      continue;\n    }\n    if (!i) {\n      result.push({ hasLinesBefore: true, hasLinesAfter: false, start: 0, end: segment.end - margin, isEqual: true });\n      result.push({ start: segment.end - margin, end: segment.end, isEqual: true });\n    } else if (i === segments.length - 1) {\n      result.push({ start: segment.start, end: segment.start + margin, isEqual: true });\n      result.push({\n        hasLinesBefore: false,\n        hasLinesAfter: true,\n        start: segment.start + margin,\n        end: l.length,\n        isEqual: true,\n      });\n    } else {\n      result.push({ start: segment.start, end: segment.start + margin, isEqual: true });\n      result.push({\n        hasLinesBefore: true,\n        hasLinesAfter: true,\n        start: segment.start + margin,\n        end: segment.end - margin,\n        isEqual: true,\n      });\n      result.push({ start: segment.end - margin, end: segment.end, isEqual: true });\n    }\n  }\n\n  return result;\n};\n\nexport default getSegments;\n"
  },
  {
    "path": "src/utils/get-type.spec.ts",
    "content": "import getType from './get-type';\n\ndescribe('Utility function: getType', () => {\n  it('should work for primitives ', () => {\n    expect(getType('a')).toBe('string');\n    expect(getType(1)).toBe('number');\n    expect(getType(true)).toBe('boolean');\n  });\n\n  it('should return \"null\" for null', () => {\n    expect(getType(null)).toBe('null');\n  });\n\n  it('should work for array & object ', () => {\n    expect(getType([1, 2, '3'])).toBe('array');\n    expect(getType({ a: 1 })).toBe('object');\n  });\n});\n"
  },
  {
    "path": "src/utils/get-type.ts",
    "content": "const getType = (value: any) => {\n  if (Array.isArray(value)) {\n    return 'array';\n  }\n  if (value === null) {\n    return 'null';\n  }\n  return typeof value;\n};\n\nexport default getType;\n"
  },
  {
    "path": "src/utils/is-equal.ts",
    "content": "import isEqualWith from 'lodash/isEqualWith';\nimport type { DifferOptions } from '../differ';\n\nconst isEqual = (a: any, b: any, options: DifferOptions) => {\n  if (options.ignoreCase) {\n    return typeof a === 'string' && typeof b === 'string' && a.toLowerCase() === b.toLowerCase();\n  }\n  if (typeof a === 'symbol' && typeof b === 'symbol') {\n    return a.toString() === b.toString();\n  }\n  if (options.recursiveEqual) {\n    return isEqualWith(a, b, (a, b) => (\n      options.ignoreCase\n        ? typeof a === 'string' && typeof b === 'string'\n          ? a.toLowerCase() === b.toLowerCase()\n          : undefined\n        : undefined\n    ));\n  }\n  return a === b;\n};\n\nexport default isEqual;\n"
  },
  {
    "path": "src/utils/pretty-append-lines.ts",
    "content": "import type { DifferOptions, DiffResult } from '../differ';\n\nimport cmp from './cmp';\nimport formatValue from './format-value';\n\nconst prettyAppendLines = (\n  linesLeft: DiffResult[],\n  linesRight: DiffResult[],\n  keyLeft: string,\n  keyRight: string,\n  valueLeft: any,\n  valueRight: any,\n  level: number,\n  options: DifferOptions,\n) => {\n  const valueCmpOptions = { ignoreCase: options.ignoreCase };\n  const _resultLeft = formatValue(valueLeft, options.maxDepth, true, options.undefinedBehavior).split('\\n');\n  const _resultRight = formatValue(valueRight, options.maxDepth, true, options.undefinedBehavior).split('\\n');\n  if (cmp(valueLeft, valueRight, valueCmpOptions) !== 0) {\n    if (options.showModifications) {\n      const maxLines = Math.max(_resultLeft.length, _resultRight.length);\n      for (let i = _resultLeft.length; i < maxLines; i++) {\n        _resultLeft.push('');\n      }\n      for (let i = _resultRight.length; i < maxLines; i++) {\n        _resultRight.push('');\n      }\n      linesLeft.push({\n        level,\n        type: 'modify',\n        text: keyLeft ? `\"${keyLeft}\": ${_resultLeft[0]}` : _resultLeft[0],\n      });\n      for (let i = 1; i < _resultLeft.length; i++) {\n        linesLeft.push({\n          level: level + (_resultLeft[i].match(/^\\s+/)?.[0]?.length || 0),\n          type: 'modify',\n          text: _resultLeft[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n        });\n      }\n      for (let i = _resultLeft.length; i < maxLines; i++) {\n        linesLeft.push({ level, type: 'equal', text: '' });\n      }\n      linesRight.push({\n        level,\n        type: 'modify',\n        text: keyRight ? `\"${keyRight}\": ${_resultRight[0]}` : _resultRight[0],\n      });\n      for (let i = 1; i < _resultRight.length; i++) {\n        linesRight.push({\n          level: level + (_resultRight[i].match(/^\\s+/)?.[0]?.length || 0),\n          type: 'modify',\n          text: _resultRight[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n        });\n      }\n      for (let i = _resultRight.length; i < maxLines; i++) {\n        linesRight.push({ level, type: 'equal', text: '' });\n      }\n    } else {\n      linesLeft.push({\n        level,\n        type: 'remove',\n        text: keyLeft ? `\"${keyLeft}\": ${_resultLeft[0]}` : _resultLeft[0],\n      });\n      for (let i = 1; i < _resultLeft.length; i++) {\n        linesLeft.push({\n          level: level + (_resultLeft[i].match(/^\\s+/)?.[0]?.length || 0),\n          type: 'remove',\n          text: _resultLeft[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n        });\n      }\n      for (let i = 0; i < _resultRight.length; i++) {\n        linesLeft.push({ level, type: 'equal', text: '' });\n      }\n      for (let i = 0; i < _resultLeft.length; i++) {\n        linesRight.push({ level, type: 'equal', text: '' });\n      }\n      linesRight.push({\n        level,\n        type: 'add',\n        text: keyRight ? `\"${keyRight}\": ${_resultRight[0]}` : _resultRight[0],\n      });\n      for (let i = 1; i < _resultRight.length; i++) {\n        linesRight.push({\n          level: level + (_resultRight[i].match(/^\\s+/)?.[0]?.length || 0),\n          type: 'add',\n          text: _resultRight[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n        });\n      }\n    }\n  } else {\n    const maxLines = Math.max(_resultLeft.length, _resultRight.length);\n    for (let i = _resultLeft.length; i < maxLines; i++) {\n      _resultLeft.push('');\n    }\n    for (let i = _resultRight.length; i < maxLines; i++) {\n      _resultRight.push('');\n    }\n    linesLeft.push({\n      level,\n      type: 'equal',\n      text: keyLeft ? `\"${keyLeft}\": ${_resultLeft[0]}` : _resultLeft[0],\n    });\n    for (let i = 1; i < _resultLeft.length; i++) {\n      linesLeft.push({\n        level: level + (_resultLeft[i].match(/^\\s+/)?.[0]?.length || 0),\n        type: 'equal',\n        text: _resultLeft[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n      });\n    }\n    linesRight.push({\n      level,\n      type: 'equal',\n      text: keyRight ? `\"${keyRight}\": ${_resultRight[0]}` : _resultRight[0],\n    });\n    for (let i = 1; i < _resultRight.length; i++) {\n      linesRight.push({\n        level: level + (_resultRight[i].match(/^\\s+/)?.[0]?.length || 0),\n        type: 'equal',\n        text: _resultRight[i].replace(/^\\s+/, '').replace(/,$/g, ''),\n      });\n    }\n  }\n};\n\nexport default prettyAppendLines;\n"
  },
  {
    "path": "src/utils/segment-util.ts",
    "content": "import type { InlineDiffResult } from './get-inline-diff';\nimport type { InlineHighlightResult } from './get-inline-syntax-highlight';\nimport type { SegmentItem, HiddenUnchangedLinesInfo } from './get-segments';\n\nexport const isExpandLine = (\n  segment: SegmentItem | HiddenUnchangedLinesInfo,\n): segment is HiddenUnchangedLinesInfo => {\n  return 'hasLinesBefore' in segment || 'hasLinesAfter' in segment;\n};\n\nexport const getSegmentHeight = (\n  segment: SegmentItem | HiddenUnchangedLinesInfo,\n  itemHeight: number,\n  expandLineHeight: number,\n) => {\n  return isExpandLine(segment)\n    ? expandLineHeight\n    : itemHeight * (segment.end - segment.start + 1);\n};\n\nexport type InlineRenderInfo = InlineDiffResult & InlineHighlightResult;\n\n/**\n * Merge two segments array into one, divide the segment if necessary.\n */\nexport const mergeSegments = (tokens: InlineHighlightResult[], diffs: InlineDiffResult[]): InlineRenderInfo[] => {\n  const result: InlineRenderInfo[] = [];\n  let token: InlineHighlightResult;\n  let diff: InlineDiffResult;\n\n  if (tokens.length && diffs.length) {\n    tokens = [...tokens];\n    diffs = [...diffs];\n    token = { ...tokens.shift()! };\n    diff = { ...diffs.shift()! };\n\n    while (1) {\n      if (token.start === diff.start) {\n        const end = Math.min(token.end, diff.end);\n        result.push({ ...token, ...diff, end });\n        token.start = diff.start = end;\n      } else if (token.start < diff.start) {\n        const end = Math.min(token.end, diff.start);\n        result.push({ ...diff, ...token, end });\n        token.start = end;\n      } else {\n        const end = Math.min(token.start, diff.end);\n        result.push({ ...token, ...diff, end });\n        diff.start = end;\n      }\n      if (!tokens.length || !diffs.length) break;\n      if (token.start === token.end) token = { ...tokens.shift()! };\n      if (diff.start === diff.end) diff = { ...diffs.shift()! };\n    }\n  }\n\n  if (!tokens.length) result.push(...diffs.map(d => ({ ...d, token: token.token || 'plain' } as InlineRenderInfo)));\n  if (!diffs.length) result.push(...tokens);\n\n  return result;\n};\n"
  },
  {
    "path": "src/utils/shallow-similarity.spec.ts",
    "content": "import shallowSimilarity from './shallow-similarity';\n\ndescribe('Utility function: shallowSimilarity', () => {\n  it('should return 1 if both values are the same', () => {\n    expect(shallowSimilarity('2', '2')).toBe(1);\n  });\n\n  it('should return 0 if either value is null', () => {\n    expect(shallowSimilarity(null, '2')).toBe(0);\n    expect(shallowSimilarity('2', null)).toBe(0);\n  });\n\n  it('should return 0 if either value is not an object', () => {\n    expect(shallowSimilarity('2', 2)).toBe(0);\n  });\n\n  it('should return 0 if both values are objects but have no common keys', () => {\n    expect(shallowSimilarity({ a: 1 }, { b: 2 })).toBe(0);\n  });\n\n  it('should return the correct value if both values are objects and have common keys', () => {\n    expect(shallowSimilarity({ a: 1 }, { a: 1 })).toBe(1);\n    expect(shallowSimilarity({ a: 1, b: 2 }, { a: 1, c: 3 })).toBe(0.5);\n    expect(shallowSimilarity({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(1);\n  });\n});\n"
  },
  {
    "path": "src/utils/shallow-similarity.ts",
    "content": "const shallowSimilarity = (left: any, right: any): number => {\n  if (left === right) {\n    return 1;\n  }\n  if (left === null || right === null) {\n    return 0;\n  }\n  if (typeof left !== 'object' || typeof right !== 'object') {\n    return 0;\n  }\n  let intersection = 0;\n  for (const key in left) {\n    if (\n      Object.prototype.hasOwnProperty.call(left, key) &&\n      Object.prototype.hasOwnProperty.call(right, key) &&\n      left[key] === right[key]\n    ) {\n      intersection++;\n    }\n  }\n  return Math.max(\n    intersection / Object.keys(left).length,\n    intersection / Object.keys(right).length,\n  );\n};\n\nexport default shallowSimilarity;\n"
  },
  {
    "path": "src/utils/sort-inner-arrays.spec.ts",
    "content": "import sortInnerArrays from './sort-inner-arrays';\n\ndescribe('Utility function: sortInnerArrays', () => {\n  it('should return the original value if value is not array or object', () => {\n    expect(sortInnerArrays(1)).toBe(1);\n    expect(sortInnerArrays('2')).toBe('2');\n    expect(sortInnerArrays(true)).toBe(true);\n  });\n\n  it('should work like `Array.prototype.sort` for one-level typed arrays', () => {\n    const numArray = Array(100).fill(0).map(() => Math.random() * 1e6 | 0);\n    const sortedNumArray = [...numArray].sort((a, b) => a - b);\n    expect(sortInnerArrays(numArray)).toStrictEqual(sortedNumArray);\n\n    const strArray = Array(100).fill(0).map(() => String(Math.random() * 1e6 | 0));\n    const sortedStrArray = [...strArray].sort((a, b) => a.localeCompare(b));\n    expect(sortInnerArrays(strArray)).toStrictEqual(sortedStrArray);\n  });\n\n  it('should sort all the levels for a nested array', () => {\n    // array as an array item\n    const arr1 = Array(100).fill(0).map(() => Math.random() * 1e6 | 0);\n    const sortedArr1 = [...arr1].sort((a, b) => a - b);\n    expect(sortInnerArrays(arr1)).toStrictEqual(sortedArr1);\n\n    // array as an object value\n    const arr2 = Array(100).fill(0).map(() => Math.random() * 1e6 | 0);\n    const sortedArr2 = [...arr2].sort((a, b) => a - b);\n    expect(sortInnerArrays(arr2)).toStrictEqual(sortedArr2);\n  });\n\n  it('should work properly for one-level mix-typed arrays', () => {\n    const arr = [1, '2', true, '3', null, [], -1, false, null, '0', {}];\n    const sortedArr = [false, true, -1, 1, '0', '2', '3', null, null, [], {}];\n    expect(sortInnerArrays(arr)).toStrictEqual(sortedArr);\n  });\n\n  it('should work properly for nested mix-typed arrays', () => {\n    const arr = [1, { t: 6 }, [{ a: 1, b: ['2', 7] }, 4], null, [], -1, null, '0', {}];\n    const sortedArr = [-1, 1, '0', null, null, [4, { a: 1, b: [7, '2'] }], [], { t: 6 }, {}];\n    expect(sortInnerArrays(arr)).toStrictEqual(sortedArr);\n  });\n});\n"
  },
  {
    "path": "src/utils/sort-inner-arrays.ts",
    "content": "import type { DifferOptions } from '../differ';\nimport cmp from './cmp';\n\nconst sortInnerArrays = (source: any, options?: DifferOptions) => {\n  if (!source || typeof source !== 'object') {\n    return source;\n  }\n\n  if (Array.isArray(source)) {\n    const result = [...source];\n    result.sort((a, b) => cmp(a, b, { ignoreCase: options?.ignoreCase }));\n    return result.map(item => sortInnerArrays(item, options));\n  }\n\n  const result = { ...source };\n  for (const key in result) {\n    result[key] = sortInnerArrays(result[key], options);\n  }\n  return result;\n};\n\nexport default sortInnerArrays;\n"
  },
  {
    "path": "src/utils/sort-keys.ts",
    "content": "import type { DifferOptions } from '../differ';\nimport cmp from './cmp';\n\nconst sortKeys = (arr: string[], options: DifferOptions) => {\n  return arr.sort((a, b) => cmp(a, b, { ignoreCase: options.ignoreCaseForKey }));\n};\n\nexport default sortKeys;\n"
  },
  {
    "path": "src/utils/stringify.spec.ts",
    "content": "import stringify from './stringify';\n\ndescribe('Utility function: stringify', () => {\n  it('should work like `JSON.stringify` if does not provide a `depth`', () => {\n    const testCases = [\n      1,\n      '2',\n      true,\n      [],\n      [1, '2', true],\n      {},\n      { a: 1, b: '2', c: [3, { d: 4 }] },\n    ];\n    for (const testCase of testCases) {\n      expect(stringify(testCase, undefined, 2)).toBe(JSON.stringify(testCase, undefined, 2));\n    }\n  });\n\n  it('should deal with the `depth` param correctly', () => {\n    // depth 0\n    const depth0 = (value: any) => stringify(value, undefined, 2, 0);\n    expect(depth0(1)).toBe('1');\n    expect(depth0([1, '2', true])).toBe('\"...\"');\n    expect(depth0({})).toBe('\"...\"');\n    expect(depth0({ a: 1, b: '2', c: [3, { d: 4 }] })).toBe('\"...\"');\n\n    const depth1 = (value: any) => stringify(value, undefined, 2, 1);\n    expect(depth1(1)).toBe('1');\n    expect(depth1([1, '2', true])).toBe('[\\n  1,\\n  \"2\",\\n  true\\n]');\n    expect(depth1({})).toBe('{}');\n    expect(depth1({ a: 1, b: '2', c: [3, { d: 4 }] })).toBe('{\\n  \"a\": 1,\\n  \"b\": \"2\",\\n  \"c\": \"...\"\\n}');\n\n    const depth2 = (value: any) => stringify(value, undefined, 2, 2);\n    expect(depth2({ a: 1, b: '2', c: [3, { d: 4 }] }))\n      .toBe('{\\n  \"a\": 1,\\n  \"b\": \"2\",\\n  \"c\": [\\n    3,\\n    \"...\"\\n  ]\\n}');\n\n    const depth3 = (value: any) => stringify(value, undefined, 2, 3);\n    expect(depth3({ a: 1, b: '2', c: [3, { d: 4 }] }))\n      .toBe('{\\n  \"a\": 1,\\n  \"b\": \"2\",\\n  \"c\": [\\n    3,\\n    {\\n      \"d\": 4\\n    }\\n  ]\\n}');\n  });\n});\n"
  },
  {
    "path": "src/utils/stringify.ts",
    "content": "import { UndefinedBehavior } from '../differ';\n\n// https://gist.github.com/RexSkz/c4f78a6e143e9008f9c717623b7a2bc1\nconst stringify = (\n  obj: any,\n  replacer?: (this: any, key: string, value: any) => any,\n  space?: string | number,\n  depth = Infinity,\n  undefinedBehavior?: UndefinedBehavior,\n): string => {\n  if (!obj || typeof obj !== 'object') {\n    let result: string | undefined = undefined;\n    if (!Number.isNaN(obj) && obj !== Infinity && obj !== -Infinity && typeof obj !== 'bigint') {\n      result = JSON.stringify(obj, replacer, space);\n    }\n    if (result === undefined) {\n      switch (undefinedBehavior) {\n        case UndefinedBehavior.throw:\n          throw new Error(`Value is not valid in JSON, got ${String(obj)}`);\n        case UndefinedBehavior.stringify:\n          return stringifyInvalidValue(obj);\n        default:\n          throw new Error(`Should not reach here, please report this bug.`);\n      }\n    }\n    return result;\n  }\n  const t = depth < 1\n    ? '\"...\"'\n    : Array.isArray(obj)\n      ? `[${obj.map(v => stringify(v, replacer, space, depth - 1, undefinedBehavior)).join(',')}]`\n      : `{${Object.keys(obj)\n        .map((k) => `\"${k}\": ${stringify(obj[k], replacer, space, depth - 1, undefinedBehavior)}`)\n        .join(', ')}}`;\n  return JSON.stringify(JSON.parse(t), replacer, space);\n};\n\nconst stringifyInvalidValue = (value: any) => {\n  if (value === undefined) {\n    return 'undefined';\n  }\n  if (value === Infinity) {\n    return 'Infinity';\n  }\n  if (value === -Infinity) {\n    return '-Infinity';\n  }\n  if (Number.isNaN(value)) {\n    return 'NaN';\n  }\n  if (typeof value === 'bigint') {\n    return `${value}n`;\n  }\n  return String(value);\n};\n\nexport default stringify;\n"
  },
  {
    "path": "src/viewer-monokai.less",
    "content": ".json-diff-viewer.json-diff-viewer-theme-monokai {\n  background: #272822;\n  color: #f8f8f2;\n\n  .line-number {\n    color: #999;\n  }\n\n  tr {\n    &:hover {\n      background: #3e3d32;\n    }\n\n    .line-add {\n      background: #040;\n    }\n\n    .line-remove {\n      background: #400;\n    }\n\n    .line-modify {\n      background: #440;\n    }\n\n    &.expand-line button {\n      color: #f8f8f2;\n    }\n  }\n\n  .string {\n    color: #e6db74;\n  }\n\n  .number,\n  .boolean,\n  .null {\n    color: #ae81ff;\n  }\n\n  .key {\n    color: #f92672;\n  }\n\n  .invalid {\n    background: #960050;\n    color: #fff;\n  }\n}\n"
  },
  {
    "path": "src/viewer.less",
    "content": ".json-diff-viewer {\n  width: 100%;\n  border-collapse: collapse;\n  border-spacing: 0;\n  table-layout: fixed;\n\n  tr {\n    vertical-align: top;\n\n    .line-add {\n      background: #a5d6a7;\n    }\n\n    .line-remove {\n      background: #ef9a9a;\n    }\n\n    .line-modify {\n      background: #ffe082;\n    }\n\n    &:hover td {\n      position: relative;\n\n      &:before {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        background: rgba(0, 0, 0, 0.05);\n        content: '';\n        pointer-events: none;\n      }\n    }\n\n    &.message-line {\n      border-top: 1px solid;\n      border-bottom: 1px solid;\n      text-align: center;\n\n      td {\n        padding: 4px 0;\n        font-size: 12px;\n      }\n    }\n\n    &.expand-line {\n      text-align: center;\n\n      td {\n        padding: 4px 0;\n      }\n\n      &:hover td:before {\n        background: transparent;\n      }\n\n      .has-lines-before {\n        border-bottom: 1px solid;\n      }\n\n      .has-lines-after {\n        border-top: 1px solid;\n      }\n\n      button {\n        padding: 0;\n        border: none;\n        margin: 0 0.5em;\n        background: transparent;\n        color: #2196f3;\n        cursor: pointer;\n        font-size: 12px;\n        user-select: none;\n\n        &:hover {\n          text-decoration: underline;\n        }\n      }\n    }\n  }\n\n  td {\n    padding: 1px;\n    font-size: 0;\n\n    &.line-number {\n      box-sizing: content-box;\n      padding: 0 8px;\n      border-right: 1px solid;\n      font-family: monospace;\n      font-size: 14px;\n      text-align: right;\n      user-select: none;\n    }\n  }\n\n  pre {\n    overflow: hidden;\n    margin: 0;\n    font-size: 12px;\n    line-height: 16px;\n    white-space: pre-wrap;\n    word-break: break-all;\n\n    .inline-diff-add {\n      background: rgba(0, 0, 0, 0.08);\n      text-decoration: underline;\n      word-break: break-all;\n    }\n\n    .inline-diff-remove {\n      background: rgba(0, 0, 0, 0.08);\n      text-decoration: line-through;\n      word-break: break-all;\n    }\n  }\n\n  &-virtual pre {\n    overflow-x: auto;\n    white-space: pre;\n\n    &::-webkit-scrollbar {\n      display: none;\n    }\n  }\n}\n"
  },
  {
    "path": "src/viewer.tsx",
    "content": "import * as React from 'react';\n\nimport type { DiffResult } from './differ';\n\nimport calculatePlaceholderHeight from './utils/calculate-placeholder-height';\nimport findVisibleLines from './utils/find-visible-lines';\nimport getInlineDiff from './utils/get-inline-diff';\nimport type { InlineDiffOptions } from './utils/get-inline-diff';\nimport getInlineSyntaxHighlight from './utils/get-inline-syntax-highlight';\nimport getSegments from './utils/get-segments';\nimport type { HiddenUnchangedLinesInfo, SegmentItem } from './utils/get-segments';\nimport { isExpandLine, mergeSegments, type InlineRenderInfo } from './utils/segment-util';\n\ninterface ExpandLineRendererOptions {\n  /**\n   * If this is `true`, you can show a \"⭡ Show xx lines before\" button\n   */\n  hasLinesBefore: boolean;\n  /**\n   * If this is `true`, you can show a \"⭣ Show xx lines after\" button\n   */\n  hasLinesAfter: boolean;\n  /**\n   * Call this function to expand `lines` lines before,\n   * if there are not enough lines before, it will expand all lines before.\n   */\n  onExpandBefore: (lines: number) => void;\n  /**\n   * Call this function to expand `lines` lines after,\n   * if there are not enough lines after, it will expand all lines after.\n   */\n  onExpandAfter: (lines: number) => void;\n  /**\n   * Call this function to expand all lines in this continuous part.\n   */\n  onExpandAll: () => void;\n}\n\nexport type HideUnchangedLinesOptions = boolean | {\n  /**\n   * If there are continuous unchanged lines exceeding the limit, they should be hidden,\n   * default is `8`.\n   */\n  threshold?: number;\n  /**\n   * We can keep displaying some lines around the \"expand\" line for a better context,\n   * default is `3`.\n   */\n  margin?: number;\n  /**\n   * Controls how many lines will be displayed when clicking the \"Show xx lines before\"\n   * or \"Show xx lines after\" button in the \"expand\" line, default is `20`.\n   */\n  expandMoreLinesLimit?: number;\n  /**\n   * The custom renderer of the \"expand\" line,\n   * default renderer will produce the following buttons in this line:\n   *\n   * ```text\n   * [⭡ Show 20 lines] [⭥ Show all unchanged lines] [⭣ Show 20 lines]\n   * ```\n   */\n  expandLineRenderer?: (options?: ExpandLineRendererOptions) => JSX.Element;\n}\n\nexport interface ViewerProps {\n  /** The diff result `[before, after]`. */\n  diff: readonly [DiffResult[], DiffResult[]];\n  /** Configure indent, default `2` means 2 spaces. */\n  indent?: number | 'tab';\n  /** Background colour for 3 types of lines. */\n  bgColour?: {\n    add?: string;\n    remove?: string;\n    modify?: string;\n  };\n  /** Display line numbers, default is `false`. */\n  lineNumbers?: boolean;\n  /** Whether to show the inline diff highlight, default is `false`. */\n  highlightInlineDiff?: boolean;\n  /** Controls the inline diff behaviour, the `highlightInlineDiff` must be enabled. */\n  inlineDiffOptions?: InlineDiffOptions;\n  /**\n   * Hide continuous unchanged lines and display an \"expand\" instead,\n   * default `false` means it won't hide unchanged lines.\n   */\n  hideUnchangedLines?: HideUnchangedLinesOptions;\n  /**\n   * Use virtual list to speed up rendering, default is `false`.\n   */\n  virtual?: boolean | {\n    /** @default 'body' */\n    scrollContainer?: string;\n    /** @default 18 */\n    itemHeight?: number;\n    /** @default 26 */\n    expandLineHeight?: number;\n  };\n  /**\n   * Enable the syntax highlight feature, default is `false`.\n   */\n  syntaxHighlight?: false | {\n    /**\n     * The syntax highlighting theme; it will add a className to `table`.\n     *\n     * NOTICE:\n     * - You need to import the corresponding CSS file manually.\n     * @default 'monokai'\n     */\n    theme?: string;\n  };\n  /**\n   * Configure the texts in the viewer, you can use it to implement i18n.\n   */\n  texts?: {\n    /** @default 'No change detected' */\n    noChangeDetected?: string;\n    /** @default '⭡ Show %d lines before', where %d is the number */\n    showLinesBefore?: string;\n    /** @default '⭣ Show %d lines after', where %d is the number */\n    showLinesAfter?: string;\n    /** @default '⭥ Show all unchanged lines' */\n    showAll?: string;\n  };\n  /** Extra class names */\n  className?: string;\n  /** Extra styles */\n  style?: React.CSSProperties;\n}\n\nconst DEFAULT_INDENT = 2;\nconst DEFAULT_EXPAND_MORE_LINES_LIMIT = 20;\nconst DEFAULT_TEXTS = {\n  noChangeDetected: 'No change detected',\n  showLinesBefore: '⭡ Show %d lines before',\n  showLinesAfter: '⭣ Show %d lines after',\n  showAll: '⭥ Show all unchanged lines',\n};\n\nconst Viewer: React.FC<ViewerProps> = props => {\n  const [linesLeft, linesRight] = props.diff;\n  const jsonsAreEqual = React.useMemo(() => {\n    return (\n      linesLeft.length === linesRight.length &&\n      linesLeft.every(item => item.type === 'equal') &&\n      linesRight.every(item => item.type === 'equal')\n    );\n  }, [linesLeft, linesRight]);\n\n  const mergedTexts = { ...DEFAULT_TEXTS, ...props.texts };\n\n  const lineNumberWidth = props.lineNumbers ? `calc(${String(linesLeft.length).length}ch + 16px)` : 0;\n  const indent = props.indent ?? DEFAULT_INDENT;\n  const indentChar = indent === 'tab' ? '\\t' : ' ';\n  const indentSize = indent === 'tab' ? 1 : indent;\n  const inlineDiffOptions: InlineDiffOptions = {\n    mode: props.inlineDiffOptions?.mode || 'char',\n    wordSeparator: props.inlineDiffOptions?.wordSeparator || '',\n  };\n  const hideUnchangedLines = props.hideUnchangedLines ?? false;\n  const {\n    scrollContainer: _scrollContainer = 'body',\n    itemHeight = 18,\n    expandLineHeight = 26,\n  } = !props.virtual || props.virtual === true ? {} : props.virtual;\n  const scrollContainer = _scrollContainer === 'body'\n    ? document.body\n    : document.querySelector(_scrollContainer);\n  const totalColumns = props.lineNumbers ? 4 : 2;\n\n  // Use these refs to keep the diff data and segments sync,\n  // or it may cause runtime error because of their mismatch.\n  // Do not use the states to render, use the refs to render and use `updateViewer` to update.\n  const linesLeftRef = React.useRef(linesLeft);\n  const linesRightRef = React.useRef(linesRight);\n  const segmentsRef = React.useRef(getSegments(linesLeft, linesRight, hideUnchangedLines, jsonsAreEqual));\n  const accTopRef = React.useRef<number[]>([]);\n  const totalHeightRef = React.useRef(0);\n  const tbodyRef = React.useRef<HTMLTableSectionElement>(null);\n  const [, forceUpdate] = React.useState({});\n\n  const updateViewer = () => {\n    accTopRef.current = [];\n    if (props.virtual) {\n      let acc = 0;\n      for (const segment of segmentsRef.current) {\n        if (isExpandLine(segment)) {\n          accTopRef.current.push(acc);\n          acc += expandLineHeight;\n        } else {\n          accTopRef.current.push(acc);\n          acc += itemHeight * (segment.end - segment.start);\n        }\n      }\n      totalHeightRef.current = segmentsRef.current.reduce((acc, segment) => {\n        if (!isExpandLine(segment)) {\n          return acc + (segment.end - segment.start) * itemHeight;\n        }\n        return acc + expandLineHeight;\n      }, 0);\n    }\n    forceUpdate({});\n  };\n\n  React.useEffect(() => {\n    linesLeftRef.current = linesLeft;\n    linesRightRef.current = linesRight;\n    segmentsRef.current = getSegments(linesLeft, linesRight, hideUnchangedLines, jsonsAreEqual);\n    updateViewer();\n  }, [hideUnchangedLines, linesLeft, linesRight]);\n\n  React.useEffect(() => {\n    if (!props.virtual || !scrollContainer) {\n      return;\n    }\n    const onScroll = () => forceUpdate({});\n    scrollContainer.addEventListener('scroll', onScroll);\n    return () => {\n      scrollContainer.removeEventListener('scroll', onScroll);\n    };\n  }, [props.virtual, scrollContainer]);\n\n  const onExpandBefore = (segmentIndex: number) => (lines: number) => {\n    const newSegments = [...segmentsRef.current];\n    const newSegment = newSegments[segmentIndex] as HiddenUnchangedLinesInfo;\n    newSegments[segmentIndex] = {\n      ...newSegment,\n      end: Math.max(newSegment.end - lines, newSegment.start),\n    };\n    if (segmentIndex + 1 < segmentsRef.current.length - 1) {\n      newSegments[segmentIndex + 1] = {\n        ...newSegments[segmentIndex + 1],\n        start: Math.max(newSegment.end - lines, newSegment.start),\n      };\n    }\n    segmentsRef.current = newSegments;\n    updateViewer();\n  };\n\n  const onExpandAfter = (segmentIndex: number) => (lines: number) => {\n    const newSegments = [...segmentsRef.current];\n    const newSegment = newSegments[segmentIndex] as HiddenUnchangedLinesInfo;\n    newSegments[segmentIndex] = {\n      ...newSegment,\n      start: Math.min(newSegment.start + lines, newSegment.end),\n    };\n    if (segmentIndex > 1) {\n      newSegments[segmentIndex - 1] = {\n        ...newSegments[segmentIndex - 1],\n        end: Math.min(newSegment.start + lines, newSegment.end),\n      };\n    }\n    segmentsRef.current = newSegments;\n    updateViewer();\n  };\n\n  const onExpandAll = (segmentIndex: number) => () => {\n    const newSegments = [...segmentsRef.current];\n    const newSegment = newSegments[segmentIndex] as HiddenUnchangedLinesInfo;\n    newSegments[segmentIndex] = {\n      ...newSegment,\n      start: newSegment.start,\n      end: newSegment.start,\n    };\n    if (segmentIndex + 1 < segmentsRef.current.length - 1) {\n      newSegments[segmentIndex + 1] = {\n        ...newSegments[segmentIndex + 1],\n        start: newSegment.start,\n      };\n    } else {\n      newSegments[segmentIndex - 1] = {\n        ...newSegments[segmentIndex - 1],\n        end: newSegment.end,\n      };\n    }\n    segmentsRef.current = newSegments;\n    updateViewer();\n  };\n\n  const renderInlineResult = (\n    text: string,\n    info: InlineRenderInfo[] = [],\n    comma = false,\n    syntaxHighlightEnabled = false,\n  ) => (\n    <>\n      {\n        info.map((item, index) => {\n          const frag = text.slice(item.start, item.end);\n\n          if (!item.type && !item.token) {\n            return frag;\n          }\n\n          const className = [\n            item.type ? `inline-diff-${item.type}` : '',\n            item.token ? `token ${item.token}` : '',\n          ].filter(Boolean).join(' ');\n          return (\n            <span key={`${index}-${item.type}-${frag}`} className={className}>\n              {frag}\n            </span>\n          );\n        })\n      }\n      {comma && (syntaxHighlightEnabled ? <span className=\"token punctuation\">,</span> : ',')}\n    </>\n  );\n\n  const renderLine = (index: number, syntaxHighlightEnabled: boolean) => {\n    const l = linesLeftRef.current[index];\n    const r = linesRightRef.current[index];\n\n    const [lDiff, rDiff] = props.highlightInlineDiff && l.type === 'modify' && r.type === 'modify'\n      ? getInlineDiff(l.text, r.text, inlineDiffOptions)\n      : [[], []];\n    const lTokens = getInlineSyntaxHighlight(syntaxHighlightEnabled, l.text, 0);\n    const rTokens = getInlineSyntaxHighlight(syntaxHighlightEnabled, r.text, 0);\n    const lResult = mergeSegments(lTokens, lDiff);\n    const rResult = mergeSegments(rTokens, rDiff);\n\n    const bgLeft = l.type !== 'equal' ? props.bgColour?.[l.type] ?? '' : '';\n    const bgRight = r.type !== 'equal' ? props.bgColour?.[r.type] ?? '' : '';\n\n    return (\n      // eslint-disable-next-line react/no-array-index-key\n      <tr key={index}>\n        {\n          props.lineNumbers && (\n            <td\n              className={`line-${l.type} line-number`}\n              style={{ backgroundColor: bgLeft }}\n            >\n              {l.lineNumber}\n            </td>\n          )\n        }\n        <td className={`line-${l.type}`} style={{ backgroundColor: bgLeft }}>\n          <pre>\n            {l.text && indentChar.repeat(l.level * indentSize)}\n            {renderInlineResult(l.text, lResult, l.comma, syntaxHighlightEnabled)}\n          </pre>\n        </td>\n        {\n          props.lineNumbers && (\n            <td\n              className={`line-${r.type} line-number`}\n              style={{ backgroundColor: bgRight }}\n            >\n              {r.lineNumber}\n            </td>\n          )\n        }\n        <td className={`line-${r.type}`} style={{ backgroundColor: bgRight }}>\n          <pre>\n            {r.text && indentChar.repeat(r.level * indentSize)}\n            {renderInlineResult(r.text, rResult, r.comma, syntaxHighlightEnabled)}\n          </pre>\n        </td>\n      </tr>\n    );\n  };\n\n  const renderExpandLine = (\n    hasLinesBefore: boolean,\n    hasLinesAfter: boolean,\n    expandMoreLinesLimit: number,\n    index: number,\n  ) => {\n    return (\n      <>\n        {\n          hasLinesBefore && (\n            <button onClick={() => onExpandBefore(index)(expandMoreLinesLimit)}>\n              {mergedTexts.showLinesBefore.replaceAll('%d', String(expandMoreLinesLimit))}\n            </button>\n          )\n        }\n        <button onClick={() => onExpandAll(index)()}>\n          {mergedTexts.showAll}\n        </button>\n        {\n          hasLinesAfter && (\n            <button onClick={() => onExpandAfter(index)(expandMoreLinesLimit)}>\n              {mergedTexts.showLinesAfter.replaceAll('%d', String(expandMoreLinesLimit))}\n            </button>\n          )\n        }\n      </>\n    );\n  };\n\n  const renderSegment = (\n    segment: SegmentItem | HiddenUnchangedLinesInfo,\n    index: number,\n    renderStart: number,\n    renderEnd: number,\n    syntaxHighlightEnabled: boolean,\n  ) => {\n    let { start, end } = segment;\n    start = Math.max(start, renderStart);\n    end = Math.min(end, renderEnd);\n    if (start === end) {\n      return null;\n    }\n    if (!isExpandLine(segment)) {\n      return Array(end - start).fill(0).map((_, index) => renderLine(start + index, syntaxHighlightEnabled));\n    }\n    const { hasLinesBefore, hasLinesAfter } = segment;\n    const expandMoreLinesLimit = typeof hideUnchangedLines === 'boolean'\n      ? DEFAULT_EXPAND_MORE_LINES_LIMIT\n      : hideUnchangedLines.expandMoreLinesLimit || DEFAULT_EXPAND_MORE_LINES_LIMIT;\n    return [\n      <tr key={`expand-line-${index}`} className=\"expand-line\">\n        <td\n          colSpan={totalColumns}\n          className={`${hasLinesBefore ? 'has-lines-before' : ''} ${hasLinesAfter ? 'has-lines-after' : ''}`}\n        >\n          {\n            typeof hideUnchangedLines !== 'boolean' && hideUnchangedLines.expandLineRenderer ? (\n              hideUnchangedLines.expandLineRenderer({\n                hasLinesBefore,\n                hasLinesAfter,\n                onExpandBefore: onExpandBefore(index),\n                onExpandAfter: onExpandAfter(index),\n                onExpandAll: onExpandAll(index),\n              })\n            ) : renderExpandLine(hasLinesBefore, hasLinesAfter, expandMoreLinesLimit, index)\n          }\n        </td>\n      </tr>,\n    ];\n  };\n\n  const renderTbody = (syntaxHighlightEnabled: boolean) => {\n    if (jsonsAreEqual && hideUnchangedLines) {\n      return (\n        <tr key=\"message-line\" className=\"message-line\">\n          <td colSpan={totalColumns}>\n            {mergedTexts.noChangeDetected}\n          </td>\n        </tr>\n      );\n    }\n    if (!props.virtual) {\n      return segmentsRef.current.map((item, index) => (\n        renderSegment(item, index, 0, linesLeftRef.current.length, syntaxHighlightEnabled)\n      ));\n    }\n    const containerHeight = (scrollContainer as HTMLElement)?.clientHeight ?? 0;\n    const scrollTop = (scrollContainer as HTMLElement)?.scrollTop ?? 0;\n    const scrollBottom = scrollTop + containerHeight;\n\n    let t: HTMLElement = tbodyRef.current!;\n    let firstElementTop = t?.offsetTop ?? 0;\n    while (t?.offsetParent && t?.offsetParent !== scrollContainer) {\n      t = t.offsetParent as HTMLElement;\n      firstElementTop += t.offsetTop;\n    }\n\n    if (firstElementTop > scrollBottom || firstElementTop + totalHeightRef.current < scrollTop) {\n      return (\n        <tr>\n          <td colSpan={totalColumns} style={{ height: `${totalHeightRef.current}px` }} />\n        </tr>\n      );\n    }\n    const viewportTop = scrollTop - firstElementTop;\n    const viewportBottom = scrollBottom - firstElementTop;\n    const [\n      startSegment,\n      startLine,\n      endSegment,\n      endLine,\n    ] = findVisibleLines(\n      segmentsRef.current,\n      accTopRef.current,\n      viewportTop,\n      viewportBottom,\n      itemHeight,\n      expandLineHeight,\n    );\n    const [topHeight, bottomHeight] = calculatePlaceholderHeight(\n      segmentsRef.current,\n      accTopRef.current,\n      startSegment,\n      startLine,\n      endSegment,\n      endLine,\n      itemHeight,\n      expandLineHeight,\n      totalHeightRef.current,\n    );\n    const visibleSegments = segmentsRef.current.slice(startSegment, endSegment + 1);\n    return visibleSegments.length ? (\n      <>\n        <tr><td colSpan={totalColumns} style={{ height: topHeight, padding: 0 }} /></tr>\n        {\n          visibleSegments.map((segment, index) => (\n            renderSegment(segment, index, startLine, endLine, syntaxHighlightEnabled)\n          ))\n        }\n        <tr><td colSpan={totalColumns} style={{ height: bottomHeight, padding: 0 }} /></tr>\n      </>\n    ) : (\n      <tr>\n        <td colSpan={totalColumns} style={{ height: `${totalHeightRef.current}px` }} />\n      </tr>\n    );\n  };\n\n  const renderMeasureLine = () => (\n    <colgroup className=\"measure-line\">\n      {props.lineNumbers && <col style={{ width: lineNumberWidth }} />}\n      <col />\n      {props.lineNumbers && <col style={{ width: lineNumberWidth }} />}\n      <col />\n    </colgroup>\n  );\n\n  const classes = [\n    'json-diff-viewer',\n    props.virtual && 'json-diff-viewer-virtual',\n    props.syntaxHighlight && `json-diff-viewer-theme-${props.syntaxHighlight.theme || 'monokai'}`,\n    props.className,\n  ].filter(Boolean).join(' ');\n\n  const syntaxHighlightEnabled = !!props.syntaxHighlight;\n  return (\n    <table className={classes} style={props.style}>\n      {renderMeasureLine()}\n      <tbody ref={tbodyRef}>\n        {renderTbody(syntaxHighlightEnabled)}\n      </tbody>\n    </table>\n  );\n};\n\nViewer.displayName = 'Viewer';\n\nexport default Viewer;\n"
  },
  {
    "path": "tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src\"],\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"baseUrl\": \".\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react\",\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"node\",\n    \"outDir\": \"typings\",\n    \"resolveJsonModule\": true,\n    \"sourceMap\": true,\n    \"strictNullChecks\": true,\n    \"target\": \"esnext\",\n    \"types\": [\"node\", \"jest\"],\n  },\n  \"include\": [\n    \"src\",\n    \"playground\",\n    \".eslintrc.cjs\",\n    \"./*.mjs\",\n  ],\n}\n"
  }
]