[
  {
    "path": ".gitattributes",
    "content": "*.golden -text\n"
  },
  {
    "path": ".github/images/autocomplete.tape",
    "content": "Output autocomplete.gif\nOutput autocomplete.mp4\n\nSet Shell zsh\nSet FontSize 32\nSet Width 1800\nSet Height 400\nSet TypingSpeed 200ms\n\nHide\nType \". ~/.zshrc && clear\"\nEnter\nShow\n\nType \"fx example.json \"\nSleep 1s\n\nType \".\"\nTab@500ms 2\nSleep 1s\nTab\nSleep 500ms\nTab\nSleep 500ms\nTab\nSleep 500ms\nTab\nSleep 500ms\nTab\nSleep 500ms\nTab\nSleep 1s\n\nType \".\"\nTab@500ms 2\nSleep 500ms\nTab\nSleep 2s\n\nEnter\nSleep 2s\n\n"
  },
  {
    "path": ".github/images/preview-mode.tape",
    "content": "Output preview-mode.gif\nOutput preview-mode.mp4\n\nSet FontSize 32\nSet Width 1800\nSet Height 1200\nSet TypingSpeed 200ms\n\nHide\nType \"fx testdata/blog.json\"\nEnter\nShow\n\nSleep 1s\nDown\nSleep 1s\nDown\nSleep 1s\n\nType \"p\"\nSleep 1s\nDown\nSleep 300ms\nDown\nSleep 300ms\nDown\nSleep 300ms\nDown\nSleep 300ms\nDown\nSleep 300ms\nDown\nSleep 300ms\nDown\nSleep 4s\nType \"p\"\n\nSleep 1s\nUp\nSleep 1s\nUp\n"
  },
  {
    "path": ".github/images/preview.tape",
    "content": "Output preview.gif\nOutput preview.mp4\n\nSet FontSize 32\nSet Width 1800\nSet Height 1200\nSet TypingSpeed 200ms\n\nHide\nType \"fx testdata/example.json\"\nEnter\nShow\n\nSleep 1s\nDown\nSleep 1s\nDown\nSleep 1s\nDown 6\nSleep 1s\nLeft\nSleep 1s\nDown\nSleep 1s\nDown\nSleep 1s\nDown\nSleep 1s\nLeft\nSleep 1s\nUp 12\nSleep 1s\n"
  },
  {
    "path": ".github/stream.mjs",
    "content": "#!/usr/bin/env zx\n\nprocess.on('SIGPIPE', () => process.exit(0))\nprocess.on('SIGINT', () => process.exit(0))\nprocess.on('SIGTERM', () => process.exit(0))\nprocess.stdout.on('error', (err) => {\n  if (err.code === 'EPIPE') process.exit(0)\n  throw err\n})\n\nconst names = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry']\nconst cities = ['New York', 'London', 'Tokyo', 'Paris', 'Berlin', 'Sydney', 'Toronto', 'Mumbai']\nconst colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink', 'cyan']\nconst departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance', 'Operations']\nconst skills = ['JavaScript', 'Python', 'Go', 'Rust', 'SQL', 'Docker', 'Kubernetes', 'AWS']\n\nfunction randomItem(arr) {\n  return arr[Math.floor(Math.random() * arr.length)]\n}\n\nfunction randomItems(arr, min = 1, max = 3) {\n  const count = min + Math.floor(Math.random() * (max - min + 1))\n  const shuffled = [...arr].sort(() => Math.random() - 0.5)\n  return shuffled.slice(0, count)\n}\n\nfunction randomObject() {\n  return {\n    id: Math.floor(Math.random() * 1000000),\n    name: randomItem(names),\n    active: Math.random() > 0.5,\n    timestamp: new Date().toISOString(),\n    profile: {\n      age: 20 + Math.floor(Math.random() * 40),\n      city: randomItem(cities),\n      preferences: {\n        color: randomItem(colors),\n        notifications: Math.random() > 0.5,\n        theme: Math.random() > 0.5 ? 'dark' : 'light',\n      },\n    },\n    work: {\n      department: randomItem(departments),\n      salary: Math.floor(50000 + Math.random() * 100000),\n      skills: randomItems(skills, 2, 5),\n    },\n    scores: Array.from({length: 3}, () => Math.round(Math.random() * 100)),\n    tags: randomItems(colors, 1, 3),\n  }\n}\n\nconst randomTexts = [\n  'Processing records...',\n  'Loading next batch',\n  '--- checkpoint ---',\n  'Fetching data from server',\n  'INFO: Connection stable',\n  'DEBUG: Buffer flushed',\n  'Waiting for response...',\n  '>> sync complete',\n]\n\nconst count = parseInt(argv._[0]) || Infinity\nconst delay = parseInt(argv.delay) || 100\nconst withText = argv['with-text'] || argv.withText\n\nfor (let i = 0; i < count; i++) {\n  if (withText && Math.random() < 0.15) {\n    console.log(randomItem(randomTexts))\n    if (delay > 0) await sleep(delay)\n  }\n  console.log(JSON.stringify(randomObject()))\n  if (delay > 0 && i < count - 1) {\n    await sleep(delay)\n  }\n}\n"
  },
  {
    "path": ".github/workflows/brew.yml",
    "content": "name: brew\n\non: [workflow_dispatch]\n\njobs:\n  brew:\n    runs-on: macos-latest\n    steps:\n      - name: Set up Homebrew\n        id: set-up-homebrew\n        uses: Homebrew/actions/setup-homebrew@master\n        with:\n          test-bot: false\n\n      - name: Install Homebrew Bundler RubyGems\n        run: brew install-bundler-gems\n\n      - name: Configure Git user\n        uses: Homebrew/actions/git-user-config@master\n\n      - name: Update brew\n        run: brew update\n\n      - name: Bump formulae\n        uses: Homebrew/actions/bump-packages@master\n        with:\n          token: ${{ secrets.MY_HOMEBREW_RELEASE_GITHUB_TOKEN }}\n          formulae: |\n            fx\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: docker\n\non: [workflow_dispatch]\n\njobs:\n  docker:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: antonmedv\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: ./Dockerfile\n          push: true\n          platforms: linux/amd64,linux/arm64\n          tags: antonmedv/fx:latest\n"
  },
  {
    "path": ".github/workflows/snap.yml",
    "content": "name: snap\n\non: [workflow_dispatch]\n\njobs:\n  snap:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          ref: master\n\n      - name: Set up Go\n        uses: actions/setup-go@v3\n        with:\n          go-version: 1.21\n\n      - uses: snapcore/action-build@v1\n        id: build\n\n      - uses: snapcore/action-publish@v1\n        env:\n          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.MY_SNAPCRAFT_CREDENTIALS }}\n        with:\n          snap: ${{ steps.build.outputs.snap }}\n          release: stable\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  go:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-go@v3\n        with:\n          go-version: 1.21\n      - name: Test\n        run: go test ./...\n\n  go-arm:\n    runs-on: ubuntu-24.04-arm\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-go@v3\n        with:\n          go-version: 1.21\n      - name: Test on ARM\n        run: go test ./...\n\n  node:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Test NPM version\n        run: cd npm && node test.js\n"
  },
  {
    "path": ".gitignore",
    "content": "*.prof\nfx\nfx.exe\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:latest as builder\n\nWORKDIR /go\n\nCOPY go.mod go.sum ./\n\nRUN go mod download\n\nCOPY . .\n\nRUN CGO_ENABLED=0 go build -o fx .\n\nFROM alpine\n\nCOPY --from=builder /go/fx /bin/fx\n\nWORKDIR /data\n\nENV COLORTERM=truecolor\n\nENTRYPOINT [\"/bin/fx\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Anton Medvedev\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": "# f(x)\n\n<p align=\"center\"><a href=\"https://fx.wtf\"><img src=\".github/images/preview.gif\" width=\"500\" alt=\"fx preview\"></a></p>\n\n## Documentation\n\nSee full documentation at [fx.wtf](https://fx.wtf).\n\n## Related\n\n- [walk](https://github.com/antonmedv/walk) – terminal file manager\n- [howto](https://github.com/antonmedv/howto) – terminal command LLM helper\n- [countdown](https://github.com/antonmedv/countdown) – terminal countdown timer\n\n## License\n\n[MIT](LICENSE)\n\n<p align=\"center\">\n  <a href=\"https://crow.watch/join/fx\">\n    <img src=\"https://github.com/user-attachments/assets/37c84073-6533-4746-951d-d879f90a7fd2\" alt=\"Join Crow Watch\" width=\"900\" hight=\"600\">  \n  </a>\n</p>\n"
  },
  {
    "path": "RELEASE.md",
    "content": "# Release\n\n1. Bump version in [version.go](version.go).\n2. Bump version in [snapcraft.yaml](snap/snapcraft.yaml).\n3. Bump version in [package.json](npm/package.json).\n4. Commit changes.\n5. Publish npm package.\n6. Trigger [GitHub Actions](https://github.com/antonmedv/fx/actions) (brew, snap, docker).\n7. Create a new release on [GitHub](https://github.com/antonmedv/fx/releases/new).\n8. Run [build.mjs](scripts/build.mjs) to upload binaries to the release.\n   ```sh\n   npx zx scripts/build.mjs\n   ```\n9. Bump version in [install.sh](https://github.com/antonmedv/fx.wtf/blob/master/public/install.sh) and upload it\n   to [fx.wtf](https://fx.wtf).\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/antonmedv/fx\n\ngo 1.23.0\n\ntoolchain go1.23.6\n\nrequire (\n\tgithub.com/antonmedv/clipboard v1.0.1\n\tgithub.com/charmbracelet/bubbles v0.21.0\n\tgithub.com/charmbracelet/bubbletea v1.3.6\n\tgithub.com/charmbracelet/lipgloss v1.1.0\n\tgithub.com/charmbracelet/x/exp/teatest v0.0.0-20231025135604-4a717d4fb812\n\tgithub.com/charmbracelet/x/term v0.2.1\n\tgithub.com/dop251/goja v0.0.0-20250630131328-58d95d85e994\n\tgithub.com/goccy/go-yaml v1.18.0\n\tgithub.com/mattn/go-isatty v0.0.20\n\tgithub.com/mattn/go-runewidth v0.0.16\n\tgithub.com/muesli/termenv v0.16.0\n\tgithub.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646\n\tgithub.com/pelletier/go-toml/v2 v2.2.3\n\tgithub.com/rivo/uniseg v0.4.7\n\tgithub.com/stretchr/testify v1.9.0\n)\n\nrequire (\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/aymanbagabas/go-udiff v0.2.0 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.3.1 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.10.1 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.13 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect\n\tgithub.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgolang.org/x/sync v0.16.0 // indirect\n\tgolang.org/x/sys v0.35.0 // indirect\n\tgolang.org/x/text v0.27.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=\ngithub.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=\ngithub.com/antonmedv/clipboard v1.0.1 h1:z9rRBhSKt4lDb6uNcMykUmNbspk/6v07JeiTaOfYYOY=\ngithub.com/antonmedv/clipboard v1.0.1/go.mod h1:3jcOUCdraVHehZaOsMaJZoE92MxURt5fovC1gDAiZ2s=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=\ngithub.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=\ngithub.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=\ngithub.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=\ngithub.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=\ngithub.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=\ngithub.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=\ngithub.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=\ngithub.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=\ngithub.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=\ngithub.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=\ngithub.com/charmbracelet/x/exp/teatest v0.0.0-20231025135604-4a717d4fb812 h1:W/hU7Z+y+QsZo2qg0hwjv56qSMP12Z72DJR8k+ULbA4=\ngithub.com/charmbracelet/x/exp/teatest v0.0.0-20231025135604-4a717d4fb812/go.mod h1:TckAxPtan3aJ5wbTgBkySpc50SZhXJRZ8PtYICnZJEw=\ngithub.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=\ngithub.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 h1:aQYWswi+hRL2zJqGacdCZx32XjKYV8ApXFGntw79XAM=\ngithub.com/dop251/goja v0.0.0-20250630131328-58d95d85e994/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=\ngithub.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=\ngithub.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=\ngithub.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ=\ngithub.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=\ngithub.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=\ngithub.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=\ngithub.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngolang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=\ngolang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=\ngolang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=\ngolang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=\ngolang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngolang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=\ngolang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "help.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nfunc usage() string {\n\ttitle := lipgloss.NewStyle().Bold(true)\n\treturn fmt.Sprintf(`\n  %v\n    Terminal JSON viewer\n\n  %v\n    fx data.json\n    fx data.json .field\n    curl ... | fx\n\n  %v\n    -h, --help            print help\n    -v, --version         print version\n    --themes              print themes\n    --comp <shell>        print completion script\n    -r, --raw             treat input as a raw string\n    -s, --slurp           read all inputs into an array\n    --yaml                parse input as YAML\n    --toml                parse input as TOML\n    --strict              strict mode\n    --no-inline           disable inlining in output\n    --game-of-life        play the game of life\n\n  %v\n    https://fx.wtf\n\n  %v\n    Anton Medvedev <anton@medv.io>\n`,\n\t\ttitle.Render(\"fx \"+version),\n\t\ttitle.Render(\"Usage\"),\n\t\ttitle.Render(\"Flags\"),\n\t\ttitle.Render(\"More info\"),\n\t\ttitle.Render(\"Author\"),\n\t)\n}\n\nvar categoryOrder = []string{\n\t\"Navigation\",\n\t\"Expand / Collapse\",\n\t\"Search\",\n\t\"Actions\",\n\t\"View\",\n\t\"Other\",\n}\n\nfunc help(keyMap KeyMap) string {\n\ttitleStyle := lipgloss.NewStyle().\n\t\tBold(true)\n\n\tcategoryStyle := lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"8\")).\n\t\tMarginTop(1)\n\n\tkeyStyle := lipgloss.NewStyle()\n\n\tdescStyle := lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"8\"))\n\n\tdimStyle := lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(\"8\"))\n\n\t// Group bindings by category using struct tags\n\tv := reflect.ValueOf(keyMap)\n\tt := v.Type()\n\tcategories := make(map[string][]key.Binding)\n\n\tfor _, field := range reflect.VisibleFields(t) {\n\t\tcategory := field.Tag.Get(\"category\")\n\t\tif category == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tbinding := v.FieldByName(field.Name).Interface().(key.Binding)\n\t\tcategories[category] = append(categories[category], binding)\n\t}\n\n\tvar sb strings.Builder\n\n\t// Header\n\tsb.WriteString(\"\\n\")\n\tsb.WriteString(titleStyle.Render(\"  Key Bindings\"))\n\tsb.WriteString(\"\\n\")\n\tsb.WriteString(dimStyle.Render(\"  ─────────────────────────────────────────\"))\n\tsb.WriteString(\"\\n\")\n\n\tfor _, cat := range categoryOrder {\n\t\tbindings, ok := categories[cat]\n\t\tif !ok || len(bindings) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tsb.WriteString(categoryStyle.Render(\"  \" + cat))\n\t\tsb.WriteString(\"\\n\")\n\n\t\tfor _, binding := range bindings {\n\t\t\tkeyStr := binding.Help().Key\n\t\t\tif len(keyStr) == 0 {\n\t\t\t\tkeys := binding.Keys()\n\t\t\t\tif len(keys) > 5 {\n\t\t\t\t\tkeyStr = fmt.Sprintf(\"%v-%v\", keys[0], keys[len(keys)-1])\n\t\t\t\t} else {\n\t\t\t\t\tkeyStr = strings.Join(keys, \", \")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tdesc := binding.Help().Desc\n\t\t\tkeyFormatted := keyStyle.Render(fmt.Sprintf(\"%-20s\", keyStr))\n\t\t\tdescFormatted := descStyle.Render(desc)\n\n\t\t\tsb.WriteString(fmt.Sprintf(\"    %s  %s\\n\", keyFormatted, descFormatted))\n\t\t}\n\t}\n\n\tsb.WriteString(\"\\n\")\n\tsb.WriteString(dimStyle.Render(\"  ─────────────────────────────────────────\"))\n\tsb.WriteString(\"\\n\")\n\tsb.WriteString(dimStyle.Render(\"  Press q or ? to close\"))\n\tsb.WriteString(\"\\n\")\n\n\treturn sb.String()\n}\n\nfunc exit() {\n\tif showLetter(time.Now()) {\n\t\tstyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(1, 2)\n\t\t_, _ = fmt.Fprintln(os.Stderr, style.Render(`Hello, kind human. :)\n\nThis is fx speaking. I know you’re busy, and I won’t take much\nof your time.\n\nEvery day, I quietly sit in your terminal, helping you explore\nand shape your data. No popups, no ads, quiet, helpful work.\n\nBut today is different.\n\nToday, I’m asking for something small in return. Just for today.\n\nIf fx has saved you time, solved a problem, or simply made your \nlife in the terminal a little easier, please consider supporting \nthe developer who made me:\n\n    https://github.com/sponsors/antonmedv\n\nHe built fx as a passion project, shared it freely with the world, \nand has kept improving it—all without asking much.\n\nYour support helps keep fx alive, maintained, and improving.\nEven a small donation means a lot. It shows that you care, that \nthis kind of work matters.\n\nThis message only appears once, on the first Tuesday of December.\nTomorrow I’ll be silent again.\n\nThank you for reading. And thank you for using fx.`))\n\t}\n}\n\nfunc showLetter(t time.Time) bool {\n\tif t.Month() != time.December {\n\t\treturn false\n\t}\n\tfirstOfDecember := time.Date(t.Year(), time.December, 1, 0, 0, 0, 0, t.Location())\n\toffset := (int(time.Tuesday) - int(firstOfDecember.Weekday()) + 7) % 7\n\tfirstTuesday := firstOfDecember.AddDate(0, 0, offset)\n\treturn t.Year() == firstTuesday.Year() &&\n\t\tt.Month() == firstTuesday.Month() &&\n\t\tt.Day() == firstTuesday.Day()\n}\n"
  },
  {
    "path": "internal/complete/complete.bash",
    "content": "complete -o filenames -C fx fx\n"
  },
  {
    "path": "internal/complete/complete.fish",
    "content": "complete --command fx --arguments '(COMP_FISH=(commandline -cp) fx)'\n"
  },
  {
    "path": "internal/complete/complete.go",
    "content": "package complete\n\nimport (\n\t_ \"embed\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/dop251/goja\"\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/pelletier/go-toml/v2\"\n\n\t\"github.com/antonmedv/fx/internal/engine\"\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n\t\"github.com/antonmedv/fx/internal/shlex\"\n)\n\ntype Reply struct {\n\tDisplay string\n\tValue   string\n\tType    string // \"file\" for files, others optional\n}\n\nvar Flags []Reply\n\n//go:embed complete.bash\nvar Bash string\n\n//go:embed complete.zsh\nvar Zsh string\n\n//go:embed complete.fish\nvar Fish string\n\n//go:embed prelude.js\nvar prelude string\n\nfunc Complete() bool {\n\tcompLine, ok := os.LookupEnv(\"COMP_LINE\")\n\tif ok && len(os.Args) >= 3 {\n\t\tdoComplete(compLine, os.Args[2], false)\n\t\treturn true\n\t}\n\n\tcompZsh, ok := os.LookupEnv(\"COMP_ZSH\")\n\tif ok {\n\t\tdoComplete(compZsh, lastWord(compZsh), true)\n\t\treturn true\n\t}\n\n\tcompFish, ok := os.LookupEnv(\"COMP_FISH\")\n\tif ok {\n\t\tdoComplete(compFish, lastWord(compFish), false)\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc doComplete(compLine string, compWord string, withDisplay bool) {\n\tif strings.HasPrefix(compWord, \"-\") {\n\t\tcompReply(filterReply(Flags, compWord), withDisplay)\n\t\treturn\n\t}\n\n\targs, err := shlex.Split(compLine)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tcompWord = shlex.Parse(compWord)\n\n\tvar flagYaml bool\n\tvar flagToml bool\n\tfor _, arg := range args {\n\t\tif arg == \"--yaml\" {\n\t\t\tflagYaml = true\n\t\t}\n\t\tif arg == \"--toml\" {\n\t\t\tflagToml = true\n\t\t}\n\t}\n\n\t// Remove Flags from args.\n\targs = filterArgs(args)\n\n\tisSecondArgIsFile := false\n\tif len(args) == 0 {\n\t\treturn\n\t} else if len(args) == 1 {\n\t\treply := fileComplete(compWord)\n\t\tcompReply(reply, withDisplay)\n\t\treturn\n\t} else if len(args) == 2 {\n\t\tisSecondArgIsFile = isFile(args[1])\n\t\tif !isSecondArgIsFile {\n\t\t\treply := fileComplete(compWord)\n\t\t\tcompReply(reply, withDisplay)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tisSecondArgIsFile = isFile(args[1])\n\t}\n\n\tvar reply []Reply\n\n\tif isSecondArgIsFile {\n\t\tfile := args[1]\n\n\t\thasYamlExt, _ := regexp.MatchString(`(?i)\\.ya?ml$`, file)\n\t\thasTomlExt, _ := regexp.MatchString(`(?i)\\.toml$`, file)\n\t\tif !flagYaml && hasYamlExt {\n\t\t\tflagYaml = true\n\t\t}\n\t\tif !flagToml && hasTomlExt {\n\t\t\tflagToml = true\n\t\t}\n\n\t\tif strings.HasPrefix(file, \"~\") {\n\t\t\thome, err := os.UserHomeDir()\n\t\t\tif err == nil {\n\t\t\t\tfile = filepath.Join(home, file[1:])\n\t\t\t}\n\t\t}\n\n\t\tresultCh := make(chan []Reply, 1)\n\n\t\tgo func() {\n\t\t\tinput, err := os.ReadFile(file)\n\t\t\tif err != nil {\n\t\t\t\tresultCh <- []Reply{}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif flagYaml {\n\t\t\t\tinput, err = yaml.YAMLToJSON(input)\n\t\t\t\tif err != nil {\n\t\t\t\t\tresultCh <- []Reply{}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else if flagToml {\n\t\t\t\tvar v any\n\t\t\t\tif err := toml.Unmarshal(input, &v); err != nil {\n\t\t\t\t\tresultCh <- []Reply{}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tb, err := json.Marshal(v)\n\t\t\t\tif err != nil {\n\t\t\t\t\tresultCh <- []Reply{}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tinput = b\n\t\t\t}\n\n\t\t\tnode, err := jsonx.Parse(input)\n\t\t\tif err != nil {\n\t\t\t\tresultCh <- []Reply{}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresultCh <- KeysComplete(node, args, compWord)\n\t\t}()\n\n\t\tselect {\n\t\tcase result := <-resultCh:\n\t\t\treply = append(reply, result...)\n\t\tcase <-time.After(3 * time.Second):\n\t\t\treturn\n\t\t}\n\t}\n\n\treply = filterReply(reply, compWord)\n\tif len(reply) > 0 {\n\t\tcompReply(reply, withDisplay)\n\t\treturn\n\t}\n\n\tif len(compWord) > 0 {\n\t\t// Only show globals if compWord is not empty,\n\t\t// as we do not want to be very verbose and show all globals.\n\t\tcompReply(filterReply(globalsComplete(), compWord), withDisplay)\n\t}\n}\n\nfunc globalsComplete() []Reply {\n\tvar code strings.Builder\n\tcode.WriteString(prelude)\n\tcode.WriteString(engine.Stdlib)\n\tcode.WriteString(\"\\n__autocomplete()\\n\")\n\n\tvm := goja.New()\n\tvalue, err := vm.RunString(code.String())\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tif array, ok := value.Export().([]any); ok {\n\t\tvar reply []Reply\n\t\tfor _, key := range array {\n\t\t\treply = append(reply, Reply{\n\t\t\t\tDisplay: key.(string),\n\t\t\t\tValue:   key.(string),\n\t\t\t\tType:    \"global\",\n\t\t\t})\n\t\t}\n\t\treturn reply\n\t}\n\treturn nil\n}\n\nfunc KeysComplete(input *jsonx.Node, args []string, compWord string) []Reply {\n\targs = args[2:] // Drop binary & file from the args.\n\n\tif compWord == \"\" {\n\t\targs = append(args, \".__keys()\")\n\t} else {\n\t\tif len(args) > 0 {\n\t\t\tlast := args[len(args)-1]\n\t\t\tlast = dropTail(args[len(args)-1])\n\t\t\tlast = last + \".__keys()\"\n\t\t\tlast = balanceBrackets(last)\n\t\t\targs[len(args)-1] = last\n\t\t}\n\t}\n\n\tvar code strings.Builder\n\tcode.WriteString(prelude)\n\tcode.WriteString(engine.Stdlib)\n\tcode.WriteString(engine.JS(args))\n\tcode.WriteString(\"\\n__main__(json)\\n__keys\\n\")\n\n\tvm := goja.New()\n\tif err := vm.Set(\"json\", input.ToValue(vm)); err != nil {\n\t\treturn nil\n\t}\n\tvalue, err := vm.RunString(code.String())\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tif array, ok := value.Export().([]interface{}); ok {\n\t\tprefix := dropTail(compWord)\n\t\tvar reply []Reply\n\t\tfor _, key := range array {\n\t\t\tk := key.(string)\n\t\t\treply = append(reply, Reply{\n\t\t\t\tDisplay: join(\"\", k),\n\t\t\t\tValue:   join(prefix, k),\n\t\t\t\tType:    \"key\",\n\t\t\t})\n\t\t}\n\t\treturn reply\n\t}\n\treturn nil\n}\n\nvar alphaRe = regexp.MustCompile(`^[A-Za-z_$][A-Za-z0-9_$]*$`)\n\nfunc join(prefix, key string) string {\n\tif alphaRe.MatchString(key) {\n\t\treturn prefix + \".\" + key\n\t} else {\n\t\tif prefix == \"\" {\n\t\t\treturn fmt.Sprintf(\".[%q]\", key)\n\t\t}\n\t\treturn fmt.Sprintf(\"%s[%q]\", prefix, key)\n\t}\n}\n\nfunc filterArgs(args []string) []string {\n\tfiltered := make([]string, 0, len(args))\n\tfor _, arg := range args {\n\t\tfound := false\n\t\tfor _, flag := range Flags {\n\t\t\tif arg == flag.Value {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\tfiltered = append(filtered, arg)\n\t\t}\n\t}\n\treturn filtered\n}\n\nfunc fileComplete(compWord string) []Reply {\n\toriginal := compWord\n\n\t// Step 1: Expand ~ to home directory\n\tif strings.HasPrefix(compWord, \"~\") {\n\t\tif compWord == \"~\" || strings.HasPrefix(compWord, \"~/\") {\n\t\t\thome, err := os.UserHomeDir()\n\t\t\tif err == nil {\n\t\t\t\tcompWord = filepath.Join(home, compWord[1:])\n\t\t\t}\n\t\t} else {\n\t\t\t// We don't support ~username completion\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// Step 2: If compWord ends in \"/\", treat it as a directory and add a \"*\" pattern\n\tinfo, err := os.Stat(compWord)\n\tif err == nil && info.IsDir() && !strings.HasSuffix(compWord, \"*\") {\n\t\tcompWord = filepath.Join(compWord, \"*\")\n\t} else if !strings.HasSuffix(compWord, \"*\") {\n\t\t// Add wildcard if not already present\n\t\tcompWord = compWord + \"*\"\n\t}\n\n\t// Step 3: Perform globbing\n\tfiles, err := filepath.Glob(compWord)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\t// Step 4: Format matches\n\tvar matches []Reply\n\tfor _, match := range files {\n\t\tif match == \".\" || match == \"..\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar suggestion string\n\t\tif strings.HasPrefix(original, \"~\") {\n\t\t\thome, _ := os.UserHomeDir()\n\t\t\tif strings.HasPrefix(match, home) {\n\t\t\t\tsuggestion = \"~\" + strings.TrimPrefix(match, home)\n\t\t\t} else {\n\t\t\t\tsuggestion = match\n\t\t\t}\n\t\t} else if filepath.IsAbs(original) {\n\t\t\tsuggestion = match\n\t\t} else {\n\t\t\trel, err := filepath.Rel(\".\", match)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tsuggestion = rel\n\t\t}\n\n\t\tdirSuffix := \"\"\n\t\tinfo, err := os.Stat(match)\n\t\tif err == nil {\n\t\t\tif info.IsDir() {\n\t\t\t\tdirSuffix = \"/\"\n\t\t\t}\n\t\t}\n\n\t\tmatches = append(matches, Reply{\n\t\t\tDisplay: filepath.Base(suggestion) + dirSuffix,\n\t\t\tValue:   suggestion,\n\t\t\tType:    \"file\",\n\t\t})\n\t}\n\n\treturn matches\n}\n"
  },
  {
    "path": "internal/complete/complete.zsh",
    "content": "#compdef fx\n\n_fx() {\n    local -a reply\n    reply=(\"${(@f)$(COMP_ZSH=\"${LBUFFER}\" fx)}\")\n    if (( ${#reply} )); then\n        local -a insert_files display_files insert_other display_other\n        local line display rest value typ\n        \n        for line in \"${reply[@]}\"; do\n            display=\"${line%%$'\\t'*}\"\n            rest=\"${line#*$'\\t'}\"\n            value=\"${rest%%$'\\t'*}\"\n            typ=\"${rest#*$'\\t'}\"\n\n            if [[ \"$typ\" == \"file\" ]]; then\n                display_files+=(\"$display\")\n                insert_files+=(\"$value\")\n            else\n                display_other+=(\"$display\")\n                insert_other+=(\"$value\")\n            fi\n        done\n\n        if (( ${#insert_files} )); then\n            compadd -f -d display_files -a insert_files\n        fi\n        if (( ${#insert_other} )); then\n            compadd -S '' -d display_other -a insert_other\n        fi\n    fi\n}\n\nif [ \"$funcstack[1]\" = \"_fx\" ]; then\n    _fx \"$@\"\nelse\n    compdef _fx fx\nfi\n"
  },
  {
    "path": "internal/complete/complete_test.go",
    "content": "package complete\n\nimport (\n\t\"testing\"\n\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc TestKeysComplete(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tjson     string\n\t\targs     []string\n\t\tcompWord string\n\t\twant     []string\n\t}{\n\t\t{\n\t\t\tname:     \"simple object keys with empty compWord\",\n\t\t\tjson:     `{\"foo\": 1, \"bar\": 2, \"baz\": 3}`,\n\t\t\targs:     []string{\"fx\", \"file.json\"},\n\t\t\tcompWord: \"\",\n\t\t\twant:     []string{\".foo\", \".bar\", \".baz\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"nested object keys with trailing dot\",\n\t\t\tjson:     `{\"outer\": {\"inner1\": 1, \"inner2\": 2}}`,\n\t\t\targs:     []string{\"fx\", \"file.json\", \".outer.\"},\n\t\t\tcompWord: \".outer.\",\n\t\t\twant:     []string{\".outer.inner1\", \".outer.inner2\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"nested object without trailing dot returns root keys\",\n\t\t\tjson:     `{\"outer\": {\"inner1\": 1, \"inner2\": 2}}`,\n\t\t\targs:     []string{\"fx\", \"file.json\", \".outer\"},\n\t\t\tcompWord: \".outer\",\n\t\t\twant:     []string{\".outer\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"nested object with partial compWord\",\n\t\t\tjson:     `{\"data\": {\"name\": \"test\", \"value\": 42}}`,\n\t\t\targs:     []string{\"fx\", \"file.json\", \".data.\"},\n\t\t\tcompWord: \".data.\",\n\t\t\twant:     []string{\".data.name\", \".data.value\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty object\",\n\t\t\tjson:     `{}`,\n\t\t\targs:     []string{\"fx\", \"file.json\"},\n\t\t\tcompWord: \"\",\n\t\t\twant:     nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"key with special characters\",\n\t\t\tjson:     `{\"normal\": 1, \"with-dash\": 2, \"with space\": 3}`,\n\t\t\targs:     []string{\"fx\", \"file.json\"},\n\t\t\tcompWord: \"\",\n\t\t\twant:     []string{\".normal\", \".[\\\"with-dash\\\"]\", \".[\\\"with space\\\"]\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"deeply nested with trailing dot\",\n\t\t\tjson:     `{\"a\": {\"b\": {\"c\": {\"d\": 1}}}}`,\n\t\t\targs:     []string{\"fx\", \"file.json\", \".a.b.c.\"},\n\t\t\tcompWord: \".a.b.c.\",\n\t\t\twant:     []string{\".a.b.c.d\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"deeply nested without trailing dot returns parent keys\",\n\t\t\tjson:     `{\"a\": {\"b\": {\"c\": {\"d\": 1}}}}`,\n\t\t\targs:     []string{\"fx\", \"file.json\", \".a.b.c\"},\n\t\t\tcompWord: \".a.b.c\",\n\t\t\twant:     []string{\".a.b.c\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"array returns no keys\",\n\t\t\tjson:     `[1, 2, 3]`,\n\t\t\targs:     []string{\"fx\", \"file.json\"},\n\t\t\tcompWord: \"\",\n\t\t\twant:     nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"primitive value returns no keys\",\n\t\t\tjson:     `\"hello\"`,\n\t\t\targs:     []string{\"fx\", \"file.json\"},\n\t\t\tcompWord: \"\",\n\t\t\twant:     nil,\n\t\t},\n\t\t{\n\t\t\tname:     \"numeric keys use bracket notation\",\n\t\t\tjson:     `{\"123\": \"numeric\", \"abc\": \"alpha\"}`,\n\t\t\targs:     []string{\"fx\", \"file.json\"},\n\t\t\tcompWord: \"\",\n\t\t\twant:     []string{\".[\\\"123\\\"]\", \".abc\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"key starting with underscore\",\n\t\t\tjson:     `{\"_private\": 1, \"public\": 2}`,\n\t\t\targs:     []string{\"fx\", \"file.json\"},\n\t\t\tcompWord: \"\",\n\t\t\twant:     []string{\"._private\", \".public\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"key starting with dollar\",\n\t\t\tjson:     `{\"$ref\": \"#/defs\", \"name\": \"test\"}`,\n\t\t\targs:     []string{\"fx\", \"file.json\"},\n\t\t\tcompWord: \"\",\n\t\t\twant:     []string{\".$ref\", \".name\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"object inside array via bracket access with trailing dot\",\n\t\t\tjson:     `{\"items\": [{\"x\": 1, \"y\": 2}]}`,\n\t\t\targs:     []string{\"fx\", \"file.json\", \".items[0].\"},\n\t\t\tcompWord: \".items[0].\",\n\t\t\twant:     []string{\".items[0].x\", \".items[0].y\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple args combines path\",\n\t\t\tjson:     `{\"a\": {\"b\": {\"c\": 1}}}`,\n\t\t\targs:     []string{\"fx\", \"file.json\", \".a\", \".b.\"},\n\t\t\tcompWord: \".b.\",\n\t\t\twant:     []string{\".b.c\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode, err := jsonx.Parse([]byte(tt.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to parse JSON: %v\", err)\n\t\t\t}\n\n\t\t\tgot := KeysComplete(node, tt.args, tt.compWord)\n\n\t\t\tif len(got) != len(tt.want) {\n\t\t\t\tt.Errorf(\"KeysComplete() returned %d replies, want %d\", len(got), len(tt.want))\n\t\t\t\tt.Errorf(\"got: %v\", replyValues(got))\n\t\t\t\tt.Errorf(\"want: %v\", tt.want)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tgotValues := replyValues(got)\n\t\t\tfor i, want := range tt.want {\n\t\t\t\tif gotValues[i] != want {\n\t\t\t\t\tt.Errorf(\"KeysComplete()[%d].Value = %q, want %q\", i, gotValues[i], want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc replyValues(replies []Reply) []string {\n\tvalues := make([]string, len(replies))\n\tfor i, r := range replies {\n\t\tvalues[i] = r.Value\n\t}\n\treturn values\n}\n\nfunc TestKeysComplete_Display(t *testing.T) {\n\tjson := `{\"foo\": 1, \"bar\": 2}`\n\tnode, err := jsonx.Parse([]byte(json))\n\tif err != nil {\n\t\tt.Fatalf(\"failed to parse JSON: %v\", err)\n\t}\n\n\tgot := KeysComplete(node, []string{\"fx\", \"file.json\"}, \"\")\n\n\tfor _, r := range got {\n\t\tif r.Type != \"key\" {\n\t\t\tt.Errorf(\"expected Type to be 'key', got %q\", r.Type)\n\t\t}\n\t\tif r.Display == \"\" {\n\t\t\tt.Error(\"expected Display to be non-empty\")\n\t\t}\n\t}\n}\n\nfunc TestKeysComplete_DisplayVsValue(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tjson        string\n\t\targs        []string\n\t\tcompWord    string\n\t\twantDisplay string\n\t\twantValue   string\n\t}{\n\t\t{\n\t\t\tname:        \"display shows key with dot prefix, value shows full path\",\n\t\t\tjson:        `{\"outer\": {\"inner\": 1}}`,\n\t\t\targs:        []string{\"fx\", \"file.json\", \".outer.\"},\n\t\t\tcompWord:    \".outer.\",\n\t\t\twantDisplay: \".inner\",\n\t\t\twantValue:   \".outer.inner\",\n\t\t},\n\t\t{\n\t\t\tname:        \"special key display and value both use bracket notation\",\n\t\t\tjson:        `{\"key-dash\": 1}`,\n\t\t\targs:        []string{\"fx\", \"file.json\"},\n\t\t\tcompWord:    \"\",\n\t\t\twantDisplay: \".[\\\"key-dash\\\"]\",\n\t\t\twantValue:   \".[\\\"key-dash\\\"]\",\n\t\t},\n\t\t{\n\t\t\tname:        \"regular key at root\",\n\t\t\tjson:        `{\"foo\": 1}`,\n\t\t\targs:        []string{\"fx\", \"file.json\"},\n\t\t\tcompWord:    \"\",\n\t\t\twantDisplay: \".foo\",\n\t\t\twantValue:   \".foo\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode, err := jsonx.Parse([]byte(tt.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to parse JSON: %v\", err)\n\t\t\t}\n\n\t\t\tgot := KeysComplete(node, tt.args, tt.compWord)\n\t\t\tif len(got) == 0 {\n\t\t\t\tt.Fatal(\"expected at least one reply\")\n\t\t\t}\n\n\t\t\tif got[0].Display != tt.wantDisplay {\n\t\t\t\tt.Errorf(\"Display = %q, want %q\", got[0].Display, tt.wantDisplay)\n\t\t\t}\n\t\t\tif got[0].Value != tt.wantValue {\n\t\t\t\tt.Errorf(\"Value = %q, want %q\", got[0].Value, tt.wantValue)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/complete/prelude.js",
    "content": "const __keys = new Set()\n\nObject.prototype.__keys = function () {\n  if (Array.isArray(this)) return\n  if (typeof this === 'string') return\n  if (this instanceof String) return\n  if (this === globalThis) return\n  if (typeof this === 'object' && this !== null)\n    Object.keys(this).forEach(x => __keys.add(x))\n}\n\nfunction __autocomplete() {\n  const keys = []\n  for (const key of Object.keys(globalThis)) {\n    if (key.startsWith('__')) continue\n    keys.push(key)\n  }\n  keys.push(\n    'JSON.stringify',\n    'JSON.parse',\n    'YAML.stringify',\n    'YAML.parse',\n    'Object.keys',\n    'Object.values',\n    'Object.entries',\n    'Object.fromEntries',\n    'Array.isArray',\n    'Array.from',\n    'console.log',\n  )\n  return keys\n}\n"
  },
  {
    "path": "internal/complete/utils.go",
    "content": "package complete\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nfunc compReply(reply []Reply, withDisplay bool) {\n\tvar lines []string\n\tfor _, line := range reply {\n\t\tif withDisplay {\n\t\t\tlines = append(lines, fmt.Sprintf(\"%s\\t%s\\t%s\", line.Display, line.Value, line.Type))\n\t\t} else {\n\t\t\tlines = append(lines, line.Value)\n\t\t}\n\t}\n\tfmt.Print(strings.Join(lines, \"\\n\"))\n}\n\nfunc filterReply(reply []Reply, compWord string) []Reply {\n\tvar filtered []Reply\n\tfor _, word := range reply {\n\t\tif strings.HasPrefix(word.Value, compWord) {\n\t\t\tfiltered = append(filtered, word)\n\t\t}\n\t}\n\treturn filtered\n}\n\nfunc isFile(path string) bool {\n\tif strings.HasPrefix(path, \"~\") {\n\t\thome, err := os.UserHomeDir()\n\t\tif err == nil {\n\t\t\tpath = filepath.Join(home, path[1:])\n\t\t}\n\t}\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn !info.IsDir()\n}\n\nfunc dropTail(s string) string {\n\tparts := strings.Split(s, \".\")\n\tif len(parts) == 1 {\n\t\treturn s\n\t}\n\treturn strings.Join(parts[:len(parts)-1], \".\")\n}\n\nfunc balanceBrackets(code string) string {\n\tvar stack []rune\n\tbrackets := map[rune]rune{')': '(', '}': '{', ']': '['}\n\treverseBrackets := map[rune]rune{'(': ')', '{': '}', '[': ']'}\n\n\tfor _, char := range code {\n\t\tswitch char {\n\t\tcase '(', '{', '[':\n\t\t\tstack = append(stack, char)\n\t\tcase ')', '}', ']':\n\t\t\tif len(stack) > 0 && brackets[char] == stack[len(stack)-1] {\n\t\t\t\tstack = stack[:len(stack)-1] // Pop\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i := len(stack) - 1; i >= 0; i-- {\n\t\tcode += string(reverseBrackets[stack[i]])\n\t}\n\n\treturn code\n}\n\nfunc lastWord(line string) string {\n\twords := strings.Split(line, \" \")\n\tvar s string\n\tif len(words) > 0 {\n\t\ts = words[len(words)-1]\n\t}\n\treturn s\n}\n\nfunc writeLog(args ...interface{}) {\n\tfile, err := os.OpenFile(\"complete.log\", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)\n\tif err != nil {\n\t\treturn\n\t}\n\t_, _ = fmt.Fprintln(file, args...)\n\t_ = file.Close()\n}\n"
  },
  {
    "path": "internal/engine/engine.go",
    "content": "package engine\n\nimport (\n\t_ \"embed\"\n\t\"io\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/dop251/goja\"\n\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n\t\"github.com/antonmedv/fx/internal/pretty\"\n)\n\n//go:embed stdlib.js\nvar Stdlib string\n\nfunc init() {\n\tfxrc, err := readFxrc()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tStdlib += fxrc\n}\n\ntype Parser interface {\n\tParse() (*jsonx.Node, error)\n\tRecover() *jsonx.Node\n}\n\ntype Options struct {\n\tSlurp      bool\n\tWithInline bool\n\tWriteOut   func(string)\n\tWriteErr   func(string)\n}\n\nfunc Start(parser Parser, args []string, opts Options) int {\n\tif opts.Slurp {\n\t\tvar ok bool\n\t\tparser, ok = Slurp(parser, opts.WriteErr)\n\t\tif !ok {\n\t\t\treturn 1\n\t\t}\n\t}\n\n\tisPrettyPrintArg := len(args) == 1 && (args[0] == \".\" || args[0] == \"this\" || args[0] == \"x\")\n\n\t// Fast path.\n\tif isPrettyPrintArg {\n\t\tfor {\n\t\t\tnode, err := parser.Parse()\n\n\t\t\tif err != nil {\n\t\t\t\tif err == io.EOF {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\topts.WriteErr(err.Error())\n\t\t\t\treturn 1\n\t\t\t}\n\n\t\t\tif node.Kind == jsonx.String {\n\t\t\t\tunquoted, err := strconv.Unquote(node.Value)\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t\topts.WriteOut(unquoted)\n\t\t\t} else {\n\t\t\t\topts.WriteOut(pretty.Print(node, opts.WithInline))\n\t\t\t}\n\t\t}\n\n\t\treturn 0\n\t}\n\n\tfor i := range args {\n\t\tif err := validateSyntax(args, i); err != nil {\n\t\t\tjsCode := transpile(args[i])\n\t\t\tsnippet := formatErr(args, i, jsCode)\n\t\t\tmessage := errorToString(err)\n\t\t\topts.WriteErr(snippet + message)\n\t\t\treturn 1\n\t\t}\n\t}\n\n\tvar code strings.Builder\n\tcode.WriteString(Stdlib)\n\tcode.WriteString(JS(args))\n\n\tvm := NewVM(opts.WriteOut)\n\tif _, err := vm.RunString(code.String()); err != nil {\n\t\topts.WriteErr(errorToString(err))\n\t\treturn 1\n\t}\n\n\tskip := vm.Get(\"skip\")\n\tundefined := vm.Get(\"undefined\")\n\tmain, _ := goja.AssertFunction(vm.Get(\"__main__\"))\n\n\techo := func(output goja.Value) {\n\t\trtype := output.ExportType()\n\t\tif output.StrictEquals(undefined) {\n\t\t\topts.WriteErr(\"undefined\")\n\t\t} else if rtype != nil && rtype.Kind() == reflect.String {\n\t\t\topts.WriteOut(output.String())\n\t\t} else {\n\t\t\tjsonOut := Stringify(output, vm, 0)\n\t\t\tnodeOut, err := jsonx.Parse([]byte(jsonOut))\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\topts.WriteOut(pretty.Print(nodeOut, opts.WithInline))\n\t\t}\n\t}\n\n\tfor {\n\t\tnode, err := parser.Parse()\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\topts.WriteErr(err.Error())\n\t\t\treturn 1\n\t\t}\n\n\t\tinput := node.ToValue(vm)\n\t\toutput, exitCode, err := callMain(main, input)\n\t\tif exitCode >= 0 {\n\t\t\treturn exitCode\n\t\t}\n\t\tif err != nil {\n\t\t\topts.WriteErr(errorToString(err))\n\t\t\treturn 1\n\t\t}\n\n\t\tif output.StrictEquals(skip) {\n\t\t\tcontinue\n\t\t}\n\t\techo(output)\n\t}\n\n\treturn 0\n}\n\nfunc callMain(main goja.Callable, input goja.Value) (output goja.Value, exitCode int, err error) {\n\texitCode = -1\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tif e, ok := r.(ExitError); ok {\n\t\t\t\texitCode = e.Code\n\t\t\t} else {\n\t\t\t\tpanic(r)\n\t\t\t}\n\t\t}\n\t}()\n\toutput, err = main(goja.Undefined(), input)\n\treturn\n}\n\nfunc validateSyntax(args []string, i int) error {\n\tvar code strings.Builder\n\tcode.WriteString(\"\\nfunction __main__(json) {\\n\")\n\tcode.WriteString(Body(args, i))\n\tcode.WriteString(\"  return json\\n}\\n\")\n\n\tvm := goja.New()\n\t_, err := vm.RunString(code.String())\n\treturn err\n}\n"
  },
  {
    "path": "internal/engine/engine_test.go",
    "content": "package engine_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/antonmedv/fx/internal/engine\"\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc TestEngine(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\targs     []string\n\t\texpects  []string\n\t\terrCount int\n\t}{\n\t\t{\n\t\t\tname:     \"fast path: string as raw\",\n\t\t\tinput:    `\"Hello, world!\"`,\n\t\t\targs:     []string{\".\"},\n\t\t\texpects:  []string{\"Hello, world!\"},\n\t\t\terrCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"string as raw\",\n\t\t\tinput:    `\"Hello, world!\"`,\n\t\t\targs:     []string{\"x => this\"},\n\t\t\texpects:  []string{\"Hello, world!\"},\n\t\t\terrCount: 0,\n\t\t},\n\t\t{\n\t\t\tname:     \"skip works\",\n\t\t\tinput:    \"1 2 3 4\",\n\t\t\targs:     []string{\"x % 2 != 0 ? skip : x\"},\n\t\t\texpects:  []string{\"2\", \"4\"},\n\t\t\terrCount: 0,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tparser := jsonx.NewJsonParser(strings.NewReader(tc.input), false)\n\n\t\t\tvar outs, errs []string\n\t\t\twriteOut := func(s string) { outs = append(outs, s) }\n\t\t\twriteErr := func(s string) { errs = append(errs, s) }\n\n\t\t\topts := engine.Options{\n\t\t\t\tSlurp:      false,\n\t\t\t\tWithInline: false,\n\t\t\t\tWriteOut:   writeOut,\n\t\t\t\tWriteErr:   writeErr,\n\t\t\t}\n\t\t\texitCode := engine.Start(parser, tc.args, opts)\n\n\t\t\tassert.Equal(t, 0, exitCode)\n\t\t\tassert.Len(t, errs, tc.errCount, \"%s: unexpected error count\", tc.name)\n\t\t\tassert.Equal(t, tc.expects, outs, \"%s: outputs mismatch\", tc.name)\n\t\t})\n\t}\n}\n\nfunc TestStart_InvalidJSON(t *testing.T) {\n\tinput := `{\"unclosed\": 1`\n\tparser := jsonx.NewJsonParser(strings.NewReader(input), false)\n\n\tvar outs, errs []string\n\twriteOut := func(s string) { outs = append(outs, s) }\n\twriteErr := func(s string) { errs = append(errs, s) }\n\n\topts := engine.Options{\n\t\tSlurp:      false,\n\t\tWithInline: false,\n\t\tWriteOut:   writeOut,\n\t\tWriteErr:   writeErr,\n\t}\n\texitCode := engine.Start(parser, []string{\".unclosed + '!'\"}, opts)\n\n\tassert.Equal(t, 1, exitCode)\n\tassert.Len(t, errs, 1, \"Expected one error message\")\n}\n\nfunc TestStart_FastPath_InvalidJSON(t *testing.T) {\n\tinput := `{\"unclosed\": 1`\n\tparser := jsonx.NewJsonParser(strings.NewReader(input), false)\n\n\tvar outs, errs []string\n\twriteOut := func(s string) { outs = append(outs, s) }\n\twriteErr := func(s string) { errs = append(errs, s) }\n\n\topts := engine.Options{\n\t\tSlurp:      false,\n\t\tWithInline: false,\n\t\tWriteOut:   writeOut,\n\t\tWriteErr:   writeErr,\n\t}\n\texitCode := engine.Start(parser, []string{\".\"}, opts)\n\n\tassert.Equal(t, 1, exitCode)\n\tassert.Len(t, errs, 1, \"Expected one error message\")\n}\n\nfunc TestStart_EscapeSequences(t *testing.T) {\n\tinput := `{\"emoji\": \"\\ud83d\\ude80\"}`\n\tparser := jsonx.NewJsonParser(strings.NewReader(input), false)\n\n\tvar outs, errs []string\n\twriteOut := func(s string) { outs = append(outs, s) }\n\twriteErr := func(s string) { errs = append(errs, s) }\n\n\topts := engine.Options{\n\t\tSlurp:      false,\n\t\tWithInline: false,\n\t\tWriteOut:   writeOut,\n\t\tWriteErr:   writeErr,\n\t}\n\texitCode := engine.Start(parser, []string{\".emoji\"}, opts)\n\n\tassert.Equal(t, 0, exitCode)\n\tassert.Len(t, errs, 0, \"Expected no error messages\")\n\tassert.Equal(t, \"🚀\", outs[0])\n}\n\nfunc TestStart_EscapeSequences_in_key(t *testing.T) {\n\tinput := `{\"\\ud83d\\ude80\": \"\\ud83d\\ude80\"}`\n\tparser := jsonx.NewJsonParser(strings.NewReader(input), false)\n\n\tvar outs, errs []string\n\twriteOut := func(s string) { outs = append(outs, s) }\n\twriteErr := func(s string) { errs = append(errs, s) }\n\n\topts := engine.Options{\n\t\tSlurp:      false,\n\t\tWithInline: false,\n\t\tWriteOut:   writeOut,\n\t\tWriteErr:   writeErr,\n\t}\n\texitCode := engine.Start(parser, []string{\"x => x\"}, opts)\n\n\tassert.Equal(t, 0, exitCode)\n\tassert.Len(t, errs, 0, \"Expected no error messages\")\n}\n"
  },
  {
    "path": "internal/engine/format_err.go",
    "content": "package engine\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/x/term\"\n\t\"github.com/mattn/go-runewidth\"\n)\n\nfunc formatErr(args []string, i int, jsCode string) string {\n\twidth, _, err := term.GetSize(os.Stdout.Fd())\n\tif err != nil || width <= 0 {\n\t\twidth = 80\n\t}\n\n\tif i < 0 || i >= len(args) {\n\t\treturn fmt.Sprintf(\"Invalid argument index: %d\", i)\n\t}\n\tcode := args[i]\n\n\tconst indentCols = 2 // we print \"  \" before everything\n\tconst sepCols = 1    // the single space between pre and code\n\treserve := indentCols + sepCols + runewidth.StringWidth(code) + sepCols\n\n\tavailable := width - reserve\n\tif available < 0 {\n\t\tavailable = 0\n\t}\n\tmaxCtx := available / 2\n\n\tpre := strings.Join(args[:i], \" \")\n\tpost := strings.Join(args[i+1:], \" \")\n\n\tpre = trimLeft(pre, maxCtx)\n\tpost = trimRight(post, maxCtx)\n\n\tleftSep := 0\n\tif pre != \"\" {\n\t\tleftSep = sepCols\n\t}\n\tspacerCols := indentCols + runewidth.StringWidth(pre) + leftSep\n\tspacer := strings.Repeat(\" \", spacerCols)\n\n\tvar sb strings.Builder\n\tsb.WriteString(\"\\n\")\n\tsb.WriteString(strings.Repeat(\" \", indentCols))\n\tif pre != \"\" {\n\t\tsb.WriteString(pre)\n\t\tsb.WriteByte(' ')\n\t}\n\tsb.WriteString(code)\n\tif post != \"\" {\n\t\tsb.WriteByte(' ')\n\t\tsb.WriteString(post)\n\t}\n\tsb.WriteByte('\\n')\n\n\tsb.WriteString(spacer)\n\tsb.WriteString(strings.Repeat(\"^\", runewidth.StringWidth(code)))\n\tsb.WriteByte('\\n')\n\n\tif jsCode != \"\" && jsCode != code {\n\t\tsnippet := jsCode\n\t\tif runewidth.StringWidth(snippet) > width {\n\t\t\tsnippet = trimRight(snippet, width)\n\t\t}\n\t\tsb.WriteByte('\\n')\n\t\tsb.WriteString(snippet)\n\t\tsb.WriteByte('\\n')\n\t}\n\n\tsb.WriteString(\"\\n\")\n\n\treturn sb.String()\n}\n\nfunc trimLeft(s string, ctx int) string {\n\tif runewidth.StringWidth(s) <= ctx {\n\t\treturn s\n\t}\n\trs := []rune(s)\n\twidthAccum := 0\n\tvar out []rune\n\tfor i := len(rs) - 1; i >= 0; i-- {\n\t\tw := runewidth.RuneWidth(rs[i])\n\t\tif widthAccum+w > ctx-1 {\n\t\t\tbreak\n\t\t}\n\t\twidthAccum += w\n\t\tout = append([]rune{rs[i]}, out...)\n\t}\n\treturn \"…\" + string(out)\n}\n\nfunc trimRight(s string, ctx int) string {\n\tif runewidth.StringWidth(s) <= ctx {\n\t\treturn s\n\t}\n\trs := []rune(s)\n\twidthAccum := 0\n\tvar out []rune\n\tfor _, r := range rs {\n\t\tw := runewidth.RuneWidth(r)\n\t\tif widthAccum+w > ctx-1 {\n\t\t\tbreak\n\t\t}\n\t\tout = append(out, r)\n\t\twidthAccum += w\n\t}\n\treturn string(out) + \"…\"\n}\n"
  },
  {
    "path": "internal/engine/fxrc.go",
    "content": "package engine\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nfunc readFxrc() (string, error) {\n\tvar builder strings.Builder\n\n\t// Determine search paths\n\tcwd, err := os.Getwd()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"get cwd: %w\", err)\n\t}\n\tpaths := []string{filepath.Join(cwd, \".fxrc.js\")}\n\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"get home: %w\", err)\n\t}\n\tpaths = append(paths, filepath.Join(home, \".fxrc.js\"))\n\n\txdgHome := os.Getenv(\"XDG_CONFIG_HOME\")\n\tif xdgHome == \"\" {\n\t\txdgHome = filepath.Join(home, \".config\")\n\t}\n\tpaths = append(paths, filepath.Join(xdgHome, \"fx\", \".fxrc.js\"))\n\n\txdgDirs := os.Getenv(\"XDG_CONFIG_DIRS\")\n\tif xdgDirs == \"\" {\n\t\txdgDirs = \"/etc/xdg\"\n\t}\n\tfor _, dir := range strings.Split(xdgDirs, \":\") {\n\t\tpaths = append(paths, filepath.Join(dir, \"fx\", \".fxrc.js\"))\n\t}\n\n\t// Read and combine\n\tfor _, path := range uniq(paths) {\n\t\tinfo, err := os.Stat(path)\n\t\tif err != nil || info.IsDir() {\n\t\t\tcontinue // skip missing or directories\n\t\t}\n\t\tdata, err := os.ReadFile(path)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"read %s: %w\", path, err)\n\t\t}\n\t\tbuilder.Write(data)\n\t\tbuilder.WriteString(\"\\n\")\n\t}\n\n\treturn builder.String(), nil\n}\n\nfunc uniq(paths []string) []string {\n\tseen := make(map[string]bool)\n\tresult := []string{}\n\tfor _, path := range paths {\n\t\tif !seen[path] {\n\t\t\tseen[path] = true\n\t\t\tresult = append(result, path)\n\t\t}\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "internal/engine/quote.go",
    "content": "package engine\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\nfunc Quote(s string) string {\n\tvar err error\n\tvar b strings.Builder\n\tb.WriteByte('\"')\n\n\tfor i := 0; i < len(s); {\n\t\tr, width := utf8.DecodeRuneInString(s[i:])\n\n\t\tswitch r {\n\t\tcase '\"':\n\t\t\tb.WriteString(`\\\"`)\n\t\tcase '\\\\':\n\t\t\tb.WriteString(`\\\\`)\n\t\tcase '\\b':\n\t\t\tb.WriteString(`\\b`)\n\t\tcase '\\f':\n\t\t\tb.WriteString(`\\f`)\n\t\tcase '\\n':\n\t\t\tb.WriteString(`\\n`)\n\t\tcase '\\r':\n\t\t\tb.WriteString(`\\r`)\n\t\tcase '\\t':\n\t\t\tb.WriteString(`\\t`)\n\t\tdefault:\n\t\t\tif r < 0x20 || r == 0x7F {\n\t\t\t\t// Control characters must be escaped as \\uXXXX\n\t\t\t\t_, err = fmt.Fprintf(&b, `\\u%04x`, r)\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t} else if r == utf8.RuneError && width == 1 {\n\t\t\t\t// Invalid UTF-8 sequence - escape the byte\n\t\t\t\t_, err = fmt.Fprintf(&b, `\\u%04x`, s[i])\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Regular character - write as-is\n\t\t\t\tb.WriteRune(r)\n\t\t\t}\n\t\t}\n\n\t\ti += width\n\t}\n\n\tb.WriteByte('\"')\n\treturn b.String()\n}\n"
  },
  {
    "path": "internal/engine/quote_test.go",
    "content": "package engine_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/antonmedv/fx/internal/engine\"\n)\n\nfunc TestQuote_BasicASCII(t *testing.T) {\n\tassert.Equal(t, \"\\\"hello\\\"\", engine.Quote(\"hello\"))\n\tassert.Equal(t, \"\\\"\\\"\", engine.Quote(\"\"))\n\tassert.Equal(t, \"\\\"Hello, world!\\\"\", engine.Quote(\"Hello, world!\"))\n}\n\nfunc TestQuote_EscapesSpecialCharacters(t *testing.T) {\n\tassert.Equal(t, `\"\\\"\"`, engine.Quote(\"\\\"\"))\n\tassert.Equal(t, `\"\\\\\"`, engine.Quote(\"\\\\\"))\n\tassert.Equal(t, `\"\\b\"`, engine.Quote(\"\\b\"))\n\tassert.Equal(t, `\"\\f\"`, engine.Quote(\"\\f\"))\n\tassert.Equal(t, `\"\\n\"`, engine.Quote(\"\\n\"))\n\tassert.Equal(t, `\"\\r\"`, engine.Quote(\"\\r\"))\n\tassert.Equal(t, `\"\\t\"`, engine.Quote(\"\\t\"))\n}\n\nfunc TestQuote_ControlCharactersAndDEL(t *testing.T) {\n\thex4Lower := func(n int) string {\n\t\tconst hexdigits = \"0123456789abcdef\"\n\t\tb0 := hexdigits[(n>>12)&0xF]\n\t\tb1 := hexdigits[(n>>8)&0xF]\n\t\tb2 := hexdigits[(n>>4)&0xF]\n\t\tb3 := hexdigits[n&0xF]\n\t\treturn string([]byte{b0, b1, b2, b3})\n\t}\n\n\t// 0x00 .. 0x1F should be \\uXXXX\n\tfor b := 0; b < 0x20; b++ {\n\t\ts := string([]byte{byte(b)})\n\t\tq := engine.Quote(s)\n\t\texpected := \"\\\"\\\\u\" + hex4Lower(b) + \"\\\"\"\n\t\t// For those with dedicated escapes, engine.Quote uses short escapes; both are valid.\n\t\t// We'll accept either short escape or \\uXXXX for those particular bytes.\n\t\tswitch b {\n\t\tcase '\\b':\n\t\t\tassert.Equal(t, `\"\\b\"`, q)\n\t\tcase '\\f':\n\t\t\tassert.Equal(t, `\"\\f\"`, q)\n\t\tcase '\\n':\n\t\t\tassert.Equal(t, `\"\\n\"`, q)\n\t\tcase '\\r':\n\t\t\tassert.Equal(t, `\"\\r\"`, q)\n\t\tcase '\\t':\n\t\t\tassert.Equal(t, `\"\\t\"`, q)\n\t\tdefault:\n\t\t\tassert.Equal(t, expected, q, \"byte %d\", b)\n\t\t}\n\t}\n\t// 0x7F DEL\n\tassert.Equal(t, `\"\\u007f\"`, engine.Quote(string([]byte{0x7F})))\n}\n\nfunc TestQuote_BMP_Characters_AsIs(t *testing.T) {\n\t// Latin-1 supplement, Cyrillic, CJK BMP characters should appear as-is\n\tassert.Equal(t, \"\\\"café\\\"\", engine.Quote(\"café\"))\n\tassert.Equal(t, \"\\\"Привет\\\"\", engine.Quote(\"Привет\"))\n\tassert.Equal(t, \"\\\"漢字\\\"\", engine.Quote(\"漢字\"))\n}\n\nfunc TestQuote_SurrogatePairs_AsIs(t *testing.T) {\n\tassert.Equal(t, `\"🚀\"`, engine.Quote(\"🚀\"))\n\tassert.Equal(t, `\"👍🏻\"`, engine.Quote(\"👍🏻\"))\n\tassert.Equal(t, `\"𝄞\"`, engine.Quote(\"𝄞\"))\n}\n\nfunc TestQuote_InvalidUTF8BytesAreEscaped(t *testing.T) {\n\t// Construct a string with invalid UTF-8 byte 0xFF and 0xC0 (overlong lead)\n\ts := string([]byte{'A', 0xFF, 'B', 0xC0, 'C'})\n\tgot := engine.Quote(s)\n\t// Expect bytes to be escaped as \\u00xx in lowercase hex\n\twant := `\"A\\u00ffB\\u00c0C\"`\n\tassert.Equal(t, want, got)\n}\n\nfunc TestQuote_JSONRoundTrip_ValidUTF8(t *testing.T) {\n\ttests := []struct{ input string }{\n\t\t{\"\"},\n\t\t{\"simple\"},\n\t\t{\"line\\nfeed\"},\n\t\t{\"tab\\tchar\"},\n\t\t{\"quote \\\" here\"},\n\t\t{\"backslash \\\\\"},\n\t\t{\"café\"},\n\t\t{\"Привет\"},\n\t\t{\"漢字\"},\n\t\t{\"emoji 🚀\"},\n\t\t{\"mix: \\b\\f\\n\\r\\t and \\u007F:\" + string([]byte{0x7F})},\n\t\t{\"Line1\\n\\t\\\"Quote\\\" and backslash \\\\ and DEL:\" + string([]byte{0x7F}) + \" and emoji 🚀\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tq := engine.Quote(tt.input)\n\t\t\tvar v string\n\t\t\terr := json.Unmarshal([]byte(q), &v)\n\t\t\tassert.NoError(t, err, \"failed to unmarshal: %q\", q)\n\t\t\tassert.Equal(t, tt.input, v)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/engine/slurp.go",
    "content": "package engine\n\nimport (\n\t\"io\"\n\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc Slurp(parser Parser, writeErr func(string)) (Parser, bool) {\n\tarr := &jsonx.Node{\n\t\tKind:       jsonx.Array,\n\t\tValue:      \"[\",\n\t\tLineNumber: 1,\n\t}\n\n\tend := arr\n\tfor {\n\t\tnode, err := parser.Parse()\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\twriteErr(err.Error())\n\t\t\treturn nil, false\n\t\t}\n\n\t\tnode.Parent = arr\n\n\t\tit := node\n\t\tfor it != nil {\n\t\t\tit.Depth++\n\t\t\tit.LineNumber++\n\t\t\tit = it.Next\n\t\t}\n\n\t\tend.Next = node\n\t\tend = node.Bottom()\n\t\tend.Comma = true\n\t}\n\n\tend.Comma = false\n\tend.Next = &jsonx.Node{\n\t\tKind:       jsonx.Array,\n\t\tLineNumber: end.LineNumber + 1,\n\t\tValue:      \"]\",\n\t}\n\tarr.End = end.Next\n\n\treturn &slurpParser{node: arr}, true\n}\n\ntype slurpParser struct {\n\tnode *jsonx.Node\n}\n\nfunc (p *slurpParser) Parse() (*jsonx.Node, error) {\n\tif p.node == nil {\n\t\treturn nil, io.EOF\n\t}\n\tnode := p.node\n\tp.node = nil\n\treturn node, nil\n}\n\nfunc (p *slurpParser) Recover() *jsonx.Node {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/engine/stdlib.js",
    "content": "'use strict'\n\nconst console = {\n  log: function (...args) {\n    const parts = []\n    for (const arg of args) {\n      if (typeof arg === 'undefined') {\n        parts.push('undefined')\n      } else if (typeof arg === 'string') {\n        parts.push(arg)\n      } else {\n        parts.push(JSON.stringify(arg, null, 2))\n      }\n    }\n    println(parts.join(' '))\n  },\n}\n\nconst skip = Symbol('skip')\n\nfunction apply(fn, ...args) {\n  if (typeof fn === 'function') return fn(...args)\n  return fn\n}\n\nfunction len(x) {\n  if (Array.isArray(x)) return x.length\n  if (typeof x === 'string') return x.length\n  if (typeof x === 'object' && x !== null) return Object.keys(x).length\n  throw new Error(`Cannot get length of ${typeof x}`)\n}\n\nfunction uniq(x) {\n  if (Array.isArray(x)) return [...new Set(x)]\n  throw new Error(`Cannot get unique values of ${typeof x}`)\n}\n\nfunction sort(x) {\n  if (Array.isArray(x)) return x.sort()\n  throw new Error(`Cannot sort ${typeof x}`)\n}\n\nfunction isFalsely(x) {\n  return x === false || x === null || x === undefined\n}\n\nfunction filter(fn) {\n  return function (x) {\n    if (Array.isArray(x)) {\n      return x.filter((v, i) => !isFalsely(fn(v, i)))\n    }\n    return isFalsely(fn(x)) ? skip : x\n  }\n}\n\nfunction map(fn) {\n  return function (x) {\n    if (Array.isArray(x)) {\n      return x.map((v, i) => fn(v, i))\n    }\n    return fn(x)\n  }\n}\n\nfunction walk(fn) {\n  return function recurse(value, key = null) {\n    if (Array.isArray(value)) {\n      const mapped = value.map((v, i) => recurse(v, i))\n      return fn(mapped, key)\n    } else if (value !== null && typeof value === 'object') {\n      const result = {}\n      for (const [k, v] of Object.entries(value)) {\n        result[k] = recurse(v, k)\n      }\n      return fn(result, key)\n    } else {\n      return fn(value, key)\n    }\n  }\n}\n\nfunction sortBy(fn) {\n  return function (x) {\n    if (Array.isArray(x)) return x.sort((a, b) => {\n      const fa = fn(a)\n      const fb = fn(b)\n      return fa < fb ? -1 : fa > fb ? 1 : 0\n    })\n    throw new Error(`Cannot sort ${typeof x}`)\n  }\n}\n\nfunction sortKeys(x) {\n  if (Array.isArray(x)) {\n    return x.map(sortKeys)\n  }\n  if (typeof x === 'object' && x !== null) {\n    const sorted = {}\n    for (const key of Object.keys(x).sort()) {\n      sorted[key] = sortKeys(x[key])\n    }\n    return sorted\n  }\n  return x\n}\n\nfunction groupBy(keyFn) {\n  return function (x) {\n    const grouped = {}\n    for (const item of x) {\n      const key = typeof keyFn === 'function' ? keyFn(item) : item[keyFn]\n      if (!Object.prototype.hasOwnProperty.call(grouped, key)) grouped[key] = []\n      grouped[key].push(item)\n    }\n    return grouped\n  }\n}\n\nfunction chunk(size) {\n  return function (x) {\n    const res = []\n    let i = 0\n    while (i < x.length) {\n      res.push(x.slice(i, i += size))\n    }\n    return res\n  }\n}\n\nfunction zip(...x) {\n  const length = Math.min(...x.map(a => a.length))\n  const res = []\n  for (let i = 0; i < length; i++) {\n    res.push(x.map(a => a[i]))\n  }\n  return res\n}\n\nfunction flatten(x) {\n  if (Array.isArray(x)) return x.flat()\n  throw new Error(`Cannot flatten ${typeof x}`)\n}\n\nfunction reverse(x) {\n  if (Array.isArray(x)) return x.reverse()\n  throw new Error(`Cannot reverse ${typeof x}`)\n}\n\nfunction keys(x) {\n  if (typeof x === 'object' && x !== null) return Object.keys(x)\n  throw new Error(`Cannot get keys of ${typeof x}`)\n}\n\nfunction values(x) {\n  if (typeof x === 'object' && x !== null) return Object.values(x)\n  throw new Error(`Cannot get values of ${typeof x}`)\n}\n\nfunction list(x) {\n  if (Array.isArray(x)) {\n    for (const y of x) console.log(y)\n    return skip\n  }\n  throw new Error(`Cannot list ${typeof x}`)\n}\n\nfunction del(key) {\n  return function (x) {\n    if (Array.isArray(x)) {\n      const copy = [...x]\n      copy.splice(key, 1)\n      return copy\n    }\n    if (typeof x === 'object' && x !== null) {\n      const copy = {...x}\n      delete copy[key]\n      return copy\n    }\n    throw new Error(`Cannot delete key from ${typeof x}`)\n  }\n}\n\nfunction exit(code) {\n  __exit__(code)\n}\n\nfunction save(x) {\n  if (typeof x === 'undefined') throw new Error('Cannot save undefined')\n  __save__(__stringify__(x, null, 2))\n  return x\n}\n\nfunction toBase64(x) {\n  return __toBase64__(x)\n}\n\nfunction fromBase64(x) {\n  return __fromBase64__(x)\n}\n\nconst YAML = {\n  stringify: x => __yaml_stringify__(x),\n  parse: x => JSON.parse(__yaml_parse__(x)),\n}\n"
  },
  {
    "path": "internal/engine/stdlib_test.go",
    "content": "package engine_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/dop251/goja\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/antonmedv/fx/internal/engine\"\n)\n\n// setupVM creates a new goja runtime with stdlib loaded.\nfunc setupVM(t *testing.T) *goja.Runtime {\n\tvar output []string\n\tvm := engine.NewVM(func(s string) {\n\t\toutput = append(output, s)\n\t})\n\t_, err := vm.RunString(engine.Stdlib)\n\trequire.NoError(t, err, \"Failed to load stdlib\")\n\treturn vm\n}\n\n// setupVMWithOutput creates a new goja runtime with stdlib loaded and returns output slice.\nfunc setupVMWithOutput(t *testing.T) (*goja.Runtime, *[]string) {\n\toutput := &[]string{}\n\tvm := engine.NewVM(func(s string) {\n\t\t*output = append(*output, s)\n\t})\n\t_, err := vm.RunString(engine.Stdlib)\n\trequire.NoError(t, err, \"Failed to load stdlib\")\n\treturn vm, output\n}\n\n// TestApply tests the apply function.\nfunc TestApply(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t}{\n\t\t{\"apply function\", \"apply(x => x * 2, 5)\", int64(10)},\n\t\t{\"apply non-function\", \"apply(42)\", int64(42)},\n\t\t{\"apply with multiple args\", \"apply((a, b) => a + b, 2, 3)\", int64(5)},\n\t\t{\"apply string\", \"apply('hello')\", \"hello\"},\n\t\t{\"apply null\", \"apply(null)\", nil},\n\t\t{\"apply undefined\", \"apply(undefined)\", goja.Undefined()},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\tif tc.expected == goja.Undefined() {\n\t\t\t\tassert.True(t, goja.IsUndefined(result))\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestLen tests the len function.\nfunc TestLen(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t\thasError bool\n\t}{\n\t\t{\"array length\", \"len([1, 2, 3])\", int64(3), false},\n\t\t{\"empty array\", \"len([])\", int64(0), false},\n\t\t{\"string length\", \"len('hello')\", int64(5), false},\n\t\t{\"empty string\", \"len('')\", int64(0), false},\n\t\t{\"object keys count\", \"len({a: 1, b: 2})\", int64(2), false},\n\t\t{\"empty object\", \"len({})\", int64(0), false},\n\t\t{\"number error\", \"len(42)\", nil, true},\n\t\t{\"null error\", \"len(null)\", nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestUniq tests the uniq function.\nfunc TestUniq(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t\thasError bool\n\t}{\n\t\t{\"unique numbers\", \"uniq([1, 2, 2, 3, 3, 3])\", []interface{}{int64(1), int64(2), int64(3)}, false},\n\t\t{\"unique strings\", \"uniq(['a', 'b', 'a', 'c'])\", []interface{}{\"a\", \"b\", \"c\"}, false},\n\t\t{\"already unique\", \"uniq([1, 2, 3])\", []interface{}{int64(1), int64(2), int64(3)}, false},\n\t\t{\"empty array\", \"uniq([])\", []interface{}{}, false},\n\t\t{\"non-array error\", \"uniq('hello')\", nil, true},\n\t\t{\"number error\", \"uniq(42)\", nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestSort tests the sort function.\nfunc TestSort(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t\thasError bool\n\t}{\n\t\t{\"sort numbers\", \"sort([3, 1, 2])\", []interface{}{int64(1), int64(2), int64(3)}, false},\n\t\t{\"sort strings\", \"sort(['c', 'a', 'b'])\", []interface{}{\"a\", \"b\", \"c\"}, false},\n\t\t{\"empty array\", \"sort([])\", []interface{}{}, false},\n\t\t{\"already sorted\", \"sort([1, 2, 3])\", []interface{}{int64(1), int64(2), int64(3)}, false},\n\t\t{\"non-array error\", \"sort('hello')\", nil, true},\n\t\t{\"number error\", \"sort(42)\", nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestFilter tests the filter function.\nfunc TestFilter(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t}{\n\t\t{\"filter even\", \"filter(x => x % 2 === 0)([1, 2, 3, 4])\", []interface{}{int64(2), int64(4)}},\n\t\t{\"filter with index\", \"filter((x, i) => i > 0)(['a', 'b', 'c'])\", []interface{}{\"b\", \"c\"}},\n\t\t{\"filter all true\", \"filter(x => true)([1, 2, 3])\", []interface{}{int64(1), int64(2), int64(3)}},\n\t\t{\"filter all false\", \"filter(x => false)([1, 2, 3])\", []interface{}{}},\n\t\t{\"filter empty\", \"filter(x => true)([])\", []interface{}{}},\n\t\t{\"filter non-array truthy\", \"filter(x => x > 0)(5)\", int64(5)},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t})\n\t}\n\n\t// Test skip symbol for non-array falsy\n\tt.Run(\"filter non-array falsy returns skip\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"filter(x => x > 10)(5) === skip\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, true, result.Export())\n\t})\n\n\t// Test null/undefined/false filtering\n\tt.Run(\"filter removes null\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"filter(x => null)([1, 2, 3])\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []interface{}{}, result.Export())\n\t})\n\n\tt.Run(\"filter removes undefined\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"filter(x => undefined)([1, 2, 3])\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []interface{}{}, result.Export())\n\t})\n}\n\n// TestMap tests the map function.\nfunc TestMap(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t}{\n\t\t{\"map double\", \"map(x => x * 2)([1, 2, 3])\", []interface{}{int64(2), int64(4), int64(6)}},\n\t\t{\"map with index\", \"map((x, i) => i)(['a', 'b', 'c'])\", []interface{}{int64(0), int64(1), int64(2)}},\n\t\t{\"map empty\", \"map(x => x * 2)([])\", []interface{}{}},\n\t\t{\"map non-array\", \"map(x => x * 2)(5)\", int64(10)},\n\t\t{\"map to string\", \"map(x => String(x))([1, 2, 3])\", []interface{}{\"1\", \"2\", \"3\"}},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t})\n\t}\n}\n\n// TestWalk tests the walk function.\nfunc TestWalk(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t}{\n\t\t{\"walk multiply primitives\", \"walk(x => typeof x === 'number' ? x * 2 : x)({a: 1, b: 2})\",\n\t\t\tmap[string]interface{}{\"a\": int64(2), \"b\": int64(4)}},\n\t\t{\"walk nested array\", \"walk(x => typeof x === 'number' ? x + 1 : x)([[1, 2], [3, 4]])\",\n\t\t\t[]interface{}{[]interface{}{int64(2), int64(3)}, []interface{}{int64(4), int64(5)}}},\n\t\t{\"walk primitive\", \"walk(x => x * 2)(5)\", int64(10)},\n\t\t{\"walk with key\", \"walk((v, k) => k === 'double' ? v * 2 : v)({double: 5, keep: 10})\",\n\t\t\tmap[string]interface{}{\"double\": int64(10), \"keep\": int64(10)}},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t})\n\t}\n}\n\n// TestSortBy tests the sortBy function.\nfunc TestSortBy(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t\thasError bool\n\t}{\n\t\t{\"sortBy property\", \"sortBy(x => x.age)([{name: 'b', age: 30}, {name: 'a', age: 20}])\",\n\t\t\t[]interface{}{\n\t\t\t\tmap[string]interface{}{\"name\": \"a\", \"age\": int64(20)},\n\t\t\t\tmap[string]interface{}{\"name\": \"b\", \"age\": int64(30)},\n\t\t\t}, false},\n\t\t{\"sortBy computed\", \"sortBy(x => -x)([1, 3, 2])\",\n\t\t\t[]interface{}{int64(3), int64(2), int64(1)}, false},\n\t\t{\"sortBy string length\", \"sortBy(x => x.length)(['aaa', 'a', 'aa'])\",\n\t\t\t[]interface{}{\"a\", \"aa\", \"aaa\"}, false},\n\t\t{\"sortBy non-array error\", \"sortBy(x => x)('hello')\", nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestSortKeys tests the sortKeys function.\nfunc TestSortKeys(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname  string\n\t\tcode  string\n\t\tcheck func(t *testing.T, result goja.Value)\n\t}{\n\t\t{\n\t\t\tname: \"sort object keys\",\n\t\t\tcode: \"JSON.stringify(sortKeys({c: 1, a: 2, b: 3}))\",\n\t\t\tcheck: func(t *testing.T, result goja.Value) {\n\t\t\t\tassert.Equal(t, `{\"a\":2,\"b\":3,\"c\":1}`, result.Export())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"sort nested object keys\",\n\t\t\tcode: \"JSON.stringify(sortKeys({z: {c: 1, a: 2}, y: 3}))\",\n\t\t\tcheck: func(t *testing.T, result goja.Value) {\n\t\t\t\tassert.Equal(t, `{\"y\":3,\"z\":{\"a\":2,\"c\":1}}`, result.Export())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"sort array of objects\",\n\t\t\tcode: \"JSON.stringify(sortKeys([{b: 1, a: 2}, {d: 3, c: 4}]))\",\n\t\t\tcheck: func(t *testing.T, result goja.Value) {\n\t\t\t\tassert.Equal(t, `[{\"a\":2,\"b\":1},{\"c\":4,\"d\":3}]`, result.Export())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"primitives unchanged\",\n\t\t\tcode: \"sortKeys(42)\",\n\t\t\tcheck: func(t *testing.T, result goja.Value) {\n\t\t\t\tassert.Equal(t, int64(42), result.Export())\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"null unchanged\",\n\t\t\tcode: \"sortKeys(null)\",\n\t\t\tcheck: func(t *testing.T, result goja.Value) {\n\t\t\t\tassert.Nil(t, result.Export())\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\ttc.check(t, result)\n\t\t})\n\t}\n}\n\n// TestGroupBy tests the groupBy function.\nfunc TestGroupBy(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t}{\n\t\t{\"groupBy function\", \"groupBy(x => x % 2 === 0 ? 'even' : 'odd')([1, 2, 3, 4])\",\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"odd\":  []interface{}{int64(1), int64(3)},\n\t\t\t\t\"even\": []interface{}{int64(2), int64(4)},\n\t\t\t}},\n\t\t{\"groupBy property name\", \"groupBy('type')([{type: 'a', v: 1}, {type: 'b', v: 2}, {type: 'a', v: 3}])\",\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"a\": []interface{}{\n\t\t\t\t\tmap[string]interface{}{\"type\": \"a\", \"v\": int64(1)},\n\t\t\t\t\tmap[string]interface{}{\"type\": \"a\", \"v\": int64(3)},\n\t\t\t\t},\n\t\t\t\t\"b\": []interface{}{\n\t\t\t\t\tmap[string]interface{}{\"type\": \"b\", \"v\": int64(2)},\n\t\t\t\t},\n\t\t\t}},\n\t\t{\"groupBy empty\", \"groupBy(x => x)([])\",\n\t\t\tmap[string]interface{}{}},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t})\n\t}\n}\n\n// TestChunk tests the chunk function.\nfunc TestChunk(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t}{\n\t\t{\"chunk by 2\", \"chunk(2)([1, 2, 3, 4, 5])\",\n\t\t\t[]interface{}{\n\t\t\t\t[]interface{}{int64(1), int64(2)},\n\t\t\t\t[]interface{}{int64(3), int64(4)},\n\t\t\t\t[]interface{}{int64(5)},\n\t\t\t}},\n\t\t{\"chunk by 3\", \"chunk(3)([1, 2, 3, 4, 5, 6])\",\n\t\t\t[]interface{}{\n\t\t\t\t[]interface{}{int64(1), int64(2), int64(3)},\n\t\t\t\t[]interface{}{int64(4), int64(5), int64(6)},\n\t\t\t}},\n\t\t{\"chunk empty\", \"chunk(2)([])\",\n\t\t\t[]interface{}{}},\n\t\t{\"chunk by 1\", \"chunk(1)([1, 2, 3])\",\n\t\t\t[]interface{}{\n\t\t\t\t[]interface{}{int64(1)},\n\t\t\t\t[]interface{}{int64(2)},\n\t\t\t\t[]interface{}{int64(3)},\n\t\t\t}},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t})\n\t}\n}\n\n// TestZip tests the zip function.\nfunc TestZip(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t}{\n\t\t{\"zip two arrays\", \"zip([1, 2, 3], ['a', 'b', 'c'])\",\n\t\t\t[]interface{}{\n\t\t\t\t[]interface{}{int64(1), \"a\"},\n\t\t\t\t[]interface{}{int64(2), \"b\"},\n\t\t\t\t[]interface{}{int64(3), \"c\"},\n\t\t\t}},\n\t\t{\"zip three arrays\", \"zip([1, 2], ['a', 'b'], [true, false])\",\n\t\t\t[]interface{}{\n\t\t\t\t[]interface{}{int64(1), \"a\", true},\n\t\t\t\t[]interface{}{int64(2), \"b\", false},\n\t\t\t}},\n\t\t{\"zip different lengths\", \"zip([1, 2, 3], ['a', 'b'])\",\n\t\t\t[]interface{}{\n\t\t\t\t[]interface{}{int64(1), \"a\"},\n\t\t\t\t[]interface{}{int64(2), \"b\"},\n\t\t\t}},\n\t\t{\"zip empty\", \"zip([], [])\",\n\t\t\t[]interface{}{}},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t})\n\t}\n}\n\n// TestFlatten tests the flatten function.\nfunc TestFlatten(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t\thasError bool\n\t}{\n\t\t{\"flatten nested\", \"flatten([[1, 2], [3, 4]])\",\n\t\t\t[]interface{}{int64(1), int64(2), int64(3), int64(4)}, false},\n\t\t{\"flatten mixed\", \"flatten([[1], 2, [3, 4]])\",\n\t\t\t[]interface{}{int64(1), int64(2), int64(3), int64(4)}, false},\n\t\t{\"flatten one level\", \"flatten([[[1, 2]], [[3, 4]]])\",\n\t\t\t[]interface{}{\n\t\t\t\t[]interface{}{int64(1), int64(2)},\n\t\t\t\t[]interface{}{int64(3), int64(4)},\n\t\t\t}, false},\n\t\t{\"flatten empty\", \"flatten([])\", []interface{}{}, false},\n\t\t{\"flatten non-array error\", \"flatten('hello')\", nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestReverse tests the reverse function.\nfunc TestReverse(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t\thasError bool\n\t}{\n\t\t{\"reverse numbers\", \"reverse([1, 2, 3])\",\n\t\t\t[]interface{}{int64(3), int64(2), int64(1)}, false},\n\t\t{\"reverse strings\", \"reverse(['a', 'b', 'c'])\",\n\t\t\t[]interface{}{\"c\", \"b\", \"a\"}, false},\n\t\t{\"reverse empty\", \"reverse([])\", []interface{}{}, false},\n\t\t{\"reverse single\", \"reverse([1])\", []interface{}{int64(1)}, false},\n\t\t{\"reverse non-array error\", \"reverse('hello')\", nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestKeys tests the keys function.\nfunc TestKeys(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\thasError bool\n\t}{\n\t\t{\"object keys\", \"keys({a: 1, b: 2}).sort()\", false},\n\t\t{\"empty object\", \"keys({})\", false},\n\t\t{\"array keys\", \"keys([10, 20, 30])\", false},\n\t\t{\"null error\", \"keys(null)\", true},\n\t\t{\"number error\", \"keys(42)\", true},\n\t\t{\"string error\", \"keys('hello')\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.NotNil(t, result.Export())\n\t\t\t}\n\t\t})\n\t}\n\n\t// Specific value checks\n\tt.Run(\"object keys values\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"keys({a: 1, b: 2}).sort()\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []interface{}{\"a\", \"b\"}, result.Export())\n\t})\n\n\tt.Run(\"array keys values\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"keys([10, 20])\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []interface{}{\"0\", \"1\"}, result.Export())\n\t})\n}\n\n// TestValues tests the values function.\nfunc TestValues(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\thasError bool\n\t}{\n\t\t{\"object values\", \"values({a: 1, b: 2}).sort()\", false},\n\t\t{\"empty object\", \"values({})\", false},\n\t\t{\"array values\", \"values([10, 20, 30])\", false},\n\t\t{\"null error\", \"values(null)\", true},\n\t\t{\"number error\", \"values(42)\", true},\n\t\t{\"string error\", \"values('hello')\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.NotNil(t, result.Export())\n\t\t\t}\n\t\t})\n\t}\n\n\t// Specific value checks\n\tt.Run(\"object values sorted\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"values({a: 1, b: 2}).sort()\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []interface{}{int64(1), int64(2)}, result.Export())\n\t})\n\n\tt.Run(\"array values\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"values([10, 20, 30])\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []interface{}{int64(10), int64(20), int64(30)}, result.Export())\n\t})\n}\n\n// TestList tests the list function.\nfunc TestList(t *testing.T) {\n\tvm, output := setupVMWithOutput(t)\n\n\tt.Run(\"list prints each item\", func(t *testing.T) {\n\t\t*output = []string{} // Reset output\n\t\tresult, err := vm.RunString(\"list([1, 2, 3]) === skip\")\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, result.Export().(bool))\n\t\tassert.Equal(t, []string{\"1\", \"2\", \"3\"}, *output)\n\t})\n\n\tt.Run(\"list with objects\", func(t *testing.T) {\n\t\t*output = []string{}\n\t\tresult, err := vm.RunString(`list([{a: 1}]) === skip`)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, result.Export().(bool))\n\t\tassert.Len(t, *output, 1)\n\t})\n\n\tt.Run(\"list non-array error\", func(t *testing.T) {\n\t\t_, err := vm.RunString(\"list('hello')\")\n\t\tassert.Error(t, err)\n\t})\n}\n\n// TestDel tests the del function.\nfunc TestDel(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t\thasError bool\n\t}{\n\t\t{\"del from object\", \"del('a')({a: 1, b: 2})\",\n\t\t\tmap[string]interface{}{\"b\": int64(2)}, false},\n\t\t{\"del non-existent key\", \"del('c')({a: 1, b: 2})\",\n\t\t\tmap[string]interface{}{\"a\": int64(1), \"b\": int64(2)}, false},\n\t\t{\"del from array\", \"del(1)([1, 2, 3])\",\n\t\t\t[]interface{}{int64(1), int64(3)}, false},\n\t\t{\"del first from array\", \"del(0)([1, 2, 3])\",\n\t\t\t[]interface{}{int64(2), int64(3)}, false},\n\t\t{\"del last from array\", \"del(2)([1, 2, 3])\",\n\t\t\t[]interface{}{int64(1), int64(2)}, false},\n\t\t{\"del from empty array\", \"del(0)([])\", []interface{}{}, false},\n\t\t{\"del from primitive error\", \"del('a')(42)\", nil, true},\n\t\t{\"del from null error\", \"del('a')(null)\", nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestSkipSymbol tests the skip symbol.\nfunc TestSkipSymbol(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"skip is a symbol\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"typeof skip\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"symbol\", result.Export())\n\t})\n\n\tt.Run(\"skip is unique\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"skip === skip\")\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, result.Export().(bool))\n\t})\n}\n\n// TestConsoleLog tests console.log.\nfunc TestConsoleLog(t *testing.T) {\n\tvm, output := setupVMWithOutput(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected []string\n\t}{\n\t\t{\"log string\", \"console.log('hello')\", []string{\"hello\"}},\n\t\t{\"log number\", \"console.log(42)\", []string{\"42\"}},\n\t\t{\"log object\", \"console.log({a: 1})\", []string{\"{\\n  \\\"a\\\": 1\\n}\"}},\n\t\t{\"log undefined\", \"console.log(undefined)\", []string{\"undefined\"}},\n\t\t{\"log multiple args\", \"console.log('a', 'b')\", []string{\"a b\"}},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t*output = []string{}\n\t\t\t_, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, *output)\n\t\t})\n\t}\n}\n\n// TestToBase64 tests the toBase64 function.\nfunc TestToBase64(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"encode hello\", \"toBase64('hello')\", \"aGVsbG8=\"},\n\t\t{\"encode empty\", \"toBase64('')\", \"\"},\n\t\t{\"encode unicode\", \"toBase64('こんにちは')\", \"44GT44KT44Gr44Gh44Gv\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.input)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t})\n\t}\n}\n\n// TestFromBase64 tests the fromBase64 function.\nfunc TestFromBase64(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t\thasError bool\n\t}{\n\t\t{\"decode hello\", \"fromBase64('aGVsbG8=')\", \"hello\", false},\n\t\t{\"decode empty\", \"fromBase64('')\", \"\", false},\n\t\t{\"decode unicode\", \"fromBase64('44GT44KT44Gr44Gh44Gv')\", \"こんにちは\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.input)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"invalid base64 error\", func(t *testing.T) {\n\t\t_, err := vm.RunString(\"fromBase64('not-valid-base64!!!')\")\n\t\tassert.Error(t, err)\n\t})\n}\n\n// TestYAML tests YAML.parse and YAML.stringify.\nfunc TestYAML(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"YAML.parse simple\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`YAML.parse('name: John\\nage: 30')`)\n\t\trequire.NoError(t, err)\n\t\texpected := map[string]interface{}{\"name\": \"John\", \"age\": int64(30)}\n\t\tassert.Equal(t, expected, result.Export())\n\t})\n\n\tt.Run(\"YAML.parse array\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`YAML.parse('- 1\\n- 2\\n- 3')`)\n\t\trequire.NoError(t, err)\n\t\texpected := []interface{}{int64(1), int64(2), int64(3)}\n\t\tassert.Equal(t, expected, result.Export())\n\t})\n\n\tt.Run(\"YAML.stringify simple\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`YAML.stringify({name: 'John', age: 30})`)\n\t\trequire.NoError(t, err)\n\t\tyamlStr := result.Export().(string)\n\t\tassert.Contains(t, yamlStr, \"name: John\")\n\t\tassert.Contains(t, yamlStr, \"age: 30\")\n\t})\n\n\tt.Run(\"YAML.stringify array\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`YAML.stringify([1, 2, 3])`)\n\t\trequire.NoError(t, err)\n\t\tyamlStr := result.Export().(string)\n\t\tassert.Contains(t, yamlStr, \"- 1\")\n\t\tassert.Contains(t, yamlStr, \"- 2\")\n\t\tassert.Contains(t, yamlStr, \"- 3\")\n\t})\n\n\tt.Run(\"YAML.parse invalid\", func(t *testing.T) {\n\t\t_, err := vm.RunString(`YAML.parse('invalid: [unclosed')`)\n\t\tassert.Error(t, err)\n\t})\n}\n\n// TestIsFalsely tests the internal isFalsely function behavior.\nfunc TestIsFalsely(t *testing.T) {\n\tvm := setupVM(t)\n\n\t// isFalsely is used internally by filter\n\t// Testing through filter behavior\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t}{\n\t\t{\"false is falsely\", \"filter(x => false)([1])\", []interface{}{}},\n\t\t{\"null is falsely\", \"filter(x => null)([1])\", []interface{}{}},\n\t\t{\"undefined is falsely\", \"filter(x => undefined)([1])\", []interface{}{}},\n\t\t{\"0 is not falsely\", \"filter(x => 0)([1])\", []interface{}{int64(1)}},\n\t\t{\"empty string is not falsely\", \"filter(x => '')([1])\", []interface{}{int64(1)}},\n\t\t{\"true is not falsely\", \"filter(x => true)([1])\", []interface{}{int64(1)}},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t})\n\t}\n}\n\n// TestChainedOperations tests chaining multiple stdlib functions.\nfunc TestChainedOperations(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []struct {\n\t\tname     string\n\t\tcode     string\n\t\texpected interface{}\n\t}{\n\t\t{\n\t\t\t\"filter then map\",\n\t\t\t\"map(x => x * 2)(filter(x => x > 1)([1, 2, 3]))\",\n\t\t\t[]interface{}{int64(4), int64(6)},\n\t\t},\n\t\t{\n\t\t\t\"map then filter\",\n\t\t\t\"filter(x => x > 3)(map(x => x * 2)([1, 2, 3]))\",\n\t\t\t[]interface{}{int64(4), int64(6)},\n\t\t},\n\t\t{\n\t\t\t\"sort then reverse\",\n\t\t\t\"reverse(sort([3, 1, 2]))\",\n\t\t\t[]interface{}{int64(3), int64(2), int64(1)},\n\t\t},\n\t\t{\n\t\t\t\"flatten then uniq\",\n\t\t\t\"uniq(flatten([[1, 2], [2, 3]]))\",\n\t\t\t[]interface{}{int64(1), int64(2), int64(3)},\n\t\t},\n\t\t{\n\t\t\t\"groupBy then keys\",\n\t\t\t\"keys(groupBy(x => x % 2)([1, 2, 3, 4])).sort()\",\n\t\t\t[]interface{}{\"0\", \"1\"},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := vm.RunString(tc.code)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, result.Export())\n\t\t})\n\t}\n}\n\n// TestEdgeCases tests various edge cases.\nfunc TestEdgeCases(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"deeply nested walk\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\twalk(x => typeof x === 'number' ? x * 2 : x)({\n\t\t\t\ta: {\n\t\t\t\t\tb: {\n\t\t\t\t\t\tc: 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\texpected := map[string]interface{}{\n\t\t\t\"a\": map[string]interface{}{\n\t\t\t\t\"b\": map[string]interface{}{\n\t\t\t\t\t\"c\": int64(2),\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tassert.Equal(t, expected, result.Export())\n\t})\n\n\tt.Run(\"empty input handling\", func(t *testing.T) {\n\t\ttests := []string{\n\t\t\t\"len([])\",\n\t\t\t\"len({})\",\n\t\t\t\"len('')\",\n\t\t\t\"uniq([])\",\n\t\t\t\"sort([])\",\n\t\t\t\"flatten([])\",\n\t\t\t\"reverse([])\",\n\t\t\t\"filter(x => true)([])\",\n\t\t\t\"map(x => x)([])\",\n\t\t\t\"chunk(2)([])\",\n\t\t\t\"zip([], [])\",\n\t\t\t\"keys({})\",\n\t\t\t\"values({})\",\n\t\t}\n\t\tfor _, code := range tests {\n\t\t\t_, err := vm.RunString(code)\n\t\t\tassert.NoError(t, err, \"Failed for: %s\", code)\n\t\t}\n\t})\n\n\tt.Run(\"large array handling\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\tconst arr = [];\n\t\t\tfor (let i = 0; i < 1000; i++) arr.push(i);\n\t\t\tlen(arr)\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, int64(1000), result.Export())\n\t})\n\n\tt.Run(\"unicode strings\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`len('你好世界')`)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, int64(4), result.Export())\n\t})\n}\n\n// TestBase64RoundTrip tests that base64 encoding/decoding is reversible.\nfunc TestBase64RoundTrip(t *testing.T) {\n\tvm := setupVM(t)\n\n\ttests := []string{\n\t\t\"hello\",\n\t\t\"\",\n\t\t\"Hello, 世界!\",\n\t\t\"Special chars: !@#$%^&*()\",\n\t\tstrings.Repeat(\"a\", 1000),\n\t}\n\n\tfor _, input := range tests {\n\t\tt.Run(input[:min(len(input), 20)], func(t *testing.T) {\n\t\t\tcode := \"fromBase64(toBase64('\" + escapeJS(input) + \"'))\"\n\t\t\tresult, err := vm.RunString(code)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, input, result.Export())\n\t\t})\n\t}\n}\n\nfunc escapeJS(s string) string {\n\ts = strings.ReplaceAll(s, \"\\\\\", \"\\\\\\\\\")\n\ts = strings.ReplaceAll(s, \"'\", \"\\\\'\")\n\ts = strings.ReplaceAll(s, \"\\n\", \"\\\\n\")\n\ts = strings.ReplaceAll(s, \"\\r\", \"\\\\r\")\n\treturn s\n}\n\nfunc min(a, b int) int {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\n\n// TestExit tests the exit function.\nfunc TestExit(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"exit panics with ExitError\", func(t *testing.T) {\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\trequire.NotNil(t, r, \"Expected panic from exit()\")\n\t\t\texitErr, ok := r.(engine.ExitError)\n\t\t\trequire.True(t, ok, \"Expected ExitError, got %T\", r)\n\t\t\tassert.Equal(t, 42, exitErr.Code)\n\t\t}()\n\t\t_, _ = vm.RunString(\"exit(42)\")\n\t})\n\n\tt.Run(\"exit with 0\", func(t *testing.T) {\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\trequire.NotNil(t, r)\n\t\t\texitErr, ok := r.(engine.ExitError)\n\t\t\trequire.True(t, ok)\n\t\t\tassert.Equal(t, 0, exitErr.Code)\n\t\t}()\n\t\t_, _ = vm.RunString(\"exit(0)\")\n\t})\n}\n\n// TestSave tests the save function.\nfunc TestSave(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"save undefined throws error\", func(t *testing.T) {\n\t\t_, err := vm.RunString(\"save(undefined)\")\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"Cannot save undefined\")\n\t})\n\n\tt.Run(\"save without file path throws error\", func(t *testing.T) {\n\t\t// FilePath is empty by default in tests\n\t\t_, err := vm.RunString(\"save({a: 1})\")\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"specify a file\")\n\t})\n}\n\n// TestSortMutation tests that sort mutates the original array (JavaScript behavior).\nfunc TestSortMutation(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"sort mutates original array\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\tconst arr = [3, 1, 2];\n\t\t\tsort(arr);\n\t\t\tarr[0]\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\t// JavaScript sort mutates the array\n\t\tassert.Equal(t, int64(1), result.Export())\n\t})\n}\n\n// TestReverseMutation tests that reverse mutates the original array (JavaScript behavior).\nfunc TestReverseMutation(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"reverse mutates original array\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\tconst arr = [1, 2, 3];\n\t\t\treverse(arr);\n\t\t\tarr[0]\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\t// JavaScript reverse mutates the array\n\t\tassert.Equal(t, int64(3), result.Export())\n\t})\n}\n\n// TestDelImmutability tests that del does not mutate the original.\nfunc TestDelImmutability(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"del does not mutate original object\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\tconst obj = {a: 1, b: 2};\n\t\t\tdel('a')(obj);\n\t\t\tobj.a\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, int64(1), result.Export())\n\t})\n\n\tt.Run(\"del does not mutate original array\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\tconst arr = [1, 2, 3];\n\t\t\tdel(0)(arr);\n\t\t\tarr[0]\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, int64(1), result.Export())\n\t})\n}\n\n// TestWalkWithNull tests walk behavior with null values.\nfunc TestWalkWithNull(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"walk handles null values\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\twalk(x => x === null ? 'was null' : x)({a: null, b: 1})\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\texpected := map[string]interface{}{\"a\": \"was null\", \"b\": int64(1)}\n\t\tassert.Equal(t, expected, result.Export())\n\t})\n}\n\n// TestGroupByWithPrototypeKeys tests groupBy doesn't have prototype pollution issues.\nfunc TestGroupByWithPrototypeKeys(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"groupBy with hasOwnProperty key\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\tgroupBy(x => x)(['hasOwnProperty', 'toString', 'normal'])\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\texported := result.Export().(map[string]interface{})\n\t\tassert.Len(t, exported[\"hasOwnProperty\"], 1)\n\t\tassert.Len(t, exported[\"toString\"], 1)\n\t\tassert.Len(t, exported[\"normal\"], 1)\n\t})\n}\n\n// TestChunkEdgeCases tests chunk with edge case sizes.\nfunc TestChunkEdgeCases(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"chunk size larger than array\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"chunk(10)([1, 2, 3])\")\n\t\trequire.NoError(t, err)\n\t\texpected := []interface{}{[]interface{}{int64(1), int64(2), int64(3)}}\n\t\tassert.Equal(t, expected, result.Export())\n\t})\n\n\tt.Run(\"chunk size equals array length\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"chunk(3)([1, 2, 3])\")\n\t\trequire.NoError(t, err)\n\t\texpected := []interface{}{[]interface{}{int64(1), int64(2), int64(3)}}\n\t\tassert.Equal(t, expected, result.Export())\n\t})\n}\n\n// TestZipEdgeCases tests zip with edge cases.\nfunc TestZipEdgeCases(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"zip single array\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"zip([1, 2, 3])\")\n\t\trequire.NoError(t, err)\n\t\texpected := []interface{}{\n\t\t\t[]interface{}{int64(1)},\n\t\t\t[]interface{}{int64(2)},\n\t\t\t[]interface{}{int64(3)},\n\t\t}\n\t\tassert.Equal(t, expected, result.Export())\n\t})\n\n\tt.Run(\"zip with one empty array\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(\"zip([1, 2, 3], [])\")\n\t\trequire.NoError(t, err)\n\t\texpected := []interface{}{}\n\t\tassert.Equal(t, expected, result.Export())\n\t})\n}\n\n// TestFilterWithObjects tests filter with object predicates.\nfunc TestFilterWithObjects(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"filter objects by property\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\tfilter(x => x.active)([\n\t\t\t\t{name: 'a', active: true},\n\t\t\t\t{name: 'b', active: false},\n\t\t\t\t{name: 'c', active: true}\n\t\t\t])\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\texported := result.Export().([]interface{})\n\t\tassert.Len(t, exported, 2)\n\t})\n}\n\n// TestMapWithObjects tests map transforming objects.\nfunc TestMapWithObjects(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"map extract property\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\tmap(x => x.name)([\n\t\t\t\t{name: 'a', value: 1},\n\t\t\t\t{name: 'b', value: 2}\n\t\t\t])\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\texpected := []interface{}{\"a\", \"b\"}\n\t\tassert.Equal(t, expected, result.Export())\n\t})\n\n\tt.Run(\"map transform object\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\tmap(x => ({...x, doubled: x.value * 2}))([\n\t\t\t\t{value: 1},\n\t\t\t\t{value: 2}\n\t\t\t])\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\texported := result.Export().([]interface{})\n\t\tassert.Equal(t, int64(2), exported[0].(map[string]interface{})[\"doubled\"])\n\t\tassert.Equal(t, int64(4), exported[1].(map[string]interface{})[\"doubled\"])\n\t})\n}\n\n// TestSortByStability tests sortBy with equal keys.\nfunc TestSortByStability(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"sortBy preserves order for equal keys\", func(t *testing.T) {\n\t\t// Note: JavaScript sort is not guaranteed to be stable, but this tests the behavior\n\t\tresult, err := vm.RunString(`\n\t\t\tsortBy(x => x.group)([\n\t\t\t\t{name: 'a', group: 1},\n\t\t\t\t{name: 'b', group: 2},\n\t\t\t\t{name: 'c', group: 1}\n\t\t\t]).map(x => x.name)\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\texported := result.Export().([]interface{})\n\t\t// Group 1 items should come before group 2\n\t\tassert.Equal(t, \"b\", exported[2])\n\t})\n}\n\n// TestNestedOperations tests deeply nested function compositions.\nfunc TestNestedOperations(t *testing.T) {\n\tvm := setupVM(t)\n\n\tt.Run(\"complex nested operations\", func(t *testing.T) {\n\t\tresult, err := vm.RunString(`\n\t\t\tmap(x => x * 2)(\n\t\t\t\tfilter(x => x > 0)(\n\t\t\t\t\tflatten([\n\t\t\t\t\t\t[-1, 0, 1],\n\t\t\t\t\t\t[2, 3]\n\t\t\t\t\t])\n\t\t\t\t)\n\t\t\t)\n\t\t`)\n\t\trequire.NoError(t, err)\n\t\texpected := []interface{}{int64(2), int64(4), int64(6)}\n\t\tassert.Equal(t, expected, result.Export())\n\t})\n}\n"
  },
  {
    "path": "internal/engine/stringify.go",
    "content": "package engine\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"math/big\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/dop251/goja\"\n)\n\nfunc Stringify(value goja.Value, vm *goja.Runtime, depth int) string {\n\trtype := value.ExportType()\n\tif rtype == nil {\n\t\t// Convert both null and undefined to null (save as JSON.stringify)\n\t\treturn \"null\"\n\t}\n\n\tswitch rtype {\n\tcase bigIntType:\n\t\tbi := value.Export().(*big.Int)\n\t\treturn bi.String()\n\tcase timeTimeType:\n\t\tt := value.Export().(time.Time)\n\t\tquoted := Quote(t.String())\n\t\treturn quoted\n\t}\n\n\tswitch rtype.Kind() {\n\tcase reflect.Bool:\n\t\tif value.ToBoolean() {\n\t\t\treturn \"true\"\n\t\t} else {\n\t\t\treturn \"false\"\n\t\t}\n\n\tcase reflect.Int64:\n\t\treturn value.String()\n\n\tcase reflect.Float64:\n\t\tf := value.ToFloat()\n\t\tif math.IsInf(f, 0) {\n\t\t\treturn value.String()\n\t\t} else if math.IsNaN(f) {\n\t\t\treturn value.String()\n\t\t}\n\t\treturn value.String()\n\n\tcase reflect.String:\n\t\treturn Quote(value.String())\n\n\tcase reflect.Map:\n\t\tobj := value.ToObject(vm)\n\t\tkeys := obj.Keys()\n\n\t\tif len(keys) == 0 {\n\t\t\treturn \"{}\"\n\t\t}\n\n\t\tvar out strings.Builder\n\t\tout.WriteString(\"{\")\n\t\tout.WriteString(\"\\n\")\n\n\t\tident := strings.Repeat(\"  \", depth)\n\t\tidentKey := strings.Repeat(\"  \", depth+1)\n\n\t\tfor i, key := range keys {\n\t\t\tout.WriteString(identKey)\n\t\t\tout.WriteString(Quote(key))\n\t\t\tout.WriteString(\":\")\n\t\t\tout.WriteString(\" \")\n\t\t\tout.WriteString(Stringify(obj.Get(key), vm, depth+1))\n\t\t\tif i < len(keys)-1 {\n\t\t\t\tout.WriteString(\",\")\n\t\t\t}\n\t\t\tout.WriteString(\"\\n\")\n\n\t\t}\n\n\t\tout.WriteString(ident)\n\t\tout.WriteString(\"}\")\n\t\treturn out.String()\n\n\tcase reflect.Slice:\n\t\tarr := value.ToObject(vm)\n\t\tkeys := arr.Keys()\n\n\t\tif len(keys) == 0 {\n\t\t\treturn \"[]\"\n\t\t}\n\n\t\tvar out strings.Builder\n\t\tout.WriteString(\"[\")\n\t\tout.WriteString(\"\\n\")\n\n\t\tfor i, key := range keys {\n\t\t\titem := arr.Get(key)\n\t\t\tout.WriteString(strings.Repeat(\"  \", depth+1))\n\t\t\tout.WriteString(Stringify(item, vm, depth+1))\n\t\t\tif i < len(keys)-1 {\n\t\t\t\tout.WriteString(\",\")\n\t\t\t}\n\t\t\tout.WriteString(\"\\n\")\n\t\t}\n\n\t\tout.WriteString(strings.Repeat(\"  \", depth))\n\t\tout.WriteString(\"]\")\n\t\treturn out.String()\n\t}\n\tpanic(fmt.Sprintf(\"Unsupported value type: %v\", rtype.Kind()))\n}\n"
  },
  {
    "path": "internal/engine/transpile.go",
    "content": "package engine\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc JS(args []string) string {\n\tvar code strings.Builder\n\tcode.WriteString(\"\\nfunction __main__(json) {\\n\")\n\tfor i := range args {\n\t\tif args[i] == \"\" {\n\t\t\t// In autocomplete: after dropTail, we can have empty strings.\n\t\t\tcontinue\n\t\t}\n\t\tcode.WriteString(Body(args, i))\n\t}\n\tcode.WriteString(\"\\n  return json\\n}\\n\")\n\treturn code.String()\n}\n\nfunc Body(args []string, i int) string {\n\tjsCode := transpile(args[i])\n\tsnippet := formatErr(args, i, jsCode)\n\treturn fmt.Sprintf(`\n  try {\n    json = apply((function () {\n      const x = this\n      return %s\n    }).call(json), json)\n  } catch (e) {\n    throw %s\n  }\n\n  if (json === skip) return skip\n`, jsCode, strconv.Quote(snippet)+\" + e.toString()\")\n}\n\nvar (\n\treBracket      = regexp.MustCompile(`^(\\.\\w*)+\\[]`)\n\treBracketStart = regexp.MustCompile(`^\\.\\[`)\n\treDotStart     = regexp.MustCompile(`^\\.`)\n\treAt           = regexp.MustCompile(`^@`)\n\treFilter       = regexp.MustCompile(`^\\?`)\n)\n\nfunc transpile(code string) string {\n\tif code == \".\" {\n\t\treturn \"x\"\n\t}\n\n\tif reBracket.MatchString(code) {\n\t\treturn fmt.Sprintf(\"(%s)(x)\", fold(strings.Split(code, \"[]\")))\n\t}\n\n\tif reBracketStart.MatchString(code) {\n\t\treturn \"x\" + code[1:]\n\t}\n\n\tif reDotStart.MatchString(code) {\n\t\treturn \"x\" + code\n\t}\n\n\tif reAt.MatchString(code) {\n\t\tjsCode := transpile(code[1:])\n\t\treturn fmt.Sprintf(`map((x, i) => apply(%s, x, i))`, jsCode)\n\t}\n\n\tif reFilter.MatchString(code) {\n\t\tjsCode := transpile(code[1:])\n\t\treturn fmt.Sprintf(`filter((x, i) => apply(%s, x, i))`, jsCode)\n\t}\n\n\treturn code\n}\n\nfunc fold(s []string) string {\n\tif len(s) == 1 {\n\t\treturn \"x => x\" + s[0]\n\t}\n\tobj := s[0]\n\ts = s[1:]\n\tif obj == \".\" {\n\t\tobj = \"x\"\n\t} else {\n\t\tobj = \"x\" + obj\n\t}\n\treturn fmt.Sprintf(`x => %s.flatMap(%s)`, obj, fold(s))\n}\n"
  },
  {
    "path": "internal/engine/transpile_test.go",
    "content": "package engine\n\nimport (\n\t\"testing\"\n)\n\nfunc TestTranspile(t *testing.T) {\n\ttests := []struct {\n\t\tcode string\n\t\twant string\n\t}{\n\t\t{\".\", \"x\"},\n\t\t{\".foo\", \"x.foo\"},\n\t\t{\".[0]\", \"x[0]\"},\n\t\t{\"foo\", \"foo\"},\n\t\t{\"@.baz\", \"map((x, i) => apply(x.baz, x, i))\"},\n\t\t{\"?.foo > 42\", \"filter((x, i) => apply(x.foo > 42, x, i))\"},\n\t\t{\".foo[].bar[]\", \"(x => x.foo.flatMap(x => x.bar.flatMap(x => x)))(x)\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.code, func(t *testing.T) {\n\t\t\tgot := transpile(tt.code)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"transpile(%q) = %q; want %q\", tt.code, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFoldSimple(t *testing.T) {\n\ttests := []struct {\n\t\tparts []string\n\t\twant  string\n\t}{\n\t\t{[]string{\".foo\"}, \"x => x.foo\"},\n\t\t{[]string{\".foo\", \".bar\"}, \"x => x.foo.flatMap(x => x.bar)\"},\n\t}\n\tfor _, tt := range tests {\n\t\tgot := fold(tt.parts)\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"fold(%v) = %q; want %q\", tt.parts, got, tt.want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/engine/utils.go",
    "content": "package engine\n\nimport (\n\t\"math/big\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/dop251/goja\"\n)\n\nvar (\n\tbigIntType   = reflect.TypeOf((*big.Int)(nil))\n\ttimeTimeType = reflect.TypeOf((*time.Time)(nil)).Elem()\n)\n\nvar (\n\tsyntaxErrorRe = regexp.MustCompile(`^SyntaxError: SyntaxError: \\(anonymous\\): Line \\d+:\\d+\\s+`)\n\tandMoreErrors = regexp.MustCompile(`\\(and \\d+ more errors\\)$`)\n)\n\nfunc extractErrorMessage(s string) string {\n\ts = syntaxErrorRe.ReplaceAllString(s, \"\")\n\ts = andMoreErrors.ReplaceAllString(s, \"\")\n\treturn s\n}\n\nfunc errorToString(err error) string {\n\tif exception, ok := err.(*goja.Exception); ok {\n\t\tmessage := exception.Value().String()\n\t\tmessage = extractErrorMessage(message)\n\t\treturn message\n\t}\n\treturn err.Error()\n}\n"
  },
  {
    "path": "internal/engine/vm.go",
    "content": "package engine\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/dop251/goja\"\n\t\"github.com/goccy/go-yaml\"\n)\n\n// FilePath is the file being processed, empty if stdin.\nvar FilePath string\n\n// ExitError is used by exit() to signal a specific exit code.\ntype ExitError struct {\n\tCode int\n}\n\nfunc NewVM(writeOut func(string)) *goja.Runtime {\n\tvm := goja.New()\n\n\tif err := vm.Set(\"println\", func(s string) any {\n\t\twriteOut(s)\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := vm.Set(\"__save__\", func(json string) error {\n\t\tif FilePath == \"\" {\n\t\t\treturn fmt.Errorf(\"specify a file as the first argument to be able to save: fx file.json \")\n\t\t}\n\t\tif info, err := os.Lstat(FilePath); err == nil && info.Mode()&os.ModeSymlink != 0 {\n\t\t\treturn fmt.Errorf(\"cannot save to a symbolic link: %s\", FilePath)\n\t\t}\n\t\tif err := os.WriteFile(FilePath, []byte(json), 0644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := vm.Set(\"__stringify__\", func(x goja.Value) string {\n\t\treturn Stringify(x, vm, 0) + \"\\n\"\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := vm.Set(\"__toBase64__\", func(x string) string {\n\t\treturn base64.StdEncoding.EncodeToString([]byte(x))\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := vm.Set(\"__fromBase64__\", func(x string) (string, error) {\n\t\tdecoded, err := base64.StdEncoding.DecodeString(x)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn string(decoded), err\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := vm.Set(\"__yaml_parse__\", func(x string) (string, error) {\n\t\tb, err := yaml.YAMLToJSON([]byte(x))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn string(b), err\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := vm.Set(\"__yaml_stringify__\", func(x goja.Value) string {\n\t\tb, err := yaml.JSONToYAML([]byte(Stringify(x, vm, 0)))\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn string(b)\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\n\tif err := vm.Set(\"__exit__\", func(code int) {\n\t\tpanic(ExitError{Code: code})\n\t}); err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn vm\n}\n"
  },
  {
    "path": "internal/fuzzy/algo.go",
    "content": "package fuzzy\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\nvar delimiterChars = \".\"\n\nconst whiteChars = \" \\t\\n\\v\\f\\r\\x85\\xA0\"\n\ntype Result struct {\n\tStart int\n\tEnd   int\n\tScore int\n}\n\nconst (\n\tscoreMatch        = 16\n\tscoreGapStart     = -3\n\tscoreGapExtension = -1\n\n\t// We prefer matches at the beginning of a word, but the bonus should not be\n\t// too great to prevent the longer acronym matches from always winning over\n\t// shorter fuzzy matches. The bonus point here was specifically chosen that\n\t// the bonus is cancelled when the gap between the acronyms grows over\n\t// 8 characters, which is approximately the average length of the words found\n\t// in web2 dictionary and my file system.\n\tbonusBoundary = scoreMatch / 2\n\n\t// Although bonus point for non-word characters is non-contextual, we need it\n\t// for computing bonus points for consecutive chunks starting with a non-word\n\t// character.\n\tbonusNonWord = scoreMatch / 2\n\n\t// Edge-triggered bonus for matches in camelCase words.\n\t// Compared to word-boundary case, they don't accompany single-character gaps\n\t// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.\n\tbonusCamel123 = bonusBoundary + scoreGapExtension\n\n\t// Minimum bonus point given to characters in consecutive chunks.\n\t// Note that bonus points for consecutive matches shouldn't have needed if we\n\t// used fixed match score as in the original algorithm.\n\tbonusConsecutive = -(scoreGapStart + scoreGapExtension)\n\n\t// The first character in the typed pattern usually has more significance\n\t// than the rest so it's important that it appears at special positions where\n\t// bonus points are given, e.g. \"to-go\" vs. \"ongoing\" on \"og\" or on \"ogo\".\n\t// The amount of the extra bonus should be limited so that the gap penalty is\n\t// still respected.\n\tbonusFirstCharMultiplier = 2\n)\n\nvar (\n\t// Extra bonus for word boundary after whitespace character or beginning of the string\n\tbonusBoundaryWhite int16 = bonusBoundary\n\n\t// Extra bonus for word boundary after slash, colon, semi-colon, and comma\n\tbonusBoundaryDelimiter int16 = bonusBoundary + 1\n\n\tinitialCharClass = charDelimiter\n\n\t// A minor optimization that can give 15%+ performance boost\n\tasciiCharClasses [unicode.MaxASCII + 1]charClass\n\n\t// A minor optimization that can give yet another 5% performance boost\n\tbonusMatrix [charNumber + 1][charNumber + 1]int16\n)\n\ntype charClass int\n\nconst (\n\tcharWhite charClass = iota\n\tcharNonWord\n\tcharDelimiter\n\tcharLower\n\tcharUpper\n\tcharLetter\n\tcharNumber\n)\n\nfunc init() {\n\tfor i := 0; i <= unicode.MaxASCII; i++ {\n\t\tchar := rune(i)\n\t\tc := charNonWord\n\t\tif char >= 'a' && char <= 'z' {\n\t\t\tc = charLower\n\t\t} else if char >= 'A' && char <= 'Z' {\n\t\t\tc = charUpper\n\t\t} else if char >= '0' && char <= '9' {\n\t\t\tc = charNumber\n\t\t} else if strings.ContainsRune(whiteChars, char) {\n\t\t\tc = charWhite\n\t\t} else if strings.ContainsRune(delimiterChars, char) {\n\t\t\tc = charDelimiter\n\t\t}\n\t\tasciiCharClasses[i] = c\n\t}\n\tfor i := 0; i <= int(charNumber); i++ {\n\t\tfor j := 0; j <= int(charNumber); j++ {\n\t\t\tbonusMatrix[i][j] = bonusFor(charClass(i), charClass(j))\n\t\t}\n\t}\n}\n\nfunc posArray(withPos bool, len int) *[]int {\n\tif withPos {\n\t\tpos := make([]int, 0, len)\n\t\treturn &pos\n\t}\n\treturn nil\n}\n\nfunc alloc16(offset int, slab *Slab, size int) (int, []int16) {\n\tif slab != nil && cap(slab.I16) > offset+size {\n\t\tslice := slab.I16[offset : offset+size]\n\t\treturn offset + size, slice\n\t}\n\treturn offset, make([]int16, size)\n}\n\nfunc alloc32(offset int, slab *Slab, size int) (int, []int32) {\n\tif slab != nil && cap(slab.I32) > offset+size {\n\t\tslice := slab.I32[offset : offset+size]\n\t\treturn offset + size, slice\n\t}\n\treturn offset, make([]int32, size)\n}\n\nfunc charClassOfNonAscii(char rune) charClass {\n\tif unicode.IsLower(char) {\n\t\treturn charLower\n\t} else if unicode.IsUpper(char) {\n\t\treturn charUpper\n\t} else if unicode.IsNumber(char) {\n\t\treturn charNumber\n\t} else if unicode.IsLetter(char) {\n\t\treturn charLetter\n\t} else if unicode.IsSpace(char) {\n\t\treturn charWhite\n\t} else if strings.ContainsRune(delimiterChars, char) {\n\t\treturn charDelimiter\n\t}\n\treturn charNonWord\n}\n\nfunc charClassOf(char rune) charClass {\n\tif char <= unicode.MaxASCII {\n\t\treturn asciiCharClasses[char]\n\t}\n\treturn charClassOfNonAscii(char)\n}\n\nfunc bonusFor(prevClass charClass, class charClass) int16 {\n\tif class > charNonWord {\n\t\tswitch prevClass {\n\t\tcase charWhite:\n\t\t\t// Word boundary after whitespace\n\t\t\treturn bonusBoundaryWhite\n\t\tcase charDelimiter:\n\t\t\t// Word boundary after a delimiter character\n\t\t\treturn bonusBoundaryDelimiter\n\t\tcase charNonWord:\n\t\t\t// Word boundary\n\t\t\treturn bonusBoundary\n\t\t}\n\t}\n\n\tif prevClass == charLower && class == charUpper ||\n\t\tprevClass != charNumber && class == charNumber {\n\t\t// camelCase letter123\n\t\treturn bonusCamel123\n\t}\n\n\tswitch class {\n\tcase charNonWord, charDelimiter:\n\t\treturn bonusNonWord\n\tcase charWhite:\n\t\treturn bonusBoundaryWhite\n\t}\n\treturn 0\n}\n\nfunc normalizeRune(r rune) rune {\n\tif r < 0x00C0 || r > 0x2184 {\n\t\treturn r\n\t}\n\n\tn := normalized[r]\n\tif n > 0 {\n\t\treturn n\n\t}\n\treturn r\n}\n\nfunc trySkip(input *Chars, caseSensitive bool, b byte, from int) int {\n\tbyteArray := input.Bytes()[from:]\n\tidx := bytes.IndexByte(byteArray, b)\n\tif idx == 0 {\n\t\t// Can't skip any further\n\t\treturn from\n\t}\n\t// We may need to search for the uppercase letter again. We don't have to\n\t// consider normalization as we can be sure that this is an ASCII string.\n\tif !caseSensitive && b >= 'a' && b <= 'z' {\n\t\tif idx > 0 {\n\t\t\tbyteArray = byteArray[:idx]\n\t\t}\n\t\tuidx := bytes.IndexByte(byteArray, b-32)\n\t\tif uidx >= 0 {\n\t\t\tidx = uidx\n\t\t}\n\t}\n\tif idx < 0 {\n\t\treturn -1\n\t}\n\treturn from + idx\n}\n\nfunc isAscii(runes []rune) bool {\n\tfor _, r := range runes {\n\t\tif r >= utf8.RuneSelf {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc asciiFuzzyIndex(input *Chars, pattern []rune, caseSensitive bool) (int, int) {\n\t// Can't determine\n\tif !input.IsBytes() {\n\t\treturn 0, input.Length()\n\t}\n\n\t// Not possible\n\tif !isAscii(pattern) {\n\t\treturn -1, -1\n\t}\n\n\tfirstIdx, idx, lastIdx := 0, 0, 0\n\tvar b byte\n\tfor pidx := 0; pidx < len(pattern); pidx++ {\n\t\tb = byte(pattern[pidx])\n\t\tidx = trySkip(input, caseSensitive, b, idx)\n\t\tif idx < 0 {\n\t\t\treturn -1, -1\n\t\t}\n\t\tif pidx == 0 && idx > 0 {\n\t\t\t// Step back to find the right bonus point\n\t\t\tfirstIdx = idx - 1\n\t\t}\n\t\tlastIdx = idx\n\t\tidx++\n\t}\n\n\t// Find the last appearance of the last character of the pattern to limit the search scope\n\tbu := b\n\tif !caseSensitive && b >= 'a' && b <= 'z' {\n\t\tbu = b - 32\n\t}\n\tscope := input.Bytes()[lastIdx:]\n\tfor offset := len(scope) - 1; offset > 0; offset-- {\n\t\tif scope[offset] == b || scope[offset] == bu {\n\t\t\treturn firstIdx, lastIdx + offset + 1\n\t\t}\n\t}\n\treturn firstIdx, lastIdx + 1\n}\n\ntype Slab struct {\n\tI16 []int16\n\tI32 []int32\n}\n\nconst (\n\tcaseSensitive = false\n\tnormalize     = true\n\tforward       = true\n)\n\nfunc fuzzyMatch(input *Chars, pattern []rune) (Result, *[]int) {\n\tvar slab *Slab\n\t// Assume that pattern is given in lowercase if case-insensitive.\n\t// First check if there's a match and calculate bonus for each position.\n\t// If the input string is too long, consider finding the matching chars in\n\t// this phase as well (non-optimal alignment).\n\tM := len(pattern)\n\tif M == 0 {\n\t\treturn Result{0, 0, 0}, posArray(true, M)\n\t}\n\tN := input.Length()\n\tif M > N {\n\t\treturn Result{-1, -1, 0}, nil\n\t}\n\n\t// Phase 1. Optimized search for ASCII string\n\tminIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)\n\tif minIdx < 0 {\n\t\treturn Result{-1, -1, 0}, nil\n\t}\n\t// fmt.Println(N, maxIdx, idx, maxIdx-idx, input.ToString())\n\tN = maxIdx - minIdx\n\n\t// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages\n\toffset16 := 0\n\toffset32 := 0\n\toffset16, H0 := alloc16(offset16, slab, N)\n\toffset16, C0 := alloc16(offset16, slab, N)\n\t// Bonus point for each position\n\toffset16, B := alloc16(offset16, slab, N)\n\t// The first occurrence of each character in the pattern\n\toffset32, F := alloc32(offset32, slab, M)\n\t// Rune array\n\t_, T := alloc32(offset32, slab, N)\n\tinput.CopyRunes(T, minIdx)\n\n\t// Phase 2. Calculate bonus for each point\n\tmaxScore, maxScorePos := int16(0), 0\n\tpidx, lastIdx := 0, 0\n\tpchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false\n\tfor off, char := range T {\n\t\tvar class charClass\n\t\tif char <= unicode.MaxASCII {\n\t\t\tclass = asciiCharClasses[char]\n\t\t\tif !caseSensitive && class == charUpper {\n\t\t\t\tchar += 32\n\t\t\t\tT[off] = char\n\t\t\t}\n\t\t} else {\n\t\t\tclass = charClassOfNonAscii(char)\n\t\t\tif !caseSensitive && class == charUpper {\n\t\t\t\tchar = unicode.To(unicode.LowerCase, char)\n\t\t\t}\n\t\t\tif normalize {\n\t\t\t\tchar = normalizeRune(char)\n\t\t\t}\n\t\t\tT[off] = char\n\t\t}\n\n\t\tbonus := bonusMatrix[prevClass][class]\n\t\tB[off] = bonus\n\t\tprevClass = class\n\n\t\tif char == pchar {\n\t\t\tif pidx < M {\n\t\t\t\tF[pidx] = int32(off)\n\t\t\t\tpidx++\n\t\t\t\tpchar = pattern[min(pidx, M-1)]\n\t\t\t}\n\t\t\tlastIdx = off\n\t\t}\n\n\t\tif char == pchar0 {\n\t\t\tscore := scoreMatch + bonus*bonusFirstCharMultiplier\n\t\t\tH0[off] = score\n\t\t\tC0[off] = 1\n\t\t\tif M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {\n\t\t\t\tmaxScore, maxScorePos = score, off\n\t\t\t\tif forward && bonus >= bonusBoundary {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tinGap = false\n\t\t} else {\n\t\t\tif inGap {\n\t\t\t\tH0[off] = max(prevH0+scoreGapExtension, 0)\n\t\t\t} else {\n\t\t\t\tH0[off] = max(prevH0+scoreGapStart, 0)\n\t\t\t}\n\t\t\tC0[off] = 0\n\t\t\tinGap = true\n\t\t}\n\t\tprevH0 = H0[off]\n\t}\n\tif pidx != M {\n\t\treturn Result{-1, -1, 0}, nil\n\t}\n\tif M == 1 {\n\t\tresult := Result{minIdx + maxScorePos, minIdx + maxScorePos + 1, int(maxScore)}\n\t\tpos := []int{minIdx + maxScorePos}\n\t\treturn result, &pos\n\t}\n\n\t// Phase 3. Fill in score matrix (H)\n\t// Unlike the original algorithm, we do not allow omission.\n\tf0 := int(F[0])\n\twidth := lastIdx - f0 + 1\n\toffset16, H := alloc16(offset16, slab, width*M)\n\tcopy(H, H0[f0:lastIdx+1])\n\n\t// Possible length of consecutive chunk at each position.\n\t_, C := alloc16(offset16, slab, width*M)\n\tcopy(C, C0[f0:lastIdx+1])\n\n\tFsub := F[1:]\n\tPsub := pattern[1:][:len(Fsub)]\n\tfor off, f := range Fsub {\n\t\tf := int(f)\n\t\tpchar := Psub[off]\n\t\tpidx := off + 1\n\t\trow := pidx * width\n\t\tinGap := false\n\t\tTsub := T[f : lastIdx+1]\n\t\tBsub := B[f:][:len(Tsub)]\n\t\tCsub := C[row+f-f0:][:len(Tsub)]\n\t\tCdiag := C[row+f-f0-1-width:][:len(Tsub)]\n\t\tHsub := H[row+f-f0:][:len(Tsub)]\n\t\tHdiag := H[row+f-f0-1-width:][:len(Tsub)]\n\t\tHleft := H[row+f-f0-1:][:len(Tsub)]\n\t\tHleft[0] = 0\n\t\tfor off, char := range Tsub {\n\t\t\tcol := off + f\n\t\t\tvar s1, s2, consecutive int16\n\n\t\t\tif inGap {\n\t\t\t\ts2 = Hleft[off] + scoreGapExtension\n\t\t\t} else {\n\t\t\t\ts2 = Hleft[off] + scoreGapStart\n\t\t\t}\n\n\t\t\tif pchar == char {\n\t\t\t\ts1 = Hdiag[off] + scoreMatch\n\t\t\t\tb := Bsub[off]\n\t\t\t\tconsecutive = Cdiag[off] + 1\n\t\t\t\tif consecutive > 1 {\n\t\t\t\t\tfb := B[col-int(consecutive)+1]\n\t\t\t\t\t// Break consecutive chunk\n\t\t\t\t\tif b >= bonusBoundary && b > fb {\n\t\t\t\t\t\tconsecutive = 1\n\t\t\t\t\t} else {\n\t\t\t\t\t\tb = max(b, max(bonusConsecutive, fb))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif s1+b < s2 {\n\t\t\t\t\ts1 += Bsub[off]\n\t\t\t\t\tconsecutive = 0\n\t\t\t\t} else {\n\t\t\t\t\ts1 += b\n\t\t\t\t}\n\t\t\t}\n\t\t\tCsub[off] = consecutive\n\n\t\t\tinGap = s1 < s2\n\t\t\tscore := max(max(s1, s2), 0)\n\t\t\tif pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {\n\t\t\t\tmaxScore, maxScorePos = score, col\n\t\t\t}\n\t\t\tHsub[off] = score\n\t\t}\n\t}\n\n\t// Phase 4. (Optional) Backtrace to find character positions\n\tpos := posArray(true, M)\n\tj := f0\n\ti := M - 1\n\tj = maxScorePos\n\tpreferMatch := true\n\tfor {\n\t\tI := i * width\n\t\tj0 := j - f0\n\t\ts := H[I+j0]\n\n\t\tvar s1, s2 int16\n\t\tif i > 0 && j >= int(F[i]) {\n\t\t\ts1 = H[I-width+j0-1]\n\t\t}\n\t\tif j > int(F[i]) {\n\t\t\ts2 = H[I+j0-1]\n\t\t}\n\n\t\tif s > s1 && (s > s2 || s == s2 && preferMatch) {\n\t\t\t*pos = append(*pos, j+minIdx)\n\t\t\tif i == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ti--\n\t\t}\n\t\tpreferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0\n\t\tj--\n\t}\n\t// Start offset we return here is only relevant when begin tiebreak is used.\n\t// However finding the accurate offset requires backtracking, and we don't\n\t// want to pay extra cost for the option that has lost its importance.\n\treturn Result{minIdx + j, minIdx + maxScorePos + 1, int(maxScore)}, pos\n}\n"
  },
  {
    "path": "internal/fuzzy/chars.go",
    "content": "package fuzzy\n\nimport (\n\t\"bytes\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n\t\"unsafe\"\n)\n\nconst (\n\toverflow64 uint64 = 0x8080808080808080\n\toverflow32 uint32 = 0x80808080\n)\n\ntype Chars struct {\n\tslice           []byte // or []rune\n\tinBytes         bool\n\ttrimLengthKnown bool\n\ttrimLength      uint16\n\n\t// XXX Piggybacking item index here is a horrible idea. But I'm trying to\n\t// minimize the memory footprint by not wasting padded spaces.\n\tIndex int32\n}\n\nfunc checkAscii(bytes []byte) (bool, int) {\n\ti := 0\n\tfor ; i <= len(bytes)-8; i += 8 {\n\t\tif (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 {\n\t\t\treturn false, i\n\t\t}\n\t}\n\tfor ; i <= len(bytes)-4; i += 4 {\n\t\tif (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 {\n\t\t\treturn false, i\n\t\t}\n\t}\n\tfor ; i < len(bytes); i++ {\n\t\tif bytes[i] >= utf8.RuneSelf {\n\t\t\treturn false, i\n\t\t}\n\t}\n\treturn true, 0\n}\n\n// ToChars converts byte array into rune array\nfunc ToChars(bytes []byte) Chars {\n\tinBytes, bytesUntil := checkAscii(bytes)\n\tif inBytes {\n\t\treturn Chars{slice: bytes, inBytes: inBytes}\n\t}\n\n\trunes := make([]rune, bytesUntil, len(bytes))\n\tfor i := 0; i < bytesUntil; i++ {\n\t\trunes[i] = rune(bytes[i])\n\t}\n\tfor i := bytesUntil; i < len(bytes); {\n\t\tr, sz := utf8.DecodeRune(bytes[i:])\n\t\ti += sz\n\t\trunes = append(runes, r)\n\t}\n\treturn RunesToChars(runes)\n}\n\nfunc RunesToChars(runes []rune) Chars {\n\treturn Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false}\n}\n\nfunc (chars *Chars) IsBytes() bool {\n\treturn chars.inBytes\n}\n\nfunc (chars *Chars) Bytes() []byte {\n\treturn chars.slice\n}\n\nfunc (chars *Chars) NumLines(atMost int) (int, bool) {\n\tlines := 1\n\tif runes := chars.optionalRunes(); runes != nil {\n\t\tfor _, r := range runes {\n\t\t\tif r == '\\n' {\n\t\t\t\tlines++\n\t\t\t}\n\t\t\tif lines > atMost {\n\t\t\t\treturn atMost, true\n\t\t\t}\n\t\t}\n\t\treturn lines, false\n\t}\n\n\tfor idx := 0; idx < len(chars.slice); idx++ {\n\t\tfound := bytes.IndexByte(chars.slice[idx:], '\\n')\n\t\tif found < 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tidx += found\n\t\tlines++\n\t\tif lines > atMost {\n\t\t\treturn atMost, true\n\t\t}\n\t}\n\treturn lines, false\n}\n\nfunc (chars *Chars) optionalRunes() []rune {\n\tif chars.inBytes {\n\t\treturn nil\n\t}\n\treturn *(*[]rune)(unsafe.Pointer(&chars.slice))\n}\n\nfunc (chars *Chars) Get(i int) rune {\n\tif runes := chars.optionalRunes(); runes != nil {\n\t\treturn runes[i]\n\t}\n\treturn rune(chars.slice[i])\n}\n\nfunc (chars *Chars) Length() int {\n\tif runes := chars.optionalRunes(); runes != nil {\n\t\treturn len(runes)\n\t}\n\treturn len(chars.slice)\n}\n\n// TrimLength returns the length after trimming leading and trailing whitespaces\nfunc (chars *Chars) TrimLength() uint16 {\n\tif chars.trimLengthKnown {\n\t\treturn chars.trimLength\n\t}\n\tchars.trimLengthKnown = true\n\tvar i int\n\tlen := chars.Length()\n\tfor i = len - 1; i >= 0; i-- {\n\t\tchar := chars.Get(i)\n\t\tif !unicode.IsSpace(char) {\n\t\t\tbreak\n\t\t}\n\t}\n\t// Completely empty\n\tif i < 0 {\n\t\treturn 0\n\t}\n\n\tvar j int\n\tfor j = 0; j < len; j++ {\n\t\tchar := chars.Get(j)\n\t\tif !unicode.IsSpace(char) {\n\t\t\tbreak\n\t\t}\n\t}\n\tchars.trimLength = AsUint16(i - j + 1)\n\treturn chars.trimLength\n}\n\nfunc (chars *Chars) LeadingWhitespaces() int {\n\twhitespaces := 0\n\tfor i := 0; i < chars.Length(); i++ {\n\t\tchar := chars.Get(i)\n\t\tif !unicode.IsSpace(char) {\n\t\t\tbreak\n\t\t}\n\t\twhitespaces++\n\t}\n\treturn whitespaces\n}\n\nfunc (chars *Chars) TrailingWhitespaces() int {\n\twhitespaces := 0\n\tfor i := chars.Length() - 1; i >= 0; i-- {\n\t\tchar := chars.Get(i)\n\t\tif !unicode.IsSpace(char) {\n\t\t\tbreak\n\t\t}\n\t\twhitespaces++\n\t}\n\treturn whitespaces\n}\n\nfunc (chars *Chars) TrimTrailingWhitespaces() {\n\twhitespaces := chars.TrailingWhitespaces()\n\tchars.slice = chars.slice[0 : len(chars.slice)-whitespaces]\n}\n\nfunc (chars *Chars) TrimSuffix(runes []rune) {\n\tlastIdx := len(chars.slice)\n\tfirstIdx := lastIdx - len(runes)\n\tif firstIdx < 0 {\n\t\treturn\n\t}\n\n\tfor i := firstIdx; i < lastIdx; i++ {\n\t\tchar := chars.Get(i)\n\t\tif char != runes[i-firstIdx] {\n\t\t\treturn\n\t\t}\n\t}\n\n\tchars.slice = chars.slice[0:firstIdx]\n}\n\nfunc (chars *Chars) SliceRight(last int) {\n\tchars.slice = chars.slice[:last]\n}\n\nfunc (chars *Chars) ToString() string {\n\tif runes := chars.optionalRunes(); runes != nil {\n\t\treturn string(runes)\n\t}\n\treturn unsafe.String(unsafe.SliceData(chars.slice), len(chars.slice))\n}\n\nfunc (chars *Chars) ToRunes() []rune {\n\tif runes := chars.optionalRunes(); runes != nil {\n\t\treturn runes\n\t}\n\tbytes := chars.slice\n\trunes := make([]rune, len(bytes))\n\tfor idx, b := range bytes {\n\t\trunes[idx] = rune(b)\n\t}\n\treturn runes\n}\n\nfunc (chars *Chars) CopyRunes(dest []rune, from int) {\n\tif runes := chars.optionalRunes(); runes != nil {\n\t\tcopy(dest, runes[from:])\n\t\treturn\n\t}\n\tfor idx, b := range chars.slice[from:][:len(dest)] {\n\t\tdest[idx] = rune(b)\n\t}\n}\n\nfunc (chars *Chars) Prepend(prefix string) {\n\tif runes := chars.optionalRunes(); runes != nil {\n\t\trunes = append([]rune(prefix), runes...)\n\t\tchars.slice = *(*[]byte)(unsafe.Pointer(&runes))\n\t} else {\n\t\tchars.slice = append([]byte(prefix), chars.slice...)\n\t}\n}\n\nfunc (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {\n\ttext := make([]rune, chars.Length())\n\tcopy(text, chars.ToRunes())\n\n\tlines := [][]rune{}\n\toverflow := false\n\tif !multiLine {\n\t\tlines = append(lines, text)\n\t} else {\n\t\tfrom := 0\n\t\tfor off := 0; off < len(text); off++ {\n\t\t\tif text[off] == '\\n' {\n\t\t\t\tlines = append(lines, text[from:off+1]) // Include '\\n'\n\t\t\t\tfrom = off + 1\n\t\t\t\tif len(lines) >= maxLines {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar lastLine []rune\n\t\tif from < len(text) {\n\t\t\tlastLine = text[from:]\n\t\t}\n\n\t\toverflow = false\n\t\tif len(lines) >= maxLines {\n\t\t\toverflow = true\n\t\t} else {\n\t\t\tlines = append(lines, lastLine)\n\t\t}\n\t}\n\n\t// If wrapping is disabled, we're done\n\tif wrapCols == 0 {\n\t\treturn lines, overflow\n\t}\n\n\twrapped := [][]rune{}\n\tfor _, line := range lines {\n\t\t// Remove trailing '\\n' and remember if it was there\n\t\tnewline := len(line) > 0 && line[len(line)-1] == '\\n'\n\t\tif newline {\n\t\t\tline = line[:len(line)-1]\n\t\t}\n\n\t\thasWrapSign := false\n\t\tfor {\n\t\t\tcols := wrapCols\n\t\t\tif hasWrapSign {\n\t\t\t\tcols -= wrapSignWidth\n\t\t\t}\n\t\t\t_, overflowIdx := RunesWidth(line, 0, tabstop, cols)\n\t\t\tif overflowIdx >= 0 {\n\t\t\t\t// Might be a wide character\n\t\t\t\tif overflowIdx == 0 {\n\t\t\t\t\toverflowIdx = 1\n\t\t\t\t}\n\t\t\t\tif len(wrapped) >= maxLines {\n\t\t\t\t\treturn wrapped, true\n\t\t\t\t}\n\t\t\t\twrapped = append(wrapped, line[:overflowIdx])\n\t\t\t\thasWrapSign = true\n\t\t\t\tline = line[overflowIdx:]\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\thasWrapSign = false\n\n\t\t\t// Restore trailing '\\n'\n\t\t\tif newline {\n\t\t\t\tline = append(line, '\\n')\n\t\t\t}\n\n\t\t\tif len(wrapped) >= maxLines {\n\t\t\t\treturn wrapped, true\n\t\t\t}\n\n\t\t\twrapped = append(wrapped, line)\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn wrapped, overflow\n}\n"
  },
  {
    "path": "internal/fuzzy/chars_test.go",
    "content": "package fuzzy\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestToCharsAscii(t *testing.T) {\n\tchars := ToChars([]byte(\"foobar\"))\n\tif !chars.inBytes || chars.ToString() != \"foobar\" || !chars.inBytes {\n\t\tt.Error()\n\t}\n}\n\nfunc TestCharsLength(t *testing.T) {\n\tchars := ToChars([]byte(\"\\tabc한글  \"))\n\tif chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {\n\t\tt.Error()\n\t}\n}\n\nfunc TestCharsToString(t *testing.T) {\n\ttext := \"\\tabc한글  \"\n\tchars := ToChars([]byte(text))\n\tif chars.ToString() != text {\n\t\tt.Error()\n\t}\n}\n\nfunc TestTrimLength(t *testing.T) {\n\tcheck := func(str string, exp uint16) {\n\t\tchars := ToChars([]byte(str))\n\t\ttrimmed := chars.TrimLength()\n\t\tif trimmed != exp {\n\t\t\tt.Errorf(\"Invalid TrimLength result for '%s': %d (expected %d)\",\n\t\t\t\tstr, trimmed, exp)\n\t\t}\n\t}\n\tcheck(\"hello\", 5)\n\tcheck(\"hello \", 5)\n\tcheck(\"hello  \", 5)\n\tcheck(\" hello\", 5)\n\tcheck(\"  hello\", 5)\n\tcheck(\" hello \", 5)\n\tcheck(\"  hello  \", 5)\n\tcheck(\"h   o\", 5)\n\tcheck(\"  h   o  \", 5)\n\tcheck(\"         \", 0)\n}\n\nfunc TestCharsLines(t *testing.T) {\n\tchars := ToChars([]byte(\"abcdef\\n가나다\\n\\tdef\"))\n\tcheck := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {\n\t\tlines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop)\n\t\tfmt.Println(lines, overflow)\n\t\tif len(lines) != expectedNumLines || overflow != expectedOverflow {\n\t\t\tt.Errorf(\"Invalid result: %d %v (expected %d %v)\", len(lines), overflow, expectedNumLines, expectedOverflow)\n\t\t}\n\t}\n\n\t// No wrap\n\tcheck(true, 1, 0, 0, 8, 1, true)\n\tcheck(true, 2, 0, 0, 8, 2, true)\n\tcheck(true, 3, 0, 0, 8, 3, false)\n\n\t// Wrap (2)\n\tcheck(true, 4, 2, 0, 8, 4, true)\n\tcheck(true, 5, 2, 0, 8, 5, true)\n\tcheck(true, 6, 2, 0, 8, 6, true)\n\tcheck(true, 7, 2, 0, 8, 7, true)\n\tcheck(true, 8, 2, 0, 8, 8, true)\n\tcheck(true, 9, 2, 0, 8, 9, false)\n\tcheck(true, 9, 2, 0, 1, 8, false) // Smaller tab size\n\n\t// With wrap sign (3 + 1)\n\tcheck(true, 100, 3, 1, 1, 8, false)\n\n\t// With wrap sign (3 + 2)\n\tcheck(true, 100, 3, 2, 1, 10, false)\n\n\t// With wrap sign (3 + 2) and no multi-line\n\tcheck(false, 100, 3, 2, 1, 13, false)\n}\n"
  },
  {
    "path": "internal/fuzzy/find.go",
    "content": "package fuzzy\n\ntype Match struct {\n\tIndex int\n\tStr   string\n\tScore int\n\tPos   []int\n}\n\nfunc Find(pattern []rune, array []string) *Match {\n\tvar result Result\n\tvar pos *[]int\n\tfoundIndex := -1\n\tfor i := range array {\n\t\tinput := ToChars([]byte(array[i]))\n\t\tr, p := fuzzyMatch(&input, pattern)\n\t\tif r.Score > result.Score {\n\t\t\tresult = r\n\t\t\tpos = p\n\t\t\tfoundIndex = i\n\t\t}\n\t}\n\tif foundIndex >= 0 && pos != nil {\n\t\treturn &Match{\n\t\t\tIndex: foundIndex,\n\t\t\tStr:   array[foundIndex],\n\t\t\tScore: result.Score,\n\t\t\tPos:   *pos,\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/fuzzy/fuzzy_test.go",
    "content": "package fuzzy\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFind(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tpattern   string\n\t\tarray     []string\n\t\texpectNil bool\n\t\texpectIdx int\n\t\texpectStr string\n\t}{\n\t\t{\n\t\t\tname:      \"exact match\",\n\t\t\tpattern:   \"hello\",\n\t\t\tarray:     []string{\"world\", \"hello\", \"foo\"},\n\t\t\texpectNil: false,\n\t\t\texpectIdx: 1,\n\t\t\texpectStr: \"hello\",\n\t\t},\n\t\t{\n\t\t\tname:      \"fuzzy match\",\n\t\t\tpattern:   \"hlo\",\n\t\t\tarray:     []string{\"world\", \"hello\", \"foo\"},\n\t\t\texpectNil: false,\n\t\t\texpectIdx: 1,\n\t\t\texpectStr: \"hello\",\n\t\t},\n\t\t{\n\t\t\tname:      \"no match\",\n\t\t\tpattern:   \"xyz\",\n\t\t\tarray:     []string{\"hello\", \"world\", \"foo\"},\n\t\t\texpectNil: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty array\",\n\t\t\tpattern:   \"hello\",\n\t\t\tarray:     []string{},\n\t\t\texpectNil: true,\n\t\t},\n\t\t{\n\t\t\tname:      \"empty pattern\",\n\t\t\tpattern:   \"\",\n\t\t\tarray:     []string{\"hello\", \"world\"},\n\t\t\texpectNil: true, // Empty pattern returns nil\n\t\t},\n\t\t{\n\t\t\tname:      \"single char pattern\",\n\t\t\tpattern:   \"w\",\n\t\t\tarray:     []string{\"hello\", \"world\"},\n\t\t\texpectNil: false,\n\t\t\texpectIdx: 1,\n\t\t\texpectStr: \"world\",\n\t\t},\n\t\t{\n\t\t\tname:      \"best match selected\",\n\t\t\tpattern:   \"foo\",\n\t\t\tarray:     []string{\"foobar\", \"foo\", \"barfoo\"},\n\t\t\texpectNil: false,\n\t\t\texpectIdx: 0, // Algorithm scores \"foobar\" highest due to consecutive bonus\n\t\t\texpectStr: \"foobar\",\n\t\t},\n\t\t{\n\t\t\tname:      \"case insensitive\",\n\t\t\tpattern:   \"hello\",\n\t\t\tarray:     []string{\"HELLO\", \"world\"},\n\t\t\texpectNil: false,\n\t\t\texpectIdx: 0,\n\t\t\texpectStr: \"HELLO\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := Find([]rune(tc.pattern), tc.array)\n\t\t\tif tc.expectNil {\n\t\t\t\tassert.Nil(t, result)\n\t\t\t} else {\n\t\t\t\trequire.NotNil(t, result)\n\t\t\t\tassert.Equal(t, tc.expectIdx, result.Index)\n\t\t\t\tassert.Equal(t, tc.expectStr, result.Str)\n\t\t\t\tassert.GreaterOrEqual(t, result.Score, 0)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFuzzyMatch(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\tpattern     string\n\t\texpectMatch bool\n\t}{\n\t\t{\"exact match\", \"hello\", \"hello\", true},\n\t\t{\"prefix match\", \"hello\", \"hel\", true},\n\t\t{\"suffix match\", \"hello\", \"llo\", true},\n\t\t{\"scattered match\", \"hello\", \"hlo\", true},\n\t\t{\"no match\", \"hello\", \"xyz\", false},\n\t\t{\"empty pattern\", \"hello\", \"\", true},\n\t\t{\"pattern longer than input\", \"hi\", \"hello\", false},\n\t\t{\"case insensitive\", \"HELLO\", \"hello\", true},\n\t\t{\"unicode input\", \"你好世界\", \"好界\", true},\n\t\t{\"unicode no match\", \"你好世界\", \"abc\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tinput := ToChars([]byte(tc.input))\n\t\t\tresult, pos := fuzzyMatch(&input, []rune(tc.pattern))\n\t\t\tif tc.expectMatch {\n\t\t\t\tassert.GreaterOrEqual(t, result.Start, 0, \"Expected match but got Start=-1\")\n\t\t\t\tif tc.pattern != \"\" {\n\t\t\t\t\tassert.NotNil(t, pos)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, -1, result.Start, \"Expected no match but got Start>=0\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFuzzyMatchScoring(t *testing.T) {\n\tinput1 := ToChars([]byte(\"foobar\"))\n\tinput2 := ToChars([]byte(\"foo_bar\"))\n\n\t// Exact prefix should score higher\n\tresult1, _ := fuzzyMatch(&input1, []rune(\"foo\"))\n\tresult2, _ := fuzzyMatch(&input2, []rune(\"foo\"))\n\n\tassert.Greater(t, result1.Score, 0)\n\tassert.Greater(t, result2.Score, 0)\n}\n\nfunc TestNormalizeRune(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    rune\n\t\texpected rune\n\t}{\n\t\t{\"ascii a\", 'a', 'a'},\n\t\t{\"ascii A\", 'A', 'A'},\n\t\t{\"ascii 0\", '0', '0'},\n\t\t{\"a with acute\", 'á', 'a'},\n\t\t{\"a with grave\", 'à', 'a'},\n\t\t{\"a with circumflex\", 'â', 'a'},\n\t\t{\"e with acute\", 'é', 'e'},\n\t\t{\"o with umlaut\", 'ö', 'o'},\n\t\t{\"n with tilde\", 'ñ', 'n'},\n\t\t{\"c with cedilla\", 'ç', 'c'},\n\t\t// Capital letters\n\t\t{\"A with acute\", 'Á', 'A'},\n\t\t{\"O with umlaut\", 'Ö', 'O'},\n\t\t// Characters outside normalization range\n\t\t{\"chinese\", '中', '中'},\n\t\t{\"emoji\", '😀', '😀'},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := normalizeRune(tc.input)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestNormalizeRunes(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []rune\n\t\texpected []rune\n\t}{\n\t\t{\"ascii only\", []rune(\"hello\"), []rune(\"hello\")},\n\t\t{\"with accents\", []rune(\"héllo\"), []rune(\"hello\")},\n\t\t{\"all accents\", []rune(\"àéîõü\"), []rune(\"aeiou\")},\n\t\t{\"mixed\", []rune(\"café\"), []rune(\"cafe\")},\n\t\t{\"empty\", []rune(\"\"), []rune(\"\")},\n\t\t{\"no change needed\", []rune(\"test123\"), []rune(\"test123\")},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := NormalizeRunes(tc.input)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestCharClass(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tchar     rune\n\t\texpected charClass\n\t}{\n\t\t{\"lowercase a\", 'a', charLower},\n\t\t{\"lowercase z\", 'z', charLower},\n\t\t{\"uppercase A\", 'A', charUpper},\n\t\t{\"uppercase Z\", 'Z', charUpper},\n\t\t{\"digit 0\", '0', charNumber},\n\t\t{\"digit 9\", '9', charNumber},\n\t\t{\"space\", ' ', charWhite},\n\t\t{\"tab\", '\\t', charWhite},\n\t\t{\"newline\", '\\n', charWhite},\n\t\t{\"dot delimiter\", '.', charDelimiter},\n\t\t{\"special char\", '@', charNonWord},\n\t\t{\"underscore\", '_', charNonWord},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := charClassOf(tc.char)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestCharClassOfNonAscii(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tchar     rune\n\t\texpected charClass\n\t}{\n\t\t{\"unicode lower\", 'ä', charLower},\n\t\t{\"unicode upper\", 'Ä', charUpper},\n\t\t{\"unicode number\", '①', charNumber},\n\t\t{\"unicode letter\", '中', charLetter},\n\t\t{\"unicode space\", '\\u00A0', charWhite}, // non-breaking space\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := charClassOfNonAscii(tc.char)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestBonusFor(t *testing.T) {\n\t// Test word boundary bonuses\n\tassert.Equal(t, bonusBoundaryWhite, bonusFor(charWhite, charLower))\n\tassert.Equal(t, bonusBoundaryDelimiter, bonusFor(charDelimiter, charLower))\n\tassert.Equal(t, int16(bonusBoundary), bonusFor(charNonWord, charLower))\n\n\t// Test camelCase bonus\n\tassert.Equal(t, int16(bonusCamel123), bonusFor(charLower, charUpper))\n\n\t// Test number after non-number\n\tassert.Equal(t, int16(bonusCamel123), bonusFor(charLower, charNumber))\n\tassert.Equal(t, int16(0), bonusFor(charNumber, charNumber))\n}\n\nfunc TestAsUint16(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    int\n\t\texpected uint16\n\t}{\n\t\t{\"zero\", 0, 0},\n\t\t{\"positive\", 100, 100},\n\t\t{\"max uint16\", 65535, 65535},\n\t\t{\"above max\", 70000, 65535},\n\t\t{\"negative\", -1, 0},\n\t\t{\"large negative\", -1000, 0},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := AsUint16(tc.input)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestStringWidth(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected int\n\t}{\n\t\t{\"ascii\", \"hello\", 5},\n\t\t{\"empty\", \"\", 0},\n\t\t{\"with newline\", \"a\\nb\", 3}, // 2 chars + 1 for newline\n\t\t{\"with cr\", \"a\\rb\", 3},\n\t\t{\"wide chars\", \"你好\", 4}, // each Chinese char is 2 columns\n\t\t{\"mixed\", \"ab你好cd\", 8},  // 4 ascii + 4 wide\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := StringWidth(tc.input)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestRunesWidth(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\trunes         []rune\n\t\tprefixWidth   int\n\t\ttabstop       int\n\t\tlimit         int\n\t\texpectWidth   int\n\t\texpectOverIdx int\n\t}{\n\t\t{\n\t\t\tname:          \"ascii within limit\",\n\t\t\trunes:         []rune(\"hello\"),\n\t\t\tprefixWidth:   0,\n\t\t\ttabstop:       8,\n\t\t\tlimit:         10,\n\t\t\texpectWidth:   5,\n\t\t\texpectOverIdx: -1,\n\t\t},\n\t\t{\n\t\t\tname:          \"ascii exceeds limit\",\n\t\t\trunes:         []rune(\"hello\"),\n\t\t\tprefixWidth:   0,\n\t\t\ttabstop:       8,\n\t\t\tlimit:         3,\n\t\t\texpectWidth:   4,\n\t\t\texpectOverIdx: 3,\n\t\t},\n\t\t{\n\t\t\tname:          \"tab expansion\",\n\t\t\trunes:         []rune(\"\\t\"),\n\t\t\tprefixWidth:   0,\n\t\t\ttabstop:       8,\n\t\t\tlimit:         10,\n\t\t\texpectWidth:   8,\n\t\t\texpectOverIdx: -1,\n\t\t},\n\t\t{\n\t\t\tname:          \"tab with prefix\",\n\t\t\trunes:         []rune(\"\\t\"),\n\t\t\tprefixWidth:   3,\n\t\t\ttabstop:       8,\n\t\t\tlimit:         10,\n\t\t\texpectWidth:   5, // 8 - 3 = 5\n\t\t\texpectOverIdx: -1,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\twidth, overIdx := RunesWidth(tc.runes, tc.prefixWidth, tc.tabstop, tc.limit)\n\t\t\tassert.Equal(t, tc.expectWidth, width)\n\t\t\tassert.Equal(t, tc.expectOverIdx, overIdx)\n\t\t})\n\t}\n}\n\nfunc TestTrySkip(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tinput         string\n\t\tcaseSensitive bool\n\t\tb             byte\n\t\tfrom          int\n\t\texpected      int\n\t}{\n\t\t{\"find at start\", \"hello\", false, 'h', 0, 0},\n\t\t{\"find in middle\", \"hello\", false, 'l', 0, 2},\n\t\t{\"find from offset\", \"hello\", false, 'l', 3, 3},\n\t\t{\"not found\", \"hello\", false, 'x', 0, -1},\n\t\t{\"case insensitive upper\", \"HELLO\", false, 'h', 0, 0},\n\t\t{\"case sensitive no match\", \"HELLO\", true, 'h', 0, -1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tinput := ToChars([]byte(tc.input))\n\t\t\tresult := trySkip(&input, tc.caseSensitive, tc.b, tc.from)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestIsAscii(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\trunes    []rune\n\t\texpected bool\n\t}{\n\t\t{\"ascii only\", []rune(\"hello\"), true},\n\t\t{\"empty\", []rune(\"\"), true},\n\t\t{\"with unicode\", []rune(\"hello世界\"), false},\n\t\t{\"unicode only\", []rune(\"世界\"), false},\n\t\t{\"edge of ascii\", []rune{127}, true},\n\t\t{\"beyond ascii\", []rune{128}, false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := isAscii(tc.runes)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestAsciiFuzzyIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tinput         string\n\t\tpattern       string\n\t\tcaseSensitive bool\n\t\texpectMin     int\n\t\texpectMax     int\n\t}{\n\t\t{\"exact match\", \"hello\", \"hello\", false, 0, 5},\n\t\t{\"partial match\", \"hello world\", \"wor\", false, 5, 9},\n\t\t{\"no match\", \"hello\", \"xyz\", false, -1, -1},\n\t\t{\"case insensitive\", \"HELLO\", \"hel\", false, 0, 3},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tinput := ToChars([]byte(tc.input))\n\t\t\tminIdx, maxIdx := asciiFuzzyIndex(&input, []rune(tc.pattern), tc.caseSensitive)\n\t\t\tassert.Equal(t, tc.expectMin, minIdx)\n\t\t\tif tc.expectMin >= 0 {\n\t\t\t\tassert.GreaterOrEqual(t, maxIdx, tc.expectMax)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/fuzzy/normalize.go",
    "content": "// Normalization of latin script letters\n// Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt\n\npackage fuzzy\n\nvar normalized = map[rune]rune{\n\t0x00E1: 'a', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x0103: 'a', //  WITH BREVE, LATIN SMALL LETTER\n\t0x01CE: 'a', //  WITH CARON, LATIN SMALL LETTER\n\t0x00E2: 'a', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x00E4: 'a', //  WITH DIAERESIS, LATIN SMALL LETTER\n\t0x0227: 'a', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1EA1: 'a', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0201: 'a', //  WITH DOUBLE GRAVE, LATIN SMALL LETTER\n\t0x00E0: 'a', //  WITH GRAVE, LATIN SMALL LETTER\n\t0x1EA3: 'a', //  WITH HOOK ABOVE, LATIN SMALL LETTER\n\t0x0203: 'a', //  WITH INVERTED BREVE, LATIN SMALL LETTER\n\t0x0101: 'a', //  WITH MACRON, LATIN SMALL LETTER\n\t0x0105: 'a', //  WITH OGONEK, LATIN SMALL LETTER\n\t0x1E9A: 'a', //  WITH RIGHT HALF RING, LATIN SMALL LETTER\n\t0x00E5: 'a', //  WITH RING ABOVE, LATIN SMALL LETTER\n\t0x1E01: 'a', //  WITH RING BELOW, LATIN SMALL LETTER\n\t0x00E3: 'a', //  WITH TILDE, LATIN SMALL LETTER\n\t0x0363: 'a', // , COMBINING LATIN SMALL LETTER\n\t0x0250: 'a', // , LATIN SMALL LETTER TURNED\n\t0x1E03: 'b', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1E05: 'b', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0253: 'b', //  WITH HOOK, LATIN SMALL LETTER\n\t0x1E07: 'b', //  WITH LINE BELOW, LATIN SMALL LETTER\n\t0x0180: 'b', //  WITH STROKE, LATIN SMALL LETTER\n\t0x0183: 'b', //  WITH TOPBAR, LATIN SMALL LETTER\n\t0x0107: 'c', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x010D: 'c', //  WITH CARON, LATIN SMALL LETTER\n\t0x00E7: 'c', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x0109: 'c', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x0255: 'c', //  WITH CURL, LATIN SMALL LETTER\n\t0x010B: 'c', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x0188: 'c', //  WITH HOOK, LATIN SMALL LETTER\n\t0x023C: 'c', //  WITH STROKE, LATIN SMALL LETTER\n\t0x0368: 'c', // , COMBINING LATIN SMALL LETTER\n\t0x0297: 'c', // , LATIN LETTER STRETCHED\n\t0x2184: 'c', // , LATIN SMALL LETTER REVERSED\n\t0x010F: 'd', //  WITH CARON, LATIN SMALL LETTER\n\t0x1E11: 'd', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x1E13: 'd', //  WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER\n\t0x0221: 'd', //  WITH CURL, LATIN SMALL LETTER\n\t0x1E0B: 'd', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1E0D: 'd', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0257: 'd', //  WITH HOOK, LATIN SMALL LETTER\n\t0x1E0F: 'd', //  WITH LINE BELOW, LATIN SMALL LETTER\n\t0x0111: 'd', //  WITH STROKE, LATIN SMALL LETTER\n\t0x0256: 'd', //  WITH TAIL, LATIN SMALL LETTER\n\t0x018C: 'd', //  WITH TOPBAR, LATIN SMALL LETTER\n\t0x0369: 'd', // , COMBINING LATIN SMALL LETTER\n\t0x00E9: 'e', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x0115: 'e', //  WITH BREVE, LATIN SMALL LETTER\n\t0x011B: 'e', //  WITH CARON, LATIN SMALL LETTER\n\t0x0229: 'e', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x1E19: 'e', //  WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER\n\t0x00EA: 'e', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x00EB: 'e', //  WITH DIAERESIS, LATIN SMALL LETTER\n\t0x0117: 'e', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1EB9: 'e', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0205: 'e', //  WITH DOUBLE GRAVE, LATIN SMALL LETTER\n\t0x00E8: 'e', //  WITH GRAVE, LATIN SMALL LETTER\n\t0x1EBB: 'e', //  WITH HOOK ABOVE, LATIN SMALL LETTER\n\t0x025D: 'e', //  WITH HOOK, LATIN SMALL LETTER REVERSED OPEN\n\t0x0207: 'e', //  WITH INVERTED BREVE, LATIN SMALL LETTER\n\t0x0113: 'e', //  WITH MACRON, LATIN SMALL LETTER\n\t0x0119: 'e', //  WITH OGONEK, LATIN SMALL LETTER\n\t0x0247: 'e', //  WITH STROKE, LATIN SMALL LETTER\n\t0x1E1B: 'e', //  WITH TILDE BELOW, LATIN SMALL LETTER\n\t0x1EBD: 'e', //  WITH TILDE, LATIN SMALL LETTER\n\t0x0364: 'e', // , COMBINING LATIN SMALL LETTER\n\t0x029A: 'e', // , LATIN SMALL LETTER CLOSED OPEN\n\t0x025E: 'e', // , LATIN SMALL LETTER CLOSED REVERSED OPEN\n\t0x025B: 'e', // , LATIN SMALL LETTER OPEN\n\t0x0258: 'e', // , LATIN SMALL LETTER REVERSED\n\t0x025C: 'e', // , LATIN SMALL LETTER REVERSED OPEN\n\t0x01DD: 'e', // , LATIN SMALL LETTER TURNED\n\t0x1D08: 'e', // , LATIN SMALL LETTER TURNED OPEN\n\t0x1E1F: 'f', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x0192: 'f', //  WITH HOOK, LATIN SMALL LETTER\n\t0x01F5: 'g', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x011F: 'g', //  WITH BREVE, LATIN SMALL LETTER\n\t0x01E7: 'g', //  WITH CARON, LATIN SMALL LETTER\n\t0x0123: 'g', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x011D: 'g', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x0121: 'g', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x0260: 'g', //  WITH HOOK, LATIN SMALL LETTER\n\t0x1E21: 'g', //  WITH MACRON, LATIN SMALL LETTER\n\t0x01E5: 'g', //  WITH STROKE, LATIN SMALL LETTER\n\t0x0261: 'g', // , LATIN SMALL LETTER SCRIPT\n\t0x1E2B: 'h', //  WITH BREVE BELOW, LATIN SMALL LETTER\n\t0x021F: 'h', //  WITH CARON, LATIN SMALL LETTER\n\t0x1E29: 'h', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x0125: 'h', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x1E27: 'h', //  WITH DIAERESIS, LATIN SMALL LETTER\n\t0x1E23: 'h', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1E25: 'h', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x02AE: 'h', //  WITH FISHHOOK, LATIN SMALL LETTER TURNED\n\t0x0266: 'h', //  WITH HOOK, LATIN SMALL LETTER\n\t0x1E96: 'h', //  WITH LINE BELOW, LATIN SMALL LETTER\n\t0x0127: 'h', //  WITH STROKE, LATIN SMALL LETTER\n\t0x036A: 'h', // , COMBINING LATIN SMALL LETTER\n\t0x0265: 'h', // , LATIN SMALL LETTER TURNED\n\t0x2095: 'h', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x00ED: 'i', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x012D: 'i', //  WITH BREVE, LATIN SMALL LETTER\n\t0x01D0: 'i', //  WITH CARON, LATIN SMALL LETTER\n\t0x00EE: 'i', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x00EF: 'i', //  WITH DIAERESIS, LATIN SMALL LETTER\n\t0x1ECB: 'i', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0209: 'i', //  WITH DOUBLE GRAVE, LATIN SMALL LETTER\n\t0x00EC: 'i', //  WITH GRAVE, LATIN SMALL LETTER\n\t0x1EC9: 'i', //  WITH HOOK ABOVE, LATIN SMALL LETTER\n\t0x020B: 'i', //  WITH INVERTED BREVE, LATIN SMALL LETTER\n\t0x012B: 'i', //  WITH MACRON, LATIN SMALL LETTER\n\t0x012F: 'i', //  WITH OGONEK, LATIN SMALL LETTER\n\t0x0268: 'i', //  WITH STROKE, LATIN SMALL LETTER\n\t0x1E2D: 'i', //  WITH TILDE BELOW, LATIN SMALL LETTER\n\t0x0129: 'i', //  WITH TILDE, LATIN SMALL LETTER\n\t0x0365: 'i', // , COMBINING LATIN SMALL LETTER\n\t0x0131: 'i', // , LATIN SMALL LETTER DOTLESS\n\t0x1D09: 'i', // , LATIN SMALL LETTER TURNED\n\t0x1D62: 'i', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x2071: 'i', // , SUPERSCRIPT LATIN SMALL LETTER\n\t0x01F0: 'j', //  WITH CARON, LATIN SMALL LETTER\n\t0x0135: 'j', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x029D: 'j', //  WITH CROSSED-TAIL, LATIN SMALL LETTER\n\t0x0249: 'j', //  WITH STROKE, LATIN SMALL LETTER\n\t0x025F: 'j', //  WITH STROKE, LATIN SMALL LETTER DOTLESS\n\t0x0237: 'j', // , LATIN SMALL LETTER DOTLESS\n\t0x1E31: 'k', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x01E9: 'k', //  WITH CARON, LATIN SMALL LETTER\n\t0x0137: 'k', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x1E33: 'k', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0199: 'k', //  WITH HOOK, LATIN SMALL LETTER\n\t0x1E35: 'k', //  WITH LINE BELOW, LATIN SMALL LETTER\n\t0x029E: 'k', // , LATIN SMALL LETTER TURNED\n\t0x2096: 'k', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x013A: 'l', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x019A: 'l', //  WITH BAR, LATIN SMALL LETTER\n\t0x026C: 'l', //  WITH BELT, LATIN SMALL LETTER\n\t0x013E: 'l', //  WITH CARON, LATIN SMALL LETTER\n\t0x013C: 'l', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x1E3D: 'l', //  WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER\n\t0x0234: 'l', //  WITH CURL, LATIN SMALL LETTER\n\t0x1E37: 'l', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x1E3B: 'l', //  WITH LINE BELOW, LATIN SMALL LETTER\n\t0x0140: 'l', //  WITH MIDDLE DOT, LATIN SMALL LETTER\n\t0x026B: 'l', //  WITH MIDDLE TILDE, LATIN SMALL LETTER\n\t0x026D: 'l', //  WITH RETROFLEX HOOK, LATIN SMALL LETTER\n\t0x0142: 'l', //  WITH STROKE, LATIN SMALL LETTER\n\t0x2097: 'l', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x1E3F: 'm', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x1E41: 'm', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1E43: 'm', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0271: 'm', //  WITH HOOK, LATIN SMALL LETTER\n\t0x0270: 'm', //  WITH LONG LEG, LATIN SMALL LETTER TURNED\n\t0x036B: 'm', // , COMBINING LATIN SMALL LETTER\n\t0x1D1F: 'm', // , LATIN SMALL LETTER SIDEWAYS TURNED\n\t0x026F: 'm', // , LATIN SMALL LETTER TURNED\n\t0x2098: 'm', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x0144: 'n', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x0148: 'n', //  WITH CARON, LATIN SMALL LETTER\n\t0x0146: 'n', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x1E4B: 'n', //  WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER\n\t0x0235: 'n', //  WITH CURL, LATIN SMALL LETTER\n\t0x1E45: 'n', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1E47: 'n', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x01F9: 'n', //  WITH GRAVE, LATIN SMALL LETTER\n\t0x0272: 'n', //  WITH LEFT HOOK, LATIN SMALL LETTER\n\t0x1E49: 'n', //  WITH LINE BELOW, LATIN SMALL LETTER\n\t0x019E: 'n', //  WITH LONG RIGHT LEG, LATIN SMALL LETTER\n\t0x0273: 'n', //  WITH RETROFLEX HOOK, LATIN SMALL LETTER\n\t0x00F1: 'n', //  WITH TILDE, LATIN SMALL LETTER\n\t0x2099: 'n', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x00F3: 'o', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x014F: 'o', //  WITH BREVE, LATIN SMALL LETTER\n\t0x01D2: 'o', //  WITH CARON, LATIN SMALL LETTER\n\t0x00F4: 'o', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x00F6: 'o', //  WITH DIAERESIS, LATIN SMALL LETTER\n\t0x022F: 'o', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1ECD: 'o', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0151: 'o', //  WITH DOUBLE ACUTE, LATIN SMALL LETTER\n\t0x020D: 'o', //  WITH DOUBLE GRAVE, LATIN SMALL LETTER\n\t0x00F2: 'o', //  WITH GRAVE, LATIN SMALL LETTER\n\t0x1ECF: 'o', //  WITH HOOK ABOVE, LATIN SMALL LETTER\n\t0x01A1: 'o', //  WITH HORN, LATIN SMALL LETTER\n\t0x020F: 'o', //  WITH INVERTED BREVE, LATIN SMALL LETTER\n\t0x014D: 'o', //  WITH MACRON, LATIN SMALL LETTER\n\t0x01EB: 'o', //  WITH OGONEK, LATIN SMALL LETTER\n\t0x00F8: 'o', //  WITH STROKE, LATIN SMALL LETTER\n\t0x1D13: 'o', //  WITH STROKE, LATIN SMALL LETTER SIDEWAYS\n\t0x00F5: 'o', //  WITH TILDE, LATIN SMALL LETTER\n\t0x0366: 'o', // , COMBINING LATIN SMALL LETTER\n\t0x0275: 'o', // , LATIN SMALL LETTER BARRED\n\t0x1D17: 'o', // , LATIN SMALL LETTER BOTTOM HALF\n\t0x0254: 'o', // , LATIN SMALL LETTER OPEN\n\t0x1D11: 'o', // , LATIN SMALL LETTER SIDEWAYS\n\t0x1D12: 'o', // , LATIN SMALL LETTER SIDEWAYS OPEN\n\t0x1D16: 'o', // , LATIN SMALL LETTER TOP HALF\n\t0x1E55: 'p', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x1E57: 'p', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x01A5: 'p', //  WITH HOOK, LATIN SMALL LETTER\n\t0x209A: 'p', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x024B: 'q', //  WITH HOOK TAIL, LATIN SMALL LETTER\n\t0x02A0: 'q', //  WITH HOOK, LATIN SMALL LETTER\n\t0x0155: 'r', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x0159: 'r', //  WITH CARON, LATIN SMALL LETTER\n\t0x0157: 'r', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x1E59: 'r', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1E5B: 'r', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0211: 'r', //  WITH DOUBLE GRAVE, LATIN SMALL LETTER\n\t0x027E: 'r', //  WITH FISHHOOK, LATIN SMALL LETTER\n\t0x027F: 'r', //  WITH FISHHOOK, LATIN SMALL LETTER REVERSED\n\t0x027B: 'r', //  WITH HOOK, LATIN SMALL LETTER TURNED\n\t0x0213: 'r', //  WITH INVERTED BREVE, LATIN SMALL LETTER\n\t0x1E5F: 'r', //  WITH LINE BELOW, LATIN SMALL LETTER\n\t0x027C: 'r', //  WITH LONG LEG, LATIN SMALL LETTER\n\t0x027A: 'r', //  WITH LONG LEG, LATIN SMALL LETTER TURNED\n\t0x024D: 'r', //  WITH STROKE, LATIN SMALL LETTER\n\t0x027D: 'r', //  WITH TAIL, LATIN SMALL LETTER\n\t0x036C: 'r', // , COMBINING LATIN SMALL LETTER\n\t0x0279: 'r', // , LATIN SMALL LETTER TURNED\n\t0x1D63: 'r', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x015B: 's', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x0161: 's', //  WITH CARON, LATIN SMALL LETTER\n\t0x015F: 's', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x015D: 's', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x0219: 's', //  WITH COMMA BELOW, LATIN SMALL LETTER\n\t0x1E61: 's', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1E9B: 's', //  WITH DOT ABOVE, LATIN SMALL LETTER LONG\n\t0x1E63: 's', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0282: 's', //  WITH HOOK, LATIN SMALL LETTER\n\t0x023F: 's', //  WITH SWASH TAIL, LATIN SMALL LETTER\n\t0x017F: 's', // , LATIN SMALL LETTER LONG\n\t0x00DF: 's', // , LATIN SMALL LETTER SHARP\n\t0x209B: 's', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x0165: 't', //  WITH CARON, LATIN SMALL LETTER\n\t0x0163: 't', //  WITH CEDILLA, LATIN SMALL LETTER\n\t0x1E71: 't', //  WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER\n\t0x021B: 't', //  WITH COMMA BELOW, LATIN SMALL LETTER\n\t0x0236: 't', //  WITH CURL, LATIN SMALL LETTER\n\t0x1E97: 't', //  WITH DIAERESIS, LATIN SMALL LETTER\n\t0x1E6B: 't', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1E6D: 't', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x01AD: 't', //  WITH HOOK, LATIN SMALL LETTER\n\t0x1E6F: 't', //  WITH LINE BELOW, LATIN SMALL LETTER\n\t0x01AB: 't', //  WITH PALATAL HOOK, LATIN SMALL LETTER\n\t0x0288: 't', //  WITH RETROFLEX HOOK, LATIN SMALL LETTER\n\t0x0167: 't', //  WITH STROKE, LATIN SMALL LETTER\n\t0x036D: 't', // , COMBINING LATIN SMALL LETTER\n\t0x0287: 't', // , LATIN SMALL LETTER TURNED\n\t0x209C: 't', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x0289: 'u', //  BAR, LATIN SMALL LETTER\n\t0x00FA: 'u', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x016D: 'u', //  WITH BREVE, LATIN SMALL LETTER\n\t0x01D4: 'u', //  WITH CARON, LATIN SMALL LETTER\n\t0x1E77: 'u', //  WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER\n\t0x00FB: 'u', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x1E73: 'u', //  WITH DIAERESIS BELOW, LATIN SMALL LETTER\n\t0x00FC: 'u', //  WITH DIAERESIS, LATIN SMALL LETTER\n\t0x1EE5: 'u', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0171: 'u', //  WITH DOUBLE ACUTE, LATIN SMALL LETTER\n\t0x0215: 'u', //  WITH DOUBLE GRAVE, LATIN SMALL LETTER\n\t0x00F9: 'u', //  WITH GRAVE, LATIN SMALL LETTER\n\t0x1EE7: 'u', //  WITH HOOK ABOVE, LATIN SMALL LETTER\n\t0x01B0: 'u', //  WITH HORN, LATIN SMALL LETTER\n\t0x0217: 'u', //  WITH INVERTED BREVE, LATIN SMALL LETTER\n\t0x016B: 'u', //  WITH MACRON, LATIN SMALL LETTER\n\t0x0173: 'u', //  WITH OGONEK, LATIN SMALL LETTER\n\t0x016F: 'u', //  WITH RING ABOVE, LATIN SMALL LETTER\n\t0x1E75: 'u', //  WITH TILDE BELOW, LATIN SMALL LETTER\n\t0x0169: 'u', //  WITH TILDE, LATIN SMALL LETTER\n\t0x0367: 'u', // , COMBINING LATIN SMALL LETTER\n\t0x1D1D: 'u', // , LATIN SMALL LETTER SIDEWAYS\n\t0x1D1E: 'u', // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED\n\t0x1D64: 'u', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x1E7F: 'v', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x028B: 'v', //  WITH HOOK, LATIN SMALL LETTER\n\t0x1E7D: 'v', //  WITH TILDE, LATIN SMALL LETTER\n\t0x036E: 'v', // , COMBINING LATIN SMALL LETTER\n\t0x028C: 'v', // , LATIN SMALL LETTER TURNED\n\t0x1D65: 'v', // , LATIN SUBSCRIPT SMALL LETTER\n\t0x1E83: 'w', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x0175: 'w', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x1E85: 'w', //  WITH DIAERESIS, LATIN SMALL LETTER\n\t0x1E87: 'w', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1E89: 'w', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x1E81: 'w', //  WITH GRAVE, LATIN SMALL LETTER\n\t0x1E98: 'w', //  WITH RING ABOVE, LATIN SMALL LETTER\n\t0x028D: 'w', // , LATIN SMALL LETTER TURNED\n\t0x1E8D: 'x', //  WITH DIAERESIS, LATIN SMALL LETTER\n\t0x1E8B: 'x', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x036F: 'x', // , COMBINING LATIN SMALL LETTER\n\t0x00FD: 'y', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x0177: 'y', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x00FF: 'y', //  WITH DIAERESIS, LATIN SMALL LETTER\n\t0x1E8F: 'y', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1EF5: 'y', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x1EF3: 'y', //  WITH GRAVE, LATIN SMALL LETTER\n\t0x1EF7: 'y', //  WITH HOOK ABOVE, LATIN SMALL LETTER\n\t0x01B4: 'y', //  WITH HOOK, LATIN SMALL LETTER\n\t0x0233: 'y', //  WITH MACRON, LATIN SMALL LETTER\n\t0x1E99: 'y', //  WITH RING ABOVE, LATIN SMALL LETTER\n\t0x024F: 'y', //  WITH STROKE, LATIN SMALL LETTER\n\t0x1EF9: 'y', //  WITH TILDE, LATIN SMALL LETTER\n\t0x028E: 'y', // , LATIN SMALL LETTER TURNED\n\t0x017A: 'z', //  WITH ACUTE, LATIN SMALL LETTER\n\t0x017E: 'z', //  WITH CARON, LATIN SMALL LETTER\n\t0x1E91: 'z', //  WITH CIRCUMFLEX, LATIN SMALL LETTER\n\t0x0291: 'z', //  WITH CURL, LATIN SMALL LETTER\n\t0x017C: 'z', //  WITH DOT ABOVE, LATIN SMALL LETTER\n\t0x1E93: 'z', //  WITH DOT BELOW, LATIN SMALL LETTER\n\t0x0225: 'z', //  WITH HOOK, LATIN SMALL LETTER\n\t0x1E95: 'z', //  WITH LINE BELOW, LATIN SMALL LETTER\n\t0x0290: 'z', //  WITH RETROFLEX HOOK, LATIN SMALL LETTER\n\t0x01B6: 'z', //  WITH STROKE, LATIN SMALL LETTER\n\t0x0240: 'z', //  WITH SWASH TAIL, LATIN SMALL LETTER\n\t0x0251: 'a', // , latin small letter script\n\t0x00C1: 'A', //  WITH ACUTE, LATIN CAPITAL LETTER\n\t0x00C2: 'A', //  WITH CIRCUMFLEX, LATIN CAPITAL LETTER\n\t0x00C4: 'A', //  WITH DIAERESIS, LATIN CAPITAL LETTER\n\t0x00C0: 'A', //  WITH GRAVE, LATIN CAPITAL LETTER\n\t0x00C5: 'A', //  WITH RING ABOVE, LATIN CAPITAL LETTER\n\t0x023A: 'A', //  WITH STROKE, LATIN CAPITAL LETTER\n\t0x00C3: 'A', //  WITH TILDE, LATIN CAPITAL LETTER\n\t0x1D00: 'A', // , LATIN LETTER SMALL CAPITAL\n\t0x0181: 'B', //  WITH HOOK, LATIN CAPITAL LETTER\n\t0x0243: 'B', //  WITH STROKE, LATIN CAPITAL LETTER\n\t0x0299: 'B', // , LATIN LETTER SMALL CAPITAL\n\t0x1D03: 'B', // , LATIN LETTER SMALL CAPITAL BARRED\n\t0x00C7: 'C', //  WITH CEDILLA, LATIN CAPITAL LETTER\n\t0x023B: 'C', //  WITH STROKE, LATIN CAPITAL LETTER\n\t0x1D04: 'C', // , LATIN LETTER SMALL CAPITAL\n\t0x018A: 'D', //  WITH HOOK, LATIN CAPITAL LETTER\n\t0x0189: 'D', // , LATIN CAPITAL LETTER AFRICAN\n\t0x1D05: 'D', // , LATIN LETTER SMALL CAPITAL\n\t0x00C9: 'E', //  WITH ACUTE, LATIN CAPITAL LETTER\n\t0x00CA: 'E', //  WITH CIRCUMFLEX, LATIN CAPITAL LETTER\n\t0x00CB: 'E', //  WITH DIAERESIS, LATIN CAPITAL LETTER\n\t0x00C8: 'E', //  WITH GRAVE, LATIN CAPITAL LETTER\n\t0x0246: 'E', //  WITH STROKE, LATIN CAPITAL LETTER\n\t0x0190: 'E', // , LATIN CAPITAL LETTER OPEN\n\t0x018E: 'E', // , LATIN CAPITAL LETTER REVERSED\n\t0x1D07: 'E', // , LATIN LETTER SMALL CAPITAL\n\t0x0193: 'G', //  WITH HOOK, LATIN CAPITAL LETTER\n\t0x029B: 'G', //  WITH HOOK, LATIN LETTER SMALL CAPITAL\n\t0x0262: 'G', // , LATIN LETTER SMALL CAPITAL\n\t0x029C: 'H', // , LATIN LETTER SMALL CAPITAL\n\t0x00CD: 'I', //  WITH ACUTE, LATIN CAPITAL LETTER\n\t0x00CE: 'I', //  WITH CIRCUMFLEX, LATIN CAPITAL LETTER\n\t0x00CF: 'I', //  WITH DIAERESIS, LATIN CAPITAL LETTER\n\t0x0130: 'I', //  WITH DOT ABOVE, LATIN CAPITAL LETTER\n\t0x00CC: 'I', //  WITH GRAVE, LATIN CAPITAL LETTER\n\t0x0197: 'I', //  WITH STROKE, LATIN CAPITAL LETTER\n\t0x026A: 'I', // , LATIN LETTER SMALL CAPITAL\n\t0x0248: 'J', //  WITH STROKE, LATIN CAPITAL LETTER\n\t0x1D0A: 'J', // , LATIN LETTER SMALL CAPITAL\n\t0x1D0B: 'K', // , LATIN LETTER SMALL CAPITAL\n\t0x023D: 'L', //  WITH BAR, LATIN CAPITAL LETTER\n\t0x1D0C: 'L', //  WITH STROKE, LATIN LETTER SMALL CAPITAL\n\t0x029F: 'L', // , LATIN LETTER SMALL CAPITAL\n\t0x019C: 'M', // , LATIN CAPITAL LETTER TURNED\n\t0x1D0D: 'M', // , LATIN LETTER SMALL CAPITAL\n\t0x019D: 'N', //  WITH LEFT HOOK, LATIN CAPITAL LETTER\n\t0x0220: 'N', //  WITH LONG RIGHT LEG, LATIN CAPITAL LETTER\n\t0x00D1: 'N', //  WITH TILDE, LATIN CAPITAL LETTER\n\t0x0274: 'N', // , LATIN LETTER SMALL CAPITAL\n\t0x1D0E: 'N', // , LATIN LETTER SMALL CAPITAL REVERSED\n\t0x00D3: 'O', //  WITH ACUTE, LATIN CAPITAL LETTER\n\t0x00D4: 'O', //  WITH CIRCUMFLEX, LATIN CAPITAL LETTER\n\t0x00D6: 'O', //  WITH DIAERESIS, LATIN CAPITAL LETTER\n\t0x00D2: 'O', //  WITH GRAVE, LATIN CAPITAL LETTER\n\t0x019F: 'O', //  WITH MIDDLE TILDE, LATIN CAPITAL LETTER\n\t0x00D8: 'O', //  WITH STROKE, LATIN CAPITAL LETTER\n\t0x00D5: 'O', //  WITH TILDE, LATIN CAPITAL LETTER\n\t0x0186: 'O', // , LATIN CAPITAL LETTER OPEN\n\t0x1D0F: 'O', // , LATIN LETTER SMALL CAPITAL\n\t0x1D10: 'O', // , LATIN LETTER SMALL CAPITAL OPEN\n\t0x1D18: 'P', // , LATIN LETTER SMALL CAPITAL\n\t0x024A: 'Q', //  WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL\n\t0x024C: 'R', //  WITH STROKE, LATIN CAPITAL LETTER\n\t0x0280: 'R', // , LATIN LETTER SMALL CAPITAL\n\t0x0281: 'R', // , LATIN LETTER SMALL CAPITAL INVERTED\n\t0x1D19: 'R', // , LATIN LETTER SMALL CAPITAL REVERSED\n\t0x1D1A: 'R', // , LATIN LETTER SMALL CAPITAL TURNED\n\t0x023E: 'T', //  WITH DIAGONAL STROKE, LATIN CAPITAL LETTER\n\t0x01AE: 'T', //  WITH RETROFLEX HOOK, LATIN CAPITAL LETTER\n\t0x1D1B: 'T', // , LATIN LETTER SMALL CAPITAL\n\t0x0244: 'U', //  BAR, LATIN CAPITAL LETTER\n\t0x00DA: 'U', //  WITH ACUTE, LATIN CAPITAL LETTER\n\t0x00DB: 'U', //  WITH CIRCUMFLEX, LATIN CAPITAL LETTER\n\t0x00DC: 'U', //  WITH DIAERESIS, LATIN CAPITAL LETTER\n\t0x00D9: 'U', //  WITH GRAVE, LATIN CAPITAL LETTER\n\t0x1D1C: 'U', // , LATIN LETTER SMALL CAPITAL\n\t0x01B2: 'V', //  WITH HOOK, LATIN CAPITAL LETTER\n\t0x0245: 'V', // , LATIN CAPITAL LETTER TURNED\n\t0x1D20: 'V', // , LATIN LETTER SMALL CAPITAL\n\t0x1D21: 'W', // , LATIN LETTER SMALL CAPITAL\n\t0x00DD: 'Y', //  WITH ACUTE, LATIN CAPITAL LETTER\n\t0x0178: 'Y', //  WITH DIAERESIS, LATIN CAPITAL LETTER\n\t0x024E: 'Y', //  WITH STROKE, LATIN CAPITAL LETTER\n\t0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL\n\t0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL\n\n\t'Ắ': 'A',\n\t'Ấ': 'A',\n\t'Ằ': 'A',\n\t'Ầ': 'A',\n\t'Ẳ': 'A',\n\t'Ẩ': 'A',\n\t'Ẵ': 'A',\n\t'Ẫ': 'A',\n\t'Ặ': 'A',\n\t'Ậ': 'A',\n\n\t'ắ': 'a',\n\t'ấ': 'a',\n\t'ằ': 'a',\n\t'ầ': 'a',\n\t'ẳ': 'a',\n\t'ẩ': 'a',\n\t'ẵ': 'a',\n\t'ẫ': 'a',\n\t'ặ': 'a',\n\t'ậ': 'a',\n\n\t'Ế': 'E',\n\t'Ề': 'E',\n\t'Ể': 'E',\n\t'Ễ': 'E',\n\t'Ệ': 'E',\n\n\t'ế': 'e',\n\t'ề': 'e',\n\t'ể': 'e',\n\t'ễ': 'e',\n\t'ệ': 'e',\n\n\t'Ố': 'O',\n\t'Ớ': 'O',\n\t'Ồ': 'O',\n\t'Ờ': 'O',\n\t'Ổ': 'O',\n\t'Ở': 'O',\n\t'Ỗ': 'O',\n\t'Ỡ': 'O',\n\t'Ộ': 'O',\n\t'Ợ': 'O',\n\n\t'ố': 'o',\n\t'ớ': 'o',\n\t'ồ': 'o',\n\t'ờ': 'o',\n\t'ổ': 'o',\n\t'ở': 'o',\n\t'ỗ': 'o',\n\t'ỡ': 'o',\n\t'ộ': 'o',\n\t'ợ': 'o',\n\n\t'Ứ': 'U',\n\t'Ừ': 'U',\n\t'Ử': 'U',\n\t'Ữ': 'U',\n\t'Ự': 'U',\n\n\t'ứ': 'u',\n\t'ừ': 'u',\n\t'ử': 'u',\n\t'ữ': 'u',\n\t'ự': 'u',\n}\n\n// NormalizeRunes normalizes latin script letters\nfunc NormalizeRunes(runes []rune) []rune {\n\tret := make([]rune, len(runes))\n\tcopy(ret, runes)\n\tfor idx, r := range runes {\n\t\tif r < 0x00C0 || r > 0x2184 {\n\t\t\tcontinue\n\t\t}\n\t\tn := normalized[r]\n\t\tif n > 0 {\n\t\t\tret[idx] = normalized[r]\n\t\t}\n\t}\n\treturn ret\n}\n"
  },
  {
    "path": "internal/fuzzy/utils.go",
    "content": "package fuzzy\n\nimport (\n\t\"math\"\n\t\"strings\"\n\n\t\"github.com/rivo/uniseg\"\n)\n\nfunc AsUint16(val int) uint16 {\n\tif val > math.MaxUint16 {\n\t\treturn math.MaxUint16\n\t} else if val < 0 {\n\t\treturn 0\n\t}\n\treturn uint16(val)\n}\n\n// StringWidth returns string width where each CR/LF character takes 1 column\nfunc StringWidth(s string) int {\n\treturn uniseg.StringWidth(s) + strings.Count(s, \"\\n\") + strings.Count(s, \"\\r\")\n}\n\n// RunesWidth returns runes width\nfunc RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {\n\twidth := 0\n\tgr := uniseg.NewGraphemes(string(runes))\n\tidx := 0\n\tfor gr.Next() {\n\t\trs := gr.Runes()\n\t\tvar w int\n\t\tif len(rs) == 1 && rs[0] == '\\t' {\n\t\t\tw = tabstop - (prefixWidth+width)%tabstop\n\t\t} else {\n\t\t\tw = StringWidth(string(rs))\n\t\t}\n\t\twidth += w\n\t\tif width > limit {\n\t\t\treturn width, idx\n\t\t}\n\t\tidx += len(rs)\n\t}\n\treturn width, -1\n}\n"
  },
  {
    "path": "internal/ident/ident.go",
    "content": "package ident\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar Ident = \"  \"\nvar IdentBytes []byte\nvar IdentWidth int\n\nfunc init() {\n\tidentValue, ok := os.LookupEnv(\"FX_INDENT\")\n\tif ok {\n\t\tidentInt, err := strconv.Atoi(identValue)\n\t\tif err == nil {\n\t\t\tIdent = strings.Repeat(\" \", identInt)\n\t\t} else {\n\t\t\tIdent = identValue\n\t\t}\n\t}\n\tfor _, r := range Ident {\n\t\tif r == '\\n' {\n\t\t\tcontinue\n\t\t}\n\t\tif r == '\\t' {\n\t\t\tIdentBytes = append(IdentBytes, ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ')\n\t\t\tIdentWidth += 8\n\t\t\tcontinue\n\t\t}\n\t\tIdentBytes = append(IdentBytes, byte(r))\n\t\tIdentWidth++\n\t}\n}\n"
  },
  {
    "path": "internal/jsonpath/path.go",
    "content": "package jsonpath\n\nimport (\n\t\"regexp\"\n\t\"strconv\"\n\t\"unicode\"\n)\n\ntype state int\n\nconst (\n\tstart state = iota\n\tunknown\n\tpropOrIndex\n\tprop\n\tindex\n\tindexEnd\n\tnumber\n\tdoubleQuote\n\tdoubleQuoteEscape\n\tsingleQuote\n\tsingleQuoteEscape\n)\n\nfunc Split(p string) ([]any, bool) {\n\tpath := make([]any, 0)\n\ts := \"\"\n\tstate := start\n\tfor _, ch := range p {\n\t\tswitch state {\n\n\t\tcase start:\n\t\t\tswitch {\n\t\t\tcase ch == 'x':\n\t\t\t\tstate = unknown\n\t\t\tcase ch == '.':\n\t\t\t\tstate = propOrIndex\n\t\t\tcase ch == '[':\n\t\t\t\tstate = index\n\t\t\tdefault:\n\t\t\t\treturn path, false\n\t\t\t}\n\n\t\tcase unknown:\n\t\t\tswitch {\n\t\t\tcase ch == '.':\n\t\t\t\tstate = prop\n\t\t\t\ts = \"\"\n\t\t\tcase ch == '[':\n\t\t\t\tstate = index\n\t\t\t\ts = \"\"\n\t\t\tdefault:\n\t\t\t\treturn path, false\n\t\t\t}\n\n\t\tcase propOrIndex:\n\t\t\tswitch {\n\t\t\tcase isProp(ch):\n\t\t\t\tstate = prop\n\t\t\t\ts = string(ch)\n\t\t\tcase ch == '[':\n\t\t\t\tstate = index\n\t\t\tdefault:\n\t\t\t\treturn path, false\n\t\t\t}\n\n\t\tcase prop:\n\t\t\tswitch {\n\t\t\tcase isProp(ch):\n\t\t\t\ts += string(ch)\n\t\t\tcase ch == '.':\n\t\t\t\tstate = prop\n\t\t\t\tpath = append(path, s)\n\t\t\t\ts = \"\"\n\t\t\tcase ch == '[':\n\t\t\t\tstate = index\n\t\t\t\tpath = append(path, s)\n\t\t\t\ts = \"\"\n\t\t\tdefault:\n\t\t\t\treturn path, false\n\t\t\t}\n\n\t\tcase index:\n\t\t\tswitch {\n\t\t\tcase unicode.IsDigit(ch):\n\t\t\t\tstate = number\n\t\t\t\ts = string(ch)\n\t\t\tcase ch == '\"':\n\t\t\t\tstate = doubleQuote\n\t\t\t\ts = \"\"\n\t\t\tcase ch == '\\'':\n\t\t\t\tstate = singleQuote\n\t\t\t\ts = \"\"\n\t\t\tdefault:\n\t\t\t\treturn path, false\n\t\t\t}\n\n\t\tcase indexEnd:\n\t\t\tswitch {\n\t\t\tcase ch == ']':\n\t\t\t\tstate = unknown\n\t\t\tdefault:\n\t\t\t\treturn path, false\n\t\t\t}\n\n\t\tcase number:\n\t\t\tswitch {\n\t\t\tcase unicode.IsDigit(ch):\n\t\t\t\ts += string(ch)\n\t\t\tcase ch == ']':\n\t\t\t\tstate = unknown\n\t\t\t\tn, err := strconv.Atoi(s)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn path, false\n\t\t\t\t}\n\t\t\t\tpath = append(path, n)\n\t\t\t\ts = \"\"\n\t\t\tdefault:\n\t\t\t\treturn path, false\n\t\t\t}\n\n\t\tcase doubleQuote:\n\t\t\tswitch ch {\n\t\t\tcase '\"':\n\t\t\t\tstate = indexEnd\n\t\t\t\tpath = append(path, s)\n\t\t\t\ts = \"\"\n\t\t\tcase '\\\\':\n\t\t\t\tstate = doubleQuoteEscape\n\t\t\tdefault:\n\t\t\t\ts += string(ch)\n\t\t\t}\n\n\t\tcase doubleQuoteEscape:\n\t\t\tswitch ch {\n\t\t\tcase '\"':\n\t\t\t\tstate = doubleQuote\n\t\t\t\ts += string(ch)\n\t\t\tdefault:\n\t\t\t\treturn path, false\n\t\t\t}\n\n\t\tcase singleQuote:\n\t\t\tswitch ch {\n\t\t\tcase '\\'':\n\t\t\t\tstate = indexEnd\n\t\t\t\tpath = append(path, s)\n\t\t\t\ts = \"\"\n\t\t\tcase '\\\\':\n\t\t\t\tstate = singleQuoteEscape\n\t\t\t\ts += string(ch)\n\t\t\tdefault:\n\t\t\t\ts += string(ch)\n\t\t\t}\n\n\t\tcase singleQuoteEscape:\n\t\t\tswitch ch {\n\t\t\tcase '\\'':\n\t\t\t\tstate = singleQuote\n\t\t\t\ts += string(ch)\n\t\t\tdefault:\n\t\t\t\treturn path, false\n\t\t\t}\n\t\t}\n\t}\n\tif len(s) > 0 {\n\t\tif state == prop {\n\t\t\tpath = append(path, s)\n\t\t} else {\n\t\t\treturn path, false\n\t\t}\n\n\t}\n\treturn path, true\n}\n\nfunc isProp(ch rune) bool {\n\treturn unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '$'\n}\n\nvar Identifier = regexp.MustCompile(`^[$a-zA-Z_][$a-zA-Z0-9_]*$`)\n\nfunc Join(path []any) string {\n\ts := \"\"\n\tfor _, v := range path {\n\t\tswitch v := v.(type) {\n\t\tcase string:\n\t\t\tif Identifier.MatchString(v) {\n\t\t\t\ts += \".\" + v\n\t\t\t} else {\n\t\t\t\ts += \"[\" + strconv.Quote(v) + \"]\"\n\t\t\t}\n\t\tcase int:\n\t\t\ts += \"[\" + strconv.Itoa(v) + \"]\"\n\t\t}\n\t}\n\treturn s\n}\n"
  },
  {
    "path": "internal/jsonpath/path_test.go",
    "content": "package jsonpath_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/antonmedv/fx/internal/jsonpath\"\n)\n\nfunc Test_SplitPath(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\twant  []any\n\t}{\n\t\t{\n\t\t\tinput: \"\",\n\t\t\twant:  []any{},\n\t\t},\n\t\t{\n\t\t\tinput: \".\",\n\t\t\twant:  []any{},\n\t\t},\n\t\t{\n\t\t\tinput: \"x\",\n\t\t\twant:  []any{},\n\t\t},\n\t\t{\n\t\t\tinput: \".foo\",\n\t\t\twant:  []any{\"foo\"},\n\t\t},\n\t\t{\n\t\t\tinput: \"x.foo\",\n\t\t\twant:  []any{\"foo\"},\n\t\t},\n\t\t{\n\t\t\tinput: \"x[42]\",\n\t\t\twant:  []any{42},\n\t\t},\n\t\t{\n\t\t\tinput: \".[42]\",\n\t\t\twant:  []any{42},\n\t\t},\n\t\t{\n\t\t\tinput: \".42\",\n\t\t\twant:  []any{\"42\"},\n\t\t},\n\t\t{\n\t\t\tinput: \".физ\",\n\t\t\twant:  []any{\"физ\"},\n\t\t},\n\t\t{\n\t\t\tinput: \".foo.bar\",\n\t\t\twant:  []any{\"foo\", \"bar\"},\n\t\t},\n\t\t{\n\t\t\tinput: \".foo[42]\",\n\t\t\twant:  []any{\"foo\", 42},\n\t\t},\n\t\t{\n\t\t\tinput: \".foo[42].bar\",\n\t\t\twant:  []any{\"foo\", 42, \"bar\"},\n\t\t},\n\t\t{\n\t\t\tinput: \".foo[1][2]\",\n\t\t\twant:  []any{\"foo\", 1, 2},\n\t\t},\n\t\t{\n\t\t\tinput: \".foo[\\\"bar\\\"]\",\n\t\t\twant:  []any{\"foo\", \"bar\"},\n\t\t},\n\t\t{\n\t\t\tinput: \".foo[\\\"bar\\\\\\\"\\\"]\",\n\t\t\twant:  []any{\"foo\", \"bar\\\"\"},\n\t\t},\n\t\t{\n\t\t\tinput: \".foo['bar']['baz\\\\'']\",\n\t\t\twant:  []any{\"foo\", \"bar\", \"baz\\\\'\"},\n\t\t},\n\t\t{\n\t\t\tinput: \"[42]\",\n\t\t\twant:  []any{42},\n\t\t},\n\t\t{\n\t\t\tinput: \"[42].foo\",\n\t\t\twant:  []any{42, \"foo\"},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tp, ok := jsonpath.Split(tt.input)\n\t\t\trequire.Equal(t, tt.want, p)\n\t\t\trequire.True(t, ok)\n\t\t})\n\t}\n}\n\nfunc Test_SplitPath_negative(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t}{\n\t\t{\n\t\t\tinput: \"./\",\n\t\t},\n\t\t{\n\t\t\tinput: \"x/\",\n\t\t},\n\t\t{\n\t\t\tinput: \"1+1\",\n\t\t},\n\t\t{\n\t\t\tinput: \"x[42\",\n\t\t},\n\t\t{\n\t\t\tinput: \".i % 2\",\n\t\t},\n\t\t{\n\t\t\tinput: \"x[for x]\",\n\t\t},\n\t\t{\n\t\t\tinput: \"x['y'.\",\n\t\t},\n\t\t{\n\t\t\tinput: \"x[0?\",\n\t\t},\n\t\t{\n\t\t\tinput: \"x[\\\"\\\\u\",\n\t\t},\n\t\t{\n\t\t\tinput: \"x['\\\\n\",\n\t\t},\n\t\t{\n\t\t\tinput: \"x[9999999999999999999999999999999999999]\",\n\t\t},\n\t\t{\n\t\t\tinput: \"x[]\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tp, ok := jsonpath.Split(tt.input)\n\t\t\trequire.False(t, ok, p)\n\t\t})\n\t}\n}\n\nfunc TestJoin(t *testing.T) {\n\ttests := []struct {\n\t\tinput []any\n\t\twant  string\n\t}{\n\t\t{\n\t\t\tinput: []any{},\n\t\t\twant:  \"\",\n\t\t},\n\t\t{\n\t\t\tinput: []any{\"foo\"},\n\t\t\twant:  \".foo\",\n\t\t},\n\t\t{\n\t\t\tinput: []any{\"foo\", \"bar\"},\n\t\t\twant:  \".foo.bar\",\n\t\t},\n\t\t{\n\t\t\tinput: []any{\"foo\", 42},\n\t\t\twant:  \".foo[42]\",\n\t\t},\n\t\t{\n\t\t\tinput: []any{\"foo\", \"bar\", 42},\n\t\t\twant:  \".foo.bar[42]\",\n\t\t},\n\t\t{\n\t\t\tinput: []any{\"foo\", \"bar\", 42, \"baz\"},\n\t\t\twant:  \".foo.bar[42].baz\",\n\t\t},\n\t\t{\n\t\t\tinput: []any{\"foo\", \"bar\", 42, \"baz\", 1},\n\t\t\twant:  \".foo.bar[42].baz[1]\",\n\t\t},\n\t\t{\n\t\t\tinput: []any{\"foo\", \"bar\", 42, \"baz\", 1, \"qux\"},\n\t\t\twant:  \".foo.bar[42].baz[1].qux\",\n\t\t},\n\t\t{\n\t\t\tinput: []any{\"foo bar\"},\n\t\t\twant:  \"[\\\"foo bar\\\"]\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.want, func(t *testing.T) {\n\t\t\trequire.Equal(t, tt.want, jsonpath.Join(tt.input))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/jsonpath/ref.go",
    "content": "package jsonpath\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc ParseSchemaRef(ref string) ([]any, bool) {\n\t// Must start with '#'\n\tif len(ref) == 0 || ref[0] != '#' {\n\t\treturn nil, false\n\t}\n\n\t// An empty fragment refers to the whole document\n\tif ref == \"#\" {\n\t\treturn []any{}, true\n\t}\n\n\t// Must be a pointer (\"#/...\")\n\tif !strings.HasPrefix(ref, \"#/\") {\n\t\treturn nil, false\n\t}\n\n\t// Split the pointer without the leading '#/'\n\tparts := strings.Split(ref[2:], \"/\")\n\tout := make([]any, len(parts))\n\tfor i, part := range parts {\n\t\t// JSON Pointer unescaping\n\t\ts := strings.ReplaceAll(strings.ReplaceAll(part, \"~1\", \"/\"), \"~0\", \"~\")\n\t\t// Percent-unescape\n\t\tunescaped, err := url.PathUnescape(s)\n\t\tif err != nil {\n\t\t\treturn nil, false\n\t\t}\n\t\tout[i] = unescaped\n\t}\n\treturn out, true\n}\n"
  },
  {
    "path": "internal/jsonpath/ref_test.go",
    "content": "package jsonpath_test\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/antonmedv/fx/internal/jsonpath\"\n)\n\nfunc TestParseSchemaRef(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tinput  string\n\t\twant   []any\n\t\twantOk bool\n\t}{\n\t\t{\n\t\t\tname:   \"empty fragment\",\n\t\t\tinput:  \"#\",\n\t\t\twant:   []any{},\n\t\t\twantOk: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"simple defs\",\n\t\t\tinput:  \"#/$defs/OrganizationConfig/Options\",\n\t\t\twant:   []any{\"$defs\", \"OrganizationConfig\", \"Options\"},\n\t\t\twantOk: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"with slash escape\",\n\t\t\tinput:  \"#/path/with~1slash\",\n\t\t\twant:   []any{\"path\", \"with/slash\"},\n\t\t\twantOk: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"with tilde escape\",\n\t\t\tinput:  \"#/path/with~0tilde\",\n\t\t\twant:   []any{\"path\", \"with~tilde\"},\n\t\t\twantOk: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"with percent\",\n\t\t\tinput:  \"#/a%20b/c%2Fd\",\n\t\t\twant:   []any{\"a b\", \"c/d\"},\n\t\t\twantOk: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"invalid no prefix\",\n\t\t\tinput:  \"foo/bar\",\n\t\t\twant:   nil,\n\t\t\twantOk: false,\n\t\t},\n\t\t{\n\t\t\tname:   \"invalid bad escape\",\n\t\t\tinput:  \"#/bad/%GG\",\n\t\t\twant:   nil,\n\t\t\twantOk: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, ok := jsonpath.ParseSchemaRef(tt.input)\n\t\t\tif ok != tt.wantOk {\n\t\t\t\tt.Errorf(\"ParseSchemaRef(%q) ok = %v, want %v\", tt.input, ok, tt.wantOk)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"ParseSchemaRef(%q) = %v, want %v\", tt.input, got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/jsonx/delete.go",
    "content": "package jsonx\n\n// DeleteNode removes the node at from the linked structure and returns a node to select next.\n// It returns (nextToSelect, true) if deletion happened, or (nil, false) if nothing was deleted.\n// Rules:\n// - Do nothing if at is nil, points to root (no parent), or is a bracket/closing node (Index == -1).\n// - If at is a wrap placeholder (Chunk set, Value empty), operate on its parent value.\n// - Maintain Prev/Next links skipping the deleted range [at..endOf].\n// - Clear trailing comma on the previous sibling when deleting the last child before parent's End.\n// - Decrement parent.Size and reindex subsequent array siblings.\n// - Choose selection: prefer next; if next is nil or parent.End, prefer prev; else parent.\nfunc DeleteNode(at *Node) (*Node, bool) {\n\tif at == nil {\n\t\treturn nil, false\n\t}\n\t// Avoid closing bracket nodes (Index == -1 used for brackets)\n\tif at.Index == -1 {\n\t\treturn nil, false\n\t}\n\tparent := at.Parent\n\tif parent == nil { // avoid deleting root\n\t\treturn nil, false\n\t}\n\t// If current points to a wrap placeholder, move to its parent value\n\tif at.Chunk != \"\" && at.Value == \"\" && at.Parent != nil {\n\t\tat = at.Parent\n\t\tparent = at.Parent\n\t\tif parent == nil {\n\t\t\treturn nil, false\n\t\t}\n\t}\n\n\t// Determine the last node of this item (to skip its subtree or chunks)\n\tendOf := at\n\tif at.End != nil {\n\t\tendOf = at.End\n\t} else if at.ChunkEnd != nil {\n\t\tendOf = at.ChunkEnd\n\t}\n\tprev := at.Prev\n\tnext := endOf.Next\n\n\t// If deleting the last child before parent's closing bracket, clear trailing comma on previous sibling\n\tisLast := next == parent.End\n\tif isLast && prev != nil && prev != parent {\n\t\tprev.Comma = false\n\t}\n\n\t// Relink to remove [at..endOf] from the chain\n\tif prev != nil {\n\t\tprev.Next = next\n\t}\n\tif next != nil {\n\t\tnext.Prev = prev\n\t}\n\n\t// Update parent size and array indices if needed\n\tif parent.Size > 0 {\n\t\tparent.Size--\n\t}\n\tif parent.Kind == Array {\n\t\tfor it := next; it != nil && it != parent.End; {\n\t\t\tif it.Parent == parent && it.Index >= 0 {\n\t\t\t\tit.Index = it.Index - 1\n\t\t\t}\n\t\t\tif it.HasChildren() {\n\t\t\t\tit = it.End.Next\n\t\t\t} else {\n\t\t\t\tit = it.Next\n\t\t\t}\n\t\t}\n\t}\n\n\t// Select a sensible node after deletion\n\tselectTo := next\n\tif selectTo == nil || selectTo == parent.End {\n\t\tif prev != nil && prev != parent {\n\t\t\tselectTo = prev\n\t\t} else {\n\t\t\tselectTo = parent\n\t\t}\n\t}\n\treturn selectTo, true\n}\n"
  },
  {
    "path": "internal/jsonx/delete_test.go",
    "content": "package jsonx_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t. \"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc TestDeleteNode_ObjectScenarios(t *testing.T) {\n\troot, err := Parse([]byte(`{\"a\":1,\"b\":2,\"c\":3}`))\n\trequire.NoError(t, err)\n\tobj := root\n\trequire.Equal(t, Object, obj.Kind)\n\n\t// delete middle key b\n\tb := obj.FindByPath([]any{\"b\"})\n\trequire.NotNil(t, b)\n\tnext, ok := DeleteNode(b)\n\trequire.True(t, ok)\n\trequire.NotNil(t, next)\n\t// after deleting b, next should be c (or its start)\n\tc := obj.FindByPath([]any{\"c\"})\n\trequire.NotNil(t, c)\n\n\t// ensure size updated and comma on previous cleared\n\trequire.Equal(t, 2, obj.Size)\n\n\t// delete last key c -> previous comma should be cleared and selection fallback\n\tnext2, ok := DeleteNode(c)\n\trequire.True(t, ok)\n\trequire.NotNil(t, next2)\n\t// now only {\"a\":1}\n\trequire.Equal(t, 1, obj.Size)\n\n\t// delete first/only key a -> object empty\n\ta := obj.FindByPath([]any{\"a\"})\n\trequire.NotNil(t, a)\n\t_, ok = DeleteNode(a)\n\trequire.True(t, ok)\n\trequire.Equal(t, 0, obj.Size)\n}\n\nfunc TestDeleteNode_ArrayScenarios(t *testing.T) {\n\troot, err := Parse([]byte(`[10,20,30,40]`))\n\trequire.NoError(t, err)\n\tarr := root\n\trequire.Equal(t, Array, arr.Kind)\n\n\t// delete middle index 1 (20)\n\tidx1 := arr.FindByPath([]any{1})\n\trequire.NotNil(t, idx1)\n\t_, ok := DeleteNode(idx1)\n\trequire.True(t, ok)\n\t// remaining should be [10,30,40], indices 0..2\n\tzero := arr.FindByPath([]any{0})\n\trequire.NotNil(t, zero)\n\trequire.Equal(t, \"10\", zero.Value)\n\tone := arr.FindByPath([]any{1})\n\trequire.NotNil(t, one)\n\trequire.Equal(t, \"30\", one.Value)\n\ttwo := arr.FindByPath([]any{2})\n\trequire.NotNil(t, two)\n\trequire.Equal(t, \"40\", two.Value)\n\trequire.Equal(t, 3, arr.Size)\n\n\t// delete last element (now index 2 -> 40)\n\tlast := arr.FindByPath([]any{2})\n\trequire.NotNil(t, last)\n\t_, ok = DeleteNode(last)\n\trequire.True(t, ok)\n\trequire.Equal(t, 2, arr.Size)\n\n\t// delete first element (10)\n\tfirst := arr.FindByPath([]any{0})\n\trequire.NotNil(t, first)\n\t_, ok = DeleteNode(first)\n\trequire.True(t, ok)\n\trequire.Equal(t, 1, arr.Size)\n\tonly := arr.FindByPath([]any{0})\n\trequire.NotNil(t, only)\n\trequire.Equal(t, \"30\", only.Value)\n\n\t// delete the only element\n\t_, ok = DeleteNode(only)\n\trequire.True(t, ok)\n\trequire.Equal(t, 0, arr.Size)\n}\n\nfunc TestDeleteNode_EdgeCases(t *testing.T) {\n\troot, err := Parse([]byte(`{\"k\": {\"x\":1}, \"arr\":[{\"y\":2},3]}`))\n\trequire.NoError(t, err)\n\n\t// try delete root -> ignored\n\tnext, ok := DeleteNode(root)\n\trequire.False(t, ok)\n\trequire.Nil(t, next)\n\n\t// delete nested object {\"y\":2} inside arr[0]\n\tnested := root.FindByPath([]any{\"arr\", 0})\n\trequire.NotNil(t, nested)\n\t_, ok = DeleteNode(nested)\n\trequire.True(t, ok)\n\t// arr should become [3]\n\tarr := root.FindByPath([]any{\"arr\"})\n\trequire.NotNil(t, arr)\n\trequire.Equal(t, 1, arr.Size)\n\tel := root.FindByPath([]any{\"arr\", 0})\n\trequire.NotNil(t, el)\n\trequire.Equal(t, Number, el.Kind)\n\trequire.Equal(t, \"3\", el.Value)\n}\n"
  },
  {
    "path": "internal/jsonx/format_err.go",
    "content": "package jsonx\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/charmbracelet/x/term\"\n\t\"github.com/mattn/go-runewidth\"\n)\n\nfunc (p *JsonParser) errorSnippet(message string) error {\n\ttermWidth, _, err := term.GetSize(os.Stdout.Fd())\n\tif err != nil {\n\t\ttermWidth = 80\n\t}\n\tmaxWidth := min(termWidth, 60)\n\tmaxWidth -= 2\n\tmaxWidth = max(maxWidth, 10)\n\n\t// As we already moved end pointer in next(), we need to move it back.\n\tp.end -= 1\n\n\tbefore, width := p.contextBefore(maxWidth / 2)\n\tafter, _ := p.contextAfter(maxWidth - width)\n\tsnippet := \"  \" + before + after\n\tsnippet += \"\\n  \" + strings.Repeat(\".\", max(0, width-1)) + \"^\"\n\n\treturn fmt.Errorf(\n\t\t\"%s on line %d.\\n\\n%s\\n\",\n\t\tmessage,\n\t\tp.realLineNumber,\n\t\tsnippet,\n\t)\n}\n\nfunc (p *JsonParser) contextBefore(maxWidth int) (s string, width int) {\n\tpos := p.end + 1\n\tif pos > len(p.data) {\n\t\tpos = len(p.data)\n\t}\n\tdata := p.data[:pos]\n\tfor len(data) > 0 {\n\t\tr, size := utf8.DecodeLastRune(data)\n\t\tif r == '\\n' {\n\t\t\tbreak\n\t\t}\n\t\truneWidth := runewidth.RuneWidth(r)\n\t\tif width+runeWidth > maxWidth {\n\t\t\tbreak\n\t\t}\n\t\twidth += runeWidth\n\t\tpos -= size\n\t\tdata = data[:pos]\n\t}\n\ts = string(p.data[pos:min(p.end+1, len(p.data))])\n\treturn\n}\n\nfunc (p *JsonParser) contextAfter(maxWidth int) (s string, width int) {\n\tpos := p.end + 1\n\tif pos >= len(p.data) {\n\t\treturn\n\t}\n\tdata := p.data[pos:]\n\tfor len(data) > 0 {\n\t\tr, size := utf8.DecodeRune(data)\n\t\tif r == '\\n' {\n\t\t\tbreak\n\t\t}\n\t\truneWidth := runewidth.RuneWidth(r)\n\t\tif width+runeWidth > maxWidth {\n\t\t\tbreak\n\t\t}\n\t\twidth += runeWidth\n\t\tpos += size\n\t\tdata = data[size:]\n\t}\n\ts = string(p.data[p.end+1 : pos]) // +1 to exclude the current character.\n\treturn\n}\n"
  },
  {
    "path": "internal/jsonx/json.go",
    "content": "package jsonx\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/antonmedv/fx/internal/utils\"\n)\n\ntype JsonParser struct {\n\tstrict         bool\n\trd             io.Reader\n\tbuf            []byte\n\tdata           []byte\n\tend            int\n\teof            bool\n\tchar           byte\n\tlineNumber     int\n\trealLineNumber int\n\tdepth          uint8\n\tcount          int\n}\n\nfunc Parse(b []byte) (*Node, error) {\n\tp := NewJsonParser(bytes.NewReader(b), false)\n\tnode, err := p.Parse()\n\tif err == io.EOF {\n\t\terr = nil\n\t}\n\treturn node, err\n}\n\nfunc NewJsonParser(rd io.Reader, strict bool) *JsonParser {\n\tp := &JsonParser{\n\t\tstrict:         strict,\n\t\trd:             rd,\n\t\tbuf:            make([]byte, 4096),\n\t\tlineNumber:     1,\n\t\trealLineNumber: 1,\n\t}\n\tp.next() // Should be called here, to support streaming.\n\treturn p\n}\n\nfunc (p *JsonParser) Parse() (node *Node, err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = p.errorSnippet(fmt.Sprintf(\"%v\", r))\n\t\t}\n\t}()\n\tif p.count > 0 {\n\t\tp.skipWhitespace()\n\t}\n\tif p.eof {\n\t\treturn nil, io.EOF\n\t}\n\tnode = p.parseValue(true)\n\tp.count++\n\treturn\n}\n\nfunc (p *JsonParser) Recover() *Node {\n\tp.eof = false\n\tp.depth = 0\n\n\tstart := p.end - 1\n\tfor {\n\t\tp.next()\n\t\tif p.eof {\n\t\t\tbreak\n\t\t}\n\t\tif p.char == '{' || p.char == '[' {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tend := p.end - 1\n\tif p.data[end-1] == '\\n' {\n\t\tend-- // Trim trailing newline.\n\t}\n\n\tstart = max(0, min(start, end))\n\ttext := string(p.data[start:end])\n\ttext = strings.ReplaceAll(text, \"\\t\", \"    \")\n\ttext = strings.ReplaceAll(text, \"\\r\", \"\")\n\tlines := strings.Split(text, \"\\n\")\n\n\ttextNode := &Node{\n\t\tKind:       Err,\n\t\tValue:      lines[0],\n\t\tIndex:      -1,\n\t\tLineNumber: p.lineNumberPlusPlus(),\n\t}\n\tfor i := 1; i < len(lines); i++ {\n\t\ttextNode.Append(&Node{\n\t\t\tKind:       Err,\n\t\t\tValue:      lines[i],\n\t\t\tIndex:      -1,\n\t\t\tParent:     textNode,\n\t\t\tLineNumber: p.lineNumberPlusPlus(),\n\t\t})\n\t}\n\treturn textNode\n}\n\nfunc (p *JsonParser) refill() {\n\tn, err := p.rd.Read(p.buf)\n\tif err != nil {\n\t\tif err == io.EOF {\n\t\t\tp.eof = true\n\t\t\treturn\n\t\t} else {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\tp.data = append(p.data, p.buf[:n]...)\n}\n\nfunc (p *JsonParser) next() {\n\tif p.end >= len(p.data) {\n\t\tp.refill()\n\t}\n\tif p.eof {\n\t\tp.char = 0\n\t\tp.end = len(p.data) + 1\n\t\treturn\n\t}\n\tp.char = p.data[p.end]\n\tif p.char == '\\n' {\n\t\tp.realLineNumber++\n\t}\n\tp.end++\n}\n\nfunc (p *JsonParser) back() {\n\tp.end--\n\tp.char = p.data[p.end]\n}\n\nfunc (p *JsonParser) set(pos int) {\n\tp.end = pos\n\tp.char = p.data[p.end]\n}\n\nfunc (p *JsonParser) lineNumberPlusPlus() int {\n\tn := p.lineNumber\n\tp.lineNumber++\n\treturn n\n}\n\nfunc (p *JsonParser) parseValue(root bool) *Node {\n\tp.skipWhitespace()\n\n\tvar l *Node\n\tswitch p.char {\n\tcase '\"':\n\t\tl = p.parseString()\n\tcase '-':\n\t\tl = p.parseMinus()\n\tcase '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':\n\t\tl = p.parseNumber(p.end - 1)\n\tcase '{':\n\t\tl = p.parseObject()\n\tcase '[':\n\t\tl = p.parseArray()\n\tcase 't':\n\t\tl = p.parseKeyword(\"true\", Bool)\n\tcase 'f':\n\t\tl = p.parseKeyword(\"false\", Bool)\n\tcase 'n':\n\t\tl = p.parseNullOrNan()\n\tcase 'N':\n\t\tl = p.parseNan(p.end - 1)\n\tcase 'i', 'I':\n\t\tl = p.parseInfinity(p.end - 1)\n\tcase 'u':\n\t\tif p.strict {\n\t\t\tpanic(fmt.Sprintf(\"Unexpected character %q\", p.char))\n\t\t}\n\t\tl = p.parseKeyword(\"undefined\", Undefined)\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"Unexpected character %q\", p.char))\n\t}\n\n\t// Skip whitespace will block parseValue (with io.Read in refill func),\n\t// as soon as we parsed the root value, return and ignore remining whitespaces.\n\tif !root {\n\t\tp.skipWhitespace()\n\t}\n\n\treturn l\n}\n\nfunc (p *JsonParser) parseString() *Node {\n\treturn &Node{\n\t\tKind:       String,\n\t\tDepth:      p.depth,\n\t\tValue:      p.scanString(),\n\t\tLineNumber: p.lineNumberPlusPlus(),\n\t}\n}\n\nfunc (p *JsonParser) scanString() string {\n\tstart := p.end - 1\n\tp.next()\n\tescaped := false\n\tfor {\n\t\tif escaped {\n\t\t\tescaped = false\n\t\t\tif p.strict {\n\t\t\t\tswitch p.char {\n\t\t\t\tcase 'u':\n\t\t\t\t\tvar s string\n\t\t\t\t\tfor i := 0; i < 4; i++ {\n\t\t\t\t\t\tp.next()\n\t\t\t\t\t\tif !utils.IsHexDigit(p.char) {\n\t\t\t\t\t\t\tpanic(fmt.Sprintf(\"Invalid Unicode escape sequence '\\\\u%s%c'\", s, p.char))\n\t\t\t\t\t\t}\n\t\t\t\t\t\ts += string(p.char)\n\t\t\t\t\t}\n\t\t\t\t\t_, err := strconv.ParseInt(s, 16, 32)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tpanic(fmt.Sprintf(\"Invalid Unicode escape sequence '\\\\u%s'\", s))\n\t\t\t\t\t}\n\t\t\t\tcase '\"', '\\\\', '/', 'b', 'f', 'n', 'r', 't':\n\t\t\t\tdefault:\n\t\t\t\t\tpanic(fmt.Sprintf(\"Invalid escape sequence '\\\\%c'\", p.char))\n\t\t\t\t}\n\t\t\t}\n\t\t} else if p.char == '\\\\' {\n\t\t\tescaped = true\n\t\t} else if p.char == '\"' {\n\t\t\tbreak\n\t\t} else if p.char == 0 {\n\t\t\tpanic(\"Unexpected end of input in string\")\n\t\t} else if rune(p.char) > unicode.MaxRune {\n\t\t\tpanic(fmt.Sprintf(\"Invalid character code point %d in string\", p.char))\n\t\t}\n\t\tp.next()\n\t}\n\n\tstr := string(p.data[start:p.end])\n\tp.next()\n\n\treturn str\n}\n\nfunc (p *JsonParser) parseMinus() *Node {\n\tstart := p.end - 1\n\tp.next()\n\tswitch p.char {\n\tcase '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':\n\t\treturn p.parseNumber(start)\n\t}\n\tif !p.strict {\n\t\tswitch p.char {\n\t\tcase 'n', 'N':\n\t\t\treturn p.parseNan(start)\n\t\tcase 'i', 'I':\n\t\t\treturn p.parseInfinity(start)\n\t\t}\n\t}\n\tpanic(fmt.Sprintf(\"Invalid character %q in number\", p.char))\n}\n\nfunc (p *JsonParser) parseNumber(start int) *Node {\n\tnum := &Node{\n\t\tKind:       Number,\n\t\tDepth:      p.depth,\n\t\tLineNumber: p.lineNumberPlusPlus(),\n\t}\n\n\t// Leading zero\n\tif p.char == '0' {\n\t\tp.next()\n\t} else {\n\t\tfor utils.IsDigit(p.char) {\n\t\t\tp.next()\n\t\t}\n\t}\n\n\t// Decimal portion\n\tif p.char == '.' {\n\t\tp.next()\n\t\tif !utils.IsDigit(p.char) {\n\t\t\tpanic(fmt.Sprintf(\"Invalid character %q in number\", p.char))\n\t\t}\n\t\tfor utils.IsDigit(p.char) {\n\t\t\tp.next()\n\t\t}\n\t}\n\n\t// Exponent\n\tif p.char == 'e' || p.char == 'E' {\n\t\tp.next()\n\t\tif p.char == '+' || p.char == '-' {\n\t\t\tp.next()\n\t\t}\n\t\tif !utils.IsDigit(p.char) {\n\t\t\tpanic(fmt.Sprintf(\"Invalid character %q in number\", p.char))\n\t\t}\n\t\tfor utils.IsDigit(p.char) {\n\t\t\tp.next()\n\t\t}\n\t}\n\n\tnum.Value = string(p.data[start : p.end-1])\n\treturn num\n}\n\nfunc (p *JsonParser) parseObject() *Node {\n\tobject := &Node{\n\t\tKind:       Object,\n\t\tDepth:      p.depth,\n\t\tLineNumber: p.lineNumberPlusPlus(),\n\t}\n\tobject.Value = curlyBracketOpen\n\n\tp.next()\n\tp.skipWhitespace()\n\n\t// Empty object\n\tif p.char == '}' {\n\t\tobject.Value = curlyBracketPair\n\t\tp.next()\n\t\treturn object\n\t}\n\n\tfor {\n\t\t// Expecting a key which should be a string\n\t\tif p.char != '\"' {\n\t\t\tpanic(fmt.Sprintf(\"Expected object key to be a string, got %q\", p.char))\n\t\t}\n\n\t\tkeyBytes := p.scanString()\n\n\t\tp.skipWhitespace()\n\n\t\t// Expecting colon after key\n\t\tif p.char != ':' {\n\t\t\tpanic(fmt.Sprintf(\"Expected colon after object key, got %q\", p.char))\n\t\t}\n\n\t\tp.next()\n\n\t\tp.depth++\n\t\tvalue := p.parseValue(false)\n\t\tvalue.Key = keyBytes\n\t\tvalue.Parent = object\n\t\tp.depth--\n\n\t\tobject.Append(value)\n\t\tobject.Size += 1\n\n\t\tp.skipWhitespace()\n\n\t\tcommaPos := p.end\n\t\tif p.char == ',' {\n\t\t\tobject.End.Comma = true\n\t\t\tp.next()\n\t\t\tp.skipWhitespace()\n\t\t\tif p.char == '}' {\n\t\t\t\tif p.strict {\n\t\t\t\t\tp.set(commaPos)\n\t\t\t\t\tpanic(\"Trailing comma is not allowed in strict mode\")\n\t\t\t\t}\n\t\t\t\tobject.End.Comma = false\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif p.char == '}' {\n\t\t\tcloseBracket := &Node{\n\t\t\t\tKind:       Object,\n\t\t\t\tDepth:      p.depth,\n\t\t\t\tLineNumber: p.lineNumberPlusPlus(),\n\t\t\t}\n\t\t\tcloseBracket.Value = curlyBracketClose\n\t\t\tcloseBracket.Parent = object\n\t\t\tcloseBracket.Index = -1\n\t\t\tobject.Append(closeBracket)\n\t\t\tp.next()\n\t\t\treturn object\n\t\t}\n\n\t\tpanic(fmt.Sprintf(\"Unexpected character %q in object\", p.char))\n\t}\n}\n\nfunc (p *JsonParser) parseArray() *Node {\n\tarr := &Node{\n\t\tKind:       Array,\n\t\tDepth:      p.depth,\n\t\tLineNumber: p.lineNumberPlusPlus(),\n\t}\n\tarr.Value = squareBracketOpen\n\n\tp.next()\n\tp.skipWhitespace()\n\n\tif p.char == ']' {\n\t\tarr.Value = squareBracketPair\n\t\tp.next()\n\t\treturn arr\n\t}\n\n\tfor i := 0; ; i++ {\n\t\tp.depth++\n\t\tvalue := p.parseValue(false)\n\t\tvalue.Parent = arr\n\t\tarr.Size += 1\n\t\tvalue.Index = i\n\t\tp.depth--\n\n\t\tarr.Append(value)\n\t\tp.skipWhitespace()\n\n\t\tcommaPos := p.end\n\t\tif p.char == ',' {\n\t\t\tarr.End.Comma = true\n\t\t\tp.next()\n\t\t\tp.skipWhitespace()\n\t\t\tif p.char == ']' {\n\t\t\t\tif p.strict {\n\t\t\t\t\tp.set(commaPos)\n\t\t\t\t\tpanic(\"Trailing comma is not allowed in strict mode\")\n\t\t\t\t}\n\t\t\t\tarr.End.Comma = false\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif p.char == ']' {\n\t\t\tcloseBracket := &Node{\n\t\t\t\tKind:       Array,\n\t\t\t\tDepth:      p.depth,\n\t\t\t\tLineNumber: p.lineNumberPlusPlus(),\n\t\t\t}\n\t\t\tcloseBracket.Value = squareBracketClose\n\t\t\tcloseBracket.Parent = arr\n\t\t\tcloseBracket.Index = -1\n\t\t\tarr.Append(closeBracket)\n\t\t\tp.next()\n\t\t\treturn arr\n\t\t}\n\n\t\tpanic(fmt.Sprintf(\"Invalid character %q in array\", p.char))\n\t}\n}\n\nfunc (p *JsonParser) parseKeyword(name string, kind Kind) *Node {\n\tstart := p.end - 1\n\tfor i := 1; i < len(name); i++ {\n\t\tp.next()\n\t\tif p.char != name[i] {\n\t\t\tpanic(fmt.Sprintf(\"Unexpected character %q in keyword\", p.char))\n\t\t}\n\t}\n\tp.next()\n\tif isEndOfValue(p.char) {\n\t\tkeyword := &Node{\n\t\t\tKind:       kind,\n\t\t\tDepth:      p.depth,\n\t\t\tValue:      string(p.data[start : p.end-1]),\n\t\t\tLineNumber: p.lineNumberPlusPlus(),\n\t\t}\n\t\treturn keyword\n\t}\n\n\tpanic(fmt.Sprintf(\"Unexpected character %q in keyword\", p.char))\n}\n\nfunc (p *JsonParser) parseNullOrNan() *Node {\n\tp.next()\n\tif p.char == 'u' {\n\t\tp.next()\n\t\tif p.char == 'l' {\n\t\t\tp.next()\n\t\t\tif p.char == 'l' {\n\t\t\t\tp.next()\n\t\t\t\tif isEndOfValue(p.char) {\n\t\t\t\t\treturn &Node{\n\t\t\t\t\t\tKind:       Null,\n\t\t\t\t\t\tDepth:      p.depth,\n\t\t\t\t\t\tValue:      \"null\",\n\t\t\t\t\t\tLineNumber: p.lineNumberPlusPlus(),\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if p.char == 'a' {\n\t\tp.back() // Put back the 'a'.\n\t\treturn p.parseNan(p.end - 1)\n\t}\n\tpanic(fmt.Sprintf(\"Unexpected character %q\", p.char))\n}\n\nfunc (p *JsonParser) parseNan(start int) *Node {\n\tif p.strict {\n\t\tpanic(fmt.Sprintf(\"Unexpected character %q\", p.char))\n\t}\n\tp.next()\n\tif p.char == 'a' || p.char == 'A' {\n\t\tp.next()\n\t\tif p.char == 'n' || p.char == 'N' {\n\t\t\tp.next()\n\t\t\tif isEndOfValue(p.char) {\n\t\t\t\treturn &Node{\n\t\t\t\t\tKind:       NaN,\n\t\t\t\t\tDepth:      p.depth,\n\t\t\t\t\tValue:      string(p.data[start : p.end-1]),\n\t\t\t\t\tLineNumber: p.lineNumberPlusPlus(),\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tpanic(fmt.Sprintf(\"Unexpected character %q\", p.char))\n}\n\nfunc (p *JsonParser) parseInfinity(start int) *Node {\n\tif p.strict {\n\t\tpanic(fmt.Sprintf(\"Unexpected character %q\", p.char))\n\t}\n\tp.next()\n\tif p.char == 'n' || p.char == 'N' {\n\t\tp.next()\n\t\tif p.char == 'f' || p.char == 'F' {\n\t\t\tp.next()\n\t\t\tif isEndOfValue(p.char) {\n\t\t\t\treturn &Node{\n\t\t\t\t\tKind:       Infinity,\n\t\t\t\t\tDepth:      p.depth,\n\t\t\t\t\tValue:      string(p.data[start : p.end-1]),\n\t\t\t\t\tLineNumber: p.lineNumberPlusPlus(),\n\t\t\t\t}\n\t\t\t}\n\t\t\tif p.char == 'i' {\n\t\t\t\tp.next()\n\t\t\t\tif p.char == 'n' {\n\t\t\t\t\tp.next()\n\t\t\t\t\tif p.char == 'i' {\n\t\t\t\t\t\tp.next()\n\t\t\t\t\t\tif p.char == 't' {\n\t\t\t\t\t\t\tp.next()\n\t\t\t\t\t\t\tif p.char == 'y' {\n\t\t\t\t\t\t\t\tp.next()\n\t\t\t\t\t\t\t\tif isEndOfValue(p.char) {\n\t\t\t\t\t\t\t\t\treturn &Node{\n\t\t\t\t\t\t\t\t\t\tKind:       Infinity,\n\t\t\t\t\t\t\t\t\t\tDepth:      p.depth,\n\t\t\t\t\t\t\t\t\t\tValue:      string(p.data[start : p.end-1]),\n\t\t\t\t\t\t\t\t\t\tLineNumber: p.lineNumberPlusPlus(),\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tpanic(fmt.Sprintf(\"Unexpected character %q\", p.char))\n}\n\nfunc isEndOfValue(ch byte) bool {\n\treturn isWhitespace(ch) || ch == ',' || ch == '}' || ch == ']' || ch == 0 // 0 is EOF\n}\n\nfunc isWhitespace(ch byte) bool {\n\treturn ch == ' ' || ch == '\\t' || ch == '\\n' || ch == '\\r'\n}\n\nfunc (p *JsonParser) skipWhitespace() {\n\tfor {\n\t\tswitch p.char {\n\t\tcase ' ', '\\t', '\\n', '\\r':\n\t\t\tp.next()\n\t\tcase '/':\n\t\t\tif p.strict {\n\t\t\t\tpanic(\"Comments are not allowed in strict mode\")\n\t\t\t}\n\t\t\tp.skipComment()\n\t\tdefault:\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (p *JsonParser) skipComment() {\n\tp.next()\n\tswitch p.char {\n\tcase '/':\n\t\tfor p.char != '\\n' && p.char != 0 {\n\t\t\tp.next()\n\t\t}\n\tcase '*':\n\t\tfor {\n\t\t\tp.next()\n\t\t\tif p.char == '*' {\n\t\t\t\tp.next()\n\t\t\t\tif p.char == '/' {\n\t\t\t\t\tp.next()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tif p.char == 0 {\n\t\t\t\tpanic(\"Unexpected end of input in comment\")\n\t\t\t}\n\t\t}\n\tdefault:\n\t\tpanic(fmt.Sprintf(\"Invalid comment: '/%c'\", p.char))\n\t}\n}\n"
  },
  {
    "path": "internal/jsonx/jsonx_test.go",
    "content": "package jsonx_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc TestJsonParser_Parse(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\twantKind jsonx.Kind\n\t}{\n\t\t{`\"hello\"`, jsonx.String},\n\t\t{`42`, jsonx.Number},\n\t\t{`-123.45`, jsonx.Number},\n\t\t{`true`, jsonx.Bool},\n\t\t{`false`, jsonx.Bool},\n\t\t{`null`, jsonx.Null},\n\t\t{`{}`, jsonx.Object},\n\t\t{`[]`, jsonx.Array},\n\t\t{`{\"key\":\"value\"}`, jsonx.Object},\n\t\t{`[1, 2, 3]`, jsonx.Array},\n\t\t{`   \"test\"   `, jsonx.String},\n\t\t{`// comment\n\t\t\"test\"`, jsonx.String},\n\t\t{`/* comment */\"test\"`, jsonx.String},\n\t\t{`{\"a\":1,}`, jsonx.Object},\n\t\t{`[1,2,]`, jsonx.Array},\n\t\t{`NaN`, jsonx.NaN},\n\t\t{`-NaN`, jsonx.NaN},\n\t\t{`nan`, jsonx.NaN},\n\t\t{`Infinity`, jsonx.Infinity},\n\t\t{`-Infinity`, jsonx.Infinity},\n\t\t{`infinity`, jsonx.Infinity},\n\t\t{`inf`, jsonx.Infinity},\n\t\t{`INF`, jsonx.Infinity},\n\t\t{`undefined`, jsonx.Undefined},\n\t\t{`\"\\g\"`, jsonx.String},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tnode, err := jsonx.Parse([]byte(tt.input))\n\t\t\tassert.NoError(t, err, \"unexpected error for input: %s\", tt.input)\n\t\t\tassert.Equal(t, tt.wantKind, node.Kind, \"unexpected kind for input: %s\", tt.input)\n\t\t})\n\t}\n}\n\nfunc TestJsonParser_Parse_error(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t}{\n\t\t{`\"abc`},\n\t\t{`truth`},\n\t\t{`1e`},\n\t\t{`[1, 2`},\n\t\t{`/* test`},\n\t\t{`[,]`},\n\t\t{`{,}`},\n\t\t{`[1,,]`},\n\t\t{`{\"a\":1,,}`},\n\t\t{`-null`},\n\t\t{`Null`},\n\t\t{`-Null`},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\t_, err := jsonx.Parse([]byte(tt.input))\n\t\t\tassert.Error(t, err, \"expected error for input: %s\", tt.input)\n\t\t})\n\t}\n}\n\nfunc TestJsonParser_Parse_strict(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t}{\n\t\t{`{\"a\":1,}`},\n\t\t{`[1,2,]`},\n\t\t{`NaN`},\n\t\t{`-NaN`},\n\t\t{`nan`},\n\t\t{`Infinity`},\n\t\t{`-Infinity`},\n\t\t{`infinity`},\n\t\t{`inf`},\n\t\t{`INF`},\n\t\t{`-null`},\n\t\t{`Null`},\n\t\t{`-Null`},\n\t\t{`/*comment*/ 42`},\n\t\t{`undefined`},\n\t\t{`\"\\g\"`},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tparser := jsonx.NewJsonParser(strings.NewReader(tt.input), true)\n\t\t\t_, err := parser.Parse()\n\t\t\tassert.Error(t, err, \"expected error for input: %s\", tt.input)\n\t\t})\n\t}\n}\n\nfunc TestJsonParser_Recovery(t *testing.T) {\n\tbrokenJSON := `{ \"a\": 1 }here goes the text`\n\tt.Run(\"Recover\", func(t *testing.T) {\n\t\tp := jsonx.NewJsonParser(strings.NewReader(brokenJSON), false)\n\t\t_, _ = p.Parse() // trigger error\n\t\tnode := p.Recover()\n\n\t\tassert.Equal(t, jsonx.Err, node.Kind, \"expected recovery node to be of Kind Err\")\n\t\tassert.NotEmpty(t, node.Value, \"expected recovery node to contain error snippet\")\n\t\tassert.Equal(t, string(node.Value), \"here goes the text\", \"expected recovery node to contain error snippet\")\n\t})\n}\n\nfunc TestJsonParser_NestedStructureVerification(t *testing.T) {\n\tinput := `{\n\t\t\"user\": {\n\t\t\t\"name\": \"John\",\n\t\t\t\"age\": 30,\n\t\t\t\"active\": true,\n\t\t\t\"contacts\": {\n\t\t\t\t\"email\": \"john@example.com\",\n\t\t\t\t\"phone\": \"123456789\"\n\t\t\t},\n\t\t\t\"roles\": [\"admin\", \"editor\"]\n\t\t}\n\t}`\n\n\tnode, err := jsonx.Parse([]byte(input))\n\tassert.NoError(t, err)\n\tassert.Equal(t, jsonx.Object, node.Kind)\n\n\t// user object\n\tuser := node.FindByPath([]any{\"user\"})\n\tassert.NotNil(t, user)\n\tassert.Equal(t, jsonx.Object, user.Kind)\n\n\t// user.name\n\tname := node.FindByPath([]any{\"user\", \"name\"})\n\tassert.NotNil(t, name)\n\tassert.Equal(t, jsonx.String, name.Kind)\n\tassert.Equal(t, `\"John\"`, string(name.Value))\n\n\t// user.age\n\tage := node.FindByPath([]any{\"user\", \"age\"})\n\tassert.NotNil(t, age)\n\tassert.Equal(t, jsonx.Number, age.Kind)\n\tassert.Equal(t, \"30\", string(age.Value))\n\n\t// user.active\n\tactive := node.FindByPath([]any{\"user\", \"active\"})\n\tassert.NotNil(t, active)\n\tassert.Equal(t, jsonx.Bool, active.Kind)\n\tassert.Equal(t, \"true\", string(active.Value))\n\n\t// user.contacts.email\n\temail := node.FindByPath([]any{\"user\", \"contacts\", \"email\"})\n\tassert.NotNil(t, email)\n\tassert.Equal(t, jsonx.String, email.Kind)\n\tassert.Equal(t, `\"john@example.com\"`, string(email.Value))\n\n\t// user.roles[1]\n\trole := node.FindByPath([]any{\"user\", \"roles\", 1})\n\tassert.NotNil(t, role)\n\tassert.Equal(t, jsonx.String, role.Kind)\n\tassert.Equal(t, `\"editor\"`, string(role.Value))\n}\n\nfunc TestJsonParser_FindByPathWithCollapsedNodes(t *testing.T) {\n\tt.Run(\"collapsed array access\", func(t *testing.T) {\n\t\tnode, err := jsonx.Parse([]byte(`{\n\t\t\t\"items\": [1, 2, 3, 4, 5]\n\t\t}`))\n\t\trequire.NoError(t, err)\n\n\t\tnode.CollapseRecursively()\n\n\t\titems := node.FindByPath([]any{\"items\"})\n\t\trequire.NotNil(t, items)\n\n\t\telement := node.FindByPath([]any{\"items\", 2})\n\t\trequire.NotNil(t, element)\n\t\trequire.Equal(t, jsonx.Number, element.Kind)\n\t\trequire.Equal(t, \"3\", element.Value)\n\t})\n\n\tt.Run(\"collapsed object access\", func(t *testing.T) {\n\t\tnode, err := jsonx.Parse([]byte(`{\n\t\t\t\"user\": {\n\t\t\t\t\"settings\": {\n\t\t\t\t\t\"theme\": \"dark\",\n\t\t\t\t\t\"notifications\": true\n\t\t\t\t}\n\t\t\t}\n\t\t}`))\n\t\trequire.NoError(t, err)\n\n\t\tnode.CollapseRecursively()\n\n\t\tsettings := node.FindByPath([]any{\"user\", \"settings\"})\n\t\trequire.NotNil(t, settings)\n\n\t\ttheme := node.FindByPath([]any{\"user\", \"settings\", \"theme\"})\n\t\trequire.NotNil(t, theme)\n\t\trequire.Equal(t, jsonx.String, theme.Kind)\n\t\trequire.Equal(t, `\"dark\"`, string(theme.Value))\n\t})\n\n\tt.Run(\"nested collapsed structures\", func(t *testing.T) {\n\t\tnode, err := jsonx.Parse([]byte(`{\n\t\t\t\"data\": {\n\t\t\t\t\"users\": [\n\t\t\t\t\t{\"id\": 1, \"name\": \"John\"},\n\t\t\t\t\t{\"id\": 2, \"name\": \"Jane\"}\n\t\t\t\t]\n\t\t\t}\n\t\t}`))\n\t\trequire.NoError(t, err)\n\n\t\tnode.CollapseRecursively()\n\n\t\tusers := node.FindByPath([]any{\"data\", \"users\"})\n\t\trequire.NotNil(t, users)\n\n\t\tuserName := node.FindByPath([]any{\"data\", \"users\", 1, \"name\"})\n\t\trequire.NotNil(t, userName)\n\t\trequire.Equal(t, jsonx.String, userName.Kind)\n\t\trequire.Equal(t, `\"Jane\"`, string(userName.Value))\n\t})\n\n\tt.Run(\"nested collapsed structures with arrays\", func(t *testing.T) {\n\t\tnode, err := jsonx.Parse([]byte(`{\n\t\t  \"data\": [\n\t\t\t{\n\t\t\t  \"first\": [\n\t\t\t\t\"tmp\",\n\t\t\t\t{\n\t\t\t\t  \"foo\": [\n\t\t\t\t\t1,\n\t\t\t\t\t2,\n\t\t\t\t\ttrue\n\t\t\t\t  ]\n\t\t\t\t}\n\t\t\t  ]\n\t\t\t},\n\t\t\t{\n\t\t\t  \"second\": []\n\t\t\t}\n\t\t  ]\n\t\t}`))\n\t\trequire.NoError(t, err)\n\n\t\tnode.CollapseRecursively()\n\n\t\tvalue := node.FindByPath([]any{\"data\", 0, \"first\", 1, \"foo\", 2})\n\t\trequire.NotNil(t, value)\n\t\trequire.Equal(t, jsonx.Bool, value.Kind)\n\t})\n}\n"
  },
  {
    "path": "internal/jsonx/line.go",
    "content": "package jsonx\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype LineParser struct {\n\tbuf        *bufio.Reader\n\teof        error\n\tlineNumber int\n}\n\nfunc NewLineParser(in io.Reader) *LineParser {\n\tp := &LineParser{\n\t\tbuf:        bufio.NewReader(in),\n\t\tlineNumber: 1,\n\t}\n\treturn p\n}\n\nfunc (p *LineParser) Parse() (*Node, error) {\n\tif p.eof != nil {\n\t\treturn nil, p.eof\n\t}\n\tb, err := p.buf.ReadBytes('\\n')\n\tif err != nil {\n\t\tif err == io.EOF {\n\t\t\tp.eof = err\n\t\t} else {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif len(b) == 0 {\n\t\treturn nil, err\n\t}\n\ts := strings.TrimRight(string(b), \"\\r\\n\")\n\tquoted := strconv.Quote(s)\n\tnode := &Node{\n\t\tKind:       String,\n\t\tValue:      quoted,\n\t\tLineNumber: p.lineNumber,\n\t\tDepth:      0,\n\t}\n\tp.lineNumber++\n\treturn node, nil\n}\n\nfunc (p *LineParser) Recover() *Node {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/jsonx/node.go",
    "content": "package jsonx\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/antonmedv/fx/internal/jsonpath\"\n)\n\ntype Kind byte\n\nconst (\n\tErr Kind = iota\n\tNull\n\tBool\n\tNumber\n\tString\n\tObject\n\tArray\n\tNaN\n\tInfinity\n\tUndefined\n)\n\ntype Node struct {\n\tPrev, Next, End *Node\n\tParent          *Node\n\tCollapsed       *Node\n\tDepth           uint8\n\tKind            Kind\n\tKey             string\n\tValue           string\n\tSize            int\n\tChunk           string\n\tChunkEnd        *Node\n\tComma           bool\n\tIndex           int\n\tLineNumber      int\n}\n\n// Append ands a node as a child to the current node (body of {...} or [...]).\nfunc (n *Node) Append(child *Node) {\n\tif n.End == nil {\n\t\tn.End = n\n\t}\n\tn.End.Next = child\n\tchild.Prev = n.End\n\tif child.End == nil {\n\t\tn.End = child\n\t} else {\n\t\tn.End = child.End\n\t}\n}\n\n// Adjacent adds a node as a sibling to the current node ({}{}{} or [][][]).\nfunc (n *Node) Adjacent(child *Node) {\n\tend := n.End\n\tif end == nil {\n\t\tend = n\n\t}\n\tend.Next = child\n\tchild.Prev = end\n\tif n.IsCollapsed() {\n\t\t// Also attach to collapsed node.\n\t\tn.Next = child\n\t\tchild.Prev = n\n\t}\n}\n\nfunc (n *Node) insertChunk(chunk *Node) {\n\tif n.ChunkEnd == nil {\n\t\tn.insertAfter(chunk)\n\t} else {\n\t\tn.ChunkEnd.insertAfter(chunk)\n\t}\n\tn.ChunkEnd = chunk\n}\n\nfunc (n *Node) insertAfter(child *Node) {\n\tif n.Next == nil {\n\t\tn.Next = child\n\t\tchild.Prev = n\n\t} else {\n\t\told := n.Next\n\t\tn.Next = child\n\t\tchild.Prev = n\n\t\tchild.Next = old\n\t\told.Prev = child\n\t}\n}\n\nfunc (n *Node) dropChunks() {\n\tif n.ChunkEnd == nil {\n\t\treturn\n\t}\n\n\tn.Chunk = \"\"\n\n\tn.Next = n.ChunkEnd.Next\n\tif n.Next != nil {\n\t\tn.Next.Prev = n\n\t}\n\n\tn.ChunkEnd = nil\n}\n\nfunc (n *Node) HasChildren() bool {\n\treturn n.End != nil\n}\n\nfunc (n *Node) Root() *Node {\n\tparent := n.Parent\n\tfor parent != nil {\n\t\tn = parent\n\t\tparent = n.Parent\n\t}\n\treturn n\n}\n\nfunc (n *Node) IsWrap() bool {\n\treturn n.Value == \"\" && n.Chunk != \"\"\n}\n\nfunc (n *Node) IsCollapsed() bool {\n\treturn n.Collapsed != nil\n}\n\nfunc (n *Node) Collapse() *Node {\n\tif n.End != nil && !n.IsCollapsed() {\n\t\tn.Collapsed = n.Next\n\t\tn.Next = n.End.Next\n\t\tif n.Next != nil {\n\t\t\tn.Next.Prev = n\n\t\t}\n\t}\n\treturn n\n}\n\nfunc (n *Node) CollapseRecursively() {\n\tvar at *Node\n\tif n.IsCollapsed() {\n\t\tat = n.Collapsed\n\t} else {\n\t\tat = n.Next\n\t}\n\tfor at != nil && at != n.End {\n\t\tif at.HasChildren() {\n\t\t\tat.CollapseRecursively()\n\t\t\tat.Collapse()\n\t\t}\n\t\tat = at.Next\n\t}\n}\n\nfunc (n *Node) Expand() {\n\tif n.IsCollapsed() {\n\t\tif n.Next != nil {\n\t\t\tn.Next.Prev = n.End\n\t\t}\n\t\tn.Next = n.Collapsed\n\t\tn.Collapsed = nil\n\t}\n}\n\nfunc (n *Node) ExpandRecursively(level, maxLevel int) {\n\tif level >= maxLevel {\n\t\treturn\n\t}\n\tif n.IsCollapsed() {\n\t\tn.Expand()\n\t}\n\tit := n.Next\n\tfor it != nil && it != n.End {\n\t\tif it.HasChildren() {\n\t\t\tit.ExpandRecursively(level+1, maxLevel)\n\t\t\tit = it.End.Next\n\t\t} else {\n\t\t\tit = it.Next\n\t\t}\n\t}\n}\n\nfunc (n *Node) FindByPath(path []any) *Node {\n\tit := n\n\tfor _, part := range path {\n\t\tif it == nil {\n\t\t\treturn nil\n\t\t}\n\t\tswitch part := part.(type) {\n\t\tcase string:\n\t\t\tit = it.findChildByKey(part)\n\t\tcase int:\n\t\t\tit = it.findChildByIndex(part)\n\t\t}\n\t}\n\treturn it\n}\n\nfunc (n *Node) findChildByKey(key string) *Node {\n\tvar it *Node\n\tif n.Collapsed != nil {\n\t\tit = n.Collapsed\n\t} else {\n\t\tit = n.Next\n\t}\n\tfor it != nil && it != n.End {\n\t\tif it.Key != \"\" {\n\t\t\tk, err := strconv.Unquote(it.Key)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif k == key {\n\t\t\t\treturn it\n\t\t\t}\n\t\t}\n\t\tif it.ChunkEnd != nil {\n\t\t\tit = it.ChunkEnd.Next\n\t\t} else if it.End != nil {\n\t\t\tit = it.End.Next\n\t\t} else {\n\t\t\tit = it.Next\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (n *Node) findChildByIndex(index int) *Node {\n\tvar at *Node\n\tif n.Collapsed != nil {\n\t\tat = n.Collapsed\n\t} else {\n\t\tat = n.Next\n\t}\n\tfor at != nil && at != n.End {\n\t\tif at.Index == index {\n\t\t\treturn at\n\t\t}\n\t\tif at.End != nil {\n\t\t\tat = at.End.Next\n\t\t} else {\n\t\t\tat = at.Next\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (n *Node) FindNextNonErr() *Node {\n\tit := n\n\tfor it != nil && it.Kind == Err {\n\t\tit = it.Next\n\t}\n\treturn it\n}\n\nfunc (n *Node) Children() ([]string, []*Node) {\n\tif !n.HasChildren() {\n\t\treturn nil, nil\n\t}\n\n\tvar paths []string\n\tvar nodes []*Node\n\n\tvar it *Node\n\tif n.IsCollapsed() {\n\t\tit = n.Collapsed\n\t} else {\n\t\tit = n.Next\n\t}\n\n\tfor it != nil && it != n.End {\n\t\tif it.Key != \"\" {\n\t\t\tkey := it.Key\n\t\t\tunquoted, err := strconv.Unquote(key)\n\t\t\tif err == nil {\n\t\t\t\tkey = unquoted\n\t\t\t}\n\t\t\tpaths = append(paths, key)\n\t\t\tnodes = append(nodes, it)\n\t\t}\n\n\t\tif it.HasChildren() {\n\t\t\tit = it.End.Next\n\t\t} else {\n\t\t\tit = it.Next\n\t\t}\n\t}\n\n\treturn paths, nodes\n}\n\nfunc (n *Node) Bottom() *Node {\n\tit := n\n\tfor it.Next != nil {\n\t\tif it.End != nil {\n\t\t\tit = it.End\n\t\t} else {\n\t\t\tit = it.Next\n\t\t}\n\t}\n\treturn it\n}\n\nfunc (n *Node) Paths(paths *[]string, nodes *[]*Node) {\n\tjoinPath := func(prefix string, n *Node) string {\n\t\tvar path string\n\t\tif n.Key != \"\" {\n\t\t\tquoted := n.Key\n\t\t\tunquoted, err := strconv.Unquote(quoted)\n\t\t\tif err == nil && jsonpath.Identifier.MatchString(unquoted) {\n\t\t\t\tpath = prefix + \".\" + unquoted\n\t\t\t} else {\n\t\t\t\tpath = prefix + \"[\" + quoted + \"]\"\n\t\t\t}\n\t\t} else if n.Index >= 0 {\n\t\t\tpath = prefix + \"[\" + strconv.Itoa(n.Index) + \"]\"\n\t\t}\n\t\treturn path\n\t}\n\n\ttype item struct {\n\t\tnode *Node\n\t\tpath string\n\t}\n\tvar queue []item\n\tqueue = append(queue, item{node: n, path: \"\"})\n\n\tfor len(queue) > 0 {\n\t\tcurr := queue[0]\n\t\tqueue = queue[1:]\n\n\t\tit := curr.node\n\t\tprefix := curr.path\n\n\t\tif it.IsCollapsed() {\n\t\t\tit = it.Collapsed\n\t\t} else {\n\t\t\tit = it.Next\n\t\t}\n\n\t\tfor it != nil && it != curr.node.End {\n\t\t\tpath := joinPath(prefix, it)\n\t\t\tif path != \"\" {\n\t\t\t\tif len(*paths) == cap(*paths) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t*paths = append(*paths, path)\n\t\t\t\t*nodes = append(*nodes, it)\n\t\t\t}\n\n\t\t\tif it.HasChildren() {\n\t\t\t\tqueue = append(queue, item{node: it, path: path})\n\t\t\t\tit = it.End.Next\n\t\t\t} else {\n\t\t\t\tit = it.Next\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (n *Node) ForEach(cb func(*Node)) {\n\tit := n.Next\n\tfor it != nil && it != n.End {\n\t\tcb(it)\n\t\tif it.HasChildren() {\n\t\t\tit = it.End.Next\n\t\t} else {\n\t\t\tit = it.Next\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/jsonx/node_test.go",
    "content": "package jsonx\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNode_children(t *testing.T) {\n\tn, err := Parse([]byte(`{\"a\": 1, \"b\": {\"f\": 2}, \"c\": [3, 4]}`))\n\trequire.NoError(t, err)\n\n\tpaths, _ := n.Children()\n\tassert.Equal(t, []string{\"a\", \"b\", \"c\"}, paths)\n}\n\nfunc TestNode_expandRecursively(t *testing.T) {\n\tn, err := Parse([]byte(`{\"a\": {\"b\": {\"c\": 1}}}`))\n\trequire.NoError(t, err)\n\n\tn.CollapseRecursively()\n\tn.ExpandRecursively(0, 3)\n\tassert.Equal(t, `\"c\"`, n.Next.Next.Next.Key)\n}\n\nfunc TestNode_Paths(t *testing.T) {\n\tn, err := Parse([]byte(`{\"a\": 1, \"b\": {\"f\": 2}, \"c\": [3, {\"d\": 4}]}`))\n\trequire.NoError(t, err)\n\n\tpaths := make([]string, 0, 10)\n\tnodes := make([]*Node, 0, 10)\n\tn.Paths(&paths, &nodes)\n\tassert.Equal(t, []string{\n\t\t\".a\",\n\t\t\".b\",\n\t\t\".c\",\n\t\t\".b.f\",\n\t\t\".c[0]\",\n\t\t\".c[1]\",\n\t\t\".c[1].d\",\n\t}, paths)\n}\n\nfunc TestNode_Paths_Collapsed(t *testing.T) {\n\tn, err := Parse([]byte(`{\"a\": 1, \"b\": {\"f\": 2}, \"c\": [3, {\"d\": 4}]}`))\n\trequire.NoError(t, err)\n\tn.CollapseRecursively()\n\n\tpaths := make([]string, 0, 10)\n\tnodes := make([]*Node, 0, 10)\n\tn.Paths(&paths, &nodes)\n\tassert.Equal(t, []string{\n\t\t\".a\",\n\t\t\".b\",\n\t\t\".c\",\n\t\t\".b.f\",\n\t\t\".c[0]\",\n\t\t\".c[1]\",\n\t\t\".c[1].d\",\n\t}, paths)\n}\n\nfunc TestNode_ForEach(t *testing.T) {\n\tn, err := Parse([]byte(`{\"a\": 1, \"b\": 2, \"c\": 3}`))\n\trequire.NoError(t, err)\n\n\tvar keys []string\n\tn.ForEach(func(node *Node) {\n\t\tif k, err := strconv.Unquote(node.Key); err == nil {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t})\n\tassert.Equal(t, []string{\"a\", \"b\", \"c\"}, keys)\n}\n\nfunc TestNode_ForEach_Empty(t *testing.T) {\n\tn, err := Parse([]byte(`{}`))\n\trequire.NoError(t, err)\n\n\tcalled := false\n\tn.ForEach(func(node *Node) {\n\t\tcalled = true\n\t})\n\tassert.False(t, called)\n}\n\nfunc TestNode_ForEach_SkipsNested(t *testing.T) {\n\tn, err := Parse([]byte(`{\"a\": {\"b\": 1}, \"c\": [2, {\"d\": 3}]}`))\n\trequire.NoError(t, err)\n\n\tvar keys []string\n\tn.ForEach(func(node *Node) {\n\t\tif k, err := strconv.Unquote(node.Key); err == nil {\n\t\t\tkeys = append(keys, k)\n\t\t}\n\t})\n\tassert.Equal(t, []string{\"a\", \"c\"}, keys)\n}\n"
  },
  {
    "path": "internal/jsonx/string.go",
    "content": "package jsonx\n\nimport (\n\t\"strings\"\n)\n\nconst (\n\tcurlyBracketOpen   = \"{\"\n\tcurlyBracketClose  = \"}\"\n\tcurlyBracketPair   = \"{}\"\n\tsquareBracketOpen  = \"[\"\n\tsquareBracketClose = \"]\"\n\tsquareBracketPair  = \"[]\"\n)\n\nfunc (n *Node) String() string {\n\tvar out strings.Builder\n\n\tit := n\n\tfor it != nil {\n\t\tif it.Key != \"\" {\n\t\t\tout.WriteString(it.Key)\n\t\t\tout.WriteByte(':')\n\t\t}\n\t\tif it.Value != \"\" {\n\t\t\tout.WriteString(it.Value)\n\t\t}\n\t\tif it.Comma {\n\t\t\tout.WriteByte(',')\n\t\t}\n\t\tif it.IsCollapsed() {\n\t\t\tit = it.Collapsed\n\t\t} else {\n\t\t\tit = it.Next\n\t\t}\n\t}\n\n\treturn out.String()\n}\n"
  },
  {
    "path": "internal/jsonx/to_value.go",
    "content": "package jsonx\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"math/big\"\n\t\"strconv\"\n\n\t\"github.com/dop251/goja\"\n\n\t\"github.com/antonmedv/fx/internal/utils\"\n)\n\nfunc (n *Node) ToValue(vm *goja.Runtime) goja.Value {\n\tswitch n.Kind {\n\tcase Null:\n\t\treturn goja.Null()\n\n\tcase Bool:\n\t\tif n.Value == \"true\" {\n\t\t\treturn vm.ToValue(true)\n\t\t} else {\n\t\t\treturn vm.ToValue(false)\n\t\t}\n\n\tcase Number:\n\t\ti, ok := ParseNumber(n.Value)\n\t\tif ok {\n\t\t\treturn vm.ToValue(i)\n\t\t}\n\t\tf, err := strconv.ParseFloat(n.Value, 64)\n\t\tif err == nil {\n\t\t\treturn vm.ToValue(f)\n\t\t}\n\t\tpanic(err)\n\n\tcase String:\n\t\tunquoted, err := utils.Unquote(n.Value)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn vm.ToValue(unquoted)\n\n\tcase Object:\n\t\tobj := vm.NewObject()\n\n\t\tif n.HasChildren() {\n\t\t\tit := n\n\t\t\tif it.IsCollapsed() {\n\t\t\t\tit = it.Collapsed\n\t\t\t} else {\n\t\t\t\tit = it.Next\n\t\t\t}\n\n\t\t\tfor it != nil && it != n.End {\n\t\t\t\tunquotedKey, err := utils.Unquote(it.Key)\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\n\t\t\t\terr = obj.Set(unquotedKey, it.ToValue(vm))\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\n\t\t\t\tif it.HasChildren() {\n\t\t\t\t\tit = it.End.Next\n\t\t\t\t} else {\n\t\t\t\t\tit = it.Next\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn obj\n\n\tcase Array:\n\t\tvar arr []any\n\n\t\tif n.HasChildren() {\n\t\t\tit := n\n\t\t\tif it.IsCollapsed() {\n\t\t\t\tit = it.Collapsed\n\t\t\t} else {\n\t\t\t\tit = it.Next\n\t\t\t}\n\n\t\t\tfor it != nil && it != n.End {\n\t\t\t\tarr = append(arr, it.ToValue(vm))\n\n\t\t\t\tif it.HasChildren() {\n\t\t\t\t\tit = it.End.Next\n\t\t\t\t} else {\n\t\t\t\t\tit = it.Next\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn vm.NewArray(arr...)\n\n\tcase NaN:\n\t\treturn vm.ToValue(math.NaN())\n\n\tcase Infinity:\n\t\tif n.Value[0] == '-' {\n\t\t\treturn vm.ToValue(math.Inf(-1))\n\t\t}\n\t\treturn vm.ToValue(math.Inf(1))\n\n\tcase Undefined:\n\t\treturn goja.Undefined()\n\n\t}\n\tpanic(fmt.Sprintf(\"unsupported node kind %d\", n.Kind))\n}\n\n// maxSafeInt is 2^53 - 1, the largest integer JS can represent exactly.\nconst maxSafeInt = 1<<53 - 1\n\n// minSafeInt is -(2^53 - 1).\nconst minSafeInt = -maxSafeInt\n\n// ParseNumber parses a number from a string as int64 or *big.Int.\nfunc ParseNumber(s string) (interface{}, bool) {\n\tbi := new(big.Int)\n\tif _, ok := bi.SetString(s, 10); !ok {\n\t\treturn nil, false\n\t}\n\n\t// Quickly reject values whose bit-length exceeds 54 (i.e. >= 2^53).\n\t// big.Int.BitLen returns the length of the absolute value in bits.\n\tif bi.BitLen() <= 53 {\n\t\t// Safe to convert to int64 and check full range.\n\t\tv := bi.Int64()\n\t\tif v >= minSafeInt && v <= maxSafeInt {\n\t\t\treturn int(v), true\n\t\t}\n\t}\n\n\treturn bi, true\n}\n"
  },
  {
    "path": "internal/jsonx/wrap.go",
    "content": "package jsonx\n\nimport (\n\t\"unicode/utf8\"\n\n\t\"github.com/mattn/go-runewidth\"\n\n\t\"github.com/antonmedv/fx/internal/ident\"\n)\n\nfunc DropWrapAll(n *Node) {\n\tfor n != nil {\n\t\tif n.Kind == String || n.Kind == Err {\n\t\t\tn.dropChunks()\n\t\t}\n\t\tif n.IsCollapsed() {\n\t\t\tn = n.Collapsed\n\t\t} else {\n\t\t\tn = n.Next\n\t\t}\n\t}\n}\n\nfunc Wrap(n *Node, termWidth int) {\n\tif termWidth <= 0 {\n\t\treturn\n\t}\n\tfor n != nil {\n\t\tif n.Kind == String || n.Kind == Err {\n\t\t\tn.dropChunks()\n\t\t\tlines, count := doWrap(n, termWidth)\n\t\t\tif count > 1 {\n\t\t\t\tn.Chunk = lines[0]\n\t\t\t\tfor i := 1; i < count; i++ {\n\t\t\t\t\tchild := &Node{\n\t\t\t\t\t\tKind:   n.Kind,\n\t\t\t\t\t\tParent: n,\n\t\t\t\t\t\tDepth:  n.Depth,\n\t\t\t\t\t\tChunk:  lines[i],\n\t\t\t\t\t}\n\t\t\t\t\tif n.Comma && i == count-1 {\n\t\t\t\t\t\tchild.Comma = true\n\t\t\t\t\t}\n\t\t\t\t\tn.insertChunk(child)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif n.IsCollapsed() {\n\t\t\tn = n.Collapsed\n\t\t} else {\n\t\t\tn = n.Next\n\t\t}\n\t}\n}\n\nfunc doWrap(n *Node, termWidth int) ([]string, int) {\n\tlines := make([]string, 0, 1)\n\twidth := int(n.Depth) * ident.IdentWidth\n\n\tif n.Key != \"\" {\n\t\tfor _, ch := range n.Key {\n\t\t\twidth += runewidth.RuneWidth(ch)\n\t\t}\n\t\twidth += 2 // for \": \"\n\t}\n\n\tlinesCount := 0\n\tstart, end := 0, 0\n\tb := []byte(n.Value)\n\n\tfor len(b) > 0 {\n\t\tr, size := utf8.DecodeRune(b)\n\t\tw := runewidth.RuneWidth(r)\n\t\tif width+w > termWidth {\n\t\t\tlines = append(lines, n.Value[start:end])\n\t\t\tstart = end\n\t\t\twidth = int(n.Depth) * 2\n\t\t\tlinesCount++\n\t\t}\n\t\twidth += w\n\t\tend += size\n\t\tb = b[size:]\n\t}\n\n\tif start < end {\n\t\tlines = append(lines, n.Value[start:])\n\t\tlinesCount++\n\t}\n\n\treturn lines, linesCount\n}\n"
  },
  {
    "path": "internal/pretty/inlineable.go",
    "content": "package pretty\n\nimport (\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc isInlineable(n *jsonx.Node) bool {\n\tif n.Kind == jsonx.Array && len(n.Key) > 0 {\n\t\tif isSimpleNumbersArray(n) {\n\t\t\treturn true\n\t\t}\n\t\tif isSingleElementArray(n) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSingleElementArray(n *jsonx.Node) bool {\n\tif n.Kind == jsonx.Array && n.Size == 1 {\n\t\tit := n.Next\n\t\tif it != nil {\n\t\t\tif it.Kind == jsonx.Null || it.Kind == jsonx.Bool || it.Kind == jsonx.Number {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif it.Kind == jsonx.String && len(it.Value) <= 80 {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc isSimpleNumbersArray(n *jsonx.Node) bool {\n\tif n.Kind == jsonx.Array {\n\t\tisAllNumbers := true\n\t\tcount := 0\n\t\tn.ForEach(func(child *jsonx.Node) {\n\t\t\tcount++\n\t\t\tif child.Kind != jsonx.Number {\n\t\t\t\tisAllNumbers = false\n\t\t\t}\n\t\t})\n\t\treturn isAllNumbers && count > 0\n\t}\n\treturn false\n}\n\nfunc isSimpleObject(n *jsonx.Node) bool {\n\tif n.Kind == jsonx.Object {\n\t\t// Special case for empty objects\n\t\tif n.Size == 0 {\n\t\t\treturn true\n\t\t}\n\n\t\t// Special case: exactly one key with string value and len(key+value) <= 80 chars\n\t\tif n.Size == 1 {\n\t\t\tvar hasOneStringValue bool\n\t\t\tvar keyLength, valueLength int\n\n\t\t\tn.ForEach(func(child *jsonx.Node) {\n\t\t\t\tkeyLength = len(child.Key)\n\t\t\t\tif child.Kind == jsonx.String {\n\t\t\t\t\tvalueLength = len(child.Value)\n\t\t\t\t\thasOneStringValue = true\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif hasOneStringValue && keyLength+valueLength <= 80 {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\t// Original implementation\n\t\tisSimple := true\n\t\tcount := 0\n\t\tnumStrings := 0\n\t\tnumOther := 0\n\n\t\tn.ForEach(func(child *jsonx.Node) {\n\t\t\tcount++\n\t\t\tif len(child.Key) > 10 {\n\t\t\t\tisSimple = false\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif child.Kind == jsonx.String {\n\t\t\t\tnumStrings++\n\t\t\t\tif len(child.Value) > 20 {\n\t\t\t\t\tisSimple = false\n\t\t\t\t}\n\t\t\t} else if child.Kind == jsonx.Number || child.Kind == jsonx.Bool || child.Kind == jsonx.Null {\n\t\t\t\tnumOther++\n\t\t\t} else {\n\t\t\t\tisSimple = false\n\t\t\t}\n\t\t})\n\n\t\t// Apply limits based on the types of values present\n\t\tif numStrings > 2 || numOther > 3 {\n\t\t\tisSimple = false\n\t\t}\n\n\t\treturn isSimple\n\t}\n\treturn false\n}\n\nfunc isNestedArrays(n *jsonx.Node) bool {\n\tif n.Kind != jsonx.Array || n.Size == 0 {\n\t\treturn false\n\t}\n\n\tisValid := true\n\tn.ForEach(func(child *jsonx.Node) {\n\t\tif child.Kind != jsonx.Array {\n\t\t\tisValid = false\n\t\t\treturn\n\t\t}\n\t\tchild.ForEach(func(innerChild *jsonx.Node) {\n\t\t\tif innerChild.Kind != jsonx.Number {\n\t\t\t\tisValid = false\n\t\t\t}\n\t\t})\n\t})\n\treturn isValid\n}\n\nfunc isArrayOfSimpleObject(n *jsonx.Node) bool {\n\tif n.Kind != jsonx.Array || n.Size == 0 {\n\t\treturn false\n\t}\n\n\tisValid := true\n\tn.ForEach(func(child *jsonx.Node) {\n\t\tif !isSimpleObject(child) {\n\t\t\tisValid = false\n\t\t}\n\t})\n\treturn isValid\n}\n"
  },
  {
    "path": "internal/pretty/inlineable_test.go",
    "content": "package pretty_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n\t\"github.com/antonmedv/fx/internal/pretty\"\n)\n\nfunc TestIsInlineable(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tjson     string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"simple array with numbers\",\n\t\t\tjson:     `{\"key\": [1, 2, 3]}`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"array with non-number elements\",\n\t\t\tjson:     `{\"key\": [1, \"string\", true]}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty array\",\n\t\t\tjson:     `{\"key\": []}`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"array without key\",\n\t\t\tjson:     `[1, 2, 3]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"simple object with number values\",\n\t\t\tjson:     `{\"key\": {\"a\": 1, \"b\": 2, \"c\": 3}}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"simple object with boolean values\",\n\t\t\tjson:     `{\"key\": {\"a\": true, \"b\": false, \"c\": true}}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"simple object with short string values\",\n\t\t\tjson:     `{\"key\": {\"a\": \"short\", \"b\": \"string\"}}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"object with long key\",\n\t\t\tjson:     `{\"key\": {\"thisIsAVeryLongKey\": 1, \"b\": 2}}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"object with mixed value types\",\n\t\t\tjson:     `{\"key\": {\"a\": 1, \"b\": \"string\"}}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"object with long string value\",\n\t\t\tjson:     `{\"key\": {\"a\": \"this is a very long string that exceeds twenty characters\"}}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"object with too many string values\",\n\t\t\tjson:     `{\"key\": {\"a\": \"string1\", \"b\": \"string2\", \"c\": \"string3\"}}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"object with too many number values\",\n\t\t\tjson:     `{\"key\": {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"object without key\",\n\t\t\tjson:     `{\"a\": 1, \"b\": 2}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty object\",\n\t\t\tjson:     `{\"key\": {}}`,\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode, err := jsonx.Parse([]byte(tt.json))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar testNode *jsonx.Node\n\t\t\tif strings.Contains(tt.json, `\"key\":`) {\n\t\t\t\ttestNode = node.FindByPath([]any{\"key\"})\n\t\t\t\trequire.NotNil(t, testNode, \"Could not find node with key 'key'\")\n\t\t\t} else {\n\t\t\t\ttestNode = node\n\t\t\t}\n\n\t\t\toutput := pretty.Print(testNode, true)\n\n\t\t\tlineCount := strings.Count(output, \"\\n\")\n\t\t\tisInlined := lineCount == 1\n\t\t\tassert.Equal(t, tt.expected, isInlined,\n\t\t\t\t\"Expected isInlineable to be %v for %s, but got %v\\nOutput:\\n%s\",\n\t\t\t\ttt.expected, tt.json, isInlined, output)\n\t\t})\n\t}\n}\n\nfunc TestIsNestedArrays(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tjson     string\n\t\texpected bool\n\t}{\n\t\t// Valid tables\n\t\t{\n\t\t\tname:     \"valid table - array of arrays with numbers of same size\",\n\t\t\tjson:     `[[1, 2, 3], [4, 5, 6], [7, 8, 9]]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid table - array of arrays with single number\",\n\t\t\tjson:     `[[1], [2], [3]]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"not a table - table with key\",\n\t\t\tjson:     `{\"table\": [[1, 2], [3, 4]]}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid table with many rows\",\n\t\t\tjson:     `[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid table with only one inner array\",\n\t\t\tjson:     `[[1, 2, 3]]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid table with multiple arrays of different sizes\",\n\t\t\tjson:     `[[1, 2, 3, 4], [5, 6], [7, 8, 9], [10]]`,\n\t\t\texpected: true,\n\t\t},\n\n\t\t// Invalid tables\n\t\t{\n\t\t\tname:     \"not a table - array with non-array elements\",\n\t\t\tjson:     `[1, 2, 3]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"not a table - array of arrays with non-number elements\",\n\t\t\tjson:     `[[1, 2], [\"a\", \"b\"]]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"valid table - array of arrays with different sizes\",\n\t\t\tjson:     `[[1, 2, 3], [4, 5]]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"not a table - empty array\",\n\t\t\tjson:     `[]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"not a table - array with mixed content\",\n\t\t\tjson:     `[[1, 2], 3, [4, 5]]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"not a table - array of arrays with boolean values\",\n\t\t\tjson:     `[[true, false], [false, true]]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"not a table - array of arrays with string values\",\n\t\t\tjson:     `[[\"a\", \"b\"], [\"c\", \"d\"]]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"not a table - array of arrays with null values\",\n\t\t\tjson:     `[[null, null], [null, null]]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"not a table - array of arrays with object values\",\n\t\t\tjson:     `[[{\"a\": 1}, {\"b\": 2}], [{\"c\": 3}, {\"d\": 4}]]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"not a table - array of arrays with array values\",\n\t\t\tjson:     `[[[1], [2]], [[3], [4]]]`,\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode, err := jsonx.Parse([]byte(tt.json))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar testNode *jsonx.Node\n\t\t\tif strings.Contains(tt.json, `\"table\":`) {\n\t\t\t\ttestNode = node.FindByPath([]any{\"table\"})\n\t\t\t\trequire.NotNil(t, testNode, \"Could not find node with key 'table'\")\n\t\t\t} else {\n\t\t\t\ttestNode = node\n\t\t\t}\n\n\t\t\toutput := pretty.Print(testNode, true)\n\n\t\t\t// Check if the output has the characteristics of a table format\n\t\t\t// For a table, each inner array should be on its own line in a tabular format\n\t\t\tlines := strings.Split(output, \"\\n\")\n\n\t\t\t// A table should have at least 3 lines (opening bracket, content, closing bracket)\n\t\t\t// and the content lines should be formatted in a specific way\n\t\t\tisTable := false\n\t\t\tif len(lines) >= 3 {\n\t\t\t\t// Check if the first line contains the opening bracket\n\t\t\t\tif strings.Contains(lines[0], \"[\") {\n\t\t\t\t\t// Check if the last non-empty line contains the closing bracket\n\t\t\t\t\tlastNonEmptyIndex := len(lines) - 1\n\t\t\t\t\tfor lastNonEmptyIndex >= 0 && lines[lastNonEmptyIndex] == \"\" {\n\t\t\t\t\t\tlastNonEmptyIndex--\n\t\t\t\t\t}\n\n\t\t\t\t\tif lastNonEmptyIndex >= 0 && strings.Contains(lines[lastNonEmptyIndex], \"]\") {\n\t\t\t\t\t\t// Check if the middle lines have a consistent format\n\t\t\t\t\t\t// In a table, each line should start with the same indentation and contain numbers\n\t\t\t\t\t\tisTable = true\n\t\t\t\t\t\tfor i := 1; i < lastNonEmptyIndex; i++ {\n\t\t\t\t\t\t\tif lines[i] == \"\" {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Each line in a table should contain numbers and be properly indented\n\t\t\t\t\t\t\tif !strings.Contains(lines[i], \"[\") || !strings.Contains(lines[i], \"]\") {\n\t\t\t\t\t\t\t\tisTable = false\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expected, isTable,\n\t\t\t\t\"Expected isNestedArrays to be %v for %s, but got %v\\nOutput:\\n%s\",\n\t\t\t\ttt.expected, tt.json, isTable, output)\n\t\t})\n\t}\n}\n\nfunc TestIsArrayOfSimpleObject(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tjson     string\n\t\texpected bool\n\t}{\n\t\t// Valid arrays of simple objects\n\t\t{\n\t\t\tname:     \"array of simple objects with number values\",\n\t\t\tjson:     `[{\"a\": 1, \"b\": 2}, {\"a\": 3, \"b\": 4}, {\"a\": 5, \"b\": 6}]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of simple objects with boolean values\",\n\t\t\tjson:     `[{\"a\": true, \"b\": false}, {\"a\": false, \"b\": true}]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of simple objects with short string values\",\n\t\t\tjson:     `[{\"a\": \"short\", \"b\": \"text\"}, {\"a\": \"another\", \"b\": \"value\"}]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"array with single simple object\",\n\t\t\tjson:     `[{\"a\": 1, \"b\": 2, \"c\": 3}]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of simple objects with different keys but same value types\",\n\t\t\tjson:     `[{\"a\": 1, \"b\": 2}, {\"c\": 3, \"d\": 4}]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"object containing array of simple objects\",\n\t\t\tjson:     `{\"data\": [{\"a\": 1, \"b\": 2}, {\"a\": 3, \"b\": 4}]}`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty array\",\n\t\t\tjson:     `[]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of non-objects\",\n\t\t\tjson:     `[1, 2, 3]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of mixed types\",\n\t\t\tjson:     `[{\"a\": 1}, 2, {\"b\": 3}]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of objects with long keys\",\n\t\t\tjson:     `[{\"veryLongKey\": 1, \"b\": 2}, {\"veryLongKey\": 3, \"b\": 4}]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"array - array of objects with mixed value types\",\n\t\t\tjson:     `[{\"a\": 1, \"b\": \"string\"}, {\"a\": 2, \"b\": \"text\"}]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of objects with long string values\",\n\t\t\tjson:     `[{\"a\": \"this is a very long string that exceeds twenty characters\"}, {\"a\": \"short\"}]`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of objects with too many string values\",\n\t\t\tjson:     `[{\"a\": \"string1\", \"b\": \"string2\", \"c\": \"string3\"}, {\"a\": \"text1\", \"b\": \"text2\", \"c\": \"text3\"}]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of objects with too many number values\",\n\t\t\tjson:     `[{\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}, {\"a\": 5, \"b\": 6, \"c\": 7, \"d\": 8}]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of objects with nested objects\",\n\t\t\tjson:     `[{\"a\": {\"nested\": 1}}, {\"a\": {\"nested\": 2}}]`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"array of objects with arrays\",\n\t\t\tjson:     `[{\"a\": [1, 2]}, {\"a\": [3, 4]}]`,\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode, err := jsonx.Parse([]byte(tt.json))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar testNode *jsonx.Node\n\t\t\tif strings.Contains(tt.json, `\"data\":`) {\n\t\t\t\ttestNode = node.FindByPath([]any{\"data\"})\n\t\t\t\trequire.NotNil(t, testNode, \"Could not find node with key 'data'\")\n\t\t\t} else {\n\t\t\t\ttestNode = node\n\t\t\t}\n\n\t\t\toutput := pretty.Print(testNode, true)\n\n\t\t\t// Check if the output has the characteristics of a table format\n\t\t\t// For a table, each object should be on its own line in a tabular format\n\t\t\tlines := strings.Split(output, \"\\n\")\n\n\t\t\t// A table should have at least 3 lines (opening bracket, content, closing bracket)\n\t\t\t// and the content lines should be formatted in a specific way\n\t\t\tisTable := false\n\t\t\tif len(lines) >= 3 {\n\t\t\t\t// Check if the first line contains the opening bracket\n\t\t\t\tif strings.Contains(lines[0], \"[\") {\n\t\t\t\t\t// Check if the last non-empty line contains the closing bracket\n\t\t\t\t\tlastNonEmptyIndex := len(lines) - 1\n\t\t\t\t\tfor lastNonEmptyIndex >= 0 && lines[lastNonEmptyIndex] == \"\" {\n\t\t\t\t\t\tlastNonEmptyIndex--\n\t\t\t\t\t}\n\n\t\t\t\t\tif lastNonEmptyIndex >= 0 && strings.Contains(lines[lastNonEmptyIndex], \"]\") {\n\t\t\t\t\t\t// Check if the middle lines have a consistent format\n\t\t\t\t\t\t// In a table, each line should start with the same indentation and contain objects\n\t\t\t\t\t\tisTable = true\n\t\t\t\t\t\tfor i := 1; i < lastNonEmptyIndex; i++ {\n\t\t\t\t\t\t\tif lines[i] == \"\" {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Each line in a table should contain objects and be properly indented\n\t\t\t\t\t\t\tif !strings.Contains(lines[i], \"{\") || !strings.Contains(lines[i], \"}\") {\n\t\t\t\t\t\t\t\tisTable = false\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expected, isTable,\n\t\t\t\t\"Expected isArrayOfSimpleObject to be %v for %s, but got %v\\nOutput:\\n%s\",\n\t\t\t\ttt.expected, tt.json, isTable, output)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/pretty/pretty_print.go",
    "content": "package pretty\n\nimport (\n\t\"strings\"\n\n\t\"github.com/antonmedv/fx/internal/ident\"\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n\t\"github.com/antonmedv/fx/internal/theme\"\n)\n\n// Print pretty prints a Node. Node must be the top (head),\n// as everything will be printed.\nfunc Print(n *jsonx.Node, withInline bool) string {\n\tvar out strings.Builder\n\n\tit := n\n\tfor it != nil {\n\t\tif withInline {\n\t\t\tif isNestedArrays(it) {\n\t\t\t\tit = table(&out, it)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif isArrayOfSimpleObject(it) {\n\t\t\t\tit = table(&out, it)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif isInlineable(it) {\n\t\t\t\tit = inline(&out, it)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tprintIdent(&out, it)\n\t\tprintKey(&out, it)\n\t\tprintValue(&out, it)\n\t\tit = next(it)\n\t\tif it != nil {\n\t\t\tout.WriteByte('\\n')\n\t\t}\n\t}\n\n\treturn out.String()\n}\n\nfunc table(out *strings.Builder, n *jsonx.Node) *jsonx.Node {\n\tprintIdent(out, n)\n\tprintKey(out, n)\n\tprintValue(out, n)\n\tout.WriteByte('\\n')\n\n\tit := next(n)\n\tend := n.End\n\tfor it != nil && it != end {\n\t\tit = inline(out, it)\n\t}\n\n\tprintIdent(out, end)\n\tprintValue(out, end)\n\n\tit = next(it)\n\tif it != nil {\n\t\tout.WriteByte('\\n')\n\t}\n\treturn it\n}\n\nfunc inline(out *strings.Builder, n *jsonx.Node) *jsonx.Node {\n\tprintIdent(out, n)\n\tprintSpace := false\n\tit := n\n\tend := afterEnd(n)\n\tfor it != nil && it != end {\n\t\tif printSpace {\n\t\t\tout.WriteString(\" \")\n\t\t} else {\n\t\t\tprintSpace = true\n\t\t}\n\t\tprintKey(out, it)\n\t\tprintValue(out, it)\n\t\tit = next(it)\n\t}\n\n\tout.WriteByte('\\n')\n\treturn it\n}\n\nfunc printIdent(out *strings.Builder, n *jsonx.Node) {\n\tfor i := 0; i < int(n.Depth); i++ {\n\t\tout.WriteString(ident.Ident)\n\t}\n}\n\nfunc printKey(out *strings.Builder, n *jsonx.Node) {\n\tif n.Key != \"\" {\n\t\tout.WriteString(theme.CurrentTheme.Key(n.Key))\n\t\tout.WriteString(theme.Colon)\n\t}\n}\n\nfunc printValue(out *strings.Builder, n *jsonx.Node) {\n\tif n.Value != \"\" {\n\t\tout.WriteString(theme.Value(n.Kind)(n.Value))\n\t}\n\tif n.Comma {\n\t\tout.WriteString(theme.Comma)\n\t}\n}\n\nfunc next(n *jsonx.Node) *jsonx.Node {\n\tif n.IsCollapsed() {\n\t\treturn n.Collapsed\n\t} else {\n\t\treturn n.Next\n\t}\n}\n\nfunc afterEnd(n *jsonx.Node) *jsonx.Node {\n\tif n.End != nil {\n\t\treturn n.End.Next\n\t}\n\treturn n.Next\n}\n"
  },
  {
    "path": "internal/pretty/pretty_print_test.go",
    "content": "package pretty_test\n\nimport (\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n\t\"github.com/antonmedv/fx/internal/pretty\"\n)\n\nfunc stripEscapeSequences(s string) string {\n\tre := regexp.MustCompile(`\\x1b\\[[0-9;]*[a-zA-Z]`)\n\treturn re.ReplaceAllString(s, \"\")\n}\n\nconst (\n\tyes byte = iota\n\tno\n\tboth\n)\n\nfunc TestPrettyPrint(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tjson     string\n\t\texpected string\n\t\tinline   byte\n\t}{\n\t\t{\n\t\t\tname:     \"standalone null with inline\",\n\t\t\tjson:     `null`,\n\t\t\texpected: `null`,\n\t\t\tinline:   both,\n\t\t},\n\t\t{\n\t\t\tname:     \"standalone true with inline\",\n\t\t\tjson:     `true`,\n\t\t\texpected: `true`,\n\t\t\tinline:   both,\n\t\t},\n\t\t{\n\t\t\tname:     \"standalone false with inline\",\n\t\t\tjson:     `false`,\n\t\t\texpected: `false`,\n\t\t\tinline:   both,\n\t\t},\n\t\t{\n\t\t\tname: \"array with empty object and empty array with inline\",\n\t\t\tjson: `[{}, []]`,\n\t\t\texpected: `[\n  {},\n  []\n]`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"simple object with inline\",\n\t\t\tjson: `{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}`,\n\t\t\texpected: `{\n  \"name\": \"John\",\n  \"age\": 30,\n  \"city\": \"New York\"\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"nested object without inline\",\n\t\t\tjson: `{\"person\":{\"name\":\"John\",\"age\":30,\"address\":{\"city\":\"New York\",\"zip\":\"10001\"}}}`,\n\t\t\texpected: `{\n  \"person\": {\n    \"name\": \"John\",\n    \"age\": 30,\n    \"address\": {\n      \"city\": \"New York\",\n      \"zip\": \"10001\"\n    }\n  }\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"array of numbers with inline\",\n\t\t\tjson: `{\"numbers\":[1,2,3,4,5]}`,\n\t\t\texpected: `{\n  \"numbers\": [ 1, 2, 3, 4, 5 ]\n}`,\n\t\t\tinline: yes,\n\t\t},\n\t\t{\n\t\t\tname: \"array of numbers without inline\",\n\t\t\tjson: `{\"numbers\":[1,2,3,4,5]}`,\n\t\t\texpected: `{\n  \"numbers\": [\n    1,\n    2,\n    3,\n    4,\n    5\n  ]\n}`,\n\t\t\tinline: no,\n\t\t},\n\t\t{\n\t\t\tname: \"array of objects with inline\",\n\t\t\tjson: `{\"people\":[{\"name\":\"John\",\"age\":30},{\"name\":\"Jane\",\"age\":25}]}`,\n\t\t\texpected: `{\n  \"people\": [\n    { \"name\": \"John\", \"age\": 30 },\n    { \"name\": \"Jane\", \"age\": 25 }\n  ]\n}`,\n\t\t\tinline: yes,\n\t\t},\n\t\t{\n\t\t\tname: \"array of objects without inline\",\n\t\t\tjson: `{\"people\":[{\"name\":\"John\",\"age\":30},{\"name\":\"Jane\",\"age\":25}]}`,\n\t\t\texpected: `{\n  \"people\": [\n    {\n      \"name\": \"John\",\n      \"age\": 30\n    },\n    {\n      \"name\": \"Jane\",\n      \"age\": 25\n    }\n  ]\n}`,\n\t\t\tinline: no,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty object with inline\",\n\t\t\tjson:     `{}`,\n\t\t\texpected: `{}`,\n\t\t\tinline:   both,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty array with inline\",\n\t\t\tjson:     `[]`,\n\t\t\texpected: `[]`,\n\t\t\tinline:   both,\n\t\t},\n\t\t{\n\t\t\tname: \"null value with inline\",\n\t\t\tjson: `{\"value\":null}`,\n\t\t\texpected: `{\n  \"value\": null\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"boolean values with inline\",\n\t\t\tjson: `{\"active\":true,\"verified\":false}`,\n\t\t\texpected: `{\n  \"active\": true,\n  \"verified\": false\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"string with special characters without inline\",\n\t\t\tjson: `{\"message\":\"Hello, \\\"World\\\"!\\nNew line\\tTab\"}`,\n\t\t\texpected: `{\n  \"message\": \"Hello, \\\"World\\\"!\\nNew line\\tTab\"\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"deeply nested structure with inline\",\n\t\t\tjson: `{\"level1\":{\"level2\":{\"level3\":{\"level4\":{\"level5\":\"deep value\"}}}}}`,\n\t\t\texpected: `{\n  \"level1\": {\n    \"level2\": {\n      \"level3\": {\n        \"level4\": {\n          \"level5\": \"deep value\"\n        }\n      }\n    }\n  }\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed array elements with inline\",\n\t\t\tjson: `{\"mixed\":[1,\"string\",true,null,{\"key\":\"value\"},[1,2,3]]}`,\n\t\t\texpected: `{\n  \"mixed\": [\n    1,\n    \"string\",\n    true,\n    null,\n    {\n      \"key\": \"value\"\n    },\n    [\n      1,\n      2,\n      3\n    ]\n  ]\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"table-like structure with inline\",\n\t\t\tjson: `{\"table\":[[1,2,3],[4,5,6],[7,8,9]]}`,\n\t\t\texpected: `{\n  \"table\": [\n    [ 1, 2, 3 ],\n    [ 4, 5, 6 ],\n    [ 7, 8, 9 ]\n  ]\n}`,\n\t\t\tinline: yes,\n\t\t},\n\t\t{\n\t\t\tname: \"table-like structure without inline\",\n\t\t\tjson: `{\"table\":[[1,2,3],[4,5,6],[7,8,9]]}`,\n\t\t\texpected: `{\n  \"table\": [\n    [\n      1,\n      2,\n      3\n    ],\n    [\n      4,\n      5,\n      6\n    ],\n    [\n      7,\n      8,\n      9\n    ]\n  ]\n}`,\n\t\t\tinline: no,\n\t\t},\n\t\t{\n\t\t\tname: \"empty string with inline\",\n\t\t\tjson: `{\"value\":\"\"}`,\n\t\t\texpected: `{\n  \"value\": \"\"\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"very long string with inline\",\n\t\t\tjson: `{\"longText\":\"This is a very long string that should not be inlined because it exceeds the maximum length for inlining. It should be displayed on its own line even when inlining is enabled.\"}`,\n\t\t\texpected: `{\n  \"longText\": \"This is a very long string that should not be inlined because it exceeds the maximum length for inlining. It should be displayed on its own line even when inlining is enabled.\"\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"very large array with inline\",\n\t\t\tjson: `{\"largeArray\":[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]}`,\n\t\t\texpected: `{\n  \"largeArray\": [ 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 ]\n}`,\n\t\t\tinline: yes,\n\t\t},\n\t\t{\n\t\t\tname: \"very large array without inline\",\n\t\t\tjson: `{\"largeArray\":[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]}`,\n\t\t\texpected: `{\n  \"largeArray\": [\n    1,\n    2,\n    3,\n    4,\n    5,\n    6,\n    7,\n    8,\n    9,\n    10,\n    11,\n    12,\n    13,\n    14,\n    15,\n    16,\n    17,\n    18,\n    19,\n    20,\n    21,\n    22,\n    23,\n    24,\n    25,\n    26,\n    27,\n    28,\n    29,\n    30\n  ]\n}`,\n\t\t\tinline: no,\n\t\t},\n\t\t{\n\t\t\tname: \"special number values without inline\",\n\t\t\tjson: `{\"nan\":NaN,\"infinity\":Infinity,\"negInfinity\":-Infinity}`,\n\t\t\texpected: `{\n  \"nan\": NaN,\n  \"infinity\": Infinity,\n  \"negInfinity\": -Infinity\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"object with empty keys without inline\",\n\t\t\tjson: `{\"\":\"empty key\"}`,\n\t\t\texpected: `{\n  \"\": \"empty key\"\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"array with single element with inline\",\n\t\t\tjson: `{\"singleElement\":[42]}`,\n\t\t\texpected: `{\n  \"singleElement\": [ 42 ]\n}`,\n\t\t\tinline: yes,\n\t\t},\n\t\t{\n\t\t\tname: \"array with single element without inline\",\n\t\t\tjson: `{\"singleElement\":[42]}`,\n\t\t\texpected: `{\n  \"singleElement\": [\n    42\n  ]\n}`,\n\t\t\tinline: no,\n\t\t},\n\t\t{\n\t\t\tname: \"nested empty structures without inline\",\n\t\t\tjson: `{\"emptyObject\":{},\"emptyArray\":[],\"nestedEmpty\":{\"empty\":{},\"alsoEmpty\":[]}}`,\n\t\t\texpected: `{\n  \"emptyObject\": {},\n  \"emptyArray\": [],\n  \"nestedEmpty\": {\n    \"empty\": {},\n    \"alsoEmpty\": []\n  }\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"extremely deep nesting without inline\",\n\t\t\tjson: `{\"l1\":{\"l2\":{\"l3\":{\"l4\":{\"l5\":{\"l6\":{\"l7\":{\"l8\":{\"l9\":{\"l10\":{\"l11\":{\"l12\":{\"l13\":{\"l14\":{\"l15\":{\"l16\":{\"l17\":{\"l18\":{\"l19\":{\"l20\":\"deep\"}}}}}}}}}}}}}}}}}}}}`,\n\t\t\texpected: `{\n  \"l1\": {\n    \"l2\": {\n      \"l3\": {\n        \"l4\": {\n          \"l5\": {\n            \"l6\": {\n              \"l7\": {\n                \"l8\": {\n                  \"l9\": {\n                    \"l10\": {\n                      \"l11\": {\n                        \"l12\": {\n                          \"l13\": {\n                            \"l14\": {\n                              \"l15\": {\n                                \"l16\": {\n                                  \"l17\": {\n                                    \"l18\": {\n                                      \"l19\": {\n                                        \"l20\": \"deep\"\n                                      }\n                                    }\n                                  }\n                                }\n                              }\n                            }\n                          }\n                        }\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"extremely long key name without inline\",\n\t\t\tjson: `{\"thisIsAnExtremelyLongKeyNameThatShouldTestTheFormattingCapabilitiesOfThePrettyPrinterAndEnsureThatItHandlesVeryLongKeysCorrectlyWithoutBreakingOrCausingAnyIssuesInTheOutput\":\"value\"}`,\n\t\t\texpected: `{\n  \"thisIsAnExtremelyLongKeyNameThatShouldTestTheFormattingCapabilitiesOfThePrettyPrinterAndEnsureThatItHandlesVeryLongKeysCorrectlyWithoutBreakingOrCausingAnyIssuesInTheOutput\": \"value\"\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"unusual escape sequences without inline\",\n\t\t\tjson: `{\"escapes\":\"\\\\u0000\\\\u0001\\\\u0002\\\\u0003\\\\u0004\\\\u0005\\\\u0006\\\\u0007\\\\b\\\\t\\\\n\\\\u000B\\\\f\\\\r\\\\u000E\\\\u000F\"}`,\n\t\t\texpected: `{\n  \"escapes\": \"\\\\u0000\\\\u0001\\\\u0002\\\\u0003\\\\u0004\\\\u0005\\\\u0006\\\\u0007\\\\b\\\\t\\\\n\\\\u000B\\\\f\\\\r\\\\u000E\\\\u000F\"\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"array with mixed sized elements without inline\",\n\t\t\tjson: `{\"mixedArray\":[\"small\",{\"medium\":\"object with some text\"},{\"large\":\"this is a much larger object with significantly more text that should cause different formatting depending on the pretty printer settings\"}]}`,\n\t\t\texpected: `{\n  \"mixedArray\": [\n    \"small\",\n    {\n      \"medium\": \"object with some text\"\n    },\n    {\n      \"large\": \"this is a much larger object with significantly more text that should cause different formatting depending on the pretty printer settings\"\n    }\n  ]\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"boundary number values with inline\",\n\t\t\tjson: `{\"maxInt\":9007199254740991,\"minInt\":-9007199254740991,\"smallFloat\":0.0000000000000001,\"largeFloat\":1.7976931348623157e+308,\"smallNegativeFloat\":-0.0000000000000001}`,\n\t\t\texpected: `{\n  \"maxInt\": 9007199254740991,\n  \"minInt\": -9007199254740991,\n  \"smallFloat\": 0.0000000000000001,\n  \"largeFloat\": 1.7976931348623157e+308,\n  \"smallNegativeFloat\": -0.0000000000000001\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"boundary number values without inline\",\n\t\t\tjson: `{\"maxInt\":9007199254740991,\"minInt\":-9007199254740991,\"smallFloat\":0.0000000000000001,\"largeFloat\":1.7976931348623157e+308,\"smallNegativeFloat\":-0.0000000000000001}`,\n\t\t\texpected: `{\n  \"maxInt\": 9007199254740991,\n  \"minInt\": -9007199254740991,\n  \"smallFloat\": 0.0000000000000001,\n  \"largeFloat\": 1.7976931348623157e+308,\n  \"smallNegativeFloat\": -0.0000000000000001\n}`,\n\t\t\tinline: both,\n\t\t},\n\t\t{\n\t\t\tname: \"array with single element\",\n\t\t\tjson: `{\"key\":[42]}`,\n\t\t\texpected: `{\n  \"key\": [ 42 ]\n}`,\n\t\t\tinline: yes,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode, err := jsonx.Parse([]byte(tt.json))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tt.inline == both {\n\t\t\t\t{\n\t\t\t\t\toutput := pretty.Print(node, true)\n\t\t\t\t\tstrippedOutput := stripEscapeSequences(output)\n\t\t\t\t\tassert.Equal(t, tt.expected, strippedOutput,\n\t\t\t\t\t\t\"Output doesn't match expected for %s\", tt.name)\n\t\t\t\t}\n\t\t\t\t{\n\t\t\t\t\toutput := pretty.Print(node, false)\n\t\t\t\t\tstrippedOutput := stripEscapeSequences(output)\n\t\t\t\t\tassert.Equal(t, tt.expected, strippedOutput,\n\t\t\t\t\t\t\"Output doesn't match expected for %s\", tt.name)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\toutput := pretty.Print(node, tt.inline == yes)\n\t\t\t\tstrippedOutput := stripEscapeSequences(output)\n\t\t\t\tassert.Equal(t, tt.expected, strippedOutput,\n\t\t\t\t\t\"Output doesn't match expected for %s\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/shlex/shlex.go",
    "content": "/*\nCopyright 2012 Google Inc. All Rights Reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/*\nPackage shlex implements a simple lexer which splits input in to tokens using\nshell-style rules for quoting and commenting.\n\nThe basic use case uses the default ASCII lexer to split a string into sub-strings:\n\n\tshlex.Split(\"one \\\"two three\\\" four\") -> []string{\"one\", \"two three\", \"four\"}\n\nTo process a stream of strings:\n\n\tl := NewLexer(os.Stdin)\n\tfor ; token, err := l.Next(); err != nil {\n\t\t// process token\n\t}\n\nTo access the raw token stream (which includes tokens for comments):\n\n\t  t := NewTokenizer(os.Stdin)\n\t  for ; token, err := t.Next(); err != nil {\n\t\t// process token\n\t  }\n*/\npackage shlex\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n)\n\n// TokenType is a top-level token classification: A word, space, comment, unknown.\ntype TokenType int\n\n// runeTokenClass is the type of a UTF-8 character classification: A quote, space, escape.\ntype runeTokenClass int\n\n// the internal state used by the lexer state machine\ntype lexerState int\n\n// Token is a (type, value) pair representing a lexographical token.\ntype Token struct {\n\ttokenType TokenType\n\tvalue     string\n}\n\n// Equal reports whether tokens a, and b, are equal.\n// Two tokens are equal if both their types and values are equal. A nil token can\n// never be equal to another token.\nfunc (a *Token) Equal(b *Token) bool {\n\tif a == nil || b == nil {\n\t\treturn false\n\t}\n\tif a.tokenType != b.tokenType {\n\t\treturn false\n\t}\n\treturn a.value == b.value\n}\n\n// Named classes of UTF-8 runes\nconst (\n\tspaceRunes            = \" \\t\\r\\n\"\n\tescapingQuoteRunes    = `\"`\n\tnonEscapingQuoteRunes = \"'\"\n\tescapeRunes           = `\\`\n\tcommentRunes          = \"#\"\n)\n\n// Classes of rune token\nconst (\n\tunknownRuneClass runeTokenClass = iota\n\tspaceRuneClass\n\tescapingQuoteRuneClass\n\tnonEscapingQuoteRuneClass\n\tescapeRuneClass\n\tcommentRuneClass\n\teofRuneClass\n)\n\n// Classes of lexographic token\nconst (\n\tUnknownToken TokenType = iota\n\tWordToken\n\tSpaceToken\n\tCommentToken\n)\n\n// Lexer state machine states\nconst (\n\tstartState           lexerState = iota // no runes have been seen\n\tinWordState                            // processing regular runes in a word\n\tescapingState                          // we have just consumed an escape rune; the next rune is literal\n\tescapingQuotedState                    // we have just consumed an escape rune within a quoted string\n\tquotingEscapingState                   // we are within a quoted string that supports escaping (\"...\")\n\tquotingState                           // we are within a string that does not support escaping ('...')\n\tcommentState                           // we are within a comment (everything following an unquoted or unescaped #\n)\n\n// tokenClassifier is used for classifying rune characters.\ntype tokenClassifier map[rune]runeTokenClass\n\nfunc (typeMap tokenClassifier) addRuneClass(runes string, tokenType runeTokenClass) {\n\tfor _, runeChar := range runes {\n\t\ttypeMap[runeChar] = tokenType\n\t}\n}\n\n// newDefaultClassifier creates a new classifier for ASCII characters.\nfunc newDefaultClassifier() tokenClassifier {\n\tt := tokenClassifier{}\n\tt.addRuneClass(spaceRunes, spaceRuneClass)\n\tt.addRuneClass(escapingQuoteRunes, escapingQuoteRuneClass)\n\tt.addRuneClass(nonEscapingQuoteRunes, nonEscapingQuoteRuneClass)\n\tt.addRuneClass(escapeRunes, escapeRuneClass)\n\tt.addRuneClass(commentRunes, commentRuneClass)\n\treturn t\n}\n\n// ClassifyRune classifiees a rune\nfunc (t tokenClassifier) ClassifyRune(runeVal rune) runeTokenClass {\n\treturn t[runeVal]\n}\n\n// Lexer turns an input stream into a sequence of tokens. Whitespace and comments are skipped.\ntype Lexer Tokenizer\n\n// NewLexer creates a new lexer from an input stream.\nfunc NewLexer(r io.Reader) *Lexer {\n\treturn (*Lexer)(NewTokenizer(r))\n}\n\n// Next returns the next word, or an error. If there are no more words,\n// the error will be io.EOF.\nfunc (l *Lexer) Next() (string, error) {\n\tfor {\n\t\ttoken, err := (*Tokenizer)(l).Next()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tswitch token.tokenType {\n\t\tcase WordToken:\n\t\t\treturn token.value, nil\n\t\tcase CommentToken:\n\t\t\t// skip comments\n\t\tdefault:\n\t\t\treturn \"\", fmt.Errorf(\"Unknown token type: %v\", token.tokenType)\n\t\t}\n\t}\n}\n\n// Tokenizer turns an input stream into a sequence of typed tokens\ntype Tokenizer struct {\n\tinput      bufio.Reader\n\tclassifier tokenClassifier\n}\n\n// NewTokenizer creates a new tokenizer from an input stream.\nfunc NewTokenizer(r io.Reader) *Tokenizer {\n\tinput := bufio.NewReader(r)\n\tclassifier := newDefaultClassifier()\n\treturn &Tokenizer{\n\t\tinput:      *input,\n\t\tclassifier: classifier}\n}\n\n// scanStream scans the stream for the next token using the internal state machine.\n// It will panic if it encounters a rune which it does not know how to handle.\nfunc (t *Tokenizer) scanStream() (*Token, error) {\n\tstate := startState\n\tvar tokenType TokenType\n\tvar value []rune\n\tvar nextRune rune\n\tvar nextRuneType runeTokenClass\n\tvar err error\n\n\tfor {\n\t\tnextRune, _, err = t.input.ReadRune()\n\t\tnextRuneType = t.classifier.ClassifyRune(nextRune)\n\n\t\tif err == io.EOF {\n\t\t\tnextRuneType = eofRuneClass\n\t\t\terr = nil\n\t\t} else if err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tswitch state {\n\t\tcase startState: // no runes read yet\n\t\t\t{\n\t\t\t\tswitch nextRuneType {\n\t\t\t\tcase eofRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\treturn nil, io.EOF\n\t\t\t\t\t}\n\t\t\t\tcase spaceRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t}\n\t\t\t\tcase escapingQuoteRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttokenType = WordToken\n\t\t\t\t\t\tstate = quotingEscapingState\n\t\t\t\t\t}\n\t\t\t\tcase nonEscapingQuoteRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttokenType = WordToken\n\t\t\t\t\t\tstate = quotingState\n\t\t\t\t\t}\n\t\t\t\tcase escapeRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttokenType = WordToken\n\t\t\t\t\t\tstate = escapingState\n\t\t\t\t\t}\n\t\t\t\tcase commentRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttokenType = CommentToken\n\t\t\t\t\t\tstate = commentState\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\t{\n\t\t\t\t\t\ttokenType = WordToken\n\t\t\t\t\t\tvalue = append(value, nextRune)\n\t\t\t\t\t\tstate = inWordState\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase inWordState: // in a regular word\n\t\t\t{\n\t\t\t\tswitch nextRuneType {\n\t\t\t\tcase eofRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttoken := &Token{\n\t\t\t\t\t\t\ttokenType: tokenType,\n\t\t\t\t\t\t\tvalue:     string(value)}\n\t\t\t\t\t\treturn token, err\n\t\t\t\t\t}\n\t\t\t\tcase spaceRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttoken := &Token{\n\t\t\t\t\t\t\ttokenType: tokenType,\n\t\t\t\t\t\t\tvalue:     string(value)}\n\t\t\t\t\t\treturn token, err\n\t\t\t\t\t}\n\t\t\t\tcase escapingQuoteRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\tstate = quotingEscapingState\n\t\t\t\t\t}\n\t\t\t\tcase nonEscapingQuoteRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\tstate = quotingState\n\t\t\t\t\t}\n\t\t\t\tcase escapeRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\tstate = escapingState\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue = append(value, nextRune)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase escapingState: // the rune after an escape character\n\t\t\t{\n\t\t\t\tswitch nextRuneType {\n\t\t\t\tcase eofRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttoken := &Token{\n\t\t\t\t\t\t\ttokenType: tokenType,\n\t\t\t\t\t\t\tvalue:     string(value)}\n\t\t\t\t\t\treturn token, nil\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\t{\n\t\t\t\t\t\tstate = inWordState\n\t\t\t\t\t\tvalue = append(value, nextRune)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase escapingQuotedState: // the next rune after an escape character, in double quotes\n\t\t\t{\n\t\t\t\tswitch nextRuneType {\n\t\t\t\tcase eofRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttoken := &Token{\n\t\t\t\t\t\t\ttokenType: tokenType,\n\t\t\t\t\t\t\tvalue:     string(value)}\n\t\t\t\t\t\treturn token, nil\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\t{\n\t\t\t\t\t\tstate = quotingEscapingState\n\t\t\t\t\t\tvalue = append(value, nextRune)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase quotingEscapingState: // in escaping double quotes\n\t\t\t{\n\t\t\t\tswitch nextRuneType {\n\t\t\t\tcase eofRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttoken := &Token{\n\t\t\t\t\t\t\ttokenType: tokenType,\n\t\t\t\t\t\t\tvalue:     string(value)}\n\t\t\t\t\t\treturn token, nil\n\t\t\t\t\t}\n\t\t\t\tcase escapingQuoteRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\tstate = inWordState\n\t\t\t\t\t}\n\t\t\t\tcase escapeRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\tstate = escapingQuotedState\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue = append(value, nextRune)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase quotingState: // in non-escaping single quotes\n\t\t\t{\n\t\t\t\tswitch nextRuneType {\n\t\t\t\tcase eofRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttoken := &Token{\n\t\t\t\t\t\t\ttokenType: tokenType,\n\t\t\t\t\t\t\tvalue:     string(value)}\n\t\t\t\t\t\treturn token, nil\n\t\t\t\t\t}\n\t\t\t\tcase nonEscapingQuoteRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\tstate = inWordState\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue = append(value, nextRune)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase commentState: // in a comment\n\t\t\t{\n\t\t\t\tswitch nextRuneType {\n\t\t\t\tcase eofRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\ttoken := &Token{\n\t\t\t\t\t\t\ttokenType: tokenType,\n\t\t\t\t\t\t\tvalue:     string(value)}\n\t\t\t\t\t\treturn token, err\n\t\t\t\t\t}\n\t\t\t\tcase spaceRuneClass:\n\t\t\t\t\t{\n\t\t\t\t\t\tif nextRune == '\\n' {\n\t\t\t\t\t\t\tstate = startState\n\t\t\t\t\t\t\ttoken := &Token{\n\t\t\t\t\t\t\t\ttokenType: tokenType,\n\t\t\t\t\t\t\t\tvalue:     string(value)}\n\t\t\t\t\t\t\treturn token, err\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tvalue = append(value, nextRune)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\t{\n\t\t\t\t\t\tvalue = append(value, nextRune)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\t{\n\t\t\t\treturn nil, fmt.Errorf(\"Unexpected state: %v\", state)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Next returns the next token in the stream.\nfunc (t *Tokenizer) Next() (*Token, error) {\n\treturn t.scanStream()\n}\n\n// Split partitions a string into a slice of strings.\nfunc Split(s string) ([]string, error) {\n\tl := NewLexer(strings.NewReader(s))\n\tsubStrings := make([]string, 0)\n\tfor {\n\t\tword, err := l.Next()\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\treturn subStrings, nil\n\t\t\t}\n\t\t\treturn subStrings, err\n\t\t}\n\t\tsubStrings = append(subStrings, word)\n\t}\n}\n\n// Parse removes the shell-style quoting from a string.\nfunc Parse(s string) string {\n\tl := NewLexer(strings.NewReader(s))\n\tvar b strings.Builder\n\tfor {\n\t\tword, err := l.Next()\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\treturn b.String()\n\t\t\t}\n\t\t\treturn \"\"\n\t\t}\n\t\tb.WriteString(word)\n\t}\n}\n"
  },
  {
    "path": "internal/shlex/shlex_test.go",
    "content": "/*\nCopyright 2012 Google Inc. All Rights Reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage shlex\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\t// one two \"three four\" \"five \\\"six\\\"\" seven#eight # nine # ten\n\t// eleven 'twelve\\'\n\ttestString = \"one two \\\"three four\\\" \\\"five \\\\\\\"six\\\\\\\"\\\" seven#eight # nine # ten\\n eleven 'twelve\\\\' thirteen=13 fourteen/14\"\n)\n\nfunc TestClassifier(t *testing.T) {\n\tclassifier := newDefaultClassifier()\n\ttests := map[rune]runeTokenClass{\n\t\t' ':  spaceRuneClass,\n\t\t'\"':  escapingQuoteRuneClass,\n\t\t'\\'': nonEscapingQuoteRuneClass,\n\t\t'#':  commentRuneClass}\n\tfor runeChar, want := range tests {\n\t\tgot := classifier.ClassifyRune(runeChar)\n\t\tif got != want {\n\t\t\tt.Errorf(\"ClassifyRune(%v) -> %v. Want: %v\", runeChar, got, want)\n\t\t}\n\t}\n}\n\nfunc TestTokenizer(t *testing.T) {\n\ttestInput := strings.NewReader(testString)\n\texpectedTokens := []*Token{\n\t\t&Token{WordToken, \"one\"},\n\t\t&Token{WordToken, \"two\"},\n\t\t&Token{WordToken, \"three four\"},\n\t\t&Token{WordToken, \"five \\\"six\\\"\"},\n\t\t&Token{WordToken, \"seven#eight\"},\n\t\t&Token{CommentToken, \" nine # ten\"},\n\t\t&Token{WordToken, \"eleven\"},\n\t\t&Token{WordToken, \"twelve\\\\\"},\n\t\t&Token{WordToken, \"thirteen=13\"},\n\t\t&Token{WordToken, \"fourteen/14\"}}\n\n\ttokenizer := NewTokenizer(testInput)\n\tfor i, want := range expectedTokens {\n\t\tgot, err := tokenizer.Next()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif !got.Equal(want) {\n\t\t\tt.Errorf(\"Tokenizer.Next()[%v] of %q -> %v. Want: %v\", i, testString, got, want)\n\t\t}\n\t}\n}\n\nfunc TestLexer(t *testing.T) {\n\ttestInput := strings.NewReader(testString)\n\texpectedStrings := []string{\"one\", \"two\", \"three four\", \"five \\\"six\\\"\", \"seven#eight\", \"eleven\", \"twelve\\\\\", \"thirteen=13\", \"fourteen/14\"}\n\n\tlexer := NewLexer(testInput)\n\tfor i, want := range expectedStrings {\n\t\tgot, err := lexer.Next()\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif got != want {\n\t\t\tt.Errorf(\"Lexer.Next()[%v] of %q -> %v. Want: %v\", i, testString, got, want)\n\t\t}\n\t}\n}\n\nfunc TestSplit(t *testing.T) {\n\twant := []string{\"one\", \"two\", \"three four\", \"five \\\"six\\\"\", \"seven#eight\", \"eleven\", \"twelve\\\\\", \"thirteen=13\", \"fourteen/14\"}\n\tgot, err := Split(testString)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif len(want) != len(got) {\n\t\tt.Errorf(\"Split(%q) -> %v. Want: %v\", testString, got, want)\n\t}\n\tfor i := range got {\n\t\tif got[i] != want[i] {\n\t\t\tt.Errorf(\"Split(%q)[%v] -> %v. Want: %v\", testString, i, got[i], want[i])\n\t\t}\n\t}\n}\n\nfunc TestSplit_unfinished(t *testing.T) {\n\tgot, err := Split(\"one 'two\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\"one\", \"two\"}, got)\n\n\tgot, err = Split(\"one \\\"two\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, []string{\"one\", \"two\"}, got)\n}\n"
  },
  {
    "path": "internal/theme/theme.go",
    "content": "package theme\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"sort\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/muesli/termenv\"\n\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\ntype Theme struct {\n\tCursor     Color\n\tSyntax     Color\n\tPreview    Color\n\tStatusBar  Color\n\tSearch     Color\n\tKey        Color\n\tString     Color\n\tNull       Color\n\tBoolean    Color\n\tNumber     Color\n\tSize       Color\n\tRef        Color\n\tLineNumber Color\n\tError      Color\n}\n\ntype Color func(s string) string\n\nfunc Value(kind jsonx.Kind) Color {\n\tswitch kind {\n\tcase jsonx.String:\n\t\treturn CurrentTheme.String\n\tcase jsonx.Bool:\n\t\treturn CurrentTheme.Boolean\n\tcase jsonx.Null:\n\t\treturn CurrentTheme.Null\n\tcase jsonx.Object, jsonx.Array:\n\t\treturn CurrentTheme.Syntax\n\tcase jsonx.Number:\n\t\treturn CurrentTheme.Number\n\tcase jsonx.NaN:\n\t\treturn CurrentTheme.Error\n\tcase jsonx.Infinity:\n\t\treturn CurrentTheme.Error\n\tcase jsonx.Undefined:\n\t\treturn CurrentTheme.Error\n\tdefault:\n\t\treturn noColor\n\t}\n\n}\n\nvar (\n\tTermOutput = termenv.NewOutput(os.Stderr)\n)\n\nfunc init() {\n\tthemeNames = make([]string, 0, len(themes))\n\tfor name := range themes {\n\t\tthemeNames = append(themeNames, name)\n\t}\n\tsort.Strings(themeNames)\n\n\tthemeId, ok := os.LookupEnv(\"FX_THEME\")\n\tif !ok {\n\t\tthemeId = \"1\"\n\t}\n\n\tCurrentTheme, ok = themes[themeId]\n\tif !ok {\n\t\t_, _ = fmt.Fprintf(os.Stderr, \"fx: unknown theme %q, available themes: %v\\n\", themeId, themeNames)\n\t\tos.Exit(1)\n\t}\n\n\tif TermOutput.ColorProfile() == termenv.Ascii {\n\t\tCurrentTheme = themes[\"0\"]\n\t}\n\n\tColon = CurrentTheme.Syntax(\": \")\n\tColonPreview = CurrentTheme.Preview(\":\")\n\tComma = CurrentTheme.Syntax(\",\")\n\tCommaPreview = CurrentTheme.Preview(\",\")\n\tEmpty = CurrentTheme.Preview(\"~\")\n\tDot3 = CurrentTheme.Preview(\"…\")\n\tCloseCurlyBracket = CurrentTheme.Syntax(\"}\")\n\tCloseSquareBracket = CurrentTheme.Syntax(\"]\")\n}\n\nvar (\n\tthemeNames []string\n\n\tCurrentTheme Theme\n\n\tunderline         = toColor(lipgloss.NewStyle().Underline(true).Render)\n\tdefaultCursor     = toColor(lipgloss.NewStyle().Reverse(true).Render)\n\tdefaultPreview    = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color(\"8\")).Render)\n\tdefaultStatusBar  = toColor(lipgloss.NewStyle().Background(lipgloss.Color(\"7\")).Foreground(lipgloss.Color(\"0\")).Render)\n\tdefaultSearch     = toColor(lipgloss.NewStyle().Background(lipgloss.Color(\"11\")).Foreground(lipgloss.Color(\"16\")).Render)\n\tdefaultNull       = fg(\"243\")\n\tdefaultSize       = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color(\"8\")).Render)\n\tdefaultLineNumber = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color(\"8\")).Render)\n\tdefaultError      = toColor(lipgloss.NewStyle().Background(lipgloss.Color(\"196\")).Foreground(lipgloss.Color(\"255\")).Render)\n)\n\nvar (\n\tColon              string\n\tColonPreview       string\n\tComma              string\n\tCommaPreview       string\n\tEmpty              string\n\tDot3               string\n\tCloseCurlyBracket  string\n\tCloseSquareBracket string\n)\n\nvar NoColor = Theme{\n\tCursor:     defaultCursor,\n\tSyntax:     noColor,\n\tPreview:    noColor,\n\tStatusBar:  noColor,\n\tSearch:     defaultSearch,\n\tKey:        noColor,\n\tString:     noColor,\n\tNull:       noColor,\n\tBoolean:    noColor,\n\tNumber:     noColor,\n\tSize:       noColor,\n\tRef:        noColor,\n\tLineNumber: defaultLineNumber,\n\tError:      defaultError,\n}\n\nvar themes = map[string]Theme{\n\t\"0\": NoColor,\n\t\"1\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     noColor,\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        boldFg(\"4\"),\n\t\tString:     fg(\"2\"),\n\t\tNull:       defaultNull,\n\t\tBoolean:    fg(\"5\"),\n\t\tNumber:     fg(\"6\"),\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"2\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"2\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     noColor,\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        fg(\"2\"),\n\t\tString:     fg(\"4\"),\n\t\tNull:       defaultNull,\n\t\tBoolean:    fg(\"5\"),\n\t\tNumber:     fg(\"6\"),\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"4\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"3\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     noColor,\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        fg(\"13\"),\n\t\tString:     fg(\"11\"),\n\t\tNull:       defaultNull,\n\t\tBoolean:    fg(\"1\"),\n\t\tNumber:     fg(\"14\"),\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"11\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"4\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     noColor,\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        fg(\"#00F5D4\"),\n\t\tString:     fg(\"#00BBF9\"),\n\t\tNull:       defaultNull,\n\t\tBoolean:    fg(\"#F15BB5\"),\n\t\tNumber:     fg(\"#9B5DE5\"),\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"#00BBF9\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"5\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     noColor,\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        fg(\"#faf0ca\"),\n\t\tString:     fg(\"#f4d35e\"),\n\t\tNull:       defaultNull,\n\t\tBoolean:    fg(\"#ee964b\"),\n\t\tNumber:     fg(\"#ee964b\"),\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"#f4d35e\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"6\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     noColor,\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        fg(\"#4D96FF\"),\n\t\tString:     fg(\"#6BCB77\"),\n\t\tNull:       defaultNull,\n\t\tBoolean:    fg(\"#FF6B6B\"),\n\t\tNumber:     fg(\"#FFD93D\"),\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"#6BCB77\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"7\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     noColor,\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        boldFg(\"42\"),\n\t\tString:     boldFg(\"213\"),\n\t\tNull:       defaultNull,\n\t\tBoolean:    boldFg(\"201\"),\n\t\tNumber:     boldFg(\"201\"),\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"213\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"8\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     noColor,\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        boldFg(\"51\"),\n\t\tString:     fg(\"195\"),\n\t\tNull:       defaultNull,\n\t\tBoolean:    fg(\"50\"),\n\t\tNumber:     fg(\"123\"),\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"195\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"9\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     noColor,\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        boldFg(\"39\"), // deep sky blue\n\t\tString:     fg(\"49\"),     // spring green 2\n\t\tNull:       defaultNull,\n\t\tBoolean:    fg(\"205\"), // hot pink\n\t\tNumber:     fg(\"220\"), // gold\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"49\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"🔵\": {\n\t\tCursor: toColor(lipgloss.NewStyle().\n\t\t\tForeground(lipgloss.Color(\"15\")).\n\t\t\tBackground(lipgloss.Color(\"33\")).\n\t\t\tRender),\n\t\tSyntax:     boldFg(\"33\"),\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        fg(\"33\"),\n\t\tString:     noColor,\n\t\tNull:       noColor,\n\t\tBoolean:    noColor,\n\t\tNumber:     noColor,\n\t\tSize:       defaultSize,\n\t\tRef:        underline,\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"🥝\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     fg(\"179\"),\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        boldFg(\"154\"),\n\t\tString:     fg(\"82\"),\n\t\tNull:       fg(\"230\"),\n\t\tBoolean:    fg(\"226\"),\n\t\tNumber:     fg(\"226\"),\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"82\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"🔥\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     boldFg(\"208\"),\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        boldFg(\"202\"),\n\t\tString:     fg(\"214\"),\n\t\tNull:       defaultNull,\n\t\tBoolean:    fg(\"196\"),\n\t\tNumber:     fg(\"202\"),\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"214\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n\t\"🟣\": {\n\t\tCursor:     defaultCursor,\n\t\tSyntax:     noColor,\n\t\tPreview:    defaultPreview,\n\t\tStatusBar:  defaultStatusBar,\n\t\tSearch:     defaultSearch,\n\t\tKey:        boldFg(\"141\"), // orchid\n\t\tString:     fg(\"183\"),     // light pink/purple\n\t\tNull:       defaultNull,\n\t\tBoolean:    fg(\"81\"),  // cyan\n\t\tNumber:     fg(\"219\"), // light magenta\n\t\tSize:       defaultSize,\n\t\tRef:        underlineFg(\"183\"),\n\t\tLineNumber: defaultLineNumber,\n\t\tError:      defaultError,\n\t},\n}\n\nfunc noColor(s string) string {\n\treturn s\n}\n\nfunc toColor(f func(s ...string) string) Color {\n\treturn func(s string) string {\n\t\treturn f(s)\n\t}\n}\n\nfunc fg(color string) Color {\n\treturn toColor(lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render)\n}\n\nfunc underlineFg(color string) Color {\n\treturn toColor(lipgloss.NewStyle().Underline(true).Foreground(lipgloss.Color(color)).Render)\n}\n\nfunc boldFg(color string) Color {\n\treturn toColor(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(color)).Render)\n}\n\nfunc ThemeTester() {\n\tfor _, name := range themeNames {\n\t\tt := themes[name]\n\t\tcomma := t.Syntax(\",\")\n\t\tcolon := t.Syntax(\":\")\n\n\t\tfmt.Println(fmt.Sprintf(\"export FX_THEME=%q\", name))\n\t\tfmt.Println(t.Syntax(\"{\"))\n\n\t\tfmt.Printf(\"  %v%v %v%v\\n\",\n\t\t\tt.Key(\"\\\"string\\\"\"),\n\t\t\tcolon,\n\t\t\tt.String(\"\\\"Fox jumps over the lazy dog\\\"\"),\n\t\t\tcomma)\n\n\t\tfmt.Printf(\"  %v%v %v%v\\n\",\n\t\t\tt.Key(\"\\\"number\\\"\"),\n\t\t\tcolon,\n\t\t\tt.Number(\"1234567890\"),\n\t\t\tcomma)\n\n\t\tfmt.Printf(\"  %v%v %v%v\\n\",\n\t\t\tt.Key(\"\\\"boolean\\\"\"),\n\t\t\tcolon,\n\t\t\tt.Boolean(\"true\"),\n\t\t\tcomma)\n\t\tfmt.Printf(\"  %v%v %v%v\\n\",\n\t\t\tt.Key(\"\\\"null\\\"\"),\n\t\t\tcolon,\n\t\t\tt.Null(\"null\"),\n\t\t\tcomma)\n\t\tfmt.Printf(\"  %v%v %v%v%v\\n\",\n\t\t\tt.Key(\"\\\"collapsed\\\"\"),\n\t\t\tcolon,\n\t\t\tt.Syntax(\"{\"),\n\t\t\tt.Preview(\"\\\"preview\\\":…\"),\n\t\t\tt.Syntax(\"}\"),\n\t\t)\n\t\tfmt.Println(t.Syntax(\"}\"))\n\t\tprintln()\n\t}\n}\n\nfunc ExportThemes() {\n\tlipgloss.SetColorProfile(termenv.ANSI256) // Export in Terminal.app compatible colors\n\tplaceholder := \"_\"\n\textract := func(b string) string {\n\t\tmatches := regexp.\n\t\t\tMustCompile(`^\\x1b\\[(.+)m_`).\n\t\t\tFindStringSubmatch(b)\n\t\tif len(matches) == 0 {\n\t\t\treturn \"\"\n\t\t} else {\n\t\t\treturn matches[1]\n\t\t}\n\t}\n\tvar export = map[string][]string{}\n\tfor _, name := range themeNames {\n\t\tt := themes[name]\n\t\texport[name] = append(export[name], extract(t.Syntax(placeholder)))\n\t\texport[name] = append(export[name], extract(t.Key(placeholder)))\n\t\texport[name] = append(export[name], extract(t.String(placeholder)))\n\t\texport[name] = append(export[name], extract(t.Number(placeholder)))\n\t\texport[name] = append(export[name], extract(t.Boolean(placeholder)))\n\t\texport[name] = append(export[name], extract(t.Null(placeholder)))\n\t}\n\tdata, err := json.Marshal(export)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Println(string(data))\n}\n"
  },
  {
    "path": "internal/toml/toml.go",
    "content": "package toml\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"strings\"\n\n\ttoml \"github.com/pelletier/go-toml/v2\"\n\t\"github.com/pelletier/go-toml/v2/unstable\"\n\n\t\"github.com/antonmedv/fx/internal/engine\"\n)\n\ntype jnode interface{}\n\ntype jobject struct {\n\tfields []jfield\n}\n\ntype jfield struct {\n\tkey string\n\tval jnode\n}\n\ntype jarray struct {\n\telems []jnode\n}\n\nfunc ToJSON(in []byte) ([]byte, error) {\n\tvar typed any\n\tif err := toml.Unmarshal(in, &typed); err != nil {\n\t\tpanic(in)\n\t}\n\n\troot := &jobject{}\n\tp := unstable.Parser{}\n\tp.Reset(in)\n\n\taotActive := map[string]int{} // path -> current index for that AOT\n\taotCount := map[string]int{}  // path -> how many elems seen\n\tcurrentTablePath := []string{}\n\n\tfor p.NextExpression() {\n\t\te := p.Expression()\n\t\tswitch e.Kind {\n\t\tcase unstable.Table:\n\t\t\tcurrentTablePath = keyParts(e.Key())\n\t\t\t_ = ensureContainer(root, currentTablePath, aotActive)\n\n\t\tcase unstable.ArrayTable:\n\t\t\tpath := keyParts(e.Key())\n\t\t\tk := dot(path)\n\t\t\tidx := aotCount[k]\n\t\t\taotCount[k] = idx + 1\n\t\t\taotActive[k] = idx\n\t\t\tcurrentTablePath = path\n\t\t\t_ = ensureContainer(root, path, aotActive)\n\n\t\tcase unstable.KeyValue:\n\t\t\trel := keyParts(e.Key())\n\t\t\t// Resolve the actual typed value from the fully-decoded structure.\n\t\t\tval, ok := lookupTyped(typed, currentTablePath, rel, aotActive)\n\t\t\tif !ok {\n\t\t\t\tcontinue // skip gracefully on weird edge cases\n\t\t\t}\n\t\t\tobj := ensureContainer(root, currentTablePath, aotActive)\n\t\t\tsetNested(obj, rel, toJ(val))\n\t\t}\n\t}\n\tif err := p.Error(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := writeJSON(&buf, root); err != nil {\n\t\treturn nil, err\n\t}\n\treturn buf.Bytes(), nil\n}\n\nfunc keyParts(it unstable.Iterator) []string {\n\tvar out []string\n\tfor it.Next() {\n\t\tout = append(out, string(it.Node().Data))\n\t}\n\treturn out\n}\nfunc dot(parts []string) string { return strings.Join(parts, \".\") }\n\nfunc ensureContainer(root *jobject, tablePath []string, aotActive map[string]int) *jobject {\n\tcur := root\n\tif len(tablePath) == 0 {\n\t\treturn cur\n\t}\n\n\tprefix := []string{}\n\tfor i, seg := range tablePath {\n\t\tprefix = append(prefix, seg)\n\t\tpkey := dot(prefix)\n\n\t\t// If this prefix is an active AOT, ensure an array & select the element.\n\t\tif idx, ok := aotActive[pkey]; ok {\n\t\t\t// Ensure field seg is an array\n\t\t\tf, exists := getField(cur, seg)\n\t\t\tif !exists {\n\t\t\t\tf = jfield{key: seg, val: &jarray{}}\n\t\t\t\tcur.fields = append(cur.fields, f)\n\t\t\t}\n\t\t\tarr, ok := f.val.(*jarray)\n\t\t\tif !ok {\n\t\t\t\t// Convert/replace if necessary\n\t\t\t\tarr = &jarray{}\n\t\t\t\treplaceField(cur, seg, arr)\n\t\t\t}\n\t\t\t// Ensure element at idx is an object\n\t\t\tfor len(arr.elems) <= idx {\n\t\t\t\tarr.elems = append(arr.elems, &jobject{})\n\t\t\t}\n\t\t\telemObj, _ := arr.elems[idx].(*jobject)\n\t\t\tif elemObj == nil {\n\t\t\t\telemObj = &jobject{}\n\t\t\t\tarr.elems[idx] = elemObj\n\t\t\t}\n\t\t\tcur = elemObj\n\t\t\tcontinue\n\t\t}\n\n\t\t// Regular table segment: ensure an object at seg\n\t\tchild, ok := getField(cur, seg)\n\t\tif !ok {\n\t\t\tobj := &jobject{}\n\t\t\tcur.fields = append(cur.fields, jfield{key: seg, val: obj})\n\t\t\tcur = obj\n\t\t\tcontinue\n\t\t}\n\t\tif obj, ok := child.val.(*jobject); ok {\n\t\t\tcur = obj\n\t\t} else {\n\t\t\tnewObj := &jobject{}\n\t\t\treplaceField(cur, seg, newObj)\n\t\t\tcur = newObj\n\t\t}\n\n\t\t// If this isn't the last segment and we just ensured parent exists,\n\t\t// loop continues to drill down.\n\t\tif i == len(tablePath)-1 {\n\t\t\t// current table container reached\n\t\t}\n\t}\n\treturn cur\n}\n\nfunc setNested(obj *jobject, parts []string, val jnode) {\n\tif len(parts) == 0 {\n\t\treturn\n\t}\n\tcur := obj\n\tfor i := 0; i < len(parts)-1; i++ {\n\t\tk := parts[i]\n\t\tf, ok := getField(cur, k)\n\t\tif !ok {\n\t\t\tn := &jobject{}\n\t\t\tcur.fields = append(cur.fields, jfield{key: k, val: n})\n\t\t\tcur = n\n\t\t\tcontinue\n\t\t}\n\t\tif as, ok := f.val.(*jobject); ok {\n\t\t\tcur = as\n\t\t\tcontinue\n\t\t}\n\t\t// Replace if something else is there\n\t\tn := &jobject{}\n\t\treplaceField(cur, k, n)\n\t\tcur = n\n\t}\n\t// final key\n\tlast := parts[len(parts)-1]\n\tif _, ok := getField(cur, last); ok {\n\t\treplaceField(cur, last, val)\n\t} else {\n\t\tcur.fields = append(cur.fields, jfield{key: last, val: val})\n\t}\n}\n\nfunc getField(obj *jobject, key string) (jfield, bool) {\n\tfor _, f := range obj.fields {\n\t\tif f.key == key {\n\t\t\treturn f, true\n\t\t}\n\t}\n\treturn jfield{}, false\n}\nfunc replaceField(obj *jobject, key string, val jnode) {\n\tfor i := range obj.fields {\n\t\tif obj.fields[i].key == key {\n\t\t\tobj.fields[i].val = val\n\t\t\treturn\n\t\t}\n\t}\n\tobj.fields = append(obj.fields, jfield{key: key, val: val})\n}\n\n// Convert typed TOML -> jnode. Note: order inside *inline tables* cannot be\n// recovered from the typed map; if you need that too, we’d have to walk the\n// value expression via unstable APIs as well.\nfunc toJ(v any) jnode {\n\tswitch x := v.(type) {\n\tcase map[string]any:\n\t\tobj := &jobject{}\n\t\t// Map iteration order is undefined; this only affects inline tables.\n\t\tfor k, vv := range x {\n\t\t\tobj.fields = append(obj.fields, jfield{key: k, val: toJ(vv)})\n\t\t}\n\t\treturn obj\n\tcase []any:\n\t\tarr := &jarray{elems: make([]jnode, len(x))}\n\t\tfor i := range x {\n\t\t\tarr.elems[i] = toJ(x[i])\n\t\t}\n\t\treturn arr\n\tdefault:\n\t\treturn x // primitives, time.Time, etc. json.Marshal will handle them.\n\t}\n}\n\nfunc lookupTyped(typed any, tablePath, rel []string, aotActive map[string]int) (any, bool) {\n\tx := typed\n\tprefix := []string{}\n\t// Walk table path, applying AOT indices when present.\n\tfor _, k := range tablePath {\n\t\tmp, ok := asMap(x)\n\t\tif !ok {\n\t\t\treturn nil, false\n\t\t}\n\t\tvar ok2 bool\n\t\tx, ok2 = mp[k]\n\t\tif !ok2 {\n\t\t\treturn nil, false\n\t\t}\n\t\tprefix = append(prefix, k)\n\t\tif idx, ok := aotActive[dot(prefix)]; ok {\n\t\t\tarr, ok := x.([]any)\n\t\t\tif !ok || idx >= len(arr) {\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t\tx = arr[idx]\n\t\t}\n\t}\n\t// Then the dotted relative key\n\tfor _, k := range rel {\n\t\tmp, ok := asMap(x)\n\t\tif !ok {\n\t\t\treturn nil, false\n\t\t}\n\t\tvar ok2 bool\n\t\tx, ok2 = mp[k]\n\t\tif !ok2 {\n\t\t\treturn nil, false\n\t\t}\n\t}\n\treturn x, true\n}\n\nfunc asMap(x any) (map[string]any, bool) {\n\tif m, ok := x.(map[string]any); ok {\n\t\treturn m, true\n\t}\n\tif m, ok := x.(map[string]interface{}); ok {\n\t\treturn m, true\n\t}\n\treturn nil, false\n}\n\nfunc writeJSON(w io.Writer, n jnode) error {\n\tswitch v := n.(type) {\n\tcase *jobject:\n\t\tif _, err := io.WriteString(w, \"{\"); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor i, f := range v.fields {\n\t\t\tif i > 0 {\n\t\t\t\tif _, err := io.WriteString(w, \",\"); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\t// key\n\t\t\tkb, _ := json.Marshal(f.key)\n\t\t\tif _, err := w.Write(kb); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif _, err := io.WriteString(w, \":\"); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := writeJSON(w, f.val); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\t_, err := io.WriteString(w, \"}\")\n\t\treturn err\n\tcase *jarray:\n\t\tif _, err := io.WriteString(w, \"[\"); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor i, e := range v.elems {\n\t\t\tif i > 0 {\n\t\t\t\tif _, err := io.WriteString(w, \",\"); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err := writeJSON(w, e); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\t_, err := io.WriteString(w, \"]\")\n\t\treturn err\n\tdefault:\n\t\tif str, ok := v.(string); ok {\n\t\t\tquoted := engine.Quote(str)\n\t\t\t_, err := w.Write([]byte(quoted))\n\t\t\treturn err\n\t\t}\n\t\tb, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = w.Write(b)\n\t\treturn err\n\t}\n}\n"
  },
  {
    "path": "internal/toml/toml_test.go",
    "content": "package toml\n\nimport (\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n// helper: compare JSON as exact bytes\nfunc assertJSONBytesEqual(t *testing.T, got []byte, want string) {\n\tt.Helper()\n\trequire.Equal(t, want, string(got), \"unexpected JSON\")\n}\n\n// helper: compare JSON by unmarshalling into interface{} (order-insensitive for objects)\nfunc assertJSONStructEqual(t *testing.T, got []byte, want string) {\n\tt.Helper()\n\tvar g any\n\tvar w any\n\trequire.NoErrorf(t, json.Unmarshal(got, &g), \"json.Unmarshal(got) failed; json: %s\", string(got))\n\trequire.NoErrorf(t, json.Unmarshal([]byte(want), &w), \"json.Unmarshal(want) failed; json: %s\", want)\n\tif !reflect.DeepEqual(g, w) {\n\t\tgb, _ := json.Marshal(g)\n\t\twb, _ := json.Marshal(w)\n\t\trequire.Equal(t, string(wb), string(gb), \"JSON structures differ\")\n\t}\n}\n\nfunc TestToJSON_SimpleScalars(t *testing.T) {\n\tin := []byte(`a = 1\nb = \"x\"\nc = true\n`)\n\tgot, err := ToJSON(in)\n\trequire.NoError(t, err)\n\tassertJSONBytesEqual(t, got, `{\"a\":1,\"b\":\"x\",\"c\":true}`)\n}\n\nfunc TestToJSON_NestedTable(t *testing.T) {\n\tin := []byte(`[a]\nb = 1\n`)\n\tgot, err := ToJSON(in)\n\trequire.NoError(t, err)\n\tassertJSONBytesEqual(t, got, `{\"a\":{\"b\":1}}`)\n}\n\nfunc TestToJSON_DottedKeys(t *testing.T) {\n\tin := []byte(`a.b.c = 2\na.b.d = 3\n`)\n\tgot, err := ToJSON(in)\n\trequire.NoError(t, err)\n\tassertJSONBytesEqual(t, got, `{\"a\":{\"b\":{\"c\":2,\"d\":3}}}`)\n}\n\nfunc TestToJSON_ArraysAndInlineTables(t *testing.T) {\n\tin := []byte(`arr = [1, 2, \"x\"]\nobj = { b = 1, c = 2 }\n`)\n\tgot, err := ToJSON(in)\n\trequire.NoError(t, err)\n\t// Inline table field order is not guaranteed; compare structurally\n\tassertJSONStructEqual(t, got, `{\"arr\":[1,2,\"x\"],\"obj\":{\"b\":1,\"c\":2}}`)\n}\n\nfunc TestToJSON_ArraysOfTables(t *testing.T) {\n\tin := []byte(`[[fruit]]\nname = \"apple\"\n[fruit.variety]\nname = \"red delicious\"\n\n[[fruit]]\nname = \"banana\"\n`)\n\tgot, err := ToJSON(in)\n\trequire.NoError(t, err)\n\tassertJSONStructEqual(t, got, `{\n\t\t\"fruit\": [\n\t\t\t{\"name\":\"apple\",\"variety\":{\"name\":\"red delicious\"}},\n\t\t\t{\"name\":\"banana\"}\n\t\t]\n\t}`)\n}\n\nfunc TestToJSON_NestedAOT(t *testing.T) {\n\tin := []byte(`[a]\n[[a.b]]\nx = 1\n[[a.b]]\nx = 2\n`)\n\tgot, err := ToJSON(in)\n\trequire.NoError(t, err)\n\tassertJSONStructEqual(t, got, `{\"a\":{\"b\":[{\"x\":1},{\"x\":2}]}}`)\n}\n\nfunc TestToJSON_MixedTablesAndKeysOrder(t *testing.T) {\n\tin := []byte(`title = \"TOML Example\"\n\n[owner]\nname = \"Tom\"\nage = 42\n\n[database]\nserver = \"192.168.1.1\"\nports = [ 8001, 8001, 8002 ]\n`)\n\tgot, err := ToJSON(in)\n\trequire.NoError(t, err)\n\t// Expect compact JSON and correct nesting; top-level key order preserved\n\tassertJSONBytesEqual(t, got, `{\"title\":\"TOML Example\",\"owner\":{\"name\":\"Tom\",\"age\":42},\"database\":{\"server\":\"192.168.1.1\",\"ports\":[8001,8001,8002]}}`)\n}\n\nfunc TestToJSON_Datetime(t *testing.T) {\n\tin := []byte(`dt = 1979-05-27T07:32:00Z\n`)\n\tgot, err := ToJSON(in)\n\trequire.NoError(t, err)\n\t// Marshal time values usually as RFC3339 strings\n\tvar obj map[string]any\n\trequire.NoError(t, json.Unmarshal(got, &obj))\n\tv, ok := obj[\"dt\"].(string)\n\trequire.Truef(t, ok && v != \"\", \"expected dt to be a JSON string, got: %T %v\", obj[\"dt\"], obj[\"dt\"])\n}\n"
  },
  {
    "path": "internal/utils/image.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"image\"\n\t_ \"image/gif\"\n\t_ \"image/jpeg\"\n\t_ \"image/png\"\n\t\"io\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/nfnt/resize\"\n)\n\nfunc DrawImage(r io.Reader, width, height int) (string, error) {\n\timg, _, err := image.Decode(r)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// width = number of horizontal \"blocks\"\n\t// height = number of vertical \"blocks\"\n\t// each block is two pixels tall, so max pixel dims are:\n\tmaxW := uint(width)\n\tmaxH := uint(height * 2)\n\n\t// Use Thumbnail to resize into the bounding box [maxW × maxH], preserving aspect ratio\n\timg = resize.Thumbnail(maxW, maxH, img, resize.Lanczos3)\n\tbounds := img.Bounds()\n\n\tvar buffer bytes.Buffer\n\tfor y := bounds.Min.Y + 1; y < bounds.Max.Y-1; y += 2 {\n\t\tfor x := bounds.Min.X + 1; x < bounds.Max.X-1; x++ {\n\t\t\tr1, g1, b1, a1 := img.At(x, y+1).RGBA()\n\t\t\tr2, g2, b2, a2 := img.At(x, y).RGBA()\n\n\t\t\t// If both pixels are transparent, print a space.\n\t\t\tif a1 < 6553 && a2 < 6553 {\n\t\t\t\tbuffer.WriteByte(' ')\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcolorStr1 := fmt.Sprintf(\"#%02X%02X%02X\", r1>>8, g1>>8, b1>>8)\n\t\t\tcolorStr2 := fmt.Sprintf(\"#%02X%02X%02X\", r2>>8, g2>>8, b2>>8)\n\n\t\t\tblock := lipgloss.NewStyle().\n\t\t\t\tForeground(lipgloss.Color(colorStr1)).\n\t\t\t\tBackground(lipgloss.Color(colorStr2)).\n\t\t\t\tRender(\"▄\")\n\n\t\t\tbuffer.WriteString(block)\n\t\t}\n\t\tbuffer.WriteByte('\\n')\n\t}\n\treturn buffer.String(), nil\n}\n"
  },
  {
    "path": "internal/utils/life.go",
    "content": "package utils\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"os/signal\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/x/term\"\n)\n\nfunc GameOfLife() {\n\tw, rows, err := term.GetSize(os.Stdout.Fd())\n\tif err != nil || w <= 0 || rows <= 0 {\n\t\tw, rows = 80, 24\n\t}\n\th := rows * 2\n\tsize := w * h\n\ts := make([]bool, size)\n\n\tswitch rand.Int() % 3 {\n\tcase 0:\n\t\tfor i := 0; i < size; i++ {\n\t\t\tif rand.Float64() < 0.16 {\n\t\t\t\ts[i] = true\n\t\t\t}\n\t\t}\n\tcase 1:\n\t\tcx := w/2 - 6\n\t\tcy := h/2 - 7\n\t\ts[cx+1+(2+cy)*w] = true\n\t\ts[cx+2+(1+cy)*w] = true\n\t\ts[cx+2+(3+cy)*w] = true\n\t\ts[cx+3+(2+cy)*w] = true\n\t\ts[cx+5+(15+cy)*w] = true\n\t\ts[cx+6+(13+cy)*w] = true\n\t\ts[cx+6+(15+cy)*w] = true\n\t\ts[cx+7+(12+cy)*w] = true\n\t\ts[cx+7+(13+cy)*w] = true\n\t\ts[cx+7+(15+cy)*w] = true\n\t\ts[cx+9+(11+cy)*w] = true\n\t\ts[cx+9+(12+cy)*w] = true\n\t\ts[cx+9+(13+cy)*w] = true\n\tcase 2:\n\t\ts[1+5*w] = true\n\t\ts[1+6*w] = true\n\t\ts[2+5*w] = true\n\t\ts[2+6*w] = true\n\t\ts[12+5*w] = true\n\t\ts[12+6*w] = true\n\t\ts[12+7*w] = true\n\t\ts[13+4*w] = true\n\t\ts[13+8*w] = true\n\t\ts[14+3*w] = true\n\t\ts[14+9*w] = true\n\t\ts[15+4*w] = true\n\t\ts[15+8*w] = true\n\t\ts[16+5*w] = true\n\t\ts[16+6*w] = true\n\t\ts[16+7*w] = true\n\t\ts[17+5*w] = true\n\t\ts[17+6*w] = true\n\t\ts[17+7*w] = true\n\t\ts[22+3*w] = true\n\t\ts[22+4*w] = true\n\t\ts[22+5*w] = true\n\t\ts[23+2*w] = true\n\t\ts[23+3*w] = true\n\t\ts[23+5*w] = true\n\t\ts[23+6*w] = true\n\t\ts[24+2*w] = true\n\t\ts[24+3*w] = true\n\t\ts[24+5*w] = true\n\t\ts[24+6*w] = true\n\t\ts[25+2*w] = true\n\t\ts[25+3*w] = true\n\t\ts[25+4*w] = true\n\t\ts[25+5*w] = true\n\t\ts[25+6*w] = true\n\t\ts[26+w] = true\n\t\ts[26+2*w] = true\n\t\ts[26+6*w] = true\n\t\ts[26+7*w] = true\n\t\ts[35+3*w] = true\n\t\ts[35+4*w] = true\n\t\ts[36+3*w] = true\n\t\ts[36+4*w] = true\n\t}\n\n\tout := bufio.NewWriter(os.Stdout)\n\n\tesc := func(codes ...string) {\n\t\tfor _, c := range codes {\n\t\t\tfmt.Fprintf(out, \"\\x1b[%s\", c)\n\t\t}\n\t}\n\n\tesc(\"2J\", \"?25l\")\n\n\tsigc := make(chan os.Signal, 1)\n\tsignal.Notify(sigc, os.Interrupt)\n\tgo func() {\n\t\t<-sigc\n\t\tesc(\"?25h\")\n\t\tout.Flush()\n\t\tfmt.Printf(\"\\n\")\n\t\tos.Exit(2)\n\t}()\n\n\tat := func(i, j int) bool {\n\t\tif i < 0 {\n\t\t\ti = h - 1\n\t\t}\n\t\tif i >= h {\n\t\t\ti = 0\n\t\t}\n\t\tif j < 0 {\n\t\t\tj = w - 1\n\t\t}\n\t\tif j >= w {\n\t\t\tj = 0\n\t\t}\n\t\treturn s[i*w+j]\n\t}\n\n\tfullBlock := \"\\u2588\"\n\ttopHalf := \"\\u2580\"\n\tbotHalf := \"\\u2584\"\n\n\tticker := time.NewTicker(30 * time.Millisecond)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\t<-ticker.C\n\n\t\tesc(\"H\")\n\n\t\tgen := make([]bool, size)\n\t\tfor i := h - 1; i >= 0; i-- {\n\t\t\tfor j := w - 1; j >= 0; j-- {\n\t\t\t\tn := 0\n\t\t\t\tif at(i-1, j-1) {\n\t\t\t\t\tn++\n\t\t\t\t}\n\t\t\t\tif at(i-1, j) {\n\t\t\t\t\tn++\n\t\t\t\t}\n\t\t\t\tif at(i-1, j+1) {\n\t\t\t\t\tn++\n\t\t\t\t}\n\t\t\t\tif at(i, j-1) {\n\t\t\t\t\tn++\n\t\t\t\t}\n\t\t\t\tif at(i, j+1) {\n\t\t\t\t\tn++\n\t\t\t\t}\n\t\t\t\tif at(i+1, j-1) {\n\t\t\t\t\tn++\n\t\t\t\t}\n\t\t\t\tif at(i+1, j) {\n\t\t\t\t\tn++\n\t\t\t\t}\n\t\t\t\tif at(i+1, j+1) {\n\t\t\t\t\tn++\n\t\t\t\t}\n\t\t\t\tz := i*w + j\n\t\t\t\tif s[z] {\n\t\t\t\t\tgen[z] = n == 2 || n == 3\n\t\t\t\t} else {\n\t\t\t\t\tgen[z] = n == 3\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ts = gen\n\n\t\tfor i := 0; i < rows; i++ {\n\t\t\tfor j := 0; j < w; j++ {\n\t\t\t\ttop := s[i*2*w+j]\n\t\t\t\tbot := s[(i*2+1)*w+j]\n\t\t\t\tswitch {\n\t\t\t\tcase top && bot:\n\t\t\t\t\tout.WriteString(fullBlock)\n\t\t\t\tcase top && !bot:\n\t\t\t\t\tout.WriteString(topHalf)\n\t\t\t\tcase !top && bot:\n\t\t\t\t\tout.WriteString(botHalf)\n\t\t\t\tdefault:\n\t\t\t\t\tout.WriteByte(' ')\n\t\t\t\t}\n\t\t\t}\n\t\t\tif i != rows-1 {\n\t\t\t\tout.WriteByte('\\n')\n\t\t\t}\n\t\t}\n\t\tout.Flush()\n\t}\n}\n"
  },
  {
    "path": "internal/utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"encoding/json\"\n)\n\nfunc IsHexDigit(ch byte) bool {\n\treturn (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')\n}\n\nfunc IsDigit(ch byte) bool {\n\treturn ch >= '0' && ch <= '9'\n}\n\nfunc Contains(needle int, haystack []int) bool {\n\tfor _, i := range haystack {\n\t\tif needle == i {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc Unquote(s string) (string, error) {\n\tvar unquoted string\n\terr := json.Unmarshal([]byte(s), &unquoted)\n\treturn unquoted, err\n}\n"
  },
  {
    "path": "internal/utils/utils_test.go",
    "content": "package utils\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsHexDigit(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    byte\n\t\texpected bool\n\t}{\n\t\t{\"digit 0\", '0', true},\n\t\t{\"digit 5\", '5', true},\n\t\t{\"digit 9\", '9', true},\n\t\t{\"lowercase a\", 'a', true},\n\t\t{\"lowercase f\", 'f', true},\n\t\t{\"uppercase A\", 'A', true},\n\t\t{\"uppercase F\", 'F', true},\n\t\t{\"lowercase g\", 'g', false},\n\t\t{\"uppercase G\", 'G', false},\n\t\t{\"space\", ' ', false},\n\t\t{\"lowercase z\", 'z', false},\n\t\t{\"special char\", '@', false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := IsHexDigit(tc.input)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestIsDigit(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    byte\n\t\texpected bool\n\t}{\n\t\t{\"digit 0\", '0', true},\n\t\t{\"digit 5\", '5', true},\n\t\t{\"digit 9\", '9', true},\n\t\t{\"lowercase a\", 'a', false},\n\t\t{\"uppercase A\", 'A', false},\n\t\t{\"space\", ' ', false},\n\t\t{\"special char\", '-', false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := IsDigit(tc.input)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestContains(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tneedle   int\n\t\thaystack []int\n\t\texpected bool\n\t}{\n\t\t{\"found at start\", 1, []int{1, 2, 3}, true},\n\t\t{\"found in middle\", 2, []int{1, 2, 3}, true},\n\t\t{\"found at end\", 3, []int{1, 2, 3}, true},\n\t\t{\"not found\", 4, []int{1, 2, 3}, false},\n\t\t{\"empty slice\", 1, []int{}, false},\n\t\t{\"single element found\", 1, []int{1}, true},\n\t\t{\"single element not found\", 2, []int{1}, false},\n\t\t{\"negative number found\", -1, []int{-1, 0, 1}, true},\n\t\t{\"zero found\", 0, []int{-1, 0, 1}, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := Contains(tc.needle, tc.haystack)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestUnquote(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t\thasError bool\n\t}{\n\t\t{\"simple string\", `\"hello\"`, \"hello\", false},\n\t\t{\"empty string\", `\"\"`, \"\", false},\n\t\t{\"with spaces\", `\"hello world\"`, \"hello world\", false},\n\t\t{\"with escape\", `\"hello\\nworld\"`, \"hello\\nworld\", false},\n\t\t{\"with tab\", `\"hello\\tworld\"`, \"hello\\tworld\", false},\n\t\t{\"with unicode\", `\"hello \\u4e16\\u754c\"`, \"hello 世界\", false},\n\t\t{\"with quotes inside\", `\"hello \\\"world\\\"\"`, `hello \"world\"`, false},\n\t\t{\"with backslash\", `\"hello\\\\world\"`, `hello\\world`, false},\n\t\t{\"invalid json\", `hello`, \"\", true},\n\t\t{\"number\", `123`, \"\", true},\n\t\t{\"unclosed quote\", `\"hello`, \"\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult, err := Unquote(tc.input)\n\t\t\tif tc.hasError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "keymap.go",
    "content": "package main\n\nimport \"github.com/charmbracelet/bubbles/key\"\n\ntype KeyMap struct {\n\tUp                  key.Binding `category:\"Navigation\"`\n\tDown                key.Binding `category:\"Navigation\"`\n\tPageUp              key.Binding `category:\"Navigation\"`\n\tPageDown            key.Binding `category:\"Navigation\"`\n\tHalfPageUp          key.Binding `category:\"Navigation\"`\n\tHalfPageDown        key.Binding `category:\"Navigation\"`\n\tGotoTop             key.Binding `category:\"Navigation\"`\n\tGotoBottom          key.Binding `category:\"Navigation\"`\n\tNextSibling         key.Binding `category:\"Navigation\"`\n\tPrevSibling         key.Binding `category:\"Navigation\"`\n\tExpand              key.Binding `category:\"Expand / Collapse\"`\n\tCollapse            key.Binding `category:\"Expand / Collapse\"`\n\tExpandRecursively   key.Binding `category:\"Expand / Collapse\"`\n\tCollapseRecursively key.Binding `category:\"Expand / Collapse\"`\n\tExpandAll           key.Binding `category:\"Expand / Collapse\"`\n\tCollapseAll         key.Binding `category:\"Expand / Collapse\"`\n\tCollapseLevel       key.Binding `category:\"Expand / Collapse\"`\n\tSearch              key.Binding `category:\"Search\"`\n\tSearchNext          key.Binding `category:\"Search\"`\n\tSearchPrev          key.Binding `category:\"Search\"`\n\tGotoSymbol          key.Binding `category:\"Search\"`\n\tGotoRef             key.Binding `category:\"Search\"`\n\tYank                key.Binding `category:\"Actions\"`\n\tDelete              key.Binding `category:\"Actions\"`\n\tPreview             key.Binding `category:\"Actions\"`\n\tPrint               key.Binding `category:\"Actions\"`\n\tOpen                key.Binding `category:\"Actions\"`\n\tToggleWrap          key.Binding `category:\"View\"`\n\tShowSelector        key.Binding `category:\"View\"`\n\tGoBack              key.Binding `category:\"Navigation\"`\n\tGoForward           key.Binding `category:\"Navigation\"`\n\tHelp                key.Binding `category:\"Other\"`\n\tCommandLine         key.Binding `category:\"Other\"`\n\tQuit                key.Binding `category:\"Other\"`\n\tSuspend             key.Binding `category:\"Other\"`\n}\n\nvar keyMap KeyMap\n\nfunc init() {\n\tkeyMap = KeyMap{\n\t\tQuit: key.NewBinding(\n\t\t\tkey.WithKeys(\"q\", \"ctrl+c\", \"esc\"),\n\t\t\tkey.WithHelp(\"\", \"exit program\"),\n\t\t),\n\t\tSuspend: key.NewBinding(\n\t\t\tkey.WithKeys(\"ctrl+z\"),\n\t\t\tkey.WithHelp(\"\", \"suspend program\"),\n\t\t),\n\t\tPageDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"pgdown\", \" \", \"f\"),\n\t\t\tkey.WithHelp(\"pgdown, space, f\", \"page down\"),\n\t\t),\n\t\tPageUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"pgup\", \"b\"),\n\t\t\tkey.WithHelp(\"pgup, b\", \"page up\"),\n\t\t),\n\t\tHalfPageUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"ctrl+u\"),\n\t\t\tkey.WithHelp(\"\", \"half page up\"),\n\t\t),\n\t\tHalfPageDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"ctrl+d\"),\n\t\t\tkey.WithHelp(\"\", \"half page down\"),\n\t\t),\n\t\tGotoTop: key.NewBinding(\n\t\t\tkey.WithKeys(\"g\", \"home\"),\n\t\t\tkey.WithHelp(\"\", \"goto top\"),\n\t\t),\n\t\tGotoBottom: key.NewBinding(\n\t\t\tkey.WithKeys(\"G\", \"end\"),\n\t\t\tkey.WithHelp(\"\", \"goto bottom\"),\n\t\t),\n\t\tGotoSymbol: key.NewBinding(\n\t\t\tkey.WithKeys(\"@\"),\n\t\t\tkey.WithHelp(\"\", \"goto symbol\"),\n\t\t),\n\t\tGotoRef: key.NewBinding(\n\t\t\tkey.WithKeys(\"ctrl+g\"),\n\t\t\tkey.WithHelp(\"\", \"goto ref\"),\n\t\t),\n\t\tDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"down\", \"j\"),\n\t\t\tkey.WithHelp(\"\", \"down\"),\n\t\t),\n\t\tUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"up\", \"k\"),\n\t\t\tkey.WithHelp(\"\", \"up\"),\n\t\t),\n\t\tHelp: key.NewBinding(\n\t\t\tkey.WithKeys(\"?\"),\n\t\t\tkey.WithHelp(\"\", \"show help\"),\n\t\t),\n\t\tExpand: key.NewBinding(\n\t\t\tkey.WithKeys(\"right\", \"l\", \"enter\"),\n\t\t\tkey.WithHelp(\"\", \"expand\"),\n\t\t),\n\t\tCollapse: key.NewBinding(\n\t\t\tkey.WithKeys(\"left\", \"h\", \"backspace\"),\n\t\t\tkey.WithHelp(\"\", \"collapse\"),\n\t\t),\n\t\tExpandRecursively: key.NewBinding(\n\t\t\tkey.WithKeys(\"L\", \"shift+right\"),\n\t\t\tkey.WithHelp(\"\", \"expand recursively\"),\n\t\t),\n\t\tCollapseRecursively: key.NewBinding(\n\t\t\tkey.WithKeys(\"H\", \"shift+left\"),\n\t\t\tkey.WithHelp(\"\", \"collapse recursively\"),\n\t\t),\n\t\tExpandAll: key.NewBinding(\n\t\t\tkey.WithKeys(\"e\"),\n\t\t\tkey.WithHelp(\"\", \"expand all\"),\n\t\t),\n\t\tCollapseAll: key.NewBinding(\n\t\t\tkey.WithKeys(\"E\"),\n\t\t\tkey.WithHelp(\"\", \"collapse all\"),\n\t\t),\n\t\tCollapseLevel: key.NewBinding(\n\t\t\tkey.WithKeys(\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"),\n\t\t\tkey.WithHelp(\"\", \"collapse to nth level\"),\n\t\t),\n\t\tNextSibling: key.NewBinding(\n\t\t\tkey.WithKeys(\"J\", \"shift+down\"),\n\t\t\tkey.WithHelp(\"\", \"next sibling\"),\n\t\t),\n\t\tPrevSibling: key.NewBinding(\n\t\t\tkey.WithKeys(\"K\", \"shift+up\"),\n\t\t\tkey.WithHelp(\"\", \"previous sibling\"),\n\t\t),\n\t\tToggleWrap: key.NewBinding(\n\t\t\tkey.WithKeys(\"z\"),\n\t\t\tkey.WithHelp(\"\", \"toggle strings wrap\"),\n\t\t),\n\t\tShowSelector: key.NewBinding(\n\t\t\tkey.WithKeys(\"s\"),\n\t\t\tkey.WithHelp(\"\", \"show sizes/line numbers\"),\n\t\t),\n\t\tYank: key.NewBinding(\n\t\t\tkey.WithKeys(\"y\"),\n\t\t\tkey.WithHelp(\"\", \"yank/copy\"),\n\t\t),\n\t\tDelete: key.NewBinding(\n\t\t\tkey.WithKeys(\"d\"),\n\t\t\tkey.WithHelp(\"\", \"delete node\"),\n\t\t),\n\t\tCommandLine: key.NewBinding(\n\t\t\tkey.WithKeys(\":\"),\n\t\t\tkey.WithHelp(\"\", \"open command line\"),\n\t\t),\n\t\tSearch: key.NewBinding(\n\t\t\tkey.WithKeys(\"/\"),\n\t\t\tkey.WithHelp(\"\", \"search regexp\"),\n\t\t),\n\t\tSearchNext: key.NewBinding(\n\t\t\tkey.WithKeys(\"n\"),\n\t\t\tkey.WithHelp(\"\", \"next search result\"),\n\t\t),\n\t\tSearchPrev: key.NewBinding(\n\t\t\tkey.WithKeys(\"N\"),\n\t\t\tkey.WithHelp(\"\", \"prev search result\"),\n\t\t),\n\t\tPreview: key.NewBinding(\n\t\t\tkey.WithKeys(\"p\"),\n\t\t\tkey.WithHelp(\"\", \"preview node\"),\n\t\t),\n\t\tPrint: key.NewBinding(\n\t\t\tkey.WithKeys(\"P\"),\n\t\t\tkey.WithHelp(\"\", \"print to stdout\"),\n\t\t),\n\t\tOpen: key.NewBinding(\n\t\t\tkey.WithKeys(\"v\"),\n\t\t\tkey.WithHelp(\"\", \"open in editor\"),\n\t\t),\n\t\tGoBack: key.NewBinding(\n\t\t\tkey.WithKeys(\"[\"),\n\t\t\tkey.WithHelp(\"\", \"go back\"),\n\t\t),\n\t\tGoForward: key.NewBinding(\n\t\t\tkey.WithKeys(\"]\"),\n\t\t\tkey.WithHelp(\"\", \"go forward\"),\n\t\t),\n\t}\n}\n\nvar (\n\tyankValueY      = key.NewBinding(key.WithKeys(\"y\"))\n\tyankValueV      = key.NewBinding(key.WithKeys(\"v\"))\n\tyankKey         = key.NewBinding(key.WithKeys(\"k\"))\n\tyankPath        = key.NewBinding(key.WithKeys(\"p\"))\n\tyankKeyValue    = key.NewBinding(key.WithKeys(\"b\"))\n\tarrowUp         = key.NewBinding(key.WithKeys(\"up\"))\n\tarrowDown       = key.NewBinding(key.WithKeys(\"down\"))\n\tshowSizes       = key.NewBinding(key.WithKeys(\"s\"))\n\tshowLineNumbers = key.NewBinding(key.WithKeys(\"l\"))\n)\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime/pprof\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/antonmedv/clipboard\"\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/bubbles/spinner\"\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"github.com/charmbracelet/bubbles/viewport\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/mattn/go-isatty\"\n\n\t\"github.com/antonmedv/fx/internal/complete\"\n\t\"github.com/antonmedv/fx/internal/engine\"\n\t\"github.com/antonmedv/fx/internal/fuzzy\"\n\t\"github.com/antonmedv/fx/internal/jsonpath\"\n\t. \"github.com/antonmedv/fx/internal/jsonx\"\n\t\"github.com/antonmedv/fx/internal/theme\"\n\t\"github.com/antonmedv/fx/internal/toml\"\n\t\"github.com/antonmedv/fx/internal/utils\"\n)\n\nvar (\n\tflagYaml     bool\n\tflagToml     bool\n\tflagRaw      bool\n\tflagSlurp    bool\n\tflagComp     bool\n\tflagStrict   bool\n\tflagNoInline bool\n)\n\nvar flags = []string{\n\t\"--help\",\n\t\"--raw\",\n\t\"--slurp\",\n\t\"--themes\",\n\t\"--version\",\n\t\"--yaml\",\n\t\"--toml\",\n\t\"--strict\",\n\t\"--no-inline\",\n}\n\nfunc init() {\n\tfor _, name := range flags {\n\t\tcomplete.Flags = append(complete.Flags, complete.Reply{name, name, \"flag\"})\n\t}\n}\n\nfunc main() {\n\tif _, ok := os.LookupEnv(\"FX_PPROF\"); ok {\n\t\tf, err := os.Create(\"cpu.prof\")\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\terr = pprof.StartCPUProfile(f)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tdefer f.Close()\n\t\tdefer pprof.StopCPUProfile()\n\t\tmemProf, err := os.Create(\"mem.prof\")\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tdefer memProf.Close()\n\t\tdefer pprof.WriteHeapProfile(memProf)\n\t}\n\n\tif complete.Complete() {\n\t\tos.Exit(0)\n\t\treturn\n\t}\n\n\tvar args []string\n\tfor _, arg := range os.Args[1:] {\n\t\tif strings.HasPrefix(arg, \"--comp\") {\n\t\t\tflagComp = true\n\t\t\tcontinue\n\t\t}\n\t\tswitch arg {\n\t\tcase \"-h\", \"--help\":\n\t\t\tfmt.Println(usage())\n\t\t\treturn\n\t\tcase \"-v\", \"-V\", \"--version\":\n\t\t\tfmt.Println(version)\n\t\t\treturn\n\t\tcase \"--themes\":\n\t\t\ttheme.ThemeTester()\n\t\t\treturn\n\t\tcase \"--export-themes\":\n\t\t\ttheme.ExportThemes()\n\t\t\treturn\n\t\tcase \"--yaml\":\n\t\t\tflagYaml = true\n\t\tcase \"--toml\":\n\t\t\tflagToml = true\n\t\tcase \"--raw\", \"-r\":\n\t\t\tflagRaw = true\n\t\tcase \"--slurp\", \"-s\":\n\t\t\tflagSlurp = true\n\t\tcase \"-rs\", \"-sr\":\n\t\t\tflagRaw = true\n\t\t\tflagSlurp = true\n\t\tcase \"--strict\":\n\t\t\tflagStrict = true\n\t\tcase \"--no-inline\":\n\t\t\tflagNoInline = true\n\t\tcase \"--game-of-life\":\n\t\t\tutils.GameOfLife()\n\t\t\treturn\n\t\tdefault:\n\t\t\targs = append(args, arg)\n\t\t}\n\t}\n\n\tif (flagYaml || flagToml) && flagRaw {\n\t\tprintln(\"Error: can't use --yaml/--toml and --raw flags together\")\n\t\tos.Exit(1)\n\t}\n\tif flagYaml && flagToml {\n\t\tprintln(\"Error: can't use both --yaml and --toml flags together\")\n\t\tos.Exit(1)\n\t}\n\n\tif flagComp {\n\t\tshell := flag.String(\"comp\", \"\", \"\")\n\t\tflag.Parse()\n\t\tswitch *shell {\n\t\tcase \"bash\":\n\t\t\tfmt.Print(complete.Bash)\n\t\tcase \"zsh\":\n\t\t\tfmt.Print(complete.Zsh)\n\t\tcase \"fish\":\n\t\t\tfmt.Print(complete.Fish)\n\t\tdefault:\n\t\t\tfmt.Println(\"unknown shell type\")\n\t\t}\n\t\treturn\n\t}\n\n\tfd := os.Stdin.Fd()\n\tstdinIsTty := isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)\n\n\tvar fileName string\n\tvar src io.Reader\n\n\tif stdinIsTty {\n\t\tif len(args) == 0 {\n\t\t\t// $ fx\n\t\t\tfmt.Println(usage())\n\t\t\treturn\n\t\t} else {\n\t\t\t// $ fx file.json arg*\n\t\t\tfilePath := args[0]\n\t\t\tsrc = open(filePath, &flagYaml, &flagToml)\n\t\t\tengine.FilePath = filePath\n\t\t\tfileName = filepath.Base(filePath)\n\t\t\targs = args[1:]\n\t\t}\n\t} else {\n\t\t// cat file.json | fx arg*\n\t\tsrc = os.Stdin\n\t}\n\n\tvar parser engine.Parser\n\n\tif flagYaml {\n\t\tb, err := io.ReadAll(src)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tjsonBytes, err := parseYAML(b)\n\t\tif err != nil {\n\t\t\tfmt.Print(err.Error())\n\t\t\tos.Exit(1)\n\t\t\treturn\n\t\t}\n\t\tparser = NewJsonParser(bytes.NewReader(jsonBytes), flagStrict)\n\t} else if flagToml {\n\t\tb, err := io.ReadAll(src)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tjsonBytes, err := toml.ToJSON(b)\n\t\tif err != nil {\n\t\t\tfmt.Print(err.Error())\n\t\t\tos.Exit(1)\n\t\t\treturn\n\t\t}\n\t\tparser = NewJsonParser(bytes.NewReader(jsonBytes), flagStrict)\n\t} else if flagRaw {\n\t\tparser = NewLineParser(src)\n\t} else {\n\t\tparser = NewJsonParser(src, flagStrict)\n\t}\n\n\tif len(args) > 0 || flagSlurp {\n\t\topts := engine.Options{\n\t\t\tSlurp:      flagSlurp,\n\t\t\tWithInline: !flagNoInline,\n\t\t\tWriteOut:   func(s string) { fmt.Println(s) },\n\t\t\tWriteErr:   func(s string) { fmt.Fprintln(os.Stderr, s) },\n\t\t}\n\t\texitCode := engine.Start(parser, args, opts)\n\t\tif exitCode != 0 {\n\t\t\tos.Exit(exitCode)\n\t\t}\n\t\treturn\n\t}\n\n\tcommandInput := textinput.New()\n\tcommandInput.Prompt = \":\"\n\n\tsearchInput := textinput.New()\n\tsearchInput.Prompt = \"/\"\n\n\tgotoSymbolInput := textinput.New()\n\tgotoSymbolInput.Prompt = \"@\"\n\n\tpreviewSearchInput := textinput.New()\n\tpreviewSearchInput.Prompt = \"/\"\n\n\tspinnerModel := spinner.New()\n\tspinnerModel.Spinner = spinner.MiniDot\n\n\tcollapsed := false\n\tif _, ok := os.LookupEnv(\"FX_COLLAPSED\"); ok {\n\t\tcollapsed = true\n\t}\n\n\tshowLineNumbers := false\n\tif _, ok := os.LookupEnv(\"FX_LINE_NUMBERS\"); ok {\n\t\tshowLineNumbers = true\n\t}\n\n\tshowSizes := false\n\tshowSizesValue, ok := os.LookupEnv(\"FX_SHOW_SIZE\")\n\tif ok {\n\t\tshowSizesValue := strings.ToLower(showSizesValue)\n\t\tshowSizes = showSizesValue == \"true\" || showSizesValue == \"yes\" || showSizesValue == \"on\" || showSizesValue == \"1\"\n\t}\n\n\tm := &model{\n\t\tsuspending:          false,\n\t\tshowCursor:          true,\n\t\twrap:                true,\n\t\tcollapsed:           collapsed,\n\t\tshowSizes:           showSizes,\n\t\tshowLineNumbers:     showLineNumbers,\n\t\tfileName:            fileName,\n\t\tgotoSymbolInput:     gotoSymbolInput,\n\t\tcommandInput:        commandInput,\n\t\tsearchInput:         searchInput,\n\t\tsearch:              newSearch(),\n\t\tpreviewSearchInput:  previewSearchInput,\n\t\tpreviewSearchCursor: -1,\n\t\tspinner:             spinnerModel,\n\t}\n\n\tlipgloss.SetColorProfile(theme.TermOutput.ColorProfile())\n\n\twithMouse := tea.WithMouseCellMotion()\n\tif _, ok := os.LookupEnv(\"FX_NO_MOUSE\"); ok {\n\t\twithMouse = tea.WithAltScreen()\n\t}\n\n\tp := tea.NewProgram(m,\n\t\ttea.WithAltScreen(),\n\t\twithMouse,\n\t\ttea.WithOutput(os.Stderr),\n\t)\n\n\tgo func() {\n\t\tfirstOk := false\n\t\tfor {\n\t\t\tnode, err := parser.Parse()\n\t\t\tif err != nil {\n\t\t\t\tif err == io.EOF {\n\t\t\t\t\tp.Send(eofMsg{})\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tif flagStrict {\n\t\t\t\t\tp.Send(errorMsg{err: err})\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\ttextNode := parser.Recover()\n\t\t\t\tif !firstOk && !strings.HasPrefix(textNode.Value, \"HTTP\") {\n\t\t\t\t\tp.Send(errorMsg{err: err})\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tp.Send(nodeMsg{node: textNode})\n\t\t\t} else {\n\t\t\t\tfirstOk = true\n\t\t\t\tp.Send(nodeMsg{node: node})\n\t\t\t}\n\t\t}\n\t}()\n\n\t_, err := p.Run()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif m.printErrorOnExit != nil {\n\t\tfmt.Println(m.printErrorOnExit.Error())\n\t} else if m.printOnExit {\n\t\tfmt.Println(m.cursorValue())\n\t} else {\n\t\texit()\n\t}\n}\n\ntype model struct {\n\ttermWidth, termHeight int\n\thead, top, bottom     *Node\n\teof                   bool\n\tcursor                int // cursor position [0, termHeight)\n\tsuspending            bool\n\tshowCursor            bool\n\twrap                  bool\n\tcollapsed             bool\n\tshowShowSelector      bool\n\tshowSizes             bool\n\tshowLineNumbers       bool\n\ttotalLines            int\n\tfileName              string\n\tgotoSymbolInput       textinput.Model\n\tcommandInput          textinput.Model\n\tsearchInput           textinput.Model\n\tsearch                *search\n\tsearching             bool          // search in progress\n\tsearchCancel          chan struct{} // cancel channel for search\n\tsearchID              uint64        // increments with each search to detect stale results\n\tyank                  bool\n\tshowHelp              bool\n\thelp                  viewport.Model\n\tshowPreview           bool\n\tpreview               viewport.Model\n\tpreviewValue          string\n\tpreviewSearchInput    textinput.Model\n\tpreviewSearchResults  []int\n\tpreviewSearchCursor   int\n\tprintOnExit           bool\n\tprintErrorOnExit      error\n\tspinner               spinner.Model\n\tlocationHistory       []location\n\tlocationIndex         int // position in locationHistory\n\tkeysIndex             []string\n\tkeysIndexNodes        []*Node\n\tfuzzyMatch            *fuzzy.Match\n\tdeletePending         bool\n}\n\ntype location struct {\n\thead *Node\n\tnode *Node\n}\n\ntype nodeMsg struct {\n\tnode *Node\n}\n\ntype errorMsg struct {\n\terr error\n}\n\ntype eofMsg struct{}\n\ntype searchResultMsg struct {\n\tid     uint64\n\tquery  string\n\tsearch *search\n}\n\ntype searchCancelledMsg struct {\n\tid uint64\n}\n\nfunc (m *model) Init() tea.Cmd {\n\treturn m.spinner.Tick\n}\n\nfunc (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tswitch msg := msg.(type) {\n\tcase tea.WindowSizeMsg:\n\t\tm.termWidth = msg.Width\n\t\tm.termHeight = msg.Height\n\t\tm.help.Width = m.termWidth\n\t\tm.help.Height = m.termHeight - 1\n\t\tm.preview.Width = m.termWidth\n\t\tm.preview.Height = m.termHeight - 1\n\t\tWrap(m.top, m.viewWidth())\n\t\tm.redoSearch()\n\n\tcase eofMsg:\n\t\tm.eof = true\n\t\treturn m, nil\n\n\tcase errorMsg:\n\t\tm.printErrorOnExit = msg.err\n\t\treturn m, tea.Quit\n\n\tcase nodeMsg:\n\t\tif m.wrap {\n\t\t\tWrap(msg.node, m.viewWidth())\n\t\t}\n\t\tif m.collapsed {\n\t\t\tmsg.node.CollapseRecursively()\n\t\t}\n\t\tm.totalLines = msg.node.Bottom().LineNumber\n\n\t\tif m.head == nil {\n\t\t\tm.head = msg.node\n\t\t\tm.top = msg.node\n\t\t\tm.bottom = msg.node\n\t\t} else {\n\t\t\tto, ok := m.cursorPointsTo()\n\t\t\tif !ok {\n\t\t\t\treturn m, nil\n\t\t\t}\n\t\t\tscrollToBottom := to == m.bottom.Bottom()\n\t\t\tmsg.node.Index = -1 // To fix the statusbar path (to show .key instead of [0].key).\n\t\t\tm.bottom.Adjacent(msg.node)\n\t\t\tm.bottom = msg.node\n\t\t\tif scrollToBottom {\n\t\t\t\tm.scrollToBottom()\n\t\t\t}\n\t\t}\n\t\treturn m, nil\n\n\tcase spinner.TickMsg:\n\t\tif !m.eof || m.searching {\n\t\t\tvar cmd tea.Cmd\n\t\t\tm.spinner, cmd = m.spinner.Update(msg)\n\t\t\treturn m, cmd\n\t\t}\n\n\tcase searchResultMsg:\n\t\tif msg.id != m.searchID {\n\t\t\treturn m, nil\n\t\t}\n\t\tm.searching = false\n\t\tm.searchCancel = nil\n\t\tif msg.search != nil {\n\t\t\tm.search = msg.search\n\t\t\tm.selectSearchResult(0)\n\t\t}\n\t\treturn m, nil\n\n\tcase searchCancelledMsg:\n\t\tif msg.id != m.searchID {\n\t\t\treturn m, nil\n\t\t}\n\t\tm.searching = false\n\t\tm.searchCancel = nil\n\t\treturn m, nil\n\n\tcase tea.ResumeMsg:\n\t\tm.suspending = false\n\t\treturn m, nil\n\t}\n\n\tif m.showHelp {\n\t\treturn m.handleHelpKey(msg)\n\t}\n\n\tif m.showPreview {\n\t\treturn m.handlePreviewKey(msg)\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.MouseMsg:\n\t\tm.handlePendingDelete(msg)\n\n\t\tswitch {\n\t\tcase msg.Button == tea.MouseButtonWheelUp:\n\t\t\tm.up()\n\n\t\tcase msg.Button == tea.MouseButtonWheelDown:\n\t\t\tm.down()\n\n\t\tcase msg.Button == tea.MouseButtonLeft && msg.Action == tea.MouseActionPress:\n\t\t\tm.showCursor = true\n\t\t\tif msg.Y < m.viewHeight() {\n\t\t\t\tif m.cursor == msg.Y {\n\t\t\t\t\tto, ok := m.cursorPointsTo()\n\t\t\t\t\tif ok {\n\t\t\t\t\t\tif to.IsCollapsed() {\n\t\t\t\t\t\t\tto.Expand()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tto.Collapse()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvalue, isRef := isRefNode(to)\n\t\t\t\t\t\tif isRef {\n\t\t\t\t\t\t\trefPath, ok := jsonpath.ParseSchemaRef(value)\n\t\t\t\t\t\t\tif ok {\n\t\t\t\t\t\t\t\tm.selectNode(m.findByPath(refPath))\n\t\t\t\t\t\t\t\tm.recordHistory()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tto := m.at(msg.Y)\n\t\t\t\t\tif to != nil {\n\t\t\t\t\t\tm.cursor = msg.Y\n\t\t\t\t\t\tif to.IsCollapsed() {\n\t\t\t\t\t\t\tto.Expand()\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tm.recordHistory()\n\t\t\t}\n\t\t}\n\n\tcase tea.KeyMsg:\n\t\tif m.commandInput.Focused() {\n\t\t\treturn m.handleGotoLineKey(msg)\n\t\t}\n\t\tif m.searchInput.Focused() {\n\t\t\treturn m.handleSearchKey(msg)\n\t\t}\n\t\tif m.gotoSymbolInput.Focused() {\n\t\t\treturn m.handleGotoSymbolKey(msg)\n\t\t}\n\t\tif m.yank {\n\t\t\treturn m.handleYankKey(msg)\n\t\t}\n\t\tif m.showShowSelector {\n\t\t\treturn m.handleShowSelectorKey(msg)\n\t\t}\n\t\treturn m.handleKey(msg)\n\t}\n\treturn m, nil\n}\n\nfunc (m *model) handleHelpKey(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar cmd tea.Cmd\n\tif msg, ok := msg.(tea.KeyMsg); ok {\n\t\tswitch {\n\t\tcase key.Matches(msg, keyMap.Quit), key.Matches(msg, keyMap.Help):\n\t\t\tm.showHelp = false\n\t\t}\n\t}\n\tm.help, cmd = m.help.Update(msg)\n\treturn m, cmd\n}\n\nfunc (m *model) handleGotoLineKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {\n\tvar cmd tea.Cmd\n\tswitch {\n\tcase msg.Type == tea.KeyEscape:\n\t\tm.commandInput.Blur()\n\t\tm.commandInput.SetValue(\"\")\n\t\tm.showCursor = true\n\n\tcase msg.Type == tea.KeyEnter:\n\t\tm.commandInput.Blur()\n\t\tcommand := m.commandInput.Value()\n\t\tm.commandInput.SetValue(\"\")\n\t\treturn m.runCommand(command)\n\n\tdefault:\n\t\tm.commandInput, cmd = m.commandInput.Update(msg)\n\t}\n\treturn m, cmd\n}\n\nfunc (m *model) handleSearchKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {\n\tvar cmd tea.Cmd\n\tswitch {\n\tcase msg.Type == tea.KeyEscape:\n\t\tm.cancelSearch()\n\t\tm.search = newSearch()\n\t\tm.searchInput.Blur()\n\t\tm.searchInput.SetValue(\"\")\n\t\tm.showCursor = true\n\n\tcase msg.Type == tea.KeyEnter:\n\t\tm.searchInput.Blur()\n\t\tm.cancelSearch()\n\t\tm.search = newSearch()\n\t\treturn m, m.doSearch(m.searchInput.Value())\n\n\tdefault:\n\t\tm.searchInput, cmd = m.searchInput.Update(msg)\n\t}\n\treturn m, cmd\n}\n\nfunc (m *model) handleGotoSymbolKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {\n\tvar cmd tea.Cmd\n\tswitch msg.Type {\n\tcase tea.KeyEscape, tea.KeyEnter, tea.KeyUp, tea.KeyDown:\n\t\tm.gotoSymbolInput.Blur()\n\t\tm.gotoSymbolInput.SetValue(\"\")\n\t\tm.recordHistory()\n\n\tdefault:\n\t\tm.gotoSymbolInput, cmd = m.gotoSymbolInput.Update(msg)\n\t\tpattern := []rune(m.gotoSymbolInput.Value())\n\t\tfound := fuzzy.Find(pattern, m.keysIndex)\n\t\tif found != nil {\n\t\t\tm.fuzzyMatch = found\n\t\t\tm.selectNode(m.keysIndexNodes[found.Index])\n\t\t}\n\t}\n\n\tswitch msg.Type {\n\tcase tea.KeyUp:\n\t\tm.up()\n\n\tcase tea.KeyDown:\n\t\tm.down()\n\t}\n\n\treturn m, cmd\n}\n\nfunc (m *model) handleYankKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {\n\tswitch {\n\tcase key.Matches(msg, yankPath):\n\t\t_ = clipboard.WriteAll(m.cursorPath())\n\tcase key.Matches(msg, yankKey):\n\t\t_ = clipboard.WriteAll(m.cursorKey())\n\tcase key.Matches(msg, yankValueY, yankValueV):\n\t\t_ = clipboard.WriteAll(m.cursorValue())\n\tcase key.Matches(msg, yankKeyValue):\n\t\tk := m.cursorKey()\n\t\tv := m.cursorValue()\n\t\tkeyValue := k + \": \" + v\n\t\t_ = clipboard.WriteAll(keyValue)\n\t}\n\tm.yank = false\n\treturn m, nil\n}\n\nfunc (m *model) handleShowSelectorKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {\n\tswitch {\n\tcase key.Matches(msg, showSizes):\n\t\tm.showSizes = !m.showSizes\n\tcase key.Matches(msg, showLineNumbers):\n\t\tm.showLineNumbers = !m.showLineNumbers\n\t\tWrap(m.top, m.viewWidth())\n\t}\n\tm.showShowSelector = false\n\treturn m, nil\n}\n\nfunc (m *model) handlePendingDelete(msg tea.Msg) {\n\t// Handle potential 'dd' sequence for delete\n\tif m.deletePending {\n\t\tif keyMsg, ok := msg.(tea.KeyMsg); ok {\n\t\t\tif key.Matches(keyMsg, keyMap.Delete) {\n\t\t\t\tm.deleteAtCursor()\n\t\t\t\tm.deletePending = true\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tm.deletePending = false\n\t}\n}\n\nfunc (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {\n\tm.handlePendingDelete(msg)\n\n\tswitch {\n\tcase key.Matches(msg, keyMap.Suspend):\n\t\tm.suspending = true\n\t\treturn m, tea.Suspend\n\n\tcase key.Matches(msg, keyMap.Quit):\n\t\treturn m, tea.Quit\n\n\tcase key.Matches(msg, keyMap.Help):\n\t\tm.help.SetContent(help(keyMap))\n\t\tm.showHelp = true\n\n\tcase key.Matches(msg, keyMap.Up):\n\t\tm.up()\n\n\tcase key.Matches(msg, keyMap.Down):\n\t\tm.down()\n\n\tcase key.Matches(msg, keyMap.PageUp):\n\t\tm.cursor = m.viewHeight() - 1\n\t\tm.showCursor = true\n\t\tm.scrollBackward(max(0, m.viewHeight()-2))\n\t\tm.scrollIntoView() // As the cursor is at the bottom, and it may be empty.\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.PageDown):\n\t\tm.cursor = 0\n\t\tm.showCursor = true\n\t\tm.scrollForward(max(0, m.viewHeight()-2))\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.HalfPageUp):\n\t\tm.showCursor = true\n\t\tm.scrollBackward(m.viewHeight() / 2)\n\t\tm.scrollIntoView() // As the cursor stays at the same position, and it may be empty.\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.HalfPageDown):\n\t\tm.showCursor = true\n\t\tm.scrollForward(m.viewHeight() / 2)\n\t\tm.scrollIntoView() // As the cursor stays at the same position, and it may be empty.\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.GotoTop):\n\t\tm.head = m.top\n\t\tm.cursor = 0\n\t\tm.showCursor = true\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.GotoBottom):\n\t\tm.scrollToBottom()\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.NextSibling):\n\t\tpointsTo, ok := m.cursorPointsTo()\n\t\tif !ok {\n\t\t\treturn m, nil\n\t\t}\n\t\tvar nextSibling *Node\n\t\tif pointsTo.End != nil && pointsTo.End.Next != nil {\n\t\t\tnextSibling = pointsTo.End.Next\n\t\t} else if pointsTo.ChunkEnd != nil && pointsTo.ChunkEnd.Next != nil {\n\t\t\tnextSibling = pointsTo.ChunkEnd.Next\n\t\t} else {\n\t\t\tnextSibling = pointsTo.Next\n\t\t}\n\t\tif nextSibling != nil {\n\t\t\tm.selectNode(nextSibling)\n\t\t}\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.PrevSibling):\n\t\tpointsTo, ok := m.cursorPointsTo()\n\t\tif !ok {\n\t\t\treturn m, nil\n\t\t}\n\t\tvar prevSibling *Node\n\t\tparent := pointsTo.Parent\n\t\tif parent != nil && parent.End == pointsTo {\n\t\t\tprevSibling = parent\n\t\t} else if pointsTo.Prev != nil {\n\t\t\tprevSibling = pointsTo.Prev\n\t\t\tparent := prevSibling.Parent\n\t\t\tif parent != nil && parent.End == prevSibling {\n\t\t\t\tprevSibling = parent\n\t\t\t} else if prevSibling.Chunk != \"\" {\n\t\t\t\tprevSibling = parent\n\t\t\t}\n\t\t}\n\t\tif prevSibling != nil {\n\t\t\tm.selectNode(prevSibling)\n\t\t}\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.Collapse):\n\t\tn, ok := m.cursorPointsTo()\n\t\tif !ok {\n\t\t\treturn m, nil\n\t\t}\n\t\tif n.HasChildren() && !n.IsCollapsed() {\n\t\t\tn.Collapse()\n\t\t} else {\n\t\t\tif n.Parent != nil {\n\t\t\t\tn = n.Parent\n\t\t\t}\n\t\t}\n\t\tm.selectNode(n)\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.Expand):\n\t\tn, ok := m.cursorPointsTo()\n\t\tif !ok {\n\t\t\treturn m, nil\n\t\t}\n\t\tn.Expand()\n\t\tm.showCursor = true\n\n\tcase key.Matches(msg, keyMap.CollapseRecursively):\n\t\tn, ok := m.cursorPointsTo()\n\t\tif !ok {\n\t\t\treturn m, nil\n\t\t}\n\t\tif n.HasChildren() {\n\t\t\tn.CollapseRecursively()\n\t\t}\n\t\tm.showCursor = true\n\n\tcase key.Matches(msg, keyMap.ExpandRecursively):\n\t\tn, ok := m.cursorPointsTo()\n\t\tif !ok {\n\t\t\treturn m, nil\n\t\t}\n\t\tif n.HasChildren() {\n\t\t\tn.ExpandRecursively(0, math.MaxInt)\n\t\t}\n\t\tm.showCursor = true\n\n\tcase key.Matches(msg, keyMap.CollapseAll):\n\t\tat, ok := m.cursorPointsTo()\n\t\tif ok {\n\t\t\tm.collapsed = true\n\t\t\tn := m.top\n\t\t\tfor n != nil {\n\t\t\t\tif n.Kind != Err {\n\t\t\t\t\tn.CollapseRecursively()\n\t\t\t\t}\n\t\t\t\tif n.End == nil {\n\t\t\t\t\tn = nil\n\t\t\t\t} else {\n\t\t\t\t\tn = n.End.Next\n\t\t\t\t}\n\t\t\t}\n\t\t\tm.selectNode(at.Root())\n\t\t\tm.recordHistory()\n\t\t}\n\n\tcase key.Matches(msg, keyMap.ExpandAll):\n\t\tat, ok := m.cursorPointsTo()\n\t\tif !ok {\n\t\t\treturn m, nil\n\t\t}\n\t\tm.collapsed = false\n\t\tn := m.top\n\t\tfor n != nil {\n\t\t\tn.ExpandRecursively(0, math.MaxInt)\n\t\t\tif n.End == nil {\n\t\t\t\tn = nil\n\t\t\t} else {\n\t\t\t\tn = n.End.Next\n\t\t\t}\n\t\t}\n\t\tm.selectNode(at)\n\n\tcase key.Matches(msg, keyMap.CollapseLevel):\n\t\tat, ok := m.cursorPointsTo()\n\t\tif ok && at.HasChildren() {\n\t\t\ttoLevel, _ := strconv.Atoi(msg.String())\n\t\t\tat.CollapseRecursively()\n\t\t\tat.ExpandRecursively(0, toLevel)\n\t\t\tm.showCursor = true\n\t\t}\n\n\tcase key.Matches(msg, keyMap.ToggleWrap):\n\t\tat, ok := m.cursorPointsTo()\n\t\tif !ok {\n\t\t\treturn m, nil\n\t\t}\n\t\tm.wrap = !m.wrap\n\t\tif m.wrap {\n\t\t\tWrap(m.top, m.viewWidth())\n\t\t} else {\n\t\t\tDropWrapAll(m.top)\n\t\t}\n\t\tif at.Chunk != \"\" && at.Value == \"\" {\n\t\t\tat = at.Parent\n\t\t}\n\t\tm.redoSearch()\n\t\tm.selectNode(at)\n\n\tcase key.Matches(msg, keyMap.ShowSelector):\n\t\tm.showShowSelector = true\n\n\tcase key.Matches(msg, keyMap.Yank):\n\t\tm.yank = true\n\n\tcase key.Matches(msg, keyMap.Preview):\n\t\tm.showPreview = true\n\t\tvalue := m.cursorValue()\n\t\tvar view string\n\t\tif decodedValue, err := base64.StdEncoding.DecodeString(value); err == nil {\n\t\t\timg, err := utils.DrawImage(bytes.NewReader(decodedValue), m.termWidth, m.termHeight)\n\t\t\tif err == nil {\n\t\t\t\tview = strings.TrimRight(img, \"\\n\")\n\t\t\t}\n\t\t}\n\t\tif view == \"\" {\n\t\t\tview = lipgloss.NewStyle().Width(m.termWidth).Render(value)\n\t\t}\n\t\tm.previewValue = value\n\t\tm.previewSearchInput.SetValue(\"\")\n\t\tm.previewSearchResults = nil\n\t\tm.previewSearchCursor = -1\n\t\tm.preview.SetContent(view)\n\t\tm.preview.GotoTop()\n\n\tcase key.Matches(msg, keyMap.Print):\n\t\treturn m, m.print()\n\n\tcase key.Matches(msg, keyMap.Open):\n\t\treturn m, m.open()\n\n\tcase key.Matches(msg, keyMap.GotoSymbol):\n\t\tm.gotoSymbolInput.CursorEnd()\n\t\tm.gotoSymbolInput.Width = m.termWidth - 2 // -1 for the prompt, -1 for the cursor\n\t\tm.gotoSymbolInput.Focus()\n\t\tm.createKeysIndex()\n\n\tcase key.Matches(msg, keyMap.GotoRef):\n\t\tat, ok := m.cursorPointsTo()\n\t\tif !ok {\n\t\t\treturn m, nil\n\t\t}\n\t\tvalue, isRef := isRefNode(at)\n\t\tif isRef {\n\t\t\trefPath, ok := jsonpath.ParseSchemaRef(value)\n\t\t\tif ok {\n\t\t\t\tm.selectNode(m.findByPath(refPath))\n\t\t\t\tm.recordHistory()\n\t\t\t}\n\t\t}\n\n\tcase key.Matches(msg, keyMap.CommandLine):\n\t\tm.commandInput.CursorEnd()\n\t\tm.commandInput.Width = m.termWidth - 2 // -1 for the prompt, -1 for the cursor\n\t\tm.commandInput.Focus()\n\n\tcase key.Matches(msg, keyMap.Search):\n\t\tm.searchInput.CursorEnd()\n\t\tm.searchInput.Width = m.termWidth - 2 // -1 for the prompt, -1 for the cursor\n\t\tm.searchInput.Focus()\n\n\tcase key.Matches(msg, keyMap.SearchNext):\n\t\tm.selectSearchResult(m.search.cursor + 1)\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.SearchPrev):\n\t\tm.selectSearchResult(m.search.cursor - 1)\n\t\tm.recordHistory()\n\n\tcase key.Matches(msg, keyMap.GoBack):\n\t\tif m.locationIndex > 0 {\n\t\t\tat, ok := m.cursorPointsTo()\n\t\t\tif !ok {\n\t\t\t\treturn m, nil\n\t\t\t}\n\t\t\tm.locationIndex--\n\n\t\t\tloc := m.locationHistory[m.locationIndex]\n\t\t\tfor loc.node == at && m.locationIndex > 0 {\n\t\t\t\tm.locationIndex--\n\t\t\t\tloc = m.locationHistory[m.locationIndex]\n\t\t\t}\n\t\t\tm.selectNode(loc.head)\n\t\t\tm.selectNode(loc.node)\n\t\t}\n\n\tcase key.Matches(msg, keyMap.GoForward):\n\t\tif m.locationIndex < len(m.locationHistory)-1 {\n\t\t\tm.locationIndex++\n\t\t\tloc := m.locationHistory[m.locationIndex]\n\t\t\tm.selectNode(loc.head)\n\t\t\tm.selectNode(loc.node)\n\t\t}\n\n\tcase key.Matches(msg, keyMap.Delete):\n\t\tm.deletePending = true\n\t}\n\treturn m, nil\n}\n\nfunc (m *model) up() {\n\tif m.head == nil {\n\t\treturn\n\t}\n\tm.showCursor = true\n\tm.cursor--\n\tif m.cursor < 0 {\n\t\tm.cursor = 0\n\t\tif m.head.Prev != nil {\n\t\t\tm.head = m.head.Prev\n\t\t}\n\t}\n}\n\nfunc (m *model) down() {\n\tif m.head == nil {\n\t\treturn\n\t}\n\tm.showCursor = true\n\tm.cursor++\n\t_, ok := m.cursorPointsTo()\n\tif !ok {\n\t\tm.cursor--\n\t\treturn\n\t}\n\tif m.cursor >= m.viewHeight() {\n\t\tm.cursor = m.viewHeight() - 1\n\t\tif m.head.Next != nil {\n\t\t\tm.head = m.head.Next\n\t\t}\n\t}\n}\n\nfunc (m *model) recordHistory() {\n\tat, ok := m.cursorPointsTo()\n\tif !ok {\n\t\treturn\n\t}\n\tif at.Chunk != \"\" && at.Value == \"\" {\n\t\t// We at the wrapped string, save the location of the original string node.\n\t\tat = at.Parent\n\t}\n\tif len(m.locationHistory) > 0 && m.locationHistory[len(m.locationHistory)-1].node == at {\n\t\treturn\n\t}\n\tif m.locationIndex < len(m.locationHistory) {\n\t\tm.locationHistory = m.locationHistory[:m.locationIndex+1]\n\t}\n\tm.locationHistory = append(m.locationHistory, location{\n\t\thead: m.head,\n\t\tnode: at,\n\t})\n\tm.locationIndex = len(m.locationHistory)\n}\n\nfunc (m *model) scrollToBottom() {\n\tif m.bottom == nil {\n\t\treturn\n\t}\n\tm.head = m.bottom.Bottom()\n\tm.cursor = 0\n\tm.showCursor = true\n\tm.scrollIntoView()\n}\n\nfunc (m *model) visibleLines() int {\n\tvisibleLines := 0\n\tn := m.head\n\tfor n != nil && visibleLines < m.viewHeight() {\n\t\tvisibleLines++\n\t\tn = n.Next\n\t}\n\treturn visibleLines\n}\n\nfunc (m *model) scrollIntoView() {\n\tif m.head == nil {\n\t\treturn\n\t}\n\tvisibleLines := m.visibleLines()\n\tif m.cursor >= visibleLines {\n\t\tm.cursor = visibleLines - 1\n\t}\n\tfor visibleLines < m.viewHeight() && m.head.Prev != nil {\n\t\tvisibleLines++\n\t\tm.cursor++\n\t\tm.head = m.head.Prev\n\t}\n}\n\nfunc (m *model) scrollBackward(lines int) {\n\tit := m.head\n\tfor it.Prev != nil {\n\t\tit = it.Prev\n\t\tif lines--; lines == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\tm.head = it\n}\n\nfunc (m *model) scrollForward(lines int) {\n\tif m.head == nil {\n\t\treturn\n\t}\n\tit := m.head\n\tfor it.Next != nil {\n\t\tit = it.Next\n\t\tif lines--; lines == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\tm.head = it\n}\n\nfunc (m *model) prettyKey(node *Node, selected bool) []byte {\n\tb := node.Key\n\n\tstyle := theme.CurrentTheme.Key\n\tif selected {\n\t\tstyle = theme.CurrentTheme.Cursor\n\t}\n\n\tif indexes, ok := m.search.keys[node]; ok {\n\t\tvar out []byte\n\t\tfor i, p := range splitByIndexes(b, indexes) {\n\t\t\tif i%2 == 0 {\n\t\t\t\tout = append(out, style(p.b)...)\n\t\t\t} else if p.index == m.search.cursor {\n\t\t\t\tout = append(out, theme.CurrentTheme.Cursor(p.b)...)\n\t\t\t} else {\n\t\t\t\tout = append(out, theme.CurrentTheme.Search(p.b)...)\n\t\t\t}\n\t\t}\n\t\treturn out\n\t} else {\n\t\treturn []byte(style(b))\n\t}\n}\n\nfunc (m *model) prettyPrint(node *Node, isSelected, isRef bool) string {\n\tvar s string\n\tif node.Chunk != \"\" {\n\t\ts = node.Chunk\n\t} else {\n\t\ts = node.Value\n\t}\n\n\tif len(s) == 0 {\n\t\tif isSelected {\n\t\t\treturn theme.CurrentTheme.Cursor(\" \")\n\t\t} else {\n\t\t\treturn s\n\t\t}\n\t}\n\n\tvar style theme.Color\n\n\tif isSelected {\n\t\tstyle = theme.CurrentTheme.Cursor\n\t} else {\n\t\tstyle = theme.Value(node.Kind)\n\t}\n\n\tif isRef {\n\t\tstyle = theme.CurrentTheme.Ref\n\t}\n\n\tif indexes, ok := m.search.values[node]; ok {\n\t\tvar out strings.Builder\n\t\tfor i, p := range splitByIndexes(s, indexes) {\n\t\t\tif i%2 == 0 {\n\t\t\t\tout.WriteString(style(p.b))\n\t\t\t} else if p.index == m.search.cursor {\n\t\t\t\tout.WriteString(theme.CurrentTheme.Cursor(p.b))\n\t\t\t} else {\n\t\t\t\tout.WriteString(theme.CurrentTheme.Search(p.b))\n\t\t\t}\n\t\t}\n\t\treturn out.String()\n\t} else {\n\t\treturn style(s)\n\t}\n}\n\nfunc (m *model) viewWidth() int {\n\twidth := m.termWidth\n\tif m.showLineNumbers {\n\t\twidth -= len(strconv.Itoa(m.totalLines))\n\t\twidth -= 2 // For margin between line numbers and JSON.\n\t}\n\treturn width\n}\n\nfunc (m *model) viewHeight() int {\n\tif m.gotoSymbolInput.Focused() {\n\t\treturn m.termHeight - 2\n\t}\n\tif m.commandInput.Focused() {\n\t\treturn m.termHeight - 2\n\t}\n\tif m.searchInput.Focused() || m.searchInput.Value() != \"\" {\n\t\treturn m.termHeight - 2\n\t}\n\tif m.yank {\n\t\treturn m.termHeight - 2\n\t}\n\tif m.showShowSelector {\n\t\treturn m.termHeight - 2\n\t}\n\treturn m.termHeight - 1\n}\n\nfunc (m *model) cursorPointsTo() (*Node, bool) {\n\tn := m.at(m.cursor)\n\treturn n, n != nil\n}\n\nfunc (m *model) at(pos int) *Node {\n\thead := m.head\n\tfor i := 0; i < pos; i++ {\n\t\tif head == nil {\n\t\t\tbreak\n\t\t}\n\t\thead = head.Next\n\t}\n\treturn head\n}\n\nfunc (m *model) nodeInsideView(n *Node) bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\thead := m.head\n\tfor i := 0; i < m.viewHeight(); i++ {\n\t\tif head == nil {\n\t\t\tbreak\n\t\t}\n\t\tif head == n {\n\t\t\treturn true\n\t\t}\n\t\thead = head.Next\n\t}\n\treturn false\n}\n\nfunc (m *model) selectNodeInView(n *Node) {\n\thead := m.head\n\tfor i := 0; i < m.viewHeight(); i++ {\n\t\tif head == nil {\n\t\t\tbreak\n\t\t}\n\t\tif head == n {\n\t\t\tm.cursor = i\n\t\t\treturn\n\t\t}\n\t\thead = head.Next\n\t}\n}\n\nfunc (m *model) selectNode(n *Node) {\n\tif n == nil {\n\t\treturn\n\t}\n\tm.showCursor = true\n\tif m.nodeInsideView(n) {\n\t\tm.selectNodeInView(n)\n\t\tm.scrollIntoView()\n\t} else {\n\t\tm.cursor = 0\n\t\tm.head = n\n\t\t{\n\t\t\tparent := n.Parent\n\t\t\tfor parent != nil {\n\t\t\t\tparent.Expand()\n\t\t\t\tparent = parent.Parent\n\t\t\t}\n\t\t}\n\t\tm.centerLine(n)\n\t\tm.scrollIntoView()\n\t}\n}\n\nfunc (m *model) cursorPath() string {\n\tat, ok := m.cursorPointsTo()\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tpath := \"\"\n\tfor at != nil {\n\t\tif at.Prev != nil {\n\t\t\tif at.Chunk != \"\" && at.Value == \"\" {\n\t\t\t\tat = at.Parent\n\t\t\t}\n\t\t\tif at.Key != \"\" {\n\t\t\t\tquoted := at.Key\n\t\t\t\tunquoted, err := strconv.Unquote(quoted)\n\t\t\t\tif err == nil && jsonpath.Identifier.MatchString(unquoted) {\n\t\t\t\t\tpath = \".\" + unquoted + path\n\t\t\t\t} else {\n\t\t\t\t\tpath = \"[\" + quoted + \"]\" + path\n\t\t\t\t}\n\t\t\t} else if at.Index >= 0 {\n\t\t\t\tpath = \"[\" + strconv.Itoa(at.Index) + \"]\" + path\n\t\t\t}\n\t\t}\n\t\tat = at.Parent\n\t}\n\treturn path\n}\n\nfunc (m *model) cursorValue() string {\n\tat, ok := m.cursorPointsTo()\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tparent := at.Parent\n\tif parent != nil {\n\t\t// wrapped string part\n\t\tif at.Chunk != \"\" && at.Value == \"\" {\n\t\t\tat = parent\n\t\t}\n\t\tif len(at.Value) >= 1 && at.Value[0] == '}' || at.Value[0] == ']' {\n\t\t\tat = parent\n\t\t}\n\t}\n\n\tif at.Kind == String {\n\t\tstr, err := strconv.Unquote(at.Value)\n\t\tif err == nil {\n\t\t\treturn str\n\t\t}\n\t\treturn at.Value\n\t}\n\n\tvar out strings.Builder\n\tout.WriteString(at.Value)\n\tout.WriteString(\"\\n\")\n\tif at.HasChildren() {\n\t\tit := at.Next\n\t\tif at.IsCollapsed() {\n\t\t\tit = at.Collapsed\n\t\t}\n\t\tfor it != nil {\n\t\t\tout.WriteString(strings.Repeat(\"  \", int(it.Depth-at.Depth)))\n\t\t\tif it.Key != \"\" {\n\t\t\t\tout.WriteString(it.Key)\n\t\t\t\tout.WriteString(\": \")\n\t\t\t}\n\t\t\tif it.Value != \"\" {\n\t\t\t\tout.WriteString(it.Value)\n\t\t\t}\n\t\t\tif it == at.End {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif it.Comma {\n\t\t\t\tout.WriteString(\",\")\n\t\t\t}\n\t\t\tout.WriteString(\"\\n\")\n\t\t\tif it.ChunkEnd != nil {\n\t\t\t\tit = it.ChunkEnd.Next\n\t\t\t} else if it.IsCollapsed() {\n\t\t\t\tit = it.Collapsed\n\t\t\t} else {\n\t\t\t\tit = it.Next\n\t\t\t}\n\t\t}\n\t}\n\treturn out.String()\n}\n\nfunc (m *model) cursorKey() string {\n\tat, ok := m.cursorPointsTo()\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tif at.IsWrap() {\n\t\tat = at.Parent\n\t}\n\tif at.Key != \"\" {\n\t\tvar v string\n\t\t_ = json.Unmarshal([]byte(at.Key), &v)\n\t\treturn v\n\t}\n\treturn strconv.Itoa(at.Index)\n}\n\nfunc (m *model) findByPath(path []any) *Node {\n\tn := m.currentTopNode()\n\treturn n.FindByPath(path)\n}\n\nfunc (m *model) currentTopNode() *Node {\n\tat, ok := m.cursorPointsTo()\n\tif !ok {\n\t\treturn nil\n\t}\n\tfor at.Parent != nil {\n\t\tat = at.Parent\n\t}\n\treturn at\n}\n\nfunc (m *model) createKeysIndex() {\n\tat, ok := m.cursorPointsTo()\n\tif !ok {\n\t\treturn\n\t}\n\troot := at.Root()\n\tif root == nil {\n\t\treturn\n\t}\n\tpaths := make([]string, 0, 100_000)\n\tnodes := make([]*Node, 0, 100_000)\n\n\troot.Paths(&paths, &nodes)\n\n\tm.keysIndex = paths\n\tm.keysIndexNodes = nodes\n\tm.fuzzyMatch = nil\n}\n\nfunc (m *model) dig(v string) *Node {\n\tp, ok := jsonpath.Split(v)\n\tif !ok {\n\t\treturn nil\n\t}\n\tat := m.findByPath(p)\n\tif at != nil {\n\t\treturn at\n\t}\n\n\tlastPart := p[len(p)-1]\n\tsearchTerm, ok := lastPart.(string)\n\tif !ok {\n\t\treturn nil\n\t}\n\tp = p[:len(p)-1]\n\n\tat = m.findByPath(p)\n\tif at == nil {\n\t\treturn nil\n\t}\n\n\tkeys, nodes := at.Children()\n\n\tfound := fuzzy.Find([]rune(searchTerm), keys)\n\tif found == nil {\n\t\treturn nil\n\t}\n\n\treturn nodes[found.Index]\n}\n\nfunc (m *model) print() tea.Cmd {\n\tm.printOnExit = true\n\treturn tea.Quit\n}\n\nfunc (m *model) open() tea.Cmd {\n\tif engine.FilePath == \"\" {\n\t\treturn nil\n\t}\n\tcommand := append(\n\t\tstrings.Split(lookup([]string{\"FX_EDITOR\", \"EDITOR\"}, \"vim\"), \" \"),\n\t\tengine.FilePath,\n\t)\n\tif command[0] == \"vi\" || command[0] == \"vim\" || command[0] == \"hx\" {\n\t\tat, ok := m.cursorPointsTo()\n\t\tif ok {\n\t\t\ttail := command[1:]\n\t\t\tcommand = append([]string{command[0]}, fmt.Sprintf(\"+%d\", at.LineNumber))\n\t\t\tcommand = append(command, tail...)\n\t\t}\n\t}\n\texecCmd := exec.Command(command[0], command[1:]...)\n\treturn tea.ExecProcess(execCmd, func(err error) tea.Msg {\n\t\treturn nil\n\t})\n}\n\n// deleteAtCursor deletes the current key/value (node) from the view structure.\nfunc (m *model) deleteAtCursor() {\n\tat, ok := m.cursorPointsTo()\n\tif !ok || at == nil {\n\t\treturn\n\t}\n\tif next, ok := DeleteNode(at); ok {\n\t\tm.selectNode(next)\n\t\tm.recordHistory()\n\t}\n}\n"
  },
  {
    "path": "main_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/charmbracelet/x/exp/teatest\"\n\t\"github.com/muesli/termenv\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc init() {\n\tlipgloss.SetColorProfile(termenv.ANSI)\n}\n\ntype options struct {\n\tshowSizes       bool\n\tshowLineNumbers bool\n}\n\nfunc prepare(t *testing.T, opts ...options) *teatest.TestModel {\n\tfile, err := os.Open(\"testdata/example.json\")\n\trequire.NoError(t, err)\n\n\tjson, err := io.ReadAll(file)\n\trequire.NoError(t, err)\n\n\thead, err := jsonx.Parse(json)\n\trequire.NoError(t, err)\n\n\tm := &model{\n\t\ttop:          head,\n\t\thead:         head,\n\t\tbottom:       head,\n\t\ttotalLines:   head.Bottom().LineNumber,\n\t\teof:          true,\n\t\twrap:         true,\n\t\tshowCursor:   true,\n\t\tsearchInput:  textinput.New(),\n\t\tsearch:       newSearch(),\n\t\tcommandInput: textinput.New(),\n\t}\n\n\tif len(opts) > 0 {\n\t\tm.showSizes = opts[0].showSizes\n\t\tm.showLineNumbers = opts[0].showLineNumbers\n\t}\n\n\ttm := teatest.NewTestModel(\n\t\tt, m,\n\t\tteatest.WithInitialTermSize(80, 40),\n\t)\n\treturn tm\n}\n\nfunc read(t *testing.T, tm *teatest.TestModel) []byte {\n\tvar out []byte\n\tteatest.WaitFor(t,\n\t\ttm.Output(),\n\t\tfunc(b []byte) bool {\n\t\t\tout = b\n\t\t\treturn bytes.Contains(b, []byte(\"{\"))\n\t\t},\n\t\tteatest.WithCheckInterval(time.Millisecond*100),\n\t\tteatest.WithDuration(time.Second),\n\t)\n\treturn out\n}\n\nfunc TestOutput(t *testing.T) {\n\ttm := prepare(t)\n\n\tteatest.RequireEqualOutput(t, read(t, tm))\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"q\")})\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))\n}\n\nfunc TestNavigation(t *testing.T) {\n\ttm := prepare(t)\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\tteatest.RequireEqualOutput(t, read(t, tm))\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"q\")})\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))\n}\n\nfunc TestCollapseRecursive(t *testing.T) {\n\ttm := prepare(t)\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyShiftLeft})\n\tteatest.RequireEqualOutput(t, read(t, tm))\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"q\")})\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))\n}\n\nfunc TestCollapseRecursiveWithSizes(t *testing.T) {\n\ttm := prepare(t, options{showSizes: true})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyShiftLeft})\n\tteatest.RequireEqualOutput(t, read(t, tm))\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"q\")})\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))\n}\n"
  },
  {
    "path": "npm/README.md",
    "content": "# fx\n\nA non-interactive, JavaScript version of the [**fx**](https://fx.wtf). \nShort for _Function eXecution_ or _f(x)_.\n\n```sh\nnpm i -g fx\n```\n\nOr use **npx**:\n\n```sh\ncat file.json | npx fx .field\n```\n\nOr use **deno**:\n\n```sh\ncat file.json | deno run -A npm:fx .field\n```\n\n## Usage\n\nFx treats arguments as JavaScript functions. Fx passes the input data to the first\nfunction and then passes the result of the first function to the second function \nand so on.\n\n```sh\necho '{\"name\": \"world\"}' | fx 'x => x.name' 'x => `Hello, ${x}!`'\n```\n\nUse `this` to access the input data. Use `.` at the start of the expression to \naccess the input data without a `x => x` part.\n\n```sh\necho '{\"name\": \"world\"}' | fx '.name' '`Hello, ${this}!`'\n```\n\nUse other JS functions to process the data.\n\n```sh\necho '{\"name\": \"world\"}' | fx 'Object.keys'\n```\n\n### Stream processing\n\nFx can process a stream of json objects. Fx will apply arguments to each object.\n\n```sh\necho '{\"name\": \"hello\"}\\n{\"name\": \"world\"}' | fx '.name'\n```\n\nIf you want to process a stream of json objects as a single array, \nuse the **--slurp** or **-s** flag.\n\n```sh\necho '{\"name\": \"hello\"}\\n{\"name\": \"world\"}' | fx --slurp '.map(x => x.name)' '.join(\", \")'\n```\n\n### Raw input\n\nIf you want to process non-JSON data, use the **--raw** or **-r** flag.\n\n```sh\nls | fx -r '[this, this.includes(\".md\")]'\n```\n\nYou can use **--raw** and **--slurp** (or **-rs**) together to get a single array of strings.\n\n```sh\nls | fx -rs '.filter(x => x.includes(\".md\"))'\n```\n\nFx has a special symbol **skip** for skipping the printing of the result.\n\n```sh\nls | fx -r '.includes(\".md\") ? this : skip'\n```\n\n### Built-in functions\n\nFx comes with a set of useful functions: **uniq**, **sort**, **groupBy**, **chunk**, **zip**.\n\n```sh\ncat file.json | fx 'uniq' 'sort' 'groupBy(x => x.name)'\n```\n\n### Edit-in-place\n\nYou can use special function **save** to edit-in-place the input data.\n\n```sh\nfx file.json 'x.name = x.name.toUpperCase(), x' 'save'\n```\n\nThe edited data will be saved to the same `file.json` file.\n\n### Syntactic Sugar\n\nFx has a shortcut for the map function. Fox example, `this.map(x => x.commit.message)`\ncan be rewritten without leading dot and without `x => x` parts.  \n\n```sh\ncurl https://api.github.com/repos/antonmedv/fx/commits | fx 'map(.commit.message)'\n```\n\n```sh\necho '[{\"name\": \"world\"}]' | fx 'map(`Hello, ${x.name}!`)'\n```\n\nFx has a special syntax for the flatMap function. Fox example,\n`.issues.flatMap(x => x.labels.flatMap(x => x))` can be rewritten in the next way.\n\n```sh\ncurl https://fx.wtf/example.json | fx '.issues[].labels[]'\n```\n\n### .fxrc.js\n\nFx supports `.fxrc.js` file in the current directory, or in the home directory, or in XDG config directory.\n\nPut the next code in the `.fxrc.js` file to make `myFunction` available in the fx.\n\n```js\nfunction addOne(x) {\n  return x + 1\n}\n```\n\nNow you can use `addOne` in the fx.\n\n```sh\necho '1' | fx addOne\n```\n\nIf you would like to create global variables use `var` instead of `let` or `const`.\n\n## License\n\n[MIT](../LICENSE)\n"
  },
  {
    "path": "npm/index.js",
    "content": "#!/usr/bin/env node\n'use strict'\n\nvoid async function main() {\n  const os = await import('node:os')\n  const fs = await import('node:fs')\n  const path = await import('node:path')\n  const process = await import('node:process')\n\n  let flagHelp = false\n  let flagRaw = false\n  let flagSlurp = false\n  let flagYaml = false\n  const args = []\n  for (const arg of process.argv.slice(2)) {\n    if (arg === '--help' || arg === '-h') flagHelp = true\n    else if (arg === '--raw' || arg === '-r') flagRaw = true\n    else if (arg === '--slurp' || arg === '-s') flagSlurp = true\n    else if (arg === '-rs' || arg === '-sr') flagRaw = flagSlurp = true\n    else if (arg === '--yaml') flagYaml = true\n    else args.push(arg)\n  }\n\n  if (flagHelp || (args.length === 0 && process.stdin.isTTY)) {\n    return printUsage()\n  }\n\n  const theme = themes(process.stdout.isTTY ? (process.env.FX_THEME || '1') : '0')\n\n  loadFxrc(os, fs, path, process)\n\n  let fd = 0 // stdin\n  if (args.length > 0) {\n    let filename =\n      isFile(fs, args[0]) ? args.shift() :\n        isFile(fs, args.at(-1)) ? args.pop() : false\n    if (filename) {\n      globalThis.__file__ = filename\n      fd = fs.openSync(filename, 'r')\n      if (!flagYaml) flagYaml = /\\.ya?ml$/i.test(filename)\n    }\n  }\n\n  const gen = await read(fd)\n  const input =\n    flagRaw\n      ? readLine(gen)\n      : flagYaml\n        ? parseYaml(gen)\n        : parseJson(gen)\n\n  if (flagSlurp) {\n    const array = []\n    for (const json of input) {\n      array.push(json)\n    }\n    await transform(array, args, theme)\n  } else {\n    for (const json of input) {\n      await transform(json, args, theme)\n    }\n  }\n}()\n\nconst skip = Symbol('skip')\n\nasync function transform(json, args, theme) {\n  let i, code, jsCode, output = json\n  for ([i, code] of args.entries()) try {\n    jsCode = transpile(code)\n    const fn = `(function () {\n      const x = this\n      return ${jsCode}\n    })`\n    output = await run(output, fn)\n    if (output === skip) break\n  } catch (err) {\n    await printErr(err)\n  }\n\n  if (typeof output === 'undefined')\n    console.error('undefined')\n  else if (typeof output === 'string')\n    console.log(output)\n  else if (output === skip)\n    return\n  else\n    console.log(stringify(output, theme))\n\n  async function printErr(err) {\n    const process = await import('node:process')\n    let pre = args.slice(0, i).join(' ')\n    let post = args.slice(i + 1).join(' ')\n    if (pre.length > 20) pre = '...' + pre.substring(pre.length - 20)\n    if (post.length > 20) post = post.substring(0, 20) + '...'\n    console.error(\n      `\\n  ${pre} ${code} ${post}\\n` +\n      `  ${' '.repeat(pre.length + 1)}${'^'.repeat(code.length)}\\n` +\n      (jsCode !== code ? `\\n${jsCode}\\n` : ``) +\n      `\\n${err.stack || err}`,\n    )\n    process.exit(1)\n  }\n}\n\nfunction transpile(code) {\n  if ('.' === code)\n    return 'x'\n\n  if (/^(\\.\\w*)+\\[]/.test(code))\n    return `(${fold(code.split('[]'))})(x)`\n\n  function fold(s) {\n    if (s.length === 1)\n      return 'x => x' + s[0]\n    let obj = s.shift()\n    obj = obj === '.' ? 'x' : 'x' + obj\n    return `x => ${obj}.flatMap(${fold(s)})`\n  }\n\n  if (/^\\.\\[/.test(code))\n    return `x${code.substring(1)}`\n\n  if (/^\\./.test(code))\n    return `x${code}`\n\n  if (/^@/.test(code)) {\n    const jsCode = transpile(code.substring(1))\n    return `map((x, i) => apply(${jsCode}, x, i))`\n  }\n\n  if (/^\\?/.test(code)) {\n    const jsCode = transpile(code.substring(1))\n    return `filter((x, i) => apply(${jsCode}, x, i))`\n  }\n\n  return code\n}\n\nasync function run(json, code) {\n  const fs = await import('node:fs')\n  const fn = eval(code).call(json)\n\n  return apply(fn, json)\n\n  function apply(fn, ...args) {\n    if (typeof fn === 'function') return fn(...args)\n    return fn\n  }\n\n  function len(x) {\n    if (Array.isArray(x)) return x.length\n    if (typeof x === 'string') return x.length\n    if (typeof x === 'object' && x !== null) return Object.keys(x).length\n    throw new Error(`Cannot get length of ${typeof x}`)\n  }\n\n  function uniq(x) {\n    if (Array.isArray(x)) return [...new Set(x)]\n    throw new Error(`Cannot get unique values of ${typeof x}`)\n  }\n\n  function sort(x) {\n    if (Array.isArray(x)) return x.sort()\n    throw new Error(`Cannot sort ${typeof x}`)\n  }\n\n  function isFalsely(x) {\n    return x === false || x === null || x === undefined\n  }\n\n  function filter(fn) {\n    return function (x) {\n      if (Array.isArray(x)) {\n        return x.filter((v, i) => !isFalsely(fn(v, i)))\n      }\n      return isFalsely(fn(x))? skip : x\n    }\n  }\n\n  function map(fn) {\n    return function (x) {\n      if (Array.isArray(x)) {\n        return x.map((v, i) => fn(v, i))\n      }\n      return fn(x)\n    }\n  }\n\n  function walk(fn) {\n    return function recurse(value, key = null) {\n      if (Array.isArray(value)) {\n        const mapped = value.map((v, i) => recurse(v, i))\n        return fn(mapped, key)\n      } else if (value !== null && typeof value === 'object') {\n        const result = {}\n        for (const [k, v] of Object.entries(value)) {\n          result[k] = recurse(v, k)\n        }\n        return fn(result, key)\n      } else {\n        return fn(value, key)\n      }\n    }\n  }\n\n  function sortBy(fn) {\n    return function (x) {\n      if (Array.isArray(x)) return x.sort((a, b) => {\n        const fa = fn(a)\n        const fb = fn(b)\n        return fa < fb ? -1 : fa > fb ? 1 : 0\n      })\n      throw new Error(`Cannot sort ${typeof x}`)\n    }\n  }\n\n  function sortKeys(x) {\n    if (Array.isArray(x)) {\n      return x.map(sortKeys)\n    }\n    if (typeof x === 'object' && x !== null) {\n      const sorted = {}\n      for (const key of Object.keys(x).sort()) {\n        sorted[key] = sortKeys(x[key])\n      }\n      return sorted\n    }\n    return x\n  }\n\n  function groupBy(keyFn) {\n    return function (x) {\n      const grouped = {}\n      for (const item of x) {\n        const key = typeof keyFn === 'function' ? keyFn(item) : item[keyFn]\n        if (!Object.prototype.hasOwnProperty.call(grouped, key)) grouped[key] = []\n        grouped[key].push(item)\n      }\n      return grouped\n    }\n  }\n\n  function chunk(size) {\n    return function (x) {\n      const res = []\n      let i = 0\n      while (i < x.length) {\n        res.push(x.slice(i, i += size))\n      }\n      return res\n    }\n  }\n\n  function zip(...x) {\n    const length = Math.min(...x.map(a => a.length))\n    const res = []\n    for (let i = 0; i < length; i++) {\n      res.push(x.map(a => a[i]))\n    }\n    return res\n  }\n\n  function flatten(x) {\n    if (Array.isArray(x)) return x.flat()\n    throw new Error(`Cannot flatten ${typeof x}`)\n  }\n\n  function reverse(x) {\n    if (Array.isArray(x)) return x.reverse()\n    throw new Error(`Cannot reverse ${typeof x}`)\n  }\n\n  function keys(x) {\n    if (typeof x === 'object' && x !== null) return Object.keys(x)\n    throw new Error(`Cannot get keys of ${typeof x}`)\n  }\n\n  function values(x) {\n    if (typeof x === 'object' && x !== null) return Object.values(x)\n    throw new Error(`Cannot get values of ${typeof x}`)\n  }\n\n  function list(x) {\n    if (Array.isArray(x)) {\n      for (const y of x) console.log(y)\n      return skip\n    }\n    throw new Error(`Cannot list ${typeof x}`)\n  }\n\n  function del(key) {\n    return function (x) {\n      if (Array.isArray(x)) {\n        const copy = [...x]\n        copy.splice(key, 1)\n        return copy\n      }\n      if (typeof x === 'object' && x !== null) {\n        const copy = {...x}\n        delete copy[key]\n        return copy\n      }\n      throw new Error(`Cannot delete key from ${typeof x}`)\n    }\n  }\n\n  function exit(code) {\n    process.exit(code)\n  }\n\n  function save(x) {\n    if (!globalThis.__file__) throw new Error('Specify a file as the first argument to be able to save: fx file.json ...')\n    fs.writeFileSync(globalThis.__file__, JSON.stringify(x, null, 2))\n    return x\n  }\n\n  function toBase64(x) {\n    return Buffer.from(x).toString('base64')\n  }\n\n  function fromBase64(x) {\n    return Buffer.from(x, 'base64').toString()\n  }\n}\n\nasync function read(fd = 0) {\n  const fs = await import('node:fs')\n  const {Buffer} = await import('node:buffer')\n  const {StringDecoder} = await import('node:string_decoder')\n  const decoder = new StringDecoder('utf8')\n  return function* () {\n    while (true) {\n      const buffer = Buffer.alloc(4_096)\n      let bytesRead\n      try {\n        bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null)\n      } catch (e) {\n        if (e.code === 'EAGAIN' || e.code === 'EWOULDBLOCK') {\n          sleepSync(10)\n          continue\n        }\n        if (e.code === 'EOF') break\n        throw e\n      }\n      if (bytesRead === 0) break\n      for (const ch of decoder.write(buffer.subarray(0, bytesRead)))\n        yield ch\n    }\n    for (const ch of decoder.end())\n      yield ch\n  }()\n}\n\nfunction isFile(fs, path) {\n  try {\n    const stat = fs.statSync(path, {throwIfNoEntry: false})\n    return stat !== undefined && stat.isFile()\n  } catch (err) {\n    return false\n  }\n}\n\nfunction sleepSync(ms) {\n  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)\n}\n\nfunction* readLine(stdin) {\n  let buffer = ''\n  for (const ch of stdin) {\n    if (ch === '\\n') {\n      yield buffer\n      buffer = ''\n    } else {\n      buffer += ch\n    }\n  }\n  if (buffer.length > 0) yield buffer\n  return buffer\n}\n\nfunction* parseYaml(gen) {\n  let buffer = ''\n  for (const ch of gen) {\n    buffer += ch\n  }\n  try {\n    yield YAML.parse(buffer)\n  } catch (err) {\n    throw new SyntaxError(err.message)\n  }\n}\n\nfunction* parseJson(gen) {\n  let lineNumber = 1, buffer = '', lastChar, done = false\n\n  function next() {\n    ({value: lastChar, done} = gen.next())\n    if (lastChar === '\\n') lineNumber++\n    buffer += (lastChar || '')\n    if (buffer.length > 100) buffer = buffer.slice(-40)\n  }\n\n  next()\n  while (!done) {\n    const value = parseValue()\n    expectValue(value)\n    yield value\n  }\n\n  function parseValue() {\n    skipWhitespace()\n    const value =\n      parseString() ??\n      parseNumber() ??\n      parseObject() ??\n      parseArray() ??\n      parseKeyword('true', true) ??\n      parseKeyword('false', false) ??\n      parseKeyword('null', null)\n    skipWhitespace()\n    return value\n  }\n\n  function parseString() {\n    if (lastChar !== '\"') return\n    let str = ''\n    let escaped = false\n    while (true) {\n      next()\n      if (escaped) {\n        if (lastChar === 'u') {\n          let unicode = ''\n          for (let i = 0; i < 4; i++) {\n            next()\n            if (!isHexDigit(lastChar)) {\n              throw new SyntaxError(errorSnippet(`Invalid Unicode escape sequence '\\\\u${unicode}${lastChar}'`))\n            }\n            unicode += lastChar\n          }\n          str += String.fromCharCode(parseInt(unicode, 16))\n        } else {\n          const escapedChar = {\n            '\"': '\"',\n            '\\\\': '\\\\',\n            '/': '/',\n            'b': '\\b',\n            'f': '\\f',\n            'n': '\\n',\n            'r': '\\r',\n            't': '\\t',\n          }[lastChar]\n          if (!escapedChar) {\n            throw new SyntaxError(errorSnippet())\n          }\n          str += escapedChar\n        }\n        escaped = false\n      } else if (lastChar === '\\\\') {\n        escaped = true\n      } else if (lastChar === '\"') {\n        break\n      } else if (lastChar < '\\x1F') {\n        throw new SyntaxError(errorSnippet(`Unescaped control character ${JSON.stringify(lastChar)}`))\n      } else if (lastChar === undefined) {\n        throw new SyntaxError(errorSnippet())\n      } else {\n        str += lastChar\n      }\n    }\n    next()\n    return str\n  }\n\n  function parseNumber() {\n    if (!isDigit(lastChar) && lastChar !== '-') return\n    let numStr = ''\n    if (lastChar === '-') {\n      numStr += lastChar\n      next()\n      if (!isDigit(lastChar)) {\n        throw new SyntaxError(errorSnippet())\n      }\n    }\n    if (lastChar === '0') {\n      numStr += lastChar\n      next()\n    } else {\n      while (isDigit(lastChar)) {\n        numStr += lastChar\n        next()\n      }\n    }\n    if (lastChar === '.') {\n      numStr += lastChar\n      next()\n      if (!isDigit(lastChar)) {\n        throw new SyntaxError(errorSnippet())\n      }\n      while (isDigit(lastChar)) {\n        numStr += lastChar\n        next()\n      }\n    }\n    if (lastChar === 'e' || lastChar === 'E') {\n      numStr += lastChar\n      next()\n      if (lastChar === '+' || lastChar === '-') {\n        numStr += lastChar\n        next()\n      }\n      if (!isDigit(lastChar)) {\n        throw new SyntaxError(errorSnippet())\n      }\n      while (isDigit(lastChar)) {\n        numStr += lastChar\n        next()\n      }\n    }\n    return isInteger(numStr) ? toSafeNumber(numStr) : parseFloat(numStr)\n  }\n\n  function parseObject() {\n    if (lastChar !== '{') return\n    next()\n    skipWhitespace()\n    const obj = {}\n    if (lastChar === '}') {\n      next()\n      return obj\n    }\n    while (true) {\n      if (lastChar !== '\"') {\n        throw new SyntaxError(errorSnippet())\n      }\n      const key = parseString()\n      skipWhitespace()\n      if (lastChar !== ':') {\n        throw new SyntaxError(errorSnippet())\n      }\n      next()\n      const value = parseValue()\n      expectValue(value)\n      obj[key] = value\n      skipWhitespace()\n      if (lastChar === '}') {\n        next()\n        return obj\n      } else if (lastChar === ',') {\n        next()\n        skipWhitespace()\n        if (lastChar === '}') {\n          next()\n          return obj\n        }\n      } else {\n        throw new SyntaxError(errorSnippet())\n      }\n    }\n  }\n\n  function parseArray() {\n    if (lastChar !== '[') return\n    next()\n    skipWhitespace()\n    const array = []\n    if (lastChar === ']') {\n      next()\n      return array\n    }\n    while (true) {\n      const value = parseValue()\n      expectValue(value)\n      array.push(value)\n      skipWhitespace()\n      if (lastChar === ']') {\n        next()\n        return array\n      } else if (lastChar === ',') {\n        next()\n        skipWhitespace()\n        if (lastChar === ']') {\n          next()\n          return array\n        }\n      } else {\n        throw new SyntaxError(errorSnippet())\n      }\n    }\n  }\n\n  function parseKeyword(name, value) {\n    if (lastChar !== name[0]) return\n    for (let i = 1; i < name.length; i++) {\n      next()\n      if (lastChar !== name[i]) {\n        throw new SyntaxError(errorSnippet())\n      }\n    }\n    next()\n    if (isWhitespace(lastChar) || lastChar === ',' || lastChar === '}' || lastChar === ']' || lastChar === undefined) {\n      return value\n    }\n    throw new SyntaxError(errorSnippet())\n  }\n\n  function skipWhitespace() {\n    while (isWhitespace(lastChar)) {\n      next()\n    }\n    skipComment()\n  }\n\n  function skipComment() {\n    if (lastChar === '/') {\n      next()\n      if (lastChar === '/') {\n        while (!done && lastChar !== '\\n') {\n          next()\n        }\n        skipWhitespace()\n      } else if (lastChar === '*') {\n        while (!done) {\n          next()\n          if (lastChar === '*') {\n            next()\n            if (lastChar === '/') {\n              next()\n              break\n            }\n          }\n        }\n        skipWhitespace()\n      } else {\n        throw new SyntaxError(errorSnippet())\n      }\n    }\n  }\n\n  function isWhitespace(ch) {\n    return ch === ' ' || ch === '\\n' || ch === '\\t' || ch === '\\r'\n  }\n\n  function isHexDigit(ch) {\n    return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')\n  }\n\n  function isDigit(ch) {\n    return ch >= '0' && ch <= '9'\n  }\n\n  function isInteger(value) {\n    return /^-?[0-9]+$/.test(value)\n  }\n\n  function toSafeNumber(str) {\n    const maxSafeInteger = Number.MAX_SAFE_INTEGER\n    const minSafeInteger = Number.MIN_SAFE_INTEGER\n    const num = BigInt(str)\n    return num >= minSafeInteger && num <= maxSafeInteger ? Number(num) : num\n  }\n\n  function expectValue(value) {\n    if (value === undefined) {\n      throw new SyntaxError(errorSnippet(`JSON value expected`))\n    }\n  }\n\n  function errorSnippet(message = `Unexpected character '${lastChar}'`) {\n    if (!lastChar) {\n      message = 'Unexpected end of input'\n    }\n    const lines = buffer.slice(-40).split('\\n')\n    const lastLine = lines.pop()\n    const source =\n      lines.map(line => `    ${line}\\n`).join('')\n      + `    ${lastLine}${readEOL()}\\n`\n    const p = `    ${'.'.repeat(Math.max(0, lastLine.length - 1))}^\\n`\n    return `${message} on line ${lineNumber}.\\n\\n${source}${p}`\n  }\n\n  function readEOL() {\n    let line = ''\n    for (const ch of gen) {\n      if (!ch || ch === '\\n' || line.length >= 60) break\n      line += ch\n    }\n    return line\n  }\n}\n\nfunction stringify(value, theme) {\n  function color(id, str) {\n    if (theme[id] === '') return str\n    return `\\x1b[${theme[id]}m${str}\\x1b[0m`\n  }\n\n  function getIndent(level) {\n    return ' '.repeat(2 * level)\n  }\n\n  function stringifyValue(value, level = 0) {\n    if (typeof value === 'string') {\n      return color(2, JSON.stringify(value))\n    } else if (typeof value === 'number') {\n      return color(3, `${value}`)\n    } else if (typeof value === 'bigint') {\n      return color(3, `${value}`)\n    } else if (typeof value === 'boolean') {\n      return color(4, `${value}`)\n    } else if (value === null || typeof value === 'undefined') {\n      return color(5, `null`)\n    } else if (Array.isArray(value)) {\n      if (value.length === 0) {\n        return color(0, `[]`)\n      }\n      const items = value\n        .map((v) => getIndent(level + 1) + stringifyValue(v, level + 1))\n        .join(color(0, ',') + '\\n')\n      return color(0, '[') + '\\n' + items + '\\n' + getIndent(level) + color(0, ']')\n    } else if (typeof value === 'object') {\n      const keys = Object.keys(value)\n      if (keys.length === 0) {\n        return color(0, '{}')\n      }\n      const entries = keys\n        .map((key) =>\n          getIndent(level + 1) + color(1, `\"${key}\"`) + color(0, ': ') +\n          stringifyValue(value[key], level + 1),\n        )\n        .join(color(0, ',') + '\\n')\n      return color(0, '{') + '\\n' + entries + '\\n' + getIndent(level) + color(0, '}')\n    }\n    throw new Error(`Unsupported value type: ${typeof value}`)\n  }\n\n  return stringifyValue(value)\n}\n\nfunction themes(id) {\n  const themes = {\n    '0': ['', '', '', '', '', ''],\n    '1': ['', '1;34', '32', '36', '35', '38;5;243'],\n    '2': ['', '32', '34', '36', '35', '38;5;243'],\n    '3': ['', '95', '93', '96', '31', '38;5;243'],\n    '4': ['', '38;5;50', '38;5;39', '38;5;98', '38;5;205', '38;5;243'],\n    '5': ['', '38;5;230', '38;5;221', '38;5;209', '38;5;209', '38;5;243'],\n    '6': ['', '38;5;69', '38;5;78', '38;5;221', '38;5;203', '38;5;243'],\n    '7': ['', '1;38;5;42', '1;38;5;213', '1;38;5;201', '1;38;5;201', '38;5;243'],\n    '8': ['', '1;38;5;51', '38;5;195', '38;5;123', '38;5;50', '38;5;243'],\n    '9': ['', '1;38;5;39', '38;5;49', '38;5;220', '38;5;205', '38;5;243'],\n    '🔥': ['1;38;5;208', '1;38;5;202', '38;5;214', '38;5;202', '38;5;196', '38;5;243'],\n    '🔵': ['1;38;5;33', '38;5;33', '', '', '', ''],\n    '🟣': ['', '1;38;5;141', '38;5;183', '38;5;219', '38;5;81', '38;5;243'],\n    '🥝': ['38;5;179', '1;38;5;154', '38;5;82', '38;5;226', '38;5;226', '38;5;230'],\n  }\n  return themes[id] || themes['1']\n}\n\nasync function importFxrc(path) {\n  const {join} = await import('node:path')\n  const {pathToFileURL} = await import('node:url')\n  try {\n    await import(pathToFileURL(join(path, '.fxrc.js')))\n  } catch (err) {\n    if (err.code !== 'ERR_MODULE_NOT_FOUND') throw err\n  }\n}\n\nfunction loadFxrc(os, fs, path, process) {\n  let script = ''\n\n  const cwd = process.cwd()\n  const home = os.homedir()\n  const xdgHome = process.env.XDG_CONFIG_HOME || path.join(home, '.config')\n  const xdgDirsEnv = process.env.XDG_CONFIG_DIRS || '/etc/xdg'\n\n  const paths = [path.join(cwd, '.fxrc.js')]\n  paths.push(path.join(home, '.fxrc.js'))\n  paths.push(path.join(xdgHome, 'fx', '.fxrc.js'))\n  for (const dir of xdgDirsEnv.split(':')) {\n    paths.push(path.join(dir, 'fx', '.fxrc.js'))\n  }\n\n  for (const filePath of paths) {\n    try {\n      const stat = fs.statSync(filePath)\n      if (stat.isDirectory()) continue\n\n      const data = fs.readFileSync(filePath, 'utf8')\n      script += data + '\\n'\n    } catch (err) {\n      if (err.code === 'ENOENT') continue\n      throw new Error(`read ${filePath}: ${err.message}`)\n    }\n  }\n\n  eval?.(script)\n}\n\nfunction printUsage() {\n  const usage = `Usage\n  fx [flags] [code...]\n\nFlags\n  -h, --help    print help\n  -r, --raw     treat input as a raw string\n  -s, --slurp   read all inputs into an array\n  --yaml        parse input as YAML`\n  console.log(usage)\n}\n\n// yaml v2.4.0\n// @formatter:off\nvoid function () {var ALIAS=Symbol.for(\"yaml.alias\");var DOC=Symbol.for(\"yaml.document\");var MAP=Symbol.for(\"yaml.map\");var PAIR=Symbol.for(\"yaml.pair\");var SCALAR=Symbol.for(\"yaml.scalar\");var SEQ=Symbol.for(\"yaml.seq\");var NODE_TYPE=Symbol.for(\"yaml.node.type\");var isAlias=node=>!!node&&typeof node===\"object\"&&node[NODE_TYPE]===ALIAS;var isDocument=node=>!!node&&typeof node===\"object\"&&node[NODE_TYPE]===DOC;var isMap=node=>!!node&&typeof node===\"object\"&&node[NODE_TYPE]===MAP;var isPair=node=>!!node&&typeof node===\"object\"&&node[NODE_TYPE]===PAIR;var isScalar=node=>!!node&&typeof node===\"object\"&&node[NODE_TYPE]===SCALAR;var isSeq=node=>!!node&&typeof node===\"object\"&&node[NODE_TYPE]===SEQ;function isCollection(node){if(node&&typeof node===\"object\")switch(node[NODE_TYPE]){case MAP:case SEQ:return true}return false}function isNode(node){if(node&&typeof node===\"object\")switch(node[NODE_TYPE]){case ALIAS:case MAP:case SCALAR:case SEQ:return true}return false}var hasAnchor=node=>(isScalar(node)||isCollection(node))&&!!node.anchor;var BREAK=Symbol(\"break visit\");var SKIP=Symbol(\"skip children\");var REMOVE=Symbol(\"remove node\");function visit(node,visitor){const visitor_=initVisitor(visitor);if(isDocument(node)){const cd=visit_(null,node.contents,visitor_,Object.freeze([node]));if(cd===REMOVE)node.contents=null}else visit_(null,node,visitor_,Object.freeze([]))}visit.BREAK=BREAK;visit.SKIP=SKIP;visit.REMOVE=REMOVE;function visit_(key,node,visitor,path){const ctrl=callVisitor(key,node,visitor,path);if(isNode(ctrl)||isPair(ctrl)){replaceNode(key,path,ctrl);return visit_(key,ctrl,visitor,path)}if(typeof ctrl!==\"symbol\"){if(isCollection(node)){path=Object.freeze(path.concat(node));for(let i=0;i<node.items.length;++i){const ci=visit_(i,node.items[i],visitor,path);if(typeof ci===\"number\")i=ci-1;else if(ci===BREAK)return BREAK;else if(ci===REMOVE){node.items.splice(i,1);i-=1}}}else if(isPair(node)){path=Object.freeze(path.concat(node));const ck=visit_(\"key\",node.key,visitor,path);if(ck===BREAK)return BREAK;else if(ck===REMOVE)node.key=null;const cv=visit_(\"value\",node.value,visitor,path);if(cv===BREAK)return BREAK;else if(cv===REMOVE)node.value=null}}return ctrl}async function visitAsync(node,visitor){const visitor_=initVisitor(visitor);if(isDocument(node)){const cd=await visitAsync_(null,node.contents,visitor_,Object.freeze([node]));if(cd===REMOVE)node.contents=null}else await visitAsync_(null,node,visitor_,Object.freeze([]))}visitAsync.BREAK=BREAK;visitAsync.SKIP=SKIP;visitAsync.REMOVE=REMOVE;async function visitAsync_(key,node,visitor,path){const ctrl=await callVisitor(key,node,visitor,path);if(isNode(ctrl)||isPair(ctrl)){replaceNode(key,path,ctrl);return visitAsync_(key,ctrl,visitor,path)}if(typeof ctrl!==\"symbol\"){if(isCollection(node)){path=Object.freeze(path.concat(node));for(let i=0;i<node.items.length;++i){const ci=await visitAsync_(i,node.items[i],visitor,path);if(typeof ci===\"number\")i=ci-1;else if(ci===BREAK)return BREAK;else if(ci===REMOVE){node.items.splice(i,1);i-=1}}}else if(isPair(node)){path=Object.freeze(path.concat(node));const ck=await visitAsync_(\"key\",node.key,visitor,path);if(ck===BREAK)return BREAK;else if(ck===REMOVE)node.key=null;const cv=await visitAsync_(\"value\",node.value,visitor,path);if(cv===BREAK)return BREAK;else if(cv===REMOVE)node.value=null}}return ctrl}function initVisitor(visitor){if(typeof visitor===\"object\"&&(visitor.Collection||visitor.Node||visitor.Value)){return Object.assign({Alias:visitor.Node,Map:visitor.Node,Scalar:visitor.Node,Seq:visitor.Node},visitor.Value&&{Map:visitor.Value,Scalar:visitor.Value,Seq:visitor.Value},visitor.Collection&&{Map:visitor.Collection,Seq:visitor.Collection},visitor)}return visitor}function callVisitor(key,node,visitor,path){if(typeof visitor===\"function\")return visitor(key,node,path);if(isMap(node))return visitor.Map?.(key,node,path);if(isSeq(node))return visitor.Seq?.(key,node,path);if(isPair(node))return visitor.Pair?.(key,node,path);if(isScalar(node))return visitor.Scalar?.(key,node,path);if(isAlias(node))return visitor.Alias?.(key,node,path);return void 0}function replaceNode(key,path,node){const parent=path[path.length-1];if(isCollection(parent)){parent.items[key]=node}else if(isPair(parent)){if(key===\"key\")parent.key=node;else parent.value=node}else if(isDocument(parent)){parent.contents=node}else{const pt=isAlias(parent)?\"alias\":\"scalar\";throw new Error(`Cannot replace node with ${pt} parent`)}}var escapeChars={\"!\":\"%21\",\",\":\"%2C\",\"[\":\"%5B\",\"]\":\"%5D\",\"{\":\"%7B\",\"}\":\"%7D\"};var escapeTagName=tn=>tn.replace(/[!,[\\]{}]/g,ch=>escapeChars[ch]);var Directives=class _Directives{constructor(yaml,tags){this.docStart=null;this.docEnd=false;this.yaml=Object.assign({},_Directives.defaultYaml,yaml);this.tags=Object.assign({},_Directives.defaultTags,tags)}clone(){const copy=new _Directives(this.yaml,this.tags);copy.docStart=this.docStart;return copy}atDocument(){const res=new _Directives(this.yaml,this.tags);switch(this.yaml.version){case\"1.1\":this.atNextDocument=true;break;case\"1.2\":this.atNextDocument=false;this.yaml={explicit:_Directives.defaultYaml.explicit,version:\"1.2\"};this.tags=Object.assign({},_Directives.defaultTags);break}return res}add(line,onError){if(this.atNextDocument){this.yaml={explicit:_Directives.defaultYaml.explicit,version:\"1.1\"};this.tags=Object.assign({},_Directives.defaultTags);this.atNextDocument=false}const parts=line.trim().split(/[ \\t]+/);const name=parts.shift();switch(name){case\"%TAG\":{if(parts.length!==2){onError(0,\"%TAG directive should contain exactly two parts\");if(parts.length<2)return false}const[handle,prefix]=parts;this.tags[handle]=prefix;return true}case\"%YAML\":{this.yaml.explicit=true;if(parts.length!==1){onError(0,\"%YAML directive should contain exactly one part\");return false}const[version]=parts;if(version===\"1.1\"||version===\"1.2\"){this.yaml.version=version;return true}else{const isValid=/^\\d+\\.\\d+$/.test(version);onError(6,`Unsupported YAML version ${version}`,isValid);return false}}default:onError(0,`Unknown directive ${name}`,true);return false}}tagName(source,onError){if(source===\"!\")return\"!\";if(source[0]!==\"!\"){onError(`Not a valid tag: ${source}`);return null}if(source[1]===\"<\"){const verbatim=source.slice(2,-1);if(verbatim===\"!\"||verbatim===\"!!\"){onError(`Verbatim tags aren't resolved, so ${source} is invalid.`);return null}if(source[source.length-1]!==\">\")onError(\"Verbatim tags must end with a >\");return verbatim}const[,handle,suffix]=source.match(/^(.*!)([^!]*)$/s);if(!suffix)onError(`The ${source} tag has no suffix`);const prefix=this.tags[handle];if(prefix){try{return prefix+decodeURIComponent(suffix)}catch(error){onError(String(error));return null}}if(handle===\"!\")return source;onError(`Could not resolve tag: ${source}`);return null}tagString(tag){for(const[handle,prefix]of Object.entries(this.tags)){if(tag.startsWith(prefix))return handle+escapeTagName(tag.substring(prefix.length))}return tag[0]===\"!\"?tag:`!<${tag}>`}toString(doc){const lines=this.yaml.explicit?[`%YAML ${this.yaml.version||\"1.2\"}`]:[];const tagEntries=Object.entries(this.tags);let tagNames;if(doc&&tagEntries.length>0&&isNode(doc.contents)){const tags={};visit(doc.contents,(_key,node)=>{if(isNode(node)&&node.tag)tags[node.tag]=true});tagNames=Object.keys(tags)}else tagNames=[];for(const[handle,prefix]of tagEntries){if(handle===\"!!\"&&prefix===\"tag:yaml.org,2002:\")continue;if(!doc||tagNames.some(tn=>tn.startsWith(prefix)))lines.push(`%TAG ${handle} ${prefix}`)}return lines.join(\"\\n\")}};Directives.defaultYaml={explicit:false,version:\"1.2\"};Directives.defaultTags={\"!!\":\"tag:yaml.org,2002:\"};function anchorIsValid(anchor){if(/[\\x00-\\x19\\s,[\\]{}]/.test(anchor)){const sa=JSON.stringify(anchor);const msg=`Anchor must not contain whitespace or control characters: ${sa}`;throw new Error(msg)}return true}function anchorNames(root){const anchors=new Set;visit(root,{Value(_key,node){if(node.anchor)anchors.add(node.anchor)}});return anchors}function findNewAnchor(prefix,exclude){for(let i=1;true;++i){const name=`${prefix}${i}`;if(!exclude.has(name))return name}}function createNodeAnchors(doc,prefix){const aliasObjects=[];const sourceObjects=new Map;let prevAnchors=null;return{onAnchor:source=>{aliasObjects.push(source);if(!prevAnchors)prevAnchors=anchorNames(doc);const anchor=findNewAnchor(prefix,prevAnchors);prevAnchors.add(anchor);return anchor},setAnchors:()=>{for(const source of aliasObjects){const ref=sourceObjects.get(source);if(typeof ref===\"object\"&&ref.anchor&&(isScalar(ref.node)||isCollection(ref.node))){ref.node.anchor=ref.anchor}else{const error=new Error(\"Failed to resolve repeated object (this should not happen)\");error.source=source;throw error}}},sourceObjects}}function applyReviver(reviver,obj,key,val){if(val&&typeof val===\"object\"){if(Array.isArray(val)){for(let i=0,len=val.length;i<len;++i){const v0=val[i];const v1=applyReviver(reviver,val,String(i),v0);if(v1===void 0)delete val[i];else if(v1!==v0)val[i]=v1}}else if(val instanceof Map){for(const k of Array.from(val.keys())){const v0=val.get(k);const v1=applyReviver(reviver,val,k,v0);if(v1===void 0)val.delete(k);else if(v1!==v0)val.set(k,v1)}}else if(val instanceof Set){for(const v0 of Array.from(val)){const v1=applyReviver(reviver,val,v0,v0);if(v1===void 0)val.delete(v0);else if(v1!==v0){val.delete(v0);val.add(v1)}}}else{for(const[k,v0]of Object.entries(val)){const v1=applyReviver(reviver,val,k,v0);if(v1===void 0)delete val[k];else if(v1!==v0)val[k]=v1}}}return reviver.call(obj,key,val)}function toJS(value,arg,ctx){if(Array.isArray(value))return value.map((v,i)=>toJS(v,String(i),ctx));if(value&&typeof value.toJSON===\"function\"){if(!ctx||!hasAnchor(value))return value.toJSON(arg,ctx);const data={aliasCount:0,count:1,res:void 0};ctx.anchors.set(value,data);ctx.onCreate=res2=>{data.res=res2;delete ctx.onCreate};const res=value.toJSON(arg,ctx);if(ctx.onCreate)ctx.onCreate(res);return res}if(typeof value===\"bigint\"&&!ctx?.keep)return Number(value);return value}var NodeBase=class{constructor(type){Object.defineProperty(this,NODE_TYPE,{value:type})}clone(){const copy=Object.create(Object.getPrototypeOf(this),Object.getOwnPropertyDescriptors(this));if(this.range)copy.range=this.range.slice();return copy}toJS(doc,{mapAsMap,maxAliasCount,onAnchor,reviver}={}){if(!isDocument(doc))throw new TypeError(\"A document argument is required\");const ctx={anchors:new Map,doc,keep:true,mapAsMap:mapAsMap===true,mapKeyWarned:false,maxAliasCount:typeof maxAliasCount===\"number\"?maxAliasCount:100};const res=toJS(this,\"\",ctx);if(typeof onAnchor===\"function\")for(const{count,res:res2}of ctx.anchors.values())onAnchor(res2,count);return typeof reviver===\"function\"?applyReviver(reviver,{\"\":res},\"\",res):res}};var Alias=class extends NodeBase{constructor(source){super(ALIAS);this.source=source;Object.defineProperty(this,\"tag\",{set(){throw new Error(\"Alias nodes cannot have tags\")}})}resolve(doc){let found=void 0;visit(doc,{Node:(_key,node)=>{if(node===this)return visit.BREAK;if(node.anchor===this.source)found=node}});return found}toJSON(_arg,ctx){if(!ctx)return{source:this.source};const{anchors,doc,maxAliasCount}=ctx;const source=this.resolve(doc);if(!source){const msg=`Unresolved alias (the anchor must be set before the alias): ${this.source}`;throw new ReferenceError(msg)}let data=anchors.get(source);if(!data){toJS(source,null,ctx);data=anchors.get(source)}if(!data||data.res===void 0){const msg=\"This should not happen: Alias anchor was not resolved?\";throw new ReferenceError(msg)}if(maxAliasCount>=0){data.count+=1;if(data.aliasCount===0)data.aliasCount=getAliasCount(doc,source,anchors);if(data.count*data.aliasCount>maxAliasCount){const msg=\"Excessive alias count indicates a resource exhaustion attack\";throw new ReferenceError(msg)}}return data.res}toString(ctx,_onComment,_onChompKeep){const src=`*${this.source}`;if(ctx){anchorIsValid(this.source);if(ctx.options.verifyAliasOrder&&!ctx.anchors.has(this.source)){const msg=`Unresolved alias (the anchor must be set before the alias): ${this.source}`;throw new Error(msg)}if(ctx.implicitKey)return`${src} `}return src}};function getAliasCount(doc,node,anchors){if(isAlias(node)){const source=node.resolve(doc);const anchor=anchors&&source&&anchors.get(source);return anchor?anchor.count*anchor.aliasCount:0}else if(isCollection(node)){let count=0;for(const item of node.items){const c=getAliasCount(doc,item,anchors);if(c>count)count=c}return count}else if(isPair(node)){const kc=getAliasCount(doc,node.key,anchors);const vc=getAliasCount(doc,node.value,anchors);return Math.max(kc,vc)}return 1}var isScalarValue=value=>!value||typeof value!==\"function\"&&typeof value!==\"object\";var Scalar=class extends NodeBase{constructor(value){super(SCALAR);this.value=value}toJSON(arg,ctx){return ctx?.keep?this.value:toJS(this.value,arg,ctx)}toString(){return String(this.value)}};Scalar.BLOCK_FOLDED=\"BLOCK_FOLDED\";Scalar.BLOCK_LITERAL=\"BLOCK_LITERAL\";Scalar.PLAIN=\"PLAIN\";Scalar.QUOTE_DOUBLE=\"QUOTE_DOUBLE\";Scalar.QUOTE_SINGLE=\"QUOTE_SINGLE\";var defaultTagPrefix=\"tag:yaml.org,2002:\";function findTagObject(value,tagName,tags){if(tagName){const match=tags.filter(t=>t.tag===tagName);const tagObj=match.find(t=>!t.format)??match[0];if(!tagObj)throw new Error(`Tag ${tagName} not found`);return tagObj}return tags.find(t=>t.identify?.(value)&&!t.format)}function createNode(value,tagName,ctx){if(isDocument(value))value=value.contents;if(isNode(value))return value;if(isPair(value)){const map2=ctx.schema[MAP].createNode?.(ctx.schema,null,ctx);map2.items.push(value);return map2}if(value instanceof String||value instanceof Number||value instanceof Boolean||typeof BigInt!==\"undefined\"&&value instanceof BigInt){value=value.valueOf()}const{aliasDuplicateObjects,onAnchor,onTagObj,schema:schema4,sourceObjects}=ctx;let ref=void 0;if(aliasDuplicateObjects&&value&&typeof value===\"object\"){ref=sourceObjects.get(value);if(ref){if(!ref.anchor)ref.anchor=onAnchor(value);return new Alias(ref.anchor)}else{ref={anchor:null,node:null};sourceObjects.set(value,ref)}}if(tagName?.startsWith(\"!!\"))tagName=defaultTagPrefix+tagName.slice(2);let tagObj=findTagObject(value,tagName,schema4.tags);if(!tagObj){if(value&&typeof value.toJSON===\"function\"){value=value.toJSON()}if(!value||typeof value!==\"object\"){const node2=new Scalar(value);if(ref)ref.node=node2;return node2}tagObj=value instanceof Map?schema4[MAP]:Symbol.iterator in Object(value)?schema4[SEQ]:schema4[MAP]}if(onTagObj){onTagObj(tagObj);delete ctx.onTagObj}const node=tagObj?.createNode?tagObj.createNode(ctx.schema,value,ctx):typeof tagObj?.nodeClass?.from===\"function\"?tagObj.nodeClass.from(ctx.schema,value,ctx):new Scalar(value);if(tagName)node.tag=tagName;else if(!tagObj.default)node.tag=tagObj.tag;if(ref)ref.node=node;return node}function collectionFromPath(schema4,path,value){let v=value;for(let i=path.length-1;i>=0;--i){const k=path[i];if(typeof k===\"number\"&&Number.isInteger(k)&&k>=0){const a=[];a[k]=v;v=a}else{v=new Map([[k,v]])}}return createNode(v,void 0,{aliasDuplicateObjects:false,keepUndefined:false,onAnchor:()=>{throw new Error(\"This should not happen, please report a bug.\")},schema:schema4,sourceObjects:new Map})}var isEmptyPath=path=>path==null||typeof path===\"object\"&&!!path[Symbol.iterator]().next().done;var Collection=class extends NodeBase{constructor(type,schema4){super(type);Object.defineProperty(this,\"schema\",{value:schema4,configurable:true,enumerable:false,writable:true})}clone(schema4){const copy=Object.create(Object.getPrototypeOf(this),Object.getOwnPropertyDescriptors(this));if(schema4)copy.schema=schema4;copy.items=copy.items.map(it=>isNode(it)||isPair(it)?it.clone(schema4):it);if(this.range)copy.range=this.range.slice();return copy}addIn(path,value){if(isEmptyPath(path))this.add(value);else{const[key,...rest]=path;const node=this.get(key,true);if(isCollection(node))node.addIn(rest,value);else if(node===void 0&&this.schema)this.set(key,collectionFromPath(this.schema,rest,value));else throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`)}}deleteIn(path){const[key,...rest]=path;if(rest.length===0)return this.delete(key);const node=this.get(key,true);if(isCollection(node))return node.deleteIn(rest);else throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`)}getIn(path,keepScalar){const[key,...rest]=path;const node=this.get(key,true);if(rest.length===0)return!keepScalar&&isScalar(node)?node.value:node;else return isCollection(node)?node.getIn(rest,keepScalar):void 0}hasAllNullValues(allowScalar){return this.items.every(node=>{if(!isPair(node))return false;const n=node.value;return n==null||allowScalar&&isScalar(n)&&n.value==null&&!n.commentBefore&&!n.comment&&!n.tag})}hasIn(path){const[key,...rest]=path;if(rest.length===0)return this.has(key);const node=this.get(key,true);return isCollection(node)?node.hasIn(rest):false}setIn(path,value){const[key,...rest]=path;if(rest.length===0){this.set(key,value)}else{const node=this.get(key,true);if(isCollection(node))node.setIn(rest,value);else if(node===void 0&&this.schema)this.set(key,collectionFromPath(this.schema,rest,value));else throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`)}}};Collection.maxFlowStringSingleLineLength=60;var stringifyComment=str=>str.replace(/^(?!$)(?: $)?/gm,\"#\");function indentComment(comment,indent){if(/^\\n+$/.test(comment))return comment.substring(1);return indent?comment.replace(/^(?! *$)/gm,indent):comment}var lineComment=(str,indent,comment)=>str.endsWith(\"\\n\")?indentComment(comment,indent):comment.includes(\"\\n\")?\"\\n\"+indentComment(comment,indent):(str.endsWith(\" \")?\"\":\" \")+comment;var FOLD_FLOW=\"flow\";var FOLD_BLOCK=\"block\";var FOLD_QUOTED=\"quoted\";function foldFlowLines(text,indent,mode=\"flow\",{indentAtStart,lineWidth=80,minContentWidth=20,onFold,onOverflow}={}){if(!lineWidth||lineWidth<0)return text;const endStep=Math.max(1+minContentWidth,1+lineWidth-indent.length);if(text.length<=endStep)return text;const folds=[];const escapedFolds={};let end=lineWidth-indent.length;if(typeof indentAtStart===\"number\"){if(indentAtStart>lineWidth-Math.max(2,minContentWidth))folds.push(0);else end=lineWidth-indentAtStart}let split=void 0;let prev=void 0;let overflow=false;let i=-1;let escStart=-1;let escEnd=-1;if(mode===FOLD_BLOCK){i=consumeMoreIndentedLines(text,i);if(i!==-1)end=i+endStep}for(let ch;ch=text[i+=1];){if(mode===FOLD_QUOTED&&ch===\"\\\\\"){escStart=i;switch(text[i+1]){case\"x\":i+=3;break;case\"u\":i+=5;break;case\"U\":i+=9;break;default:i+=1}escEnd=i}if(ch===\"\\n\"){if(mode===FOLD_BLOCK)i=consumeMoreIndentedLines(text,i);end=i+endStep;split=void 0}else{if(ch===\" \"&&prev&&prev!==\" \"&&prev!==\"\\n\"&&prev!==\"\t\"){const next=text[i+1];if(next&&next!==\" \"&&next!==\"\\n\"&&next!==\"\t\")split=i}if(i>=end){if(split){folds.push(split);end=split+endStep;split=void 0}else if(mode===FOLD_QUOTED){while(prev===\" \"||prev===\"\t\"){prev=ch;ch=text[i+=1];overflow=true}const j=i>escEnd+1?i-2:escStart-1;if(escapedFolds[j])return text;folds.push(j);escapedFolds[j]=true;end=j+endStep;split=void 0}else{overflow=true}}}prev=ch}if(overflow&&onOverflow)onOverflow();if(folds.length===0)return text;if(onFold)onFold();let res=text.slice(0,folds[0]);for(let i2=0;i2<folds.length;++i2){const fold=folds[i2];const end2=folds[i2+1]||text.length;if(fold===0)res=`\n${indent}${text.slice(0,end2)}`;else{if(mode===FOLD_QUOTED&&escapedFolds[fold])res+=`${text[fold]}\\\\`;res+=`\n${indent}${text.slice(fold+1,end2)}`}}return res}function consumeMoreIndentedLines(text,i){let ch=text[i+1];while(ch===\" \"||ch===\"\t\"){do{ch=text[i+=1]}while(ch&&ch!==\"\\n\");ch=text[i+1]}return i}var getFoldOptions=(ctx,isBlock2)=>({indentAtStart:isBlock2?ctx.indent.length:ctx.indentAtStart,lineWidth:ctx.options.lineWidth,minContentWidth:ctx.options.minContentWidth});var containsDocumentMarker=str=>/^(%|---|\\.\\.\\.)/m.test(str);function lineLengthOverLimit(str,lineWidth,indentLength){if(!lineWidth||lineWidth<0)return false;const limit=lineWidth-indentLength;const strLen=str.length;if(strLen<=limit)return false;for(let i=0,start=0;i<strLen;++i){if(str[i]===\"\\n\"){if(i-start>limit)return true;start=i+1;if(strLen-start<=limit)return false}}return true}function doubleQuotedString(value,ctx){const json=JSON.stringify(value);if(ctx.options.doubleQuotedAsJSON)return json;const{implicitKey}=ctx;const minMultiLineLength=ctx.options.doubleQuotedMinMultiLineLength;const indent=ctx.indent||(containsDocumentMarker(value)?\"  \":\"\");let str=\"\";let start=0;for(let i=0,ch=json[i];ch;ch=json[++i]){if(ch===\" \"&&json[i+1]===\"\\\\\"&&json[i+2]===\"n\"){str+=json.slice(start,i)+\"\\\\ \";i+=1;start=i;ch=\"\\\\\"}if(ch===\"\\\\\")switch(json[i+1]){case\"u\":{str+=json.slice(start,i);const code=json.substr(i+2,4);switch(code){case\"0000\":str+=\"\\\\0\";break;case\"0007\":str+=\"\\\\a\";break;case\"000b\":str+=\"\\\\v\";break;case\"001b\":str+=\"\\\\e\";break;case\"0085\":str+=\"\\\\N\";break;case\"00a0\":str+=\"\\\\_\";break;case\"2028\":str+=\"\\\\L\";break;case\"2029\":str+=\"\\\\P\";break;default:if(code.substr(0,2)===\"00\")str+=\"\\\\x\"+code.substr(2);else str+=json.substr(i,6)}i+=5;start=i+1}break;case\"n\":if(implicitKey||json[i+2]==='\"'||json.length<minMultiLineLength){i+=1}else{str+=json.slice(start,i)+\"\\n\\n\";while(json[i+2]===\"\\\\\"&&json[i+3]===\"n\"&&json[i+4]!=='\"'){str+=\"\\n\";i+=2}str+=indent;if(json[i+2]===\" \")str+=\"\\\\\";i+=1;start=i+1}break;default:i+=1}}str=start?str+json.slice(start):json;return implicitKey?str:foldFlowLines(str,indent,FOLD_QUOTED,getFoldOptions(ctx,false))}function singleQuotedString(value,ctx){if(ctx.options.singleQuote===false||ctx.implicitKey&&value.includes(\"\\n\")||/[ \\t]\\n|\\n[ \\t]/.test(value))return doubleQuotedString(value,ctx);const indent=ctx.indent||(containsDocumentMarker(value)?\"  \":\"\");const res=\"'\"+value.replace(/'/g,\"''\").replace(/\\n+/g,`$&\n${indent}`)+\"'\";return ctx.implicitKey?res:foldFlowLines(res,indent,FOLD_FLOW,getFoldOptions(ctx,false))}function quotedString(value,ctx){const{singleQuote}=ctx.options;let qs;if(singleQuote===false)qs=doubleQuotedString;else{const hasDouble=value.includes('\"');const hasSingle=value.includes(\"'\");if(hasDouble&&!hasSingle)qs=singleQuotedString;else if(hasSingle&&!hasDouble)qs=doubleQuotedString;else qs=singleQuote?singleQuotedString:doubleQuotedString}return qs(value,ctx)}var blockEndNewlines;try{blockEndNewlines=new RegExp(\"(^|(?<!\\n))\\n+(?!\\n|$)\",\"g\")}catch{blockEndNewlines=/\\n+(?!\\n|$)/g}function blockString({comment,type,value},ctx,onComment,onChompKeep){const{blockQuote,commentString,lineWidth}=ctx.options;if(!blockQuote||/\\n[\\t ]+$/.test(value)||/^\\s*$/.test(value)){return quotedString(value,ctx)}const indent=ctx.indent||(ctx.forceBlockIndent||containsDocumentMarker(value)?\"  \":\"\");const literal=blockQuote===\"literal\"?true:blockQuote===\"folded\"||type===Scalar.BLOCK_FOLDED?false:type===Scalar.BLOCK_LITERAL?true:!lineLengthOverLimit(value,lineWidth,indent.length);if(!value)return literal?\"|\\n\":\">\\n\";let chomp;let endStart;for(endStart=value.length;endStart>0;--endStart){const ch=value[endStart-1];if(ch!==\"\\n\"&&ch!==\"\t\"&&ch!==\" \")break}let end=value.substring(endStart);const endNlPos=end.indexOf(\"\\n\");if(endNlPos===-1){chomp=\"-\"}else if(value===end||endNlPos!==end.length-1){chomp=\"+\";if(onChompKeep)onChompKeep()}else{chomp=\"\"}if(end){value=value.slice(0,-end.length);if(end[end.length-1]===\"\\n\")end=end.slice(0,-1);end=end.replace(blockEndNewlines,`$&${indent}`)}let startWithSpace=false;let startEnd;let startNlPos=-1;for(startEnd=0;startEnd<value.length;++startEnd){const ch=value[startEnd];if(ch===\" \")startWithSpace=true;else if(ch===\"\\n\")startNlPos=startEnd;else break}let start=value.substring(0,startNlPos<startEnd?startNlPos+1:startEnd);if(start){value=value.substring(start.length);start=start.replace(/\\n+/g,`$&${indent}`)}const indentSize=indent?\"2\":\"1\";let header=(literal?\"|\":\">\")+(startWithSpace?indentSize:\"\")+chomp;if(comment){header+=\" \"+commentString(comment.replace(/ ?[\\r\\n]+/g,\" \"));if(onComment)onComment()}if(literal){value=value.replace(/\\n+/g,`$&${indent}`);return`${header}\n${indent}${start}${value}${end}`}value=value.replace(/\\n+/g,\"\\n$&\").replace(/(?:^|\\n)([\\t ].*)(?:([\\n\\t ]*)\\n(?![\\n\\t ]))?/g,\"$1$2\").replace(/\\n+/g,`$&${indent}`);const body=foldFlowLines(`${start}${value}${end}`,indent,FOLD_BLOCK,getFoldOptions(ctx,true));return`${header}\n${indent}${body}`}function plainString(item,ctx,onComment,onChompKeep){const{type,value}=item;const{actualString,implicitKey,indent,indentStep,inFlow}=ctx;if(implicitKey&&value.includes(\"\\n\")||inFlow&&/[[\\]{},]/.test(value)){return quotedString(value,ctx)}if(!value||/^[\\n\\t ,[\\]{}#&*!|>'\"%@`]|^[?-]$|^[?-][ \\t]|[\\n:][ \\t]|[ \\t]\\n|[\\n\\t ]#|[\\n\\t :]$/.test(value)){return implicitKey||inFlow||!value.includes(\"\\n\")?quotedString(value,ctx):blockString(item,ctx,onComment,onChompKeep)}if(!implicitKey&&!inFlow&&type!==Scalar.PLAIN&&value.includes(\"\\n\")){return blockString(item,ctx,onComment,onChompKeep)}if(containsDocumentMarker(value)){if(indent===\"\"){ctx.forceBlockIndent=true;return blockString(item,ctx,onComment,onChompKeep)}else if(implicitKey&&indent===indentStep){return quotedString(value,ctx)}}const str=value.replace(/\\n+/g,`$&\n${indent}`);if(actualString){const test=tag=>tag.default&&tag.tag!==\"tag:yaml.org,2002:str\"&&tag.test?.test(str);const{compat,tags}=ctx.doc.schema;if(tags.some(test)||compat?.some(test))return quotedString(value,ctx)}return implicitKey?str:foldFlowLines(str,indent,FOLD_FLOW,getFoldOptions(ctx,false))}function stringifyString(item,ctx,onComment,onChompKeep){const{implicitKey,inFlow}=ctx;const ss=typeof item.value===\"string\"?item:Object.assign({},item,{value:String(item.value)});let{type}=item;if(type!==Scalar.QUOTE_DOUBLE){if(/[\\x00-\\x08\\x0b-\\x1f\\x7f-\\x9f\\u{D800}-\\u{DFFF}]/u.test(ss.value))type=Scalar.QUOTE_DOUBLE}const _stringify=_type=>{switch(_type){case Scalar.BLOCK_FOLDED:case Scalar.BLOCK_LITERAL:return implicitKey||inFlow?quotedString(ss.value,ctx):blockString(ss,ctx,onComment,onChompKeep);case Scalar.QUOTE_DOUBLE:return doubleQuotedString(ss.value,ctx);case Scalar.QUOTE_SINGLE:return singleQuotedString(ss.value,ctx);case Scalar.PLAIN:return plainString(ss,ctx,onComment,onChompKeep);default:return null}};let res=_stringify(type);if(res===null){const{defaultKeyType,defaultStringType}=ctx.options;const t=implicitKey&&defaultKeyType||defaultStringType;res=_stringify(t);if(res===null)throw new Error(`Unsupported default string type ${t}`)}return res}function createStringifyContext(doc,options){const opt=Object.assign({blockQuote:true,commentString:stringifyComment,defaultKeyType:null,defaultStringType:\"PLAIN\",directives:null,doubleQuotedAsJSON:false,doubleQuotedMinMultiLineLength:40,falseStr:\"false\",flowCollectionPadding:true,indentSeq:true,lineWidth:80,minContentWidth:20,nullStr:\"null\",simpleKeys:false,singleQuote:null,trueStr:\"true\",verifyAliasOrder:true},doc.schema.toStringOptions,options);let inFlow;switch(opt.collectionStyle){case\"block\":inFlow=false;break;case\"flow\":inFlow=true;break;default:inFlow=null}return{anchors:new Set,doc,flowCollectionPadding:opt.flowCollectionPadding?\" \":\"\",indent:\"\",indentStep:typeof opt.indent===\"number\"?\" \".repeat(opt.indent):\"  \",inFlow,options:opt}}function getTagObject(tags,item){if(item.tag){const match=tags.filter(t=>t.tag===item.tag);if(match.length>0)return match.find(t=>t.format===item.format)??match[0]}let tagObj=void 0;let obj;if(isScalar(item)){obj=item.value;const match=tags.filter(t=>t.identify?.(obj));tagObj=match.find(t=>t.format===item.format)??match.find(t=>!t.format)}else{obj=item;tagObj=tags.find(t=>t.nodeClass&&obj instanceof t.nodeClass)}if(!tagObj){const name=obj?.constructor?.name??typeof obj;throw new Error(`Tag not resolved for ${name} value`)}return tagObj}function stringifyProps(node,tagObj,{anchors,doc}){if(!doc.directives)return\"\";const props=[];const anchor=(isScalar(node)||isCollection(node))&&node.anchor;if(anchor&&anchorIsValid(anchor)){anchors.add(anchor);props.push(`&${anchor}`)}const tag=node.tag?node.tag:tagObj.default?null:tagObj.tag;if(tag)props.push(doc.directives.tagString(tag));return props.join(\" \")}function stringify(item,ctx,onComment,onChompKeep){if(isPair(item))return item.toString(ctx,onComment,onChompKeep);if(isAlias(item)){if(ctx.doc.directives)return item.toString(ctx);if(ctx.resolvedAliases?.has(item)){throw new TypeError(`Cannot stringify circular structure without alias nodes`)}else{if(ctx.resolvedAliases)ctx.resolvedAliases.add(item);else ctx.resolvedAliases=new Set([item]);item=item.resolve(ctx.doc)}}let tagObj=void 0;const node=isNode(item)?item:ctx.doc.createNode(item,{onTagObj:o=>tagObj=o});if(!tagObj)tagObj=getTagObject(ctx.doc.schema.tags,node);const props=stringifyProps(node,tagObj,ctx);if(props.length>0)ctx.indentAtStart=(ctx.indentAtStart??0)+props.length+1;const str=typeof tagObj.stringify===\"function\"?tagObj.stringify(node,ctx,onComment,onChompKeep):isScalar(node)?stringifyString(node,ctx,onComment,onChompKeep):node.toString(ctx,onComment,onChompKeep);if(!props)return str;return isScalar(node)||str[0]===\"{\"||str[0]===\"[\"?`${props} ${str}`:`${props}\n${ctx.indent}${str}`}function stringifyPair({key,value},ctx,onComment,onChompKeep){const{allNullValues,doc,indent,indentStep,options:{commentString,indentSeq,simpleKeys}}=ctx;let keyComment=isNode(key)&&key.comment||null;if(simpleKeys){if(keyComment){throw new Error(\"With simple keys, key nodes cannot have comments\")}if(isCollection(key)){const msg=\"With simple keys, collection cannot be used as a key value\";throw new Error(msg)}}let explicitKey=!simpleKeys&&(!key||keyComment&&value==null&&!ctx.inFlow||isCollection(key)||(isScalar(key)?key.type===Scalar.BLOCK_FOLDED||key.type===Scalar.BLOCK_LITERAL:typeof key===\"object\"));ctx=Object.assign({},ctx,{allNullValues:false,implicitKey:!explicitKey&&(simpleKeys||!allNullValues),indent:indent+indentStep});let keyCommentDone=false;let chompKeep=false;let str=stringify(key,ctx,()=>keyCommentDone=true,()=>chompKeep=true);if(!explicitKey&&!ctx.inFlow&&str.length>1024){if(simpleKeys)throw new Error(\"With simple keys, single line scalar must not span more than 1024 characters\");explicitKey=true}if(ctx.inFlow){if(allNullValues||value==null){if(keyCommentDone&&onComment)onComment();return str===\"\"?\"?\":explicitKey?`? ${str}`:str}}else if(allNullValues&&!simpleKeys||value==null&&explicitKey){str=`? ${str}`;if(keyComment&&!keyCommentDone){str+=lineComment(str,ctx.indent,commentString(keyComment))}else if(chompKeep&&onChompKeep)onChompKeep();return str}if(keyCommentDone)keyComment=null;if(explicitKey){if(keyComment)str+=lineComment(str,ctx.indent,commentString(keyComment));str=`? ${str}\n${indent}:`}else{str=`${str}:`;if(keyComment)str+=lineComment(str,ctx.indent,commentString(keyComment))}let vsb,vcb,valueComment;if(isNode(value)){vsb=!!value.spaceBefore;vcb=value.commentBefore;valueComment=value.comment}else{vsb=false;vcb=null;valueComment=null;if(value&&typeof value===\"object\")value=doc.createNode(value)}ctx.implicitKey=false;if(!explicitKey&&!keyComment&&isScalar(value))ctx.indentAtStart=str.length+1;chompKeep=false;if(!indentSeq&&indentStep.length>=2&&!ctx.inFlow&&!explicitKey&&isSeq(value)&&!value.flow&&!value.tag&&!value.anchor){ctx.indent=ctx.indent.substring(2)}let valueCommentDone=false;const valueStr=stringify(value,ctx,()=>valueCommentDone=true,()=>chompKeep=true);let ws=\" \";if(keyComment||vsb||vcb){ws=vsb?\"\\n\":\"\";if(vcb){const cs=commentString(vcb);ws+=`\n${indentComment(cs,ctx.indent)}`}if(valueStr===\"\"&&!ctx.inFlow){if(ws===\"\\n\")ws=\"\\n\\n\"}else{ws+=`\n${ctx.indent}`}}else if(!explicitKey&&isCollection(value)){const vs0=valueStr[0];const nl0=valueStr.indexOf(\"\\n\");const hasNewline=nl0!==-1;const flow=ctx.inFlow??value.flow??value.items.length===0;if(hasNewline||!flow){let hasPropsLine=false;if(hasNewline&&(vs0===\"&\"||vs0===\"!\")){let sp0=valueStr.indexOf(\" \");if(vs0===\"&\"&&sp0!==-1&&sp0<nl0&&valueStr[sp0+1]===\"!\"){sp0=valueStr.indexOf(\" \",sp0+1)}if(sp0===-1||nl0<sp0)hasPropsLine=true}if(!hasPropsLine)ws=`\n${ctx.indent}`}}else if(valueStr===\"\"||valueStr[0]===\"\\n\"){ws=\"\"}str+=ws+valueStr;if(ctx.inFlow){if(valueCommentDone&&onComment)onComment()}else if(valueComment&&!valueCommentDone){str+=lineComment(str,ctx.indent,commentString(valueComment))}else if(chompKeep&&onChompKeep){onChompKeep()}return str}function warn(logLevel,warning){if(logLevel===\"debug\"||logLevel===\"warn\"){if(typeof process!==\"undefined\"&&process.emitWarning)process.emitWarning(warning);else console.warn(warning)}}var MERGE_KEY=\"<<\";function addPairToJSMap(ctx,map2,{key,value}){if(ctx?.doc.schema.merge&&isMergeKey(key)){value=isAlias(value)?value.resolve(ctx.doc):value;if(isSeq(value))for(const it of value.items)mergeToJSMap(ctx,map2,it);else if(Array.isArray(value))for(const it of value)mergeToJSMap(ctx,map2,it);else mergeToJSMap(ctx,map2,value)}else{const jsKey=toJS(key,\"\",ctx);if(map2 instanceof Map){map2.set(jsKey,toJS(value,jsKey,ctx))}else if(map2 instanceof Set){map2.add(jsKey)}else{const stringKey=stringifyKey(key,jsKey,ctx);const jsValue=toJS(value,stringKey,ctx);if(stringKey in map2)Object.defineProperty(map2,stringKey,{value:jsValue,writable:true,enumerable:true,configurable:true});else map2[stringKey]=jsValue}}return map2}var isMergeKey=key=>key===MERGE_KEY||isScalar(key)&&key.value===MERGE_KEY&&(!key.type||key.type===Scalar.PLAIN);function mergeToJSMap(ctx,map2,value){const source=ctx&&isAlias(value)?value.resolve(ctx.doc):value;if(!isMap(source))throw new Error(\"Merge sources must be maps or map aliases\");const srcMap=source.toJSON(null,ctx,Map);for(const[key,value2]of srcMap){if(map2 instanceof Map){if(!map2.has(key))map2.set(key,value2)}else if(map2 instanceof Set){map2.add(key)}else if(!Object.prototype.hasOwnProperty.call(map2,key)){Object.defineProperty(map2,key,{value:value2,writable:true,enumerable:true,configurable:true})}}return map2}function stringifyKey(key,jsKey,ctx){if(jsKey===null)return\"\";if(typeof jsKey!==\"object\")return String(jsKey);if(isNode(key)&&ctx?.doc){const strCtx=createStringifyContext(ctx.doc,{});strCtx.anchors=new Set;for(const node of ctx.anchors.keys())strCtx.anchors.add(node.anchor);strCtx.inFlow=true;strCtx.inStringifyKey=true;const strKey=key.toString(strCtx);if(!ctx.mapKeyWarned){let jsonStr=JSON.stringify(strKey);if(jsonStr.length>40)jsonStr=jsonStr.substring(0,36)+'...\"';warn(ctx.doc.options.logLevel,`Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);ctx.mapKeyWarned=true}return strKey}return JSON.stringify(jsKey)}function createPair(key,value,ctx){const k=createNode(key,void 0,ctx);const v=createNode(value,void 0,ctx);return new Pair(k,v)}var Pair=class _Pair{constructor(key,value=null){Object.defineProperty(this,NODE_TYPE,{value:PAIR});this.key=key;this.value=value}clone(schema4){let{key,value}=this;if(isNode(key))key=key.clone(schema4);if(isNode(value))value=value.clone(schema4);return new _Pair(key,value)}toJSON(_,ctx){const pair=ctx?.mapAsMap?new Map:{};return addPairToJSMap(ctx,pair,this)}toString(ctx,onComment,onChompKeep){return ctx?.doc?stringifyPair(this,ctx,onComment,onChompKeep):JSON.stringify(this)}};function stringifyCollection(collection,ctx,options){const flow=ctx.inFlow??collection.flow;const stringify4=flow?stringifyFlowCollection:stringifyBlockCollection;return stringify4(collection,ctx,options)}function stringifyBlockCollection({comment,items},ctx,{blockItemPrefix,flowChars,itemIndent,onChompKeep,onComment}){const{indent,options:{commentString}}=ctx;const itemCtx=Object.assign({},ctx,{indent:itemIndent,type:null});let chompKeep=false;const lines=[];for(let i=0;i<items.length;++i){const item=items[i];let comment2=null;if(isNode(item)){if(!chompKeep&&item.spaceBefore)lines.push(\"\");addCommentBefore(ctx,lines,item.commentBefore,chompKeep);if(item.comment)comment2=item.comment}else if(isPair(item)){const ik=isNode(item.key)?item.key:null;if(ik){if(!chompKeep&&ik.spaceBefore)lines.push(\"\");addCommentBefore(ctx,lines,ik.commentBefore,chompKeep)}}chompKeep=false;let str2=stringify(item,itemCtx,()=>comment2=null,()=>chompKeep=true);if(comment2)str2+=lineComment(str2,itemIndent,commentString(comment2));if(chompKeep&&comment2)chompKeep=false;lines.push(blockItemPrefix+str2)}let str;if(lines.length===0){str=flowChars.start+flowChars.end}else{str=lines[0];for(let i=1;i<lines.length;++i){const line=lines[i];str+=line?`\n${indent}${line}`:\"\\n\"}}if(comment){str+=\"\\n\"+indentComment(commentString(comment),indent);if(onComment)onComment()}else if(chompKeep&&onChompKeep)onChompKeep();return str}function stringifyFlowCollection({comment,items},ctx,{flowChars,itemIndent,onComment}){const{indent,indentStep,flowCollectionPadding:fcPadding,options:{commentString}}=ctx;itemIndent+=indentStep;const itemCtx=Object.assign({},ctx,{indent:itemIndent,inFlow:true,type:null});let reqNewline=false;let linesAtValue=0;const lines=[];for(let i=0;i<items.length;++i){const item=items[i];let comment2=null;if(isNode(item)){if(item.spaceBefore)lines.push(\"\");addCommentBefore(ctx,lines,item.commentBefore,false);if(item.comment)comment2=item.comment}else if(isPair(item)){const ik=isNode(item.key)?item.key:null;if(ik){if(ik.spaceBefore)lines.push(\"\");addCommentBefore(ctx,lines,ik.commentBefore,false);if(ik.comment)reqNewline=true}const iv=isNode(item.value)?item.value:null;if(iv){if(iv.comment)comment2=iv.comment;if(iv.commentBefore)reqNewline=true}else if(item.value==null&&ik?.comment){comment2=ik.comment}}if(comment2)reqNewline=true;let str2=stringify(item,itemCtx,()=>comment2=null);if(i<items.length-1)str2+=\",\";if(comment2)str2+=lineComment(str2,itemIndent,commentString(comment2));if(!reqNewline&&(lines.length>linesAtValue||str2.includes(\"\\n\")))reqNewline=true;lines.push(str2);linesAtValue=lines.length}let str;const{start,end}=flowChars;if(lines.length===0){str=start+end}else{if(!reqNewline){const len=lines.reduce((sum,line)=>sum+line.length+2,2);reqNewline=ctx.options.lineWidth>0&&len>ctx.options.lineWidth}if(reqNewline){str=start;for(const line of lines)str+=line?`\n${indentStep}${indent}${line}`:\"\\n\";str+=`\n${indent}${end}`}else{str=`${start}${fcPadding}${lines.join(\" \")}${fcPadding}${end}`}}if(comment){str+=lineComment(str,indent,commentString(comment));if(onComment)onComment()}return str}function addCommentBefore({indent,options:{commentString}},lines,comment,chompKeep){if(comment&&chompKeep)comment=comment.replace(/^\\n+/,\"\");if(comment){const ic=indentComment(commentString(comment),indent);lines.push(ic.trimStart())}}function findPair(items,key){const k=isScalar(key)?key.value:key;for(const it of items){if(isPair(it)){if(it.key===key||it.key===k)return it;if(isScalar(it.key)&&it.key.value===k)return it}}return void 0}var YAMLMap=class extends Collection{static get tagName(){return\"tag:yaml.org,2002:map\"}constructor(schema4){super(MAP,schema4);this.items=[]}static from(schema4,obj,ctx){const{keepUndefined,replacer}=ctx;const map2=new this(schema4);const add=(key,value)=>{if(typeof replacer===\"function\")value=replacer.call(obj,key,value);else if(Array.isArray(replacer)&&!replacer.includes(key))return;if(value!==void 0||keepUndefined)map2.items.push(createPair(key,value,ctx))};if(obj instanceof Map){for(const[key,value]of obj)add(key,value)}else if(obj&&typeof obj===\"object\"){for(const key of Object.keys(obj))add(key,obj[key])}if(typeof schema4.sortMapEntries===\"function\"){map2.items.sort(schema4.sortMapEntries)}return map2}add(pair,overwrite){let _pair;if(isPair(pair))_pair=pair;else if(!pair||typeof pair!==\"object\"||!(\"key\"in pair)){_pair=new Pair(pair,pair?.value)}else _pair=new Pair(pair.key,pair.value);const prev=findPair(this.items,_pair.key);const sortEntries=this.schema?.sortMapEntries;if(prev){if(!overwrite)throw new Error(`Key ${_pair.key} already set`);if(isScalar(prev.value)&&isScalarValue(_pair.value))prev.value.value=_pair.value;else prev.value=_pair.value}else if(sortEntries){const i=this.items.findIndex(item=>sortEntries(_pair,item)<0);if(i===-1)this.items.push(_pair);else this.items.splice(i,0,_pair)}else{this.items.push(_pair)}}delete(key){const it=findPair(this.items,key);if(!it)return false;const del=this.items.splice(this.items.indexOf(it),1);return del.length>0}get(key,keepScalar){const it=findPair(this.items,key);const node=it?.value;return(!keepScalar&&isScalar(node)?node.value:node)??void 0}has(key){return!!findPair(this.items,key)}set(key,value){this.add(new Pair(key,value),true)}toJSON(_,ctx,Type){const map2=Type?new Type:ctx?.mapAsMap?new Map:{};if(ctx?.onCreate)ctx.onCreate(map2);for(const item of this.items)addPairToJSMap(ctx,map2,item);return map2}toString(ctx,onComment,onChompKeep){if(!ctx)return JSON.stringify(this);for(const item of this.items){if(!isPair(item))throw new Error(`Map items must all be pairs; found ${JSON.stringify(item)} instead`)}if(!ctx.allNullValues&&this.hasAllNullValues(false))ctx=Object.assign({},ctx,{allNullValues:true});return stringifyCollection(this,ctx,{blockItemPrefix:\"\",flowChars:{start:\"{\",end:\"}\"},itemIndent:ctx.indent||\"\",onChompKeep,onComment})}};var map={collection:\"map\",default:true,nodeClass:YAMLMap,tag:\"tag:yaml.org,2002:map\",resolve(map2,onError){if(!isMap(map2))onError(\"Expected a mapping for this tag\");return map2},createNode:(schema4,obj,ctx)=>YAMLMap.from(schema4,obj,ctx)};var YAMLSeq=class extends Collection{static get tagName(){return\"tag:yaml.org,2002:seq\"}constructor(schema4){super(SEQ,schema4);this.items=[]}add(value){this.items.push(value)}delete(key){const idx=asItemIndex(key);if(typeof idx!==\"number\")return false;const del=this.items.splice(idx,1);return del.length>0}get(key,keepScalar){const idx=asItemIndex(key);if(typeof idx!==\"number\")return void 0;const it=this.items[idx];return!keepScalar&&isScalar(it)?it.value:it}has(key){const idx=asItemIndex(key);return typeof idx===\"number\"&&idx<this.items.length}set(key,value){const idx=asItemIndex(key);if(typeof idx!==\"number\")throw new Error(`Expected a valid index, not ${key}.`);const prev=this.items[idx];if(isScalar(prev)&&isScalarValue(value))prev.value=value;else this.items[idx]=value}toJSON(_,ctx){const seq2=[];if(ctx?.onCreate)ctx.onCreate(seq2);let i=0;for(const item of this.items)seq2.push(toJS(item,String(i++),ctx));return seq2}toString(ctx,onComment,onChompKeep){if(!ctx)return JSON.stringify(this);return stringifyCollection(this,ctx,{blockItemPrefix:\"- \",flowChars:{start:\"[\",end:\"]\"},itemIndent:(ctx.indent||\"\")+\"  \",onChompKeep,onComment})}static from(schema4,obj,ctx){const{replacer}=ctx;const seq2=new this(schema4);if(obj&&Symbol.iterator in Object(obj)){let i=0;for(let it of obj){if(typeof replacer===\"function\"){const key=obj instanceof Set?it:String(i++);it=replacer.call(obj,key,it)}seq2.items.push(createNode(it,void 0,ctx))}}return seq2}};function asItemIndex(key){let idx=isScalar(key)?key.value:key;if(idx&&typeof idx===\"string\")idx=Number(idx);return typeof idx===\"number\"&&Number.isInteger(idx)&&idx>=0?idx:null}var seq={collection:\"seq\",default:true,nodeClass:YAMLSeq,tag:\"tag:yaml.org,2002:seq\",resolve(seq2,onError){if(!isSeq(seq2))onError(\"Expected a sequence for this tag\");return seq2},createNode:(schema4,obj,ctx)=>YAMLSeq.from(schema4,obj,ctx)};var string={identify:value=>typeof value===\"string\",default:true,tag:\"tag:yaml.org,2002:str\",resolve:str=>str,stringify(item,ctx,onComment,onChompKeep){ctx=Object.assign({actualString:true},ctx);return stringifyString(item,ctx,onComment,onChompKeep)}};var nullTag={identify:value=>value==null,createNode:()=>new Scalar(null),default:true,tag:\"tag:yaml.org,2002:null\",test:/^(?:~|[Nn]ull|NULL)?$/,resolve:()=>new Scalar(null),stringify:({source},ctx)=>typeof source===\"string\"&&nullTag.test.test(source)?source:ctx.options.nullStr};var boolTag={identify:value=>typeof value===\"boolean\",default:true,tag:\"tag:yaml.org,2002:bool\",test:/^(?:[Tt]rue|TRUE|[Ff]alse|FALSE)$/,resolve:str=>new Scalar(str[0]===\"t\"||str[0]===\"T\"),stringify({source,value},ctx){if(source&&boolTag.test.test(source)){const sv=source[0]===\"t\"||source[0]===\"T\";if(value===sv)return source}return value?ctx.options.trueStr:ctx.options.falseStr}};function stringifyNumber({format,minFractionDigits,tag,value}){if(typeof value===\"bigint\")return String(value);const num=typeof value===\"number\"?value:Number(value);if(!isFinite(num))return isNaN(num)?\".nan\":num<0?\"-.inf\":\".inf\";let n=JSON.stringify(value);if(!format&&minFractionDigits&&(!tag||tag===\"tag:yaml.org,2002:float\")&&/^\\d/.test(n)){let i=n.indexOf(\".\");if(i<0){i=n.length;n+=\".\"}let d=minFractionDigits-(n.length-i-1);while(d-- >0)n+=\"0\"}return n}var floatNaN={identify:value=>typeof value===\"number\",default:true,tag:\"tag:yaml.org,2002:float\",test:/^(?:[-+]?\\.(?:inf|Inf|INF|nan|NaN|NAN))$/,resolve:str=>str.slice(-3).toLowerCase()===\"nan\"?NaN:str[0]===\"-\"?Number.NEGATIVE_INFINITY:Number.POSITIVE_INFINITY,stringify:stringifyNumber};var floatExp={identify:value=>typeof value===\"number\",default:true,tag:\"tag:yaml.org,2002:float\",format:\"EXP\",test:/^[-+]?(?:\\.[0-9]+|[0-9]+(?:\\.[0-9]*)?)[eE][-+]?[0-9]+$/,resolve:str=>parseFloat(str),stringify(node){const num=Number(node.value);return isFinite(num)?num.toExponential():stringifyNumber(node)}};var float={identify:value=>typeof value===\"number\",default:true,tag:\"tag:yaml.org,2002:float\",test:/^[-+]?(?:\\.[0-9]+|[0-9]+\\.[0-9]*)$/,resolve(str){const node=new Scalar(parseFloat(str));const dot=str.indexOf(\".\");if(dot!==-1&&str[str.length-1]===\"0\")node.minFractionDigits=str.length-dot-1;return node},stringify:stringifyNumber};var intIdentify=value=>typeof value===\"bigint\"||Number.isInteger(value);var intResolve=(str,offset,radix,{intAsBigInt})=>intAsBigInt?BigInt(str):parseInt(str.substring(offset),radix);function intStringify(node,radix,prefix){const{value}=node;if(intIdentify(value)&&value>=0)return prefix+value.toString(radix);return stringifyNumber(node)}var intOct={identify:value=>intIdentify(value)&&value>=0,default:true,tag:\"tag:yaml.org,2002:int\",format:\"OCT\",test:/^0o[0-7]+$/,resolve:(str,_onError,opt)=>intResolve(str,2,8,opt),stringify:node=>intStringify(node,8,\"0o\")};var int={identify:intIdentify,default:true,tag:\"tag:yaml.org,2002:int\",test:/^[-+]?[0-9]+$/,resolve:(str,_onError,opt)=>intResolve(str,0,10,opt),stringify:stringifyNumber};var intHex={identify:value=>intIdentify(value)&&value>=0,default:true,tag:\"tag:yaml.org,2002:int\",format:\"HEX\",test:/^0x[0-9a-fA-F]+$/,resolve:(str,_onError,opt)=>intResolve(str,2,16,opt),stringify:node=>intStringify(node,16,\"0x\")};var schema=[map,seq,string,nullTag,boolTag,intOct,int,intHex,floatNaN,floatExp,float];function intIdentify2(value){return typeof value===\"bigint\"||Number.isInteger(value)}var stringifyJSON=({value})=>JSON.stringify(value);var jsonScalars=[{identify:value=>typeof value===\"string\",default:true,tag:\"tag:yaml.org,2002:str\",resolve:str=>str,stringify:stringifyJSON},{identify:value=>value==null,createNode:()=>new Scalar(null),default:true,tag:\"tag:yaml.org,2002:null\",test:/^null$/,resolve:()=>null,stringify:stringifyJSON},{identify:value=>typeof value===\"boolean\",default:true,tag:\"tag:yaml.org,2002:bool\",test:/^true|false$/,resolve:str=>str===\"true\",stringify:stringifyJSON},{identify:intIdentify2,default:true,tag:\"tag:yaml.org,2002:int\",test:/^-?(?:0|[1-9][0-9]*)$/,resolve:(str,_onError,{intAsBigInt})=>intAsBigInt?BigInt(str):parseInt(str,10),stringify:({value})=>intIdentify2(value)?value.toString():JSON.stringify(value)},{identify:value=>typeof value===\"number\",default:true,tag:\"tag:yaml.org,2002:float\",test:/^-?(?:0|[1-9][0-9]*)(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?$/,resolve:str=>parseFloat(str),stringify:stringifyJSON}];var jsonError={default:true,tag:\"\",test:/^/,resolve(str,onError){onError(`Unresolved plain scalar ${JSON.stringify(str)}`);return str}};var schema2=[map,seq].concat(jsonScalars,jsonError);var binary={identify:value=>value instanceof Uint8Array,default:false,tag:\"tag:yaml.org,2002:binary\",resolve(src,onError){if(typeof Buffer===\"function\"){return Buffer.from(src,\"base64\")}else if(typeof atob===\"function\"){const str=atob(src.replace(/[\\n\\r]/g,\"\"));const buffer=new Uint8Array(str.length);for(let i=0;i<str.length;++i)buffer[i]=str.charCodeAt(i);return buffer}else{onError(\"This environment does not support reading binary tags; either Buffer or atob is required\");return src}},stringify({comment,type,value},ctx,onComment,onChompKeep){const buf=value;let str;if(typeof Buffer===\"function\"){str=buf instanceof Buffer?buf.toString(\"base64\"):Buffer.from(buf.buffer).toString(\"base64\")}else if(typeof btoa===\"function\"){let s=\"\";for(let i=0;i<buf.length;++i)s+=String.fromCharCode(buf[i]);str=btoa(s)}else{throw new Error(\"This environment does not support writing binary tags; either Buffer or btoa is required\")}if(!type)type=Scalar.BLOCK_LITERAL;if(type!==Scalar.QUOTE_DOUBLE){const lineWidth=Math.max(ctx.options.lineWidth-ctx.indent.length,ctx.options.minContentWidth);const n=Math.ceil(str.length/lineWidth);const lines=new Array(n);for(let i=0,o=0;i<n;++i,o+=lineWidth){lines[i]=str.substr(o,lineWidth)}str=lines.join(type===Scalar.BLOCK_LITERAL?\"\\n\":\" \")}return stringifyString({comment,type,value:str},ctx,onComment,onChompKeep)}};function resolvePairs(seq2,onError){if(isSeq(seq2)){for(let i=0;i<seq2.items.length;++i){let item=seq2.items[i];if(isPair(item))continue;else if(isMap(item)){if(item.items.length>1)onError(\"Each pair must have its own sequence indicator\");const pair=item.items[0]||new Pair(new Scalar(null));if(item.commentBefore)pair.key.commentBefore=pair.key.commentBefore?`${item.commentBefore}\n${pair.key.commentBefore}`:item.commentBefore;if(item.comment){const cn=pair.value??pair.key;cn.comment=cn.comment?`${item.comment}\n${cn.comment}`:item.comment}item=pair}seq2.items[i]=isPair(item)?item:new Pair(item)}}else onError(\"Expected a sequence for this tag\");return seq2}function createPairs(schema4,iterable,ctx){const{replacer}=ctx;const pairs2=new YAMLSeq(schema4);pairs2.tag=\"tag:yaml.org,2002:pairs\";let i=0;if(iterable&&Symbol.iterator in Object(iterable))for(let it of iterable){if(typeof replacer===\"function\")it=replacer.call(iterable,String(i++),it);let key,value;if(Array.isArray(it)){if(it.length===2){key=it[0];value=it[1]}else throw new TypeError(`Expected [key, value] tuple: ${it}`)}else if(it&&it instanceof Object){const keys=Object.keys(it);if(keys.length===1){key=keys[0];value=it[key]}else{throw new TypeError(`Expected tuple with one key, not ${keys.length} keys`)}}else{key=it}pairs2.items.push(createPair(key,value,ctx))}return pairs2}var pairs={collection:\"seq\",default:false,tag:\"tag:yaml.org,2002:pairs\",resolve:resolvePairs,createNode:createPairs};var YAMLOMap=class _YAMLOMap extends YAMLSeq{constructor(){super();this.add=YAMLMap.prototype.add.bind(this);this.delete=YAMLMap.prototype.delete.bind(this);this.get=YAMLMap.prototype.get.bind(this);this.has=YAMLMap.prototype.has.bind(this);this.set=YAMLMap.prototype.set.bind(this);this.tag=_YAMLOMap.tag}toJSON(_,ctx){if(!ctx)return super.toJSON(_);const map2=new Map;if(ctx?.onCreate)ctx.onCreate(map2);for(const pair of this.items){let key,value;if(isPair(pair)){key=toJS(pair.key,\"\",ctx);value=toJS(pair.value,key,ctx)}else{key=toJS(pair,\"\",ctx)}if(map2.has(key))throw new Error(\"Ordered maps must not include duplicate keys\");map2.set(key,value)}return map2}static from(schema4,iterable,ctx){const pairs2=createPairs(schema4,iterable,ctx);const omap2=new this;omap2.items=pairs2.items;return omap2}};YAMLOMap.tag=\"tag:yaml.org,2002:omap\";var omap={collection:\"seq\",identify:value=>value instanceof Map,nodeClass:YAMLOMap,default:false,tag:\"tag:yaml.org,2002:omap\",resolve(seq2,onError){const pairs2=resolvePairs(seq2,onError);const seenKeys=[];for(const{key}of pairs2.items){if(isScalar(key)){if(seenKeys.includes(key.value)){onError(`Ordered maps must not include duplicate keys: ${key.value}`)}else{seenKeys.push(key.value)}}}return Object.assign(new YAMLOMap,pairs2)},createNode:(schema4,iterable,ctx)=>YAMLOMap.from(schema4,iterable,ctx)};function boolStringify({value,source},ctx){const boolObj=value?trueTag:falseTag;if(source&&boolObj.test.test(source))return source;return value?ctx.options.trueStr:ctx.options.falseStr}var trueTag={identify:value=>value===true,default:true,tag:\"tag:yaml.org,2002:bool\",test:/^(?:Y|y|[Yy]es|YES|[Tt]rue|TRUE|[Oo]n|ON)$/,resolve:()=>new Scalar(true),stringify:boolStringify};var falseTag={identify:value=>value===false,default:true,tag:\"tag:yaml.org,2002:bool\",test:/^(?:N|n|[Nn]o|NO|[Ff]alse|FALSE|[Oo]ff|OFF)$/i,resolve:()=>new Scalar(false),stringify:boolStringify};var floatNaN2={identify:value=>typeof value===\"number\",default:true,tag:\"tag:yaml.org,2002:float\",test:/^[-+]?\\.(?:inf|Inf|INF|nan|NaN|NAN)$/,resolve:str=>str.slice(-3).toLowerCase()===\"nan\"?NaN:str[0]===\"-\"?Number.NEGATIVE_INFINITY:Number.POSITIVE_INFINITY,stringify:stringifyNumber};var floatExp2={identify:value=>typeof value===\"number\",default:true,tag:\"tag:yaml.org,2002:float\",format:\"EXP\",test:/^[-+]?(?:[0-9][0-9_]*)?(?:\\.[0-9_]*)?[eE][-+]?[0-9]+$/,resolve:str=>parseFloat(str.replace(/_/g,\"\")),stringify(node){const num=Number(node.value);return isFinite(num)?num.toExponential():stringifyNumber(node)}};var float2={identify:value=>typeof value===\"number\",default:true,tag:\"tag:yaml.org,2002:float\",test:/^[-+]?(?:[0-9][0-9_]*)?\\.[0-9_]*$/,resolve(str){const node=new Scalar(parseFloat(str.replace(/_/g,\"\")));const dot=str.indexOf(\".\");if(dot!==-1){const f=str.substring(dot+1).replace(/_/g,\"\");if(f[f.length-1]===\"0\")node.minFractionDigits=f.length}return node},stringify:stringifyNumber};var intIdentify3=value=>typeof value===\"bigint\"||Number.isInteger(value);function intResolve2(str,offset,radix,{intAsBigInt}){const sign=str[0];if(sign===\"-\"||sign===\"+\")offset+=1;str=str.substring(offset).replace(/_/g,\"\");if(intAsBigInt){switch(radix){case 2:str=`0b${str}`;break;case 8:str=`0o${str}`;break;case 16:str=`0x${str}`;break}const n2=BigInt(str);return sign===\"-\"?BigInt(-1)*n2:n2}const n=parseInt(str,radix);return sign===\"-\"?-1*n:n}function intStringify2(node,radix,prefix){const{value}=node;if(intIdentify3(value)){const str=value.toString(radix);return value<0?\"-\"+prefix+str.substr(1):prefix+str}return stringifyNumber(node)}var intBin={identify:intIdentify3,default:true,tag:\"tag:yaml.org,2002:int\",format:\"BIN\",test:/^[-+]?0b[0-1_]+$/,resolve:(str,_onError,opt)=>intResolve2(str,2,2,opt),stringify:node=>intStringify2(node,2,\"0b\")};var intOct2={identify:intIdentify3,default:true,tag:\"tag:yaml.org,2002:int\",format:\"OCT\",test:/^[-+]?0[0-7_]+$/,resolve:(str,_onError,opt)=>intResolve2(str,1,8,opt),stringify:node=>intStringify2(node,8,\"0\")};var int2={identify:intIdentify3,default:true,tag:\"tag:yaml.org,2002:int\",test:/^[-+]?[0-9][0-9_]*$/,resolve:(str,_onError,opt)=>intResolve2(str,0,10,opt),stringify:stringifyNumber};var intHex2={identify:intIdentify3,default:true,tag:\"tag:yaml.org,2002:int\",format:\"HEX\",test:/^[-+]?0x[0-9a-fA-F_]+$/,resolve:(str,_onError,opt)=>intResolve2(str,2,16,opt),stringify:node=>intStringify2(node,16,\"0x\")};var YAMLSet=class _YAMLSet extends YAMLMap{constructor(schema4){super(schema4);this.tag=_YAMLSet.tag}add(key){let pair;if(isPair(key))pair=key;else if(key&&typeof key===\"object\"&&\"key\"in key&&\"value\"in key&&key.value===null)pair=new Pair(key.key,null);else pair=new Pair(key,null);const prev=findPair(this.items,pair.key);if(!prev)this.items.push(pair)}get(key,keepPair){const pair=findPair(this.items,key);return!keepPair&&isPair(pair)?isScalar(pair.key)?pair.key.value:pair.key:pair}set(key,value){if(typeof value!==\"boolean\")throw new Error(`Expected boolean value for set(key, value) in a YAML set, not ${typeof value}`);const prev=findPair(this.items,key);if(prev&&!value){this.items.splice(this.items.indexOf(prev),1)}else if(!prev&&value){this.items.push(new Pair(key))}}toJSON(_,ctx){return super.toJSON(_,ctx,Set)}toString(ctx,onComment,onChompKeep){if(!ctx)return JSON.stringify(this);if(this.hasAllNullValues(true))return super.toString(Object.assign({},ctx,{allNullValues:true}),onComment,onChompKeep);else throw new Error(\"Set items must all have null values\")}static from(schema4,iterable,ctx){const{replacer}=ctx;const set2=new this(schema4);if(iterable&&Symbol.iterator in Object(iterable))for(let value of iterable){if(typeof replacer===\"function\")value=replacer.call(iterable,value,value);set2.items.push(createPair(value,null,ctx))}return set2}};YAMLSet.tag=\"tag:yaml.org,2002:set\";var set={collection:\"map\",identify:value=>value instanceof Set,nodeClass:YAMLSet,default:false,tag:\"tag:yaml.org,2002:set\",createNode:(schema4,iterable,ctx)=>YAMLSet.from(schema4,iterable,ctx),resolve(map2,onError){if(isMap(map2)){if(map2.hasAllNullValues(true))return Object.assign(new YAMLSet,map2);else onError(\"Set items must all have null values\")}else onError(\"Expected a mapping for this tag\");return map2}};function parseSexagesimal(str,asBigInt){const sign=str[0];const parts=sign===\"-\"||sign===\"+\"?str.substring(1):str;const num=n=>asBigInt?BigInt(n):Number(n);const res=parts.replace(/_/g,\"\").split(\":\").reduce((res2,p)=>res2*num(60)+num(p),num(0));return sign===\"-\"?num(-1)*res:res}function stringifySexagesimal(node){let{value}=node;let num=n=>n;if(typeof value===\"bigint\")num=n=>BigInt(n);else if(isNaN(value)||!isFinite(value))return stringifyNumber(node);let sign=\"\";if(value<0){sign=\"-\";value*=num(-1)}const _60=num(60);const parts=[value%_60];if(value<60){parts.unshift(0)}else{value=(value-parts[0])/_60;parts.unshift(value%_60);if(value>=60){value=(value-parts[0])/_60;parts.unshift(value)}}return sign+parts.map(n=>String(n).padStart(2,\"0\")).join(\":\").replace(/000000\\d*$/,\"\")}var intTime={identify:value=>typeof value===\"bigint\"||Number.isInteger(value),default:true,tag:\"tag:yaml.org,2002:int\",format:\"TIME\",test:/^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+$/,resolve:(str,_onError,{intAsBigInt})=>parseSexagesimal(str,intAsBigInt),stringify:stringifySexagesimal};var floatTime={identify:value=>typeof value===\"number\",default:true,tag:\"tag:yaml.org,2002:float\",format:\"TIME\",test:/^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*$/,resolve:str=>parseSexagesimal(str,false),stringify:stringifySexagesimal};var timestamp={identify:value=>value instanceof Date,default:true,tag:\"tag:yaml.org,2002:timestamp\",test:RegExp(\"^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})(?:(?:t|T|[ \\\\t]+)([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\\\.[0-9]+)?)(?:[ \\\\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?)?$\"),resolve(str){const match=str.match(timestamp.test);if(!match)throw new Error(\"!!timestamp expects a date, starting with yyyy-mm-dd\");const[,year,month,day,hour,minute,second]=match.map(Number);const millisec=match[7]?Number((match[7]+\"00\").substr(1,3)):0;let date=Date.UTC(year,month-1,day,hour||0,minute||0,second||0,millisec);const tz=match[8];if(tz&&tz!==\"Z\"){let d=parseSexagesimal(tz,false);if(Math.abs(d)<30)d*=60;date-=6e4*d}return new Date(date)},stringify:({value})=>value.toISOString().replace(/((T00:00)?:00)?\\.000Z$/,\"\")};var schema3=[map,seq,string,nullTag,trueTag,falseTag,intBin,intOct2,int2,intHex2,floatNaN2,floatExp2,float2,binary,omap,pairs,set,intTime,floatTime,timestamp];var schemas=new Map([[\"core\",schema],[\"failsafe\",[map,seq,string]],[\"json\",schema2],[\"yaml11\",schema3],[\"yaml-1.1\",schema3]]);var tagsByName={binary,bool:boolTag,float,floatExp,floatNaN,floatTime,int,intHex,intOct,intTime,map,null:nullTag,omap,pairs,seq,set,timestamp};var coreKnownTags={\"tag:yaml.org,2002:binary\":binary,\"tag:yaml.org,2002:omap\":omap,\"tag:yaml.org,2002:pairs\":pairs,\"tag:yaml.org,2002:set\":set,\"tag:yaml.org,2002:timestamp\":timestamp};function getTags(customTags,schemaName){let tags=schemas.get(schemaName);if(!tags){if(Array.isArray(customTags))tags=[];else{const keys=Array.from(schemas.keys()).filter(key=>key!==\"yaml11\").map(key=>JSON.stringify(key)).join(\", \");throw new Error(`Unknown schema \"${schemaName}\"; use one of ${keys} or define customTags array`)}}if(Array.isArray(customTags)){for(const tag of customTags)tags=tags.concat(tag)}else if(typeof customTags===\"function\"){tags=customTags(tags.slice())}return tags.map(tag=>{if(typeof tag!==\"string\")return tag;const tagObj=tagsByName[tag];if(tagObj)return tagObj;const keys=Object.keys(tagsByName).map(key=>JSON.stringify(key)).join(\", \");throw new Error(`Unknown custom tag \"${tag}\"; use one of ${keys}`)})}var sortMapEntriesByKey=(a,b)=>a.key<b.key?-1:a.key>b.key?1:0;var Schema=class _Schema{constructor({compat,customTags,merge,resolveKnownTags,schema:schema4,sortMapEntries,toStringDefaults}){this.compat=Array.isArray(compat)?getTags(compat,\"compat\"):compat?getTags(null,compat):null;this.merge=!!merge;this.name=typeof schema4===\"string\"&&schema4||\"core\";this.knownTags=resolveKnownTags?coreKnownTags:{};this.tags=getTags(customTags,this.name);this.toStringOptions=toStringDefaults??null;Object.defineProperty(this,MAP,{value:map});Object.defineProperty(this,SCALAR,{value:string});Object.defineProperty(this,SEQ,{value:seq});this.sortMapEntries=typeof sortMapEntries===\"function\"?sortMapEntries:sortMapEntries===true?sortMapEntriesByKey:null}clone(){const copy=Object.create(_Schema.prototype,Object.getOwnPropertyDescriptors(this));copy.tags=this.tags.slice();return copy}};function stringifyDocument(doc,options){const lines=[];let hasDirectives=options.directives===true;if(options.directives!==false&&doc.directives){const dir=doc.directives.toString(doc);if(dir){lines.push(dir);hasDirectives=true}else if(doc.directives.docStart)hasDirectives=true}if(hasDirectives)lines.push(\"---\");const ctx=createStringifyContext(doc,options);const{commentString}=ctx.options;if(doc.commentBefore){if(lines.length!==1)lines.unshift(\"\");const cs=commentString(doc.commentBefore);lines.unshift(indentComment(cs,\"\"))}let chompKeep=false;let contentComment=null;if(doc.contents){if(isNode(doc.contents)){if(doc.contents.spaceBefore&&hasDirectives)lines.push(\"\");if(doc.contents.commentBefore){const cs=commentString(doc.contents.commentBefore);lines.push(indentComment(cs,\"\"))}ctx.forceBlockIndent=!!doc.comment;contentComment=doc.contents.comment}const onChompKeep=contentComment?void 0:()=>chompKeep=true;let body=stringify(doc.contents,ctx,()=>contentComment=null,onChompKeep);if(contentComment)body+=lineComment(body,\"\",commentString(contentComment));if((body[0]===\"|\"||body[0]===\">\")&&lines[lines.length-1]===\"---\"){lines[lines.length-1]=`--- ${body}`}else lines.push(body)}else{lines.push(stringify(doc.contents,ctx))}if(doc.directives?.docEnd){if(doc.comment){const cs=commentString(doc.comment);if(cs.includes(\"\\n\")){lines.push(\"...\");lines.push(indentComment(cs,\"\"))}else{lines.push(`... ${cs}`)}}else{lines.push(\"...\")}}else{let dc=doc.comment;if(dc&&chompKeep)dc=dc.replace(/^\\n+/,\"\");if(dc){if((!chompKeep||contentComment)&&lines[lines.length-1]!==\"\")lines.push(\"\");lines.push(indentComment(commentString(dc),\"\"))}}return lines.join(\"\\n\")+\"\\n\"}var Document=class _Document{constructor(value,replacer,options){this.commentBefore=null;this.comment=null;this.errors=[];this.warnings=[];Object.defineProperty(this,NODE_TYPE,{value:DOC});let _replacer=null;if(typeof replacer===\"function\"||Array.isArray(replacer)){_replacer=replacer}else if(options===void 0&&replacer){options=replacer;replacer=void 0}const opt=Object.assign({intAsBigInt:false,keepSourceTokens:false,logLevel:\"warn\",prettyErrors:true,strict:true,uniqueKeys:true,version:\"1.2\"},options);this.options=opt;let{version}=opt;if(options?._directives){this.directives=options._directives.atDocument();if(this.directives.yaml.explicit)version=this.directives.yaml.version}else this.directives=new Directives({version});this.setSchema(version,options);this.contents=value===void 0?null:this.createNode(value,_replacer,options)}clone(){const copy=Object.create(_Document.prototype,{[NODE_TYPE]:{value:DOC}});copy.commentBefore=this.commentBefore;copy.comment=this.comment;copy.errors=this.errors.slice();copy.warnings=this.warnings.slice();copy.options=Object.assign({},this.options);if(this.directives)copy.directives=this.directives.clone();copy.schema=this.schema.clone();copy.contents=isNode(this.contents)?this.contents.clone(copy.schema):this.contents;if(this.range)copy.range=this.range.slice();return copy}add(value){if(assertCollection(this.contents))this.contents.add(value)}addIn(path,value){if(assertCollection(this.contents))this.contents.addIn(path,value)}createAlias(node,name){if(!node.anchor){const prev=anchorNames(this);node.anchor=!name||prev.has(name)?findNewAnchor(name||\"a\",prev):name}return new Alias(node.anchor)}createNode(value,replacer,options){let _replacer=void 0;if(typeof replacer===\"function\"){value=replacer.call({\"\":value},\"\",value);_replacer=replacer}else if(Array.isArray(replacer)){const keyToStr=v=>typeof v===\"number\"||v instanceof String||v instanceof Number;const asStr=replacer.filter(keyToStr).map(String);if(asStr.length>0)replacer=replacer.concat(asStr);_replacer=replacer}else if(options===void 0&&replacer){options=replacer;replacer=void 0}const{aliasDuplicateObjects,anchorPrefix,flow,keepUndefined,onTagObj,tag}=options??{};const{onAnchor,setAnchors,sourceObjects}=createNodeAnchors(this,anchorPrefix||\"a\");const ctx={aliasDuplicateObjects:aliasDuplicateObjects??true,keepUndefined:keepUndefined??false,onAnchor,onTagObj,replacer:_replacer,schema:this.schema,sourceObjects};const node=createNode(value,tag,ctx);if(flow&&isCollection(node))node.flow=true;setAnchors();return node}createPair(key,value,options={}){const k=this.createNode(key,null,options);const v=this.createNode(value,null,options);return new Pair(k,v)}delete(key){return assertCollection(this.contents)?this.contents.delete(key):false}deleteIn(path){if(isEmptyPath(path)){if(this.contents==null)return false;this.contents=null;return true}return assertCollection(this.contents)?this.contents.deleteIn(path):false}get(key,keepScalar){return isCollection(this.contents)?this.contents.get(key,keepScalar):void 0}getIn(path,keepScalar){if(isEmptyPath(path))return!keepScalar&&isScalar(this.contents)?this.contents.value:this.contents;return isCollection(this.contents)?this.contents.getIn(path,keepScalar):void 0}has(key){return isCollection(this.contents)?this.contents.has(key):false}hasIn(path){if(isEmptyPath(path))return this.contents!==void 0;return isCollection(this.contents)?this.contents.hasIn(path):false}set(key,value){if(this.contents==null){this.contents=collectionFromPath(this.schema,[key],value)}else if(assertCollection(this.contents)){this.contents.set(key,value)}}setIn(path,value){if(isEmptyPath(path)){this.contents=value}else if(this.contents==null){this.contents=collectionFromPath(this.schema,Array.from(path),value)}else if(assertCollection(this.contents)){this.contents.setIn(path,value)}}setSchema(version,options={}){if(typeof version===\"number\")version=String(version);let opt;switch(version){case\"1.1\":if(this.directives)this.directives.yaml.version=\"1.1\";else this.directives=new Directives({version:\"1.1\"});opt={merge:true,resolveKnownTags:false,schema:\"yaml-1.1\"};break;case\"1.2\":case\"next\":if(this.directives)this.directives.yaml.version=version;else this.directives=new Directives({version});opt={merge:false,resolveKnownTags:true,schema:\"core\"};break;case null:if(this.directives)delete this.directives;opt=null;break;default:{const sv=JSON.stringify(version);throw new Error(`Expected '1.1', '1.2' or null as first argument, but found: ${sv}`)}}if(options.schema instanceof Object)this.schema=options.schema;else if(opt)this.schema=new Schema(Object.assign(opt,options));else throw new Error(`With a null YAML version, the { schema: Schema } option is required`)}toJS({json,jsonArg,mapAsMap,maxAliasCount,onAnchor,reviver}={}){const ctx={anchors:new Map,doc:this,keep:!json,mapAsMap:mapAsMap===true,mapKeyWarned:false,maxAliasCount:typeof maxAliasCount===\"number\"?maxAliasCount:100};const res=toJS(this.contents,jsonArg??\"\",ctx);if(typeof onAnchor===\"function\")for(const{count,res:res2}of ctx.anchors.values())onAnchor(res2,count);return typeof reviver===\"function\"?applyReviver(reviver,{\"\":res},\"\",res):res}toJSON(jsonArg,onAnchor){return this.toJS({json:true,jsonArg,mapAsMap:false,onAnchor})}toString(options={}){if(this.errors.length>0)throw new Error(\"Document with errors cannot be stringified\");if(\"indent\"in options&&(!Number.isInteger(options.indent)||Number(options.indent)<=0)){const s=JSON.stringify(options.indent);throw new Error(`\"indent\" option must be a positive integer, not ${s}`)}return stringifyDocument(this,options)}};function assertCollection(contents){if(isCollection(contents))return true;throw new Error(\"Expected a YAML collection as document contents\")}var YAMLError=class extends Error{constructor(name,pos,code,message){super();this.name=name;this.code=code;this.message=message;this.pos=pos}};var YAMLParseError=class extends YAMLError{constructor(pos,code,message){super(\"YAMLParseError\",pos,code,message)}};var YAMLWarning=class extends YAMLError{constructor(pos,code,message){super(\"YAMLWarning\",pos,code,message)}};var prettifyError=(src,lc)=>error=>{if(error.pos[0]===-1)return;error.linePos=error.pos.map(pos=>lc.linePos(pos));const{line,col}=error.linePos[0];error.message+=` at line ${line}, column ${col}`;let ci=col-1;let lineStr=src.substring(lc.lineStarts[line-1],lc.lineStarts[line]).replace(/[\\n\\r]+$/,\"\");if(ci>=60&&lineStr.length>80){const trimStart=Math.min(ci-39,lineStr.length-79);lineStr=\"\\u2026\"+lineStr.substring(trimStart);ci-=trimStart-1}if(lineStr.length>80)lineStr=lineStr.substring(0,79)+\"\\u2026\";if(line>1&&/^ *$/.test(lineStr.substring(0,ci))){let prev=src.substring(lc.lineStarts[line-2],lc.lineStarts[line-1]);if(prev.length>80)prev=prev.substring(0,79)+\"\\u2026\\n\";lineStr=prev+lineStr}if(/[^ ]/.test(lineStr)){let count=1;const end=error.linePos[1];if(end&&end.line===line&&end.col>col){count=Math.max(1,Math.min(end.col-col,80-ci))}const pointer=\" \".repeat(ci)+\"^\".repeat(count);error.message+=`:\n\n${lineStr}\n${pointer}\n`}};function resolveProps(tokens,{flow,indicator,next,offset,onError,startOnNewline}){let spaceBefore=false;let atNewline=startOnNewline;let hasSpace=startOnNewline;let comment=\"\";let commentSep=\"\";let hasNewline=false;let hasNewlineAfterProp=false;let reqSpace=false;let anchor=null;let tag=null;let comma=null;let found=null;let start=null;for(const token of tokens){if(reqSpace){if(token.type!==\"space\"&&token.type!==\"newline\"&&token.type!==\"comma\")onError(token.offset,\"MISSING_CHAR\",\"Tags and anchors must be separated from the next token by white space\");reqSpace=false}switch(token.type){case\"space\":if(!flow&&atNewline&&indicator!==\"doc-start\"&&token.source[0]===\"\t\")onError(token,\"TAB_AS_INDENT\",\"Tabs are not allowed as indentation\");hasSpace=true;break;case\"comment\":{if(!hasSpace)onError(token,\"MISSING_CHAR\",\"Comments must be separated from other tokens by white space characters\");const cb=token.source.substring(1)||\" \";if(!comment)comment=cb;else comment+=commentSep+cb;commentSep=\"\";atNewline=false;break}case\"newline\":if(atNewline){if(comment)comment+=token.source;else spaceBefore=true}else commentSep+=token.source;atNewline=true;hasNewline=true;if(anchor||tag)hasNewlineAfterProp=true;hasSpace=true;break;case\"anchor\":if(anchor)onError(token,\"MULTIPLE_ANCHORS\",\"A node can have at most one anchor\");if(token.source.endsWith(\":\"))onError(token.offset+token.source.length-1,\"BAD_ALIAS\",\"Anchor ending in : is ambiguous\",true);anchor=token;if(start===null)start=token.offset;atNewline=false;hasSpace=false;reqSpace=true;break;case\"tag\":{if(tag)onError(token,\"MULTIPLE_TAGS\",\"A node can have at most one tag\");tag=token;if(start===null)start=token.offset;atNewline=false;hasSpace=false;reqSpace=true;break}case indicator:if(anchor||tag)onError(token,\"BAD_PROP_ORDER\",`Anchors and tags must be after the ${token.source} indicator`);if(found)onError(token,\"UNEXPECTED_TOKEN\",`Unexpected ${token.source} in ${flow??\"collection\"}`);found=token;atNewline=false;hasSpace=false;break;case\"comma\":if(flow){if(comma)onError(token,\"UNEXPECTED_TOKEN\",`Unexpected , in ${flow}`);comma=token;atNewline=false;hasSpace=false;break}default:onError(token,\"UNEXPECTED_TOKEN\",`Unexpected ${token.type} token`);atNewline=false;hasSpace=false}}const last=tokens[tokens.length-1];const end=last?last.offset+last.source.length:offset;if(reqSpace&&next&&next.type!==\"space\"&&next.type!==\"newline\"&&next.type!==\"comma\"&&(next.type!==\"scalar\"||next.source!==\"\"))onError(next.offset,\"MISSING_CHAR\",\"Tags and anchors must be separated from the next token by white space\");return{comma,found,spaceBefore,comment,hasNewline,hasNewlineAfterProp,anchor,tag,end,start:start??end}}function containsNewline(key){if(!key)return null;switch(key.type){case\"alias\":case\"scalar\":case\"double-quoted-scalar\":case\"single-quoted-scalar\":if(key.source.includes(\"\\n\"))return true;if(key.end){for(const st of key.end)if(st.type===\"newline\")return true}return false;case\"flow-collection\":for(const it of key.items){for(const st of it.start)if(st.type===\"newline\")return true;if(it.sep){for(const st of it.sep)if(st.type===\"newline\")return true}if(containsNewline(it.key)||containsNewline(it.value))return true}return false;default:return true}}function flowIndentCheck(indent,fc,onError){if(fc?.type===\"flow-collection\"){const end=fc.end[0];if(end.indent===indent&&(end.source===\"]\"||end.source===\"}\")&&containsNewline(fc)){const msg=\"Flow end indicator should be more indented than parent\";onError(end,\"BAD_INDENT\",msg,true)}}}function mapIncludes(ctx,items,search){const{uniqueKeys}=ctx.options;if(uniqueKeys===false)return false;const isEqual=typeof uniqueKeys===\"function\"?uniqueKeys:(a,b)=>a===b||isScalar(a)&&isScalar(b)&&a.value===b.value&&!(a.value===\"<<\"&&ctx.schema.merge);return items.some(pair=>isEqual(pair.key,search))}var startColMsg=\"All mapping items must start at the same column\";function resolveBlockMap({composeNode:composeNode2,composeEmptyNode:composeEmptyNode2},ctx,bm,onError,tag){const NodeClass=tag?.nodeClass??YAMLMap;const map2=new NodeClass(ctx.schema);if(ctx.atRoot)ctx.atRoot=false;let offset=bm.offset;let commentEnd=null;for(const collItem of bm.items){const{start,key,sep,value}=collItem;const keyProps=resolveProps(start,{indicator:\"explicit-key-ind\",next:key??sep?.[0],offset,onError,startOnNewline:true});const implicitKey=!keyProps.found;if(implicitKey){if(key){if(key.type===\"block-seq\")onError(offset,\"BLOCK_AS_IMPLICIT_KEY\",\"A block sequence may not be used as an implicit map key\");else if(\"indent\"in key&&key.indent!==bm.indent)onError(offset,\"BAD_INDENT\",startColMsg)}if(!keyProps.anchor&&!keyProps.tag&&!sep){commentEnd=keyProps.end;if(keyProps.comment){if(map2.comment)map2.comment+=\"\\n\"+keyProps.comment;else map2.comment=keyProps.comment}continue}if(keyProps.hasNewlineAfterProp||containsNewline(key)){onError(key??start[start.length-1],\"MULTILINE_IMPLICIT_KEY\",\"Implicit keys need to be on a single line\")}}else if(keyProps.found?.indent!==bm.indent){onError(offset,\"BAD_INDENT\",startColMsg)}const keyStart=keyProps.end;const keyNode=key?composeNode2(ctx,key,keyProps,onError):composeEmptyNode2(ctx,keyStart,start,null,keyProps,onError);if(ctx.schema.compat)flowIndentCheck(bm.indent,key,onError);if(mapIncludes(ctx,map2.items,keyNode))onError(keyStart,\"DUPLICATE_KEY\",\"Map keys must be unique\");const valueProps=resolveProps(sep??[],{indicator:\"map-value-ind\",next:value,offset:keyNode.range[2],onError,startOnNewline:!key||key.type===\"block-scalar\"});offset=valueProps.end;if(valueProps.found){if(implicitKey){if(value?.type===\"block-map\"&&!valueProps.hasNewline)onError(offset,\"BLOCK_AS_IMPLICIT_KEY\",\"Nested mappings are not allowed in compact mappings\");if(ctx.options.strict&&keyProps.start<valueProps.found.offset-1024)onError(keyNode.range,\"KEY_OVER_1024_CHARS\",\"The : indicator must be at most 1024 chars after the start of an implicit block mapping key\")}const valueNode=value?composeNode2(ctx,value,valueProps,onError):composeEmptyNode2(ctx,offset,sep,null,valueProps,onError);if(ctx.schema.compat)flowIndentCheck(bm.indent,value,onError);offset=valueNode.range[2];const pair=new Pair(keyNode,valueNode);if(ctx.options.keepSourceTokens)pair.srcToken=collItem;map2.items.push(pair)}else{if(implicitKey)onError(keyNode.range,\"MISSING_CHAR\",\"Implicit map keys need to be followed by map values\");if(valueProps.comment){if(keyNode.comment)keyNode.comment+=\"\\n\"+valueProps.comment;else keyNode.comment=valueProps.comment}const pair=new Pair(keyNode);if(ctx.options.keepSourceTokens)pair.srcToken=collItem;map2.items.push(pair)}}if(commentEnd&&commentEnd<offset)onError(commentEnd,\"IMPOSSIBLE\",\"Map comment with trailing content\");map2.range=[bm.offset,offset,commentEnd??offset];return map2}function resolveBlockSeq({composeNode:composeNode2,composeEmptyNode:composeEmptyNode2},ctx,bs,onError,tag){const NodeClass=tag?.nodeClass??YAMLSeq;const seq2=new NodeClass(ctx.schema);if(ctx.atRoot)ctx.atRoot=false;let offset=bs.offset;let commentEnd=null;for(const{start,value}of bs.items){const props=resolveProps(start,{indicator:\"seq-item-ind\",next:value,offset,onError,startOnNewline:true});if(!props.found){if(props.anchor||props.tag||value){if(value&&value.type===\"block-seq\")onError(props.end,\"BAD_INDENT\",\"All sequence items must start at the same column\");else onError(offset,\"MISSING_CHAR\",\"Sequence item without - indicator\")}else{commentEnd=props.end;if(props.comment)seq2.comment=props.comment;continue}}const node=value?composeNode2(ctx,value,props,onError):composeEmptyNode2(ctx,props.end,start,null,props,onError);if(ctx.schema.compat)flowIndentCheck(bs.indent,value,onError);offset=node.range[2];seq2.items.push(node)}seq2.range=[bs.offset,offset,commentEnd??offset];return seq2}function resolveEnd(end,offset,reqSpace,onError){let comment=\"\";if(end){let hasSpace=false;let sep=\"\";for(const token of end){const{source,type}=token;switch(type){case\"space\":hasSpace=true;break;case\"comment\":{if(reqSpace&&!hasSpace)onError(token,\"MISSING_CHAR\",\"Comments must be separated from other tokens by white space characters\");const cb=source.substring(1)||\" \";if(!comment)comment=cb;else comment+=sep+cb;sep=\"\";break}case\"newline\":if(comment)sep+=source;hasSpace=true;break;default:onError(token,\"UNEXPECTED_TOKEN\",`Unexpected ${type} at node end`)}offset+=source.length}}return{comment,offset}}var blockMsg=\"Block collections are not allowed within flow collections\";var isBlock=token=>token&&(token.type===\"block-map\"||token.type===\"block-seq\");function resolveFlowCollection({composeNode:composeNode2,composeEmptyNode:composeEmptyNode2},ctx,fc,onError,tag){const isMap2=fc.start.source===\"{\";const fcName=isMap2?\"flow map\":\"flow sequence\";const NodeClass=tag?.nodeClass??(isMap2?YAMLMap:YAMLSeq);const coll=new NodeClass(ctx.schema);coll.flow=true;const atRoot=ctx.atRoot;if(atRoot)ctx.atRoot=false;let offset=fc.offset+fc.start.source.length;for(let i=0;i<fc.items.length;++i){const collItem=fc.items[i];const{start,key,sep,value}=collItem;const props=resolveProps(start,{flow:fcName,indicator:\"explicit-key-ind\",next:key??sep?.[0],offset,onError,startOnNewline:false});if(!props.found){if(!props.anchor&&!props.tag&&!sep&&!value){if(i===0&&props.comma)onError(props.comma,\"UNEXPECTED_TOKEN\",`Unexpected , in ${fcName}`);else if(i<fc.items.length-1)onError(props.start,\"UNEXPECTED_TOKEN\",`Unexpected empty item in ${fcName}`);if(props.comment){if(coll.comment)coll.comment+=\"\\n\"+props.comment;else coll.comment=props.comment}offset=props.end;continue}if(!isMap2&&ctx.options.strict&&containsNewline(key))onError(key,\"MULTILINE_IMPLICIT_KEY\",\"Implicit keys of flow sequence pairs need to be on a single line\")}if(i===0){if(props.comma)onError(props.comma,\"UNEXPECTED_TOKEN\",`Unexpected , in ${fcName}`)}else{if(!props.comma)onError(props.start,\"MISSING_CHAR\",`Missing , between ${fcName} items`);if(props.comment){let prevItemComment=\"\";loop:for(const st of start){switch(st.type){case\"comma\":case\"space\":break;case\"comment\":prevItemComment=st.source.substring(1);break loop;default:break loop}}if(prevItemComment){let prev=coll.items[coll.items.length-1];if(isPair(prev))prev=prev.value??prev.key;if(prev.comment)prev.comment+=\"\\n\"+prevItemComment;else prev.comment=prevItemComment;props.comment=props.comment.substring(prevItemComment.length+1)}}}if(!isMap2&&!sep&&!props.found){const valueNode=value?composeNode2(ctx,value,props,onError):composeEmptyNode2(ctx,props.end,sep,null,props,onError);coll.items.push(valueNode);offset=valueNode.range[2];if(isBlock(value))onError(valueNode.range,\"BLOCK_IN_FLOW\",blockMsg)}else{const keyStart=props.end;const keyNode=key?composeNode2(ctx,key,props,onError):composeEmptyNode2(ctx,keyStart,start,null,props,onError);if(isBlock(key))onError(keyNode.range,\"BLOCK_IN_FLOW\",blockMsg);const valueProps=resolveProps(sep??[],{flow:fcName,indicator:\"map-value-ind\",next:value,offset:keyNode.range[2],onError,startOnNewline:false});if(valueProps.found){if(!isMap2&&!props.found&&ctx.options.strict){if(sep)for(const st of sep){if(st===valueProps.found)break;if(st.type===\"newline\"){onError(st,\"MULTILINE_IMPLICIT_KEY\",\"Implicit keys of flow sequence pairs need to be on a single line\");break}}if(props.start<valueProps.found.offset-1024)onError(valueProps.found,\"KEY_OVER_1024_CHARS\",\"The : indicator must be at most 1024 chars after the start of an implicit flow sequence key\")}}else if(value){if(\"source\"in value&&value.source&&value.source[0]===\":\")onError(value,\"MISSING_CHAR\",`Missing space after : in ${fcName}`);else onError(valueProps.start,\"MISSING_CHAR\",`Missing , or : between ${fcName} items`)}const valueNode=value?composeNode2(ctx,value,valueProps,onError):valueProps.found?composeEmptyNode2(ctx,valueProps.end,sep,null,valueProps,onError):null;if(valueNode){if(isBlock(value))onError(valueNode.range,\"BLOCK_IN_FLOW\",blockMsg)}else if(valueProps.comment){if(keyNode.comment)keyNode.comment+=\"\\n\"+valueProps.comment;else keyNode.comment=valueProps.comment}const pair=new Pair(keyNode,valueNode);if(ctx.options.keepSourceTokens)pair.srcToken=collItem;if(isMap2){const map2=coll;if(mapIncludes(ctx,map2.items,keyNode))onError(keyStart,\"DUPLICATE_KEY\",\"Map keys must be unique\");map2.items.push(pair)}else{const map2=new YAMLMap(ctx.schema);map2.flow=true;map2.items.push(pair);coll.items.push(map2)}offset=valueNode?valueNode.range[2]:valueProps.end}}const expectedEnd=isMap2?\"}\":\"]\";const[ce,...ee]=fc.end;let cePos=offset;if(ce&&ce.source===expectedEnd)cePos=ce.offset+ce.source.length;else{const name=fcName[0].toUpperCase()+fcName.substring(1);const msg=atRoot?`${name} must end with a ${expectedEnd}`:`${name} in block collection must be sufficiently indented and end with a ${expectedEnd}`;onError(offset,atRoot?\"MISSING_CHAR\":\"BAD_INDENT\",msg);if(ce&&ce.source.length!==1)ee.unshift(ce)}if(ee.length>0){const end=resolveEnd(ee,cePos,ctx.options.strict,onError);if(end.comment){if(coll.comment)coll.comment+=\"\\n\"+end.comment;else coll.comment=end.comment}coll.range=[fc.offset,cePos,end.offset]}else{coll.range=[fc.offset,cePos,cePos]}return coll}function resolveCollection(CN2,ctx,token,onError,tagName,tag){const coll=token.type===\"block-map\"?resolveBlockMap(CN2,ctx,token,onError,tag):token.type===\"block-seq\"?resolveBlockSeq(CN2,ctx,token,onError,tag):resolveFlowCollection(CN2,ctx,token,onError,tag);const Coll=coll.constructor;if(tagName===\"!\"||tagName===Coll.tagName){coll.tag=Coll.tagName;return coll}if(tagName)coll.tag=tagName;return coll}function composeCollection(CN2,ctx,token,tagToken,onError){const tagName=!tagToken?null:ctx.directives.tagName(tagToken.source,msg=>onError(tagToken,\"TAG_RESOLVE_FAILED\",msg));const expType=token.type===\"block-map\"?\"map\":token.type===\"block-seq\"?\"seq\":token.start.source===\"{\"?\"map\":\"seq\";if(!tagToken||!tagName||tagName===\"!\"||tagName===YAMLMap.tagName&&expType===\"map\"||tagName===YAMLSeq.tagName&&expType===\"seq\"||!expType){return resolveCollection(CN2,ctx,token,onError,tagName)}let tag=ctx.schema.tags.find(t=>t.tag===tagName&&t.collection===expType);if(!tag){const kt=ctx.schema.knownTags[tagName];if(kt&&kt.collection===expType){ctx.schema.tags.push(Object.assign({},kt,{default:false}));tag=kt}else{if(kt?.collection){onError(tagToken,\"BAD_COLLECTION_TYPE\",`${kt.tag} used for ${expType} collection, but expects ${kt.collection}`,true)}else{onError(tagToken,\"TAG_RESOLVE_FAILED\",`Unresolved tag: ${tagName}`,true)}return resolveCollection(CN2,ctx,token,onError,tagName)}}const coll=resolveCollection(CN2,ctx,token,onError,tagName,tag);const res=tag.resolve?.(coll,msg=>onError(tagToken,\"TAG_RESOLVE_FAILED\",msg),ctx.options)??coll;const node=isNode(res)?res:new Scalar(res);node.range=coll.range;node.tag=tagName;if(tag?.format)node.format=tag.format;return node}function resolveBlockScalar(scalar,strict,onError){const start=scalar.offset;const header=parseBlockScalarHeader(scalar,strict,onError);if(!header)return{value:\"\",type:null,comment:\"\",range:[start,start,start]};const type=header.mode===\">\"?Scalar.BLOCK_FOLDED:Scalar.BLOCK_LITERAL;const lines=scalar.source?splitLines(scalar.source):[];let chompStart=lines.length;for(let i=lines.length-1;i>=0;--i){const content=lines[i][1];if(content===\"\"||content===\"\\r\")chompStart=i;else break}if(chompStart===0){const value2=header.chomp===\"+\"&&lines.length>0?\"\\n\".repeat(Math.max(1,lines.length-1)):\"\";let end2=start+header.length;if(scalar.source)end2+=scalar.source.length;return{value:value2,type,comment:header.comment,range:[start,end2,end2]}}let trimIndent=scalar.indent+header.indent;let offset=scalar.offset+header.length;let contentStart=0;for(let i=0;i<chompStart;++i){const[indent,content]=lines[i];if(content===\"\"||content===\"\\r\"){if(header.indent===0&&indent.length>trimIndent)trimIndent=indent.length}else{if(indent.length<trimIndent){const message=\"Block scalars with more-indented leading empty lines must use an explicit indentation indicator\";onError(offset+indent.length,\"MISSING_CHAR\",message)}if(header.indent===0)trimIndent=indent.length;contentStart=i;break}offset+=indent.length+content.length+1}for(let i=lines.length-1;i>=chompStart;--i){if(lines[i][0].length>trimIndent)chompStart=i+1}let value=\"\";let sep=\"\";let prevMoreIndented=false;for(let i=0;i<contentStart;++i)value+=lines[i][0].slice(trimIndent)+\"\\n\";for(let i=contentStart;i<chompStart;++i){let[indent,content]=lines[i];offset+=indent.length+content.length+1;const crlf=content[content.length-1]===\"\\r\";if(crlf)content=content.slice(0,-1);if(content&&indent.length<trimIndent){const src=header.indent?\"explicit indentation indicator\":\"first line\";const message=`Block scalar lines must not be less indented than their ${src}`;onError(offset-content.length-(crlf?2:1),\"BAD_INDENT\",message);indent=\"\"}if(type===Scalar.BLOCK_LITERAL){value+=sep+indent.slice(trimIndent)+content;sep=\"\\n\"}else if(indent.length>trimIndent||content[0]===\"\t\"){if(sep===\" \")sep=\"\\n\";else if(!prevMoreIndented&&sep===\"\\n\")sep=\"\\n\\n\";value+=sep+indent.slice(trimIndent)+content;sep=\"\\n\";prevMoreIndented=true}else if(content===\"\"){if(sep===\"\\n\")value+=\"\\n\";else sep=\"\\n\"}else{value+=sep+content;sep=\" \";prevMoreIndented=false}}switch(header.chomp){case\"-\":break;case\"+\":for(let i=chompStart;i<lines.length;++i)value+=\"\\n\"+lines[i][0].slice(trimIndent);if(value[value.length-1]!==\"\\n\")value+=\"\\n\";break;default:value+=\"\\n\"}const end=start+header.length+scalar.source.length;return{value,type,comment:header.comment,range:[start,end,end]}}function parseBlockScalarHeader({offset,props},strict,onError){if(props[0].type!==\"block-scalar-header\"){onError(props[0],\"IMPOSSIBLE\",\"Block scalar header not found\");return null}const{source}=props[0];const mode=source[0];let indent=0;let chomp=\"\";let error=-1;for(let i=1;i<source.length;++i){const ch=source[i];if(!chomp&&(ch===\"-\"||ch===\"+\"))chomp=ch;else{const n=Number(ch);if(!indent&&n)indent=n;else if(error===-1)error=offset+i}}if(error!==-1)onError(error,\"UNEXPECTED_TOKEN\",`Block scalar header includes extra characters: ${source}`);let hasSpace=false;let comment=\"\";let length=source.length;for(let i=1;i<props.length;++i){const token=props[i];switch(token.type){case\"space\":hasSpace=true;case\"newline\":length+=token.source.length;break;case\"comment\":if(strict&&!hasSpace){const message=\"Comments must be separated from other tokens by white space characters\";onError(token,\"MISSING_CHAR\",message)}length+=token.source.length;comment=token.source.substring(1);break;case\"error\":onError(token,\"UNEXPECTED_TOKEN\",token.message);length+=token.source.length;break;default:{const message=`Unexpected token in block scalar header: ${token.type}`;onError(token,\"UNEXPECTED_TOKEN\",message);const ts=token.source;if(ts&&typeof ts===\"string\")length+=ts.length}}}return{mode,indent,chomp,comment,length}}function splitLines(source){const split=source.split(/\\n( *)/);const first=split[0];const m=first.match(/^( *)/);const line0=m?.[1]?[m[1],first.slice(m[1].length)]:[\"\",first];const lines=[line0];for(let i=1;i<split.length;i+=2)lines.push([split[i],split[i+1]]);return lines}function resolveFlowScalar(scalar,strict,onError){const{offset,type,source,end}=scalar;let _type;let value;const _onError=(rel,code,msg)=>onError(offset+rel,code,msg);switch(type){case\"scalar\":_type=Scalar.PLAIN;value=plainValue(source,_onError);break;case\"single-quoted-scalar\":_type=Scalar.QUOTE_SINGLE;value=singleQuotedValue(source,_onError);break;case\"double-quoted-scalar\":_type=Scalar.QUOTE_DOUBLE;value=doubleQuotedValue(source,_onError);break;default:onError(scalar,\"UNEXPECTED_TOKEN\",`Expected a flow scalar value, but found: ${type}`);return{value:\"\",type:null,comment:\"\",range:[offset,offset+source.length,offset+source.length]}}const valueEnd=offset+source.length;const re=resolveEnd(end,valueEnd,strict,onError);return{value,type:_type,comment:re.comment,range:[offset,valueEnd,re.offset]}}function plainValue(source,onError){let badChar=\"\";switch(source[0]){case\"\t\":badChar=\"a tab character\";break;case\",\":badChar=\"flow indicator character ,\";break;case\"%\":badChar=\"directive indicator character %\";break;case\"|\":case\">\":{badChar=`block scalar indicator ${source[0]}`;break}case\"@\":case\"`\":{badChar=`reserved character ${source[0]}`;break}}if(badChar)onError(0,\"BAD_SCALAR_START\",`Plain value cannot start with ${badChar}`);return foldLines(source)}function singleQuotedValue(source,onError){if(source[source.length-1]!==\"'\"||source.length===1)onError(source.length,\"MISSING_CHAR\",\"Missing closing 'quote\");return foldLines(source.slice(1,-1)).replace(/''/g,\"'\")}function foldLines(source){let first,line;try{first=new RegExp(\"(.*?)(?<![ \t])[ \t]*\\r?\\n\",\"sy\");line=new RegExp(\"[ \t]*(.*?)(?:(?<![ \t])[ \t]*)?\\r?\\n\",\"sy\")}catch(_){first=/(.*?)[ \\t]*\\r?\\n/sy;line=/[ \\t]*(.*?)[ \\t]*\\r?\\n/sy}let match=first.exec(source);if(!match)return source;let res=match[1];let sep=\" \";let pos=first.lastIndex;line.lastIndex=pos;while(match=line.exec(source)){if(match[1]===\"\"){if(sep===\"\\n\")res+=sep;else sep=\"\\n\"}else{res+=sep+match[1];sep=\" \"}pos=line.lastIndex}const last=/[ \\t]*(.*)/sy;last.lastIndex=pos;match=last.exec(source);return res+sep+(match?.[1]??\"\")}function doubleQuotedValue(source,onError){let res=\"\";for(let i=1;i<source.length-1;++i){const ch=source[i];if(ch===\"\\r\"&&source[i+1]===\"\\n\")continue;if(ch===\"\\n\"){const{fold,offset}=foldNewline(source,i);res+=fold;i=offset}else if(ch===\"\\\\\"){let next=source[++i];const cc=escapeCodes[next];if(cc)res+=cc;else if(next===\"\\n\"){next=source[i+1];while(next===\" \"||next===\"\t\")next=source[++i+1]}else if(next===\"\\r\"&&source[i+1]===\"\\n\"){next=source[++i+1];while(next===\" \"||next===\"\t\")next=source[++i+1]}else if(next===\"x\"||next===\"u\"||next===\"U\"){const length={x:2,u:4,U:8}[next];res+=parseCharCode(source,i+1,length,onError);i+=length}else{const raw=source.substr(i-1,2);onError(i-1,\"BAD_DQ_ESCAPE\",`Invalid escape sequence ${raw}`);res+=raw}}else if(ch===\" \"||ch===\"\t\"){const wsStart=i;let next=source[i+1];while(next===\" \"||next===\"\t\")next=source[++i+1];if(next!==\"\\n\"&&!(next===\"\\r\"&&source[i+2]===\"\\n\"))res+=i>wsStart?source.slice(wsStart,i+1):ch}else{res+=ch}}if(source[source.length-1]!=='\"'||source.length===1)onError(source.length,\"MISSING_CHAR\",'Missing closing \"quote');return res}function foldNewline(source,offset){let fold=\"\";let ch=source[offset+1];while(ch===\" \"||ch===\"\t\"||ch===\"\\n\"||ch===\"\\r\"){if(ch===\"\\r\"&&source[offset+2]!==\"\\n\")break;if(ch===\"\\n\")fold+=\"\\n\";offset+=1;ch=source[offset+1]}if(!fold)fold=\" \";return{fold,offset}}var escapeCodes={\"0\":\"\\0\",a:\"\\x07\",b:\"\\b\",e:\"\\x1B\",f:\"\\f\",n:\"\\n\",r:\"\\r\",t:\"\t\",v:\"\\v\",N:\"\\x85\",_:\"\\xA0\",L:\"\\u2028\",P:\"\\u2029\",\" \":\" \",'\"':'\"',\"/\":\"/\",\"\\\\\":\"\\\\\",\"\t\":\"\t\"};function parseCharCode(source,offset,length,onError){const cc=source.substr(offset,length);const ok=cc.length===length&&/^[0-9a-fA-F]+$/.test(cc);const code=ok?parseInt(cc,16):NaN;if(isNaN(code)){const raw=source.substr(offset-2,length+2);onError(offset-2,\"BAD_DQ_ESCAPE\",`Invalid escape sequence ${raw}`);return raw}return String.fromCodePoint(code)}function composeScalar(ctx,token,tagToken,onError){const{value,type,comment,range}=token.type===\"block-scalar\"?resolveBlockScalar(token,ctx.options.strict,onError):resolveFlowScalar(token,ctx.options.strict,onError);const tagName=tagToken?ctx.directives.tagName(tagToken.source,msg=>onError(tagToken,\"TAG_RESOLVE_FAILED\",msg)):null;const tag=tagToken&&tagName?findScalarTagByName(ctx.schema,value,tagName,tagToken,onError):token.type===\"scalar\"?findScalarTagByTest(ctx,value,token,onError):ctx.schema[SCALAR];let scalar;try{const res=tag.resolve(value,msg=>onError(tagToken??token,\"TAG_RESOLVE_FAILED\",msg),ctx.options);scalar=isScalar(res)?res:new Scalar(res)}catch(error){const msg=error instanceof Error?error.message:String(error);onError(tagToken??token,\"TAG_RESOLVE_FAILED\",msg);scalar=new Scalar(value)}scalar.range=range;scalar.source=value;if(type)scalar.type=type;if(tagName)scalar.tag=tagName;if(tag.format)scalar.format=tag.format;if(comment)scalar.comment=comment;return scalar}function findScalarTagByName(schema4,value,tagName,tagToken,onError){if(tagName===\"!\")return schema4[SCALAR];const matchWithTest=[];for(const tag of schema4.tags){if(!tag.collection&&tag.tag===tagName){if(tag.default&&tag.test)matchWithTest.push(tag);else return tag}}for(const tag of matchWithTest)if(tag.test?.test(value))return tag;const kt=schema4.knownTags[tagName];if(kt&&!kt.collection){schema4.tags.push(Object.assign({},kt,{default:false,test:void 0}));return kt}onError(tagToken,\"TAG_RESOLVE_FAILED\",`Unresolved tag: ${tagName}`,tagName!==\"tag:yaml.org,2002:str\");return schema4[SCALAR]}function findScalarTagByTest({directives,schema:schema4},value,token,onError){const tag=schema4.tags.find(tag2=>tag2.default&&tag2.test?.test(value))||schema4[SCALAR];if(schema4.compat){const compat=schema4.compat.find(tag2=>tag2.default&&tag2.test?.test(value))??schema4[SCALAR];if(tag.tag!==compat.tag){const ts=directives.tagString(tag.tag);const cs=directives.tagString(compat.tag);const msg=`Value may be parsed as either ${ts} or ${cs}`;onError(token,\"TAG_RESOLVE_FAILED\",msg,true)}}return tag}function emptyScalarPosition(offset,before,pos){if(before){if(pos===null)pos=before.length;for(let i=pos-1;i>=0;--i){let st=before[i];switch(st.type){case\"space\":case\"comment\":case\"newline\":offset-=st.source.length;continue}st=before[++i];while(st?.type===\"space\"){offset+=st.source.length;st=before[++i]}break}}return offset}var CN={composeNode,composeEmptyNode};function composeNode(ctx,token,props,onError){const{spaceBefore,comment,anchor,tag}=props;let node;let isSrcToken=true;switch(token.type){case\"alias\":node=composeAlias(ctx,token,onError);if(anchor||tag)onError(token,\"ALIAS_PROPS\",\"An alias node must not specify any properties\");break;case\"scalar\":case\"single-quoted-scalar\":case\"double-quoted-scalar\":case\"block-scalar\":node=composeScalar(ctx,token,tag,onError);if(anchor)node.anchor=anchor.source.substring(1);break;case\"block-map\":case\"block-seq\":case\"flow-collection\":node=composeCollection(CN,ctx,token,tag,onError);if(anchor)node.anchor=anchor.source.substring(1);break;default:{const message=token.type===\"error\"?token.message:`Unsupported token (type: ${token.type})`;onError(token,\"UNEXPECTED_TOKEN\",message);node=composeEmptyNode(ctx,token.offset,void 0,null,props,onError);isSrcToken=false}}if(anchor&&node.anchor===\"\")onError(anchor,\"BAD_ALIAS\",\"Anchor cannot be an empty string\");if(spaceBefore)node.spaceBefore=true;if(comment){if(token.type===\"scalar\"&&token.source===\"\")node.comment=comment;else node.commentBefore=comment}if(ctx.options.keepSourceTokens&&isSrcToken)node.srcToken=token;return node}function composeEmptyNode(ctx,offset,before,pos,{spaceBefore,comment,anchor,tag,end},onError){const token={type:\"scalar\",offset:emptyScalarPosition(offset,before,pos),indent:-1,source:\"\"};const node=composeScalar(ctx,token,tag,onError);if(anchor){node.anchor=anchor.source.substring(1);if(node.anchor===\"\")onError(anchor,\"BAD_ALIAS\",\"Anchor cannot be an empty string\")}if(spaceBefore)node.spaceBefore=true;if(comment){node.comment=comment;node.range[2]=end}return node}function composeAlias({options},{offset,source,end},onError){const alias=new Alias(source.substring(1));if(alias.source===\"\")onError(offset,\"BAD_ALIAS\",\"Alias cannot be an empty string\");if(alias.source.endsWith(\":\"))onError(offset+source.length-1,\"BAD_ALIAS\",\"Alias ending in : is ambiguous\",true);const valueEnd=offset+source.length;const re=resolveEnd(end,valueEnd,options.strict,onError);alias.range=[offset,valueEnd,re.offset];if(re.comment)alias.comment=re.comment;return alias}function composeDoc(options,directives,{offset,start,value,end},onError){const opts=Object.assign({_directives:directives},options);const doc=new Document(void 0,opts);const ctx={atRoot:true,directives:doc.directives,options:doc.options,schema:doc.schema};const props=resolveProps(start,{indicator:\"doc-start\",next:value??end?.[0],offset,onError,startOnNewline:true});if(props.found){doc.directives.docStart=true;if(value&&(value.type===\"block-map\"||value.type===\"block-seq\")&&!props.hasNewline)onError(props.end,\"MISSING_CHAR\",\"Block collection cannot start on same line with directives-end marker\")}doc.contents=value?composeNode(ctx,value,props,onError):composeEmptyNode(ctx,props.end,start,null,props,onError);const contentEnd=doc.contents.range[2];const re=resolveEnd(end,contentEnd,false,onError);if(re.comment)doc.comment=re.comment;doc.range=[offset,contentEnd,re.offset];return doc}function getErrorPos(src){if(typeof src===\"number\")return[src,src+1];if(Array.isArray(src))return src.length===2?src:[src[0],src[1]];const{offset,source}=src;return[offset,offset+(typeof source===\"string\"?source.length:1)]}function parsePrelude(prelude){let comment=\"\";let atComment=false;let afterEmptyLine=false;for(let i=0;i<prelude.length;++i){const source=prelude[i];switch(source[0]){case\"#\":comment+=(comment===\"\"?\"\":afterEmptyLine?\"\\n\\n\":\"\\n\")+(source.substring(1)||\" \");atComment=true;afterEmptyLine=false;break;case\"%\":if(prelude[i+1]?.[0]!==\"#\")i+=1;atComment=false;break;default:if(!atComment)afterEmptyLine=true;atComment=false}}return{comment,afterEmptyLine}}var Composer=class{constructor(options={}){this.doc=null;this.atDirectives=false;this.prelude=[];this.errors=[];this.warnings=[];this.onError=(source,code,message,warning)=>{const pos=getErrorPos(source);if(warning)this.warnings.push(new YAMLWarning(pos,code,message));else this.errors.push(new YAMLParseError(pos,code,message))};this.directives=new Directives({version:options.version||\"1.2\"});this.options=options}decorate(doc,afterDoc){const{comment,afterEmptyLine}=parsePrelude(this.prelude);if(comment){const dc=doc.contents;if(afterDoc){doc.comment=doc.comment?`${doc.comment}\n${comment}`:comment}else if(afterEmptyLine||doc.directives.docStart||!dc){doc.commentBefore=comment}else if(isCollection(dc)&&!dc.flow&&dc.items.length>0){let it=dc.items[0];if(isPair(it))it=it.key;const cb=it.commentBefore;it.commentBefore=cb?`${comment}\n${cb}`:comment}else{const cb=dc.commentBefore;dc.commentBefore=cb?`${comment}\n${cb}`:comment}}if(afterDoc){Array.prototype.push.apply(doc.errors,this.errors);Array.prototype.push.apply(doc.warnings,this.warnings)}else{doc.errors=this.errors;doc.warnings=this.warnings}this.prelude=[];this.errors=[];this.warnings=[]}streamInfo(){return{comment:parsePrelude(this.prelude).comment,directives:this.directives,errors:this.errors,warnings:this.warnings}}*compose(tokens,forceDoc=false,endOffset=-1){for(const token of tokens)yield*this.next(token);yield*this.end(forceDoc,endOffset)}*next(token){switch(token.type){case\"directive\":this.directives.add(token.source,(offset,message,warning)=>{const pos=getErrorPos(token);pos[0]+=offset;this.onError(pos,\"BAD_DIRECTIVE\",message,warning)});this.prelude.push(token.source);this.atDirectives=true;break;case\"document\":{const doc=composeDoc(this.options,this.directives,token,this.onError);if(this.atDirectives&&!doc.directives.docStart)this.onError(token,\"MISSING_CHAR\",\"Missing directives-end/doc-start indicator line\");this.decorate(doc,false);if(this.doc)yield this.doc;this.doc=doc;this.atDirectives=false;break}case\"byte-order-mark\":case\"space\":break;case\"comment\":case\"newline\":this.prelude.push(token.source);break;case\"error\":{const msg=token.source?`${token.message}: ${JSON.stringify(token.source)}`:token.message;const error=new YAMLParseError(getErrorPos(token),\"UNEXPECTED_TOKEN\",msg);if(this.atDirectives||!this.doc)this.errors.push(error);else this.doc.errors.push(error);break}case\"doc-end\":{if(!this.doc){const msg=\"Unexpected doc-end without preceding document\";this.errors.push(new YAMLParseError(getErrorPos(token),\"UNEXPECTED_TOKEN\",msg));break}this.doc.directives.docEnd=true;const end=resolveEnd(token.end,token.offset+token.source.length,this.doc.options.strict,this.onError);this.decorate(this.doc,true);if(end.comment){const dc=this.doc.comment;this.doc.comment=dc?`${dc}\n${end.comment}`:end.comment}this.doc.range[2]=end.offset;break}default:this.errors.push(new YAMLParseError(getErrorPos(token),\"UNEXPECTED_TOKEN\",`Unsupported token ${token.type}`))}}*end(forceDoc=false,endOffset=-1){if(this.doc){this.decorate(this.doc,true);yield this.doc;this.doc=null}else if(forceDoc){const opts=Object.assign({_directives:this.directives},this.options);const doc=new Document(void 0,opts);if(this.atDirectives)this.onError(endOffset,\"MISSING_CHAR\",\"Missing directives-end indicator line\");doc.range=[0,endOffset,endOffset];this.decorate(doc,false);yield doc}}};var BREAK2=Symbol(\"break visit\");var SKIP2=Symbol(\"skip children\");var REMOVE2=Symbol(\"remove item\");function visit2(cst,visitor){if(\"type\"in cst&&cst.type===\"document\")cst={start:cst.start,value:cst.value};_visit(Object.freeze([]),cst,visitor)}visit2.BREAK=BREAK2;visit2.SKIP=SKIP2;visit2.REMOVE=REMOVE2;visit2.itemAtPath=(cst,path)=>{let item=cst;for(const[field,index]of path){const tok=item?.[field];if(tok&&\"items\"in tok){item=tok.items[index]}else return void 0}return item};visit2.parentCollection=(cst,path)=>{const parent=visit2.itemAtPath(cst,path.slice(0,-1));const field=path[path.length-1][0];const coll=parent?.[field];if(coll&&\"items\"in coll)return coll;throw new Error(\"Parent collection not found\")};function _visit(path,item,visitor){let ctrl=visitor(item,path);if(typeof ctrl===\"symbol\")return ctrl;for(const field of[\"key\",\"value\"]){const token=item[field];if(token&&\"items\"in token){for(let i=0;i<token.items.length;++i){const ci=_visit(Object.freeze(path.concat([[field,i]])),token.items[i],visitor);if(typeof ci===\"number\")i=ci-1;else if(ci===BREAK2)return BREAK2;else if(ci===REMOVE2){token.items.splice(i,1);i-=1}}if(typeof ctrl===\"function\"&&field===\"key\")ctrl=ctrl(item,path)}}return typeof ctrl===\"function\"?ctrl(item,path):ctrl}var BOM=\"\\uFEFF\";var DOCUMENT=\"\u0002\";var FLOW_END=\"\u0018\";var SCALAR2=\"\u001f\";function tokenType(source){switch(source){case BOM:return\"byte-order-mark\";case DOCUMENT:return\"doc-mode\";case FLOW_END:return\"flow-error-end\";case SCALAR2:return\"scalar\";case\"---\":return\"doc-start\";case\"...\":return\"doc-end\";case\"\":case\"\\n\":case\"\\r\\n\":return\"newline\";case\"-\":return\"seq-item-ind\";case\"?\":return\"explicit-key-ind\";case\":\":return\"map-value-ind\";case\"{\":return\"flow-map-start\";case\"}\":return\"flow-map-end\";case\"[\":return\"flow-seq-start\";case\"]\":return\"flow-seq-end\";case\",\":return\"comma\"}switch(source[0]){case\" \":case\"\t\":return\"space\";case\"#\":return\"comment\";case\"%\":return\"directive-line\";case\"*\":return\"alias\";case\"&\":return\"anchor\";case\"!\":return\"tag\";case\"'\":return\"single-quoted-scalar\";case'\"':return\"double-quoted-scalar\";case\"|\":case\">\":return\"block-scalar-header\"}return null}function isEmpty(ch){switch(ch){case void 0:case\" \":case\"\\n\":case\"\\r\":case\"\t\":return true;default:return false}}var hexDigits=\"0123456789ABCDEFabcdef\".split(\"\");var tagChars=\"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-#;/?:@&=+$_.!~*'()\".split(\"\");var invalidFlowScalarChars=\",[]{}\".split(\"\");var invalidAnchorChars=\" ,[]{}\\n\\r\t\".split(\"\");var isNotAnchorChar=ch=>!ch||invalidAnchorChars.includes(ch);var Lexer=class{constructor(){this.atEnd=false;this.blockScalarIndent=-1;this.blockScalarKeep=false;this.buffer=\"\";this.flowKey=false;this.flowLevel=0;this.indentNext=0;this.indentValue=0;this.lineEndPos=null;this.next=null;this.pos=0}*lex(source,incomplete=false){if(source){this.buffer=this.buffer?this.buffer+source:source;this.lineEndPos=null}this.atEnd=!incomplete;let next=this.next??\"stream\";while(next&&(incomplete||this.hasChars(1)))next=yield*this.parseNext(next)}atLineEnd(){let i=this.pos;let ch=this.buffer[i];while(ch===\" \"||ch===\"\t\")ch=this.buffer[++i];if(!ch||ch===\"#\"||ch===\"\\n\")return true;if(ch===\"\\r\")return this.buffer[i+1]===\"\\n\";return false}charAt(n){return this.buffer[this.pos+n]}continueScalar(offset){let ch=this.buffer[offset];if(this.indentNext>0){let indent=0;while(ch===\" \")ch=this.buffer[++indent+offset];if(ch===\"\\r\"){const next=this.buffer[indent+offset+1];if(next===\"\\n\"||!next&&!this.atEnd)return offset+indent+1}return ch===\"\\n\"||indent>=this.indentNext||!ch&&!this.atEnd?offset+indent:-1}if(ch===\"-\"||ch===\".\"){const dt=this.buffer.substr(offset,3);if((dt===\"---\"||dt===\"...\")&&isEmpty(this.buffer[offset+3]))return-1}return offset}getLine(){let end=this.lineEndPos;if(typeof end!==\"number\"||end!==-1&&end<this.pos){end=this.buffer.indexOf(\"\\n\",this.pos);this.lineEndPos=end}if(end===-1)return this.atEnd?this.buffer.substring(this.pos):null;if(this.buffer[end-1]===\"\\r\")end-=1;return this.buffer.substring(this.pos,end)}hasChars(n){return this.pos+n<=this.buffer.length}setNext(state){this.buffer=this.buffer.substring(this.pos);this.pos=0;this.lineEndPos=null;this.next=state;return null}peek(n){return this.buffer.substr(this.pos,n)}*parseNext(next){switch(next){case\"stream\":return yield*this.parseStream();case\"line-start\":return yield*this.parseLineStart();case\"block-start\":return yield*this.parseBlockStart();case\"doc\":return yield*this.parseDocument();case\"flow\":return yield*this.parseFlowCollection();case\"quoted-scalar\":return yield*this.parseQuotedScalar();case\"block-scalar\":return yield*this.parseBlockScalar();case\"plain-scalar\":return yield*this.parsePlainScalar()}}*parseStream(){let line=this.getLine();if(line===null)return this.setNext(\"stream\");if(line[0]===BOM){yield*this.pushCount(1);line=line.substring(1)}if(line[0]===\"%\"){let dirEnd=line.length;const cs=line.indexOf(\"#\");if(cs!==-1){const ch=line[cs-1];if(ch===\" \"||ch===\"\t\")dirEnd=cs-1}while(true){const ch=line[dirEnd-1];if(ch===\" \"||ch===\"\t\")dirEnd-=1;else break}const n=(yield*this.pushCount(dirEnd))+(yield*this.pushSpaces(true));yield*this.pushCount(line.length-n);this.pushNewline();return\"stream\"}if(this.atLineEnd()){const sp=yield*this.pushSpaces(true);yield*this.pushCount(line.length-sp);yield*this.pushNewline();return\"stream\"}yield DOCUMENT;return yield*this.parseLineStart()}*parseLineStart(){const ch=this.charAt(0);if(!ch&&!this.atEnd)return this.setNext(\"line-start\");if(ch===\"-\"||ch===\".\"){if(!this.atEnd&&!this.hasChars(4))return this.setNext(\"line-start\");const s=this.peek(3);if(s===\"---\"&&isEmpty(this.charAt(3))){yield*this.pushCount(3);this.indentValue=0;this.indentNext=0;return\"doc\"}else if(s===\"...\"&&isEmpty(this.charAt(3))){yield*this.pushCount(3);return\"stream\"}}this.indentValue=yield*this.pushSpaces(false);if(this.indentNext>this.indentValue&&!isEmpty(this.charAt(1)))this.indentNext=this.indentValue;return yield*this.parseBlockStart()}*parseBlockStart(){const[ch0,ch1]=this.peek(2);if(!ch1&&!this.atEnd)return this.setNext(\"block-start\");if((ch0===\"-\"||ch0===\"?\"||ch0===\":\")&&isEmpty(ch1)){const n=(yield*this.pushCount(1))+(yield*this.pushSpaces(true));this.indentNext=this.indentValue+1;this.indentValue+=n;return yield*this.parseBlockStart()}return\"doc\"}*parseDocument(){yield*this.pushSpaces(true);const line=this.getLine();if(line===null)return this.setNext(\"doc\");let n=yield*this.pushIndicators();switch(line[n]){case\"#\":yield*this.pushCount(line.length-n);case void 0:yield*this.pushNewline();return yield*this.parseLineStart();case\"{\":case\"[\":yield*this.pushCount(1);this.flowKey=false;this.flowLevel=1;return\"flow\";case\"}\":case\"]\":yield*this.pushCount(1);return\"doc\";case\"*\":yield*this.pushUntil(isNotAnchorChar);return\"doc\";case'\"':case\"'\":return yield*this.parseQuotedScalar();case\"|\":case\">\":n+=yield*this.parseBlockScalarHeader();n+=yield*this.pushSpaces(true);yield*this.pushCount(line.length-n);yield*this.pushNewline();return yield*this.parseBlockScalar();default:return yield*this.parsePlainScalar()}}*parseFlowCollection(){let nl,sp;let indent=-1;do{nl=yield*this.pushNewline();if(nl>0){sp=yield*this.pushSpaces(false);this.indentValue=indent=sp}else{sp=0}sp+=yield*this.pushSpaces(true)}while(nl+sp>0);const line=this.getLine();if(line===null)return this.setNext(\"flow\");if(indent!==-1&&indent<this.indentNext&&line[0]!==\"#\"||indent===0&&(line.startsWith(\"---\")||line.startsWith(\"...\"))&&isEmpty(line[3])){const atFlowEndMarker=indent===this.indentNext-1&&this.flowLevel===1&&(line[0]===\"]\"||line[0]===\"}\");if(!atFlowEndMarker){this.flowLevel=0;yield FLOW_END;return yield*this.parseLineStart()}}let n=0;while(line[n]===\",\"){n+=yield*this.pushCount(1);n+=yield*this.pushSpaces(true);this.flowKey=false}n+=yield*this.pushIndicators();switch(line[n]){case void 0:return\"flow\";case\"#\":yield*this.pushCount(line.length-n);return\"flow\";case\"{\":case\"[\":yield*this.pushCount(1);this.flowKey=false;this.flowLevel+=1;return\"flow\";case\"}\":case\"]\":yield*this.pushCount(1);this.flowKey=true;this.flowLevel-=1;return this.flowLevel?\"flow\":\"doc\";case\"*\":yield*this.pushUntil(isNotAnchorChar);return\"flow\";case'\"':case\"'\":this.flowKey=true;return yield*this.parseQuotedScalar();case\":\":{const next=this.charAt(1);if(this.flowKey||isEmpty(next)||next===\",\"){this.flowKey=false;yield*this.pushCount(1);yield*this.pushSpaces(true);return\"flow\"}}default:this.flowKey=false;return yield*this.parsePlainScalar()}}*parseQuotedScalar(){const quote=this.charAt(0);let end=this.buffer.indexOf(quote,this.pos+1);if(quote===\"'\"){while(end!==-1&&this.buffer[end+1]===\"'\")end=this.buffer.indexOf(\"'\",end+2)}else{while(end!==-1){let n=0;while(this.buffer[end-1-n]===\"\\\\\")n+=1;if(n%2===0)break;end=this.buffer.indexOf('\"',end+1)}}const qb=this.buffer.substring(0,end);let nl=qb.indexOf(\"\\n\",this.pos);if(nl!==-1){while(nl!==-1){const cs=this.continueScalar(nl+1);if(cs===-1)break;nl=qb.indexOf(\"\\n\",cs)}if(nl!==-1){end=nl-(qb[nl-1]===\"\\r\"?2:1)}}if(end===-1){if(!this.atEnd)return this.setNext(\"quoted-scalar\");end=this.buffer.length}yield*this.pushToIndex(end+1,false);return this.flowLevel?\"flow\":\"doc\"}*parseBlockScalarHeader(){this.blockScalarIndent=-1;this.blockScalarKeep=false;let i=this.pos;while(true){const ch=this.buffer[++i];if(ch===\"+\")this.blockScalarKeep=true;else if(ch>\"0\"&&ch<=\"9\")this.blockScalarIndent=Number(ch)-1;else if(ch!==\"-\")break}return yield*this.pushUntil(ch=>isEmpty(ch)||ch===\"#\")}*parseBlockScalar(){let nl=this.pos-1;let indent=0;let ch;loop:for(let i=this.pos;ch=this.buffer[i];++i){switch(ch){case\" \":indent+=1;break;case\"\\n\":nl=i;indent=0;break;case\"\\r\":{const next=this.buffer[i+1];if(!next&&!this.atEnd)return this.setNext(\"block-scalar\");if(next===\"\\n\")break}default:break loop}}if(!ch&&!this.atEnd)return this.setNext(\"block-scalar\");if(indent>=this.indentNext){if(this.blockScalarIndent===-1)this.indentNext=indent;else this.indentNext+=this.blockScalarIndent;do{const cs=this.continueScalar(nl+1);if(cs===-1)break;nl=this.buffer.indexOf(\"\\n\",cs)}while(nl!==-1);if(nl===-1){if(!this.atEnd)return this.setNext(\"block-scalar\");nl=this.buffer.length}}if(!this.blockScalarKeep){do{let i=nl-1;let ch2=this.buffer[i];if(ch2===\"\\r\")ch2=this.buffer[--i];const lastChar=i;while(ch2===\" \"||ch2===\"\t\")ch2=this.buffer[--i];if(ch2===\"\\n\"&&i>=this.pos&&i+1+indent>lastChar)nl=i;else break}while(true)}yield SCALAR2;yield*this.pushToIndex(nl+1,true);return yield*this.parseLineStart()}*parsePlainScalar(){const inFlow=this.flowLevel>0;let end=this.pos-1;let i=this.pos-1;let ch;while(ch=this.buffer[++i]){if(ch===\":\"){const next=this.buffer[i+1];if(isEmpty(next)||inFlow&&next===\",\")break;end=i}else if(isEmpty(ch)){let next=this.buffer[i+1];if(ch===\"\\r\"){if(next===\"\\n\"){i+=1;ch=\"\\n\";next=this.buffer[i+1]}else end=i}if(next===\"#\"||inFlow&&invalidFlowScalarChars.includes(next))break;if(ch===\"\\n\"){const cs=this.continueScalar(i+1);if(cs===-1)break;i=Math.max(i,cs-2)}}else{if(inFlow&&invalidFlowScalarChars.includes(ch))break;end=i}}if(!ch&&!this.atEnd)return this.setNext(\"plain-scalar\");yield SCALAR2;yield*this.pushToIndex(end+1,true);return inFlow?\"flow\":\"doc\"}*pushCount(n){if(n>0){yield this.buffer.substr(this.pos,n);this.pos+=n;return n}return 0}*pushToIndex(i,allowEmpty){const s=this.buffer.slice(this.pos,i);if(s){yield s;this.pos+=s.length;return s.length}else if(allowEmpty)yield\"\";return 0}*pushIndicators(){switch(this.charAt(0)){case\"!\":return(yield*this.pushTag())+(yield*this.pushSpaces(true))+(yield*this.pushIndicators());case\"&\":return(yield*this.pushUntil(isNotAnchorChar))+(yield*this.pushSpaces(true))+(yield*this.pushIndicators());case\"-\":case\"?\":case\":\":{const inFlow=this.flowLevel>0;const ch1=this.charAt(1);if(isEmpty(ch1)||inFlow&&invalidFlowScalarChars.includes(ch1)){if(!inFlow)this.indentNext=this.indentValue+1;else if(this.flowKey)this.flowKey=false;return(yield*this.pushCount(1))+(yield*this.pushSpaces(true))+(yield*this.pushIndicators())}}}return 0}*pushTag(){if(this.charAt(1)===\"<\"){let i=this.pos+2;let ch=this.buffer[i];while(!isEmpty(ch)&&ch!==\">\")ch=this.buffer[++i];return yield*this.pushToIndex(ch===\">\"?i+1:i,false)}else{let i=this.pos+1;let ch=this.buffer[i];while(ch){if(tagChars.includes(ch))ch=this.buffer[++i];else if(ch===\"%\"&&hexDigits.includes(this.buffer[i+1])&&hexDigits.includes(this.buffer[i+2])){ch=this.buffer[i+=3]}else break}return yield*this.pushToIndex(i,false)}}*pushNewline(){const ch=this.buffer[this.pos];if(ch===\"\\n\")return yield*this.pushCount(1);else if(ch===\"\\r\"&&this.charAt(1)===\"\\n\")return yield*this.pushCount(2);else return 0}*pushSpaces(allowTabs){let i=this.pos-1;let ch;do{ch=this.buffer[++i]}while(ch===\" \"||allowTabs&&ch===\"\t\");const n=i-this.pos;if(n>0){yield this.buffer.substr(this.pos,n);this.pos=i}return n}*pushUntil(test){let i=this.pos;let ch=this.buffer[i];while(!test(ch))ch=this.buffer[++i];return yield*this.pushToIndex(i,false)}};var LineCounter=class{constructor(){this.lineStarts=[];this.addNewLine=offset=>this.lineStarts.push(offset);this.linePos=offset=>{let low=0;let high=this.lineStarts.length;while(low<high){const mid=low+high>>1;if(this.lineStarts[mid]<offset)low=mid+1;else high=mid}if(this.lineStarts[low]===offset)return{line:low+1,col:1};if(low===0)return{line:0,col:offset};const start=this.lineStarts[low-1];return{line:low,col:offset-start+1}}}};function includesToken(list,type){for(let i=0;i<list.length;++i)if(list[i].type===type)return true;return false}function findNonEmptyIndex(list){for(let i=0;i<list.length;++i){switch(list[i].type){case\"space\":case\"comment\":case\"newline\":break;default:return i}}return-1}function isFlowToken(token){switch(token?.type){case\"alias\":case\"scalar\":case\"single-quoted-scalar\":case\"double-quoted-scalar\":case\"flow-collection\":return true;default:return false}}function getPrevProps(parent){switch(parent.type){case\"document\":return parent.start;case\"block-map\":{const it=parent.items[parent.items.length-1];return it.sep??it.start}case\"block-seq\":return parent.items[parent.items.length-1].start;default:return[]}}function getFirstKeyStartProps(prev){if(prev.length===0)return[];let i=prev.length;loop:while(--i>=0){switch(prev[i].type){case\"doc-start\":case\"explicit-key-ind\":case\"map-value-ind\":case\"seq-item-ind\":case\"newline\":break loop}}while(prev[++i]?.type===\"space\"){}return prev.splice(i,prev.length)}function fixFlowSeqItems(fc){if(fc.start.type===\"flow-seq-start\"){for(const it of fc.items){if(it.sep&&!it.value&&!includesToken(it.start,\"explicit-key-ind\")&&!includesToken(it.sep,\"map-value-ind\")){if(it.key)it.value=it.key;delete it.key;if(isFlowToken(it.value)){if(it.value.end)Array.prototype.push.apply(it.value.end,it.sep);else it.value.end=it.sep}else Array.prototype.push.apply(it.start,it.sep);delete it.sep}}}}var Parser=class{constructor(onNewLine){this.atNewLine=true;this.atScalar=false;this.indent=0;this.offset=0;this.onKeyLine=false;this.stack=[];this.source=\"\";this.type=\"\";this.lexer=new Lexer;this.onNewLine=onNewLine}*parse(source,incomplete=false){if(this.onNewLine&&this.offset===0)this.onNewLine(0);for(const lexeme of this.lexer.lex(source,incomplete))yield*this.next(lexeme);if(!incomplete)yield*this.end()}*next(source){this.source=source;if(this.atScalar){this.atScalar=false;yield*this.step();this.offset+=source.length;return}const type=tokenType(source);if(!type){const message=`Not a YAML token: ${source}`;yield*this.pop({type:\"error\",offset:this.offset,message,source});this.offset+=source.length}else if(type===\"scalar\"){this.atNewLine=false;this.atScalar=true;this.type=\"scalar\"}else{this.type=type;yield*this.step();switch(type){case\"newline\":this.atNewLine=true;this.indent=0;if(this.onNewLine)this.onNewLine(this.offset+source.length);break;case\"space\":if(this.atNewLine&&source[0]===\" \")this.indent+=source.length;break;case\"explicit-key-ind\":case\"map-value-ind\":case\"seq-item-ind\":if(this.atNewLine)this.indent+=source.length;break;case\"doc-mode\":case\"flow-error-end\":return;default:this.atNewLine=false}this.offset+=source.length}}*end(){while(this.stack.length>0)yield*this.pop()}get sourceToken(){const st={type:this.type,offset:this.offset,indent:this.indent,source:this.source};return st}*step(){const top=this.peek(1);if(this.type===\"doc-end\"&&(!top||top.type!==\"doc-end\")){while(this.stack.length>0)yield*this.pop();this.stack.push({type:\"doc-end\",offset:this.offset,source:this.source});return}if(!top)return yield*this.stream();switch(top.type){case\"document\":return yield*this.document(top);case\"alias\":case\"scalar\":case\"single-quoted-scalar\":case\"double-quoted-scalar\":return yield*this.scalar(top);case\"block-scalar\":return yield*this.blockScalar(top);case\"block-map\":return yield*this.blockMap(top);case\"block-seq\":return yield*this.blockSequence(top);case\"flow-collection\":return yield*this.flowCollection(top);case\"doc-end\":return yield*this.documentEnd(top)}yield*this.pop()}peek(n){return this.stack[this.stack.length-n]}*pop(error){const token=error??this.stack.pop();if(!token){const message=\"Tried to pop an empty stack\";yield{type:\"error\",offset:this.offset,source:\"\",message}}else if(this.stack.length===0){yield token}else{const top=this.peek(1);if(token.type===\"block-scalar\"){token.indent=\"indent\"in top?top.indent:0}else if(token.type===\"flow-collection\"&&top.type===\"document\"){token.indent=0}if(token.type===\"flow-collection\")fixFlowSeqItems(token);switch(top.type){case\"document\":top.value=token;break;case\"block-scalar\":top.props.push(token);break;case\"block-map\":{const it=top.items[top.items.length-1];if(it.value){top.items.push({start:[],key:token,sep:[]});this.onKeyLine=true;return}else if(it.sep){it.value=token}else{Object.assign(it,{key:token,sep:[]});this.onKeyLine=!includesToken(it.start,\"explicit-key-ind\");return}break}case\"block-seq\":{const it=top.items[top.items.length-1];if(it.value)top.items.push({start:[],value:token});else it.value=token;break}case\"flow-collection\":{const it=top.items[top.items.length-1];if(!it||it.value)top.items.push({start:[],key:token,sep:[]});else if(it.sep)it.value=token;else Object.assign(it,{key:token,sep:[]});return}default:yield*this.pop();yield*this.pop(token)}if((top.type===\"document\"||top.type===\"block-map\"||top.type===\"block-seq\")&&(token.type===\"block-map\"||token.type===\"block-seq\")){const last=token.items[token.items.length-1];if(last&&!last.sep&&!last.value&&last.start.length>0&&findNonEmptyIndex(last.start)===-1&&(token.indent===0||last.start.every(st=>st.type!==\"comment\"||st.indent<token.indent))){if(top.type===\"document\")top.end=last.start;else top.items.push({start:last.start});token.items.splice(-1,1)}}}}*stream(){switch(this.type){case\"directive-line\":yield{type:\"directive\",offset:this.offset,source:this.source};return;case\"byte-order-mark\":case\"space\":case\"comment\":case\"newline\":yield this.sourceToken;return;case\"doc-mode\":case\"doc-start\":{const doc={type:\"document\",offset:this.offset,start:[]};if(this.type===\"doc-start\")doc.start.push(this.sourceToken);this.stack.push(doc);return}}yield{type:\"error\",offset:this.offset,message:`Unexpected ${this.type} token in YAML stream`,source:this.source}}*document(doc){if(doc.value)return yield*this.lineEnd(doc);switch(this.type){case\"doc-start\":{if(findNonEmptyIndex(doc.start)!==-1){yield*this.pop();yield*this.step()}else doc.start.push(this.sourceToken);return}case\"anchor\":case\"tag\":case\"space\":case\"comment\":case\"newline\":doc.start.push(this.sourceToken);return}const bv=this.startBlockValue(doc);if(bv)this.stack.push(bv);else{yield{type:\"error\",offset:this.offset,message:`Unexpected ${this.type} token in YAML document`,source:this.source}}}*scalar(scalar){if(this.type===\"map-value-ind\"){const prev=getPrevProps(this.peek(2));const start=getFirstKeyStartProps(prev);let sep;if(scalar.end){sep=scalar.end;sep.push(this.sourceToken);delete scalar.end}else sep=[this.sourceToken];const map2={type:\"block-map\",offset:scalar.offset,indent:scalar.indent,items:[{start,key:scalar,sep}]};this.onKeyLine=true;this.stack[this.stack.length-1]=map2}else yield*this.lineEnd(scalar)}*blockScalar(scalar){switch(this.type){case\"space\":case\"comment\":case\"newline\":scalar.props.push(this.sourceToken);return;case\"scalar\":scalar.source=this.source;this.atNewLine=true;this.indent=0;if(this.onNewLine){let nl=this.source.indexOf(\"\\n\")+1;while(nl!==0){this.onNewLine(this.offset+nl);nl=this.source.indexOf(\"\\n\",nl)+1}}yield*this.pop();break;default:yield*this.pop();yield*this.step()}}*blockMap(map2){const it=map2.items[map2.items.length-1];switch(this.type){case\"newline\":this.onKeyLine=false;if(it.value){const end=\"end\"in it.value?it.value.end:void 0;const last=Array.isArray(end)?end[end.length-1]:void 0;if(last?.type===\"comment\")end?.push(this.sourceToken);else map2.items.push({start:[this.sourceToken]})}else if(it.sep){it.sep.push(this.sourceToken)}else{it.start.push(this.sourceToken)}return;case\"space\":case\"comment\":if(it.value){map2.items.push({start:[this.sourceToken]})}else if(it.sep){it.sep.push(this.sourceToken)}else{if(this.atIndentedComment(it.start,map2.indent)){const prev=map2.items[map2.items.length-2];const end=prev?.value?.end;if(Array.isArray(end)){Array.prototype.push.apply(end,it.start);end.push(this.sourceToken);map2.items.pop();return}}it.start.push(this.sourceToken)}return}if(this.indent>=map2.indent){const atNextItem=!this.onKeyLine&&this.indent===map2.indent&&it.sep;let start=[];if(atNextItem&&it.sep&&!it.value){const nl=[];for(let i=0;i<it.sep.length;++i){const st=it.sep[i];switch(st.type){case\"newline\":nl.push(i);break;case\"space\":break;case\"comment\":if(st.indent>map2.indent)nl.length=0;break;default:nl.length=0}}if(nl.length>=2)start=it.sep.splice(nl[1])}switch(this.type){case\"anchor\":case\"tag\":if(atNextItem||it.value){start.push(this.sourceToken);map2.items.push({start});this.onKeyLine=true}else if(it.sep){it.sep.push(this.sourceToken)}else{it.start.push(this.sourceToken)}return;case\"explicit-key-ind\":if(!it.sep&&!includesToken(it.start,\"explicit-key-ind\")){it.start.push(this.sourceToken)}else if(atNextItem||it.value){start.push(this.sourceToken);map2.items.push({start})}else{this.stack.push({type:\"block-map\",offset:this.offset,indent:this.indent,items:[{start:[this.sourceToken]}]})}this.onKeyLine=true;return;case\"map-value-ind\":if(includesToken(it.start,\"explicit-key-ind\")){if(!it.sep){if(includesToken(it.start,\"newline\")){Object.assign(it,{key:null,sep:[this.sourceToken]})}else{const start2=getFirstKeyStartProps(it.start);this.stack.push({type:\"block-map\",offset:this.offset,indent:this.indent,items:[{start:start2,key:null,sep:[this.sourceToken]}]})}}else if(it.value){map2.items.push({start:[],key:null,sep:[this.sourceToken]})}else if(includesToken(it.sep,\"map-value-ind\")){this.stack.push({type:\"block-map\",offset:this.offset,indent:this.indent,items:[{start,key:null,sep:[this.sourceToken]}]})}else if(isFlowToken(it.key)&&!includesToken(it.sep,\"newline\")){const start2=getFirstKeyStartProps(it.start);const key=it.key;const sep=it.sep;sep.push(this.sourceToken);delete it.key,delete it.sep;this.stack.push({type:\"block-map\",offset:this.offset,indent:this.indent,items:[{start:start2,key,sep}]})}else if(start.length>0){it.sep=it.sep.concat(start,this.sourceToken)}else{it.sep.push(this.sourceToken)}}else{if(!it.sep){Object.assign(it,{key:null,sep:[this.sourceToken]})}else if(it.value||atNextItem){map2.items.push({start,key:null,sep:[this.sourceToken]})}else if(includesToken(it.sep,\"map-value-ind\")){this.stack.push({type:\"block-map\",offset:this.offset,indent:this.indent,items:[{start:[],key:null,sep:[this.sourceToken]}]})}else{it.sep.push(this.sourceToken)}}this.onKeyLine=true;return;case\"alias\":case\"scalar\":case\"single-quoted-scalar\":case\"double-quoted-scalar\":{const fs=this.flowScalar(this.type);if(atNextItem||it.value){map2.items.push({start,key:fs,sep:[]});this.onKeyLine=true}else if(it.sep){this.stack.push(fs)}else{Object.assign(it,{key:fs,sep:[]});this.onKeyLine=true}return}default:{const bv=this.startBlockValue(map2);if(bv){if(atNextItem&&bv.type!==\"block-seq\"&&includesToken(it.start,\"explicit-key-ind\")){map2.items.push({start})}this.stack.push(bv);return}}}}yield*this.pop();yield*this.step()}*blockSequence(seq2){const it=seq2.items[seq2.items.length-1];switch(this.type){case\"newline\":if(it.value){const end=\"end\"in it.value?it.value.end:void 0;const last=Array.isArray(end)?end[end.length-1]:void 0;if(last?.type===\"comment\")end?.push(this.sourceToken);else seq2.items.push({start:[this.sourceToken]})}else it.start.push(this.sourceToken);return;case\"space\":case\"comment\":if(it.value)seq2.items.push({start:[this.sourceToken]});else{if(this.atIndentedComment(it.start,seq2.indent)){const prev=seq2.items[seq2.items.length-2];const end=prev?.value?.end;if(Array.isArray(end)){Array.prototype.push.apply(end,it.start);end.push(this.sourceToken);seq2.items.pop();return}}it.start.push(this.sourceToken)}return;case\"anchor\":case\"tag\":if(it.value||this.indent<=seq2.indent)break;it.start.push(this.sourceToken);return;case\"seq-item-ind\":if(this.indent!==seq2.indent)break;if(it.value||includesToken(it.start,\"seq-item-ind\"))seq2.items.push({start:[this.sourceToken]});else it.start.push(this.sourceToken);return}if(this.indent>seq2.indent){const bv=this.startBlockValue(seq2);if(bv){this.stack.push(bv);return}}yield*this.pop();yield*this.step()}*flowCollection(fc){const it=fc.items[fc.items.length-1];if(this.type===\"flow-error-end\"){let top;do{yield*this.pop();top=this.peek(1)}while(top&&top.type===\"flow-collection\")}else if(fc.end.length===0){switch(this.type){case\"comma\":case\"explicit-key-ind\":if(!it||it.sep)fc.items.push({start:[this.sourceToken]});else it.start.push(this.sourceToken);return;case\"map-value-ind\":if(!it||it.value)fc.items.push({start:[],key:null,sep:[this.sourceToken]});else if(it.sep)it.sep.push(this.sourceToken);else Object.assign(it,{key:null,sep:[this.sourceToken]});return;case\"space\":case\"comment\":case\"newline\":case\"anchor\":case\"tag\":if(!it||it.value)fc.items.push({start:[this.sourceToken]});else if(it.sep)it.sep.push(this.sourceToken);else it.start.push(this.sourceToken);return;case\"alias\":case\"scalar\":case\"single-quoted-scalar\":case\"double-quoted-scalar\":{const fs=this.flowScalar(this.type);if(!it||it.value)fc.items.push({start:[],key:fs,sep:[]});else if(it.sep)this.stack.push(fs);else Object.assign(it,{key:fs,sep:[]});return}case\"flow-map-end\":case\"flow-seq-end\":fc.end.push(this.sourceToken);return}const bv=this.startBlockValue(fc);if(bv)this.stack.push(bv);else{yield*this.pop();yield*this.step()}}else{const parent=this.peek(2);if(parent.type===\"block-map\"&&(this.type===\"map-value-ind\"&&parent.indent===fc.indent||this.type===\"newline\"&&!parent.items[parent.items.length-1].sep)){yield*this.pop();yield*this.step()}else if(this.type===\"map-value-ind\"&&parent.type!==\"flow-collection\"){const prev=getPrevProps(parent);const start=getFirstKeyStartProps(prev);fixFlowSeqItems(fc);const sep=fc.end.splice(1,fc.end.length);sep.push(this.sourceToken);const map2={type:\"block-map\",offset:fc.offset,indent:fc.indent,items:[{start,key:fc,sep}]};this.onKeyLine=true;this.stack[this.stack.length-1]=map2}else{yield*this.lineEnd(fc)}}}flowScalar(type){if(this.onNewLine){let nl=this.source.indexOf(\"\\n\")+1;while(nl!==0){this.onNewLine(this.offset+nl);nl=this.source.indexOf(\"\\n\",nl)+1}}return{type,offset:this.offset,indent:this.indent,source:this.source}}startBlockValue(parent){switch(this.type){case\"alias\":case\"scalar\":case\"single-quoted-scalar\":case\"double-quoted-scalar\":return this.flowScalar(this.type);case\"block-scalar-header\":return{type:\"block-scalar\",offset:this.offset,indent:this.indent,props:[this.sourceToken],source:\"\"};case\"flow-map-start\":case\"flow-seq-start\":return{type:\"flow-collection\",offset:this.offset,indent:this.indent,start:this.sourceToken,items:[],end:[]};case\"seq-item-ind\":return{type:\"block-seq\",offset:this.offset,indent:this.indent,items:[{start:[this.sourceToken]}]};case\"explicit-key-ind\":{this.onKeyLine=true;const prev=getPrevProps(parent);const start=getFirstKeyStartProps(prev);start.push(this.sourceToken);return{type:\"block-map\",offset:this.offset,indent:this.indent,items:[{start}]}}case\"map-value-ind\":{this.onKeyLine=true;const prev=getPrevProps(parent);const start=getFirstKeyStartProps(prev);return{type:\"block-map\",offset:this.offset,indent:this.indent,items:[{start,key:null,sep:[this.sourceToken]}]}}}return null}atIndentedComment(start,indent){if(this.type!==\"comment\")return false;if(this.indent<=indent)return false;return start.every(st=>st.type===\"newline\"||st.type===\"space\")}*documentEnd(docEnd){if(this.type!==\"doc-mode\"){if(docEnd.end)docEnd.end.push(this.sourceToken);else docEnd.end=[this.sourceToken];if(this.type===\"newline\")yield*this.pop()}}*lineEnd(token){switch(this.type){case\"comma\":case\"doc-start\":case\"doc-end\":case\"flow-seq-end\":case\"flow-map-end\":case\"map-value-ind\":yield*this.pop();yield*this.step();break;case\"newline\":this.onKeyLine=false;case\"space\":case\"comment\":default:if(token.end)token.end.push(this.sourceToken);else token.end=[this.sourceToken];if(this.type===\"newline\")yield*this.pop()}}};function parseOptions(options){const prettyErrors=options.prettyErrors!==false;const lineCounter=options.lineCounter||prettyErrors&&new LineCounter||null;return{lineCounter,prettyErrors}}function parseDocument(source,options={}){const{lineCounter,prettyErrors}=parseOptions(options);const parser=new Parser(lineCounter?.addNewLine);const composer=new Composer(options);let doc=null;for(const _doc of composer.compose(parser.parse(source),true,source.length)){if(!doc)doc=_doc;else if(doc.options.logLevel!==\"silent\"){doc.errors.push(new YAMLParseError(_doc.range.slice(0,2),\"MULTIPLE_DOCS\",\"Source contains multiple documents; please use YAML.parseAllDocuments()\"));break}}if(prettyErrors&&lineCounter){doc.errors.forEach(prettifyError(source,lineCounter));doc.warnings.forEach(prettifyError(source,lineCounter))}return doc}function parse(src,reviver,options){let _reviver=void 0;if(typeof reviver===\"function\"){_reviver=reviver}else if(options===void 0&&reviver&&typeof reviver===\"object\"){options=reviver}const doc=parseDocument(src,options);if(!doc)return null;doc.warnings.forEach(warning=>warn(doc.options.logLevel,warning));if(doc.errors.length>0){if(doc.options.logLevel!==\"silent\")throw doc.errors[0];else doc.errors=[]}return doc.toJS(Object.assign({reviver:_reviver},options))}function stringify3(value,replacer,options){let _replacer=null;if(typeof replacer===\"function\"||Array.isArray(replacer)){_replacer=replacer}else if(options===void 0&&replacer){options=replacer}if(typeof options===\"string\")options=options.length;if(typeof options===\"number\"){const indent=Math.round(options);options=indent<1?void 0:indent>8?{indent:8}:{indent}}if(value===void 0){const{keepUndefined}=options??replacer??{};if(!keepUndefined)return void 0}return new Document(value,_replacer,options).toString(options)}globalThis.YAML={parse,stringify:stringify3};\n}()\n"
  },
  {
    "path": "npm/package.json",
    "content": "{\n  \"name\": \"fx\",\n  \"version\": \"39.2.0\",\n  \"bin\": {\n    \"fx\": \"index.js\"\n  },\n  \"files\": [\n    \"index.js\"\n  ],\n  \"scripts\": {\n    \"test\": \"node test.js\"\n  },\n  \"repository\": \"antonmedv/fx\",\n  \"homepage\": \"https://fx.wtf\",\n  \"description\": \"Command-line JSON viewer\",\n  \"author\": \"Anton Medvedev <anton@medv.io>\",\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "npm/test.js",
    "content": "async function test(name, fn) {\n  try {\n    await fn(await import('node:assert/strict'))\n    console.log(`✓ ${name}`)\n  } catch (err) {\n    console.error(`✗ ${name}`)\n    throw err\n  }\n}\n\nasync function run(json, code = '') {\n  const {spawnSync} = await import('node:child_process')\n  return spawnSync(`printf -- '${typeof json === 'string' ? json : JSON.stringify(json)}' | node index.js ${code}`, {\n    stdio: 'pipe',\n    encoding: 'utf8',\n    shell: true\n  })\n}\n\nasync function runNoPipe(code = '') {\n  const {spawnSync} = await import('node:child_process')\n  return spawnSync(`node index.js ${code}`, {\n    stdio: 'pipe',\n    encoding: 'utf8',\n    shell: true\n  })\n}\n\nvoid async function main() {\n  await test('properly formatted', async t => {\n    const {stdout} = await run([{'greeting': 'hello world'}])\n    t.deepEqual(stdout, '[\\n  {\\n    \"greeting\": \"hello world\"\\n  }\\n]\\n')\n  })\n\n  await test('format - escape newline', async t => {\n    const {stdout} = await run(`{\"foo\": \"bar\\\\\\\\nbaz\"}`)\n    t.equal(stdout, '{\\n  \"foo\": \"bar\\\\nbaz\"\\n}\\n')\n  })\n\n  await test('parseJson - valid json', async t => {\n    const obj = {a: 2.3e100, b: 'str', c: null, d: false, e: [1, 2, 3]}\n    const {stdout, stderr} = await run(obj)\n    t.equal(stderr, '')\n    t.equal(stdout, JSON.stringify(obj, null, 2) + '\\n')\n  })\n\n  await test('parseJson - invalid json', async t => {\n    const {stderr, status} = await run('{invalid}')\n    t.equal(status, 1)\n    t.ok(stderr.includes('SyntaxError'))\n  })\n\n  await test('parseJson - invalid number', async t => {\n    const {stderr, status} = await run('{\"num\": 12.3.4}')\n    t.equal(status, 1)\n    t.ok(stderr.includes('SyntaxError'))\n  })\n\n  await test('parseJson - string control chars', async t => {\n    const {stderr, status} = await run('\"\\t\"')\n    t.equal(status, 1)\n    t.ok(stderr.includes('SyntaxError'))\n  })\n\n  await test('parseJson - numbers', async t => {\n    t.equal((await run('1.2e300')).stdout, '1.2e+300\\n')\n    t.equal((await run('123456789012345678901234567890')).stdout, '123456789012345678901234567890\\n')\n    t.equal((await run('23')).stdout, '23\\n')\n    t.equal((await run('0')).stdout, '0\\n')\n    t.equal((await run('0e+2')).stdout, '0\\n')\n    t.equal((await run('0e+2')).stdout, '0\\n')\n    t.equal((await run('0.0')).stdout, '0\\n')\n    t.equal((await run('-0')).stdout, '0\\n')\n    t.equal((await run('2.3')).stdout, '2.3\\n')\n    t.equal((await run('2300e3')).stdout, '2300000\\n')\n    t.equal((await run('2300e+3')).stdout, '2300000\\n')\n    t.equal((await run('-2')).stdout, '-2\\n')\n    t.equal((await run('2e-3')).stdout, '0.002\\n')\n    t.equal((await run('2.3e-3')).stdout, '0.0023\\n')\n  })\n\n  await test('parseJson - object tailing comma', async t => {\n    const {stdout} = await run('{\"a\": 1,}')\n    t.equal(stdout, '{\\n  \"a\": 1\\n}\\n')\n  })\n\n  await test('parseJson - array tailing comma', async t => {\n    const {stdout} = await run('[1,]')\n    t.equal(stdout, '[\\n  1\\n]\\n')\n  })\n\n  await test('parseJson - comments', async t => {\n    const {stdout} = await run('/* comment */ [1 // comment\\n]')\n    t.equal(stdout, '[\\n  1\\n]\\n')\n  })\n\n  await test('parseYaml', async t => {\n    const {stdout} = await run('- foo\\n- bar', '--yaml')\n    t.equal(stdout, '[\\n  \"foo\",\\n  \"bar\"\\n]\\n')\n  })\n\n  await test('transform - anonymous function', async t => {\n    const {stdout} = await run({'key': 'value'}, '\\'function (x) { return x.key }\\'')\n    t.equal(stdout, 'value\\n')\n  })\n\n  await test('transform - arrow function', async t => {\n    const {stdout} = await run({'key': 'value'}, '\\'x => x.key\\'')\n    t.equal(stdout, 'value\\n')\n  })\n\n  await test('transform - arrow function with param brackets', async t => {\n    const {stdout} = await run({'key': 'value'}, `'(x) => x.key'`)\n    t.equal(stdout, 'value\\n')\n  })\n\n  await test('transform - this is json', async t => {\n    const {stdout} = await run([1, 2, 3, 4, 5], `'this.map(x => x * this.length)'`)\n    t.deepEqual(JSON.parse(stdout), [5, 10, 15, 20, 25])\n  })\n\n  await test('transform - chain works', async t => {\n    const {stdout} = await run({'items': ['foo', 'bar']}, `'this.items' '.' 'x => x[1]'`)\n    t.equal(stdout, 'bar\\n')\n  })\n\n  await test('transform - map works with func', async t => {\n    const {stdout} = await run([{foo: 'bar'}], `'map(x => x.foo)'`)\n    t.deepEqual(JSON.parse(stdout), ['bar'])\n  })\n\n  await test('transform - map passes index', async t => {\n    const {stdout} = await run([1, 2, 3], `'map((x, i) => x * i)'`)\n    t.deepEqual(JSON.parse(stdout), [0, 2, 6])\n  })\n\n  await test('transform - @ works', async t => {\n    const {stdout} = await run([1, 2, 3], `'@x * 2'`)\n    t.deepEqual(JSON.parse(stdout), [2, 4, 6])\n  })\n\n  await test('transform - @ works with dot', async t => {\n    const {stdout} = await run([{foo: 'bar'}], `@.foo`)\n    t.deepEqual(JSON.parse(stdout), ['bar'])\n  })\n\n  await test('transform - flat map works', async t => {\n    const {stdout} = await run({master: {foo: [{bar: [{val: 1}]}]}}, '.master.foo[].bar[].val')\n    t.deepEqual(JSON.parse(stdout), [1])\n  })\n\n  await test('transform - flat map works on the first level', async t => {\n    const {stdout} = await run([{val: 1}, {val: 2}], '.[].val')\n    t.deepEqual(JSON.parse(stdout), [1, 2])\n  })\n\n  await test('transform - sort & uniq', async t => {\n    const {stdout} = await run([2, 2, 3, 1], `sort uniq`)\n    t.deepEqual(JSON.parse(stdout), [1, 2, 3])\n  })\n\n  await test('transform - skip', async t => {\n    const {stdout} = await run(42, `skip`)\n    t.equal(stdout, '')\n  })\n\n  await test('transform - invalid code argument', async t => {\n    const json = {foo: 'bar'}\n    const code = '\".foo.toUpperCase(\"'\n    const {stderr, status} = await run(json, code)\n    t.equal(status, 1)\n    t.ok(stderr.includes(`SyntaxError: Unexpected token '}'`))\n  })\n\n  await test('stream - objects', async t => {\n    const {stdout} = await run('{\"foo\": \"bar\"}\\n{\"foo\": \"baz\"}')\n    t.equal(stdout, '{\\n  \"foo\": \"bar\"\\n}\\n{\\n  \"foo\": \"baz\"\\n}\\n')\n  })\n\n  await test('stream - strings', async t => {\n    const {stdout} = await run('\"foo\"\\n\"bar\"')\n    t.equal(stdout, 'foo\\nbar\\n')\n  })\n\n  await test('flags - raw flag', async t => {\n    const {stdout} = await run(123, `-r 'x => typeof x'`)\n    t.equal(stdout, 'string\\n')\n  })\n\n  await test('flags - raw reads entire input', async t => {\n    const {stdout} = await run('foo\\bbar', `-r`)\n    t.equal(stdout, 'foo\\bbar\\n')\n  })\n\n  await test('flags - slurp flag', async t => {\n    const {stdout} = await run('{\"foo\": \"bar\"}\\n{\"foo\": \"baz\"}', `-s '.[1].foo'`)\n    t.equal(stdout, 'baz\\n')\n  })\n\n  await test('flags - slurp raw', async t => {\n    const {stdout} = await run('hello,\\nworld!', `-rs '.join(\" \")'`)\n    t.equal(stdout, 'hello, world!\\n')\n  })\n\n  await test('cli - first arg is file', async t => {\n    const {stdout} = await runNoPipe(`package.json .name`)\n    t.equal(stdout, 'fx\\n')\n  })\n\n  await test('cli - last arg is file', async t => {\n    const {stdout} = await runNoPipe(`.name package.json`)\n    t.equal(stdout, 'fx\\n')\n  })\n\n  await test('cli - very large arg', async t => {\n    const {status, stderr, stdout} = await run(42, `'x => x /* dsasdfaskjdfhaskldjfhgaslkdjfhasdlkfjhasdlkfjhasdlfkjhasdflkjasdhflkjasdhflacnskdcfhalsdkfjhasldkfjhcasdlckfajhdsflbkasjdhfclnaskdjhfalskdfgjhsdflkfjhasdlfkahjsdflkasjhdflkafdggrhdfggsdfghsdghadfgsdfgsdfglhadshfglaksjdfhalskjdfhasldkfjhaldfkjhasdlfkjhasdflkjhadflkhasdlkfjhdfkhjasdlfkjhasdflkhaflkcansdfhvlkvajhfgvbalergtcqwaleifhavslbkfchasdblkfhldsfhasdfasfasdfdfdddddddadlakfjhas */'`)\n    t.equal(status, 0, stderr)\n    t.equal(stdout, '42\\n')\n  })\n}()\n"
  },
  {
    "path": "preview.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nvar reverseStyle = lipgloss.NewStyle().Reverse(true).Render\n\nfunc (m *model) handlePreviewKey(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tvar cmd tea.Cmd\n\tif msg, ok := msg.(tea.KeyMsg); ok {\n\t\tif m.previewSearchInput.Focused() {\n\t\t\treturn m.handlePreviewSearchInput(msg)\n\t\t}\n\t\tswitch {\n\t\tcase key.Matches(msg, keyMap.Quit),\n\t\t\tkey.Matches(msg, keyMap.Preview):\n\t\t\tm.showPreview = false\n\t\t\treturn m, nil\n\n\t\tcase key.Matches(msg, keyMap.Print):\n\t\t\treturn m, m.print()\n\n\t\tcase key.Matches(msg, keyMap.GotoTop):\n\t\t\tm.preview.GotoTop()\n\t\t\treturn m, nil\n\n\t\tcase key.Matches(msg, keyMap.GotoBottom):\n\t\t\tm.preview.GotoBottom()\n\t\t\treturn m, nil\n\n\t\tcase key.Matches(msg, keyMap.HalfPageUp):\n\t\t\tm.preview.HalfPageUp()\n\t\t\treturn m, nil\n\n\t\tcase key.Matches(msg, keyMap.HalfPageDown):\n\t\t\tm.preview.HalfPageDown()\n\t\t\treturn m, nil\n\n\t\tcase key.Matches(msg, keyMap.PageUp):\n\t\t\tm.preview.PageUp()\n\t\t\treturn m, nil\n\n\t\tcase key.Matches(msg, keyMap.PageDown):\n\t\t\tm.preview.PageDown()\n\t\t\treturn m, nil\n\n\t\tcase key.Matches(msg, keyMap.Search):\n\t\t\tm.previewSearchInput.CursorEnd()\n\t\t\tm.previewSearchInput.Width = m.termWidth - 2\n\t\t\tm.previewSearchInput.Focus()\n\t\t\treturn m, nil\n\n\t\tcase key.Matches(msg, keyMap.SearchNext):\n\t\t\tm.selectPreviewSearchResult(m.previewSearchCursor + 1)\n\t\t\treturn m, nil\n\n\t\tcase key.Matches(msg, keyMap.SearchPrev):\n\t\t\tm.selectPreviewSearchResult(m.previewSearchCursor - 1)\n\t\t\treturn m, nil\n\t\t}\n\t}\n\tm.preview, cmd = m.preview.Update(msg)\n\treturn m, cmd\n}\n\nfunc (m *model) handlePreviewSearchInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {\n\tvar cmd tea.Cmd\n\tswitch {\n\tcase msg.Type == tea.KeyEscape:\n\t\tm.previewSearchInput.Blur()\n\t\tm.previewSearchInput.SetValue(\"\")\n\t\tm.previewSearchResults = nil\n\t\tm.previewSearchCursor = -1\n\t\tm.preview.SetContent(m.wrapString(m.previewValue))\n\t\treturn m, nil\n\n\tcase msg.Type == tea.KeyEnter:\n\t\tm.previewSearchInput.Blur()\n\t\tfound := m.doPreviewSearch(m.previewSearchInput.Value())\n\t\tif !found {\n\t\t\tm.previewSearchResults = nil\n\t\t\tm.previewSearchCursor = -1\n\t\t\tm.preview.SetContent(m.wrapString(m.previewValue))\n\t\t}\n\t\treturn m, nil\n\n\tdefault:\n\t\tm.previewSearchInput, cmd = m.previewSearchInput.Update(msg)\n\t}\n\treturn m, cmd\n}\n\nfunc (m *model) doPreviewSearch(pattern string) bool {\n\tif pattern == \"\" {\n\t\treturn false\n\t}\n\n\tcode, ci := regexCase(pattern)\n\tif ci {\n\t\tcode = \"(?i)\" + code\n\t}\n\n\tre, err := regexp.Compile(code)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tcontent := m.previewValue\n\tmatches := re.FindAllStringIndex(content, -1)\n\n\tm.previewSearchResults = nil\n\n\tif len(matches) == 0 {\n\t\treturn false\n\t}\n\n\t// Precalculate visual line numbers for each match, accounting for line wrapping\n\tlines := strings.Split(content, \"\\n\")\n\tvisualLineStarts := make([]int, len(lines))\n\tcumulative := 0\n\tfor i, line := range lines {\n\t\tvisualLineStarts[i] = cumulative\n\t\twrapped := m.wrapString(line)\n\t\tcumulative += strings.Count(wrapped, \"\\n\") + 1\n\t}\n\n\tfor _, match := range matches {\n\t\t// Find original line number\n\t\torigLineNum := strings.Count(content[:match[0]], \"\\n\")\n\n\t\t// Find position within the original line\n\t\tlastNewline := strings.LastIndex(content[:match[0]], \"\\n\")\n\t\tvar posInLine int\n\t\tif lastNewline == -1 {\n\t\t\tposInLine = match[0]\n\t\t} else {\n\t\t\tposInLine = match[0] - lastNewline - 1\n\t\t}\n\n\t\t// Calculate visual line: start of this original line + offset within wrapped line\n\t\tvisualLineNum := visualLineStarts[origLineNum]\n\n\t\t// Add offset for wrapping within the line\n\t\tif posInLine > 0 && m.termWidth > 0 && posInLine <= len(lines[origLineNum]) {\n\t\t\tlinePrefix := lines[origLineNum][:posInLine]\n\t\t\twrappedPrefix := m.wrapString(linePrefix)\n\t\t\tvisualLineNum += strings.Count(wrappedPrefix, \"\\n\")\n\t\t}\n\n\t\tm.previewSearchResults = append(m.previewSearchResults, visualLineNum)\n\t}\n\n\t// Highlight all matches with Reverse style (once)\n\tvar result strings.Builder\n\tlastEnd := 0\n\tfor _, match := range matches {\n\t\tstart, end := match[0], match[1]\n\t\tif start > lastEnd {\n\t\t\tresult.WriteString(content[lastEnd:start])\n\t\t}\n\t\tresult.WriteString(reverseStyle(content[start:end]))\n\t\tlastEnd = end\n\t}\n\tif lastEnd < len(content) {\n\t\tresult.WriteString(content[lastEnd:])\n\t}\n\n\tm.preview.SetContent(m.wrapString(result.String()))\n\n\t// Jump to first match\n\tm.previewSearchCursor = 0\n\tm.preview.SetYOffset(m.previewSearchResults[0])\n\treturn true\n}\n\nfunc (m *model) selectPreviewSearchResult(i int) {\n\tif len(m.previewSearchResults) == 0 {\n\t\treturn\n\t}\n\tif i < 0 {\n\t\ti = len(m.previewSearchResults) - 1\n\t}\n\tif i >= len(m.previewSearchResults) {\n\t\ti = 0\n\t}\n\tm.previewSearchCursor = i\n\n\t// Scroll to the cached line number\n\tm.preview.SetYOffset(m.previewSearchResults[i])\n}\n\nfunc (m *model) previewSearchStatusBar() string {\n\tif m.previewSearchInput.Focused() {\n\t\treturn m.previewSearchInput.View()\n\t}\n\n\tpattern := m.previewSearchInput.Value()\n\tif pattern == \"\" {\n\t\treturn \"\"\n\t}\n\n\tre, ci := regexCase(pattern)\n\tre = \"/\" + re + \"/\"\n\tif ci {\n\t\tre += \"i\"\n\t}\n\n\tif len(m.previewSearchResults) == 0 {\n\t\treturn flex(m.termWidth, re, \"not found\")\n\t}\n\n\tcursor := fmt.Sprintf(\"found: [%v/%v]\", m.previewSearchCursor+1, len(m.previewSearchResults))\n\treturn flex(m.termWidth, re, cursor)\n}\n\nfunc (m *model) wrapString(value string) string {\n\treturn lipgloss.NewStyle().Width(m.termWidth).Render(value)\n}\n"
  },
  {
    "path": "scripts/build.mjs",
    "content": "$.verbose = true\n\nconst goos = [\n  'linux',\n  'darwin',\n  'windows',\n]\nconst goarch = [\n  'amd64',\n  'arm64',\n]\n\nconst name = (GOOS, GOARCH) => `fx_${GOOS}_${GOARCH}` + (GOOS === 'windows' ? '.exe' : '')\n\nconst resp = await fetch('https://api.github.com/repos/antonmedv/fx/releases/latest')\nconst {tag_name: latest} = await resp.json()\n\nawait $`go mod download`\n\nawait Promise.all(\n  goos.flatMap(GOOS =>\n    goarch.map(GOARCH =>\n      $`GOOS=${GOOS} GOARCH=${GOARCH} go build -o ${name(GOOS, GOARCH)}`)))\n\nawait Promise.all(\n  goos.flatMap(GOOS =>\n    goarch.map(GOARCH =>\n      $`gh release upload ${latest} ${name(GOOS, GOARCH)}`)))\n\nawait Promise.all(\n  goos.flatMap(GOOS =>\n    goarch.map(GOARCH =>\n      $`rm ${name(GOOS, GOARCH)}`)))\n"
  },
  {
    "path": "search.go",
    "content": "package main\n\nimport (\n\t\"regexp\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t. \"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc (m *model) doSearch(s string) tea.Cmd {\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\n\tm.searching = true\n\tm.searchID++\n\tm.searchCancel = make(chan struct{})\n\tid := m.searchID\n\tcancel := m.searchCancel\n\ttop := m.top\n\tquery := s\n\n\treturn tea.Batch(m.spinner.Tick, func() tea.Msg {\n\t\tresult, err := executeSearch(top, query, cancel)\n\t\tif err != nil {\n\t\t\terrSearch := newSearch()\n\t\t\terrSearch.err = err\n\t\t\treturn searchResultMsg{id: id, query: query, search: errSearch}\n\t\t}\n\t\tif result == nil {\n\t\t\t// Search was cancelled\n\t\t\treturn searchCancelledMsg{}\n\t\t}\n\t\treturn searchResultMsg{id: id, query: query, search: result}\n\t})\n}\n\nfunc (m *model) cancelSearch() {\n\tif m.searchCancel != nil {\n\t\tclose(m.searchCancel)\n\t\tm.searchCancel = nil\n\t\tm.searching = false\n\t}\n}\n\nfunc (m *model) selectSearchResult(i int) {\n\tif len(m.search.results) == 0 {\n\t\treturn\n\t}\n\tif i < 0 {\n\t\ti = len(m.search.results) - 1\n\t}\n\tif i >= len(m.search.results) {\n\t\ti = 0\n\t}\n\tm.search.cursor = i\n\tresult := m.search.results[i]\n\tm.selectNode(result)\n\tm.showCursor = false\n}\n\nfunc (m *model) redoSearch() {\n\ts := m.searchInput.Value()\n\tif s == \"\" || len(m.search.results) == 0 {\n\t\treturn\n\t}\n\n\tcursor := m.search.cursor\n\n\t// Perform search synchronously (no cancellation needed for redo)\n\tresult, err := executeSearch(m.top, s, nil)\n\tif err != nil {\n\t\tm.search = newSearch()\n\t\tm.search.err = err\n\t\treturn\n\t}\n\n\tm.search = result\n\tm.selectSearchResult(cursor)\n}\n\ntype search struct {\n\terr     error\n\tresults []*Node\n\tcursor  int\n\tvalues  map[*Node][]match\n\tkeys    map[*Node][]match\n}\n\nfunc newSearch() *search {\n\treturn &search{\n\t\tresults: make([]*Node, 0),\n\t\tvalues:  make(map[*Node][]match),\n\t\tkeys:    make(map[*Node][]match),\n\t}\n}\n\ntype match struct {\n\tstart, end int\n\tindex      int\n}\n\ntype piece struct {\n\tb     string\n\tindex int\n}\n\n// executeSearch performs the core search logic and returns the results.\n// It can be cancelled via the cancel channel (pass nil for non-cancellable search).\nfunc executeSearch(top *Node, s string, cancel <-chan struct{}) (*search, error) {\n\tcode, ci := regexCase(s)\n\tif ci {\n\t\tcode = \"(?i)\" + code\n\t}\n\n\tre, err := regexp.Compile(code)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := newSearch()\n\tn := top\n\tsearchIndex := 0\n\n\tfor n != nil {\n\t\t// Check for cancellation if channel provided\n\t\tif cancel != nil {\n\t\t\tselect {\n\t\t\tcase <-cancel:\n\t\t\t\treturn nil, nil // cancelled\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\n\t\tif n.Key != \"\" {\n\t\t\tindexes := re.FindAllStringIndex(n.Key, -1)\n\t\t\tif len(indexes) > 0 {\n\t\t\t\tfor i, pair := range indexes {\n\t\t\t\t\tresult.results = append(result.results, n)\n\t\t\t\t\tresult.keys[n] = append(result.keys[n], match{start: pair[0], end: pair[1], index: searchIndex + i})\n\t\t\t\t}\n\t\t\t\tsearchIndex += len(indexes)\n\t\t\t}\n\t\t}\n\t\tindexes := re.FindAllStringIndex(n.Value, -1)\n\t\tif len(indexes) > 0 {\n\t\t\tfor range indexes {\n\t\t\t\tresult.results = append(result.results, n)\n\t\t\t}\n\t\t\tif n.Chunk != \"\" {\n\t\t\t\t// String can be split into chunks, so we need to map the indexes to the chunks.\n\t\t\t\tchunks := []string{n.Chunk}\n\t\t\t\tchunkNodes := []*Node{n}\n\n\t\t\t\tit := n.Next\n\t\t\t\tfor it != nil {\n\t\t\t\t\tchunkNodes = append(chunkNodes, it)\n\t\t\t\t\tchunks = append(chunks, it.Chunk)\n\t\t\t\t\tif it == n.ChunkEnd {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tit = it.Next\n\t\t\t\t}\n\n\t\t\t\tchunkMatches := splitIndexesToChunks(chunks, indexes, searchIndex)\n\t\t\t\tfor i, matches := range chunkMatches {\n\t\t\t\t\tresult.values[chunkNodes[i]] = matches\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor i, pair := range indexes {\n\t\t\t\t\tresult.values[n] = append(result.values[n], match{start: pair[0], end: pair[1], index: searchIndex + i})\n\t\t\t\t}\n\t\t\t}\n\t\t\tsearchIndex += len(indexes)\n\t\t}\n\n\t\tif n.IsCollapsed() {\n\t\t\tn = n.Collapsed\n\t\t} else {\n\t\t\tn = n.Next\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\nfunc splitByIndexes(s string, indexes []match) []piece {\n\tout := make([]piece, 0, 1)\n\tpos := 0\n\tfor _, pair := range indexes {\n\t\tout = append(out, piece{safeSlice(s, pos, pair.start), -1})\n\t\tout = append(out, piece{safeSlice(s, pair.start, pair.end), pair.index})\n\t\tpos = pair.end\n\t}\n\tout = append(out, piece{safeSlice(s, pos, len(s)), -1})\n\treturn out\n}\n\nfunc splitIndexesToChunks(chunks []string, indexes [][]int, searchIndex int) (chunkIndexes [][]match) {\n\tchunkIndexes = make([][]match, len(chunks))\n\n\tfor index, idx := range indexes {\n\t\tposition := 0\n\t\tfor i, chunk := range chunks {\n\t\t\t// If start index lies in this chunk\n\t\t\tif idx[0] < position+len(chunk) {\n\t\t\t\t// Calculate local start and end for this chunk\n\t\t\t\tlocalStart := idx[0] - position\n\t\t\t\tlocalEnd := idx[1] - position\n\n\t\t\t\t// If the end index also lies in this chunk\n\t\t\t\tif idx[1] <= position+len(chunk) {\n\t\t\t\t\tchunkIndexes[i] = append(chunkIndexes[i], match{start: localStart, end: localEnd, index: searchIndex + index})\n\t\t\t\t\tbreak\n\t\t\t\t} else {\n\t\t\t\t\t// If the end index is outside this chunk, split the index\n\t\t\t\t\tchunkIndexes[i] = append(chunkIndexes[i], match{start: localStart, end: len(chunk), index: searchIndex + index})\n\n\t\t\t\t\t// Adjust the starting index for the next chunk\n\t\t\t\t\tidx[0] = position + len(chunk)\n\t\t\t\t}\n\t\t\t}\n\t\t\tposition += len(chunk)\n\t\t}\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "search_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t. \"github.com/antonmedv/fx/internal/jsonx\"\n)\n\n// doSearch is a test helper that performs a synchronous search using executeSearch.\nfunc doSearch(m *model, s string) {\n\tif s == \"\" {\n\t\treturn\n\t}\n\n\tresult, err := executeSearch(m.top, s, nil)\n\tif err != nil {\n\t\tm.search = newSearch()\n\t\tm.search.err = err\n\t\treturn\n\t}\n\n\tm.search = result\n\tm.selectSearchResult(0)\n}\n\nfunc TestBasicSearch(t *testing.T) {\n\tjsonData := `{\n\t\t\"name\": \"John Doe\",\n\t\t\"age\": 30,\n\t\t\"email\": \"john@example.com\",\n\t\t\"active\": true,\n\t\t\"skills\": [\"JavaScript\", \"Go\", \"Python\"]\n\t}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\tm := &model{\n\t\ttop:    head,\n\t\thead:   head,\n\t\tsearch: newSearch(),\n\t}\n\n\ttestCases := []struct {\n\t\tsearchTerm      string\n\t\texpectedResults int\n\t\tdescription     string\n\t}{\n\t\t{\"John\", 1, \"Simple string search\"},\n\t\t{\"30\", 1, \"Number search\"},\n\t\t{\"example.com\", 1, \"Domain search\"},\n\t\t{\"JavaScript\", 1, \"Array element search\"},\n\t\t{\"active\", 1, \"Boolean key search\"},\n\t\t{\"nonexistent\", 0, \"No match search\"},\n\t\t{\"\", 0, \"Empty search\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tdoSearch(m, tc.searchTerm)\n\n\t\t\tif tc.expectedResults == 0 {\n\t\t\t\tassert.Equal(t, 0, len(m.search.results), \"Should find no results for: %s\", tc.searchTerm)\n\t\t\t} else {\n\t\t\t\tassert.Greater(t, len(m.search.results), 0, \"Should find results for: %s\", tc.searchTerm)\n\t\t\t}\n\t\t\tassert.Nil(t, m.search.err, \"Search should not error for: %s\", tc.searchTerm)\n\t\t})\n\t}\n}\n\nfunc TestRegexSearch(t *testing.T) {\n\tjsonData := `{\n\t\t\"users\": [\n\t\t\t{\"id\": \"USER-001\", \"email\": \"alice@company.com\", \"score\": 95.5},\n\t\t\t{\"id\": \"USER-002\", \"email\": \"bob@company.com\", \"score\": 87.2},\n\t\t\t{\"id\": \"ADMIN-001\", \"email\": \"admin@company.com\", \"score\": 100.0}\n\t\t],\n\t\t\"metadata\": {\n\t\t\t\"version\": \"v1.2.3\",\n\t\t\t\"timestamp\": \"2024-01-15T10:30:00Z\"\n\t\t}\n\t}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\tm := &model{\n\t\ttop:    head,\n\t\thead:   head,\n\t\tsearch: newSearch(),\n\t}\n\n\ttestCases := []struct {\n\t\tpattern     string\n\t\tshouldMatch bool\n\t\tdescription string\n\t}{\n\t\t// Pattern matching\n\t\t{\"USER-\\\\d+\", true, \"User ID pattern\"},\n\t\t{\"\\\\d+\\\\.\\\\d+\", true, \"Decimal number pattern\"},\n\t\t{\"[a-z]+@[a-z]+\\\\.com\", true, \"Email pattern\"},\n\t\t{\"v\\\\d+\\\\.\\\\d+\\\\.\\\\d+\", true, \"Version pattern\"},\n\t\t{\"\\\\d{4}-\\\\d{2}-\\\\d{2}\", true, \"Date pattern\"},\n\n\t\t// Anchored searches\n\t\t{\"^\\\"id\\\"\", true, \"Key at start of line\"},\n\t\t{\"com\\\"$\", true, \"Value at end of line\"},\n\n\t\t// Character classes\n\t\t{\"[A-Z]{4,}\", true, \"Uppercase letters\"},\n\t\t{\"[0-9]{3}\", true, \"Three digits\"},\n\n\t\t// Quantifiers\n\t\t{\"o{2}\", true, \"Double 'o'\"},\n\t\t{\"a+\", true, \"One or more 'a'\"},\n\t\t{\"z*\", true, \"Zero or more 'z'\"},\n\n\t\t// Invalid patterns should error\n\t\t{\"[\", false, \"Invalid bracket\"},\n\t\t{\"(\", false, \"Unclosed parenthesis\"},\n\t\t{\"*\", false, \"Invalid quantifier\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tdoSearch(m, tc.pattern)\n\n\t\t\tif tc.shouldMatch {\n\t\t\t\tassert.Nil(t, m.search.err, \"Pattern should be valid: %s\", tc.pattern)\n\t\t\t} else {\n\t\t\t\tassert.NotNil(t, m.search.err, \"Pattern should be invalid: %s\", tc.pattern)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCaseInsensitiveSearch(t *testing.T) {\n\tjsonData := `{\n\t\t\"Company\": \"ACME Corporation\",\n\t\t\"employees\": [\n\t\t\t{\"Name\": \"Alice Johnson\", \"Department\": \"Engineering\"},\n\t\t\t{\"name\": \"bob smith\", \"department\": \"marketing\"},\n\t\t\t{\"NAME\": \"CHARLIE BROWN\", \"DEPARTMENT\": \"SALES\"}\n\t\t]\n\t}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\tm := &model{\n\t\ttop:    head,\n\t\thead:   head,\n\t\tsearch: newSearch(),\n\t}\n\n\ttestCases := []struct {\n\t\tsearchTerm  string\n\t\tdescription string\n\t}{\n\t\t{\"alice\", \"Lowercase search for mixed case\"},\n\t\t{\"ALICE\", \"Uppercase search for mixed case\"},\n\t\t{\"Alice\", \"Proper case search\"},\n\t\t{\"aLiCe\", \"Mixed case search\"},\n\t\t{\"company\", \"Lowercase search for uppercase\"},\n\t\t{\"ENGINEERING\", \"Uppercase search for proper case\"},\n\t\t{\"marketing\", \"Exact case match\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tdoSearch(m, tc.searchTerm)\n\n\t\t\t// Case insensitive search should find matches regardless of case\n\t\t\tassert.Greater(t, len(m.search.results), 0, \"Should find case-insensitive match for: %s\", tc.searchTerm)\n\t\t\tassert.Nil(t, m.search.err, \"Search should not error\")\n\t\t})\n\t}\n}\n\nfunc TestSearchInDifferentNodeTypes(t *testing.T) {\n\tjsonData := `{\n\t\t\"string_field\": \"hello world\",\n\t\t\"number_field\": 42,\n\t\t\"boolean_field\": true,\n\t\t\"null_field\": null,\n\t\t\"array_field\": [1, \"two\", 3.14, false],\n\t\t\"object_field\": {\n\t\t\t\"nested_string\": \"nested value\",\n\t\t\t\"nested_number\": 99\n\t\t}\n\t}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\tm := &model{\n\t\ttop:    head,\n\t\thead:   head,\n\t\tsearch: newSearch(),\n\t}\n\n\ttestCases := []struct {\n\t\tsearchTerm  string\n\t\tnodeType    string\n\t\tdescription string\n\t}{\n\t\t{\"hello\", \"string\", \"Search in string values\"},\n\t\t{\"42\", \"number\", \"Search in number values\"},\n\t\t{\"true\", \"boolean\", \"Search in boolean values\"},\n\t\t{\"null\", \"null\", \"Search in null values\"},\n\t\t{\"two\", \"array element\", \"Search in array elements\"},\n\t\t{\"nested\", \"object property\", \"Search in nested objects\"},\n\t\t{\"string_field\", \"key\", \"Search in JSON keys\"},\n\t\t{\"3.14\", \"float\", \"Search in floating point numbers\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tdoSearch(m, tc.searchTerm)\n\n\t\t\tassert.Greater(t, len(m.search.results), 0, \"Should find %s in %s\", tc.searchTerm, tc.nodeType)\n\t\t\tassert.Nil(t, m.search.err, \"Search should not error\")\n\t\t})\n\t}\n}\n\nfunc TestSearchResultDetails(t *testing.T) {\n\tjsonData := `{\n\t\t\"message\": \"The quick brown fox jumps over the lazy dog\",\n\t\t\"words\": [\"fox\", \"dog\", \"fox\"]\n\t}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\tm := &model{\n\t\ttop:    head,\n\t\thead:   head,\n\t\tsearch: newSearch(),\n\t}\n\n\t// Test multiple matches in same value\n\tdoSearch(m, \"fox\")\n\n\tassert.Greater(t, len(m.search.results), 0, \"Should find fox matches\")\n\tassert.Nil(t, m.search.err, \"Search should not error\")\n\n\t// Verify that we have both key matches and value matches\n\thasKeyMatches := len(m.search.keys) > 0\n\thasValueMatches := len(m.search.values) > 0\n\n\tassert.True(t, hasKeyMatches || hasValueMatches, \"Should have either key or value matches\")\n\n\t// Test that search cursor is initialized\n\tif len(m.search.results) > 0 {\n\t\tassert.GreaterOrEqual(t, m.search.cursor, 0, \"Search cursor should be valid\")\n\t\tassert.Less(t, m.search.cursor, len(m.search.results), \"Search cursor should be within bounds\")\n\t}\n}\n\nfunc TestSearchNavigation(t *testing.T) {\n\tjsonData := `{\n\t\t\"items\": [\n\t\t\t{\"name\": \"apple\", \"color\": \"red\"},\n\t\t\t{\"name\": \"banana\", \"color\": \"yellow\"},\n\t\t\t{\"name\": \"apple\", \"color\": \"green\"}\n\t\t]\n\t}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\tm := &model{\n\t\ttop:    head,\n\t\thead:   head,\n\t\tsearch: newSearch(),\n\t}\n\n\t// Search for term with multiple matches\n\tdoSearch(m, \"apple\")\n\n\trequire.Greater(t, len(m.search.results), 1, \"Should find multiple apple matches\")\n\n\tinitialCursor := m.search.cursor\n\n\t// Test forward navigation\n\tm.selectSearchResult(m.search.cursor + 1)\n\tassert.NotEqual(t, initialCursor, m.search.cursor, \"Cursor should move forward\")\n\tassert.GreaterOrEqual(t, m.search.cursor, 0, \"Cursor should be valid\")\n\tassert.Less(t, m.search.cursor, len(m.search.results), \"Cursor should be within bounds\")\n\n\t// Test wrap-around (next from last should go to first)\n\tlastIndex := len(m.search.results) - 1\n\tm.selectSearchResult(lastIndex + 1)\n\tassert.Equal(t, 0, m.search.cursor, \"Should wrap to first result\")\n\n\t// Test backward wrap-around (previous from first should go to last)\n\tm.selectSearchResult(-1)\n\tassert.Equal(t, lastIndex, m.search.cursor, \"Should wrap to last result\")\n}\n\nfunc TestSpecialCharacterSearch(t *testing.T) {\n\tjsonData := `{\n\t\t\"symbols\": \"!@#$%^&*()_+-={}[]|\\\\:;\\\"'<>?,./'\",\n\t\t\"escaped\": \"Line 1\\nLine 2\\tTabbed\\r\\nWindows line\",\n\t\t\"unicode\": \"café, naïve, résumé, 中文, 🚀\",\n\t\t\"quotes\": \"He said \\\"Hello world!\\\"\",\n\t\t\"backslash\": \"C:\\\\Users\\\\Name\\\\file.txt\"\n\t}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\tm := &model{\n\t\ttop:    head,\n\t\thead:   head,\n\t\tsearch: newSearch(),\n\t}\n\n\ttestCases := []struct {\n\t\tsearchTerm  string\n\t\tdescription string\n\t}{\n\t\t{\"@\", \"At symbol\"},\n\t\t{\"\\\\$\", \"Dollar sign (escaped)\"},\n\t\t{\"\\\\*\", \"Asterisk (escaped)\"},\n\t\t{\"\\\\[\", \"Square bracket (escaped)\"},\n\t\t{\"\\\\\\\\n\", \"Newline escape sequence\"},\n\t\t{\"\\\\\\\\t\", \"Tab escape sequence\"},\n\t\t{\"café\", \"Unicode characters\"},\n\t\t{\"中文\", \"Chinese characters\"},\n\t\t{\"🚀\", \"Emoji\"},\n\t\t{\"\\\\\\\"\", \"Escaped quotes\"},\n\t\t{\"C:\", \"Drive letter\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tdoSearch(m, tc.searchTerm)\n\n\t\t\t// Should either find matches or have valid regex (no panic)\n\t\t\tassert.Nil(t, m.search.err, \"Should handle special characters without error: %s\", tc.searchTerm)\n\t\t})\n\t}\n}\n\nfunc TestEmptyAndEdgeCases(t *testing.T) {\n\ttestCases := []struct {\n\t\tjsonData    string\n\t\tsearchTerm  string\n\t\tdescription string\n\t}{\n\t\t{`{}`, \"anything\", \"Empty object\"},\n\t\t{`[]`, \"anything\", \"Empty array\"},\n\t\t{`\"\"`, \"empty\", \"Empty string value\"},\n\t\t{`{\"\": \"empty key\"}`, \"\", \"Empty key search\"},\n\t\t{`{\"key\": \"\"}`, \"key\", \"Search in empty value\"},\n\t\t{`null`, \"null\", \"Null document\"},\n\t\t{`false`, \"false\", \"Boolean document\"},\n\t\t{`0`, \"0\", \"Zero value\"},\n\t\t{`{\"very_long_key_name_that_exceeds_normal_length\": \"value\"}`, \"very_long\", \"Long key names\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\thead, err := Parse([]byte(tc.jsonData))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tm := &model{\n\t\t\t\ttop:    head,\n\t\t\t\thead:   head,\n\t\t\t\tsearch: newSearch(),\n\t\t\t}\n\n\t\t\tdoSearch(m, tc.searchTerm)\n\n\t\t\t// Should not panic or error on edge cases\n\t\t\tassert.Nil(t, m.search.err, \"Should handle edge case without error\")\n\t\t\tassert.NotNil(t, m.search.results, \"Results should not be nil\")\n\t\t})\n\t}\n}\n\nfunc TestLargeJSONSearch(t *testing.T) {\n\t// Build a larger JSON structure\n\tjsonBuilder := `{\"users\": [`\n\tfor i := 0; i < 100; i++ {\n\t\tif i > 0 {\n\t\t\tjsonBuilder += \",\"\n\t\t}\n\t\tjsonBuilder += `{\"id\": ` + string(rune(i)) + `, \"name\": \"User` + string(rune(i)) + `\", \"active\": `\n\t\tif i%2 == 0 {\n\t\t\tjsonBuilder += \"true\"\n\t\t} else {\n\t\t\tjsonBuilder += \"false\"\n\t\t}\n\t\tjsonBuilder += `}`\n\t}\n\tjsonBuilder += `]}`\n\n\t// Use simpler approach for test\n\tjsonData := `{\n\t\t\"users\": [\n\t\t\t{\"id\": 1, \"name\": \"User1\", \"active\": true},\n\t\t\t{\"id\": 2, \"name\": \"User2\", \"active\": false},\n\t\t\t{\"id\": 3, \"name\": \"User3\", \"active\": true}\n\t\t],\n\t\t\"repeated_data\": [\"test\", \"test\", \"test\", \"test\", \"test\"]\n\t}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\tm := &model{\n\t\ttop:    head,\n\t\thead:   head,\n\t\tsearch: newSearch(),\n\t}\n\n\t// Test that search completes in reasonable time\n\tdoSearch(m, \"User\")\n\tassert.Greater(t, len(m.search.results), 0, \"Should find users in large JSON\")\n\tassert.Nil(t, m.search.err, \"Should not error on large JSON\")\n\n\t// Test repeated terms\n\tdoSearch(m, \"test\")\n\tassert.Greater(t, len(m.search.results), 3, \"Should find multiple instances of repeated term\")\n}\n\nfunc TestSearchInWrappedStrings(t *testing.T) {\n\t// JSON with a long string that will be wrapped\n\tjsonData := `{\n\t\t\"description\": \"This is a very long string that should be wrapped across multiple lines when the terminal width is narrow enough to trigger wrapping behavior\"\n\t}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\t// Apply wrapping with a narrow terminal width to force chunks\n\tWrap(head, 40)\n\n\tm := &model{\n\t\ttop:       head,\n\t\thead:      head,\n\t\tsearch:    newSearch(),\n\t\twrap:      true,\n\t\ttermWidth: 40,\n\t}\n\n\ttestCases := []struct {\n\t\tsearchTerm  string\n\t\texpectMatch bool\n\t\tdescription string\n\t}{\n\t\t{\"very long string\", true, \"Multi-word search in wrapped text\"},\n\t\t{\"wrapped across\", true, \"Search spanning potential chunk boundary\"},\n\t\t{\"terminal width\", true, \"Search near end of wrapped text\"},\n\t\t{\"This is a\", true, \"Search at beginning of wrapped text\"},\n\t\t{\"behavior\", true, \"Search at very end of wrapped text\"},\n\t\t{\"nonexistent phrase\", false, \"No match in wrapped text\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tdoSearch(m, tc.searchTerm)\n\n\t\t\tif tc.expectMatch {\n\t\t\t\tassert.Greater(t, len(m.search.results), 0, \"Should find '%s' in wrapped text\", tc.searchTerm)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, 0, len(m.search.results), \"Should not find '%s' in wrapped text\", tc.searchTerm)\n\t\t\t}\n\t\t\tassert.Nil(t, m.search.err, \"Search should not error\")\n\t\t})\n\t}\n}\n\nfunc TestSearchChunkBoundaryMatches(t *testing.T) {\n\t// Create a string where we know approximately where chunk boundaries will be\n\t// With termWidth=30 and some indentation, each chunk will be roughly 25-28 chars\n\tjsonData := `{\"text\": \"AAAAA BBBBB CCCCC DDDDD EEEEE FFFFF GGGGG HHHHH IIIII JJJJJ\"}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\t// Apply wrapping with narrow width\n\tWrap(head, 30)\n\n\tm := &model{\n\t\ttop:       head,\n\t\thead:      head,\n\t\tsearch:    newSearch(),\n\t\twrap:      true,\n\t\ttermWidth: 30,\n\t}\n\n\t// Verify chunks were created\n\ttextNode := head.Next // Skip the opening brace to get to the \"text\" key node\n\trequire.NotNil(t, textNode, \"Should have text node\")\n\trequire.NotEmpty(t, textNode.Chunk, \"Text node should have chunks (Chunk field set)\")\n\trequire.NotNil(t, textNode.ChunkEnd, \"Text node should have ChunkEnd set\")\n\n\ttestCases := []struct {\n\t\tpattern     string\n\t\tdescription string\n\t}{\n\t\t{\"AAAAA\", \"Match in first chunk\"},\n\t\t{\"JJJJJ\", \"Match in last chunk\"},\n\t\t{\"[A-Z]{5}\", \"Regex matching all groups\"},\n\t\t{\"BBBBB CCCCC\", \"Match potentially spanning chunks\"},\n\t\t{\"FFFFF GGGGG HHHHH\", \"Match spanning multiple chunks\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tdoSearch(m, tc.pattern)\n\n\t\t\tassert.Greater(t, len(m.search.results), 0, \"Should find matches for pattern: %s\", tc.pattern)\n\t\t\tassert.Nil(t, m.search.err, \"Search should not error\")\n\n\t\t\t// Verify matches are recorded in the values map\n\t\t\tassert.Greater(t, len(m.search.values), 0, \"Should have value matches recorded\")\n\t\t})\n\t}\n}\n\nfunc TestSearchMultipleMatchesInChunks(t *testing.T) {\n\t// String with repeated pattern that will span multiple chunks\n\tjsonData := `{\"data\": \"test123 test456 test789 test000 test111 test222 test333 test444\"}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\tWrap(head, 35)\n\n\tm := &model{\n\t\ttop:       head,\n\t\thead:      head,\n\t\tsearch:    newSearch(),\n\t\twrap:      true,\n\t\ttermWidth: 35,\n\t}\n\n\t// Search for pattern that should match multiple times\n\tdoSearch(m, \"test\\\\d{3}\")\n\n\tassert.Greater(t, len(m.search.results), 0, \"Should find test patterns\")\n\tassert.Nil(t, m.search.err, \"Search should not error\")\n\n\t// Check that matches are distributed across chunk nodes\n\ttotalMatches := 0\n\tfor _, matches := range m.search.values {\n\t\ttotalMatches += len(matches)\n\t}\n\tassert.Greater(t, totalMatches, 0, \"Should have matches across chunks\")\n}\n\nfunc TestSearchWrappedVsUnwrapped(t *testing.T) {\n\tjsonData := `{\"message\": \"The quick brown fox jumps over the lazy dog multiple times today\"}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\t// First test without wrapping\n\tm := &model{\n\t\ttop:       head,\n\t\thead:      head,\n\t\tsearch:    newSearch(),\n\t\twrap:      false,\n\t\ttermWidth: 80,\n\t}\n\n\tdoSearch(m, \"fox\")\n\tunwrappedResults := len(m.search.results)\n\tassert.Greater(t, unwrappedResults, 0, \"Should find 'fox' without wrapping\")\n\n\t// Now apply wrapping and search again\n\tWrap(head, 30)\n\tm.wrap = true\n\tm.termWidth = 30\n\n\tdoSearch(m, \"fox\")\n\twrappedResults := len(m.search.results)\n\n\tassert.Equal(t, unwrappedResults, wrappedResults, \"Should find same number of results with or without wrapping\")\n}\n\nfunc TestSearchChunkIndexMapping(t *testing.T) {\n\t// Test that search result indices are correctly mapped to chunks\n\tjsonData := `{\"value\": \"START middle portion of text END\"}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\t// Use very narrow width to force multiple chunks\n\tWrap(head, 20)\n\n\tm := &model{\n\t\ttop:       head,\n\t\thead:      head,\n\t\tsearch:    newSearch(),\n\t\twrap:      true,\n\t\ttermWidth: 20,\n\t}\n\n\t// Search for terms at different positions\n\ttestCases := []struct {\n\t\tterm        string\n\t\tdescription string\n\t}{\n\t\t{\"START\", \"Beginning of string\"},\n\t\t{\"middle\", \"Middle of string\"},\n\t\t{\"END\", \"End of string\"},\n\t\t{\"portion\", \"Another middle term\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tdoSearch(m, tc.term)\n\n\t\t\tassert.Greater(t, len(m.search.results), 0, \"Should find '%s'\", tc.term)\n\t\t\tassert.Nil(t, m.search.err)\n\n\t\t\t// Verify that matches are recorded in the values map\n\t\t\tassert.Greater(t, len(m.search.values), 0, \"Should have value matches recorded for '%s'\", tc.term)\n\t\t})\n\t}\n}\n\nfunc TestSearchEmptyAndShortStringsWithWrap(t *testing.T) {\n\ttestCases := []struct {\n\t\tjsonData    string\n\t\tsearchTerm  string\n\t\texpectMatch bool\n\t\tdescription string\n\t}{\n\t\t{`{\"a\": \"\"}`, \"anything\", false, \"Empty string value\"},\n\t\t{`{\"a\": \"x\"}`, \"x\", true, \"Single char string\"},\n\t\t{`{\"a\": \"ab\"}`, \"ab\", true, \"Two char string\"},\n\t\t{`{\"a\": \"short\"}`, \"short\", true, \"Short string (no wrap needed)\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\thead, err := Parse([]byte(tc.jsonData))\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Apply wrapping even for short strings\n\t\t\tWrap(head, 30)\n\n\t\t\tm := &model{\n\t\t\t\ttop:       head,\n\t\t\t\thead:      head,\n\t\t\t\tsearch:    newSearch(),\n\t\t\t\twrap:      true,\n\t\t\t\ttermWidth: 30,\n\t\t\t}\n\n\t\t\tdoSearch(m, tc.searchTerm)\n\n\t\t\tif tc.expectMatch {\n\t\t\t\tassert.Greater(t, len(m.search.results), 0, \"Should find '%s'\", tc.searchTerm)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, 0, len(m.search.results), \"Should not find '%s'\", tc.searchTerm)\n\t\t\t}\n\t\t\tassert.Nil(t, m.search.err)\n\t\t})\n\t}\n}\n\nfunc TestSearchRegexAcrossChunks(t *testing.T) {\n\t// Test regex patterns that might match across chunk boundaries\n\tjsonData := `{\"content\": \"email: user@example.com phone: 123-456-7890 date: 2024-01-15\"}`\n\n\thead, err := Parse([]byte(jsonData))\n\trequire.NoError(t, err)\n\n\tWrap(head, 25)\n\n\tm := &model{\n\t\ttop:       head,\n\t\thead:      head,\n\t\tsearch:    newSearch(),\n\t\twrap:      true,\n\t\ttermWidth: 25,\n\t}\n\n\ttestCases := []struct {\n\t\tpattern      string\n\t\texpectMinNum int\n\t\tdescription  string\n\t}{\n\t\t{`\\w+@\\w+\\.\\w+`, 1, \"Email pattern\"},\n\t\t{`\\d{3}-\\d{3}-\\d{4}`, 1, \"Phone pattern\"},\n\t\t{`\\d{4}-\\d{2}-\\d{2}`, 1, \"Date pattern\"},\n\t\t{`\\d+`, 6, \"All number sequences (123,456,7890,2024,01,15)\"},\n\t\t{`[a-z]+:`, 3, \"Labels (email:, phone:, date:)\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tdoSearch(m, tc.pattern)\n\n\t\t\tassert.GreaterOrEqual(t, len(m.search.results), tc.expectMinNum,\n\t\t\t\t\"Pattern '%s' should find at least %d matches\", tc.pattern, tc.expectMinNum)\n\t\t\tassert.Nil(t, m.search.err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "snap/snapcraft.yaml",
    "content": "name: fx\nversion: 39.2.0\nsummary: Terminal JSON viewer\ndescription: Terminal JSON viewer\nbase: core20\ngrade: stable\nconfinement: strict\narchitectures:\n  - build-on: armhf\n  - build-on: amd64\n  - build-on: arm64\n\nplugs:\n  dot-fxrc-js:\n    interface: personal-files\n    read:\n      - $HOME/.fxrc.js\n\napps:\n  fx:\n    command: bin/fx\n    plugs: [ dot-fxrc-js, home, network ]\n\nparts:\n  fx:\n    plugin: go\n    source: https://github.com/antonmedv/fx.git\n    source-type: git\n    stage-snaps:\n      - node/18/stable\n"
  },
  {
    "path": "testdata/TestCollapseRecursive.golden",
    "content": "\u001b[?25l\u001b[?2004h\r\u001b[7m{\u001b[0m\u001b[K\r\n  \"title\": \"Lorem ipsum\",\u001b[K\r\n  \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusm\r\n  od tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam\r\n  , quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo cons\r\n  equat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum d\r\n  olore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident\r\n  , sunt in culpa qui officia deserunt mollit anim id est laborum.\",\u001b[K\r\n  \"tags\": […],\u001b[K\r\n  \"year\": 3000,\u001b[K\r\n  \"funny\": true,\u001b[K\r\n  \"author\": {\"name\":\"John Doe\",…}\u001b[K\r\n}\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n                                                                             1% \u001b[80D"
  },
  {
    "path": "testdata/TestCollapseRecursiveWithSizes.golden",
    "content": "\u001b[?25l\u001b[?2004h\r\u001b[7m{\u001b[0m (6 keys)\u001b[K\r\n  \"title\": \"Lorem ipsum\",\u001b[K\r\n  \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusm\r\n  od tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam\r\n  , quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo cons\r\n  equat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum d\r\n  olore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident\r\n  , sunt in culpa qui officia deserunt mollit anim id est laborum.\",\u001b[K\r\n  \"tags\": […], (3 items)\u001b[K\r\n  \"year\": 3000,\u001b[K\r\n  \"funny\": true,\u001b[K\r\n  \"author\": {\"name\":\"John Doe\",…} (2 keys)\u001b[K\r\n}\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n                                                                             1% \u001b[80D"
  },
  {
    "path": "testdata/TestGotoLine.golden",
    "content": "\u001b[?25l\u001b[?2004h\r\u001b[90m 1\u001b[0m  {\u001b[K\r\n\u001b[90m 2\u001b[0m    \"title\": \"Lorem ipsum\",\u001b[K\r\n\u001b[90m 3\u001b[0m    \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do e\r\n      iusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad mini\r\n      m veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea \r\n      commodo consequat. Duis aute irure dolor in reprehenderit in voluptate vel\r\n      it esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cu\r\n      pidatat non proident, sunt in culpa qui officia deserunt mollit anim id es\r\n      t laborum.\",\u001b[K\r\n\u001b[90m 4\u001b[0m    \"tags\": [\u001b[K\r\n\u001b[90m 5\u001b[0m      \u001b[7m\"lorem\"\u001b[0m,\u001b[K\r\n\u001b[90m 6\u001b[0m      \"ipsum\",\u001b[K\r\n\u001b[90m 7\u001b[0m      null\u001b[K\r\n\u001b[90m 8\u001b[0m    ],\u001b[K\r\n\u001b[90m 9\u001b[0m    \"year\": 3000,\u001b[K\r\n\u001b[90m10\u001b[0m    \"funny\": true,\u001b[K\r\n\u001b[90m11\u001b[0m    \"author\": {\u001b[K\r\n\u001b[90m12\u001b[0m      \"name\": \"John Doe\",\u001b[K\r\n\u001b[90m13\u001b[0m      \"email\": \"john@doe.com\"\u001b[K\r\n\u001b[90m14\u001b[0m    }\u001b[K\r\n\u001b[90m15\u001b[0m  }\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n.tags[0]                                                                    33% \u001b[80D"
  },
  {
    "path": "testdata/TestGotoLineCollapsed.golden",
    "content": "\u001b[?25l\u001b[?2004h\r\u001b[90m 1\u001b[0m  {\u001b[K\r\n\u001b[90m 2\u001b[0m    \"title\": \"Lorem ipsum\",\u001b[K\r\n\u001b[90m 3\u001b[0m    \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do e\r\n      iusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad mini\r\n      m veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea \r\n      commodo consequat. Duis aute irure dolor in reprehenderit in voluptate vel\r\n      it esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cu\r\n      pidatat non proident, sunt in culpa qui officia deserunt mollit anim id es\r\n      t laborum.\",\u001b[K\r\n\u001b[90m 4\u001b[0m    \"tags\": [\u001b[K\r\n\u001b[90m 5\u001b[0m      \u001b[7m\"lorem\"\u001b[0m,\u001b[K\r\n\u001b[90m 6\u001b[0m      \"ipsum\",\u001b[K\r\n\u001b[90m 7\u001b[0m      null\u001b[K\r\n\u001b[90m 8\u001b[0m    ],\u001b[K\r\n\u001b[90m 9\u001b[0m    \"year\": 3000,\u001b[K\r\n\u001b[90m10\u001b[0m    \"funny\": true,\u001b[K\r\n\u001b[90m11\u001b[0m    \"author\": {\"name\":\"John Doe\",…}\u001b[K\r\n\u001b[90m15\u001b[0m  }\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n.tags[0]                                                                    33% \u001b[80D"
  },
  {
    "path": "testdata/TestGotoLineInputGreaterThanTotalLines.golden",
    "content": "\u001b[?25l\u001b[?2004h\r\u001b[90m 1\u001b[0m  {\u001b[K\r\n\u001b[90m 2\u001b[0m    \"title\": \"Lorem ipsum\",\u001b[K\r\n\u001b[90m 3\u001b[0m    \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do e\r\n      iusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad mini\r\n      m veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea \r\n      commodo consequat. Duis aute irure dolor in reprehenderit in voluptate vel\r\n      it esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cu\r\n      pidatat non proident, sunt in culpa qui officia deserunt mollit anim id es\r\n      t laborum.\",\u001b[K\r\n\u001b[90m 4\u001b[0m    \"tags\": [\u001b[K\r\n\u001b[90m 5\u001b[0m      \"lorem\",\u001b[K\r\n\u001b[90m 6\u001b[0m      \"ipsum\",\u001b[K\r\n\u001b[90m 7\u001b[0m      null\u001b[K\r\n\u001b[90m 8\u001b[0m    ],\u001b[K\r\n\u001b[90m 9\u001b[0m    \"year\": 3000,\u001b[K\r\n\u001b[90m10\u001b[0m    \"funny\": true,\u001b[K\r\n\u001b[90m11\u001b[0m    \"author\": {\u001b[K\r\n\u001b[90m12\u001b[0m      \"name\": \"John Doe\",\u001b[K\r\n\u001b[90m13\u001b[0m      \"email\": \"john@doe.com\"\u001b[K\r\n\u001b[90m14\u001b[0m    }\u001b[K\r\n\u001b[90m15\u001b[0m  \u001b[7m}\u001b[0m\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n                                                                           100% \u001b[80D"
  },
  {
    "path": "testdata/TestGotoLineInputInvalid.golden",
    "content": "\u001b[?25l\u001b[?2004h\r\u001b[90m 1\u001b[0m  {\u001b[K\r\n\u001b[90m 2\u001b[0m    \"title\": \"Lorem ipsum\",\u001b[K\r\n\u001b[90m 3\u001b[0m    \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do e\r\n      \u001b[7miusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad mini\u001b[0m\r\n      m veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea \r\n      commodo consequat. Duis aute irure dolor in reprehenderit in voluptate vel\r\n      it esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cu\r\n      pidatat non proident, sunt in culpa qui officia deserunt mollit anim id es\r\n      t laborum.\",\u001b[K\r\n\u001b[90m 4\u001b[0m    \"tags\": […],\u001b[K\r\n\u001b[90m 9\u001b[0m    \"year\": 3000,\u001b[K\r\n\u001b[90m10\u001b[0m    \"funny\": true,\u001b[K\r\n\u001b[90m11\u001b[0m    \"author\": {\"name\":\"John Doe\",…}\u001b[K\r\n\u001b[90m15\u001b[0m  }\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n.text                                                                       20% \u001b[80D"
  },
  {
    "path": "testdata/TestGotoLineInputLessThanOne.golden",
    "content": "\u001b[?25l\u001b[?2004h\r\u001b[90m 1\u001b[0m  \u001b[7m{\u001b[0m\u001b[K\r\n\u001b[90m 2\u001b[0m    \"title\": \"Lorem ipsum\",\u001b[K\r\n\u001b[90m 3\u001b[0m    \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do e\r\n      iusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad mini\r\n      m veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea \r\n      commodo consequat. Duis aute irure dolor in reprehenderit in voluptate vel\r\n      it esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cu\r\n      pidatat non proident, sunt in culpa qui officia deserunt mollit anim id es\r\n      t laborum.\",\u001b[K\r\n\u001b[90m 4\u001b[0m    \"tags\": [\u001b[K\r\n\u001b[90m 5\u001b[0m      \"lorem\",\u001b[K\r\n\u001b[90m 6\u001b[0m      \"ipsum\",\u001b[K\r\n\u001b[90m 7\u001b[0m      null\u001b[K\r\n\u001b[90m 8\u001b[0m    ],\u001b[K\r\n\u001b[90m 9\u001b[0m    \"year\": 3000,\u001b[K\r\n\u001b[90m10\u001b[0m    \"funny\": true,\u001b[K\r\n\u001b[90m11\u001b[0m    \"author\": {\u001b[K\r\n\u001b[90m12\u001b[0m      \"name\": \"John Doe\",\u001b[K\r\n\u001b[90m13\u001b[0m      \"email\": \"john@doe.com\"\u001b[K\r\n\u001b[90m14\u001b[0m    }\u001b[K\r\n\u001b[90m15\u001b[0m  }\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n                                                                             1% \u001b[80D"
  },
  {
    "path": "testdata/TestGotoLineKeepsHistory.golden",
    "content": "\u001b[?25l\u001b[?2004h\r\u001b[90m 1\u001b[0m  {\u001b[K\r\n\u001b[90m 2\u001b[0m    \"title\": \"Lorem ipsum\",\u001b[K\r\n\u001b[90m 3\u001b[0m    \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do e\r\n      iusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad mini\r\n      m veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea \r\n      commodo consequat. Duis aute irure dolor in reprehenderit in voluptate vel\r\n      it esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cu\r\n      pidatat non proident, sunt in culpa qui officia deserunt mollit anim id es\r\n      t laborum.\",\u001b[K\r\n\u001b[90m 4\u001b[0m    \u001b[7m\"tags\"\u001b[0m: [\u001b[K\r\n\u001b[90m 5\u001b[0m      \"lorem\",\u001b[K\r\n\u001b[90m 6\u001b[0m      \"ipsum\",\u001b[K\r\n\u001b[90m 7\u001b[0m      null\u001b[K\r\n\u001b[90m 8\u001b[0m    ],\u001b[K\r\n\u001b[90m 9\u001b[0m    \"year\": 3000,\u001b[K\r\n\u001b[90m10\u001b[0m    \"funny\": true,\u001b[K\r\n\u001b[90m11\u001b[0m    \"author\": {\u001b[K\r\n\u001b[90m12\u001b[0m      \"name\": \"John Doe\",\u001b[K\r\n\u001b[90m13\u001b[0m      \"email\": \"john@doe.com\"\u001b[K\r\n\u001b[90m14\u001b[0m    }\u001b[K\r\n\u001b[90m15\u001b[0m  }\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n.tags                                                                       26% \u001b[80D"
  },
  {
    "path": "testdata/TestNavigation.golden",
    "content": "\u001b[?25l\u001b[?2004h\r{\u001b[K\r\n  \"title\": \"Lorem ipsum\",\u001b[K\r\n  \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusm\r\n  \u001b[7mod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam\u001b[0m\r\n  , quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo cons\r\n  equat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum d\r\n  olore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident\r\n  , sunt in culpa qui officia deserunt mollit anim id est laborum.\",\u001b[K\r\n  \"tags\": [\u001b[K\r\n    \"lorem\",\u001b[K\r\n    \"ipsum\",\u001b[K\r\n    null\u001b[K\r\n  ],\u001b[K\r\n  \"year\": 3000,\u001b[K\r\n  \"funny\": true,\u001b[K\r\n  \"author\": {\u001b[K\r\n    \"name\": \"John Doe\",\u001b[K\r\n    \"email\": \"john@doe.com\"\u001b[K\r\n  }\u001b[K\r\n}\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n.text                                                                       20% \u001b[80D"
  },
  {
    "path": "testdata/TestOutput.golden",
    "content": "\u001b[?25l\u001b[?2004h\r\u001b[7m{\u001b[0m\u001b[K\r\n  \"title\": \"Lorem ipsum\",\u001b[K\r\n  \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusm\r\n  od tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam\r\n  , quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo cons\r\n  equat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum d\r\n  olore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident\r\n  , sunt in culpa qui officia deserunt mollit anim id est laborum.\",\u001b[K\r\n  \"tags\": [\u001b[K\r\n    \"lorem\",\u001b[K\r\n    \"ipsum\",\u001b[K\r\n    null\u001b[K\r\n  ],\u001b[K\r\n  \"year\": 3000,\u001b[K\r\n  \"funny\": true,\u001b[K\r\n  \"author\": {\u001b[K\r\n    \"name\": \"John Doe\",\u001b[K\r\n    \"email\": \"john@doe.com\"\u001b[K\r\n  }\u001b[K\r\n}\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n~\u001b[K\r\n                                                                             1% \u001b[80D"
  },
  {
    "path": "testdata/blog.json",
    "content": "{\n  \"title\": \"Lorem ipsum\",\n  \"body\": \"# Lorem Ipsum Dolor Sit Amet\\n\\n## Consectetur Adipiscing Elit\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. *Pellentesque vitae* velit ex. **Mauris** euismod pellentesque tellus sit amet mollis.\\n\\n- Lorem ipsum dolor sit amet, consectetur adipiscing elit.\\n- Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\\n- Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\\n\\n### Duis aute irure dolor in reprehenderit\\n\\n1. In voluptate velit esse cillum dolore eu fugiat nulla pariatur.\\n2. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\\n\\n#### Laboris Nisi\\n\\nUt aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\\n\\n##### Exercitationem Ullam\\n\\n```python\\n# Example Python code\\ndef hello_world():\\n    print(\\\"Hello, world!\\\")\\n```\",\n  \"tags\": [\n    \"lorem\",\n    \"ipsum\"\n  ],\n  \"author\": {\n    \"name\": \"John Doe\",\n    \"email\": \"john@doe.com\"\n  }\n}\n"
  },
  {
    "path": "testdata/example.json",
    "content": "{\n  \"title\": \"Lorem ipsum\",\n  \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\",\n  \"tags\": [\n    \"lorem\",\n    \"ipsum\",\n    null\n  ],\n  \"year\": 3000,\n  \"funny\": true,\n  \"author\": {\n    \"name\": \"John Doe\",\n    \"email\": \"john@doe.com\"\n  }\n}\n"
  },
  {
    "path": "utils.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/goccy/go-yaml\"\n\n\t\"github.com/antonmedv/fx/internal/jsonpath\"\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc lookup(names []string, defaultEditor string) string {\n\tfor _, name := range names {\n\t\tenv, ok := os.LookupEnv(name)\n\t\tif ok && env != \"\" {\n\t\t\treturn env\n\t\t}\n\t}\n\treturn defaultEditor\n}\n\nfunc open(filePath string, flagYaml, flagToml *bool) *os.File {\n\tf, err := os.Open(filePath)\n\tif err != nil {\n\t\tvar pathError *fs.PathError\n\t\tif errors.As(err, &pathError) {\n\t\t\tprintln(err.Error())\n\t\t\tos.Exit(1)\n\t\t} else {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\tfileName := path.Base(filePath)\n\thasYamlExt, _ := regexp.MatchString(`(?i)\\.ya?ml$`, fileName)\n\thasTomlExt, _ := regexp.MatchString(`(?i)\\.toml$`, fileName)\n\tif !*flagYaml && hasYamlExt {\n\t\t*flagYaml = true\n\t}\n\tif !*flagToml && hasTomlExt {\n\t\t*flagToml = true\n\t}\n\treturn f\n}\n\nfunc regexCase(code string) (string, bool) {\n\tif strings.HasSuffix(code, \"/i\") {\n\t\treturn code[:len(code)-2], true\n\t} else if strings.HasSuffix(code, \"/\") {\n\t\treturn code[:len(code)-1], false\n\t} else {\n\t\treturn code, true\n\t}\n}\n\nfunc flex(width int, a, b string) string {\n\treturn a + strings.Repeat(\" \", max(1, width-len(a)-len(b))) + b\n}\n\nfunc safeSlice(s string, start, end int) string {\n\tlength := len(s)\n\tif start > length {\n\t\tstart = length\n\t}\n\tif end > length {\n\t\tend = length\n\t}\n\tif start < 0 {\n\t\tstart = 0\n\t}\n\tif end < 0 {\n\t\tend = 0\n\t}\n\tif start > end {\n\t\tstart = end\n\t}\n\treturn s[start:end]\n}\n\nfunc parseYAML(b []byte) ([]byte, error) {\n\tvar out []byte\n\tdecoder := yaml.NewDecoder(\n\t\tbytes.NewReader(b),\n\t\tyaml.UseOrderedMap(),\n\t)\n\tfor {\n\t\tvar v any\n\t\tif err := decoder.Decode(&v); err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tj, err := yaml.MarshalWithOptions(v, yaml.JSON())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tout = append(out, j...)\n\t}\n\treturn out, nil\n}\n\nfunc isRefNode(n *jsonx.Node) (string, bool) {\n\tif n.Kind == jsonx.String && len(n.Key) == 6 && string(n.Key) == `\"$ref\"` {\n\t\tvalue, err := strconv.Unquote(n.Value)\n\t\tif err == nil {\n\t\t\t_, ok := jsonpath.ParseSchemaRef(value)\n\t\t\tif ok {\n\t\t\t\treturn value, true\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", false\n}\n"
  },
  {
    "path": "version.go",
    "content": "package main\n\nconst version = \"39.2.0\"\n"
  },
  {
    "path": "view.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/antonmedv/fx/internal/ident\"\n\t. \"github.com/antonmedv/fx/internal/jsonx\"\n\t\"github.com/antonmedv/fx/internal/theme\"\n\t\"github.com/antonmedv/fx/internal/utils\"\n)\n\nfunc (m *model) View() string {\n\tif m.suspending {\n\t\treturn \"\"\n\t}\n\n\tif m.showHelp {\n\t\treturn m.help.View()\n\t}\n\n\tif m.showPreview {\n\t\tsearchBar := m.previewSearchStatusBar()\n\t\tif searchBar != \"\" {\n\t\t\treturn m.preview.View() + \"\\n\" + searchBar\n\t\t}\n\t\tstatusBar := flex(m.termWidth, m.cursorPath(), m.fileName)\n\t\treturn m.preview.View() + \"\\n\" + theme.CurrentTheme.StatusBar(statusBar)\n\t}\n\n\tvar screen []byte\n\tprintedLines := 0\n\tn := m.head\n\n\tvar cursorLineNumber int\n\n\tfor lineNumber := 0; lineNumber < m.viewHeight(); lineNumber++ {\n\t\tif n == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif m.showLineNumbers {\n\t\t\tlineNumbersWidth := len(strconv.Itoa(m.totalLines))\n\t\t\tif n.LineNumber == 0 {\n\t\t\t\tscreen = append(screen, bytes.Repeat([]byte{' '}, lineNumbersWidth)...)\n\t\t\t} else {\n\t\t\t\tlineNumStr := fmt.Sprintf(\"%*d\", lineNumbersWidth, n.LineNumber)\n\t\t\t\tscreen = append(screen, theme.CurrentTheme.LineNumber(lineNumStr)...)\n\t\t\t}\n\t\t\tscreen = append(screen, ' ', ' ')\n\t\t}\n\n\t\tfor i := 0; i < int(n.Depth); i++ {\n\t\t\tscreen = append(screen, ident.IdentBytes...)\n\t\t}\n\n\t\tisSelected := m.cursor == lineNumber\n\t\tif isSelected {\n\t\t\tif n.LineNumber == 0 {\n\t\t\t\tcursorLineNumber = n.Parent.LineNumber\n\t\t\t} else {\n\t\t\t\tcursorLineNumber = n.LineNumber\n\t\t\t}\n\t\t}\n\t\tif !m.showCursor {\n\t\t\tisSelected = false // don't highlight the cursor while iterating search results\n\t\t}\n\n\t\tisRef := false\n\t\tisRefSelected := false\n\n\t\tif n.Key != \"\" {\n\t\t\tscreen = append(screen, m.prettyKey(n, isSelected)...)\n\t\t\tscreen = append(screen, theme.Colon...)\n\n\t\t\t_, isRef = isRefNode(n)\n\t\t\tisRefSelected = isRef && isSelected\n\t\t\tisSelected = false // don't highlight the key's value\n\t\t}\n\n\t\tscreen = append(screen, m.prettyPrint(n, isSelected, isRef)...)\n\n\t\tif n.IsCollapsed() {\n\t\t\tif n.Kind == Object {\n\t\t\t\tif n.Collapsed.Key != \"\" {\n\t\t\t\t\tscreen = append(screen, theme.CurrentTheme.Preview(n.Collapsed.Key)...)\n\t\t\t\t\tscreen = append(screen, theme.ColonPreview...)\n\t\t\t\t\tif len(n.Collapsed.Value) > 0 &&\n\t\t\t\t\t\tlen(n.Collapsed.Value) < 42 &&\n\t\t\t\t\t\tn.Collapsed.Kind != Object &&\n\t\t\t\t\t\tn.Collapsed.Kind != Array {\n\t\t\t\t\t\tscreen = append(screen, theme.CurrentTheme.Preview(n.Collapsed.Value)...)\n\t\t\t\t\t\tif n.Size > 1 {\n\t\t\t\t\t\t\tscreen = append(screen, theme.CommaPreview...)\n\t\t\t\t\t\t\tscreen = append(screen, theme.Dot3...)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tscreen = append(screen, theme.Dot3...)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tscreen = append(screen, theme.CloseCurlyBracket...)\n\t\t\t} else if n.Kind == Array {\n\t\t\t\tscreen = append(screen, theme.Dot3...)\n\t\t\t\tscreen = append(screen, theme.CloseSquareBracket...)\n\t\t\t}\n\t\t\tif n.End != nil && n.End.Comma {\n\t\t\t\tscreen = append(screen, theme.Comma...)\n\t\t\t}\n\t\t}\n\t\tif n.Comma {\n\t\t\tscreen = append(screen, theme.Comma...)\n\t\t}\n\n\t\tif m.showSizes && n.Size > 0 {\n\t\t\tvar w string\n\t\t\tif n.Size == 1 {\n\t\t\t\tif n.Kind == Array {\n\t\t\t\t\tw = \"item\"\n\t\t\t\t} else if n.Kind == Object {\n\t\t\t\t\tw = \"key\"\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif n.Kind == Array {\n\t\t\t\t\tw = \"items\"\n\t\t\t\t} else if n.Kind == Object {\n\t\t\t\t\tw = \"keys\"\n\t\t\t\t}\n\t\t\t}\n\t\t\tscreen = append(screen, theme.CurrentTheme.Size(fmt.Sprintf(\" (%d %s)\", n.Size, w))...)\n\t\t}\n\n\t\tif isRefSelected {\n\t\t\tscreen = append(screen, theme.CurrentTheme.Preview(\"  ctrl+g goto\")...)\n\t\t}\n\n\t\tscreen = append(screen, '\\n')\n\t\tprintedLines++\n\t\tn = n.Next\n\t}\n\n\tfor i := printedLines; i < m.viewHeight(); i++ {\n\t\tif m.eof {\n\t\t\tscreen = append(screen, theme.Empty...)\n\t\t}\n\t\tscreen = append(screen, '\\n')\n\t}\n\n\tif m.gotoSymbolInput.Focused() && m.fuzzyMatch != nil {\n\t\tvar matchedStr []byte\n\t\tstr := m.fuzzyMatch.Str\n\t\tfor i := 0; i < len(str); i++ {\n\t\t\tif utils.Contains(i, m.fuzzyMatch.Pos) {\n\t\t\t\tmatchedStr = append(matchedStr, theme.CurrentTheme.Search(string(str[i]))...)\n\t\t\t} else {\n\t\t\t\tmatchedStr = append(matchedStr, theme.CurrentTheme.StatusBar(string(str[i]))...)\n\t\t\t}\n\t\t}\n\t\trepeatCount := m.termWidth - len(str)\n\t\tif repeatCount > 0 {\n\t\t\tmatchedStr = append(matchedStr, theme.CurrentTheme.StatusBar(strings.Repeat(\" \", repeatCount))...)\n\t\t}\n\t\tscreen = append(screen, matchedStr...)\n\t} else {\n\t\tstatusBarWidth := m.termWidth\n\t\tvar indicator string\n\t\tif m.eof {\n\t\t\tpercent := int(float64(cursorLineNumber) / float64(m.totalLines) * 100)\n\t\t\tif cursorLineNumber == 1 {\n\t\t\t\tpercent = min(1, percent)\n\t\t\t}\n\t\t\tindicator = fmt.Sprintf(\"%d%%\", percent)\n\t\t} else {\n\t\t\tindicator = fmt.Sprintf(\" %s\", m.spinner.View())\n\t\t\tstatusBarWidth += 2 // adjust for spinner\n\t\t}\n\n\t\tinfo := fmt.Sprintf(\"%s %s\", indicator, m.fileName)\n\t\tstatusBar := flex(statusBarWidth, m.cursorPath(), info)\n\t\tscreen = append(screen, theme.CurrentTheme.StatusBar(statusBar)...)\n\t}\n\n\tif m.yank {\n\t\tscreen = append(screen, '\\n')\n\t\tscreen = append(screen, []byte(\"(y)value  (p)path  (k)key  (b)key+value\")...)\n\t} else if m.showShowSelector {\n\t\tscreen = append(screen, '\\n')\n\t\tscreen = append(screen, []byte(\"(s)sizes  (l)line numbers\")...)\n\t} else if m.gotoSymbolInput.Focused() {\n\t\tscreen = append(screen, '\\n')\n\t\tscreen = append(screen, m.gotoSymbolInput.View()...)\n\t} else if m.commandInput.Focused() {\n\t\tscreen = append(screen, '\\n')\n\t\tscreen = append(screen, m.commandInput.View()...)\n\t} else if m.searchInput.Focused() {\n\t\tscreen = append(screen, '\\n')\n\t\tscreen = append(screen, m.searchInput.View()...)\n\t} else if m.searchInput.Value() != \"\" {\n\t\tscreen = append(screen, '\\n')\n\t\tre, ci := regexCase(m.searchInput.Value())\n\t\tre = \"/\" + re + \"/\"\n\t\tif ci {\n\t\t\tre += \"i\"\n\t\t}\n\t\tif m.searching {\n\t\t\tstatus := fmt.Sprintf(\"%s searching...\", m.spinner.View())\n\t\t\tscreen = append(screen, flex(m.termWidth, re, status)...)\n\t\t} else if m.search.err != nil {\n\t\t\tscreen = append(screen, flex(m.termWidth, re, m.search.err.Error())...)\n\t\t} else if len(m.search.results) == 0 {\n\t\t\tscreen = append(screen, flex(m.termWidth, re, \"not found\")...)\n\t\t} else {\n\t\t\tcursor := fmt.Sprintf(\"found: [%v/%v]\", m.search.cursor+1, len(m.search.results))\n\t\t\tscreen = append(screen, flex(m.termWidth, re, cursor)...)\n\t\t}\n\t}\n\n\treturn string(screen)\n}\n\nfunc (m *model) centerLine(n *Node) {\n\tmiddle := m.visibleLines() / 2\n\n\tfor range middle {\n\t\tm.up()\n\t}\n\n\tm.selectNodeInView(n)\n}\n"
  },
  {
    "path": "vim.go",
    "content": "package main\n\nimport (\n\t\"strconv\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t. \"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc (m *model) runCommand(s string) (tea.Model, tea.Cmd) {\n\tnum, err := strconv.Atoi(s)\n\tif err == nil {\n\t\tgotoLine(m, num)\n\t\treturn m, nil\n\t} else if s == \"q\" {\n\t\treturn m, tea.Quit\n\t}\n\treturn m, nil\n}\n\nfunc gotoLine(m *model, num int) {\n\tm.selectNode(findNode(m, num))\n\n\tm.commandInput.SetValue(\"\")\n\tm.recordHistory()\n}\n\nfunc findNode(m *model, line int) *Node {\n\tif line >= m.totalLines {\n\t\treturn m.top.Bottom()\n\t}\n\n\tif line <= 1 {\n\t\treturn m.top\n\t}\n\n\tnode := m.top\n\n\tfor {\n\t\tif node.ChunkEnd != nil {\n\t\t\tnode = node.ChunkEnd.Next\n\t\t} else if node.Collapsed != nil {\n\t\t\tnode = node.Collapsed\n\t\t} else {\n\t\t\tnode = node.Next\n\t\t}\n\n\t\tif node == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif node.LineNumber == line {\n\t\t\treturn node\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "vim_test.go",
    "content": "package main\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/charmbracelet/x/exp/teatest\"\n\t\"github.com/muesli/termenv\"\n)\n\nfunc init() {\n\tlipgloss.SetColorProfile(termenv.ANSI)\n}\n\nfunc TestGotoLine(t *testing.T) {\n\ttm := prepare(t, options{showLineNumbers: true})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\":\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"5\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\n\tteatest.RequireEqualOutput(t, read(t, tm))\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"q\")})\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))\n}\n\nfunc TestGotoLineCollapsed(t *testing.T) {\n\ttm := prepare(t, options{showLineNumbers: true})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"E\")})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\":\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"5\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\n\tteatest.RequireEqualOutput(t, read(t, tm))\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"q\")})\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))\n}\n\nfunc TestGotoLineInputInvalid(t *testing.T) {\n\ttm := prepare(t, options{showLineNumbers: true})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"E\")})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\":\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"invalid\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\n\tteatest.RequireEqualOutput(t, read(t, tm))\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"q\")})\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))\n}\n\nfunc TestGotoLineInputGreaterThanTotalLines(t *testing.T) {\n\ttm := prepare(t, options{showLineNumbers: true})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\":\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"500\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\n\tteatest.RequireEqualOutput(t, read(t, tm))\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"q\")})\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))\n}\n\nfunc TestGotoLineInputLessThanOne(t *testing.T) {\n\ttm := prepare(t, options{showLineNumbers: true})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\":\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"-2\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\n\tteatest.RequireEqualOutput(t, read(t, tm))\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"q\")})\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))\n}\n\nfunc TestGotoLineKeepsHistory(t *testing.T) {\n\ttm := prepare(t, options{showLineNumbers: true})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyDown})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\":\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"4\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\":\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"14\")})\n\ttm.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"[\")})\n\n\tteatest.RequireEqualOutput(t, read(t, tm))\n\n\ttm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(\"q\")})\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))\n}\n"
  }
]