Showing preview only (547K chars total). Download the full file or copy to clipboard to get everything.
Repository: antonmedv/fx
Branch: master
Commit: 56d0b9be19ee
Files: 103
Total size: 518.4 KB
Directory structure:
gitextract_4yli86d0/
├── .gitattributes
├── .github/
│ ├── images/
│ │ ├── autocomplete.tape
│ │ ├── preview-mode.tape
│ │ └── preview.tape
│ ├── stream.mjs
│ └── workflows/
│ ├── brew.yml
│ ├── docker.yml
│ ├── snap.yml
│ └── test.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── RELEASE.md
├── go.mod
├── go.sum
├── help.go
├── internal/
│ ├── complete/
│ │ ├── complete.bash
│ │ ├── complete.fish
│ │ ├── complete.go
│ │ ├── complete.zsh
│ │ ├── complete_test.go
│ │ ├── prelude.js
│ │ └── utils.go
│ ├── engine/
│ │ ├── engine.go
│ │ ├── engine_test.go
│ │ ├── format_err.go
│ │ ├── fxrc.go
│ │ ├── quote.go
│ │ ├── quote_test.go
│ │ ├── slurp.go
│ │ ├── stdlib.js
│ │ ├── stdlib_test.go
│ │ ├── stringify.go
│ │ ├── transpile.go
│ │ ├── transpile_test.go
│ │ ├── utils.go
│ │ └── vm.go
│ ├── fuzzy/
│ │ ├── algo.go
│ │ ├── chars.go
│ │ ├── chars_test.go
│ │ ├── find.go
│ │ ├── fuzzy_test.go
│ │ ├── normalize.go
│ │ └── utils.go
│ ├── ident/
│ │ └── ident.go
│ ├── jsonpath/
│ │ ├── path.go
│ │ ├── path_test.go
│ │ ├── ref.go
│ │ └── ref_test.go
│ ├── jsonx/
│ │ ├── delete.go
│ │ ├── delete_test.go
│ │ ├── format_err.go
│ │ ├── json.go
│ │ ├── jsonx_test.go
│ │ ├── line.go
│ │ ├── node.go
│ │ ├── node_test.go
│ │ ├── string.go
│ │ ├── to_value.go
│ │ └── wrap.go
│ ├── pretty/
│ │ ├── inlineable.go
│ │ ├── inlineable_test.go
│ │ ├── pretty_print.go
│ │ └── pretty_print_test.go
│ ├── shlex/
│ │ ├── shlex.go
│ │ └── shlex_test.go
│ ├── theme/
│ │ └── theme.go
│ ├── toml/
│ │ ├── toml.go
│ │ └── toml_test.go
│ └── utils/
│ ├── image.go
│ ├── life.go
│ ├── utils.go
│ └── utils_test.go
├── keymap.go
├── main.go
├── main_test.go
├── npm/
│ ├── README.md
│ ├── index.js
│ ├── package.json
│ └── test.js
├── preview.go
├── scripts/
│ └── build.mjs
├── search.go
├── search_test.go
├── snap/
│ └── snapcraft.yaml
├── testdata/
│ ├── TestCollapseRecursive.golden
│ ├── TestCollapseRecursiveWithSizes.golden
│ ├── TestGotoLine.golden
│ ├── TestGotoLineCollapsed.golden
│ ├── TestGotoLineInputGreaterThanTotalLines.golden
│ ├── TestGotoLineInputInvalid.golden
│ ├── TestGotoLineInputLessThanOne.golden
│ ├── TestGotoLineKeepsHistory.golden
│ ├── TestNavigation.golden
│ ├── TestOutput.golden
│ ├── blog.json
│ └── example.json
├── utils.go
├── version.go
├── view.go
├── vim.go
└── vim_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*.golden -text
================================================
FILE: .github/images/autocomplete.tape
================================================
Output autocomplete.gif
Output autocomplete.mp4
Set Shell zsh
Set FontSize 32
Set Width 1800
Set Height 400
Set TypingSpeed 200ms
Hide
Type ". ~/.zshrc && clear"
Enter
Show
Type "fx example.json "
Sleep 1s
Type "."
Tab@500ms 2
Sleep 1s
Tab
Sleep 500ms
Tab
Sleep 500ms
Tab
Sleep 500ms
Tab
Sleep 500ms
Tab
Sleep 500ms
Tab
Sleep 1s
Type "."
Tab@500ms 2
Sleep 500ms
Tab
Sleep 2s
Enter
Sleep 2s
================================================
FILE: .github/images/preview-mode.tape
================================================
Output preview-mode.gif
Output preview-mode.mp4
Set FontSize 32
Set Width 1800
Set Height 1200
Set TypingSpeed 200ms
Hide
Type "fx testdata/blog.json"
Enter
Show
Sleep 1s
Down
Sleep 1s
Down
Sleep 1s
Type "p"
Sleep 1s
Down
Sleep 300ms
Down
Sleep 300ms
Down
Sleep 300ms
Down
Sleep 300ms
Down
Sleep 300ms
Down
Sleep 300ms
Down
Sleep 4s
Type "p"
Sleep 1s
Up
Sleep 1s
Up
================================================
FILE: .github/images/preview.tape
================================================
Output preview.gif
Output preview.mp4
Set FontSize 32
Set Width 1800
Set Height 1200
Set TypingSpeed 200ms
Hide
Type "fx testdata/example.json"
Enter
Show
Sleep 1s
Down
Sleep 1s
Down
Sleep 1s
Down 6
Sleep 1s
Left
Sleep 1s
Down
Sleep 1s
Down
Sleep 1s
Down
Sleep 1s
Left
Sleep 1s
Up 12
Sleep 1s
================================================
FILE: .github/stream.mjs
================================================
#!/usr/bin/env zx
process.on('SIGPIPE', () => process.exit(0))
process.on('SIGINT', () => process.exit(0))
process.on('SIGTERM', () => process.exit(0))
process.stdout.on('error', (err) => {
if (err.code === 'EPIPE') process.exit(0)
throw err
})
const names = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry']
const cities = ['New York', 'London', 'Tokyo', 'Paris', 'Berlin', 'Sydney', 'Toronto', 'Mumbai']
const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink', 'cyan']
const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance', 'Operations']
const skills = ['JavaScript', 'Python', 'Go', 'Rust', 'SQL', 'Docker', 'Kubernetes', 'AWS']
function randomItem(arr) {
return arr[Math.floor(Math.random() * arr.length)]
}
function randomItems(arr, min = 1, max = 3) {
const count = min + Math.floor(Math.random() * (max - min + 1))
const shuffled = [...arr].sort(() => Math.random() - 0.5)
return shuffled.slice(0, count)
}
function randomObject() {
return {
id: Math.floor(Math.random() * 1000000),
name: randomItem(names),
active: Math.random() > 0.5,
timestamp: new Date().toISOString(),
profile: {
age: 20 + Math.floor(Math.random() * 40),
city: randomItem(cities),
preferences: {
color: randomItem(colors),
notifications: Math.random() > 0.5,
theme: Math.random() > 0.5 ? 'dark' : 'light',
},
},
work: {
department: randomItem(departments),
salary: Math.floor(50000 + Math.random() * 100000),
skills: randomItems(skills, 2, 5),
},
scores: Array.from({length: 3}, () => Math.round(Math.random() * 100)),
tags: randomItems(colors, 1, 3),
}
}
const randomTexts = [
'Processing records...',
'Loading next batch',
'--- checkpoint ---',
'Fetching data from server',
'INFO: Connection stable',
'DEBUG: Buffer flushed',
'Waiting for response...',
'>> sync complete',
]
const count = parseInt(argv._[0]) || Infinity
const delay = parseInt(argv.delay) || 100
const withText = argv['with-text'] || argv.withText
for (let i = 0; i < count; i++) {
if (withText && Math.random() < 0.15) {
console.log(randomItem(randomTexts))
if (delay > 0) await sleep(delay)
}
console.log(JSON.stringify(randomObject()))
if (delay > 0 && i < count - 1) {
await sleep(delay)
}
}
================================================
FILE: .github/workflows/brew.yml
================================================
name: brew
on: [workflow_dispatch]
jobs:
brew:
runs-on: macos-latest
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
with:
test-bot: false
- name: Install Homebrew Bundler RubyGems
run: brew install-bundler-gems
- name: Configure Git user
uses: Homebrew/actions/git-user-config@master
- name: Update brew
run: brew update
- name: Bump formulae
uses: Homebrew/actions/bump-packages@master
with:
token: ${{ secrets.MY_HOMEBREW_RELEASE_GITHUB_TOKEN }}
formulae: |
fx
================================================
FILE: .github/workflows/docker.yml
================================================
name: docker
on: [workflow_dispatch]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: antonmedv
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: antonmedv/fx:latest
================================================
FILE: .github/workflows/snap.yml
================================================
name: snap
on: [workflow_dispatch]
jobs:
snap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: master
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.21
- uses: snapcore/action-build@v1
id: build
- uses: snapcore/action-publish@v1
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.MY_SNAPCRAFT_CREDENTIALS }}
with:
snap: ${{ steps.build.outputs.snap }}
release: stable
================================================
FILE: .github/workflows/test.yml
================================================
name: test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
go:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.21
- name: Test
run: go test ./...
go-arm:
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.21
- name: Test on ARM
run: go test ./...
node:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Test NPM version
run: cd npm && node test.js
================================================
FILE: .gitignore
================================================
*.prof
fx
fx.exe
================================================
FILE: Dockerfile
================================================
FROM golang:latest as builder
WORKDIR /go
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o fx .
FROM alpine
COPY --from=builder /go/fx /bin/fx
WORKDIR /data
ENV COLORTERM=truecolor
ENTRYPOINT ["/bin/fx"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Anton Medvedev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# f(x)
<p align="center"><a href="https://fx.wtf"><img src=".github/images/preview.gif" width="500" alt="fx preview"></a></p>
## Documentation
See full documentation at [fx.wtf](https://fx.wtf).
## Related
- [walk](https://github.com/antonmedv/walk) – terminal file manager
- [howto](https://github.com/antonmedv/howto) – terminal command LLM helper
- [countdown](https://github.com/antonmedv/countdown) – terminal countdown timer
## License
[MIT](LICENSE)
<p align="center">
<a href="https://crow.watch/join/fx">
<img src="https://github.com/user-attachments/assets/37c84073-6533-4746-951d-d879f90a7fd2" alt="Join Crow Watch" width="900" hight="600">
</a>
</p>
================================================
FILE: RELEASE.md
================================================
# Release
1. Bump version in [version.go](version.go).
2. Bump version in [snapcraft.yaml](snap/snapcraft.yaml).
3. Bump version in [package.json](npm/package.json).
4. Commit changes.
5. Publish npm package.
6. Trigger [GitHub Actions](https://github.com/antonmedv/fx/actions) (brew, snap, docker).
7. Create a new release on [GitHub](https://github.com/antonmedv/fx/releases/new).
8. Run [build.mjs](scripts/build.mjs) to upload binaries to the release.
```sh
npx zx scripts/build.mjs
```
9. Bump version in [install.sh](https://github.com/antonmedv/fx.wtf/blob/master/public/install.sh) and upload it
to [fx.wtf](https://fx.wtf).
================================================
FILE: go.mod
================================================
module github.com/antonmedv/fx
go 1.23.0
toolchain go1.23.6
require (
github.com/antonmedv/clipboard v1.0.1
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/x/exp/teatest v0.0.0-20231025135604-4a717d4fb812
github.com/charmbracelet/x/term v0.2.1
github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994
github.com/goccy/go-yaml v1.18.0
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-runewidth v0.0.16
github.com/muesli/termenv v0.16.0
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/pelletier/go-toml/v2 v2.2.3
github.com/rivo/uniseg v0.4.7
github.com/stretchr/testify v1.9.0
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-udiff v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/antonmedv/clipboard v1.0.1 h1:z9rRBhSKt4lDb6uNcMykUmNbspk/6v07JeiTaOfYYOY=
github.com/antonmedv/clipboard v1.0.1/go.mod h1:3jcOUCdraVHehZaOsMaJZoE92MxURt5fovC1gDAiZ2s=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/teatest v0.0.0-20231025135604-4a717d4fb812 h1:W/hU7Z+y+QsZo2qg0hwjv56qSMP12Z72DJR8k+ULbA4=
github.com/charmbracelet/x/exp/teatest v0.0.0-20231025135604-4a717d4fb812/go.mod h1:TckAxPtan3aJ5wbTgBkySpc50SZhXJRZ8PtYICnZJEw=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 h1:aQYWswi+hRL2zJqGacdCZx32XjKYV8ApXFGntw79XAM=
github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ=
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: help.go
================================================
package main
import (
"fmt"
"os"
"reflect"
"strings"
"time"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/lipgloss"
)
func usage() string {
title := lipgloss.NewStyle().Bold(true)
return fmt.Sprintf(`
%v
Terminal JSON viewer
%v
fx data.json
fx data.json .field
curl ... | fx
%v
-h, --help print help
-v, --version print version
--themes print themes
--comp <shell> print completion script
-r, --raw treat input as a raw string
-s, --slurp read all inputs into an array
--yaml parse input as YAML
--toml parse input as TOML
--strict strict mode
--no-inline disable inlining in output
--game-of-life play the game of life
%v
https://fx.wtf
%v
Anton Medvedev <anton@medv.io>
`,
title.Render("fx "+version),
title.Render("Usage"),
title.Render("Flags"),
title.Render("More info"),
title.Render("Author"),
)
}
var categoryOrder = []string{
"Navigation",
"Expand / Collapse",
"Search",
"Actions",
"View",
"Other",
}
func help(keyMap KeyMap) string {
titleStyle := lipgloss.NewStyle().
Bold(true)
categoryStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("8")).
MarginTop(1)
keyStyle := lipgloss.NewStyle()
descStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("8"))
dimStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("8"))
// Group bindings by category using struct tags
v := reflect.ValueOf(keyMap)
t := v.Type()
categories := make(map[string][]key.Binding)
for _, field := range reflect.VisibleFields(t) {
category := field.Tag.Get("category")
if category == "" {
continue
}
binding := v.FieldByName(field.Name).Interface().(key.Binding)
categories[category] = append(categories[category], binding)
}
var sb strings.Builder
// Header
sb.WriteString("\n")
sb.WriteString(titleStyle.Render(" Key Bindings"))
sb.WriteString("\n")
sb.WriteString(dimStyle.Render(" ─────────────────────────────────────────"))
sb.WriteString("\n")
for _, cat := range categoryOrder {
bindings, ok := categories[cat]
if !ok || len(bindings) == 0 {
continue
}
sb.WriteString(categoryStyle.Render(" " + cat))
sb.WriteString("\n")
for _, binding := range bindings {
keyStr := binding.Help().Key
if len(keyStr) == 0 {
keys := binding.Keys()
if len(keys) > 5 {
keyStr = fmt.Sprintf("%v-%v", keys[0], keys[len(keys)-1])
} else {
keyStr = strings.Join(keys, ", ")
}
}
desc := binding.Help().Desc
keyFormatted := keyStyle.Render(fmt.Sprintf("%-20s", keyStr))
descFormatted := descStyle.Render(desc)
sb.WriteString(fmt.Sprintf(" %s %s\n", keyFormatted, descFormatted))
}
}
sb.WriteString("\n")
sb.WriteString(dimStyle.Render(" ─────────────────────────────────────────"))
sb.WriteString("\n")
sb.WriteString(dimStyle.Render(" Press q or ? to close"))
sb.WriteString("\n")
return sb.String()
}
func exit() {
if showLetter(time.Now()) {
style := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(1, 2)
_, _ = fmt.Fprintln(os.Stderr, style.Render(`Hello, kind human. :)
This is fx speaking. I know you’re busy, and I won’t take much
of your time.
Every day, I quietly sit in your terminal, helping you explore
and shape your data. No popups, no ads, quiet, helpful work.
But today is different.
Today, I’m asking for something small in return. Just for today.
If fx has saved you time, solved a problem, or simply made your
life in the terminal a little easier, please consider supporting
the developer who made me:
https://github.com/sponsors/antonmedv
He built fx as a passion project, shared it freely with the world,
and has kept improving it—all without asking much.
Your support helps keep fx alive, maintained, and improving.
Even a small donation means a lot. It shows that you care, that
this kind of work matters.
This message only appears once, on the first Tuesday of December.
Tomorrow I’ll be silent again.
Thank you for reading. And thank you for using fx.`))
}
}
func showLetter(t time.Time) bool {
if t.Month() != time.December {
return false
}
firstOfDecember := time.Date(t.Year(), time.December, 1, 0, 0, 0, 0, t.Location())
offset := (int(time.Tuesday) - int(firstOfDecember.Weekday()) + 7) % 7
firstTuesday := firstOfDecember.AddDate(0, 0, offset)
return t.Year() == firstTuesday.Year() &&
t.Month() == firstTuesday.Month() &&
t.Day() == firstTuesday.Day()
}
================================================
FILE: internal/complete/complete.bash
================================================
complete -o filenames -C fx fx
================================================
FILE: internal/complete/complete.fish
================================================
complete --command fx --arguments '(COMP_FISH=(commandline -cp) fx)'
================================================
FILE: internal/complete/complete.go
================================================
package complete
import (
_ "embed"
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/dop251/goja"
"github.com/goccy/go-yaml"
"github.com/pelletier/go-toml/v2"
"github.com/antonmedv/fx/internal/engine"
"github.com/antonmedv/fx/internal/jsonx"
"github.com/antonmedv/fx/internal/shlex"
)
type Reply struct {
Display string
Value string
Type string // "file" for files, others optional
}
var Flags []Reply
//go:embed complete.bash
var Bash string
//go:embed complete.zsh
var Zsh string
//go:embed complete.fish
var Fish string
//go:embed prelude.js
var prelude string
func Complete() bool {
compLine, ok := os.LookupEnv("COMP_LINE")
if ok && len(os.Args) >= 3 {
doComplete(compLine, os.Args[2], false)
return true
}
compZsh, ok := os.LookupEnv("COMP_ZSH")
if ok {
doComplete(compZsh, lastWord(compZsh), true)
return true
}
compFish, ok := os.LookupEnv("COMP_FISH")
if ok {
doComplete(compFish, lastWord(compFish), false)
return true
}
return false
}
func doComplete(compLine string, compWord string, withDisplay bool) {
if strings.HasPrefix(compWord, "-") {
compReply(filterReply(Flags, compWord), withDisplay)
return
}
args, err := shlex.Split(compLine)
if err != nil {
return
}
compWord = shlex.Parse(compWord)
var flagYaml bool
var flagToml bool
for _, arg := range args {
if arg == "--yaml" {
flagYaml = true
}
if arg == "--toml" {
flagToml = true
}
}
// Remove Flags from args.
args = filterArgs(args)
isSecondArgIsFile := false
if len(args) == 0 {
return
} else if len(args) == 1 {
reply := fileComplete(compWord)
compReply(reply, withDisplay)
return
} else if len(args) == 2 {
isSecondArgIsFile = isFile(args[1])
if !isSecondArgIsFile {
reply := fileComplete(compWord)
compReply(reply, withDisplay)
return
}
} else {
isSecondArgIsFile = isFile(args[1])
}
var reply []Reply
if isSecondArgIsFile {
file := args[1]
hasYamlExt, _ := regexp.MatchString(`(?i)\.ya?ml$`, file)
hasTomlExt, _ := regexp.MatchString(`(?i)\.toml$`, file)
if !flagYaml && hasYamlExt {
flagYaml = true
}
if !flagToml && hasTomlExt {
flagToml = true
}
if strings.HasPrefix(file, "~") {
home, err := os.UserHomeDir()
if err == nil {
file = filepath.Join(home, file[1:])
}
}
resultCh := make(chan []Reply, 1)
go func() {
input, err := os.ReadFile(file)
if err != nil {
resultCh <- []Reply{}
return
}
if flagYaml {
input, err = yaml.YAMLToJSON(input)
if err != nil {
resultCh <- []Reply{}
return
}
} else if flagToml {
var v any
if err := toml.Unmarshal(input, &v); err != nil {
resultCh <- []Reply{}
return
}
b, err := json.Marshal(v)
if err != nil {
resultCh <- []Reply{}
return
}
input = b
}
node, err := jsonx.Parse(input)
if err != nil {
resultCh <- []Reply{}
return
}
resultCh <- KeysComplete(node, args, compWord)
}()
select {
case result := <-resultCh:
reply = append(reply, result...)
case <-time.After(3 * time.Second):
return
}
}
reply = filterReply(reply, compWord)
if len(reply) > 0 {
compReply(reply, withDisplay)
return
}
if len(compWord) > 0 {
// Only show globals if compWord is not empty,
// as we do not want to be very verbose and show all globals.
compReply(filterReply(globalsComplete(), compWord), withDisplay)
}
}
func globalsComplete() []Reply {
var code strings.Builder
code.WriteString(prelude)
code.WriteString(engine.Stdlib)
code.WriteString("\n__autocomplete()\n")
vm := goja.New()
value, err := vm.RunString(code.String())
if err != nil {
return nil
}
if array, ok := value.Export().([]any); ok {
var reply []Reply
for _, key := range array {
reply = append(reply, Reply{
Display: key.(string),
Value: key.(string),
Type: "global",
})
}
return reply
}
return nil
}
func KeysComplete(input *jsonx.Node, args []string, compWord string) []Reply {
args = args[2:] // Drop binary & file from the args.
if compWord == "" {
args = append(args, ".__keys()")
} else {
if len(args) > 0 {
last := args[len(args)-1]
last = dropTail(args[len(args)-1])
last = last + ".__keys()"
last = balanceBrackets(last)
args[len(args)-1] = last
}
}
var code strings.Builder
code.WriteString(prelude)
code.WriteString(engine.Stdlib)
code.WriteString(engine.JS(args))
code.WriteString("\n__main__(json)\n__keys\n")
vm := goja.New()
if err := vm.Set("json", input.ToValue(vm)); err != nil {
return nil
}
value, err := vm.RunString(code.String())
if err != nil {
return nil
}
if array, ok := value.Export().([]interface{}); ok {
prefix := dropTail(compWord)
var reply []Reply
for _, key := range array {
k := key.(string)
reply = append(reply, Reply{
Display: join("", k),
Value: join(prefix, k),
Type: "key",
})
}
return reply
}
return nil
}
var alphaRe = regexp.MustCompile(`^[A-Za-z_$][A-Za-z0-9_$]*$`)
func join(prefix, key string) string {
if alphaRe.MatchString(key) {
return prefix + "." + key
} else {
if prefix == "" {
return fmt.Sprintf(".[%q]", key)
}
return fmt.Sprintf("%s[%q]", prefix, key)
}
}
func filterArgs(args []string) []string {
filtered := make([]string, 0, len(args))
for _, arg := range args {
found := false
for _, flag := range Flags {
if arg == flag.Value {
found = true
break
}
}
if !found {
filtered = append(filtered, arg)
}
}
return filtered
}
func fileComplete(compWord string) []Reply {
original := compWord
// Step 1: Expand ~ to home directory
if strings.HasPrefix(compWord, "~") {
if compWord == "~" || strings.HasPrefix(compWord, "~/") {
home, err := os.UserHomeDir()
if err == nil {
compWord = filepath.Join(home, compWord[1:])
}
} else {
// We don't support ~username completion
return nil
}
}
// Step 2: If compWord ends in "/", treat it as a directory and add a "*" pattern
info, err := os.Stat(compWord)
if err == nil && info.IsDir() && !strings.HasSuffix(compWord, "*") {
compWord = filepath.Join(compWord, "*")
} else if !strings.HasSuffix(compWord, "*") {
// Add wildcard if not already present
compWord = compWord + "*"
}
// Step 3: Perform globbing
files, err := filepath.Glob(compWord)
if err != nil {
return nil
}
// Step 4: Format matches
var matches []Reply
for _, match := range files {
if match == "." || match == ".." {
continue
}
var suggestion string
if strings.HasPrefix(original, "~") {
home, _ := os.UserHomeDir()
if strings.HasPrefix(match, home) {
suggestion = "~" + strings.TrimPrefix(match, home)
} else {
suggestion = match
}
} else if filepath.IsAbs(original) {
suggestion = match
} else {
rel, err := filepath.Rel(".", match)
if err != nil {
continue
}
suggestion = rel
}
dirSuffix := ""
info, err := os.Stat(match)
if err == nil {
if info.IsDir() {
dirSuffix = "/"
}
}
matches = append(matches, Reply{
Display: filepath.Base(suggestion) + dirSuffix,
Value: suggestion,
Type: "file",
})
}
return matches
}
================================================
FILE: internal/complete/complete.zsh
================================================
#compdef fx
_fx() {
local -a reply
reply=("${(@f)$(COMP_ZSH="${LBUFFER}" fx)}")
if (( ${#reply} )); then
local -a insert_files display_files insert_other display_other
local line display rest value typ
for line in "${reply[@]}"; do
display="${line%%$'\t'*}"
rest="${line#*$'\t'}"
value="${rest%%$'\t'*}"
typ="${rest#*$'\t'}"
if [[ "$typ" == "file" ]]; then
display_files+=("$display")
insert_files+=("$value")
else
display_other+=("$display")
insert_other+=("$value")
fi
done
if (( ${#insert_files} )); then
compadd -f -d display_files -a insert_files
fi
if (( ${#insert_other} )); then
compadd -S '' -d display_other -a insert_other
fi
fi
}
if [ "$funcstack[1]" = "_fx" ]; then
_fx "$@"
else
compdef _fx fx
fi
================================================
FILE: internal/complete/complete_test.go
================================================
package complete
import (
"testing"
"github.com/antonmedv/fx/internal/jsonx"
)
func TestKeysComplete(t *testing.T) {
tests := []struct {
name string
json string
args []string
compWord string
want []string
}{
{
name: "simple object keys with empty compWord",
json: `{"foo": 1, "bar": 2, "baz": 3}`,
args: []string{"fx", "file.json"},
compWord: "",
want: []string{".foo", ".bar", ".baz"},
},
{
name: "nested object keys with trailing dot",
json: `{"outer": {"inner1": 1, "inner2": 2}}`,
args: []string{"fx", "file.json", ".outer."},
compWord: ".outer.",
want: []string{".outer.inner1", ".outer.inner2"},
},
{
name: "nested object without trailing dot returns root keys",
json: `{"outer": {"inner1": 1, "inner2": 2}}`,
args: []string{"fx", "file.json", ".outer"},
compWord: ".outer",
want: []string{".outer"},
},
{
name: "nested object with partial compWord",
json: `{"data": {"name": "test", "value": 42}}`,
args: []string{"fx", "file.json", ".data."},
compWord: ".data.",
want: []string{".data.name", ".data.value"},
},
{
name: "empty object",
json: `{}`,
args: []string{"fx", "file.json"},
compWord: "",
want: nil,
},
{
name: "key with special characters",
json: `{"normal": 1, "with-dash": 2, "with space": 3}`,
args: []string{"fx", "file.json"},
compWord: "",
want: []string{".normal", ".[\"with-dash\"]", ".[\"with space\"]"},
},
{
name: "deeply nested with trailing dot",
json: `{"a": {"b": {"c": {"d": 1}}}}`,
args: []string{"fx", "file.json", ".a.b.c."},
compWord: ".a.b.c.",
want: []string{".a.b.c.d"},
},
{
name: "deeply nested without trailing dot returns parent keys",
json: `{"a": {"b": {"c": {"d": 1}}}}`,
args: []string{"fx", "file.json", ".a.b.c"},
compWord: ".a.b.c",
want: []string{".a.b.c"},
},
{
name: "array returns no keys",
json: `[1, 2, 3]`,
args: []string{"fx", "file.json"},
compWord: "",
want: nil,
},
{
name: "primitive value returns no keys",
json: `"hello"`,
args: []string{"fx", "file.json"},
compWord: "",
want: nil,
},
{
name: "numeric keys use bracket notation",
json: `{"123": "numeric", "abc": "alpha"}`,
args: []string{"fx", "file.json"},
compWord: "",
want: []string{".[\"123\"]", ".abc"},
},
{
name: "key starting with underscore",
json: `{"_private": 1, "public": 2}`,
args: []string{"fx", "file.json"},
compWord: "",
want: []string{"._private", ".public"},
},
{
name: "key starting with dollar",
json: `{"$ref": "#/defs", "name": "test"}`,
args: []string{"fx", "file.json"},
compWord: "",
want: []string{".$ref", ".name"},
},
{
name: "object inside array via bracket access with trailing dot",
json: `{"items": [{"x": 1, "y": 2}]}`,
args: []string{"fx", "file.json", ".items[0]."},
compWord: ".items[0].",
want: []string{".items[0].x", ".items[0].y"},
},
{
name: "multiple args combines path",
json: `{"a": {"b": {"c": 1}}}`,
args: []string{"fx", "file.json", ".a", ".b."},
compWord: ".b.",
want: []string{".b.c"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
node, err := jsonx.Parse([]byte(tt.json))
if err != nil {
t.Fatalf("failed to parse JSON: %v", err)
}
got := KeysComplete(node, tt.args, tt.compWord)
if len(got) != len(tt.want) {
t.Errorf("KeysComplete() returned %d replies, want %d", len(got), len(tt.want))
t.Errorf("got: %v", replyValues(got))
t.Errorf("want: %v", tt.want)
return
}
gotValues := replyValues(got)
for i, want := range tt.want {
if gotValues[i] != want {
t.Errorf("KeysComplete()[%d].Value = %q, want %q", i, gotValues[i], want)
}
}
})
}
}
func replyValues(replies []Reply) []string {
values := make([]string, len(replies))
for i, r := range replies {
values[i] = r.Value
}
return values
}
func TestKeysComplete_Display(t *testing.T) {
json := `{"foo": 1, "bar": 2}`
node, err := jsonx.Parse([]byte(json))
if err != nil {
t.Fatalf("failed to parse JSON: %v", err)
}
got := KeysComplete(node, []string{"fx", "file.json"}, "")
for _, r := range got {
if r.Type != "key" {
t.Errorf("expected Type to be 'key', got %q", r.Type)
}
if r.Display == "" {
t.Error("expected Display to be non-empty")
}
}
}
func TestKeysComplete_DisplayVsValue(t *testing.T) {
tests := []struct {
name string
json string
args []string
compWord string
wantDisplay string
wantValue string
}{
{
name: "display shows key with dot prefix, value shows full path",
json: `{"outer": {"inner": 1}}`,
args: []string{"fx", "file.json", ".outer."},
compWord: ".outer.",
wantDisplay: ".inner",
wantValue: ".outer.inner",
},
{
name: "special key display and value both use bracket notation",
json: `{"key-dash": 1}`,
args: []string{"fx", "file.json"},
compWord: "",
wantDisplay: ".[\"key-dash\"]",
wantValue: ".[\"key-dash\"]",
},
{
name: "regular key at root",
json: `{"foo": 1}`,
args: []string{"fx", "file.json"},
compWord: "",
wantDisplay: ".foo",
wantValue: ".foo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
node, err := jsonx.Parse([]byte(tt.json))
if err != nil {
t.Fatalf("failed to parse JSON: %v", err)
}
got := KeysComplete(node, tt.args, tt.compWord)
if len(got) == 0 {
t.Fatal("expected at least one reply")
}
if got[0].Display != tt.wantDisplay {
t.Errorf("Display = %q, want %q", got[0].Display, tt.wantDisplay)
}
if got[0].Value != tt.wantValue {
t.Errorf("Value = %q, want %q", got[0].Value, tt.wantValue)
}
})
}
}
================================================
FILE: internal/complete/prelude.js
================================================
const __keys = new Set()
Object.prototype.__keys = function () {
if (Array.isArray(this)) return
if (typeof this === 'string') return
if (this instanceof String) return
if (this === globalThis) return
if (typeof this === 'object' && this !== null)
Object.keys(this).forEach(x => __keys.add(x))
}
function __autocomplete() {
const keys = []
for (const key of Object.keys(globalThis)) {
if (key.startsWith('__')) continue
keys.push(key)
}
keys.push(
'JSON.stringify',
'JSON.parse',
'YAML.stringify',
'YAML.parse',
'Object.keys',
'Object.values',
'Object.entries',
'Object.fromEntries',
'Array.isArray',
'Array.from',
'console.log',
)
return keys
}
================================================
FILE: internal/complete/utils.go
================================================
package complete
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func compReply(reply []Reply, withDisplay bool) {
var lines []string
for _, line := range reply {
if withDisplay {
lines = append(lines, fmt.Sprintf("%s\t%s\t%s", line.Display, line.Value, line.Type))
} else {
lines = append(lines, line.Value)
}
}
fmt.Print(strings.Join(lines, "\n"))
}
func filterReply(reply []Reply, compWord string) []Reply {
var filtered []Reply
for _, word := range reply {
if strings.HasPrefix(word.Value, compWord) {
filtered = append(filtered, word)
}
}
return filtered
}
func isFile(path string) bool {
if strings.HasPrefix(path, "~") {
home, err := os.UserHomeDir()
if err == nil {
path = filepath.Join(home, path[1:])
}
}
info, err := os.Stat(path)
if err != nil {
return false
}
return !info.IsDir()
}
func dropTail(s string) string {
parts := strings.Split(s, ".")
if len(parts) == 1 {
return s
}
return strings.Join(parts[:len(parts)-1], ".")
}
func balanceBrackets(code string) string {
var stack []rune
brackets := map[rune]rune{')': '(', '}': '{', ']': '['}
reverseBrackets := map[rune]rune{'(': ')', '{': '}', '[': ']'}
for _, char := range code {
switch char {
case '(', '{', '[':
stack = append(stack, char)
case ')', '}', ']':
if len(stack) > 0 && brackets[char] == stack[len(stack)-1] {
stack = stack[:len(stack)-1] // Pop
}
}
}
for i := len(stack) - 1; i >= 0; i-- {
code += string(reverseBrackets[stack[i]])
}
return code
}
func lastWord(line string) string {
words := strings.Split(line, " ")
var s string
if len(words) > 0 {
s = words[len(words)-1]
}
return s
}
func writeLog(args ...interface{}) {
file, err := os.OpenFile("complete.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return
}
_, _ = fmt.Fprintln(file, args...)
_ = file.Close()
}
================================================
FILE: internal/engine/engine.go
================================================
package engine
import (
_ "embed"
"io"
"reflect"
"strconv"
"strings"
"github.com/dop251/goja"
"github.com/antonmedv/fx/internal/jsonx"
"github.com/antonmedv/fx/internal/pretty"
)
//go:embed stdlib.js
var Stdlib string
func init() {
fxrc, err := readFxrc()
if err != nil {
panic(err)
}
Stdlib += fxrc
}
type Parser interface {
Parse() (*jsonx.Node, error)
Recover() *jsonx.Node
}
type Options struct {
Slurp bool
WithInline bool
WriteOut func(string)
WriteErr func(string)
}
func Start(parser Parser, args []string, opts Options) int {
if opts.Slurp {
var ok bool
parser, ok = Slurp(parser, opts.WriteErr)
if !ok {
return 1
}
}
isPrettyPrintArg := len(args) == 1 && (args[0] == "." || args[0] == "this" || args[0] == "x")
// Fast path.
if isPrettyPrintArg {
for {
node, err := parser.Parse()
if err != nil {
if err == io.EOF {
break
}
opts.WriteErr(err.Error())
return 1
}
if node.Kind == jsonx.String {
unquoted, err := strconv.Unquote(node.Value)
if err != nil {
panic(err)
}
opts.WriteOut(unquoted)
} else {
opts.WriteOut(pretty.Print(node, opts.WithInline))
}
}
return 0
}
for i := range args {
if err := validateSyntax(args, i); err != nil {
jsCode := transpile(args[i])
snippet := formatErr(args, i, jsCode)
message := errorToString(err)
opts.WriteErr(snippet + message)
return 1
}
}
var code strings.Builder
code.WriteString(Stdlib)
code.WriteString(JS(args))
vm := NewVM(opts.WriteOut)
if _, err := vm.RunString(code.String()); err != nil {
opts.WriteErr(errorToString(err))
return 1
}
skip := vm.Get("skip")
undefined := vm.Get("undefined")
main, _ := goja.AssertFunction(vm.Get("__main__"))
echo := func(output goja.Value) {
rtype := output.ExportType()
if output.StrictEquals(undefined) {
opts.WriteErr("undefined")
} else if rtype != nil && rtype.Kind() == reflect.String {
opts.WriteOut(output.String())
} else {
jsonOut := Stringify(output, vm, 0)
nodeOut, err := jsonx.Parse([]byte(jsonOut))
if err != nil {
panic(err)
}
opts.WriteOut(pretty.Print(nodeOut, opts.WithInline))
}
}
for {
node, err := parser.Parse()
if err != nil {
if err == io.EOF {
break
}
opts.WriteErr(err.Error())
return 1
}
input := node.ToValue(vm)
output, exitCode, err := callMain(main, input)
if exitCode >= 0 {
return exitCode
}
if err != nil {
opts.WriteErr(errorToString(err))
return 1
}
if output.StrictEquals(skip) {
continue
}
echo(output)
}
return 0
}
func callMain(main goja.Callable, input goja.Value) (output goja.Value, exitCode int, err error) {
exitCode = -1
defer func() {
if r := recover(); r != nil {
if e, ok := r.(ExitError); ok {
exitCode = e.Code
} else {
panic(r)
}
}
}()
output, err = main(goja.Undefined(), input)
return
}
func validateSyntax(args []string, i int) error {
var code strings.Builder
code.WriteString("\nfunction __main__(json) {\n")
code.WriteString(Body(args, i))
code.WriteString(" return json\n}\n")
vm := goja.New()
_, err := vm.RunString(code.String())
return err
}
================================================
FILE: internal/engine/engine_test.go
================================================
package engine_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/antonmedv/fx/internal/engine"
"github.com/antonmedv/fx/internal/jsonx"
)
func TestEngine(t *testing.T) {
tests := []struct {
name string
input string
args []string
expects []string
errCount int
}{
{
name: "fast path: string as raw",
input: `"Hello, world!"`,
args: []string{"."},
expects: []string{"Hello, world!"},
errCount: 0,
},
{
name: "string as raw",
input: `"Hello, world!"`,
args: []string{"x => this"},
expects: []string{"Hello, world!"},
errCount: 0,
},
{
name: "skip works",
input: "1 2 3 4",
args: []string{"x % 2 != 0 ? skip : x"},
expects: []string{"2", "4"},
errCount: 0,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
parser := jsonx.NewJsonParser(strings.NewReader(tc.input), false)
var outs, errs []string
writeOut := func(s string) { outs = append(outs, s) }
writeErr := func(s string) { errs = append(errs, s) }
opts := engine.Options{
Slurp: false,
WithInline: false,
WriteOut: writeOut,
WriteErr: writeErr,
}
exitCode := engine.Start(parser, tc.args, opts)
assert.Equal(t, 0, exitCode)
assert.Len(t, errs, tc.errCount, "%s: unexpected error count", tc.name)
assert.Equal(t, tc.expects, outs, "%s: outputs mismatch", tc.name)
})
}
}
func TestStart_InvalidJSON(t *testing.T) {
input := `{"unclosed": 1`
parser := jsonx.NewJsonParser(strings.NewReader(input), false)
var outs, errs []string
writeOut := func(s string) { outs = append(outs, s) }
writeErr := func(s string) { errs = append(errs, s) }
opts := engine.Options{
Slurp: false,
WithInline: false,
WriteOut: writeOut,
WriteErr: writeErr,
}
exitCode := engine.Start(parser, []string{".unclosed + '!'"}, opts)
assert.Equal(t, 1, exitCode)
assert.Len(t, errs, 1, "Expected one error message")
}
func TestStart_FastPath_InvalidJSON(t *testing.T) {
input := `{"unclosed": 1`
parser := jsonx.NewJsonParser(strings.NewReader(input), false)
var outs, errs []string
writeOut := func(s string) { outs = append(outs, s) }
writeErr := func(s string) { errs = append(errs, s) }
opts := engine.Options{
Slurp: false,
WithInline: false,
WriteOut: writeOut,
WriteErr: writeErr,
}
exitCode := engine.Start(parser, []string{"."}, opts)
assert.Equal(t, 1, exitCode)
assert.Len(t, errs, 1, "Expected one error message")
}
func TestStart_EscapeSequences(t *testing.T) {
input := `{"emoji": "\ud83d\ude80"}`
parser := jsonx.NewJsonParser(strings.NewReader(input), false)
var outs, errs []string
writeOut := func(s string) { outs = append(outs, s) }
writeErr := func(s string) { errs = append(errs, s) }
opts := engine.Options{
Slurp: false,
WithInline: false,
WriteOut: writeOut,
WriteErr: writeErr,
}
exitCode := engine.Start(parser, []string{".emoji"}, opts)
assert.Equal(t, 0, exitCode)
assert.Len(t, errs, 0, "Expected no error messages")
assert.Equal(t, "🚀", outs[0])
}
func TestStart_EscapeSequences_in_key(t *testing.T) {
input := `{"\ud83d\ude80": "\ud83d\ude80"}`
parser := jsonx.NewJsonParser(strings.NewReader(input), false)
var outs, errs []string
writeOut := func(s string) { outs = append(outs, s) }
writeErr := func(s string) { errs = append(errs, s) }
opts := engine.Options{
Slurp: false,
WithInline: false,
WriteOut: writeOut,
WriteErr: writeErr,
}
exitCode := engine.Start(parser, []string{"x => x"}, opts)
assert.Equal(t, 0, exitCode)
assert.Len(t, errs, 0, "Expected no error messages")
}
================================================
FILE: internal/engine/format_err.go
================================================
package engine
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/x/term"
"github.com/mattn/go-runewidth"
)
func formatErr(args []string, i int, jsCode string) string {
width, _, err := term.GetSize(os.Stdout.Fd())
if err != nil || width <= 0 {
width = 80
}
if i < 0 || i >= len(args) {
return fmt.Sprintf("Invalid argument index: %d", i)
}
code := args[i]
const indentCols = 2 // we print " " before everything
const sepCols = 1 // the single space between pre and code
reserve := indentCols + sepCols + runewidth.StringWidth(code) + sepCols
available := width - reserve
if available < 0 {
available = 0
}
maxCtx := available / 2
pre := strings.Join(args[:i], " ")
post := strings.Join(args[i+1:], " ")
pre = trimLeft(pre, maxCtx)
post = trimRight(post, maxCtx)
leftSep := 0
if pre != "" {
leftSep = sepCols
}
spacerCols := indentCols + runewidth.StringWidth(pre) + leftSep
spacer := strings.Repeat(" ", spacerCols)
var sb strings.Builder
sb.WriteString("\n")
sb.WriteString(strings.Repeat(" ", indentCols))
if pre != "" {
sb.WriteString(pre)
sb.WriteByte(' ')
}
sb.WriteString(code)
if post != "" {
sb.WriteByte(' ')
sb.WriteString(post)
}
sb.WriteByte('\n')
sb.WriteString(spacer)
sb.WriteString(strings.Repeat("^", runewidth.StringWidth(code)))
sb.WriteByte('\n')
if jsCode != "" && jsCode != code {
snippet := jsCode
if runewidth.StringWidth(snippet) > width {
snippet = trimRight(snippet, width)
}
sb.WriteByte('\n')
sb.WriteString(snippet)
sb.WriteByte('\n')
}
sb.WriteString("\n")
return sb.String()
}
func trimLeft(s string, ctx int) string {
if runewidth.StringWidth(s) <= ctx {
return s
}
rs := []rune(s)
widthAccum := 0
var out []rune
for i := len(rs) - 1; i >= 0; i-- {
w := runewidth.RuneWidth(rs[i])
if widthAccum+w > ctx-1 {
break
}
widthAccum += w
out = append([]rune{rs[i]}, out...)
}
return "…" + string(out)
}
func trimRight(s string, ctx int) string {
if runewidth.StringWidth(s) <= ctx {
return s
}
rs := []rune(s)
widthAccum := 0
var out []rune
for _, r := range rs {
w := runewidth.RuneWidth(r)
if widthAccum+w > ctx-1 {
break
}
out = append(out, r)
widthAccum += w
}
return string(out) + "…"
}
================================================
FILE: internal/engine/fxrc.go
================================================
package engine
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func readFxrc() (string, error) {
var builder strings.Builder
// Determine search paths
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("get cwd: %w", err)
}
paths := []string{filepath.Join(cwd, ".fxrc.js")}
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("get home: %w", err)
}
paths = append(paths, filepath.Join(home, ".fxrc.js"))
xdgHome := os.Getenv("XDG_CONFIG_HOME")
if xdgHome == "" {
xdgHome = filepath.Join(home, ".config")
}
paths = append(paths, filepath.Join(xdgHome, "fx", ".fxrc.js"))
xdgDirs := os.Getenv("XDG_CONFIG_DIRS")
if xdgDirs == "" {
xdgDirs = "/etc/xdg"
}
for _, dir := range strings.Split(xdgDirs, ":") {
paths = append(paths, filepath.Join(dir, "fx", ".fxrc.js"))
}
// Read and combine
for _, path := range uniq(paths) {
info, err := os.Stat(path)
if err != nil || info.IsDir() {
continue // skip missing or directories
}
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read %s: %w", path, err)
}
builder.Write(data)
builder.WriteString("\n")
}
return builder.String(), nil
}
func uniq(paths []string) []string {
seen := make(map[string]bool)
result := []string{}
for _, path := range paths {
if !seen[path] {
seen[path] = true
result = append(result, path)
}
}
return result
}
================================================
FILE: internal/engine/quote.go
================================================
package engine
import (
"fmt"
"strings"
"unicode/utf8"
)
func Quote(s string) string {
var err error
var b strings.Builder
b.WriteByte('"')
for i := 0; i < len(s); {
r, width := utf8.DecodeRuneInString(s[i:])
switch r {
case '"':
b.WriteString(`\"`)
case '\\':
b.WriteString(`\\`)
case '\b':
b.WriteString(`\b`)
case '\f':
b.WriteString(`\f`)
case '\n':
b.WriteString(`\n`)
case '\r':
b.WriteString(`\r`)
case '\t':
b.WriteString(`\t`)
default:
if r < 0x20 || r == 0x7F {
// Control characters must be escaped as \uXXXX
_, err = fmt.Fprintf(&b, `\u%04x`, r)
if err != nil {
panic(err)
}
} else if r == utf8.RuneError && width == 1 {
// Invalid UTF-8 sequence - escape the byte
_, err = fmt.Fprintf(&b, `\u%04x`, s[i])
if err != nil {
panic(err)
}
} else {
// Regular character - write as-is
b.WriteRune(r)
}
}
i += width
}
b.WriteByte('"')
return b.String()
}
================================================
FILE: internal/engine/quote_test.go
================================================
package engine_test
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/antonmedv/fx/internal/engine"
)
func TestQuote_BasicASCII(t *testing.T) {
assert.Equal(t, "\"hello\"", engine.Quote("hello"))
assert.Equal(t, "\"\"", engine.Quote(""))
assert.Equal(t, "\"Hello, world!\"", engine.Quote("Hello, world!"))
}
func TestQuote_EscapesSpecialCharacters(t *testing.T) {
assert.Equal(t, `"\""`, engine.Quote("\""))
assert.Equal(t, `"\\"`, engine.Quote("\\"))
assert.Equal(t, `"\b"`, engine.Quote("\b"))
assert.Equal(t, `"\f"`, engine.Quote("\f"))
assert.Equal(t, `"\n"`, engine.Quote("\n"))
assert.Equal(t, `"\r"`, engine.Quote("\r"))
assert.Equal(t, `"\t"`, engine.Quote("\t"))
}
func TestQuote_ControlCharactersAndDEL(t *testing.T) {
hex4Lower := func(n int) string {
const hexdigits = "0123456789abcdef"
b0 := hexdigits[(n>>12)&0xF]
b1 := hexdigits[(n>>8)&0xF]
b2 := hexdigits[(n>>4)&0xF]
b3 := hexdigits[n&0xF]
return string([]byte{b0, b1, b2, b3})
}
// 0x00 .. 0x1F should be \uXXXX
for b := 0; b < 0x20; b++ {
s := string([]byte{byte(b)})
q := engine.Quote(s)
expected := "\"\\u" + hex4Lower(b) + "\""
// For those with dedicated escapes, engine.Quote uses short escapes; both are valid.
// We'll accept either short escape or \uXXXX for those particular bytes.
switch b {
case '\b':
assert.Equal(t, `"\b"`, q)
case '\f':
assert.Equal(t, `"\f"`, q)
case '\n':
assert.Equal(t, `"\n"`, q)
case '\r':
assert.Equal(t, `"\r"`, q)
case '\t':
assert.Equal(t, `"\t"`, q)
default:
assert.Equal(t, expected, q, "byte %d", b)
}
}
// 0x7F DEL
assert.Equal(t, `"\u007f"`, engine.Quote(string([]byte{0x7F})))
}
func TestQuote_BMP_Characters_AsIs(t *testing.T) {
// Latin-1 supplement, Cyrillic, CJK BMP characters should appear as-is
assert.Equal(t, "\"café\"", engine.Quote("café"))
assert.Equal(t, "\"Привет\"", engine.Quote("Привет"))
assert.Equal(t, "\"漢字\"", engine.Quote("漢字"))
}
func TestQuote_SurrogatePairs_AsIs(t *testing.T) {
assert.Equal(t, `"🚀"`, engine.Quote("🚀"))
assert.Equal(t, `"👍🏻"`, engine.Quote("👍🏻"))
assert.Equal(t, `"𝄞"`, engine.Quote("𝄞"))
}
func TestQuote_InvalidUTF8BytesAreEscaped(t *testing.T) {
// Construct a string with invalid UTF-8 byte 0xFF and 0xC0 (overlong lead)
s := string([]byte{'A', 0xFF, 'B', 0xC0, 'C'})
got := engine.Quote(s)
// Expect bytes to be escaped as \u00xx in lowercase hex
want := `"A\u00ffB\u00c0C"`
assert.Equal(t, want, got)
}
func TestQuote_JSONRoundTrip_ValidUTF8(t *testing.T) {
tests := []struct{ input string }{
{""},
{"simple"},
{"line\nfeed"},
{"tab\tchar"},
{"quote \" here"},
{"backslash \\"},
{"café"},
{"Привет"},
{"漢字"},
{"emoji 🚀"},
{"mix: \b\f\n\r\t and \u007F:" + string([]byte{0x7F})},
{"Line1\n\t\"Quote\" and backslash \\ and DEL:" + string([]byte{0x7F}) + " and emoji 🚀"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
q := engine.Quote(tt.input)
var v string
err := json.Unmarshal([]byte(q), &v)
assert.NoError(t, err, "failed to unmarshal: %q", q)
assert.Equal(t, tt.input, v)
})
}
}
================================================
FILE: internal/engine/slurp.go
================================================
package engine
import (
"io"
"github.com/antonmedv/fx/internal/jsonx"
)
func Slurp(parser Parser, writeErr func(string)) (Parser, bool) {
arr := &jsonx.Node{
Kind: jsonx.Array,
Value: "[",
LineNumber: 1,
}
end := arr
for {
node, err := parser.Parse()
if err != nil {
if err == io.EOF {
break
}
writeErr(err.Error())
return nil, false
}
node.Parent = arr
it := node
for it != nil {
it.Depth++
it.LineNumber++
it = it.Next
}
end.Next = node
end = node.Bottom()
end.Comma = true
}
end.Comma = false
end.Next = &jsonx.Node{
Kind: jsonx.Array,
LineNumber: end.LineNumber + 1,
Value: "]",
}
arr.End = end.Next
return &slurpParser{node: arr}, true
}
type slurpParser struct {
node *jsonx.Node
}
func (p *slurpParser) Parse() (*jsonx.Node, error) {
if p.node == nil {
return nil, io.EOF
}
node := p.node
p.node = nil
return node, nil
}
func (p *slurpParser) Recover() *jsonx.Node {
return nil
}
================================================
FILE: internal/engine/stdlib.js
================================================
'use strict'
const console = {
log: function (...args) {
const parts = []
for (const arg of args) {
if (typeof arg === 'undefined') {
parts.push('undefined')
} else if (typeof arg === 'string') {
parts.push(arg)
} else {
parts.push(JSON.stringify(arg, null, 2))
}
}
println(parts.join(' '))
},
}
const skip = Symbol('skip')
function apply(fn, ...args) {
if (typeof fn === 'function') return fn(...args)
return fn
}
function len(x) {
if (Array.isArray(x)) return x.length
if (typeof x === 'string') return x.length
if (typeof x === 'object' && x !== null) return Object.keys(x).length
throw new Error(`Cannot get length of ${typeof x}`)
}
function uniq(x) {
if (Array.isArray(x)) return [...new Set(x)]
throw new Error(`Cannot get unique values of ${typeof x}`)
}
function sort(x) {
if (Array.isArray(x)) return x.sort()
throw new Error(`Cannot sort ${typeof x}`)
}
function isFalsely(x) {
return x === false || x === null || x === undefined
}
function filter(fn) {
return function (x) {
if (Array.isArray(x)) {
return x.filter((v, i) => !isFalsely(fn(v, i)))
}
return isFalsely(fn(x)) ? skip : x
}
}
function map(fn) {
return function (x) {
if (Array.isArray(x)) {
return x.map((v, i) => fn(v, i))
}
return fn(x)
}
}
function walk(fn) {
return function recurse(value, key = null) {
if (Array.isArray(value)) {
const mapped = value.map((v, i) => recurse(v, i))
return fn(mapped, key)
} else if (value !== null && typeof value === 'object') {
const result = {}
for (const [k, v] of Object.entries(value)) {
result[k] = recurse(v, k)
}
return fn(result, key)
} else {
return fn(value, key)
}
}
}
function sortBy(fn) {
return function (x) {
if (Array.isArray(x)) return x.sort((a, b) => {
const fa = fn(a)
const fb = fn(b)
return fa < fb ? -1 : fa > fb ? 1 : 0
})
throw new Error(`Cannot sort ${typeof x}`)
}
}
function sortKeys(x) {
if (Array.isArray(x)) {
return x.map(sortKeys)
}
if (typeof x === 'object' && x !== null) {
const sorted = {}
for (const key of Object.keys(x).sort()) {
sorted[key] = sortKeys(x[key])
}
return sorted
}
return x
}
function groupBy(keyFn) {
return function (x) {
const grouped = {}
for (const item of x) {
const key = typeof keyFn === 'function' ? keyFn(item) : item[keyFn]
if (!Object.prototype.hasOwnProperty.call(grouped, key)) grouped[key] = []
grouped[key].push(item)
}
return grouped
}
}
function chunk(size) {
return function (x) {
const res = []
let i = 0
while (i < x.length) {
res.push(x.slice(i, i += size))
}
return res
}
}
function zip(...x) {
const length = Math.min(...x.map(a => a.length))
const res = []
for (let i = 0; i < length; i++) {
res.push(x.map(a => a[i]))
}
return res
}
function flatten(x) {
if (Array.isArray(x)) return x.flat()
throw new Error(`Cannot flatten ${typeof x}`)
}
function reverse(x) {
if (Array.isArray(x)) return x.reverse()
throw new Error(`Cannot reverse ${typeof x}`)
}
function keys(x) {
if (typeof x === 'object' && x !== null) return Object.keys(x)
throw new Error(`Cannot get keys of ${typeof x}`)
}
function values(x) {
if (typeof x === 'object' && x !== null) return Object.values(x)
throw new Error(`Cannot get values of ${typeof x}`)
}
function list(x) {
if (Array.isArray(x)) {
for (const y of x) console.log(y)
return skip
}
throw new Error(`Cannot list ${typeof x}`)
}
function del(key) {
return function (x) {
if (Array.isArray(x)) {
const copy = [...x]
copy.splice(key, 1)
return copy
}
if (typeof x === 'object' && x !== null) {
const copy = {...x}
delete copy[key]
return copy
}
throw new Error(`Cannot delete key from ${typeof x}`)
}
}
function exit(code) {
__exit__(code)
}
function save(x) {
if (typeof x === 'undefined') throw new Error('Cannot save undefined')
__save__(__stringify__(x, null, 2))
return x
}
function toBase64(x) {
return __toBase64__(x)
}
function fromBase64(x) {
return __fromBase64__(x)
}
const YAML = {
stringify: x => __yaml_stringify__(x),
parse: x => JSON.parse(__yaml_parse__(x)),
}
================================================
FILE: internal/engine/stdlib_test.go
================================================
package engine_test
import (
"strings"
"testing"
"github.com/dop251/goja"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/antonmedv/fx/internal/engine"
)
// setupVM creates a new goja runtime with stdlib loaded.
func setupVM(t *testing.T) *goja.Runtime {
var output []string
vm := engine.NewVM(func(s string) {
output = append(output, s)
})
_, err := vm.RunString(engine.Stdlib)
require.NoError(t, err, "Failed to load stdlib")
return vm
}
// setupVMWithOutput creates a new goja runtime with stdlib loaded and returns output slice.
func setupVMWithOutput(t *testing.T) (*goja.Runtime, *[]string) {
output := &[]string{}
vm := engine.NewVM(func(s string) {
*output = append(*output, s)
})
_, err := vm.RunString(engine.Stdlib)
require.NoError(t, err, "Failed to load stdlib")
return vm, output
}
// TestApply tests the apply function.
func TestApply(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
}{
{"apply function", "apply(x => x * 2, 5)", int64(10)},
{"apply non-function", "apply(42)", int64(42)},
{"apply with multiple args", "apply((a, b) => a + b, 2, 3)", int64(5)},
{"apply string", "apply('hello')", "hello"},
{"apply null", "apply(null)", nil},
{"apply undefined", "apply(undefined)", goja.Undefined()},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
require.NoError(t, err)
if tc.expected == goja.Undefined() {
assert.True(t, goja.IsUndefined(result))
} else {
assert.Equal(t, tc.expected, result.Export())
}
})
}
}
// TestLen tests the len function.
func TestLen(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
hasError bool
}{
{"array length", "len([1, 2, 3])", int64(3), false},
{"empty array", "len([])", int64(0), false},
{"string length", "len('hello')", int64(5), false},
{"empty string", "len('')", int64(0), false},
{"object keys count", "len({a: 1, b: 2})", int64(2), false},
{"empty object", "len({})", int64(0), false},
{"number error", "len(42)", nil, true},
{"null error", "len(null)", nil, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
if tc.hasError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
}
})
}
}
// TestUniq tests the uniq function.
func TestUniq(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
hasError bool
}{
{"unique numbers", "uniq([1, 2, 2, 3, 3, 3])", []interface{}{int64(1), int64(2), int64(3)}, false},
{"unique strings", "uniq(['a', 'b', 'a', 'c'])", []interface{}{"a", "b", "c"}, false},
{"already unique", "uniq([1, 2, 3])", []interface{}{int64(1), int64(2), int64(3)}, false},
{"empty array", "uniq([])", []interface{}{}, false},
{"non-array error", "uniq('hello')", nil, true},
{"number error", "uniq(42)", nil, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
if tc.hasError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
}
})
}
}
// TestSort tests the sort function.
func TestSort(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
hasError bool
}{
{"sort numbers", "sort([3, 1, 2])", []interface{}{int64(1), int64(2), int64(3)}, false},
{"sort strings", "sort(['c', 'a', 'b'])", []interface{}{"a", "b", "c"}, false},
{"empty array", "sort([])", []interface{}{}, false},
{"already sorted", "sort([1, 2, 3])", []interface{}{int64(1), int64(2), int64(3)}, false},
{"non-array error", "sort('hello')", nil, true},
{"number error", "sort(42)", nil, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
if tc.hasError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
}
})
}
}
// TestFilter tests the filter function.
func TestFilter(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
}{
{"filter even", "filter(x => x % 2 === 0)([1, 2, 3, 4])", []interface{}{int64(2), int64(4)}},
{"filter with index", "filter((x, i) => i > 0)(['a', 'b', 'c'])", []interface{}{"b", "c"}},
{"filter all true", "filter(x => true)([1, 2, 3])", []interface{}{int64(1), int64(2), int64(3)}},
{"filter all false", "filter(x => false)([1, 2, 3])", []interface{}{}},
{"filter empty", "filter(x => true)([])", []interface{}{}},
{"filter non-array truthy", "filter(x => x > 0)(5)", int64(5)},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
})
}
// Test skip symbol for non-array falsy
t.Run("filter non-array falsy returns skip", func(t *testing.T) {
result, err := vm.RunString("filter(x => x > 10)(5) === skip")
require.NoError(t, err)
assert.Equal(t, true, result.Export())
})
// Test null/undefined/false filtering
t.Run("filter removes null", func(t *testing.T) {
result, err := vm.RunString("filter(x => null)([1, 2, 3])")
require.NoError(t, err)
assert.Equal(t, []interface{}{}, result.Export())
})
t.Run("filter removes undefined", func(t *testing.T) {
result, err := vm.RunString("filter(x => undefined)([1, 2, 3])")
require.NoError(t, err)
assert.Equal(t, []interface{}{}, result.Export())
})
}
// TestMap tests the map function.
func TestMap(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
}{
{"map double", "map(x => x * 2)([1, 2, 3])", []interface{}{int64(2), int64(4), int64(6)}},
{"map with index", "map((x, i) => i)(['a', 'b', 'c'])", []interface{}{int64(0), int64(1), int64(2)}},
{"map empty", "map(x => x * 2)([])", []interface{}{}},
{"map non-array", "map(x => x * 2)(5)", int64(10)},
{"map to string", "map(x => String(x))([1, 2, 3])", []interface{}{"1", "2", "3"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
})
}
}
// TestWalk tests the walk function.
func TestWalk(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
}{
{"walk multiply primitives", "walk(x => typeof x === 'number' ? x * 2 : x)({a: 1, b: 2})",
map[string]interface{}{"a": int64(2), "b": int64(4)}},
{"walk nested array", "walk(x => typeof x === 'number' ? x + 1 : x)([[1, 2], [3, 4]])",
[]interface{}{[]interface{}{int64(2), int64(3)}, []interface{}{int64(4), int64(5)}}},
{"walk primitive", "walk(x => x * 2)(5)", int64(10)},
{"walk with key", "walk((v, k) => k === 'double' ? v * 2 : v)({double: 5, keep: 10})",
map[string]interface{}{"double": int64(10), "keep": int64(10)}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
})
}
}
// TestSortBy tests the sortBy function.
func TestSortBy(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
hasError bool
}{
{"sortBy property", "sortBy(x => x.age)([{name: 'b', age: 30}, {name: 'a', age: 20}])",
[]interface{}{
map[string]interface{}{"name": "a", "age": int64(20)},
map[string]interface{}{"name": "b", "age": int64(30)},
}, false},
{"sortBy computed", "sortBy(x => -x)([1, 3, 2])",
[]interface{}{int64(3), int64(2), int64(1)}, false},
{"sortBy string length", "sortBy(x => x.length)(['aaa', 'a', 'aa'])",
[]interface{}{"a", "aa", "aaa"}, false},
{"sortBy non-array error", "sortBy(x => x)('hello')", nil, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
if tc.hasError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
}
})
}
}
// TestSortKeys tests the sortKeys function.
func TestSortKeys(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
check func(t *testing.T, result goja.Value)
}{
{
name: "sort object keys",
code: "JSON.stringify(sortKeys({c: 1, a: 2, b: 3}))",
check: func(t *testing.T, result goja.Value) {
assert.Equal(t, `{"a":2,"b":3,"c":1}`, result.Export())
},
},
{
name: "sort nested object keys",
code: "JSON.stringify(sortKeys({z: {c: 1, a: 2}, y: 3}))",
check: func(t *testing.T, result goja.Value) {
assert.Equal(t, `{"y":3,"z":{"a":2,"c":1}}`, result.Export())
},
},
{
name: "sort array of objects",
code: "JSON.stringify(sortKeys([{b: 1, a: 2}, {d: 3, c: 4}]))",
check: func(t *testing.T, result goja.Value) {
assert.Equal(t, `[{"a":2,"b":1},{"c":4,"d":3}]`, result.Export())
},
},
{
name: "primitives unchanged",
code: "sortKeys(42)",
check: func(t *testing.T, result goja.Value) {
assert.Equal(t, int64(42), result.Export())
},
},
{
name: "null unchanged",
code: "sortKeys(null)",
check: func(t *testing.T, result goja.Value) {
assert.Nil(t, result.Export())
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
require.NoError(t, err)
tc.check(t, result)
})
}
}
// TestGroupBy tests the groupBy function.
func TestGroupBy(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
}{
{"groupBy function", "groupBy(x => x % 2 === 0 ? 'even' : 'odd')([1, 2, 3, 4])",
map[string]interface{}{
"odd": []interface{}{int64(1), int64(3)},
"even": []interface{}{int64(2), int64(4)},
}},
{"groupBy property name", "groupBy('type')([{type: 'a', v: 1}, {type: 'b', v: 2}, {type: 'a', v: 3}])",
map[string]interface{}{
"a": []interface{}{
map[string]interface{}{"type": "a", "v": int64(1)},
map[string]interface{}{"type": "a", "v": int64(3)},
},
"b": []interface{}{
map[string]interface{}{"type": "b", "v": int64(2)},
},
}},
{"groupBy empty", "groupBy(x => x)([])",
map[string]interface{}{}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
})
}
}
// TestChunk tests the chunk function.
func TestChunk(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
}{
{"chunk by 2", "chunk(2)([1, 2, 3, 4, 5])",
[]interface{}{
[]interface{}{int64(1), int64(2)},
[]interface{}{int64(3), int64(4)},
[]interface{}{int64(5)},
}},
{"chunk by 3", "chunk(3)([1, 2, 3, 4, 5, 6])",
[]interface{}{
[]interface{}{int64(1), int64(2), int64(3)},
[]interface{}{int64(4), int64(5), int64(6)},
}},
{"chunk empty", "chunk(2)([])",
[]interface{}{}},
{"chunk by 1", "chunk(1)([1, 2, 3])",
[]interface{}{
[]interface{}{int64(1)},
[]interface{}{int64(2)},
[]interface{}{int64(3)},
}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
})
}
}
// TestZip tests the zip function.
func TestZip(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
}{
{"zip two arrays", "zip([1, 2, 3], ['a', 'b', 'c'])",
[]interface{}{
[]interface{}{int64(1), "a"},
[]interface{}{int64(2), "b"},
[]interface{}{int64(3), "c"},
}},
{"zip three arrays", "zip([1, 2], ['a', 'b'], [true, false])",
[]interface{}{
[]interface{}{int64(1), "a", true},
[]interface{}{int64(2), "b", false},
}},
{"zip different lengths", "zip([1, 2, 3], ['a', 'b'])",
[]interface{}{
[]interface{}{int64(1), "a"},
[]interface{}{int64(2), "b"},
}},
{"zip empty", "zip([], [])",
[]interface{}{}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
})
}
}
// TestFlatten tests the flatten function.
func TestFlatten(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
hasError bool
}{
{"flatten nested", "flatten([[1, 2], [3, 4]])",
[]interface{}{int64(1), int64(2), int64(3), int64(4)}, false},
{"flatten mixed", "flatten([[1], 2, [3, 4]])",
[]interface{}{int64(1), int64(2), int64(3), int64(4)}, false},
{"flatten one level", "flatten([[[1, 2]], [[3, 4]]])",
[]interface{}{
[]interface{}{int64(1), int64(2)},
[]interface{}{int64(3), int64(4)},
}, false},
{"flatten empty", "flatten([])", []interface{}{}, false},
{"flatten non-array error", "flatten('hello')", nil, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
if tc.hasError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
}
})
}
}
// TestReverse tests the reverse function.
func TestReverse(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
hasError bool
}{
{"reverse numbers", "reverse([1, 2, 3])",
[]interface{}{int64(3), int64(2), int64(1)}, false},
{"reverse strings", "reverse(['a', 'b', 'c'])",
[]interface{}{"c", "b", "a"}, false},
{"reverse empty", "reverse([])", []interface{}{}, false},
{"reverse single", "reverse([1])", []interface{}{int64(1)}, false},
{"reverse non-array error", "reverse('hello')", nil, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
if tc.hasError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
}
})
}
}
// TestKeys tests the keys function.
func TestKeys(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
hasError bool
}{
{"object keys", "keys({a: 1, b: 2}).sort()", false},
{"empty object", "keys({})", false},
{"array keys", "keys([10, 20, 30])", false},
{"null error", "keys(null)", true},
{"number error", "keys(42)", true},
{"string error", "keys('hello')", true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
if tc.hasError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.NotNil(t, result.Export())
}
})
}
// Specific value checks
t.Run("object keys values", func(t *testing.T) {
result, err := vm.RunString("keys({a: 1, b: 2}).sort()")
require.NoError(t, err)
assert.Equal(t, []interface{}{"a", "b"}, result.Export())
})
t.Run("array keys values", func(t *testing.T) {
result, err := vm.RunString("keys([10, 20])")
require.NoError(t, err)
assert.Equal(t, []interface{}{"0", "1"}, result.Export())
})
}
// TestValues tests the values function.
func TestValues(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
hasError bool
}{
{"object values", "values({a: 1, b: 2}).sort()", false},
{"empty object", "values({})", false},
{"array values", "values([10, 20, 30])", false},
{"null error", "values(null)", true},
{"number error", "values(42)", true},
{"string error", "values('hello')", true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
if tc.hasError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.NotNil(t, result.Export())
}
})
}
// Specific value checks
t.Run("object values sorted", func(t *testing.T) {
result, err := vm.RunString("values({a: 1, b: 2}).sort()")
require.NoError(t, err)
assert.Equal(t, []interface{}{int64(1), int64(2)}, result.Export())
})
t.Run("array values", func(t *testing.T) {
result, err := vm.RunString("values([10, 20, 30])")
require.NoError(t, err)
assert.Equal(t, []interface{}{int64(10), int64(20), int64(30)}, result.Export())
})
}
// TestList tests the list function.
func TestList(t *testing.T) {
vm, output := setupVMWithOutput(t)
t.Run("list prints each item", func(t *testing.T) {
*output = []string{} // Reset output
result, err := vm.RunString("list([1, 2, 3]) === skip")
require.NoError(t, err)
assert.True(t, result.Export().(bool))
assert.Equal(t, []string{"1", "2", "3"}, *output)
})
t.Run("list with objects", func(t *testing.T) {
*output = []string{}
result, err := vm.RunString(`list([{a: 1}]) === skip`)
require.NoError(t, err)
assert.True(t, result.Export().(bool))
assert.Len(t, *output, 1)
})
t.Run("list non-array error", func(t *testing.T) {
_, err := vm.RunString("list('hello')")
assert.Error(t, err)
})
}
// TestDel tests the del function.
func TestDel(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
hasError bool
}{
{"del from object", "del('a')({a: 1, b: 2})",
map[string]interface{}{"b": int64(2)}, false},
{"del non-existent key", "del('c')({a: 1, b: 2})",
map[string]interface{}{"a": int64(1), "b": int64(2)}, false},
{"del from array", "del(1)([1, 2, 3])",
[]interface{}{int64(1), int64(3)}, false},
{"del first from array", "del(0)([1, 2, 3])",
[]interface{}{int64(2), int64(3)}, false},
{"del last from array", "del(2)([1, 2, 3])",
[]interface{}{int64(1), int64(2)}, false},
{"del from empty array", "del(0)([])", []interface{}{}, false},
{"del from primitive error", "del('a')(42)", nil, true},
{"del from null error", "del('a')(null)", nil, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
if tc.hasError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
}
})
}
}
// TestSkipSymbol tests the skip symbol.
func TestSkipSymbol(t *testing.T) {
vm := setupVM(t)
t.Run("skip is a symbol", func(t *testing.T) {
result, err := vm.RunString("typeof skip")
require.NoError(t, err)
assert.Equal(t, "symbol", result.Export())
})
t.Run("skip is unique", func(t *testing.T) {
result, err := vm.RunString("skip === skip")
require.NoError(t, err)
assert.True(t, result.Export().(bool))
})
}
// TestConsoleLog tests console.log.
func TestConsoleLog(t *testing.T) {
vm, output := setupVMWithOutput(t)
tests := []struct {
name string
code string
expected []string
}{
{"log string", "console.log('hello')", []string{"hello"}},
{"log number", "console.log(42)", []string{"42"}},
{"log object", "console.log({a: 1})", []string{"{\n \"a\": 1\n}"}},
{"log undefined", "console.log(undefined)", []string{"undefined"}},
{"log multiple args", "console.log('a', 'b')", []string{"a b"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
*output = []string{}
_, err := vm.RunString(tc.code)
require.NoError(t, err)
assert.Equal(t, tc.expected, *output)
})
}
}
// TestToBase64 tests the toBase64 function.
func TestToBase64(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
input string
expected string
}{
{"encode hello", "toBase64('hello')", "aGVsbG8="},
{"encode empty", "toBase64('')", ""},
{"encode unicode", "toBase64('こんにちは')", "44GT44KT44Gr44Gh44Gv"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.input)
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
})
}
}
// TestFromBase64 tests the fromBase64 function.
func TestFromBase64(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
input string
expected string
hasError bool
}{
{"decode hello", "fromBase64('aGVsbG8=')", "hello", false},
{"decode empty", "fromBase64('')", "", false},
{"decode unicode", "fromBase64('44GT44KT44Gr44Gh44Gv')", "こんにちは", false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.input)
if tc.hasError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
}
})
}
t.Run("invalid base64 error", func(t *testing.T) {
_, err := vm.RunString("fromBase64('not-valid-base64!!!')")
assert.Error(t, err)
})
}
// TestYAML tests YAML.parse and YAML.stringify.
func TestYAML(t *testing.T) {
vm := setupVM(t)
t.Run("YAML.parse simple", func(t *testing.T) {
result, err := vm.RunString(`YAML.parse('name: John\nage: 30')`)
require.NoError(t, err)
expected := map[string]interface{}{"name": "John", "age": int64(30)}
assert.Equal(t, expected, result.Export())
})
t.Run("YAML.parse array", func(t *testing.T) {
result, err := vm.RunString(`YAML.parse('- 1\n- 2\n- 3')`)
require.NoError(t, err)
expected := []interface{}{int64(1), int64(2), int64(3)}
assert.Equal(t, expected, result.Export())
})
t.Run("YAML.stringify simple", func(t *testing.T) {
result, err := vm.RunString(`YAML.stringify({name: 'John', age: 30})`)
require.NoError(t, err)
yamlStr := result.Export().(string)
assert.Contains(t, yamlStr, "name: John")
assert.Contains(t, yamlStr, "age: 30")
})
t.Run("YAML.stringify array", func(t *testing.T) {
result, err := vm.RunString(`YAML.stringify([1, 2, 3])`)
require.NoError(t, err)
yamlStr := result.Export().(string)
assert.Contains(t, yamlStr, "- 1")
assert.Contains(t, yamlStr, "- 2")
assert.Contains(t, yamlStr, "- 3")
})
t.Run("YAML.parse invalid", func(t *testing.T) {
_, err := vm.RunString(`YAML.parse('invalid: [unclosed')`)
assert.Error(t, err)
})
}
// TestIsFalsely tests the internal isFalsely function behavior.
func TestIsFalsely(t *testing.T) {
vm := setupVM(t)
// isFalsely is used internally by filter
// Testing through filter behavior
tests := []struct {
name string
code string
expected interface{}
}{
{"false is falsely", "filter(x => false)([1])", []interface{}{}},
{"null is falsely", "filter(x => null)([1])", []interface{}{}},
{"undefined is falsely", "filter(x => undefined)([1])", []interface{}{}},
{"0 is not falsely", "filter(x => 0)([1])", []interface{}{int64(1)}},
{"empty string is not falsely", "filter(x => '')([1])", []interface{}{int64(1)}},
{"true is not falsely", "filter(x => true)([1])", []interface{}{int64(1)}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
})
}
}
// TestChainedOperations tests chaining multiple stdlib functions.
func TestChainedOperations(t *testing.T) {
vm := setupVM(t)
tests := []struct {
name string
code string
expected interface{}
}{
{
"filter then map",
"map(x => x * 2)(filter(x => x > 1)([1, 2, 3]))",
[]interface{}{int64(4), int64(6)},
},
{
"map then filter",
"filter(x => x > 3)(map(x => x * 2)([1, 2, 3]))",
[]interface{}{int64(4), int64(6)},
},
{
"sort then reverse",
"reverse(sort([3, 1, 2]))",
[]interface{}{int64(3), int64(2), int64(1)},
},
{
"flatten then uniq",
"uniq(flatten([[1, 2], [2, 3]]))",
[]interface{}{int64(1), int64(2), int64(3)},
},
{
"groupBy then keys",
"keys(groupBy(x => x % 2)([1, 2, 3, 4])).sort()",
[]interface{}{"0", "1"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := vm.RunString(tc.code)
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Export())
})
}
}
// TestEdgeCases tests various edge cases.
func TestEdgeCases(t *testing.T) {
vm := setupVM(t)
t.Run("deeply nested walk", func(t *testing.T) {
result, err := vm.RunString(`
walk(x => typeof x === 'number' ? x * 2 : x)({
a: {
b: {
c: 1
}
}
})
`)
require.NoError(t, err)
expected := map[string]interface{}{
"a": map[string]interface{}{
"b": map[string]interface{}{
"c": int64(2),
},
},
}
assert.Equal(t, expected, result.Export())
})
t.Run("empty input handling", func(t *testing.T) {
tests := []string{
"len([])",
"len({})",
"len('')",
"uniq([])",
"sort([])",
"flatten([])",
"reverse([])",
"filter(x => true)([])",
"map(x => x)([])",
"chunk(2)([])",
"zip([], [])",
"keys({})",
"values({})",
}
for _, code := range tests {
_, err := vm.RunString(code)
assert.NoError(t, err, "Failed for: %s", code)
}
})
t.Run("large array handling", func(t *testing.T) {
result, err := vm.RunString(`
const arr = [];
for (let i = 0; i < 1000; i++) arr.push(i);
len(arr)
`)
require.NoError(t, err)
assert.Equal(t, int64(1000), result.Export())
})
t.Run("unicode strings", func(t *testing.T) {
result, err := vm.RunString(`len('你好世界')`)
require.NoError(t, err)
assert.Equal(t, int64(4), result.Export())
})
}
// TestBase64RoundTrip tests that base64 encoding/decoding is reversible.
func TestBase64RoundTrip(t *testing.T) {
vm := setupVM(t)
tests := []string{
"hello",
"",
"Hello, 世界!",
"Special chars: !@#$%^&*()",
strings.Repeat("a", 1000),
}
for _, input := range tests {
t.Run(input[:min(len(input), 20)], func(t *testing.T) {
code := "fromBase64(toBase64('" + escapeJS(input) + "'))"
result, err := vm.RunString(code)
require.NoError(t, err)
assert.Equal(t, input, result.Export())
})
}
}
func escapeJS(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "'", "\\'")
s = strings.ReplaceAll(s, "\n", "\\n")
s = strings.ReplaceAll(s, "\r", "\\r")
return s
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// TestExit tests the exit function.
func TestExit(t *testing.T) {
vm := setupVM(t)
t.Run("exit panics with ExitError", func(t *testing.T) {
defer func() {
r := recover()
require.NotNil(t, r, "Expected panic from exit()")
exitErr, ok := r.(engine.ExitError)
require.True(t, ok, "Expected ExitError, got %T", r)
assert.Equal(t, 42, exitErr.Code)
}()
_, _ = vm.RunString("exit(42)")
})
t.Run("exit with 0", func(t *testing.T) {
defer func() {
r := recover()
require.NotNil(t, r)
exitErr, ok := r.(engine.ExitError)
require.True(t, ok)
assert.Equal(t, 0, exitErr.Code)
}()
_, _ = vm.RunString("exit(0)")
})
}
// TestSave tests the save function.
func TestSave(t *testing.T) {
vm := setupVM(t)
t.Run("save undefined throws error", func(t *testing.T) {
_, err := vm.RunString("save(undefined)")
assert.Error(t, err)
assert.Contains(t, err.Error(), "Cannot save undefined")
})
t.Run("save without file path throws error", func(t *testing.T) {
// FilePath is empty by default in tests
_, err := vm.RunString("save({a: 1})")
assert.Error(t, err)
assert.Contains(t, err.Error(), "specify a file")
})
}
// TestSortMutation tests that sort mutates the original array (JavaScript behavior).
func TestSortMutation(t *testing.T) {
vm := setupVM(t)
t.Run("sort mutates original array", func(t *testing.T) {
result, err := vm.RunString(`
const arr = [3, 1, 2];
sort(arr);
arr[0]
`)
require.NoError(t, err)
// JavaScript sort mutates the array
assert.Equal(t, int64(1), result.Export())
})
}
// TestReverseMutation tests that reverse mutates the original array (JavaScript behavior).
func TestReverseMutation(t *testing.T) {
vm := setupVM(t)
t.Run("reverse mutates original array", func(t *testing.T) {
result, err := vm.RunString(`
const arr = [1, 2, 3];
reverse(arr);
arr[0]
`)
require.NoError(t, err)
// JavaScript reverse mutates the array
assert.Equal(t, int64(3), result.Export())
})
}
// TestDelImmutability tests that del does not mutate the original.
func TestDelImmutability(t *testing.T) {
vm := setupVM(t)
t.Run("del does not mutate original object", func(t *testing.T) {
result, err := vm.RunString(`
const obj = {a: 1, b: 2};
del('a')(obj);
obj.a
`)
require.NoError(t, err)
assert.Equal(t, int64(1), result.Export())
})
t.Run("del does not mutate original array", func(t *testing.T) {
result, err := vm.RunString(`
const arr = [1, 2, 3];
del(0)(arr);
arr[0]
`)
require.NoError(t, err)
assert.Equal(t, int64(1), result.Export())
})
}
// TestWalkWithNull tests walk behavior with null values.
func TestWalkWithNull(t *testing.T) {
vm := setupVM(t)
t.Run("walk handles null values", func(t *testing.T) {
result, err := vm.RunString(`
walk(x => x === null ? 'was null' : x)({a: null, b: 1})
`)
require.NoError(t, err)
expected := map[string]interface{}{"a": "was null", "b": int64(1)}
assert.Equal(t, expected, result.Export())
})
}
// TestGroupByWithPrototypeKeys tests groupBy doesn't have prototype pollution issues.
func TestGroupByWithPrototypeKeys(t *testing.T) {
vm := setupVM(t)
t.Run("groupBy with hasOwnProperty key", func(t *testing.T) {
result, err := vm.RunString(`
groupBy(x => x)(['hasOwnProperty', 'toString', 'normal'])
`)
require.NoError(t, err)
exported := result.Export().(map[string]interface{})
assert.Len(t, exported["hasOwnProperty"], 1)
assert.Len(t, exported["toString"], 1)
assert.Len(t, exported["normal"], 1)
})
}
// TestChunkEdgeCases tests chunk with edge case sizes.
func TestChunkEdgeCases(t *testing.T) {
vm := setupVM(t)
t.Run("chunk size larger than array", func(t *testing.T) {
result, err := vm.RunString("chunk(10)([1, 2, 3])")
require.NoError(t, err)
expected := []interface{}{[]interface{}{int64(1), int64(2), int64(3)}}
assert.Equal(t, expected, result.Export())
})
t.Run("chunk size equals array length", func(t *testing.T) {
result, err := vm.RunString("chunk(3)([1, 2, 3])")
require.NoError(t, err)
expected := []interface{}{[]interface{}{int64(1), int64(2), int64(3)}}
assert.Equal(t, expected, result.Export())
})
}
// TestZipEdgeCases tests zip with edge cases.
func TestZipEdgeCases(t *testing.T) {
vm := setupVM(t)
t.Run("zip single array", func(t *testing.T) {
result, err := vm.RunString("zip([1, 2, 3])")
require.NoError(t, err)
expected := []interface{}{
[]interface{}{int64(1)},
[]interface{}{int64(2)},
[]interface{}{int64(3)},
}
assert.Equal(t, expected, result.Export())
})
t.Run("zip with one empty array", func(t *testing.T) {
result, err := vm.RunString("zip([1, 2, 3], [])")
require.NoError(t, err)
expected := []interface{}{}
assert.Equal(t, expected, result.Export())
})
}
// TestFilterWithObjects tests filter with object predicates.
func TestFilterWithObjects(t *testing.T) {
vm := setupVM(t)
t.Run("filter objects by property", func(t *testing.T) {
result, err := vm.RunString(`
filter(x => x.active)([
{name: 'a', active: true},
{name: 'b', active: false},
{name: 'c', active: true}
])
`)
require.NoError(t, err)
exported := result.Export().([]interface{})
assert.Len(t, exported, 2)
})
}
// TestMapWithObjects tests map transforming objects.
func TestMapWithObjects(t *testing.T) {
vm := setupVM(t)
t.Run("map extract property", func(t *testing.T) {
result, err := vm.RunString(`
map(x => x.name)([
{name: 'a', value: 1},
{name: 'b', value: 2}
])
`)
require.NoError(t, err)
expected := []interface{}{"a", "b"}
assert.Equal(t, expected, result.Export())
})
t.Run("map transform object", func(t *testing.T) {
result, err := vm.RunString(`
map(x => ({...x, doubled: x.value * 2}))([
{value: 1},
{value: 2}
])
`)
require.NoError(t, err)
exported := result.Export().([]interface{})
assert.Equal(t, int64(2), exported[0].(map[string]interface{})["doubled"])
assert.Equal(t, int64(4), exported[1].(map[string]interface{})["doubled"])
})
}
// TestSortByStability tests sortBy with equal keys.
func TestSortByStability(t *testing.T) {
vm := setupVM(t)
t.Run("sortBy preserves order for equal keys", func(t *testing.T) {
// Note: JavaScript sort is not guaranteed to be stable, but this tests the behavior
result, err := vm.RunString(`
sortBy(x => x.group)([
{name: 'a', group: 1},
{name: 'b', group: 2},
{name: 'c', group: 1}
]).map(x => x.name)
`)
require.NoError(t, err)
exported := result.Export().([]interface{})
// Group 1 items should come before group 2
assert.Equal(t, "b", exported[2])
})
}
// TestNestedOperations tests deeply nested function compositions.
func TestNestedOperations(t *testing.T) {
vm := setupVM(t)
t.Run("complex nested operations", func(t *testing.T) {
result, err := vm.RunString(`
map(x => x * 2)(
filter(x => x > 0)(
flatten([
[-1, 0, 1],
[2, 3]
])
)
)
`)
require.NoError(t, err)
expected := []interface{}{int64(2), int64(4), int64(6)}
assert.Equal(t, expected, result.Export())
})
}
================================================
FILE: internal/engine/stringify.go
================================================
package engine
import (
"fmt"
"math"
"math/big"
"reflect"
"strings"
"time"
"github.com/dop251/goja"
)
func Stringify(value goja.Value, vm *goja.Runtime, depth int) string {
rtype := value.ExportType()
if rtype == nil {
// Convert both null and undefined to null (save as JSON.stringify)
return "null"
}
switch rtype {
case bigIntType:
bi := value.Export().(*big.Int)
return bi.String()
case timeTimeType:
t := value.Export().(time.Time)
quoted := Quote(t.String())
return quoted
}
switch rtype.Kind() {
case reflect.Bool:
if value.ToBoolean() {
return "true"
} else {
return "false"
}
case reflect.Int64:
return value.String()
case reflect.Float64:
f := value.ToFloat()
if math.IsInf(f, 0) {
return value.String()
} else if math.IsNaN(f) {
return value.String()
}
return value.String()
case reflect.String:
return Quote(value.String())
case reflect.Map:
obj := value.ToObject(vm)
keys := obj.Keys()
if len(keys) == 0 {
return "{}"
}
var out strings.Builder
out.WriteString("{")
out.WriteString("\n")
ident := strings.Repeat(" ", depth)
identKey := strings.Repeat(" ", depth+1)
for i, key := range keys {
out.WriteString(identKey)
out.WriteString(Quote(key))
out.WriteString(":")
out.WriteString(" ")
out.WriteString(Stringify(obj.Get(key), vm, depth+1))
if i < len(keys)-1 {
out.WriteString(",")
}
out.WriteString("\n")
}
out.WriteString(ident)
out.WriteString("}")
return out.String()
case reflect.Slice:
arr := value.ToObject(vm)
keys := arr.Keys()
if len(keys) == 0 {
return "[]"
}
var out strings.Builder
out.WriteString("[")
out.WriteString("\n")
for i, key := range keys {
item := arr.Get(key)
out.WriteString(strings.Repeat(" ", depth+1))
out.WriteString(Stringify(item, vm, depth+1))
if i < len(keys)-1 {
out.WriteString(",")
}
out.WriteString("\n")
}
out.WriteString(strings.Repeat(" ", depth))
out.WriteString("]")
return out.String()
}
panic(fmt.Sprintf("Unsupported value type: %v", rtype.Kind()))
}
================================================
FILE: internal/engine/transpile.go
================================================
package engine
import (
"fmt"
"regexp"
"strconv"
"strings"
)
func JS(args []string) string {
var code strings.Builder
code.WriteString("\nfunction __main__(json) {\n")
for i := range args {
if args[i] == "" {
// In autocomplete: after dropTail, we can have empty strings.
continue
}
code.WriteString(Body(args, i))
}
code.WriteString("\n return json\n}\n")
return code.String()
}
func Body(args []string, i int) string {
jsCode := transpile(args[i])
snippet := formatErr(args, i, jsCode)
return fmt.Sprintf(`
try {
json = apply((function () {
const x = this
return %s
}).call(json), json)
} catch (e) {
throw %s
}
if (json === skip) return skip
`, jsCode, strconv.Quote(snippet)+" + e.toString()")
}
var (
reBracket = regexp.MustCompile(`^(\.\w*)+\[]`)
reBracketStart = regexp.MustCompile(`^\.\[`)
reDotStart = regexp.MustCompile(`^\.`)
reAt = regexp.MustCompile(`^@`)
reFilter = regexp.MustCompile(`^\?`)
)
func transpile(code string) string {
if code == "." {
return "x"
}
if reBracket.MatchString(code) {
return fmt.Sprintf("(%s)(x)", fold(strings.Split(code, "[]")))
}
if reBracketStart.MatchString(code) {
return "x" + code[1:]
}
if reDotStart.MatchString(code) {
return "x" + code
}
if reAt.MatchString(code) {
jsCode := transpile(code[1:])
return fmt.Sprintf(`map((x, i) => apply(%s, x, i))`, jsCode)
}
if reFilter.MatchString(code) {
jsCode := transpile(code[1:])
return fmt.Sprintf(`filter((x, i) => apply(%s, x, i))`, jsCode)
}
return code
}
func fold(s []string) string {
if len(s) == 1 {
return "x => x" + s[0]
}
obj := s[0]
s = s[1:]
if obj == "." {
obj = "x"
} else {
obj = "x" + obj
}
return fmt.Sprintf(`x => %s.flatMap(%s)`, obj, fold(s))
}
================================================
FILE: internal/engine/transpile_test.go
================================================
package engine
import (
"testing"
)
func TestTranspile(t *testing.T) {
tests := []struct {
code string
want string
}{
{".", "x"},
{".foo", "x.foo"},
{".[0]", "x[0]"},
{"foo", "foo"},
{"@.baz", "map((x, i) => apply(x.baz, x, i))"},
{"?.foo > 42", "filter((x, i) => apply(x.foo > 42, x, i))"},
{".foo[].bar[]", "(x => x.foo.flatMap(x => x.bar.flatMap(x => x)))(x)"},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
got := transpile(tt.code)
if got != tt.want {
t.Errorf("transpile(%q) = %q; want %q", tt.code, got, tt.want)
}
})
}
}
func TestFoldSimple(t *testing.T) {
tests := []struct {
parts []string
want string
}{
{[]string{".foo"}, "x => x.foo"},
{[]string{".foo", ".bar"}, "x => x.foo.flatMap(x => x.bar)"},
}
for _, tt := range tests {
got := fold(tt.parts)
if got != tt.want {
t.Errorf("fold(%v) = %q; want %q", tt.parts, got, tt.want)
}
}
}
================================================
FILE: internal/engine/utils.go
================================================
package engine
import (
"math/big"
"reflect"
"regexp"
"time"
"github.com/dop251/goja"
)
var (
bigIntType = reflect.TypeOf((*big.Int)(nil))
timeTimeType = reflect.TypeOf((*time.Time)(nil)).Elem()
)
var (
syntaxErrorRe = regexp.MustCompile(`^SyntaxError: SyntaxError: \(anonymous\): Line \d+:\d+\s+`)
andMoreErrors = regexp.MustCompile(`\(and \d+ more errors\)$`)
)
func extractErrorMessage(s string) string {
s = syntaxErrorRe.ReplaceAllString(s, "")
s = andMoreErrors.ReplaceAllString(s, "")
return s
}
func errorToString(err error) string {
if exception, ok := err.(*goja.Exception); ok {
message := exception.Value().String()
message = extractErrorMessage(message)
return message
}
return err.Error()
}
================================================
FILE: internal/engine/vm.go
================================================
package engine
import (
"encoding/base64"
"fmt"
"os"
"github.com/dop251/goja"
"github.com/goccy/go-yaml"
)
// FilePath is the file being processed, empty if stdin.
var FilePath string
// ExitError is used by exit() to signal a specific exit code.
type ExitError struct {
Code int
}
func NewVM(writeOut func(string)) *goja.Runtime {
vm := goja.New()
if err := vm.Set("println", func(s string) any {
writeOut(s)
return nil
}); err != nil {
panic(err)
}
if err := vm.Set("__save__", func(json string) error {
if FilePath == "" {
return fmt.Errorf("specify a file as the first argument to be able to save: fx file.json ")
}
if info, err := os.Lstat(FilePath); err == nil && info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("cannot save to a symbolic link: %s", FilePath)
}
if err := os.WriteFile(FilePath, []byte(json), 0644); err != nil {
return err
}
return nil
}); err != nil {
panic(err)
}
if err := vm.Set("__stringify__", func(x goja.Value) string {
return Stringify(x, vm, 0) + "\n"
}); err != nil {
panic(err)
}
if err := vm.Set("__toBase64__", func(x string) string {
return base64.StdEncoding.EncodeToString([]byte(x))
}); err != nil {
panic(err)
}
if err := vm.Set("__fromBase64__", func(x string) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(x)
if err != nil {
return "", err
}
return string(decoded), err
}); err != nil {
panic(err)
}
if err := vm.Set("__yaml_parse__", func(x string) (string, error) {
b, err := yaml.YAMLToJSON([]byte(x))
if err != nil {
return "", err
}
return string(b), err
}); err != nil {
panic(err)
}
if err := vm.Set("__yaml_stringify__", func(x goja.Value) string {
b, err := yaml.JSONToYAML([]byte(Stringify(x, vm, 0)))
if err != nil {
return ""
}
return string(b)
}); err != nil {
panic(err)
}
if err := vm.Set("__exit__", func(code int) {
panic(ExitError{Code: code})
}); err != nil {
panic(err)
}
return vm
}
================================================
FILE: internal/fuzzy/algo.go
================================================
package fuzzy
import (
"bytes"
"strings"
"unicode"
"unicode/utf8"
)
var delimiterChars = "."
const whiteChars = " \t\n\v\f\r\x85\xA0"
type Result struct {
Start int
End int
Score int
}
const (
scoreMatch = 16
scoreGapStart = -3
scoreGapExtension = -1
// We prefer matches at the beginning of a word, but the bonus should not be
// too great to prevent the longer acronym matches from always winning over
// shorter fuzzy matches. The bonus point here was specifically chosen that
// the bonus is cancelled when the gap between the acronyms grows over
// 8 characters, which is approximately the average length of the words found
// in web2 dictionary and my file system.
bonusBoundary = scoreMatch / 2
// Although bonus point for non-word characters is non-contextual, we need it
// for computing bonus points for consecutive chunks starting with a non-word
// character.
bonusNonWord = scoreMatch / 2
// Edge-triggered bonus for matches in camelCase words.
// Compared to word-boundary case, they don't accompany single-character gaps
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
bonusCamel123 = bonusBoundary + scoreGapExtension
// Minimum bonus point given to characters in consecutive chunks.
// Note that bonus points for consecutive matches shouldn't have needed if we
// used fixed match score as in the original algorithm.
bonusConsecutive = -(scoreGapStart + scoreGapExtension)
// The first character in the typed pattern usually has more significance
// than the rest so it's important that it appears at special positions where
// bonus points are given, e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
// The amount of the extra bonus should be limited so that the gap penalty is
// still respected.
bonusFirstCharMultiplier = 2
)
var (
// Extra bonus for word boundary after whitespace character or beginning of the string
bonusBoundaryWhite int16 = bonusBoundary
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter int16 = bonusBoundary + 1
initialCharClass = charDelimiter
// A minor optimization that can give 15%+ performance boost
asciiCharClasses [unicode.MaxASCII + 1]charClass
// A minor optimization that can give yet another 5% performance boost
bonusMatrix [charNumber + 1][charNumber + 1]int16
)
type charClass int
const (
charWhite charClass = iota
charNonWord
charDelimiter
charLower
charUpper
charLetter
charNumber
)
func init() {
for i := 0; i <= unicode.MaxASCII; i++ {
char := rune(i)
c := charNonWord
if char >= 'a' && char <= 'z' {
c = charLower
} else if char >= 'A' && char <= 'Z' {
c = charUpper
} else if char >= '0' && char <= '9' {
c = charNumber
} else if strings.ContainsRune(whiteChars, char) {
c = charWhite
} else if strings.ContainsRune(delimiterChars, char) {
c = charDelimiter
}
asciiCharClasses[i] = c
}
for i := 0; i <= int(charNumber); i++ {
for j := 0; j <= int(charNumber); j++ {
bonusMatrix[i][j] = bonusFor(charClass(i), charClass(j))
}
}
}
func posArray(withPos bool, len int) *[]int {
if withPos {
pos := make([]int, 0, len)
return &pos
}
return nil
}
func alloc16(offset int, slab *Slab, size int) (int, []int16) {
if slab != nil && cap(slab.I16) > offset+size {
slice := slab.I16[offset : offset+size]
return offset + size, slice
}
return offset, make([]int16, size)
}
func alloc32(offset int, slab *Slab, size int) (int, []int32) {
if slab != nil && cap(slab.I32) > offset+size {
slice := slab.I32[offset : offset+size]
return offset + size, slice
}
return offset, make([]int32, size)
}
func charClassOfNonAscii(char rune) charClass {
if unicode.IsLower(char) {
return charLower
} else if unicode.IsUpper(char) {
return charUpper
} else if unicode.IsNumber(char) {
return charNumber
} else if unicode.IsLetter(char) {
return charLetter
} else if unicode.IsSpace(char) {
return charWhite
} else if strings.ContainsRune(delimiterChars, char) {
return charDelimiter
}
return charNonWord
}
func charClassOf(char rune) charClass {
if char <= unicode.MaxASCII {
return asciiCharClasses[char]
}
return charClassOfNonAscii(char)
}
func bonusFor(prevClass charClass, class charClass) int16 {
if class > charNonWord {
switch prevClass {
case charWhite:
// Word boundary after whitespace
return bonusBoundaryWhite
case charDelimiter:
// Word boundary after a delimiter character
return bonusBoundaryDelimiter
case charNonWord:
// Word boundary
return bonusBoundary
}
}
if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber {
// camelCase letter123
return bonusCamel123
}
switch class {
case charNonWord, charDelimiter:
return bonusNonWord
case charWhite:
return bonusBoundaryWhite
}
return 0
}
func normalizeRune(r rune) rune {
if r < 0x00C0 || r > 0x2184 {
return r
}
n := normalized[r]
if n > 0 {
return n
}
return r
}
func trySkip(input *Chars, caseSensitive bool, b byte, from int) int {
byteArray := input.Bytes()[from:]
idx := bytes.IndexByte(byteArray, b)
if idx == 0 {
// Can't skip any further
return from
}
// We may need to search for the uppercase letter again. We don't have to
// consider normalization as we can be sure that this is an ASCII string.
if !caseSensitive && b >= 'a' && b <= 'z' {
if idx > 0 {
byteArray = byteArray[:idx]
}
uidx := bytes.IndexByte(byteArray, b-32)
if uidx >= 0 {
idx = uidx
}
}
if idx < 0 {
return -1
}
return from + idx
}
func isAscii(runes []rune) bool {
for _, r := range runes {
if r >= utf8.RuneSelf {
return false
}
}
return true
}
func asciiFuzzyIndex(input *Chars, pattern []rune, caseSensitive bool) (int, int) {
// Can't determine
if !input.IsBytes() {
return 0, input.Length()
}
// Not possible
if !isAscii(pattern) {
return -1, -1
}
firstIdx, idx, lastIdx := 0, 0, 0
var b byte
for pidx := 0; pidx < len(pattern); pidx++ {
b = byte(pattern[pidx])
idx = trySkip(input, caseSensitive, b, idx)
if idx < 0 {
return -1, -1
}
if pidx == 0 && idx > 0 {
// Step back to find the right bonus point
firstIdx = idx - 1
}
lastIdx = idx
idx++
}
// Find the last appearance of the last character of the pattern to limit the search scope
bu := b
if !caseSensitive && b >= 'a' && b <= 'z' {
bu = b - 32
}
scope := input.Bytes()[lastIdx:]
for offset := len(scope) - 1; offset > 0; offset-- {
if scope[offset] == b || scope[offset] == bu {
return firstIdx, lastIdx + offset + 1
}
}
return firstIdx, lastIdx + 1
}
type Slab struct {
I16 []int16
I32 []int32
}
const (
caseSensitive = false
normalize = true
forward = true
)
func fuzzyMatch(input *Chars, pattern []rune) (Result, *[]int) {
var slab *Slab
// Assume that pattern is given in lowercase if case-insensitive.
// First check if there's a match and calculate bonus for each position.
// If the input string is too long, consider finding the matching chars in
// this phase as well (non-optimal alignment).
M := len(pattern)
if M == 0 {
return Result{0, 0, 0}, posArray(true, M)
}
N := input.Length()
if M > N {
return Result{-1, -1, 0}, nil
}
// Phase 1. Optimized search for ASCII string
minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)
if minIdx < 0 {
return Result{-1, -1, 0}, nil
}
// fmt.Println(N, maxIdx, idx, maxIdx-idx, input.ToString())
N = maxIdx - minIdx
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
offset16 := 0
offset32 := 0
offset16, H0 := alloc16(offset16, slab, N)
offset16, C0 := alloc16(offset16, slab, N)
// Bonus point for each position
offset16, B := alloc16(offset16, slab, N)
// The first occurrence of each character in the pattern
offset32, F := alloc32(offset32, slab, M)
// Rune array
_, T := alloc32(offset32, slab, N)
input.CopyRunes(T, minIdx)
// Phase 2. Calculate bonus for each point
maxScore, maxScorePos := int16(0), 0
pidx, lastIdx := 0, 0
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false
for off, char := range T {
var class charClass
if char <= unicode.MaxASCII {
class = asciiCharClasses[char]
if !caseSensitive && class == charUpper {
char += 32
T[off] = char
}
} else {
class = charClassOfNonAscii(char)
if !caseSensitive && class == charUpper {
char = unicode.To(unicode.LowerCase, char)
}
if normalize {
char = normalizeRune(char)
}
T[off] = char
}
bonus := bonusMatrix[prevClass][class]
B[off] = bonus
prevClass = class
if char == pchar {
if pidx < M {
F[pidx] = int32(off)
pidx++
pchar = pattern[min(pidx, M-1)]
}
lastIdx = off
}
if char == pchar0 {
score := scoreMatch + bonus*bonusFirstCharMultiplier
H0[off] = score
C0[off] = 1
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, off
if forward && bonus >= bonusBoundary {
break
}
}
inGap = false
} else {
if inGap {
H0[off] = max(prevH0+scoreGapExtension, 0)
} else {
H0[off] = max(prevH0+scoreGapStart, 0)
}
C0[off] = 0
inGap = true
}
prevH0 = H0[off]
}
if pidx != M {
return Result{-1, -1, 0}, nil
}
if M == 1 {
result := Result{minIdx + maxScorePos, minIdx + maxScorePos + 1, int(maxScore)}
pos := []int{minIdx + maxScorePos}
return result, &pos
}
// Phase 3. Fill in score matrix (H)
// Unlike the original algorithm, we do not allow omission.
f0 := int(F[0])
width := lastIdx - f0 + 1
offset16, H := alloc16(offset16, slab, width*M)
copy(H, H0[f0:lastIdx+1])
// Possible length of consecutive chunk at each position.
_, C := alloc16(offset16, slab, width*M)
copy(C, C0[f0:lastIdx+1])
Fsub := F[1:]
Psub := pattern[1:][:len(Fsub)]
for off, f := range Fsub {
f := int(f)
pchar := Psub[off]
pidx := off + 1
row := pidx * width
inGap := false
Tsub := T[f : lastIdx+1]
Bsub := B[f:][:len(Tsub)]
Csub := C[row+f-f0:][:len(Tsub)]
Cdiag := C[row+f-f0-1-width:][:len(Tsub)]
Hsub := H[row+f-f0:][:len(Tsub)]
Hdiag := H[row+f-f0-1-width:][:len(Tsub)]
Hleft := H[row+f-f0-1:][:len(Tsub)]
Hleft[0] = 0
for off, char := range Tsub {
col := off + f
var s1, s2, consecutive int16
if inGap {
s2 = Hleft[off] + scoreGapExtension
} else {
s2 = Hleft[off] + scoreGapStart
}
if pchar == char {
s1 = Hdiag[off] + scoreMatch
b := Bsub[off]
consecutive = Cdiag[off] + 1
if consecutive > 1 {
fb := B[col-int(consecutive)+1]
// Break consecutive chunk
if b >= bonusBoundary && b > fb {
consecutive = 1
} else {
b = max(b, max(bonusConsecutive, fb))
}
}
if s1+b < s2 {
s1 += Bsub[off]
consecutive = 0
} else {
s1 += b
}
}
Csub[off] = consecutive
inGap = s1 < s2
score := max(max(s1, s2), 0)
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, col
}
Hsub[off] = score
}
}
// Phase 4. (Optional) Backtrace to find character positions
pos := posArray(true, M)
j := f0
i := M - 1
j = maxScorePos
preferMatch := true
for {
I := i * width
j0 := j - f0
s := H[I+j0]
var s1, s2 int16
if i > 0 && j >= int(F[i]) {
s1 = H[I-width+j0-1]
}
if j > int(F[i]) {
s2 = H[I+j0-1]
}
if s > s1 && (s > s2 || s == s2 && preferMatch) {
*pos = append(*pos, j+minIdx)
if i == 0 {
break
}
i--
}
preferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0
j--
}
// Start offset we return here is only relevant when begin tiebreak is used.
// However finding the accurate offset requires backtracking, and we don't
// want to pay extra cost for the option that has lost its importance.
return Result{minIdx + j, minIdx + maxScorePos + 1, int(maxScore)}, pos
}
================================================
FILE: internal/fuzzy/chars.go
================================================
package fuzzy
import (
"bytes"
"unicode"
"unicode/utf8"
"unsafe"
)
const (
overflow64 uint64 = 0x8080808080808080
overflow32 uint32 = 0x80808080
)
type Chars struct {
slice []byte // or []rune
inBytes bool
trimLengthKnown bool
trimLength uint16
// XXX Piggybacking item index here is a horrible idea. But I'm trying to
// minimize the memory footprint by not wasting padded spaces.
Index int32
}
func checkAscii(bytes []byte) (bool, int) {
i := 0
for ; i <= len(bytes)-8; i += 8 {
if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 {
return false, i
}
}
for ; i <= len(bytes)-4; i += 4 {
if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 {
return false, i
}
}
for ; i < len(bytes); i++ {
if bytes[i] >= utf8.RuneSelf {
return false, i
}
}
return true, 0
}
// ToChars converts byte array into rune array
func ToChars(bytes []byte) Chars {
inBytes, bytesUntil := checkAscii(bytes)
if inBytes {
return Chars{slice: bytes, inBytes: inBytes}
}
runes := make([]rune, bytesUntil, len(bytes))
for i := 0; i < bytesUntil; i++ {
runes[i] = rune(bytes[i])
}
for i := bytesUntil; i < len(bytes); {
r, sz := utf8.DecodeRune(bytes[i:])
i += sz
runes = append(runes, r)
}
return RunesToChars(runes)
}
func RunesToChars(runes []rune) Chars {
return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false}
}
func (chars *Chars) IsBytes() bool {
return chars.inBytes
}
func (chars *Chars) Bytes() []byte {
return chars.slice
}
func (chars *Chars) NumLines(atMost int) (int, bool) {
lines := 1
if runes := chars.optionalRunes(); runes != nil {
for _, r := range runes {
if r == '\n' {
lines++
}
if lines > atMost {
return atMost, true
}
}
return lines, false
}
for idx := 0; idx < len(chars.slice); idx++ {
found := bytes.IndexByte(chars.slice[idx:], '\n')
if found < 0 {
break
}
idx += found
lines++
if lines > atMost {
return atMost, true
}
}
return lines, false
}
func (chars *Chars) optionalRunes() []rune {
if chars.inBytes {
return nil
}
return *(*[]rune)(unsafe.Pointer(&chars.slice))
}
func (chars *Chars) Get(i int) rune {
if runes := chars.optionalRunes(); runes != nil {
return runes[i]
}
return rune(chars.slice[i])
}
func (chars *Chars) Length() int {
if runes := chars.optionalRunes(); runes != nil {
return len(runes)
}
return len(chars.slice)
}
// TrimLength returns the length after trimming leading and trailing whitespaces
func (chars *Chars) TrimLength() uint16 {
if chars.trimLengthKnown {
return chars.trimLength
}
chars.trimLengthKnown = true
var i int
len := chars.Length()
for i = len - 1; i >= 0; i-- {
char := chars.Get(i)
if !unicode.IsSpace(char) {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len; j++ {
char := chars.Get(j)
if !unicode.IsSpace(char) {
break
}
}
chars.trimLength = AsUint16(i - j + 1)
return chars.trimLength
}
func (chars *Chars) LeadingWhitespaces() int {
whitespaces := 0
for i := 0; i < chars.Length(); i++ {
char := chars.Get(i)
if !unicode.IsSpace(char) {
break
}
whitespaces++
}
return whitespaces
}
func (chars *Chars) TrailingWhitespaces() int {
whitespaces := 0
for i := chars.Length() - 1; i >= 0; i-- {
char := chars.Get(i)
if !unicode.IsSpace(char) {
break
}
whitespaces++
}
return whitespaces
}
func (chars *Chars) TrimTrailingWhitespaces() {
whitespaces := chars.TrailingWhitespaces()
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
}
func (chars *Chars) TrimSuffix(runes []rune) {
lastIdx := len(chars.slice)
firstIdx := lastIdx - len(runes)
if firstIdx < 0 {
return
}
for i := firstIdx; i < lastIdx; i++ {
char := chars.Get(i)
if char != runes[i-firstIdx] {
return
}
}
chars.slice = chars.slice[0:firstIdx]
}
func (chars *Chars) SliceRight(last int) {
chars.slice = chars.slice[:last]
}
func (chars *Chars) ToString() string {
if runes := chars.optionalRunes(); runes != nil {
return string(runes)
}
return unsafe.String(unsafe.SliceData(chars.slice), len(chars.slice))
}
func (chars *Chars) ToRunes() []rune {
if runes := chars.optionalRunes(); runes != nil {
return runes
}
bytes := chars.slice
runes := make([]rune, len(bytes))
for idx, b := range bytes {
runes[idx] = rune(b)
}
return runes
}
func (chars *Chars) CopyRunes(dest []rune, from int) {
if runes := chars.optionalRunes(); runes != nil {
copy(dest, runes[from:])
return
}
for idx, b := range chars.slice[from:][:len(dest)] {
dest[idx] = rune(b)
}
}
func (chars *Chars) Prepend(prefix string) {
if runes := chars.optionalRunes(); runes != nil {
runes = append([]rune(prefix), runes...)
chars.slice = *(*[]byte)(unsafe.Pointer(&runes))
} else {
chars.slice = append([]byte(prefix), chars.slice...)
}
}
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {
text := make([]rune, chars.Length())
copy(text, chars.ToRunes())
lines := [][]rune{}
overflow := false
if !multiLine {
lines = append(lines, text)
} else {
from := 0
for off := 0; off < len(text); off++ {
if text[off] == '\n' {
lines = append(lines, text[from:off+1]) // Include '\n'
from = off + 1
if len(lines) >= maxLines {
break
}
}
}
var lastLine []rune
if from < len(text) {
lastLine = text[from:]
}
overflow = false
if len(lines) >= maxLines {
overflow = true
} else {
lines = append(lines, lastLine)
}
}
// If wrapping is disabled, we're done
if wrapCols == 0 {
return lines, overflow
}
wrapped := [][]rune{}
for _, line := range lines {
// Remove trailing '\n' and remember if it was there
newline := len(line) > 0 && line[len(line)-1] == '\n'
if newline {
line = line[:len(line)-1]
}
hasWrapSign := false
for {
cols := wrapCols
if hasWrapSign {
cols -= wrapSignWidth
}
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
if overflowIdx >= 0 {
// Might be a wide character
if overflowIdx == 0 {
overflowIdx = 1
}
if len(wrapped) >= maxLines {
return wrapped, true
}
wrapped = append(wrapped, line[:overflowIdx])
hasWrapSign = true
line = line[overflowIdx:]
continue
}
hasWrapSign = false
// Restore trailing '\n'
if newline {
line = append(line, '\n')
}
if len(wrapped) >= maxLines {
return wrapped, true
}
wrapped = append(wrapped, line)
break
}
}
return wrapped, overflow
}
================================================
FILE: internal/fuzzy/chars_test.go
================================================
package fuzzy
import (
"fmt"
"testing"
)
func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar"))
if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes {
t.Error()
}
}
func TestCharsLength(t *testing.T) {
chars := ToChars([]byte("\tabc한글 "))
if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {
t.Error()
}
}
func TestCharsToString(t *testing.T) {
text := "\tabc한글 "
chars := ToChars([]byte(text))
if chars.ToString() != text {
t.Error()
}
}
func TestTrimLength(t *testing.T) {
check := func(str string, exp uint16) {
chars := ToChars([]byte(str))
trimmed := chars.TrimLength()
if trimmed != exp {
t.Errorf("Invalid TrimLength result for '%s': %d (expected %d)",
str, trimmed, exp)
}
}
check("hello", 5)
check("hello ", 5)
check("hello ", 5)
check(" hello", 5)
check(" hello", 5)
check(" hello ", 5)
check(" hello ", 5)
check("h o", 5)
check(" h o ", 5)
check(" ", 0)
}
func TestCharsLines(t *testing.T) {
chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop)
fmt.Println(lines, overflow)
if len(lines) != expectedNumLines || overflow != expectedOverflow {
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
}
}
// No wrap
check(true, 1, 0, 0, 8, 1, true)
check(true, 2, 0, 0, 8, 2, true)
check(true, 3, 0, 0, 8, 3, false)
// Wrap (2)
check(true, 4, 2, 0, 8, 4, true)
check(true, 5, 2, 0, 8, 5, true)
check(true, 6, 2, 0, 8, 6, true)
check(true, 7, 2, 0, 8, 7, true)
check(true, 8, 2, 0, 8, 8, true)
check(true, 9, 2, 0, 8, 9, false)
check(true, 9, 2, 0, 1, 8, false) // Smaller tab size
// With wrap sign (3 + 1)
check(true, 100, 3, 1, 1, 8, false)
// With wrap sign (3 + 2)
check(true, 100, 3, 2, 1, 10, false)
// With wrap sign (3 + 2) and no multi-line
check(false, 100, 3, 2, 1, 13, false)
}
================================================
FILE: internal/fuzzy/find.go
================================================
package fuzzy
type Match struct {
Index int
Str string
Score int
Pos []int
}
func Find(pattern []rune, array []string) *Match {
var result Result
var pos *[]int
foundIndex := -1
for i := range array {
input := ToChars([]byte(array[i]))
r, p := fuzzyMatch(&input, pattern)
if r.Score > result.Score {
result = r
pos = p
foundIndex = i
}
}
if foundIndex >= 0 && pos != nil {
return &Match{
Index: foundIndex,
Str: array[foundIndex],
Score: result.Score,
Pos: *pos,
}
}
return nil
}
================================================
FILE: internal/fuzzy/fuzzy_test.go
================================================
package fuzzy
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFind(t *testing.T) {
tests := []struct {
name string
pattern string
array []string
expectNil bool
expectIdx int
expectStr string
}{
{
name: "exact match",
pattern: "hello",
array: []string{"world", "hello", "foo"},
expectNil: false,
expectIdx: 1,
expectStr: "hello",
},
{
name: "fuzzy match",
pattern: "hlo",
array: []string{"world", "hello", "foo"},
expectNil: false,
expectIdx: 1,
expectStr: "hello",
},
{
name: "no match",
pattern: "xyz",
array: []string{"hello", "world", "foo"},
expectNil: true,
},
{
name: "empty array",
pattern: "hello",
array: []string{},
expectNil: true,
},
{
name: "empty pattern",
pattern: "",
array: []string{"hello", "world"},
expectNil: true, // Empty pattern returns nil
},
{
name: "single char pattern",
pattern: "w",
array: []string{"hello", "world"},
expectNil: false,
expectIdx: 1,
expectStr: "world",
},
{
name: "best match selected",
pattern: "foo",
array: []string{"foobar", "foo", "barfoo"},
expectNil: false,
expectIdx: 0, // Algorithm scores "foobar" highest due to consecutive bonus
expectStr: "foobar",
},
{
name: "case insensitive",
pattern: "hello",
array: []string{"HELLO", "world"},
expectNil: false,
expectIdx: 0,
expectStr: "HELLO",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := Find([]rune(tc.pattern), tc.array)
if tc.expectNil {
assert.Nil(t, result)
} else {
require.NotNil(t, result)
assert.Equal(t, tc.expectIdx, result.Index)
assert.Equal(t, tc.expectStr, result.Str)
assert.GreaterOrEqual(t, result.Score, 0)
}
})
}
}
func TestFuzzyMatch(t *testing.T) {
tests := []struct {
name string
input string
pattern string
expectMatch bool
}{
{"exact match", "hello", "hello", true},
{"prefix match", "hello", "hel", true},
{"suffix match", "hello", "llo", true},
{"scattered match", "hello", "hlo", true},
{"no match", "hello", "xyz", false},
{"empty pattern", "hello", "", true},
{"pattern longer than input", "hi", "hello", false},
{"case insensitive", "HELLO", "hello", true},
{"unicode input", "你好世界", "好界", true},
{"unicode no match", "你好世界", "abc", false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
input := ToChars([]byte(tc.input))
result, pos := fuzzyMatch(&input, []rune(tc.pattern))
if tc.expectMatch {
assert.GreaterOrEqual(t, result.Start, 0, "Expected match but got Start=-1")
if tc.pattern != "" {
assert.NotNil(t, pos)
}
} else {
assert.Equal(t, -1, result.Start, "Expected no match but got Start>=0")
}
})
}
}
func TestFuzzyMatchScoring(t *testing.T) {
input1 := ToChars([]byte("foobar"))
input2 := ToChars([]byte("foo_bar"))
// Exact prefix should score higher
result1, _ := fuzzyMatch(&input1, []rune("foo"))
result2, _ := fuzzyMatch(&input2, []rune("foo"))
assert.Greater(t, result1.Score, 0)
assert.Greater(t, result2.Score, 0)
}
func TestNormalizeRune(t *testing.T) {
tests := []struct {
name string
input rune
expected rune
}{
{"ascii a", 'a', 'a'},
{"ascii A", 'A', 'A'},
{"ascii 0", '0', '0'},
{"a with acute", 'á', 'a'},
{"a with grave", 'à', 'a'},
{"a with circumflex", 'â', 'a'},
{"e with acute", 'é', 'e'},
{"o with umlaut", 'ö', 'o'},
{"n with tilde", 'ñ', 'n'},
{"c with cedilla", 'ç', 'c'},
// Capital letters
{"A with acute", 'Á', 'A'},
{"O with umlaut", 'Ö', 'O'},
// Characters outside normalization range
{"chinese", '中', '中'},
{"emoji", '😀', '😀'},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := normalizeRune(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}
func TestNormalizeRunes(t *testing.T) {
tests := []struct {
name string
input []rune
expected []rune
}{
{"ascii only", []rune("hello"), []rune("hello")},
{"with accents", []rune("héllo"), []rune("hello")},
{"all accents", []rune("àéîõü"), []rune("aeiou")},
{"mixed", []rune("café"), []rune("cafe")},
{"empty", []rune(""), []rune("")},
{"no change needed", []rune("test123"), []rune("test123")},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := NormalizeRunes(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}
func TestCharClass(t *testing.T) {
tests := []struct {
name string
char rune
expected charClass
}{
{"lowercase a", 'a', charLower},
{"lowercase z", 'z', charLower},
{"uppercase A", 'A', charUpper},
{"uppercase Z", 'Z', charUpper},
{"digit 0", '0', charNumber},
{"digit 9", '9', charNumber},
{"space", ' ', charWhite},
{"tab", '\t', charWhite},
{"newline", '\n', charWhite},
{"dot delimiter", '.', charDelimiter},
{"special char", '@', charNonWord},
{"underscore", '_', charNonWord},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := charClassOf(tc.char)
assert.Equal(t, tc.expected, result)
})
}
}
func TestCharClassOfNonAscii(t *testing.T) {
tests := []struct {
name string
char rune
expected charClass
}{
{"unicode lower", 'ä', charLower},
{"unicode upper", 'Ä', charUpper},
{"unicode number", '①', charNumber},
{"unicode letter", '中', charLetter},
{"unicode space", '\u00A0', charWhite}, // non-breaking space
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := charClassOfNonAscii(tc.char)
assert.Equal(t, tc.expected, result)
})
}
}
func TestBonusFor(t *testing.T) {
// Test word boundary bonuses
assert.Equal(t, bonusBoundaryWhite, bonusFor(charWhite, charLower))
assert.Equal(t, bonusBoundaryDelimiter, bonusFor(charDelimiter, charLower))
assert.Equal(t, int16(bonusBoundary), bonusFor(charNonWord, charLower))
// Test camelCase bonus
assert.Equal(t, int16(bonusCamel123), bonusFor(charLower, charUpper))
// Test number after non-number
assert.Equal(t, int16(bonusCamel123), bonusFor(charLower, charNumber))
assert.Equal(t, int16(0), bonusFor(charNumber, charNumber))
}
func TestAsUint16(t *testing.T) {
tests := []struct {
name string
input int
expected uint16
}{
{"zero", 0, 0},
{"positive", 100, 100},
{"max uint16", 65535, 65535},
{"above max", 70000, 65535},
{"negative", -1, 0},
{"large negative", -1000, 0},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := AsUint16(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}
func TestStringWidth(t *testing.T) {
tests := []struct {
name string
input string
expected int
}{
{"ascii", "hello", 5},
{"empty", "", 0},
{"with newline", "a\nb", 3}, // 2 chars + 1 for newline
{"with cr", "a\rb", 3},
{"wide chars", "你好", 4}, // each Chinese char is 2 columns
{"mixed", "ab你好cd", 8}, // 4 ascii + 4 wide
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := StringWidth(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}
func TestRunesWidth(t *testing.T) {
tests := []struct {
name string
runes []rune
prefixWidth int
tabstop int
limit int
expectWidth int
expectOverIdx int
}{
{
name: "ascii within limit",
runes: []rune("hello"),
prefixWidth: 0,
tabstop: 8,
limit: 10,
expectWidth: 5,
expectOverIdx: -1,
},
{
name: "ascii exceeds limit",
runes: []rune("hello"),
prefixWidth: 0,
tabstop: 8,
limit: 3,
expectWidth: 4,
expectOverIdx: 3,
},
{
name: "tab expansion",
runes: []rune("\t"),
prefixWidth: 0,
tabstop: 8,
limit: 10,
expectWidth: 8,
expectOverIdx: -1,
},
{
name: "tab with prefix",
runes: []rune("\t"),
prefixWidth: 3,
tabstop: 8,
limit: 10,
expectWidth: 5, // 8 - 3 = 5
expectOverIdx: -1,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
width, overIdx := RunesWidth(tc.runes, tc.prefixWidth, tc.tabstop, tc.limit)
assert.Equal(t, tc.expectWidth, width)
assert.Equal(t, tc.expectOverIdx, overIdx)
})
}
}
func TestTrySkip(t *testing.T) {
tests := []struct {
name string
input string
caseSensitive bool
b byte
from int
expected int
}{
{"find at start", "hello", false, 'h', 0, 0},
{"find in middle", "hello", false, 'l', 0, 2},
{"find from offset", "hello", false, 'l', 3, 3},
{"not found", "hello", false, 'x', 0, -1},
{"case insensitive upper", "HELLO", false, 'h', 0, 0},
{"case sensitive no match", "HELLO", true, 'h', 0, -1},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
input := ToChars([]byte(tc.input))
result := trySkip(&input, tc.caseSensitive, tc.b, tc.from)
assert.Equal(t, tc.expected, result)
})
}
}
func TestIsAscii(t *testing.T) {
tests := []struct {
name string
runes []rune
expected bool
}{
{"ascii only", []rune("hello"), true},
{"empty", []rune(""), true},
{"with unicode", []rune("hello世界"), false},
{"unicode only", []rune("世界"), false},
{"edge of ascii", []rune{127}, true},
{"beyond ascii", []rune{128}, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := isAscii(tc.runes)
assert.Equal(t, tc.expected, result)
})
}
}
func TestAsciiFuzzyIndex(t *testing.T) {
tests := []struct {
name string
input string
pattern string
caseSensitive bool
expectMin int
expectMax int
}{
{"exact match", "hello", "hello", false, 0, 5},
{"partial match", "hello world", "wor", false, 5, 9},
{"no match", "hello", "xyz", false, -1, -1},
{"case insensitive", "HELLO", "hel", false, 0, 3},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
input := ToChars([]byte(tc.input))
minIdx, maxIdx := asciiFuzzyIndex(&input, []rune(tc.pattern), tc.caseSensitive)
assert.Equal(t, tc.expectMin, minIdx)
if tc.expectMin >= 0 {
assert.GreaterOrEqual(t, maxIdx, tc.expectMax)
}
})
}
}
================================================
FILE: internal/fuzzy/normalize.go
================================================
// Normalization of latin script letters
// Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt
package fuzzy
var normalized = map[rune]rune{
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER
0x00E2: 'a', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00E4: 'a', // WITH DIAERESIS, LATIN SMALL LETTER
0x0227: 'a', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1EA1: 'a', // WITH DOT BELOW, LATIN SMALL LETTER
0x0201: 'a', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00E0: 'a', // WITH GRAVE, LATIN SMALL LETTER
0x1EA3: 'a', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x0203: 'a', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x0101: 'a', // WITH MACRON, LATIN SMALL LETTER
0x0105: 'a', // WITH OGONEK, LATIN SMALL LETTER
0x1E9A: 'a', // WITH RIGHT HALF RING, LATIN SMALL LETTER
0x00E5: 'a', // WITH RING ABOVE, LATIN SMALL LETTER
0x1E01: 'a', // WITH RING BELOW, LATIN SMALL LETTER
0x00E3: 'a', // WITH TILDE, LATIN SMALL LETTER
0x0363: 'a', // , COMBINING LATIN SMALL LETTER
0x0250: 'a', // , LATIN SMALL LETTER TURNED
0x1E03: 'b', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E05: 'b', // WITH DOT BELOW, LATIN SMALL LETTER
0x0253: 'b', // WITH HOOK, LATIN SMALL LETTER
0x1E07: 'b', // WITH LINE BELOW, LATIN SMALL LETTER
0x0180: 'b', // WITH STROKE, LATIN SMALL LETTER
0x0183: 'b', // WITH TOPBAR, LATIN SMALL LETTER
0x0107: 'c', // WITH ACUTE, LATIN SMALL LETTER
0x010D: 'c', // WITH CARON, LATIN SMALL LETTER
0x00E7: 'c', // WITH CEDILLA, LATIN SMALL LETTER
0x0109: 'c', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0255: 'c', // WITH CURL, LATIN SMALL LETTER
0x010B: 'c', // WITH DOT ABOVE, LATIN SMALL LETTER
0x0188: 'c', // WITH HOOK, LATIN SMALL LETTER
0x023C: 'c', // WITH STROKE, LATIN SMALL LETTER
0x0368: 'c', // , COMBINING LATIN SMALL LETTER
0x0297: 'c', // , LATIN LETTER STRETCHED
0x2184: 'c', // , LATIN SMALL LETTER REVERSED
0x010F: 'd', // WITH CARON, LATIN SMALL LETTER
0x1E11: 'd', // WITH CEDILLA, LATIN SMALL LETTER
0x1E13: 'd', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x0221: 'd', // WITH CURL, LATIN SMALL LETTER
0x1E0B: 'd', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E0D: 'd', // WITH DOT BELOW, LATIN SMALL LETTER
0x0257: 'd', // WITH HOOK, LATIN SMALL LETTER
0x1E0F: 'd', // WITH LINE BELOW, LATIN SMALL LETTER
0x0111: 'd', // WITH STROKE, LATIN SMALL LETTER
0x0256: 'd', // WITH TAIL, LATIN SMALL LETTER
0x018C: 'd', // WITH TOPBAR, LATIN SMALL LETTER
0x0369: 'd', // , COMBINING LATIN SMALL LETTER
0x00E9: 'e', // WITH ACUTE, LATIN SMALL LETTER
0x0115: 'e', // WITH BREVE, LATIN SMALL LETTER
0x011B: 'e', // WITH CARON, LATIN SMALL LETTER
0x0229: 'e', // WITH CEDILLA, LATIN SMALL LETTER
0x1E19: 'e', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x00EA: 'e', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00EB: 'e', // WITH DIAERESIS, LATIN SMALL LETTER
0x0117: 'e', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1EB9: 'e', // WITH DOT BELOW, LATIN SMALL LETTER
0x0205: 'e', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00E8: 'e', // WITH GRAVE, LATIN SMALL LETTER
0x1EBB: 'e', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x025D: 'e', // WITH HOOK, LATIN SMALL LETTER REVERSED OPEN
0x0207: 'e', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x0113: 'e', // WITH MACRON, LATIN SMALL LETTER
0x0119: 'e', // WITH OGONEK, LATIN SMALL LETTER
0x0247: 'e', // WITH STROKE, LATIN SMALL LETTER
0x1E1B: 'e', // WITH TILDE BELOW, LATIN SMALL LETTER
0x1EBD: 'e', // WITH TILDE, LATIN SMALL LETTER
0x0364: 'e', // , COMBINING LATIN SMALL LETTER
0x029A: 'e', // , LATIN SMALL LETTER CLOSED OPEN
0x025E: 'e', // , LATIN SMALL LETTER CLOSED REVERSED OPEN
0x025B: 'e', // , LATIN SMALL LETTER OPEN
0x0258: 'e', // , LATIN SMALL LETTER REVERSED
0x025C: 'e', // , LATIN SMALL LETTER REVERSED OPEN
0x01DD: 'e', // , LATIN SMALL LETTER TURNED
0x1D08: 'e', // , LATIN SMALL LETTER TURNED OPEN
0x1E1F: 'f', // WITH DOT ABOVE, LATIN SMALL LETTER
0x0192: 'f', // WITH HOOK, LATIN SMALL LETTER
0x01F5: 'g', // WITH ACUTE, LATIN SMALL LETTER
0x011F: 'g', // WITH BREVE, LATIN SMALL LETTER
0x01E7: 'g', // WITH CARON, LATIN SMALL LETTER
0x0123: 'g', // WITH CEDILLA, LATIN SMALL LETTER
0x011D: 'g', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0121: 'g', // WITH DOT ABOVE, LATIN SMALL LETTER
0x0260: 'g', // WITH HOOK, LATIN SMALL LETTER
0x1E21: 'g', // WITH MACRON, LATIN SMALL LETTER
0x01E5: 'g', // WITH STROKE, LATIN SMALL LETTER
0x0261: 'g', // , LATIN SMALL LETTER SCRIPT
0x1E2B: 'h', // WITH BREVE BELOW, LATIN SMALL LETTER
0x021F: 'h', // WITH CARON, LATIN SMALL LETTER
0x1E29: 'h', // WITH CEDILLA, LATIN SMALL LETTER
0x0125: 'h', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x1E27: 'h', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E23: 'h', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E25: 'h', // WITH DOT BELOW, LATIN SMALL LETTER
0x02AE: 'h', // WITH FISHHOOK, LATIN SMALL LETTER TURNED
0x0266: 'h', // WITH HOOK, LATIN SMALL LETTER
0x1E96: 'h', // WITH LINE BELOW, LATIN SMALL LETTER
0x0127: 'h', // WITH STROKE, LATIN SMALL LETTER
0x036A: 'h', // , COMBINING LATIN SMALL LETTER
0x0265: 'h', // , LATIN SMALL LETTER TURNED
0x2095: 'h', // , LATIN SUBSCRIPT SMALL LETTER
0x00ED: 'i', // WITH ACUTE, LATIN SMALL LETTER
0x012D: 'i', // WITH BREVE, LATIN SMALL LETTER
0x01D0: 'i', // WITH CARON, LATIN SMALL LETTER
0x00EE: 'i', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00EF: 'i', // WITH DIAERESIS, LATIN SMALL LETTER
0x1ECB: 'i', // WITH DOT BELOW, LATIN SMALL LETTER
0x0209: 'i', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00EC: 'i', // WITH GRAVE, LATIN SMALL LETTER
0x1EC9: 'i', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x020B: 'i', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x012B: 'i', // WITH MACRON, LATIN SMALL LETTER
0x012F: 'i', // WITH OGONEK, LATIN SMALL LETTER
0x0268: 'i', // WITH STROKE, LATIN SMALL LETTER
0x1E2D: 'i', // WITH TILDE BELOW, LATIN SMALL LETTER
0x0129: 'i', // WITH TILDE, LATIN SMALL LETTER
0x0365: 'i', // , COMBINING LATIN SMALL LETTER
0x0131: 'i', // , LATIN SMALL LETTER DOTLESS
0x1D09: 'i', // , LATIN SMALL LETTER TURNED
0x1D62: 'i', // , LATIN SUBSCRIPT SMALL LETTER
0x2071: 'i', // , SUPERSCRIPT LATIN SMALL LETTER
0x01F0: 'j', // WITH CARON, LATIN SMALL LETTER
0x0135: 'j', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x029D: 'j', // WITH CROSSED-TAIL, LATIN SMALL LETTER
0x0249: 'j', // WITH STROKE, LATIN SMALL LETTER
0x025F: 'j', // WITH STROKE, LATIN SMALL LETTER DOTLESS
0x0237: 'j', // , LATIN SMALL LETTER DOTLESS
0x1E31: 'k', // WITH ACUTE, LATIN SMALL LETTER
0x01E9: 'k', // WITH CARON, LATIN SMALL LETTER
0x0137: 'k', // WITH CEDILLA, LATIN SMALL LETTER
0x1E33: 'k', // WITH DOT BELOW, LATIN SMALL LETTER
0x0199: 'k', // WITH HOOK, LATIN SMALL LETTER
0x1E35: 'k', // WITH LINE BELOW, LATIN SMALL LETTER
0x029E: 'k', // , LATIN SMALL LETTER TURNED
0x2096: 'k', // , LATIN SUBSCRIPT SMALL LETTER
0x013A: 'l', // WITH ACUTE, LATIN SMALL LETTER
0x019A: 'l', // WITH BAR, LATIN SMALL LETTER
0x026C: 'l', // WITH BELT, LATIN SMALL LETTER
0x013E: 'l', // WITH CARON, LATIN SMALL LETTER
0x013C: 'l', // WITH CEDILLA, LATIN SMALL LETTER
0x1E3D: 'l', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x0234: 'l', // WITH CURL, LATIN SMALL LETTER
0x1E37: 'l', // WITH DOT BELOW, LATIN SMALL LETTER
0x1E3B: 'l', // WITH LINE BELOW, LATIN SMALL LETTER
0x0140: 'l', // WITH MIDDLE DOT, LATIN SMALL LETTER
0x026B: 'l', // WITH MIDDLE TILDE, LATIN SMALL LETTER
0x026D: 'l', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x0142: 'l', // WITH STROKE, LATIN SMALL LETTER
0x2097: 'l', // , LATIN SUBSCRIPT SMALL LETTER
0x1E3F: 'm', // WITH ACUTE, LATIN SMALL LETTER
0x1E41: 'm', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E43: 'm', // WITH DOT BELOW, LATIN SMALL LETTER
0x0271: 'm', // WITH HOOK, LATIN SMALL LETTER
0x0270: 'm', // WITH LONG LEG, LATIN SMALL LETTER TURNED
0x036B: 'm', // , COMBINING LATIN SMALL LETTER
0x1D1F: 'm', // , LATIN SMALL LETTER SIDEWAYS TURNED
0x026F: 'm', // , LATIN SMALL LETTER TURNED
0x2098: 'm', // , LATIN SUBSCRIPT SMALL LETTER
0x0144: 'n', // WITH ACUTE, LATIN SMALL LETTER
0x0148: 'n', // WITH CARON, LATIN SMALL LETTER
0x0146: 'n', // WITH CEDILLA, LATIN SMALL LETTER
0x1E4B: 'n', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x0235: 'n', // WITH CURL, LATIN SMALL LETTER
0x1E45: 'n', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E47: 'n', // WITH DOT BELOW, LATIN SMALL LETTER
0x01F9: 'n', // WITH GRAVE, LATIN SMALL LETTER
0x0272: 'n', // WITH LEFT HOOK, LATIN SMALL LETTER
0x1E49: 'n', // WITH LINE BELOW, LATIN SMALL LETTER
0x019E: 'n', // WITH LONG RIGHT LEG, LATIN SMALL LETTER
0x0273: 'n', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x00F1: 'n', // WITH TILDE, LATIN SMALL LETTER
0x2099: 'n', // , LATIN SUBSCRIPT SMALL LETTER
0x00F3: 'o', // WITH ACUTE, LATIN SMALL LETTER
0x014F: 'o', // WITH BREVE, LATIN SMALL LETTER
0x01D2: 'o', // WITH CARON, LATIN SMALL LETTER
0x00F4: 'o', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00F6: 'o', // WITH DIAERESIS, LATIN SMALL LETTER
0x022F: 'o', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1ECD: 'o', // WITH DOT BELOW, LATIN SMALL LETTER
0x0151: 'o', // WITH DOUBLE ACUTE, LATIN SMALL LETTER
0x020D: 'o', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00F2: 'o', // WITH GRAVE, LATIN SMALL LETTER
0x1ECF: 'o', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x01A1: 'o', // WITH HORN, LATIN SMALL LETTER
0x020F: 'o', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x014D: 'o', // WITH MACRON, LATIN SMALL LETTER
0x01EB: 'o', // WITH OGONEK, LATIN SMALL LETTER
0x00F8: 'o', // WITH STROKE, LATIN SMALL LETTER
0x1D13: 'o', // WITH STROKE, LATIN SMALL LETTER SIDEWAYS
0x00F5: 'o', // WITH TILDE, LATIN SMALL LETTER
0x0366: 'o', // , COMBINING LATIN SMALL LETTER
0x0275: 'o', // , LATIN SMALL LETTER BARRED
0x1D17: 'o', // , LATIN SMALL LETTER BOTTOM HALF
0x0254: 'o', // , LATIN SMALL LETTER OPEN
0x1D11: 'o', // , LATIN SMALL LETTER SIDEWAYS
0x1D12: 'o', // , LATIN SMALL LETTER SIDEWAYS OPEN
0x1D16: 'o', // , LATIN SMALL LETTER TOP HALF
0x1E55: 'p', // WITH ACUTE, LATIN SMALL LETTER
0x1E57: 'p', // WITH DOT ABOVE, LATIN SMALL LETTER
0x01A5: 'p', // WITH HOOK, LATIN SMALL LETTER
0x209A: 'p', // , LATIN SUBSCRIPT SMALL LETTER
0x024B: 'q', // WITH HOOK TAIL, LATIN SMALL LETTER
0x02A0: 'q', // WITH HOOK, LATIN SMALL LETTER
0x0155: 'r', // WITH ACUTE, LATIN SMALL LETTER
0x0159: 'r', // WITH CARON, LATIN SMALL LETTER
0x0157: 'r', // WITH CEDILLA, LATIN SMALL LETTER
0x1E59: 'r', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E5B: 'r', // WITH DOT BELOW, LATIN SMALL LETTER
0x0211: 'r', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x027E: 'r', // WITH FISHHOOK, LATIN SMALL LETTER
0x027F: 'r', // WITH FISHHOOK, LATIN SMALL LETTER REVERSED
0x027B: 'r', // WITH HOOK, LATIN SMALL LETTER TURNED
0x0213: 'r', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x1E5F: 'r', // WITH LINE BELOW, LATIN SMALL LETTER
0x027C: 'r', // WITH LONG LEG, LATIN SMALL LETTER
0x027A: 'r', // WITH LONG LEG, LATIN SMALL LETTER TURNED
0x024D: 'r', // WITH STROKE, LATIN SMALL LETTER
0x027D: 'r', // WITH TAIL, LATIN SMALL LETTER
0x036C: 'r', // , COMBINING LATIN SMALL LETTER
0x0279: 'r', // , LATIN SMALL LETTER TURNED
0x1D63: 'r', // , LATIN SUBSCRIPT SMALL LETTER
0x015B: 's', // WITH ACUTE, LATIN SMALL LETTER
0x0161: 's', // WITH CARON, LATIN SMALL LETTER
0x015F: 's', // WITH CEDILLA, LATIN SMALL LETTER
0x015D: 's', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0219: 's', // WITH COMMA BELOW, LATIN SMALL LETTER
0x1E61: 's', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E9B: 's', // WITH DOT ABOVE, LATIN SMALL LETTER LONG
0x1E63: 's', // WITH DOT BELOW, LATIN SMALL LETTER
0x0282: 's', // WITH HOOK, LATIN SMALL LETTER
0x023F: 's', // WITH SWASH TAIL, LATIN SMALL LETTER
0x017F: 's', // , LATIN SMALL LETTER LONG
0x00DF: 's', // , LATIN SMALL LETTER SHARP
0x209B: 's', // , LATIN SUBSCRIPT SMALL LETTER
0x0165: 't', // WITH CARON, LATIN SMALL LETTER
0x0163: 't', // WITH CEDILLA, LATIN SMALL LETTER
0x1E71: 't', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x021B: 't', // WITH COMMA BELOW, LATIN SMALL LETTER
0x0236: 't', // WITH CURL, LATIN SMALL LETTER
0x1E97: 't', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E6B: 't', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E6D: 't', // WITH DOT BELOW, LATIN SMALL LETTER
0x01AD: 't', // WITH HOOK, LATIN SMALL LETTER
0x1E6F: 't', // WITH LINE BELOW, LATIN SMALL LETTER
0x01AB: 't', // WITH PALATAL HOOK, LATIN SMALL LETTER
0x0288: 't', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x0167: 't', // WITH STROKE, LATIN SMALL LETTER
0x036D: 't', // , COMBINING LATIN SMALL LETTER
0x0287: 't', // , LATIN SMALL LETTER TURNED
0x209C: 't', // , LATIN SUBSCRIPT SMALL LETTER
0x0289: 'u', // BAR, LATIN SMALL LETTER
0x00FA: 'u', // WITH ACUTE, LATIN SMALL LETTER
0x016D: 'u', // WITH BREVE, LATIN SMALL LETTER
0x01D4: 'u', // WITH CARON, LATIN SMALL LETTER
0x1E77: 'u', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x00FB: 'u', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x1E73: 'u', // WITH DIAERESIS BELOW, LATIN SMALL LETTER
0x00FC: 'u', // WITH DIAERESIS, LATIN SMALL LETTER
0x1EE5: 'u', // WITH DOT BELOW, LATIN SMALL LETTER
0x0171: 'u', // WITH DOUBLE ACUTE, LATIN SMALL LETTER
0x0215: 'u', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00F9: 'u', // WITH GRAVE, LATIN SMALL LETTER
0x1EE7: 'u', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x01B0: 'u', // WITH HORN, LATIN SMALL LETTER
0x0217: 'u', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x016B: 'u', // WITH MACRON, LATIN SMALL LETTER
0x0173: 'u', // WITH OGONEK, LATIN SMALL LETTER
0x016F: 'u', // WITH RING ABOVE, LATIN SMALL LETTER
0x1E75: 'u', // WITH TILDE BELOW, LATIN SMALL LETTER
0x0169: 'u', // WITH TILDE, LATIN SMALL LETTER
0x0367: 'u', // , COMBINING LATIN SMALL LETTER
0x1D1D: 'u', // , LATIN SMALL LETTER SIDEWAYS
0x1D1E: 'u', // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED
0x1D64: 'u', // , LATIN SUBSCRIPT SMALL LETTER
0x1E7F: 'v', // WITH DOT BELOW, LATIN SMALL LETTER
0x028B: 'v', // WITH HOOK, LATIN SMALL LETTER
0x1E7D: 'v', // WITH TILDE, LATIN SMALL LETTER
0x036E: 'v', // , COMBINING LATIN SMALL LETTER
0x028C: 'v', // , LATIN SMALL LETTER TURNED
0x1D65: 'v', // , LATIN SUBSCRIPT SMALL LETTER
0x1E83: 'w', // WITH ACUTE, LATIN SMALL LETTER
0x0175: 'w', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x1E85: 'w', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E87: 'w', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E89: 'w', // WITH DOT BELOW, LATIN SMALL LETTER
0x1E81: 'w', // WITH GRAVE, LATIN SMALL LETTER
0x1E98: 'w', // WITH RING ABOVE, LATIN SMALL LETTER
0x028D: 'w', // , LATIN SMALL LETTER TURNED
0x1E8D: 'x', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E8B: 'x', // WITH DOT ABOVE, LATIN SMALL LETTER
0x036F: 'x', // , COMBINING LATIN SMALL LETTER
0x00FD: 'y', // WITH ACUTE, LATIN SMALL LETTER
0x0177: 'y', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00FF: 'y', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E8F: 'y', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1EF5: 'y', // WITH DOT BELOW, LATIN SMALL LETTER
0x1EF3: 'y', // WITH GRAVE, LATIN SMALL LETTER
0x1EF7: 'y', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x01B4: 'y', // WITH HOOK, LATIN SMALL LETTER
0x0233: 'y', // WITH MACRON, LATIN SMALL LETTER
0x1E99: 'y', // WITH RING ABOVE, LATIN SMALL LETTER
0x024F: 'y', // WITH STROKE, LATIN SMALL LETTER
0x1EF9: 'y', // WITH TILDE, LATIN SMALL LETTER
0x028E: 'y', // , LATIN SMALL LETTER TURNED
0x017A: 'z', // WITH ACUTE, LATIN SMALL LETTER
0x017E: 'z', // WITH CARON, LATIN SMALL LETTER
0x1E91: 'z', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0291: 'z', // WITH CURL, LATIN SMALL LETTER
0x017C: 'z', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E93: 'z', // WITH DOT BELOW, LATIN SMALL LETTER
0x0225: 'z', // WITH HOOK, LATIN SMALL LETTER
0x1E95: 'z', // WITH LINE BELOW, LATIN SMALL LETTER
0x0290: 'z', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x01B6: 'z', // WITH STROKE, LATIN SMALL LETTER
0x0240: 'z', // WITH SWASH TAIL, LATIN SMALL LETTER
0x0251: 'a', // , latin small letter script
0x00C1: 'A', // WITH ACUTE, LATIN CAPITAL LETTER
0x00C2: 'A', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00C4: 'A', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00C0: 'A', // WITH GRAVE, LATIN CAPITAL LETTER
0x00C5: 'A', // WITH RING ABOVE, LATIN CAPITAL LETTER
0x023A: 'A', // WITH STROKE, LATIN CAPITAL LETTER
0x00C3: 'A', // WITH TILDE, LATIN CAPITAL LETTER
0x1D00: 'A', // , LATIN LETTER SMALL CAPITAL
0x0181: 'B', // WITH HOOK, LATIN CAPITAL LETTER
0x0243: 'B', // WITH STROKE, LATIN CAPITAL LETTER
0x0299: 'B', // , LATIN LETTER SMALL CAPITAL
0x1D03: 'B', // , LATIN LETTER SMALL CAPITAL BARRED
0x00C7: 'C', // WITH CEDILLA, LATIN CAPITAL LETTER
0x023B: 'C', // WITH STROKE, LATIN CAPITAL LETTER
0x1D04: 'C', // , LATIN LETTER SMALL CAPITAL
0x018A: 'D', // WITH HOOK, LATIN CAPITAL LETTER
0x0189: 'D', // , LATIN CAPITAL LETTER AFRICAN
0x1D05: 'D', // , LATIN LETTER SMALL CAPITAL
0x00C9: 'E', // WITH ACUTE, LATIN CAPITAL LETTER
0x00CA: 'E', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00CB: 'E', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00C8: 'E', // WITH GRAVE, LATIN CAPITAL LETTER
0x0246: 'E', // WITH STROKE, LATIN CAPITAL LETTER
0x0190: 'E', // , LATIN CAPITAL LETTER OPEN
0x018E: 'E', // , LATIN CAPITAL LETTER REVERSED
0x1D07: 'E', // , LATIN LETTER SMALL CAPITAL
0x0193: 'G', // WITH HOOK, LATIN CAPITAL LETTER
0x029B: 'G', // WITH HOOK, LATIN LETTER SMALL CAPITAL
0x0262: 'G', // , LATIN LETTER SMALL CAPITAL
0x029C: 'H', // , LATIN LETTER SMALL CAPITAL
0x00CD: 'I', // WITH ACUTE, LATIN CAPITAL LETTER
0x00CE: 'I', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00CF: 'I', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x0130: 'I', // WITH DOT ABOVE, LATIN CAPITAL LETTER
0x00CC: 'I', // WITH GRAVE, LATIN CAPITAL LETTER
0x0197: 'I', // WITH STROKE, LATIN CAPITAL LETTER
0x026A: 'I', // , LATIN LETTER SMALL CAPITAL
0x0248: 'J', // WITH STROKE, LATIN CAPITAL LETTER
0x1D0A: 'J', // , LATIN LETTER SMALL CAPITAL
0x1D0B: 'K', // , LATIN LETTER SMALL CAPITAL
0x023D: 'L', // WITH BAR, LATIN CAPITAL LETTER
0x1D0C: 'L', // WITH STROKE, LATIN LETTER SMALL CAPITAL
0x029F: 'L', // , LATIN LETTER SMALL CAPITAL
0x019C: 'M', // , LATIN CAPITAL LETTER TURNED
0x1D0D: 'M', // , LATIN LETTER SMALL CAPITAL
0x019D: 'N', // WITH LEFT HOOK, LATIN CAPITAL LETTER
0x0220: 'N', // WITH LONG RIGHT LEG, LATIN CAPITAL LETTER
0x00D1: 'N', // WITH TILDE, LATIN CAPITAL LETTER
0x0274: 'N', // , LATIN LETTER SMALL CAPITAL
0x1D0E: 'N', // , LATIN LETTER SMALL CAPITAL REVERSED
0x00D3: 'O', // WITH ACUTE, LATIN CAPITAL LETTER
0x00D4: 'O', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00D6: 'O', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00D2: 'O', // WITH GRAVE, LATIN CAPITAL LETTER
0x019F: 'O', // WITH MIDDLE TILDE, LATIN CAPITAL LETTER
0x00D8: 'O', // WITH STROKE, LATIN CAPITAL LETTER
0x00D5: 'O', // WITH TILDE, LATIN CAPITAL LETTER
0x0186: 'O', // , LATIN CAPITAL LETTER OPEN
0x1D0F: 'O', // , LATIN LETTER SMALL CAPITAL
0x1D10: 'O', // , LATIN LETTER SMALL CAPITAL OPEN
0x1D18: 'P', // , LATIN LETTER SMALL CAPITAL
0x024A: 'Q', // WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL
0x024C: 'R', // WITH STROKE, LATIN CAPITAL LETTER
0x0280: 'R', // , LATIN LETTER SMALL CAPITAL
0x0281: 'R', // , LATIN LETTER SMALL CAPITAL INVERTED
0x1D19: 'R', // , LATIN LETTER SMALL CAPITAL REVERSED
0x1D1A: 'R', // , LATIN LETTER SMALL CAPITAL TURNED
0x023E: 'T', // WITH DIAGONAL STROKE, LATIN CAPITAL LETTER
0x01AE: 'T', // WITH RETROFLEX HOOK, LATIN CAPITAL LETTER
0x1D1B: 'T', // , LATIN LETTER SMALL CAPITAL
0x0244: 'U', // BAR, LATIN CAPITAL LETTER
0x00DA: 'U', // WITH ACUTE, LATIN CAPITAL LETTER
0x00DB: 'U', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00DC: 'U', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00D9: 'U', // WITH GRAVE, LATIN CAPITAL LETTER
0x1D1C: 'U', // , LATIN LETTER SMALL CAPITAL
0x01B2: 'V', // WITH HOOK, LATIN CAPITAL LETTER
0x0245: 'V', // , LATIN CAPITAL LETTER TURNED
0x1D20: 'V', // , LATIN LETTER SMALL CAPITAL
0x1D21: 'W', // , LATIN LETTER SMALL CAPITAL
0x00DD: 'Y', // WITH ACUTE, LATIN CAPITAL LETTER
0x0178: 'Y', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER
0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL
0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL
'Ắ': 'A',
'Ấ': 'A',
'Ằ': 'A',
'Ầ': 'A',
'Ẳ': 'A',
'Ẩ': 'A',
'Ẵ': 'A',
'Ẫ': 'A',
'Ặ': 'A',
'Ậ': 'A',
'ắ': 'a',
'ấ': 'a',
'ằ': 'a',
'ầ': 'a',
'ẳ': 'a',
'ẩ': 'a',
'ẵ': 'a',
'ẫ': 'a',
'ặ': 'a',
'ậ': 'a',
'Ế': 'E',
'Ề': 'E',
'Ể': 'E',
'Ễ': 'E',
'Ệ': 'E',
'ế': 'e',
'ề': 'e',
'ể': 'e',
'ễ': 'e',
'ệ': 'e',
'Ố': 'O',
'Ớ': 'O',
'Ồ': 'O',
'Ờ': 'O',
'Ổ': 'O',
'Ở': 'O',
'Ỗ': 'O',
'Ỡ': 'O',
'Ộ': 'O',
'Ợ': 'O',
'ố': 'o',
'ớ': 'o',
'ồ': 'o',
'ờ': 'o',
'ổ': 'o',
'ở': 'o',
'ỗ': 'o',
'ỡ': 'o',
'ộ': 'o',
'ợ': 'o',
'Ứ': 'U',
'Ừ': 'U',
'Ử': 'U',
'Ữ': 'U',
'Ự': 'U',
'ứ': 'u',
'ừ': 'u',
'ử': 'u',
'ữ': 'u',
'ự': 'u',
}
// NormalizeRunes normalizes latin script letters
func NormalizeRunes(runes []rune) []rune {
ret := make([]rune, len(runes))
copy(ret, runes)
for idx, r := range runes {
if r < 0x00C0 || r > 0x2184 {
continue
}
n := normalized[r]
if n > 0 {
ret[idx] = normalized[r]
}
}
return ret
}
================================================
FILE: internal/fuzzy/utils.go
================================================
package fuzzy
import (
"math"
"strings"
"github.com/rivo/uniseg"
)
func AsUint16(val int) uint16 {
if val > math.MaxUint16 {
return math.MaxUint16
} else if val < 0 {
return 0
}
return uint16(val)
}
// StringWidth returns string width where each CR/LF character takes 1 column
func StringWidth(s string) int {
return uniseg.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r")
}
// RunesWidth returns runes width
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
width := 0
gr := uniseg.NewGraphemes(string(runes))
idx := 0
for gr.Next() {
rs := gr.Runes()
var w int
if len(rs) == 1 && rs[0] == '\t' {
w = tabstop - (prefixWidth+width)%tabstop
} else {
w = StringWidth(string(rs))
}
width += w
if width > limit {
return width, idx
}
idx += len(rs)
}
return width, -1
}
================================================
FILE: internal/ident/ident.go
================================================
package ident
import (
"os"
"strconv"
"strings"
)
var Ident = " "
var IdentBytes []byte
var IdentWidth int
func init() {
identValue, ok := os.LookupEnv("FX_INDENT")
if ok {
identInt, err := strconv.Atoi(identValue)
if err == nil {
Ident = strings.Repeat(" ", identInt)
} else {
Ident = identValue
}
}
for _, r := range Ident {
if r == '\n' {
continue
}
if r == '\t' {
IdentBytes = append(IdentBytes, ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ')
IdentWidth += 8
continue
}
IdentBytes = append(IdentBytes, byte(r))
IdentWidth++
}
}
================================================
FILE: internal/jsonpath/path.go
================================================
package jsonpath
import (
"regexp"
"strconv"
"unicode"
)
type state int
const (
start state = iota
unknown
propOrIndex
prop
index
indexEnd
number
doubleQuote
doubleQuoteEscape
singleQuote
singleQuoteEscape
)
func Split(p string) ([]any, bool) {
path := make([]any, 0)
s := ""
state := start
for _, ch := range p {
switch state {
case start:
switch {
case ch == 'x':
state = unknown
case ch == '.':
state = propOrIndex
case ch == '[':
state = index
default:
return path, false
}
case unknown:
switch {
case ch == '.':
state = prop
s = ""
case ch == '[':
state = index
s = ""
default:
return path, false
}
case propOrIndex:
switch {
case isProp(ch):
state = prop
s = string(ch)
case ch == '[':
state = index
default:
return path, false
}
case prop:
switch {
case isProp(ch):
s += string(ch)
case ch == '.':
state = prop
path = append(path, s)
s = ""
case ch == '[':
state = index
path = append(path, s)
s = ""
default:
return path, false
}
case index:
switch {
case unicode.IsDigit(ch):
state = number
s = string(ch)
case ch == '"':
state = doubleQuote
s = ""
case ch == '\'':
state = singleQuote
s = ""
default:
return path, false
}
case indexEnd:
switch {
case ch == ']':
state = unknown
default:
return path, false
}
case number:
switch {
case unicode.IsDigit(ch):
s += string(ch)
case ch == ']':
state = unknown
n, err := strconv.Atoi(s)
if err != nil {
return path, false
}
path = append(path, n)
s = ""
default:
return path, false
}
case doubleQuote:
switch ch {
case '"':
state = indexEnd
path = append(path, s)
s = ""
case '\\':
state = doubleQuoteEscape
default:
s += string(ch)
}
case doubleQuoteEscape:
switch ch {
case '"':
state = doubleQuote
s += string(ch)
default:
return path, false
}
case singleQuote:
switch ch {
case '\'':
state = indexEnd
path = append(path, s)
s = ""
case '\\':
state = singleQuoteEscape
s += string(ch)
default:
s += string(ch)
}
case singleQuoteEscape:
switch ch {
case '\'':
state = singleQuote
s += string(ch)
default:
return path, false
}
}
}
if len(s) > 0 {
if state == prop {
path = append(path, s)
} else {
return path, false
}
}
return path, true
}
func isProp(ch rune) bool {
return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '$'
}
var Identifier = regexp.MustCompile(`^[$a-zA-Z_][$a-zA-Z0-9_]*$`)
func Join(path []any) string {
s := ""
for _, v := range path {
switch v := v.(type) {
case string:
if Identifier.MatchString(v) {
s += "." + v
} else {
s += "[" + strconv.Quote(v) + "]"
}
case int:
s += "[" + strconv.Itoa(v) + "]"
}
}
return s
}
================================================
FILE: internal/jsonpath/path_test.go
================================================
package jsonpath_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/antonmedv/fx/internal/jsonpath"
)
func Test_SplitPath(t *testing.T) {
tests := []struct {
input string
want []any
}{
{
input: "",
want: []any{},
},
{
input: ".",
want: []any{},
},
{
input: "x",
want: []any{},
},
{
input: ".foo",
want: []any{"foo"},
},
{
input: "x.foo",
want: []any{"foo"},
},
{
input: "x[42]",
want: []any{42},
},
{
input: ".[42]",
want: []any{42},
},
{
input: ".42",
want: []any{"42"},
},
{
input: ".физ",
want: []any{"физ"},
},
{
input: ".foo.bar",
want: []any{"foo", "bar"},
},
{
input: ".foo[42]",
want: []any{"foo", 42},
},
{
input: ".foo[42].bar",
want: []any{"foo", 42, "bar"},
},
{
input: ".foo[1][2]",
want: []any{"foo", 1, 2},
},
{
input: ".foo[\"bar\"]",
want: []any{"foo", "bar"},
},
{
input: ".foo[\"bar\\\"\"]",
want: []any{"foo", "bar\""},
},
{
input: ".foo['bar']['baz\\'']",
want: []any{"foo", "bar", "baz\\'"},
},
{
input: "[42]",
want: []any{42},
},
{
input: "[42].foo",
want: []any{42, "foo"},
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
p, ok := jsonpath.Split(tt.input)
require.Equal(t, tt.want, p)
require.True(t, ok)
})
}
}
func Test_SplitPath_negative(t *testing.T) {
tests := []struct {
input string
}{
{
input: "./",
},
{
input: "x/",
},
{
input: "1+1",
},
{
input: "x[42",
},
{
input: ".i % 2",
},
{
input: "x[for x]",
},
{
input: "x['y'.",
},
{
input: "x[0?",
},
{
input: "x[\"\\u",
},
{
input: "x['\\n",
},
{
input: "x[9999999999999999999999999999999999999]",
},
{
input: "x[]",
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
p, ok := jsonpath.Split(tt.input)
require.False(t, ok, p)
})
}
}
func TestJoin(t *testing.T) {
tests := []struct {
input []any
want string
}{
{
input: []any{},
want: "",
},
{
input: []any{"foo"},
want: ".foo",
},
{
input: []any{"foo", "bar"},
want: ".foo.bar",
},
{
input: []any{"foo", 42},
want: ".foo[42]",
},
{
input: []any{"foo", "bar", 42},
want: ".foo.bar[42]",
},
{
input: []any{"foo", "bar", 42, "baz"},
want: ".foo.bar[42].baz",
},
{
input: []any{"foo", "bar", 42, "baz", 1},
want: ".foo.bar[42].baz[1]",
},
{
input: []any{"foo", "bar", 42, "baz", 1, "qux"},
want: ".foo.bar[42].baz[1].qux",
},
{
input: []any{"foo bar"},
want: "[\"foo bar\"]",
},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
require.Equal(t, tt.want, jsonpath.Join(tt.input))
})
}
}
================================================
FILE: internal/jsonpath/ref.go
================================================
package jsonpath
import (
"net/url"
"strings"
)
func ParseSchemaRef(ref string) ([]any, bool) {
// Must start with '#'
if len(ref) == 0 || ref[0] != '#' {
return nil, false
}
// An empty fragment refers to the whole document
if ref == "#" {
return []any{}, true
}
// Must be a pointer ("#/...")
if !strings.HasPrefix(ref, "#/") {
return nil, false
}
// Split the pointer without the leading '#/'
parts := strings.Split(ref[2:], "/")
out := make([]any, len(parts))
for i, part := range parts {
// JSON Pointer unescaping
s := strings.ReplaceAll(strings.ReplaceAll(part, "~1", "/"), "~0", "~")
// Percent-unescape
unescaped, err := url.PathUnescape(s)
if err != nil {
return nil, false
}
out[i] = unescaped
}
return out, true
}
================================================
FILE: internal/jsonpath/ref_test.go
================================================
package jsonpath_test
import (
"reflect"
"testing"
"github.com/antonmedv/fx/internal/jsonpath"
)
func TestParseSchemaRef(t *testing.T) {
tests := []struct {
name string
input string
want []any
wantOk bool
}{
{
name: "empty fragment",
input: "#",
want: []any{},
wantOk: true,
},
{
name: "simple defs",
input: "#/$defs/OrganizationConfig/Options",
want: []any{"$defs", "OrganizationConfig", "Options"},
wantOk: true,
},
{
name: "with slash escape",
input: "#/path/with~1slash",
want: []any{"path", "with/slash"},
wantOk: true,
},
{
name: "with tilde escape",
input: "#/path/with~0tilde",
want: []any{"path", "with~tilde"},
wantOk: true,
},
{
name: "with percent",
input: "#/a%20b/c%2Fd",
want: []any{"a b", "c/d"},
wantOk: true,
},
{
name: "invalid no prefix",
input: "foo/bar",
want: nil,
wantOk: false,
},
{
name: "invalid bad escape",
input: "#/bad/%GG",
want: nil,
wantOk: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := jsonpath.ParseSchemaRef(tt.input)
if ok != tt.wantOk {
t.Errorf("ParseSchemaRef(%q) ok = %v, want %v", tt.input, ok, tt.wantOk)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseSchemaRef(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
================================================
FILE: internal/jsonx/delete.go
================================================
package jsonx
// DeleteNode removes the node at from the linked structure and returns a node to select next.
// It returns (nextToSelect, true) if deletion happened, or (nil, false) if nothing was deleted.
// Rules:
// - Do nothing if at is nil, points to root (no parent), or is a bracket/closing node (Index == -1).
// - If at is a wrap placeholder (Chunk set, Value empty), operate on its parent value.
// - Maintain Prev/Next links skipping the deleted range [at..endOf].
// - Clear trailing comma on the previous sibling when deleting the last child before parent's End.
// - Decrement parent.Size and reindex subsequent array siblings.
// - Choose selection: prefer next; if next is nil or parent.End, prefer prev; else parent.
func DeleteNode(at *Node) (*Node, bool) {
if at == nil {
return nil, false
}
// Avoid closing bracket nodes (Index == -1 used for brackets)
if at.Index == -1 {
return nil, false
}
parent := at.Parent
if parent == nil { // avoid deleting root
return nil, false
}
// If current points to a wrap placeholder, move to its parent value
if at.Chunk != "" && at.Value == "" && at.Parent != nil {
at = at.Parent
parent = at.Parent
if parent == nil {
return nil, false
}
}
// Determine the last node of this item (to skip its subtree or chunks)
endOf := at
if at.End != nil {
endOf = at.End
} else if at.ChunkEnd != nil {
endOf = at.ChunkEnd
}
prev := at.Prev
next := endOf.Next
// If deleting the last child before parent's closing bracket, clear trailing comma on previous sibling
isLast := next == parent.End
if isLast && prev != nil && prev != parent {
prev.Comma = false
}
// Relink to remove [at..endOf] from the chain
if prev != nil {
prev.Next = next
}
if next != nil {
next.Prev = prev
}
// Update parent size and array indices if needed
if parent.Size > 0 {
parent.Size--
}
if parent.Kind == Array {
for it := next; it != nil && it != parent.End; {
if it.Parent == parent && it.Index >= 0 {
it.Index = it.Index - 1
}
if it.HasChildren() {
it = it.End.Next
} else {
it = it.Next
}
}
}
// Select a sensible node after deletion
selectTo := next
if selectTo == nil || selectTo == parent.End {
if prev != nil && prev != parent {
selectTo = prev
} else {
selectTo = parent
}
}
return selectTo, true
}
================================================
FILE: internal/jsonx/delete_test.go
================================================
package jsonx_test
import (
"testing"
"github.com/stretchr/testify/require"
. "github.com/antonmedv/fx/internal/jsonx"
)
func TestDeleteNode_ObjectScenarios(t *testing.T) {
root, err := Parse([]byte(`{"a":1,"b":2,"c":3}`))
require.NoError(t, err)
obj := root
require.Equal(t, Object, obj.Kind)
// delete middle key b
b := obj.FindByPath([]any{"b"})
require.NotNil(t, b)
next, ok := DeleteNode(b)
require.True(t, ok)
require.NotNil(t, next)
// after deleting b, next should be c (or its start)
c := obj.FindByPath([]any{"c"})
require.NotNil(t, c)
// ensure size updated and comma on previous cleared
require.Equal(t, 2, obj.Size)
// delete last key c -> previous comma should be cleared and selection fallback
next2, ok := DeleteNode(c)
require.True(t, ok)
require.NotNil(t, next2)
// now only {"a":1}
require.Equal(t, 1, obj.Size)
// delete first/only key a -> object empty
a := obj.FindByPath([]any{"a"})
require.NotNil(t, a)
_, ok = DeleteNode(a)
require.True(t, ok)
require.Equal(t, 0, obj.Size)
}
func TestDeleteNode_ArrayScenarios(t *testing.T) {
root, err := Parse([]byte(`[10,20,30,40]`))
require.NoError(t, err)
arr := root
require.Equal(t, Array, arr.Kind)
// delete middle index 1 (20)
idx1 := arr.FindByPath([]any{1})
require.NotNil(t, idx1)
_, ok := DeleteNode(idx1)
require.True(t, ok)
// remaining should be [10,30,40], indices 0..2
zero := arr.FindByPath([]any{0})
require.NotNil(t, zero)
require.Equal(t, "10", zero.Value)
one := arr.FindByPath([]any{1})
require.NotNil(t, one)
require.Equal(t, "30", one.Value)
two := arr.FindByPath([]any{2})
require.NotNil(t, two)
require.Equal(t, "40", two.Value)
require.Equal(t, 3, arr.Size)
// delete last element (now index 2 -> 40)
last := arr.FindByPath([]any{2})
require.NotNil(t, last)
_, ok = DeleteNode(last)
require.True(t, ok)
require.Equal(t, 2, arr.Size)
// delete first element (10)
first := arr.FindByPath([]any{0})
require.NotNil(t, first)
_, ok = DeleteNode(first)
require.True(t, ok)
require.Equal(t, 1, arr.Size)
only := arr.FindByPath([]any{0})
require.NotNil(t, only)
require.Equal(t, "30", only.Value)
// delete the only element
_, ok = DeleteNode(only)
require.True(t, ok)
require.Equal(t, 0, arr.Size)
}
func TestDeleteNode_EdgeCases(t *testing.T) {
root, err := Parse([]byte(`{"k": {"x":1}, "arr":[{"y":2},3]}`))
require.NoError(t, err)
// try delete root -> ignored
next, ok := DeleteNode(root)
require.False(t, ok)
require.Nil(t, next)
// delete nested object {"y":2} inside arr[0]
nested := root.FindByPath([]any{"arr", 0})
require.NotNil(t, nested)
_, ok = DeleteNode(nested)
require.True(t, ok)
// arr should become [3]
arr := root.FindByPath([]any{"arr"})
require.NotNil(t, arr)
require.Equal(t, 1, arr.Size)
el := root.FindByPath([]any{"arr", 0})
require.NotNil(t, el)
require.Equal(t, Number, el.Kind)
require.Equal(t, "3", el.Value)
}
================================================
FILE: internal/jsonx/format_err.go
================================================
package jsonx
import (
"fmt"
"os"
"strings"
"unicode/utf8"
"github.com/charmbracelet/x/term"
"github.com/mattn/go-runewidth"
)
func (p *JsonParser) errorSnippet(message string) error {
termWidth, _, err := term.GetSize(os.Stdout.Fd())
if err != nil {
termWidth = 80
}
maxWidth := min(termWidth, 60)
maxWidth -= 2
maxWidth = max(maxWidth, 10)
// As we already moved end pointer in next(), we need to move it back.
p.end -= 1
before, width := p.contextBefore(maxWidth / 2)
after, _ := p.contextAfter(maxWidth - width)
snippet := " " + before + after
snippet += "\n " + strings.Repeat(".", max(0, width-1)) + "^"
return fmt.Errorf(
"%s on line %d.\n\n%s\n",
message,
p.realLineNumber,
snippet,
)
}
func (p *JsonParser) contextBefore(maxWidth int) (s string, width int) {
pos := p.end + 1
if pos > len(p.data) {
pos = len(p.data)
}
data := p.data[:pos]
for len(data) > 0 {
r, size := utf8.DecodeLastRune(data)
if r == '\n' {
break
}
runeWidth := runewidth.RuneWidth(r)
if width+runeWidth > maxWidth {
break
}
width += runeWidth
pos -= size
data = data[:pos]
}
s = string(p.data[pos:min(p.end+1, len(p.data))])
return
}
func (p *JsonParser) contextAfter(maxWidth int) (s string, width int) {
pos := p.end + 1
if pos >= len(p.data) {
return
}
data := p.data[pos:]
for len(data) > 0 {
r, size := utf8.DecodeRune(data)
if r == '\n' {
break
}
runeWidth := runewidth.RuneWidth(r)
if width+runeWidth > maxWidth {
break
}
width += runeWidth
pos += size
data = data[size:]
}
s = string(p.data[p.end+1 : pos]) // +1 to exclude the current character.
return
}
================================================
FILE: internal/jsonx/json.go
================================================
package jsonx
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"unicode"
"github.com/antonmedv/fx/internal/utils"
)
type JsonParser struct {
strict bool
rd io.Reader
buf []byte
data []byte
end int
eof bool
char byte
lineNumber int
realLineNumber int
depth uint8
count int
}
func Parse(b []byte) (*Node, error) {
p := NewJsonParser(bytes.NewReader(b), false)
node, err := p.Parse()
if err == io.EOF {
err = nil
}
return node, err
}
func NewJsonParser(rd io.Reader, strict bool) *JsonParser {
p := &JsonParser{
strict: strict,
rd: rd,
buf: make([]byte, 4096),
lineNumber: 1,
realLineNumber: 1,
}
p.next() // Should be called here, to support streaming.
return p
}
func (p *JsonParser) Parse() (node *Node, err error) {
defer func() {
if r := recover(); r != nil {
err = p.errorSnippet(fmt.Sprintf("%v", r))
}
}()
if p.count > 0 {
p.skipWhitespace()
}
if p.eof {
return nil, io.EOF
}
node = p.parseValue(true)
p.count++
return
}
func (p *JsonParser) Recover() *Node {
p.eof = false
p.depth = 0
start := p.end - 1
for {
p.next()
if p.eof {
break
}
if p.char == '{' || p.char == '[' {
break
}
}
end := p.end - 1
if p.data[end-1] == '\n' {
end-- // Trim trailing newline.
}
start = max(0, min(start, end))
text := string(p.data[start:end])
text = strings.ReplaceAll(text, "\t", " ")
text = strings.ReplaceAll(text, "\r", "")
lines := strings.Split(text, "\n")
textNode := &Node{
Kind: Err,
Value: lines[0],
Index: -1,
LineNumber: p.lineNumberPlusPlus(),
}
for i := 1; i < len(lines); i++ {
textNode.Append(&Node{
Kind: Err,
Value: lines[i],
Index: -1,
Parent: textNode,
LineNumber: p.lineNumberPlusPlus(),
})
}
return textNode
}
func (p *JsonParser) refill() {
n, err := p.rd.Read(p.buf)
if err != nil {
if err == io.EOF {
p.eof = true
return
} else {
panic(err)
}
}
p.data = append(p.data, p.buf[:n]...)
}
func (p *JsonParser) next() {
if p.end >= len(p.data) {
p.refill()
}
if p.eof {
p.char = 0
p.end = len(p.data) + 1
return
}
p.char = p.data[p.end]
if p.char == '\n' {
p.realLineNumber++
}
p.end++
}
func (p *JsonParser) back() {
p.end--
p.char = p.data[p.end]
}
func (p *JsonParser) set(pos int) {
p.end = pos
p.char = p.data[p.end]
}
func (p *JsonParser) lineNumberPlusPlus() int {
n := p.lineNumber
p.lineNumber++
return n
}
func (p *JsonParser) parseValue(root bool) *Node {
p.skipWhitespace()
var l *Node
switch p.char {
case '"':
l = p.parseString()
case '-':
l = p.parseMinus()
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
l = p.parseNumber(p.end - 1)
case '{':
l = p.parseObject()
case '[':
l = p.parseArray()
case 't':
l = p.parseKeyword("true", Bool)
case 'f':
l = p.parseKeyword("false", Bool)
case 'n':
l = p.parseNullOrNan()
case 'N':
l = p.parseNan(p.end - 1)
case 'i', 'I':
l = p.parseInfinity(p.end - 1)
case 'u':
if p.strict {
panic(fmt.Sprintf("Unexpected character %q", p.char))
}
l = p.parseKeyword("undefined", Undefined)
default:
panic(fmt.Sprintf("Unexpected character %q", p.char))
}
// Skip whitespace will block parseValue (with io.Read in refill func),
// as soon as we parsed the root value, return and ignore remining whitespaces.
if !root {
p.skipWhitespace()
}
return l
}
func (p *JsonParser) parseString() *Node {
return &Node{
Kind: String,
Depth: p.depth,
Value: p.scanString(),
LineNumber: p.lineNumberPlusPlus(),
}
}
func (p *JsonParser) scanString() string {
start := p.end - 1
p.next()
escaped := false
for {
if escaped {
escaped = false
if p.strict {
switch p.char {
case 'u':
var s string
for i := 0; i < 4; i++ {
p.next()
if !utils.IsHexDigit(p.char) {
panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s%c'", s, p.char))
}
s += string(p.char)
}
_, err := strconv.ParseInt(s, 16, 32)
if err != nil {
panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s'", s))
}
case '"', '\\', '/', 'b', 'f', 'n', 'r', 't':
default:
panic(fmt.Sprintf("Invalid escape sequence '\\%c'", p.char))
}
}
} else if p.char == '\\' {
escaped = true
} else if p.char == '"' {
break
} else if p.char == 0 {
panic("Unexpected end of input in string")
} else if rune(p.char) > unicode.MaxRune {
panic(fmt.Sprintf("Invalid character code point %d in string", p.char))
}
p.next()
}
str := string(p.data[start:p.end])
p.next()
return str
}
func (p *JsonParser) parseMinus() *Node {
start := p.end - 1
p.next()
switch p.char {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return p.parseNumber(start)
}
if !p.strict {
switch p.char {
case 'n', 'N':
return p.parseNan(start)
case 'i', 'I':
return p.parseInfinity(start)
}
}
panic(fmt.Sprintf("Invalid character %q in number", p.char))
}
func (p *JsonParser) parseNumber(start int) *Node {
num := &Node{
Kind: Number,
Depth: p.depth,
LineNumber: p.lineNumberPlusPlus(),
}
// Leading zero
if p.char == '0' {
p.next()
} else {
for utils.IsDigit(p.char) {
p.next()
}
}
// Decimal portion
if p.char == '.' {
p.next()
if !utils.IsDigit(p.char) {
panic(fmt.Sprintf("Invalid character %q in number", p.char))
}
for utils.IsDigit(p.char) {
p.next()
}
}
// Exponent
if p.char == 'e' || p.char == 'E' {
p.next()
if p.char == '+' || p.char == '-' {
p.next()
}
if !utils.IsDigit(p.char) {
panic(fmt.Sprintf("Invalid character %q in number", p.char))
}
for utils.IsDigit(p.char) {
p.next()
}
}
num.Value = string(p.data[start : p.end-1])
return num
}
func (p *JsonParser) parseObject() *Node {
object := &Node{
Kind: Object,
Depth: p.depth,
LineNumber: p.lineNumberPlusPlus(),
}
object.Value = curlyBracketOpen
p.next()
p.skipWhitespace()
// Empty object
if p.char == '}' {
object.Value = curlyBracketPair
p.next()
return object
}
for {
// Expecting a key which should be a string
if p.char != '"' {
panic(fmt.Sprintf("Expected object key to be a string, got %q", p.char))
}
keyBytes := p.scanString()
p.skipWhitespace()
// Expecting colon after key
if p.char != ':' {
panic(fmt.Sprintf("Expected colon after object key, got %q", p.char))
}
p.next()
p.depth++
value := p.parseValue(false)
value.Key = keyBytes
value.Parent = object
p.depth--
object.Append(value)
object.Size += 1
p.skipWhitespace()
commaPos := p.end
if p.char == ',' {
object.End.Comma = true
p.next()
p.skipWhitespace()
if p.char == '}' {
if p.strict {
p.set(commaPos)
panic("Trailing comma is not allowed in strict mode")
}
object.End.Comma = false
} else {
continue
}
}
if p.char == '}' {
closeBracket := &Node{
Kind: Object,
Depth: p.depth,
LineNumber: p.lineNumberPlusPlus(),
}
closeBracket.Value = curlyBracketClose
closeBracket.Parent = object
closeBracket.Index = -1
object.Append(closeBracket)
p.next()
return object
}
panic(fmt.Sprintf("Unexpected character %q in object", p.char))
}
}
func (p *JsonParser) parseArray() *Node {
arr := &Node{
Kind: Array,
Depth: p.depth,
LineNumber: p.lineNumberPlusPlus(),
}
arr.Value = squareBracketOpen
p.next()
p.skipWhitespace()
if p.char == ']' {
arr.Value = squareBracketPair
p.next()
return arr
}
for i := 0; ; i++ {
p.depth++
value := p.parseValue(false)
value.Parent = arr
arr.Size += 1
value.Index = i
p.depth--
arr.Append(value)
p.skipWhitespace()
commaPos := p.end
if p.char == ',' {
arr.End.Comma = true
p.next()
p.skipWhitespace()
if p.char == ']' {
if p.strict {
p.set(commaPos)
panic("Trailing comma is not allowed in strict mode")
}
arr.End.Comma = false
} else {
continue
}
}
if p.char == ']' {
closeBracket := &Node{
Kind: Array,
Depth: p.depth,
LineNumber: p.lineNumberPlusPlus(),
}
closeBracket.Value = squareBracketClose
closeBracket.Parent = arr
closeBracket.Index = -1
arr.Append(closeBracket)
p.next()
return arr
}
panic(fmt.Sprintf("Invalid character %q in array", p.char))
}
}
func (p *JsonParser) parseKeyword(name string, kind Kind) *Node {
start := p.end - 1
for i := 1; i < len(name); i++ {
p.next()
if p.char != name[i] {
panic(fmt.Sprintf("Unexpected character %q in keyword", p.char))
}
}
p.next()
if isEndOfValue(p.char) {
keyword := &Node{
Kind: kind,
Depth: p.depth,
Value: string(p.data[start : p.end-1]),
LineNumber: p.lineNumberPlusPlus(),
}
return keyword
}
panic(fmt.Sprintf("Unexpected character %q in keyword", p.char))
}
func (p *JsonParser) parseNullOrNan() *Node {
p.next()
if p.char == 'u' {
p.next()
if p.char == 'l' {
p.next()
if p.char == 'l' {
p.next()
if isEndOfValue(p.char) {
return &Node{
Kind: Null,
Depth: p.depth,
Value: "null",
LineNumber: p.lineNumberPlusPlus(),
}
}
}
}
} else if p.char == 'a' {
p.back() // Put back the 'a'.
return p.parseNan(p.end - 1)
}
panic(fmt.Sprintf("Unexpected character %q", p.char))
}
func (p *JsonParser) parseNan(start int) *Node {
if p.strict {
panic(fmt.Sprintf("Unexpected character %q", p.char))
}
p.next()
if p.char == 'a' || p.char == 'A' {
p.next()
if p.char == 'n' || p.char == 'N' {
p.next()
if isEndOfValue(p.char) {
return &Node{
Kind: NaN,
Depth: p.depth,
Value: string(p.data[start : p.end-1]),
LineNumber: p.lineNumberPlusPlus(),
}
}
}
}
panic(fmt.Sprintf("Unexpected character %q", p.char))
}
func (p *JsonParser) parseInfinity(start int) *Node {
if p.strict {
panic(fmt.Sprintf("Unexpected character %q", p.char))
}
p.next()
if p.char == 'n' || p.char == 'N' {
p.next()
if p.char == 'f' || p.char == 'F' {
p.next()
if isEndOfValue(p.char) {
return &Node{
Kind: Infinity,
Depth: p.depth,
Value: string(p.data[start : p.end-1]),
LineNumber: p.lineNumberPlusPlus(),
}
}
if p.char == 'i' {
p.next()
if p.char == 'n' {
p.next()
if p.char == 'i' {
p.next()
if p.char == 't' {
p.next()
if p.char == 'y' {
p.next()
if isEndOfValue(p.char) {
return &Node{
Kind: Infinity,
Depth: p.depth,
Value: string(p.data[start : p.end-1]),
LineNumber: p.lineNumberPlusPlus(),
}
}
}
}
}
}
}
}
}
panic(fmt.Sprintf("Unexpected character %q", p.char))
}
func isEndOfValue(ch byte) bool {
return isWhitespace(ch) || ch == ',' || ch == '}' || ch == ']' || ch == 0 // 0 is EOF
}
func isWhitespace(ch byte) bool {
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}
func (p *JsonParser) skipWhitespace() {
for {
switch p.char {
case ' ', '\t', '\n', '\r':
p.next()
case '/':
if p.strict {
panic("Comments are not allowed in strict mode")
}
p.skipComment()
default:
return
}
}
}
func (p *JsonParser) skipComment() {
p.next()
switch p.char {
case '/':
for p.char != '\n' && p.char != 0 {
p.next()
}
case '*':
for {
p.next()
if p.char == '*' {
p.next()
if p.char == '/' {
p.next()
return
}
}
if p.char == 0 {
panic("Unexpected end of input in comment")
}
}
default:
panic(fmt.Sprintf("Invalid comment: '/%c'", p.char))
}
}
================================================
FILE: internal/jsonx/jsonx_test.go
================================================
package jsonx_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/antonmedv/fx/internal/jsonx"
)
func TestJsonParser_Parse(t *testing.T) {
tests := []struct {
input string
wantKind jsonx.Kind
}{
{`"hello"`, jsonx.String},
{`42`, jsonx.Number},
{`-123.45`, jsonx.Number},
{`true`, jsonx.Bool},
{`false`, jsonx.Bool},
{`null`, jsonx.Null},
{`{}`, jsonx.Object},
{`[]`, jsonx.Array},
{`{"key":"value"}`, jsonx.Object},
{`[1, 2, 3]`, jsonx.Array},
{` "test" `, jsonx.String},
{`// comment
"test"`, jsonx.String},
{`/* comment */"test"`, jsonx.String},
{`{"a":1,}`, jsonx.Object},
{`[1,2,]`, jsonx.Array},
{`NaN`, jsonx.NaN},
{`-NaN`, jsonx.NaN},
{`nan`, jsonx.NaN},
{`Infinity`, jsonx.Infinity},
{`-Infinity`, jsonx.Infinity},
{`infinity`, jsonx.Infinity},
{`inf`, jsonx.Infinity},
{`INF`, jsonx.Infinity},
{`undefined`, jsonx.Undefined},
{`"\g"`, jsonx.String},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
node, err := jsonx.Parse([]byte(tt.input))
assert.NoError(t, err, "unexpected error for input: %s", tt.input)
assert.Equal(t, tt.wantKind, node.Kind, "unexpected kind for input: %s", tt.input)
})
}
}
func TestJsonParser_Parse_error(t *testing.T) {
tests := []struct {
input string
}{
{`"abc`},
{`truth`},
{`1e`},
{`[1, 2`},
{`/* test`},
{`[,]`},
{`{,}`},
{`[1,,]`},
{`{"a":1,,}`},
{`-null`},
{`Null`},
{`-Null`},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
_, err := jsonx.Parse([]byte(tt.input))
assert.Error(t, err, "expected error for input: %s", tt.input)
})
}
}
func TestJsonParser_Parse_strict(t *testing.T) {
tests := []struct {
input string
}{
{`{"a":1,}`},
{`[1,2,]`},
{`NaN`},
{`-NaN`},
{`nan`},
{`Infinity`},
{`-Infinity`},
{`infinity`},
{`inf`},
{`INF`},
{`-null`},
{`Null`},
{`-Null`},
{`/*comment*/ 42`},
{`undefined`},
{`"\g"`},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
parser := jsonx.NewJsonParser(strings.NewReader(tt.input), true)
_, err := parser.Parse()
assert.Error(t, err, "expected error for input: %s", tt.input)
})
}
}
func TestJsonParser_Recovery(t *testing.T) {
brokenJSON := `{ "a": 1 }here goes the text`
t.Run("Recover", func(t *testing.T) {
p := jsonx.NewJsonParser(strings.NewReader(brokenJSON), false)
_, _ = p.Parse() // trigger error
node := p.Recover()
assert.Equal(t, jsonx.Err, node.Kind, "expected recovery node to be of Kind Err")
assert.NotEmpty(t, node.Value, "expected recovery node to contain error snippet")
assert.Equal(t, string(node.Value), "here goes the text", "expected recovery node to contain error snippet")
})
}
func TestJsonParser_NestedStructureVerification(t *testing.T) {
input := `{
"user": {
"name": "John",
"age": 30,
"active": true,
"contacts": {
"email": "john@example.com",
"phone": "123456789"
},
"roles": ["admin", "editor"]
}
}`
node, err := jsonx.Parse([]byte(input))
assert.NoError(t, err)
assert.Equal(t, jsonx.Object, node.Kind)
// user object
user := node.FindByPath([]any{"user"})
assert.NotNil(t, user)
assert.Equal(t, jsonx.Object, user.Kind)
// user.name
name := node.FindByPath([]any{"user", "name"})
assert.NotNil(t, name)
assert.Equal(t, jsonx.String, name.Kind)
assert.Equal(t, `"John"`, string(name.Value))
// user.age
age := node.FindByPath([]any{"user", "age"})
assert.NotNil(t, age)
assert.Equal(t, jsonx.Number, age.Kind)
assert.Equal(t, "30", string(age.Value))
// user.active
active := node.FindByPath([]any{"user", "active"})
assert.NotNil(t, active)
assert.Equal(t, jsonx.Bool, active.Kind)
assert.Equal(t, "true", string(active.Value))
// user.contacts.email
email := node.FindByPath([]any{"user", "contacts", "email"})
assert.NotNil(t, email)
assert.Equal(t, jsonx.String, email.Kind)
assert.Equal(t, `"john@example.com"`, string(email.Value))
// user.roles[1]
role := node.FindByPath([]any{"user", "roles", 1})
assert.NotNil(t, role)
assert.Equal(t, jsonx.String, role.Kind)
assert.Equal(t, `"editor"`, string(role.Value))
}
func TestJsonParser_FindByPathWithCollapsedNodes(t *testing.T) {
t.Run("collapsed array access", func(t *testing.T) {
node, err := jsonx.Parse([]byte(`{
"items": [1, 2, 3, 4, 5]
}`))
require.NoError(t, err)
node.CollapseRecursively()
items := node.FindByPath([]any{"items"})
require.NotNil(t, items)
element := node.FindByPath([]any{"items", 2})
require.NotNil(t, element)
require.Equal(t, jsonx.Number, element.Kind)
require.Equal(t, "3", element.Value)
})
t.Run("collapsed object access", func(t *testing.T) {
node, err := jsonx.Parse([]byte(`{
"user": {
"settings": {
"theme": "dark",
"notifications": true
}
}
}`))
require.NoError(t, err)
node.CollapseRecursively()
settings := node.FindByPath([]any{"user", "settings"})
require.NotNil(t, settings)
theme := node.FindByPath([]any{"user", "settings", "theme"})
require.NotNil(t, theme)
require.Equal(t, jsonx.String, theme.Kind)
require.Equal(t, `"dark"`, string(theme.Value))
})
t.Run("nested collapsed structures", func(t *testing.T) {
node, err := jsonx.Parse([]byte(`{
"data": {
"users": [
{"id": 1, "name": "John"},
{"id": 2, "name": "Jane"}
]
}
}`))
require.NoError(t, err)
node.CollapseRecursively()
users := node.FindByPath([]any{"data", "users"})
require.NotNil(t, users)
userName := node.FindByPath([]any{"data", "users", 1, "name"})
require.NotNil(t, userName)
require.Equal(t, jsonx.String, userName.Kind)
require.Equal(t, `"Jane"`, string(userName.Value))
})
t.Run("nested collapsed structures with arrays", func(t *testing.T) {
node, err := jsonx.Parse([]byte(`{
"data": [
{
"first": [
"tmp",
{
"foo": [
1,
2,
true
]
}
]
},
{
"second": []
}
]
}`))
require.NoError(t, err)
node.CollapseRecursively()
value := node.FindByPath([]any{"data", 0, "first", 1, "foo", 2})
require.NotNil(t, value)
require.Equal(t, jsonx.Bool, value.Kind)
})
}
================================================
FILE: internal/jsonx/line.go
================================================
package jsonx
import (
"bufio"
"io"
"strconv"
"strings"
)
type LineParser struct {
buf *bufio.Reader
eof error
lineNumber int
}
func NewLineParser(in io.Reader) *LineParser {
p := &LineParser{
buf: bufio.NewReader(in),
lineNumber: 1,
}
return p
}
func (p *LineParser) Parse() (*Node, error) {
if p.eof != nil {
return nil, p.eof
}
b, err := p.buf.ReadBytes('\n')
if err != nil {
if err == io.EOF {
p.eof = err
} else {
return nil, err
}
}
if len(b) == 0 {
return nil, err
}
s := strings.TrimRight(string(b), "\r\n")
quoted := strconv.Quote(s)
node := &Node{
Kind: String,
Value: quoted,
LineNumber: p.lineNumber,
Depth: 0,
}
p.lineNumber++
return node, nil
}
func (p *LineParser) Recover() *Node {
return nil
}
================================================
FILE: internal/jsonx/node.go
================================================
package jsonx
import (
"strconv"
"github.com/antonmedv/fx/internal/jsonpath"
)
type Kind byte
const (
Err Kind = iota
Null
Bool
Number
String
Object
Array
NaN
Infinity
Undefined
)
type Node struct {
Prev, Next, End *Node
Parent *Node
Collapsed *Node
Depth uint8
Kind Kind
Key string
Value string
Size int
Chunk string
ChunkEnd *Node
Comma bool
Index int
LineNumber int
}
// Append ands a node as a child to the current node (body of {...} or [...]).
func (n *Node) Append(child *Node) {
if n.End == nil {
n.End = n
}
n.End.Next = child
child.Prev = n.End
if child.End == nil {
n.End = child
} else {
n.End = child.End
}
}
// Adjacent adds a node as a sibling to the current node ({}{}{} or [][][]).
func (n *Node) Adjacent(child *Node) {
end := n.End
if end == nil {
end = n
}
end.Next = child
child.Prev = end
if n.IsCollapsed() {
// Also attach to collapsed node.
n.Next = child
child.Prev = n
}
}
func (n *Node) insertChunk(chunk *Node) {
if n.ChunkEnd == nil {
n.insertAfter(chunk)
} else {
n.ChunkEnd.insertAfter(chunk)
}
n.ChunkEnd = chunk
}
func (n *Node) insertAfter(child *Node) {
if n.Next == nil {
n.Next = child
child.Prev = n
} else {
old := n.Next
n.Next = child
child.Prev = n
child.Next = old
old.Prev = child
}
}
func (n *Node) dropChunks() {
if n.ChunkEnd == nil {
return
}
n.Chunk = ""
n.Next = n.ChunkEnd.Next
if n.Next != nil {
n.Next.Prev = n
}
n.ChunkEnd = nil
}
func (n *Node) HasChildren() bool {
return n.End != nil
}
func (n *Node) Root() *Node {
parent := n.Parent
for parent != nil {
n = parent
parent = n.Parent
}
return n
}
func (n *Node) IsWrap() bool {
return n.Value == "" && n.Chunk != ""
}
func (n *Node) IsCollapsed() bool {
return n.Collapsed != nil
}
func (n *Node) Collapse() *Node {
if n.End != nil && !n.IsCollapsed() {
n.Collapsed = n.Next
n.Next = n.End.Next
if n.Next != nil {
n.Next.Prev = n
}
}
return n
}
func (n *Node) CollapseRecursively() {
var at *Node
if n.IsCollapsed() {
at = n.Collapsed
} else {
at = n.Next
}
for at != nil && at != n.End {
if at.HasChildren() {
at.CollapseRecursively()
at.Collapse()
}
at = at.Next
}
}
func (n *Node) Expand() {
if n.IsCollapsed() {
if n.Next != nil {
n.Next.Prev = n.End
}
n.Next = n.Collapsed
n.Collapsed = nil
}
}
func (n *Node) ExpandRecursively(level, maxLevel int) {
if level >= maxLevel {
return
}
if n.IsCollapsed() {
n.Expand()
}
it := n.Next
for it != nil && it != n.End {
if it.HasChildren() {
it.ExpandRecursively(level+1, maxLevel)
it = it.End.Next
} else {
gitextract_4yli86d0/ ├── .gitattributes ├── .github/ │ ├── images/ │ │ ├── autocomplete.tape │ │ ├── preview-mode.tape │ │ └── preview.tape │ ├── stream.mjs │ └── workflows/ │ ├── brew.yml │ ├── docker.yml │ ├── snap.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── RELEASE.md ├── go.mod ├── go.sum ├── help.go ├── internal/ │ ├── complete/ │ │ ├── complete.bash │ │ ├── complete.fish │ │ ├── complete.go │ │ ├── complete.zsh │ │ ├── complete_test.go │ │ ├── prelude.js │ │ └── utils.go │ ├── engine/ │ │ ├── engine.go │ │ ├── engine_test.go │ │ ├── format_err.go │ │ ├── fxrc.go │ │ ├── quote.go │ │ ├── quote_test.go │ │ ├── slurp.go │ │ ├── stdlib.js │ │ ├── stdlib_test.go │ │ ├── stringify.go │ │ ├── transpile.go │ │ ├── transpile_test.go │ │ ├── utils.go │ │ └── vm.go │ ├── fuzzy/ │ │ ├── algo.go │ │ ├── chars.go │ │ ├── chars_test.go │ │ ├── find.go │ │ ├── fuzzy_test.go │ │ ├── normalize.go │ │ └── utils.go │ ├── ident/ │ │ └── ident.go │ ├── jsonpath/ │ │ ├── path.go │ │ ├── path_test.go │ │ ├── ref.go │ │ └── ref_test.go │ ├── jsonx/ │ │ ├── delete.go │ │ ├── delete_test.go │ │ ├── format_err.go │ │ ├── json.go │ │ ├── jsonx_test.go │ │ ├── line.go │ │ ├── node.go │ │ ├── node_test.go │ │ ├── string.go │ │ ├── to_value.go │ │ └── wrap.go │ ├── pretty/ │ │ ├── inlineable.go │ │ ├── inlineable_test.go │ │ ├── pretty_print.go │ │ └── pretty_print_test.go │ ├── shlex/ │ │ ├── shlex.go │ │ └── shlex_test.go │ ├── theme/ │ │ └── theme.go │ ├── toml/ │ │ ├── toml.go │ │ └── toml_test.go │ └── utils/ │ ├── image.go │ ├── life.go │ ├── utils.go │ └── utils_test.go ├── keymap.go ├── main.go ├── main_test.go ├── npm/ │ ├── README.md │ ├── index.js │ ├── package.json │ └── test.js ├── preview.go ├── scripts/ │ └── build.mjs ├── search.go ├── search_test.go ├── snap/ │ └── snapcraft.yaml ├── testdata/ │ ├── TestCollapseRecursive.golden │ ├── TestCollapseRecursiveWithSizes.golden │ ├── TestGotoLine.golden │ ├── TestGotoLineCollapsed.golden │ ├── TestGotoLineInputGreaterThanTotalLines.golden │ ├── TestGotoLineInputInvalid.golden │ ├── TestGotoLineInputLessThanOne.golden │ ├── TestGotoLineKeepsHistory.golden │ ├── TestNavigation.golden │ ├── TestOutput.golden │ ├── blog.json │ └── example.json ├── utils.go ├── version.go ├── view.go ├── vim.go └── vim_test.go
SYMBOL INDEX (840 symbols across 69 files)
FILE: .github/stream.mjs
function randomItem (line 17) | function randomItem(arr) {
function randomItems (line 21) | function randomItems(arr, min = 1, max = 3) {
function randomObject (line 27) | function randomObject() {
FILE: help.go
function usage (line 14) | func usage() string {
function help (line 61) | func help(keyMap KeyMap) string {
function exit (line 137) | func exit() {
function showLetter (line 172) | func showLetter(t time.Time) bool {
FILE: internal/complete/complete.go
type Reply (line 22) | type Reply struct
function Complete (line 42) | func Complete() bool {
function doComplete (line 64) | func doComplete(compLine string, compWord string, withDisplay bool) {
function globalsComplete (line 189) | func globalsComplete() []Reply {
function KeysComplete (line 215) | func KeysComplete(input *jsonx.Node, args []string, compWord string) []R...
function join (line 263) | func join(prefix, key string) string {
function filterArgs (line 274) | func filterArgs(args []string) []string {
function fileComplete (line 291) | func fileComplete(compWord string) []Reply {
FILE: internal/complete/complete_test.go
function TestKeysComplete (line 9) | func TestKeysComplete(t *testing.T) {
function replyValues (line 150) | func replyValues(replies []Reply) []string {
function TestKeysComplete_Display (line 158) | func TestKeysComplete_Display(t *testing.T) {
function TestKeysComplete_DisplayVsValue (line 177) | func TestKeysComplete_DisplayVsValue(t *testing.T) {
FILE: internal/complete/prelude.js
function __autocomplete (line 12) | function __autocomplete() {
FILE: internal/complete/utils.go
function compReply (line 10) | func compReply(reply []Reply, withDisplay bool) {
function filterReply (line 22) | func filterReply(reply []Reply, compWord string) []Reply {
function isFile (line 32) | func isFile(path string) bool {
function dropTail (line 46) | func dropTail(s string) string {
function balanceBrackets (line 54) | func balanceBrackets(code string) string {
function lastWord (line 77) | func lastWord(line string) string {
function writeLog (line 86) | func writeLog(args ...interface{}) {
FILE: internal/engine/engine.go
function init (line 19) | func init() {
type Parser (line 27) | type Parser interface
type Options (line 32) | type Options struct
function Start (line 39) | func Start(parser Parser, args []string, opts Options) int {
function callMain (line 146) | func callMain(main goja.Callable, input goja.Value) (output goja.Value, ...
function validateSyntax (line 161) | func validateSyntax(args []string, i int) error {
FILE: internal/engine/engine_test.go
function TestEngine (line 13) | func TestEngine(t *testing.T) {
function TestStart_InvalidJSON (line 67) | func TestStart_InvalidJSON(t *testing.T) {
function TestStart_FastPath_InvalidJSON (line 87) | func TestStart_FastPath_InvalidJSON(t *testing.T) {
function TestStart_EscapeSequences (line 107) | func TestStart_EscapeSequences(t *testing.T) {
function TestStart_EscapeSequences_in_key (line 128) | func TestStart_EscapeSequences_in_key(t *testing.T) {
FILE: internal/engine/format_err.go
function formatErr (line 12) | func formatErr(args []string, i int, jsCode string) string {
function trimLeft (line 79) | func trimLeft(s string, ctx int) string {
function trimRight (line 97) | func trimRight(s string, ctx int) string {
FILE: internal/engine/fxrc.go
function readFxrc (line 10) | func readFxrc() (string, error) {
function uniq (line 57) | func uniq(paths []string) []string {
FILE: internal/engine/quote.go
function Quote (line 9) | func Quote(s string) string {
FILE: internal/engine/quote_test.go
function TestQuote_BasicASCII (line 12) | func TestQuote_BasicASCII(t *testing.T) {
function TestQuote_EscapesSpecialCharacters (line 18) | func TestQuote_EscapesSpecialCharacters(t *testing.T) {
function TestQuote_ControlCharactersAndDEL (line 28) | func TestQuote_ControlCharactersAndDEL(t *testing.T) {
function TestQuote_BMP_Characters_AsIs (line 64) | func TestQuote_BMP_Characters_AsIs(t *testing.T) {
function TestQuote_SurrogatePairs_AsIs (line 71) | func TestQuote_SurrogatePairs_AsIs(t *testing.T) {
function TestQuote_InvalidUTF8BytesAreEscaped (line 77) | func TestQuote_InvalidUTF8BytesAreEscaped(t *testing.T) {
function TestQuote_JSONRoundTrip_ValidUTF8 (line 86) | func TestQuote_JSONRoundTrip_ValidUTF8(t *testing.T) {
FILE: internal/engine/slurp.go
function Slurp (line 9) | func Slurp(parser Parser, writeErr func(string)) (Parser, bool) {
type slurpParser (line 52) | type slurpParser struct
method Parse (line 56) | func (p *slurpParser) Parse() (*jsonx.Node, error) {
method Recover (line 65) | func (p *slurpParser) Recover() *jsonx.Node {
FILE: internal/engine/stdlib.js
function apply (line 21) | function apply(fn, ...args) {
function len (line 26) | function len(x) {
function uniq (line 33) | function uniq(x) {
function sort (line 38) | function sort(x) {
function isFalsely (line 43) | function isFalsely(x) {
function filter (line 47) | function filter(fn) {
function map (line 56) | function map(fn) {
function walk (line 65) | function walk(fn) {
function sortBy (line 82) | function sortBy(fn) {
function sortKeys (line 93) | function sortKeys(x) {
function groupBy (line 107) | function groupBy(keyFn) {
function chunk (line 119) | function chunk(size) {
function zip (line 130) | function zip(...x) {
function flatten (line 139) | function flatten(x) {
function reverse (line 144) | function reverse(x) {
function keys (line 149) | function keys(x) {
function values (line 154) | function values(x) {
function list (line 159) | function list(x) {
function del (line 167) | function del(key) {
function exit (line 183) | function exit(code) {
function save (line 187) | function save(x) {
function toBase64 (line 193) | function toBase64(x) {
function fromBase64 (line 197) | function fromBase64(x) {
constant YAML (line 201) | const YAML = {
FILE: internal/engine/stdlib_test.go
function setupVM (line 15) | func setupVM(t *testing.T) *goja.Runtime {
function setupVMWithOutput (line 26) | func setupVMWithOutput(t *testing.T) (*goja.Runtime, *[]string) {
function TestApply (line 37) | func TestApply(t *testing.T) {
function TestLen (line 67) | func TestLen(t *testing.T) {
function TestUniq (line 100) | func TestUniq(t *testing.T) {
function TestSort (line 131) | func TestSort(t *testing.T) {
function TestFilter (line 162) | func TestFilter(t *testing.T) {
function TestMap (line 208) | func TestMap(t *testing.T) {
function TestWalk (line 233) | func TestWalk(t *testing.T) {
function TestSortBy (line 260) | func TestSortBy(t *testing.T) {
function TestSortKeys (line 295) | func TestSortKeys(t *testing.T) {
function TestGroupBy (line 350) | func TestGroupBy(t *testing.T) {
function TestChunk (line 387) | func TestChunk(t *testing.T) {
function TestZip (line 426) | func TestZip(t *testing.T) {
function TestFlatten (line 464) | func TestFlatten(t *testing.T) {
function TestReverse (line 500) | func TestReverse(t *testing.T) {
function TestKeys (line 532) | func TestKeys(t *testing.T) {
function TestValues (line 575) | func TestValues(t *testing.T) {
function TestList (line 618) | func TestList(t *testing.T) {
function TestDel (line 644) | func TestDel(t *testing.T) {
function TestSkipSymbol (line 682) | func TestSkipSymbol(t *testing.T) {
function TestConsoleLog (line 699) | func TestConsoleLog(t *testing.T) {
function TestToBase64 (line 725) | func TestToBase64(t *testing.T) {
function TestFromBase64 (line 748) | func TestFromBase64(t *testing.T) {
function TestYAML (line 781) | func TestYAML(t *testing.T) {
function TestIsFalsely (line 822) | func TestIsFalsely(t *testing.T) {
function TestChainedOperations (line 851) | func TestChainedOperations(t *testing.T) {
function TestEdgeCases (line 896) | func TestEdgeCases(t *testing.T) {
function TestBase64RoundTrip (line 960) | func TestBase64RoundTrip(t *testing.T) {
function escapeJS (line 981) | func escapeJS(s string) string {
function min (line 989) | func min(a, b int) int {
function TestExit (line 997) | func TestExit(t *testing.T) {
function TestSave (line 1024) | func TestSave(t *testing.T) {
function TestSortMutation (line 1042) | func TestSortMutation(t *testing.T) {
function TestReverseMutation (line 1058) | func TestReverseMutation(t *testing.T) {
function TestDelImmutability (line 1074) | func TestDelImmutability(t *testing.T) {
function TestWalkWithNull (line 1099) | func TestWalkWithNull(t *testing.T) {
function TestGroupByWithPrototypeKeys (line 1113) | func TestGroupByWithPrototypeKeys(t *testing.T) {
function TestChunkEdgeCases (line 1129) | func TestChunkEdgeCases(t *testing.T) {
function TestZipEdgeCases (line 1148) | func TestZipEdgeCases(t *testing.T) {
function TestFilterWithObjects (line 1171) | func TestFilterWithObjects(t *testing.T) {
function TestMapWithObjects (line 1189) | func TestMapWithObjects(t *testing.T) {
function TestSortByStability (line 1219) | func TestSortByStability(t *testing.T) {
function TestNestedOperations (line 1239) | func TestNestedOperations(t *testing.T) {
FILE: internal/engine/stringify.go
function Stringify (line 14) | func Stringify(value goja.Value, vm *goja.Runtime, depth int) string {
FILE: internal/engine/transpile.go
function JS (line 10) | func JS(args []string) string {
function Body (line 24) | func Body(args []string, i int) string {
function transpile (line 49) | func transpile(code string) string {
function fold (line 79) | func fold(s []string) string {
FILE: internal/engine/transpile_test.go
function TestTranspile (line 7) | func TestTranspile(t *testing.T) {
function TestFoldSimple (line 30) | func TestFoldSimple(t *testing.T) {
FILE: internal/engine/utils.go
function extractErrorMessage (line 22) | func extractErrorMessage(s string) string {
function errorToString (line 28) | func errorToString(err error) string {
FILE: internal/engine/vm.go
type ExitError (line 16) | type ExitError struct
function NewVM (line 20) | func NewVM(writeOut func(string)) *goja.Runtime {
FILE: internal/fuzzy/algo.go
constant whiteChars (line 12) | whiteChars = " \t\n\v\f\r\x85\xA0"
type Result (line 14) | type Result struct
constant scoreMatch (line 21) | scoreMatch = 16
constant scoreGapStart (line 22) | scoreGapStart = -3
constant scoreGapExtension (line 23) | scoreGapExtension = -1
constant bonusBoundary (line 31) | bonusBoundary = scoreMatch / 2
constant bonusNonWord (line 36) | bonusNonWord = scoreMatch / 2
constant bonusCamel123 (line 41) | bonusCamel123 = bonusBoundary + scoreGapExtension
constant bonusConsecutive (line 46) | bonusConsecutive = -(scoreGapStart + scoreGapExtension)
constant bonusFirstCharMultiplier (line 53) | bonusFirstCharMultiplier = 2
type charClass (line 72) | type charClass
constant charWhite (line 75) | charWhite charClass = iota
constant charNonWord (line 76) | charNonWord
constant charDelimiter (line 77) | charDelimiter
constant charLower (line 78) | charLower
constant charUpper (line 79) | charUpper
constant charLetter (line 80) | charLetter
constant charNumber (line 81) | charNumber
function init (line 84) | func init() {
function posArray (line 108) | func posArray(withPos bool, len int) *[]int {
function alloc16 (line 116) | func alloc16(offset int, slab *Slab, size int) (int, []int16) {
function alloc32 (line 124) | func alloc32(offset int, slab *Slab, size int) (int, []int32) {
function charClassOfNonAscii (line 132) | func charClassOfNonAscii(char rune) charClass {
function charClassOf (line 149) | func charClassOf(char rune) charClass {
function bonusFor (line 156) | func bonusFor(prevClass charClass, class charClass) int16 {
function normalizeRune (line 186) | func normalizeRune(r rune) rune {
function trySkip (line 198) | func trySkip(input *Chars, caseSensitive bool, b byte, from int) int {
function isAscii (line 222) | func isAscii(runes []rune) bool {
function asciiFuzzyIndex (line 231) | func asciiFuzzyIndex(input *Chars, pattern []rune, caseSensitive bool) (...
type Slab (line 272) | type Slab struct
constant caseSensitive (line 278) | caseSensitive = false
constant normalize (line 279) | normalize = true
constant forward (line 280) | forward = true
function fuzzyMatch (line 283) | func fuzzyMatch(input *Chars, pattern []rune) (Result, *[]int) {
FILE: internal/fuzzy/chars.go
constant overflow64 (line 11) | overflow64 uint64 = 0x8080808080808080
constant overflow32 (line 12) | overflow32 uint32 = 0x80808080
type Chars (line 15) | type Chars struct
method IsBytes (line 69) | func (chars *Chars) IsBytes() bool {
method Bytes (line 73) | func (chars *Chars) Bytes() []byte {
method NumLines (line 77) | func (chars *Chars) NumLines(atMost int) (int, bool) {
method optionalRunes (line 106) | func (chars *Chars) optionalRunes() []rune {
method Get (line 113) | func (chars *Chars) Get(i int) rune {
method Length (line 120) | func (chars *Chars) Length() int {
method TrimLength (line 128) | func (chars *Chars) TrimLength() uint16 {
method LeadingWhitespaces (line 157) | func (chars *Chars) LeadingWhitespaces() int {
method TrailingWhitespaces (line 169) | func (chars *Chars) TrailingWhitespaces() int {
method TrimTrailingWhitespaces (line 181) | func (chars *Chars) TrimTrailingWhitespaces() {
method TrimSuffix (line 186) | func (chars *Chars) TrimSuffix(runes []rune) {
method SliceRight (line 203) | func (chars *Chars) SliceRight(last int) {
method ToString (line 207) | func (chars *Chars) ToString() string {
method ToRunes (line 214) | func (chars *Chars) ToRunes() []rune {
method CopyRunes (line 226) | func (chars *Chars) CopyRunes(dest []rune, from int) {
method Prepend (line 236) | func (chars *Chars) Prepend(prefix string) {
method Lines (line 245) | func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, ...
function checkAscii (line 26) | func checkAscii(bytes []byte) (bool, int) {
function ToChars (line 47) | func ToChars(bytes []byte) Chars {
function RunesToChars (line 65) | func RunesToChars(runes []rune) Chars {
FILE: internal/fuzzy/chars_test.go
function TestToCharsAscii (line 8) | func TestToCharsAscii(t *testing.T) {
function TestCharsLength (line 15) | func TestCharsLength(t *testing.T) {
function TestCharsToString (line 22) | func TestCharsToString(t *testing.T) {
function TestTrimLength (line 30) | func TestTrimLength(t *testing.T) {
function TestCharsLines (line 51) | func TestCharsLines(t *testing.T) {
FILE: internal/fuzzy/find.go
type Match (line 3) | type Match struct
function Find (line 10) | func Find(pattern []rune, array []string) *Match {
FILE: internal/fuzzy/fuzzy_test.go
function TestFind (line 10) | func TestFind(t *testing.T) {
function TestFuzzyMatch (line 94) | func TestFuzzyMatch(t *testing.T) {
function TestFuzzyMatchScoring (line 129) | func TestFuzzyMatchScoring(t *testing.T) {
function TestNormalizeRune (line 141) | func TestNormalizeRune(t *testing.T) {
function TestNormalizeRunes (line 173) | func TestNormalizeRunes(t *testing.T) {
function TestCharClass (line 195) | func TestCharClass(t *testing.T) {
function TestCharClassOfNonAscii (line 223) | func TestCharClassOfNonAscii(t *testing.T) {
function TestBonusFor (line 244) | func TestBonusFor(t *testing.T) {
function TestAsUint16 (line 258) | func TestAsUint16(t *testing.T) {
function TestStringWidth (line 280) | func TestStringWidth(t *testing.T) {
function TestRunesWidth (line 302) | func TestRunesWidth(t *testing.T) {
function TestTrySkip (line 359) | func TestTrySkip(t *testing.T) {
function TestIsAscii (line 385) | func TestIsAscii(t *testing.T) {
function TestAsciiFuzzyIndex (line 407) | func TestAsciiFuzzyIndex(t *testing.T) {
FILE: internal/fuzzy/normalize.go
function NormalizeRunes (line 479) | func NormalizeRunes(runes []rune) []rune {
FILE: internal/fuzzy/utils.go
function AsUint16 (line 10) | func AsUint16(val int) uint16 {
function StringWidth (line 20) | func StringWidth(s string) int {
function RunesWidth (line 25) | func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (...
FILE: internal/ident/ident.go
function init (line 13) | func init() {
FILE: internal/jsonpath/path.go
type state (line 9) | type state
constant start (line 12) | start state = iota
constant unknown (line 13) | unknown
constant propOrIndex (line 14) | propOrIndex
constant prop (line 15) | prop
constant index (line 16) | index
constant indexEnd (line 17) | indexEnd
constant number (line 18) | number
constant doubleQuote (line 19) | doubleQuote
constant doubleQuoteEscape (line 20) | doubleQuoteEscape
constant singleQuote (line 21) | singleQuote
constant singleQuoteEscape (line 22) | singleQuoteEscape
function Split (line 25) | func Split(p string) ([]any, bool) {
function isProp (line 177) | func isProp(ch rune) bool {
function Join (line 183) | func Join(path []any) string {
FILE: internal/jsonpath/path_test.go
function Test_SplitPath (line 11) | func Test_SplitPath(t *testing.T) {
function Test_SplitPath_negative (line 98) | func Test_SplitPath_negative(t *testing.T) {
function TestJoin (line 147) | func TestJoin(t *testing.T) {
FILE: internal/jsonpath/ref.go
function ParseSchemaRef (line 8) | func ParseSchemaRef(ref string) ([]any, bool) {
FILE: internal/jsonpath/ref_test.go
function TestParseSchemaRef (line 10) | func TestParseSchemaRef(t *testing.T) {
FILE: internal/jsonx/delete.go
function DeleteNode (line 12) | func DeleteNode(at *Node) (*Node, bool) {
FILE: internal/jsonx/delete_test.go
function TestDeleteNode_ObjectScenarios (line 11) | func TestDeleteNode_ObjectScenarios(t *testing.T) {
function TestDeleteNode_ArrayScenarios (line 45) | func TestDeleteNode_ArrayScenarios(t *testing.T) {
function TestDeleteNode_EdgeCases (line 91) | func TestDeleteNode_EdgeCases(t *testing.T) {
FILE: internal/jsonx/format_err.go
method errorSnippet (line 13) | func (p *JsonParser) errorSnippet(message string) error {
method contextBefore (line 38) | func (p *JsonParser) contextBefore(maxWidth int) (s string, width int) {
method contextAfter (line 61) | func (p *JsonParser) contextAfter(maxWidth int) (s string, width int) {
FILE: internal/jsonx/json.go
type JsonParser (line 14) | type JsonParser struct
method Parse (line 49) | func (p *JsonParser) Parse() (node *Node, err error) {
method Recover (line 66) | func (p *JsonParser) Recover() *Node {
method refill (line 110) | func (p *JsonParser) refill() {
method next (line 123) | func (p *JsonParser) next() {
method back (line 139) | func (p *JsonParser) back() {
method set (line 144) | func (p *JsonParser) set(pos int) {
method lineNumberPlusPlus (line 149) | func (p *JsonParser) lineNumberPlusPlus() int {
method parseValue (line 155) | func (p *JsonParser) parseValue(root bool) *Node {
method parseString (line 198) | func (p *JsonParser) parseString() *Node {
method scanString (line 207) | func (p *JsonParser) scanString() string {
method parseMinus (line 252) | func (p *JsonParser) parseMinus() *Node {
method parseNumber (line 270) | func (p *JsonParser) parseNumber(start int) *Node {
method parseObject (line 315) | func (p *JsonParser) parseObject() *Node {
method parseArray (line 395) | func (p *JsonParser) parseArray() *Node {
method parseKeyword (line 457) | func (p *JsonParser) parseKeyword(name string, kind Kind) *Node {
method parseNullOrNan (line 479) | func (p *JsonParser) parseNullOrNan() *Node {
method parseNan (line 504) | func (p *JsonParser) parseNan(start int) *Node {
method parseInfinity (line 527) | func (p *JsonParser) parseInfinity(start int) *Node {
method skipWhitespace (line 580) | func (p *JsonParser) skipWhitespace() {
method skipComment (line 596) | func (p *JsonParser) skipComment() {
function Parse (line 28) | func Parse(b []byte) (*Node, error) {
function NewJsonParser (line 37) | func NewJsonParser(rd io.Reader, strict bool) *JsonParser {
function isEndOfValue (line 572) | func isEndOfValue(ch byte) bool {
function isWhitespace (line 576) | func isWhitespace(ch byte) bool {
FILE: internal/jsonx/jsonx_test.go
function TestJsonParser_Parse (line 13) | func TestJsonParser_Parse(t *testing.T) {
function TestJsonParser_Parse_error (line 55) | func TestJsonParser_Parse_error(t *testing.T) {
function TestJsonParser_Parse_strict (line 81) | func TestJsonParser_Parse_strict(t *testing.T) {
function TestJsonParser_Recovery (line 112) | func TestJsonParser_Recovery(t *testing.T) {
function TestJsonParser_NestedStructureVerification (line 125) | func TestJsonParser_NestedStructureVerification(t *testing.T) {
function TestJsonParser_FindByPathWithCollapsedNodes (line 179) | func TestJsonParser_FindByPathWithCollapsedNodes(t *testing.T) {
FILE: internal/jsonx/line.go
type LineParser (line 10) | type LineParser struct
method Parse (line 24) | func (p *LineParser) Parse() (*Node, error) {
method Recover (line 51) | func (p *LineParser) Recover() *Node {
function NewLineParser (line 16) | func NewLineParser(in io.Reader) *LineParser {
FILE: internal/jsonx/node.go
type Kind (line 9) | type Kind
constant Err (line 12) | Err Kind = iota
constant Null (line 13) | Null
constant Bool (line 14) | Bool
constant Number (line 15) | Number
constant String (line 16) | String
constant Object (line 17) | Object
constant Array (line 18) | Array
constant NaN (line 19) | NaN
constant Infinity (line 20) | Infinity
constant Undefined (line 21) | Undefined
type Node (line 24) | type Node struct
method Append (line 41) | func (n *Node) Append(child *Node) {
method Adjacent (line 55) | func (n *Node) Adjacent(child *Node) {
method insertChunk (line 69) | func (n *Node) insertChunk(chunk *Node) {
method insertAfter (line 78) | func (n *Node) insertAfter(child *Node) {
method dropChunks (line 91) | func (n *Node) dropChunks() {
method HasChildren (line 106) | func (n *Node) HasChildren() bool {
method Root (line 110) | func (n *Node) Root() *Node {
method IsWrap (line 119) | func (n *Node) IsWrap() bool {
method IsCollapsed (line 123) | func (n *Node) IsCollapsed() bool {
method Collapse (line 127) | func (n *Node) Collapse() *Node {
method CollapseRecursively (line 138) | func (n *Node) CollapseRecursively() {
method Expand (line 154) | func (n *Node) Expand() {
method ExpandRecursively (line 164) | func (n *Node) ExpandRecursively(level, maxLevel int) {
method FindByPath (line 182) | func (n *Node) FindByPath(path []any) *Node {
method findChildByKey (line 198) | func (n *Node) findChildByKey(key string) *Node {
method findChildByIndex (line 226) | func (n *Node) findChildByIndex(index int) *Node {
method FindNextNonErr (line 246) | func (n *Node) FindNextNonErr() *Node {
method Children (line 254) | func (n *Node) Children() ([]string, []*Node) {
method Bottom (line 290) | func (n *Node) Bottom() *Node {
method Paths (line 302) | func (n *Node) Paths(paths *[]string, nodes *[]*Node) {
method ForEach (line 359) | func (n *Node) ForEach(cb func(*Node)) {
FILE: internal/jsonx/node_test.go
function TestNode_children (line 11) | func TestNode_children(t *testing.T) {
function TestNode_expandRecursively (line 19) | func TestNode_expandRecursively(t *testing.T) {
function TestNode_Paths (line 28) | func TestNode_Paths(t *testing.T) {
function TestNode_Paths_Collapsed (line 46) | func TestNode_Paths_Collapsed(t *testing.T) {
function TestNode_ForEach (line 65) | func TestNode_ForEach(t *testing.T) {
function TestNode_ForEach_Empty (line 78) | func TestNode_ForEach_Empty(t *testing.T) {
function TestNode_ForEach_SkipsNested (line 89) | func TestNode_ForEach_SkipsNested(t *testing.T) {
FILE: internal/jsonx/string.go
constant curlyBracketOpen (line 8) | curlyBracketOpen = "{"
constant curlyBracketClose (line 9) | curlyBracketClose = "}"
constant curlyBracketPair (line 10) | curlyBracketPair = "{}"
constant squareBracketOpen (line 11) | squareBracketOpen = "["
constant squareBracketClose (line 12) | squareBracketClose = "]"
constant squareBracketPair (line 13) | squareBracketPair = "[]"
method String (line 16) | func (n *Node) String() string {
FILE: internal/jsonx/to_value.go
method ToValue (line 14) | func (n *Node) ToValue(vm *goja.Runtime) goja.Value {
constant maxSafeInt (line 117) | maxSafeInt = 1<<53 - 1
constant minSafeInt (line 120) | minSafeInt = -maxSafeInt
function ParseNumber (line 123) | func ParseNumber(s string) (interface{}, bool) {
FILE: internal/jsonx/wrap.go
function DropWrapAll (line 11) | func DropWrapAll(n *Node) {
function Wrap (line 24) | func Wrap(n *Node, termWidth int) {
function doWrap (line 56) | func doWrap(n *Node, termWidth int) ([]string, int) {
FILE: internal/pretty/inlineable.go
function isInlineable (line 7) | func isInlineable(n *jsonx.Node) bool {
function isSingleElementArray (line 19) | func isSingleElementArray(n *jsonx.Node) bool {
function isSimpleNumbersArray (line 34) | func isSimpleNumbersArray(n *jsonx.Node) bool {
function isSimpleObject (line 49) | func isSimpleObject(n *jsonx.Node) bool {
function isNestedArrays (line 109) | func isNestedArrays(n *jsonx.Node) bool {
function isArrayOfSimpleObject (line 129) | func isArrayOfSimpleObject(n *jsonx.Node) bool {
FILE: internal/pretty/inlineable_test.go
function TestIsInlineable (line 14) | func TestIsInlineable(t *testing.T) {
function TestIsNestedArrays (line 116) | func TestIsNestedArrays(t *testing.T) {
function TestIsArrayOfSimpleObject (line 263) | func TestIsArrayOfSimpleObject(t *testing.T) {
FILE: internal/pretty/pretty_print.go
function Print (line 13) | func Print(n *jsonx.Node, withInline bool) string {
function table (line 44) | func table(out *strings.Builder, n *jsonx.Node) *jsonx.Node {
function inline (line 66) | func inline(out *strings.Builder, n *jsonx.Node) *jsonx.Node {
function printIdent (line 86) | func printIdent(out *strings.Builder, n *jsonx.Node) {
function printKey (line 92) | func printKey(out *strings.Builder, n *jsonx.Node) {
function printValue (line 99) | func printValue(out *strings.Builder, n *jsonx.Node) {
function next (line 108) | func next(n *jsonx.Node) *jsonx.Node {
function afterEnd (line 116) | func afterEnd(n *jsonx.Node) *jsonx.Node {
FILE: internal/pretty/pretty_print_test.go
function stripEscapeSequences (line 14) | func stripEscapeSequences(s string) string {
constant yes (line 20) | yes byte = iota
constant no (line 21) | no
constant both (line 22) | both
function TestPrettyPrint (line 25) | func TestPrettyPrint(t *testing.T) {
FILE: internal/shlex/shlex.go
type TokenType (line 49) | type TokenType
type runeTokenClass (line 52) | type runeTokenClass
type lexerState (line 55) | type lexerState
type Token (line 58) | type Token struct
method Equal (line 66) | func (a *Token) Equal(b *Token) bool {
constant spaceRunes (line 78) | spaceRunes = " \t\r\n"
constant escapingQuoteRunes (line 79) | escapingQuoteRunes = `"`
constant nonEscapingQuoteRunes (line 80) | nonEscapingQuoteRunes = "'"
constant escapeRunes (line 81) | escapeRunes = `\`
constant commentRunes (line 82) | commentRunes = "#"
constant unknownRuneClass (line 87) | unknownRuneClass runeTokenClass = iota
constant spaceRuneClass (line 88) | spaceRuneClass
constant escapingQuoteRuneClass (line 89) | escapingQuoteRuneClass
constant nonEscapingQuoteRuneClass (line 90) | nonEscapingQuoteRuneClass
constant escapeRuneClass (line 91) | escapeRuneClass
constant commentRuneClass (line 92) | commentRuneClass
constant eofRuneClass (line 93) | eofRuneClass
constant UnknownToken (line 98) | UnknownToken TokenType = iota
constant WordToken (line 99) | WordToken
constant SpaceToken (line 100) | SpaceToken
constant CommentToken (line 101) | CommentToken
constant startState (line 106) | startState lexerState = iota
constant inWordState (line 107) | inWordState
constant escapingState (line 108) | escapingState
constant escapingQuotedState (line 109) | escapingQuotedState
constant quotingEscapingState (line 110) | quotingEscapingState
constant quotingState (line 111) | quotingState
constant commentState (line 112) | commentState
type tokenClassifier (line 116) | type tokenClassifier
method addRuneClass (line 118) | func (typeMap tokenClassifier) addRuneClass(runes string, tokenType ru...
method ClassifyRune (line 136) | func (t tokenClassifier) ClassifyRune(runeVal rune) runeTokenClass {
function newDefaultClassifier (line 125) | func newDefaultClassifier() tokenClassifier {
type Lexer (line 141) | type Lexer
method Next (line 150) | func (l *Lexer) Next() (string, error) {
function NewLexer (line 144) | func NewLexer(r io.Reader) *Lexer {
type Tokenizer (line 168) | type Tokenizer struct
method scanStream (line 184) | func (t *Tokenizer) scanStream() (*Token, error) {
method Next (line 392) | func (t *Tokenizer) Next() (*Token, error) {
function NewTokenizer (line 174) | func NewTokenizer(r io.Reader) *Tokenizer {
function Split (line 397) | func Split(s string) ([]string, error) {
function Parse (line 413) | func Parse(s string) string {
FILE: internal/shlex/shlex_test.go
function TestClassifier (line 32) | func TestClassifier(t *testing.T) {
function TestTokenizer (line 47) | func TestTokenizer(t *testing.T) {
function TestLexer (line 73) | func TestLexer(t *testing.T) {
function TestSplit (line 89) | func TestSplit(t *testing.T) {
function TestSplit_unfinished (line 105) | func TestSplit_unfinished(t *testing.T) {
FILE: internal/theme/theme.go
type Theme (line 16) | type Theme struct
type Color (line 33) | type Color
function Value (line 35) | func Value(kind jsonx.Kind) Color {
function init (line 63) | func init() {
function noColor (line 354) | func noColor(s string) string {
function toColor (line 358) | func toColor(f func(s ...string) string) Color {
function fg (line 364) | func fg(color string) Color {
function underlineFg (line 368) | func underlineFg(color string) Color {
function boldFg (line 372) | func boldFg(color string) Color {
function ThemeTester (line 376) | func ThemeTester() {
function ExportThemes (line 419) | func ExportThemes() {
FILE: internal/toml/toml.go
type jnode (line 15) | type jnode interface
type jobject (line 17) | type jobject struct
type jfield (line 21) | type jfield struct
type jarray (line 26) | type jarray struct
function ToJSON (line 30) | func ToJSON(in []byte) ([]byte, error) {
function keyParts (line 82) | func keyParts(it unstable.Iterator) []string {
function dot (line 89) | func dot(parts []string) string { return strings.Join(parts, ".") }
function ensureContainer (line 91) | func ensureContainer(root *jobject, tablePath []string, aotActive map[st...
function setNested (line 154) | func setNested(obj *jobject, parts []string, val jnode) {
function getField (line 186) | func getField(obj *jobject, key string) (jfield, bool) {
function replaceField (line 194) | func replaceField(obj *jobject, key string, val jnode) {
function toJ (line 207) | func toJ(v any) jnode {
function lookupTyped (line 227) | func lookupTyped(typed any, tablePath, rel []string, aotActive map[strin...
function asMap (line 265) | func asMap(x any) (map[string]any, bool) {
function writeJSON (line 275) | func writeJSON(w io.Writer, n jnode) error {
FILE: internal/toml/toml_test.go
function assertJSONBytesEqual (line 12) | func assertJSONBytesEqual(t *testing.T, got []byte, want string) {
function assertJSONStructEqual (line 18) | func assertJSONStructEqual(t *testing.T, got []byte, want string) {
function TestToJSON_SimpleScalars (line 31) | func TestToJSON_SimpleScalars(t *testing.T) {
function TestToJSON_NestedTable (line 41) | func TestToJSON_NestedTable(t *testing.T) {
function TestToJSON_DottedKeys (line 50) | func TestToJSON_DottedKeys(t *testing.T) {
function TestToJSON_ArraysAndInlineTables (line 59) | func TestToJSON_ArraysAndInlineTables(t *testing.T) {
function TestToJSON_ArraysOfTables (line 69) | func TestToJSON_ArraysOfTables(t *testing.T) {
function TestToJSON_NestedAOT (line 88) | func TestToJSON_NestedAOT(t *testing.T) {
function TestToJSON_MixedTablesAndKeysOrder (line 100) | func TestToJSON_MixedTablesAndKeysOrder(t *testing.T) {
function TestToJSON_Datetime (line 117) | func TestToJSON_Datetime(t *testing.T) {
FILE: internal/utils/image.go
function DrawImage (line 16) | func DrawImage(r io.Reader, width, height int) (string, error) {
FILE: internal/utils/life.go
function GameOfLife (line 14) | func GameOfLife() {
FILE: internal/utils/utils.go
function IsHexDigit (line 7) | func IsHexDigit(ch byte) bool {
function IsDigit (line 11) | func IsDigit(ch byte) bool {
function Contains (line 15) | func Contains(needle int, haystack []int) bool {
function Unquote (line 24) | func Unquote(s string) (string, error) {
FILE: internal/utils/utils_test.go
function TestIsHexDigit (line 9) | func TestIsHexDigit(t *testing.T) {
function TestIsDigit (line 37) | func TestIsDigit(t *testing.T) {
function TestContains (line 60) | func TestContains(t *testing.T) {
function TestUnquote (line 86) | func TestUnquote(t *testing.T) {
FILE: keymap.go
type KeyMap (line 5) | type KeyMap struct
function init (line 45) | func init() {
FILE: main.go
function init (line 59) | func init() {
function main (line 65) | func main() {
type model (line 329) | type model struct
method Init (line 395) | func (m *model) Init() tea.Cmd {
method Update (line 399) | func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
method handleHelpKey (line 553) | func (m *model) handleHelpKey(msg tea.Msg) (tea.Model, tea.Cmd) {
method handleGotoLineKey (line 565) | func (m *model) handleGotoLineKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
method handleSearchKey (line 585) | func (m *model) handleSearchKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
method handleGotoSymbolKey (line 607) | func (m *model) handleGotoSymbolKey(msg tea.KeyMsg) (tea.Model, tea.Cm...
method handleYankKey (line 636) | func (m *model) handleYankKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
method handleShowSelectorKey (line 654) | func (m *model) handleShowSelectorKey(msg tea.KeyMsg) (tea.Model, tea....
method handlePendingDelete (line 666) | func (m *model) handlePendingDelete(msg tea.Msg) {
method handleKey (line 680) | func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
method up (line 983) | func (m *model) up() {
method down (line 997) | func (m *model) down() {
method recordHistory (line 1016) | func (m *model) recordHistory() {
method scrollToBottom (line 1038) | func (m *model) scrollToBottom() {
method visibleLines (line 1048) | func (m *model) visibleLines() int {
method scrollIntoView (line 1058) | func (m *model) scrollIntoView() {
method scrollBackward (line 1073) | func (m *model) scrollBackward(lines int) {
method scrollForward (line 1084) | func (m *model) scrollForward(lines int) {
method prettyKey (line 1098) | func (m *model) prettyKey(node *Node, selected bool) []byte {
method prettyPrint (line 1123) | func (m *model) prettyPrint(node *Node, isSelected, isRef bool) string {
method viewWidth (line 1168) | func (m *model) viewWidth() int {
method viewHeight (line 1177) | func (m *model) viewHeight() int {
method cursorPointsTo (line 1196) | func (m *model) cursorPointsTo() (*Node, bool) {
method at (line 1201) | func (m *model) at(pos int) *Node {
method nodeInsideView (line 1212) | func (m *model) nodeInsideView(n *Node) bool {
method selectNodeInView (line 1229) | func (m *model) selectNodeInView(n *Node) {
method selectNode (line 1243) | func (m *model) selectNode(n *Node) {
method cursorPath (line 1266) | func (m *model) cursorPath() string {
method cursorValue (line 1294) | func (m *model) cursorValue() string {
method cursorKey (line 1354) | func (m *model) cursorKey() string {
method findByPath (line 1370) | func (m *model) findByPath(path []any) *Node {
method currentTopNode (line 1375) | func (m *model) currentTopNode() *Node {
method createKeysIndex (line 1386) | func (m *model) createKeysIndex() {
method dig (line 1405) | func (m *model) dig(v string) *Node {
method print (line 1437) | func (m *model) print() tea.Cmd {
method open (line 1442) | func (m *model) open() tea.Cmd {
method deleteAtCursor (line 1465) | func (m *model) deleteAtCursor() {
type location (line 370) | type location struct
type nodeMsg (line 375) | type nodeMsg struct
type errorMsg (line 379) | type errorMsg struct
type eofMsg (line 383) | type eofMsg struct
type searchResultMsg (line 385) | type searchResultMsg struct
type searchCancelledMsg (line 391) | type searchCancelledMsg struct
FILE: main_test.go
function init (line 20) | func init() {
type options (line 24) | type options struct
function prepare (line 29) | func prepare(t *testing.T, opts ...options) *teatest.TestModel {
function read (line 64) | func read(t *testing.T, tm *teatest.TestModel) []byte {
function TestOutput (line 78) | func TestOutput(t *testing.T) {
function TestNavigation (line 87) | func TestNavigation(t *testing.T) {
function TestCollapseRecursive (line 99) | func TestCollapseRecursive(t *testing.T) {
function TestCollapseRecursiveWithSizes (line 109) | func TestCollapseRecursiveWithSizes(t *testing.T) {
FILE: npm/index.js
function transform (line 67) | async function transform(json, args, theme) {
function transpile (line 106) | function transpile(code) {
function run (line 140) | async function run(json, code) {
function read (line 327) | async function read(fd = 0) {
function isFile (line 355) | function isFile(fs, path) {
function sleepSync (line 364) | function sleepSync(ms) {
function next (line 397) | function next() {
function parseValue (line 411) | function parseValue() {
function parseString (line 425) | function parseString() {
function parseNumber (line 475) | function parseNumber() {
function parseObject (line 523) | function parseObject() {
function parseArray (line 562) | function parseArray() {
function parseKeyword (line 592) | function parseKeyword(name, value) {
function skipWhitespace (line 607) | function skipWhitespace() {
function skipComment (line 614) | function skipComment() {
function isWhitespace (line 640) | function isWhitespace(ch) {
function isHexDigit (line 644) | function isHexDigit(ch) {
function isDigit (line 648) | function isDigit(ch) {
function isInteger (line 652) | function isInteger(value) {
function toSafeNumber (line 656) | function toSafeNumber(str) {
function expectValue (line 663) | function expectValue(value) {
function errorSnippet (line 669) | function errorSnippet(message = `Unexpected character '${lastChar}'`) {
function readEOL (line 682) | function readEOL() {
function stringify (line 692) | function stringify(value, theme) {
function themes (line 740) | function themes(id) {
function importFxrc (line 760) | async function importFxrc(path) {
function loadFxrc (line 770) | function loadFxrc(os, fs, path, process) {
function printUsage (line 801) | function printUsage() {
function isCollection (line 815) | function isCollection(node){if(node&&typeof node==="object")switch(node[...
function isNode (line 815) | function isNode(node){if(node&&typeof node==="object")switch(node[NODE_T...
function visit (line 815) | function visit(node,visitor){const visitor_=initVisitor(visitor);if(isDo...
function visit_ (line 815) | function visit_(key,node,visitor,path){const ctrl=callVisitor(key,node,v...
function visitAsync (line 815) | async function visitAsync(node,visitor){const visitor_=initVisitor(visit...
function visitAsync_ (line 815) | async function visitAsync_(key,node,visitor,path){const ctrl=await callV...
function initVisitor (line 815) | function initVisitor(visitor){if(typeof visitor==="object"&&(visitor.Col...
function callVisitor (line 815) | function callVisitor(key,node,visitor,path){if(typeof visitor==="functio...
function replaceNode (line 815) | function replaceNode(key,path,node){const parent=path[path.length-1];if(...
method constructor (line 815) | constructor(yaml,tags){this.docStart=null;this.docEnd=false;this.yaml=Ob...
method clone (line 815) | clone(){const copy=new _Directives(this.yaml,this.tags);copy.docStart=th...
method atDocument (line 815) | atDocument(){const res=new _Directives(this.yaml,this.tags);switch(this....
method add (line 815) | add(line,onError){if(this.atNextDocument){this.yaml={explicit:_Directive...
method tagName (line 815) | tagName(source,onError){if(source==="!")return"!";if(source[0]!=="!"){on...
method tagString (line 815) | tagString(tag){for(const[handle,prefix]of Object.entries(this.tags)){if(...
method toString (line 815) | toString(doc){const lines=this.yaml.explicit?[`%YAML ${this.yaml.version...
function anchorIsValid (line 815) | function anchorIsValid(anchor){if(/[\x00-\x19\s,[\]{}]/.test(anchor)){co...
function anchorNames (line 815) | function anchorNames(root){const anchors=new Set;visit(root,{Value(_key,...
function findNewAnchor (line 815) | function findNewAnchor(prefix,exclude){for(let i=1;true;++i){const name=...
function createNodeAnchors (line 815) | function createNodeAnchors(doc,prefix){const aliasObjects=[];const sourc...
function applyReviver (line 815) | function applyReviver(reviver,obj,key,val){if(val&&typeof val==="object"...
function toJS (line 815) | function toJS(value,arg,ctx){if(Array.isArray(value))return value.map((v...
method constructor (line 815) | constructor(type){Object.defineProperty(this,NODE_TYPE,{value:type})}
method clone (line 815) | clone(){const copy=Object.create(Object.getPrototypeOf(this),Object.getO...
method toJS (line 815) | toJS(doc,{mapAsMap,maxAliasCount,onAnchor,reviver}={}){if(!isDocument(do...
method constructor (line 815) | constructor(source){super(ALIAS);this.source=source;Object.definePropert...
method resolve (line 815) | resolve(doc){let found=void 0;visit(doc,{Node:(_key,node)=>{if(node===th...
method toJSON (line 815) | toJSON(_arg,ctx){if(!ctx)return{source:this.source};const{anchors,doc,ma...
method toString (line 815) | toString(ctx,_onComment,_onChompKeep){const src=`*${this.source}`;if(ctx...
function getAliasCount (line 815) | function getAliasCount(doc,node,anchors){if(isAlias(node)){const source=...
method constructor (line 815) | constructor(value){super(SCALAR);this.value=value}
method toJSON (line 815) | toJSON(arg,ctx){return ctx?.keep?this.value:toJS(this.value,arg,ctx)}
method toString (line 815) | toString(){return String(this.value)}
function findTagObject (line 815) | function findTagObject(value,tagName,tags){if(tagName){const match=tags....
function createNode (line 815) | function createNode(value,tagName,ctx){if(isDocument(value))value=value....
function collectionFromPath (line 815) | function collectionFromPath(schema4,path,value){let v=value;for(let i=pa...
method constructor (line 815) | constructor(type,schema4){super(type);Object.defineProperty(this,"schema...
method clone (line 815) | clone(schema4){const copy=Object.create(Object.getPrototypeOf(this),Obje...
method addIn (line 815) | addIn(path,value){if(isEmptyPath(path))this.add(value);else{const[key,.....
method deleteIn (line 815) | deleteIn(path){const[key,...rest]=path;if(rest.length===0)return this.de...
method getIn (line 815) | getIn(path,keepScalar){const[key,...rest]=path;const node=this.get(key,t...
method hasAllNullValues (line 815) | hasAllNullValues(allowScalar){return this.items.every(node=>{if(!isPair(...
method hasIn (line 815) | hasIn(path){const[key,...rest]=path;if(rest.length===0)return this.has(k...
method setIn (line 815) | setIn(path,value){const[key,...rest]=path;if(rest.length===0){this.set(k...
function indentComment (line 815) | function indentComment(comment,indent){if(/^\n+$/.test(comment))return c...
function foldFlowLines (line 815) | function foldFlowLines(text,indent,mode="flow",{indentAtStart,lineWidth=...
function consumeMoreIndentedLines (line 817) | function consumeMoreIndentedLines(text,i){let ch=text[i+1];while(ch===" ...
function lineLengthOverLimit (line 817) | function lineLengthOverLimit(str,lineWidth,indentLength){if(!lineWidth||...
function doubleQuotedString (line 817) | function doubleQuotedString(value,ctx){const json=JSON.stringify(value);...
function singleQuotedString (line 817) | function singleQuotedString(value,ctx){if(ctx.options.singleQuote===fals...
function quotedString (line 818) | function quotedString(value,ctx){const{singleQuote}=ctx.options;let qs;i...
function blockString (line 818) | function blockString({comment,type,value},ctx,onComment,onChompKeep){con...
function plainString (line 820) | function plainString(item,ctx,onComment,onChompKeep){const{type,value}=i...
function stringifyString (line 821) | function stringifyString(item,ctx,onComment,onChompKeep){const{implicitK...
function createStringifyContext (line 821) | function createStringifyContext(doc,options){const opt=Object.assign({bl...
function getTagObject (line 821) | function getTagObject(tags,item){if(item.tag){const match=tags.filter(t=...
function stringifyProps (line 821) | function stringifyProps(node,tagObj,{anchors,doc}){if(!doc.directives)re...
function stringify (line 821) | function stringify(item,ctx,onComment,onChompKeep){if(isPair(item))retur...
function stringifyPair (line 822) | function stringifyPair({key,value},ctx,onComment,onChompKeep){const{allN...
function warn (line 826) | function warn(logLevel,warning){if(logLevel==="debug"||logLevel==="warn"...
function addPairToJSMap (line 826) | function addPairToJSMap(ctx,map2,{key,value}){if(ctx?.doc.schema.merge&&...
function mergeToJSMap (line 826) | function mergeToJSMap(ctx,map2,value){const source=ctx&&isAlias(value)?v...
function stringifyKey (line 826) | function stringifyKey(key,jsKey,ctx){if(jsKey===null)return"";if(typeof ...
function createPair (line 826) | function createPair(key,value,ctx){const k=createNode(key,void 0,ctx);co...
method constructor (line 826) | constructor(key,value=null){Object.defineProperty(this,NODE_TYPE,{value:...
method clone (line 826) | clone(schema4){let{key,value}=this;if(isNode(key))key=key.clone(schema4)...
method toJSON (line 826) | toJSON(_,ctx){const pair=ctx?.mapAsMap?new Map:{};return addPairToJSMap(...
method toString (line 826) | toString(ctx,onComment,onChompKeep){return ctx?.doc?stringifyPair(this,c...
function stringifyCollection (line 826) | function stringifyCollection(collection,ctx,options){const flow=ctx.inFl...
function stringifyBlockCollection (line 826) | function stringifyBlockCollection({comment,items},ctx,{blockItemPrefix,f...
function stringifyFlowCollection (line 827) | function stringifyFlowCollection({comment,items},ctx,{flowChars,itemInde...
function addCommentBefore (line 829) | function addCommentBefore({indent,options:{commentString}},lines,comment...
function findPair (line 829) | function findPair(items,key){const k=isScalar(key)?key.value:key;for(con...
method tagName (line 829) | static get tagName(){return"tag:yaml.org,2002:map"}
method constructor (line 829) | constructor(schema4){super(MAP,schema4);this.items=[]}
method from (line 829) | static from(schema4,obj,ctx){const{keepUndefined,replacer}=ctx;const map...
method add (line 829) | add(pair,overwrite){let _pair;if(isPair(pair))_pair=pair;else if(!pair||...
method delete (line 829) | delete(key){const it=findPair(this.items,key);if(!it)return false;const ...
method get (line 829) | get(key,keepScalar){const it=findPair(this.items,key);const node=it?.val...
method has (line 829) | has(key){return!!findPair(this.items,key)}
method set (line 829) | set(key,value){this.add(new Pair(key,value),true)}
method toJSON (line 829) | toJSON(_,ctx,Type){const map2=Type?new Type:ctx?.mapAsMap?new Map:{};if(...
method toString (line 829) | toString(ctx,onComment,onChompKeep){if(!ctx)return JSON.stringify(this);...
method resolve (line 829) | resolve(map2,onError){if(!isMap(map2))onError("Expected a mapping for th...
method tagName (line 829) | static get tagName(){return"tag:yaml.org,2002:seq"}
method constructor (line 829) | constructor(schema4){super(SEQ,schema4);this.items=[]}
method add (line 829) | add(value){this.items.push(value)}
method delete (line 829) | delete(key){const idx=asItemIndex(key);if(typeof idx!=="number")return f...
method get (line 829) | get(key,keepScalar){const idx=asItemIndex(key);if(typeof idx!=="number")...
method has (line 829) | has(key){const idx=asItemIndex(key);return typeof idx==="number"&&idx<th...
method set (line 829) | set(key,value){const idx=asItemIndex(key);if(typeof idx!=="number")throw...
method toJSON (line 829) | toJSON(_,ctx){const seq2=[];if(ctx?.onCreate)ctx.onCreate(seq2);let i=0;...
method toString (line 829) | toString(ctx,onComment,onChompKeep){if(!ctx)return JSON.stringify(this);...
method from (line 829) | static from(schema4,obj,ctx){const{replacer}=ctx;const seq2=new this(sch...
function asItemIndex (line 829) | function asItemIndex(key){let idx=isScalar(key)?key.value:key;if(idx&&ty...
method resolve (line 829) | resolve(seq2,onError){if(!isSeq(seq2))onError("Expected a sequence for t...
method stringify (line 829) | stringify(item,ctx,onComment,onChompKeep){ctx=Object.assign({actualStrin...
method stringify (line 829) | stringify({source,value},ctx){if(source&&boolTag.test.test(source)){cons...
function stringifyNumber (line 829) | function stringifyNumber({format,minFractionDigits,tag,value}){if(typeof...
method stringify (line 829) | stringify(node){const num=Number(node.value);return isFinite(num)?num.to...
method resolve (line 829) | resolve(str){const node=new Scalar(parseFloat(str));const dot=str.indexO...
function intStringify (line 829) | function intStringify(node,radix,prefix){const{value}=node;if(intIdentif...
function intIdentify2 (line 829) | function intIdentify2(value){return typeof value==="bigint"||Number.isIn...
method resolve (line 829) | resolve(str,onError){onError(`Unresolved plain scalar ${JSON.stringify(s...
method resolve (line 829) | resolve(src,onError){if(typeof Buffer==="function"){return Buffer.from(s...
method stringify (line 829) | stringify({comment,type,value},ctx,onComment,onChompKeep){const buf=valu...
function resolvePairs (line 829) | function resolvePairs(seq2,onError){if(isSeq(seq2)){for(let i=0;i<seq2.i...
function createPairs (line 831) | function createPairs(schema4,iterable,ctx){const{replacer}=ctx;const pai...
method constructor (line 831) | constructor(){super();this.add=YAMLMap.prototype.add.bind(this);this.del...
method toJSON (line 831) | toJSON(_,ctx){if(!ctx)return super.toJSON(_);const map2=new Map;if(ctx?....
method from (line 831) | static from(schema4,iterable,ctx){const pairs2=createPairs(schema4,itera...
method resolve (line 831) | resolve(seq2,onError){const pairs2=resolvePairs(seq2,onError);const seen...
function boolStringify (line 831) | function boolStringify({value,source},ctx){const boolObj=value?trueTag:f...
method stringify (line 831) | stringify(node){const num=Number(node.value);return isFinite(num)?num.to...
method resolve (line 831) | resolve(str){const node=new Scalar(parseFloat(str.replace(/_/g,"")));con...
function intResolve2 (line 831) | function intResolve2(str,offset,radix,{intAsBigInt}){const sign=str[0];i...
function intStringify2 (line 831) | function intStringify2(node,radix,prefix){const{value}=node;if(intIdenti...
method constructor (line 831) | constructor(schema4){super(schema4);this.tag=_YAMLSet.tag}
method add (line 831) | add(key){let pair;if(isPair(key))pair=key;else if(key&&typeof key==="obj...
method get (line 831) | get(key,keepPair){const pair=findPair(this.items,key);return!keepPair&&i...
method set (line 831) | set(key,value){if(typeof value!=="boolean")throw new Error(`Expected boo...
method toJSON (line 831) | toJSON(_,ctx){return super.toJSON(_,ctx,Set)}
method toString (line 831) | toString(ctx,onComment,onChompKeep){if(!ctx)return JSON.stringify(this);...
method from (line 831) | static from(schema4,iterable,ctx){const{replacer}=ctx;const set2=new thi...
method resolve (line 831) | resolve(map2,onError){if(isMap(map2)){if(map2.hasAllNullValues(true))ret...
function parseSexagesimal (line 831) | function parseSexagesimal(str,asBigInt){const sign=str[0];const parts=si...
function stringifySexagesimal (line 831) | function stringifySexagesimal(node){let{value}=node;let num=n=>n;if(type...
method resolve (line 831) | resolve(str){const match=str.match(timestamp.test);if(!match)throw new E...
function getTags (line 831) | function getTags(customTags,schemaName){let tags=schemas.get(schemaName)...
method constructor (line 831) | constructor({compat,customTags,merge,resolveKnownTags,schema:schema4,sor...
method clone (line 831) | clone(){const copy=Object.create(_Schema.prototype,Object.getOwnProperty...
function stringifyDocument (line 831) | function stringifyDocument(doc,options){const lines=[];let hasDirectives...
method constructor (line 831) | constructor(value,replacer,options){this.commentBefore=null;this.comment...
method clone (line 831) | clone(){const copy=Object.create(_Document.prototype,{[NODE_TYPE]:{value...
method add (line 831) | add(value){if(assertCollection(this.contents))this.contents.add(value)}
method addIn (line 831) | addIn(path,value){if(assertCollection(this.contents))this.contents.addIn...
method createAlias (line 831) | createAlias(node,name){if(!node.anchor){const prev=anchorNames(this);nod...
method createNode (line 831) | createNode(value,replacer,options){let _replacer=void 0;if(typeof replac...
method createPair (line 831) | createPair(key,value,options={}){const k=this.createNode(key,null,option...
method delete (line 831) | delete(key){return assertCollection(this.contents)?this.contents.delete(...
method deleteIn (line 831) | deleteIn(path){if(isEmptyPath(path)){if(this.contents==null)return false...
method get (line 831) | get(key,keepScalar){return isCollection(this.contents)?this.contents.get...
method getIn (line 831) | getIn(path,keepScalar){if(isEmptyPath(path))return!keepScalar&&isScalar(...
method has (line 831) | has(key){return isCollection(this.contents)?this.contents.has(key):false}
method hasIn (line 831) | hasIn(path){if(isEmptyPath(path))return this.contents!==void 0;return is...
method set (line 831) | set(key,value){if(this.contents==null){this.contents=collectionFromPath(...
method setIn (line 831) | setIn(path,value){if(isEmptyPath(path)){this.contents=value}else if(this...
method setSchema (line 831) | setSchema(version,options={}){if(typeof version==="number")version=Strin...
method toJS (line 831) | toJS({json,jsonArg,mapAsMap,maxAliasCount,onAnchor,reviver}={}){const ct...
method toJSON (line 831) | toJSON(jsonArg,onAnchor){return this.toJS({json:true,jsonArg,mapAsMap:fa...
method toString (line 831) | toString(options={}){if(this.errors.length>0)throw new Error("Document w...
function assertCollection (line 831) | function assertCollection(contents){if(isCollection(contents))return tru...
method constructor (line 831) | constructor(name,pos,code,message){super();this.name=name;this.code=code...
method constructor (line 831) | constructor(pos,code,message){super("YAMLParseError",pos,code,message)}
method constructor (line 831) | constructor(pos,code,message){super("YAMLWarning",pos,code,message)}
function resolveProps (line 835) | function resolveProps(tokens,{flow,indicator,next,offset,onError,startOn...
function containsNewline (line 835) | function containsNewline(key){if(!key)return null;switch(key.type){case"...
function flowIndentCheck (line 835) | function flowIndentCheck(indent,fc,onError){if(fc?.type==="flow-collecti...
function mapIncludes (line 835) | function mapIncludes(ctx,items,search){const{uniqueKeys}=ctx.options;if(...
function resolveBlockMap (line 835) | function resolveBlockMap({composeNode:composeNode2,composeEmptyNode:comp...
function resolveBlockSeq (line 835) | function resolveBlockSeq({composeNode:composeNode2,composeEmptyNode:comp...
function resolveEnd (line 835) | function resolveEnd(end,offset,reqSpace,onError){let comment="";if(end){...
function resolveFlowCollection (line 835) | function resolveFlowCollection({composeNode:composeNode2,composeEmptyNod...
function resolveCollection (line 835) | function resolveCollection(CN2,ctx,token,onError,tagName,tag){const coll...
function composeCollection (line 835) | function composeCollection(CN2,ctx,token,tagToken,onError){const tagName...
function resolveBlockScalar (line 835) | function resolveBlockScalar(scalar,strict,onError){const start=scalar.of...
function parseBlockScalarHeader (line 835) | function parseBlockScalarHeader({offset,props},strict,onError){if(props[...
function splitLines (line 835) | function splitLines(source){const split=source.split(/\n( *)/);const fir...
function resolveFlowScalar (line 835) | function resolveFlowScalar(scalar,strict,onError){const{offset,type,sour...
function plainValue (line 835) | function plainValue(source,onError){let badChar="";switch(source[0]){cas...
function singleQuotedValue (line 835) | function singleQuotedValue(source,onError){if(source[source.length-1]!==...
function foldLines (line 835) | function foldLines(source){let first,line;try{first=new RegExp("(.*?)(?<...
function doubleQuotedValue (line 835) | function doubleQuotedValue(source,onError){let res="";for(let i=1;i<sour...
function foldNewline (line 835) | function foldNewline(source,offset){let fold="";let ch=source[offset+1];...
function parseCharCode (line 835) | function parseCharCode(source,offset,length,onError){const cc=source.sub...
function composeScalar (line 835) | function composeScalar(ctx,token,tagToken,onError){const{value,type,comm...
function findScalarTagByName (line 835) | function findScalarTagByName(schema4,value,tagName,tagToken,onError){if(...
function findScalarTagByTest (line 835) | function findScalarTagByTest({directives,schema:schema4},value,token,onE...
function emptyScalarPosition (line 835) | function emptyScalarPosition(offset,before,pos){if(before){if(pos===null...
function composeNode (line 835) | function composeNode(ctx,token,props,onError){const{spaceBefore,comment,...
function composeEmptyNode (line 835) | function composeEmptyNode(ctx,offset,before,pos,{spaceBefore,comment,anc...
function composeAlias (line 835) | function composeAlias({options},{offset,source,end},onError){const alias...
function composeDoc (line 835) | function composeDoc(options,directives,{offset,start,value,end},onError)...
function getErrorPos (line 835) | function getErrorPos(src){if(typeof src==="number")return[src,src+1];if(...
function parsePrelude (line 835) | function parsePrelude(prelude){let comment="";let atComment=false;let af...
method constructor (line 835) | constructor(options={}){this.doc=null;this.atDirectives=false;this.prelu...
method decorate (line 835) | decorate(doc,afterDoc){const{comment,afterEmptyLine}=parsePrelude(this.p...
method streamInfo (line 838) | streamInfo(){return{comment:parsePrelude(this.prelude).comment,directive...
method compose (line 838) | *compose(tokens,forceDoc=false,endOffset=-1){for(const token of tokens)y...
method next (line 838) | *next(token){switch(token.type){case"directive":this.directives.add(toke...
method end (line 839) | *end(forceDoc=false,endOffset=-1){if(this.doc){this.decorate(this.doc,tr...
function visit2 (line 839) | function visit2(cst,visitor){if("type"in cst&&cst.type==="document")cst=...
function _visit (line 839) | function _visit(path,item,visitor){let ctrl=visitor(item,path);if(typeof...
function tokenType (line 839) | function tokenType(source){switch(source){case BOM:return"byte-order-mar...
function isEmpty (line 839) | function isEmpty(ch){switch(ch){case void 0:case" ":case"\n":case"\r":ca...
method constructor (line 839) | constructor(){this.atEnd=false;this.blockScalarIndent=-1;this.blockScala...
method lex (line 839) | *lex(source,incomplete=false){if(source){this.buffer=this.buffer?this.bu...
method atLineEnd (line 839) | atLineEnd(){let i=this.pos;let ch=this.buffer[i];while(ch===" "||ch===" ...
method charAt (line 839) | charAt(n){return this.buffer[this.pos+n]}
method continueScalar (line 839) | continueScalar(offset){let ch=this.buffer[offset];if(this.indentNext>0){...
method getLine (line 839) | getLine(){let end=this.lineEndPos;if(typeof end!=="number"||end!==-1&&en...
method hasChars (line 839) | hasChars(n){return this.pos+n<=this.buffer.length}
method setNext (line 839) | setNext(state){this.buffer=this.buffer.substring(this.pos);this.pos=0;th...
method peek (line 839) | peek(n){return this.buffer.substr(this.pos,n)}
method parseNext (line 839) | *parseNext(next){switch(next){case"stream":return yield*this.parseStream...
method parseStream (line 839) | *parseStream(){let line=this.getLine();if(line===null)return this.setNex...
method parseLineStart (line 839) | *parseLineStart(){const ch=this.charAt(0);if(!ch&&!this.atEnd)return thi...
method parseBlockStart (line 839) | *parseBlockStart(){const[ch0,ch1]=this.peek(2);if(!ch1&&!this.atEnd)retu...
method parseDocument (line 839) | *parseDocument(){yield*this.pushSpaces(true);const line=this.getLine();i...
method parseFlowCollection (line 839) | *parseFlowCollection(){let nl,sp;let indent=-1;do{nl=yield*this.pushNewl...
method parseQuotedScalar (line 839) | *parseQuotedScalar(){const quote=this.charAt(0);let end=this.buffer.inde...
method parseBlockScalarHeader (line 839) | *parseBlockScalarHeader(){this.blockScalarIndent=-1;this.blockScalarKeep...
method parseBlockScalar (line 839) | *parseBlockScalar(){let nl=this.pos-1;let indent=0;let ch;loop:for(let i...
method parsePlainScalar (line 839) | *parsePlainScalar(){const inFlow=this.flowLevel>0;let end=this.pos-1;let...
method pushCount (line 839) | *pushCount(n){if(n>0){yield this.buffer.substr(this.pos,n);this.pos+=n;r...
method pushToIndex (line 839) | *pushToIndex(i,allowEmpty){const s=this.buffer.slice(this.pos,i);if(s){y...
method pushIndicators (line 839) | *pushIndicators(){switch(this.charAt(0)){case"!":return(yield*this.pushT...
method pushTag (line 839) | *pushTag(){if(this.charAt(1)==="<"){let i=this.pos+2;let ch=this.buffer[...
method pushNewline (line 839) | *pushNewline(){const ch=this.buffer[this.pos];if(ch==="\n")return yield*...
method pushSpaces (line 839) | *pushSpaces(allowTabs){let i=this.pos-1;let ch;do{ch=this.buffer[++i]}wh...
method pushUntil (line 839) | *pushUntil(test){let i=this.pos;let ch=this.buffer[i];while(!test(ch))ch...
method constructor (line 839) | constructor(){this.lineStarts=[];this.addNewLine=offset=>this.lineStarts...
function includesToken (line 839) | function includesToken(list,type){for(let i=0;i<list.length;++i)if(list[...
function findNonEmptyIndex (line 839) | function findNonEmptyIndex(list){for(let i=0;i<list.length;++i){switch(l...
function isFlowToken (line 839) | function isFlowToken(token){switch(token?.type){case"alias":case"scalar"...
function getPrevProps (line 839) | function getPrevProps(parent){switch(parent.type){case"document":return ...
function getFirstKeyStartProps (line 839) | function getFirstKeyStartProps(prev){if(prev.length===0)return[];let i=p...
function fixFlowSeqItems (line 839) | function fixFlowSeqItems(fc){if(fc.start.type==="flow-seq-start"){for(co...
method constructor (line 839) | constructor(onNewLine){this.atNewLine=true;this.atScalar=false;this.inde...
method parse (line 839) | *parse(source,incomplete=false){if(this.onNewLine&&this.offset===0)this....
method next (line 839) | *next(source){this.source=source;if(this.atScalar){this.atScalar=false;y...
method end (line 839) | *end(){while(this.stack.length>0)yield*this.pop()}
method sourceToken (line 839) | get sourceToken(){const st={type:this.type,offset:this.offset,indent:thi...
method step (line 839) | *step(){const top=this.peek(1);if(this.type==="doc-end"&&(!top||top.type...
method peek (line 839) | peek(n){return this.stack[this.stack.length-n]}
method pop (line 839) | *pop(error){const token=error??this.stack.pop();if(!token){const message...
method stream (line 839) | *stream(){switch(this.type){case"directive-line":yield{type:"directive",...
method document (line 839) | *document(doc){if(doc.value)return yield*this.lineEnd(doc);switch(this.t...
method scalar (line 839) | *scalar(scalar){if(this.type==="map-value-ind"){const prev=getPrevProps(...
method blockScalar (line 839) | *blockScalar(scalar){switch(this.type){case"space":case"comment":case"ne...
method blockMap (line 839) | *blockMap(map2){const it=map2.items[map2.items.length-1];switch(this.typ...
method blockSequence (line 839) | *blockSequence(seq2){const it=seq2.items[seq2.items.length-1];switch(thi...
method flowCollection (line 839) | *flowCollection(fc){const it=fc.items[fc.items.length-1];if(this.type===...
method flowScalar (line 839) | flowScalar(type){if(this.onNewLine){let nl=this.source.indexOf("\n")+1;w...
method startBlockValue (line 839) | startBlockValue(parent){switch(this.type){case"alias":case"scalar":case"...
method atIndentedComment (line 839) | atIndentedComment(start,indent){if(this.type!=="comment")return false;if...
method documentEnd (line 839) | *documentEnd(docEnd){if(this.type!=="doc-mode"){if(docEnd.end)docEnd.end...
method lineEnd (line 839) | *lineEnd(token){switch(this.type){case"comma":case"doc-start":case"doc-e...
function parseOptions (line 839) | function parseOptions(options){const prettyErrors=options.prettyErrors!=...
function parseDocument (line 839) | function parseDocument(source,options={}){const{lineCounter,prettyErrors...
function parse (line 839) | function parse(src,reviver,options){let _reviver=void 0;if(typeof revive...
function stringify3 (line 839) | function stringify3(value,replacer,options){let _replacer=null;if(typeof...
FILE: npm/test.js
function test (line 1) | async function test(name, fn) {
function run (line 11) | async function run(json, code = '') {
function runNoPipe (line 20) | async function runNoPipe(code = '') {
FILE: preview.go
method handlePreviewKey (line 15) | func (m *model) handlePreviewKey(msg tea.Msg) (tea.Model, tea.Cmd) {
method handlePreviewSearchInput (line 73) | func (m *model) handlePreviewSearchInput(msg tea.KeyMsg) (tea.Model, tea...
method doPreviewSearch (line 100) | func (m *model) doPreviewSearch(pattern string) bool {
method selectPreviewSearchResult (line 183) | func (m *model) selectPreviewSearchResult(i int) {
method previewSearchStatusBar (line 199) | func (m *model) previewSearchStatusBar() string {
method wrapString (line 223) | func (m *model) wrapString(value string) string {
FILE: search.go
method doSearch (line 11) | func (m *model) doSearch(s string) tea.Cmd {
method cancelSearch (line 39) | func (m *model) cancelSearch() {
method selectSearchResult (line 47) | func (m *model) selectSearchResult(i int) {
method redoSearch (line 63) | func (m *model) redoSearch() {
type search (line 83) | type search struct
function newSearch (line 91) | func newSearch() *search {
type match (line 99) | type match struct
type piece (line 104) | type piece struct
function executeSearch (line 111) | func executeSearch(top *Node, s string, cancel <-chan struct{}) (*search...
function splitByIndexes (line 188) | func splitByIndexes(s string, indexes []match) []piece {
function splitIndexesToChunks (line 200) | func splitIndexesToChunks(chunks []string, indexes [][]int, searchIndex ...
FILE: search_test.go
function doSearch (line 13) | func doSearch(m *model, s string) {
function TestBasicSearch (line 29) | func TestBasicSearch(t *testing.T) {
function TestRegexSearch (line 75) | func TestRegexSearch(t *testing.T) {
function TestCaseInsensitiveSearch (line 141) | func TestCaseInsensitiveSearch(t *testing.T) {
function TestSearchInDifferentNodeTypes (line 184) | func TestSearchInDifferentNodeTypes(t *testing.T) {
function TestSearchResultDetails (line 231) | func TestSearchResultDetails(t *testing.T) {
function TestSearchNavigation (line 265) | func TestSearchNavigation(t *testing.T) {
function TestSpecialCharacterSearch (line 306) | func TestSpecialCharacterSearch(t *testing.T) {
function TestEmptyAndEdgeCases (line 351) | func TestEmptyAndEdgeCases(t *testing.T) {
function TestLargeJSONSearch (line 388) | func TestLargeJSONSearch(t *testing.T) {
function TestSearchInWrappedStrings (line 434) | func TestSearchInWrappedStrings(t *testing.T) {
function TestSearchChunkBoundaryMatches (line 481) | func TestSearchChunkBoundaryMatches(t *testing.T) {
function TestSearchMultipleMatchesInChunks (line 530) | func TestSearchMultipleMatchesInChunks(t *testing.T) {
function TestSearchWrappedVsUnwrapped (line 561) | func TestSearchWrappedVsUnwrapped(t *testing.T) {
function TestSearchChunkIndexMapping (line 591) | func TestSearchChunkIndexMapping(t *testing.T) {
function TestSearchEmptyAndShortStringsWithWrap (line 633) | func TestSearchEmptyAndShortStringsWithWrap(t *testing.T) {
function TestSearchRegexAcrossChunks (line 674) | func TestSearchRegexAcrossChunks(t *testing.T) {
FILE: utils.go
function lookup (line 20) | func lookup(names []string, defaultEditor string) string {
function open (line 30) | func open(filePath string, flagYaml, flagToml *bool) *os.File {
function regexCase (line 53) | func regexCase(code string) (string, bool) {
function flex (line 63) | func flex(width int, a, b string) string {
function safeSlice (line 67) | func safeSlice(s string, start, end int) string {
function parseYAML (line 87) | func parseYAML(b []byte) ([]byte, error) {
function isRefNode (line 110) | func isRefNode(n *jsonx.Node) (string, bool) {
FILE: version.go
constant version (line 3) | version = "39.2.0"
FILE: view.go
method View (line 15) | func (m *model) View() string {
method centerLine (line 222) | func (m *model) centerLine(n *Node) {
FILE: vim.go
method runCommand (line 11) | func (m *model) runCommand(s string) (tea.Model, tea.Cmd) {
function gotoLine (line 22) | func gotoLine(m *model, num int) {
function findNode (line 29) | func findNode(m *model, line int) *Node {
FILE: vim_test.go
function init (line 13) | func init() {
function TestGotoLine (line 17) | func TestGotoLine(t *testing.T) {
function TestGotoLineCollapsed (line 30) | func TestGotoLineCollapsed(t *testing.T) {
function TestGotoLineInputInvalid (line 45) | func TestGotoLineInputInvalid(t *testing.T) {
function TestGotoLineInputGreaterThanTotalLines (line 63) | func TestGotoLineInputGreaterThanTotalLines(t *testing.T) {
function TestGotoLineInputLessThanOne (line 76) | func TestGotoLineInputLessThanOne(t *testing.T) {
function TestGotoLineKeepsHistory (line 92) | func TestGotoLineKeepsHistory(t *testing.T) {
Condensed preview — 103 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (589K chars).
[
{
"path": ".gitattributes",
"chars": 15,
"preview": "*.golden -text\n"
},
{
"path": ".github/images/autocomplete.tape",
"chars": 397,
"preview": "Output autocomplete.gif\nOutput autocomplete.mp4\n\nSet Shell zsh\nSet FontSize 32\nSet Width 1800\nSet Height 400\nSet TypingS"
},
{
"path": ".github/images/preview-mode.tape",
"chars": 371,
"preview": "Output preview-mode.gif\nOutput preview-mode.mp4\n\nSet FontSize 32\nSet Width 1800\nSet Height 1200\nSet TypingSpeed 200ms\n\nH"
},
{
"path": ".github/images/preview.tape",
"chars": 296,
"preview": "Output preview.gif\nOutput preview.mp4\n\nSet FontSize 32\nSet Width 1800\nSet Height 1200\nSet TypingSpeed 200ms\n\nHide\nType \""
},
{
"path": ".github/stream.mjs",
"chars": 2374,
"preview": "#!/usr/bin/env zx\n\nprocess.on('SIGPIPE', () => process.exit(0))\nprocess.on('SIGINT', () => process.exit(0))\nprocess.on('"
},
{
"path": ".github/workflows/brew.yml",
"chars": 664,
"preview": "name: brew\n\non: [workflow_dispatch]\n\njobs:\n brew:\n runs-on: macos-latest\n steps:\n - name: Set up Homebrew\n "
},
{
"path": ".github/workflows/docker.yml",
"chars": 635,
"preview": "name: docker\n\non: [workflow_dispatch]\n\njobs:\n docker:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n "
},
{
"path": ".github/workflows/snap.yml",
"chars": 539,
"preview": "name: snap\n\non: [workflow_dispatch]\n\njobs:\n snap:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@"
},
{
"path": ".github/workflows/test.yml",
"chars": 658,
"preview": "name: test\n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ master ]\n\njobs:\n go:\n runs-on: ubun"
},
{
"path": ".gitignore",
"chars": 17,
"preview": "*.prof\nfx\nfx.exe\n"
},
{
"path": "Dockerfile",
"chars": 246,
"preview": "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 b"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2018 Anton Medvedev\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.md",
"chars": 680,
"preview": "# f(x)\n\n<p align=\"center\"><a href=\"https://fx.wtf\"><img src=\".github/images/preview.gif\" width=\"500\" alt=\"fx preview\"></"
},
{
"path": "RELEASE.md",
"chars": 646,
"preview": "# Release\n\n1. Bump version in [version.go](version.go).\n2. Bump version in [snapcraft.yaml](snap/snapcraft.yaml).\n3. Bum"
},
{
"path": "go.mod",
"chars": 1879,
"preview": "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."
},
{
"path": "go.sum",
"chars": 7735,
"preview": "github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=\ngithub.com/Masterminds/semver/v3"
},
{
"path": "help.go",
"chars": 4601,
"preview": "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/"
},
{
"path": "internal/complete/complete.bash",
"chars": 31,
"preview": "complete -o filenames -C fx fx\n"
},
{
"path": "internal/complete/complete.fish",
"chars": 69,
"preview": "complete --command fx --arguments '(COMP_FISH=(commandline -cp) fx)'\n"
},
{
"path": "internal/complete/complete.go",
"chars": 7235,
"preview": "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\"git"
},
{
"path": "internal/complete/complete.zsh",
"chars": 984,
"preview": "#compdef fx\n\n_fx() {\n local -a reply\n reply=(\"${(@f)$(COMP_ZSH=\"${LBUFFER}\" fx)}\")\n if (( ${#reply} )); then\n "
},
{
"path": "internal/complete/complete_test.go",
"chars": 6111,
"preview": "package complete\n\nimport (\n\t\"testing\"\n\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc TestKeysComplete(t *testing.T) "
},
{
"path": "internal/complete/prelude.js",
"chars": 726,
"preview": "const __keys = new Set()\n\nObject.prototype.__keys = function () {\n if (Array.isArray(this)) return\n if (typeof this =="
},
{
"path": "internal/complete/utils.go",
"chars": 1880,
"preview": "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) {"
},
{
"path": "internal/engine/engine.go",
"chars": 3198,
"preview": "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/anto"
},
{
"path": "internal/engine/engine_test.go",
"chars": 3701,
"preview": "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/in"
},
{
"path": "internal/engine/format_err.go",
"chars": 2264,
"preview": "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"
},
{
"path": "internal/engine/fxrc.go",
"chars": 1407,
"preview": "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 stri"
},
{
"path": "internal/engine/quote.go",
"chars": 983,
"preview": "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 string"
},
{
"path": "internal/engine/quote_test.go",
"chars": 3150,
"preview": "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"
},
{
"path": "internal/engine/slurp.go",
"chars": 999,
"preview": "package engine\n\nimport (\n\t\"io\"\n\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc Slurp(parser Parser, writeErr func(str"
},
{
"path": "internal/engine/stdlib.js",
"chars": 4390,
"preview": "'use strict'\n\nconst console = {\n log: function (...args) {\n const parts = []\n for (const arg of args) {\n if "
},
{
"path": "internal/engine/stdlib_test.go",
"chars": 33553,
"preview": "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\"g"
},
{
"path": "internal/engine/stringify.go",
"chars": 2113,
"preview": "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 Str"
},
{
"path": "internal/engine/transpile.go",
"chars": 1802,
"preview": "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.Bui"
},
{
"path": "internal/engine/transpile_test.go",
"chars": 933,
"preview": "package engine\n\nimport (\n\t\"testing\"\n)\n\nfunc TestTranspile(t *testing.T) {\n\ttests := []struct {\n\t\tcode string\n\t\twant stri"
},
{
"path": "internal/engine/utils.go",
"chars": 734,
"preview": "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 = re"
},
{
"path": "internal/engine/vm.go",
"chars": 2000,
"preview": "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// Fi"
},
{
"path": "internal/fuzzy/algo.go",
"chars": 12018,
"preview": "package fuzzy\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\nvar delimiterChars = \".\"\n\nconst whiteChars = \""
},
{
"path": "internal/fuzzy/chars.go",
"chars": 6596,
"preview": "package fuzzy\n\nimport (\n\t\"bytes\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n\t\"unsafe\"\n)\n\nconst (\n\toverflow64 uint64 = 0x8080808080808080"
},
{
"path": "internal/fuzzy/chars_test.go",
"chars": 2125,
"preview": "package fuzzy\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestToCharsAscii(t *testing.T) {\n\tchars := ToChars([]byte(\"foobar\"))\n\t"
},
{
"path": "internal/fuzzy/find.go",
"chars": 535,
"preview": "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 ["
},
{
"path": "internal/fuzzy/fuzzy_test.go",
"chars": 10439,
"preview": "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"
},
{
"path": "internal/fuzzy/normalize.go",
"chars": 21646,
"preview": "// Normalization of latin script letters\n// Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt\n\npackage f"
},
{
"path": "internal/fuzzy/utils.go",
"chars": 865,
"preview": "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."
},
{
"path": "internal/ident/ident.go",
"chars": 575,
"preview": "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 i"
},
{
"path": "internal/jsonpath/path.go",
"chars": 3017,
"preview": "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\tprop"
},
{
"path": "internal/jsonpath/path_test.go",
"chars": 2838,
"preview": "package jsonpath_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/antonmedv/fx/internal/j"
},
{
"path": "internal/jsonpath/ref.go",
"chars": 771,
"preview": "package jsonpath\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc ParseSchemaRef(ref string) ([]any, bool) {\n\t// Must start with "
},
{
"path": "internal/jsonpath/ref_test.go",
"chars": 1417,
"preview": "package jsonpath_test\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/antonmedv/fx/internal/jsonpath\"\n)\n\nfunc TestParseSch"
},
{
"path": "internal/jsonx/delete.go",
"chars": 2345,
"preview": "package jsonx\n\n// DeleteNode removes the node at from the linked structure and returns a node to select next.\n// It retu"
},
{
"path": "internal/jsonx/delete_test.go",
"chars": 2927,
"preview": "package jsonx_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t. \"github.com/antonmedv/fx/internal/js"
},
{
"path": "internal/jsonx/format_err.go",
"chars": 1653,
"preview": "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/g"
},
{
"path": "internal/jsonx/json.go",
"chars": 11865,
"preview": "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"
},
{
"path": "internal/jsonx/jsonx_test.go",
"chars": 6261,
"preview": "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/"
},
{
"path": "internal/jsonx/line.go",
"chars": 805,
"preview": "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 "
},
{
"path": "internal/jsonx/node.go",
"chars": 5900,
"preview": "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 = "
},
{
"path": "internal/jsonx/node_test.go",
"chars": 2085,
"preview": "package jsonx\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/requi"
},
{
"path": "internal/jsonx/string.go",
"chars": 565,
"preview": "package jsonx\n\nimport (\n\t\"strings\"\n)\n\nconst (\n\tcurlyBracketOpen = \"{\"\n\tcurlyBracketClose = \"}\"\n\tcurlyBracketPair = "
},
{
"path": "internal/jsonx/to_value.go",
"chars": 2434,
"preview": "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/inte"
},
{
"path": "internal/jsonx/wrap.go",
"chars": 1531,
"preview": "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\n"
},
{
"path": "internal/pretty/inlineable.go",
"chars": 2709,
"preview": "package pretty\n\nimport (\n\t\"github.com/antonmedv/fx/internal/jsonx\"\n)\n\nfunc isInlineable(n *jsonx.Node) bool {\n\tif n.Kind"
},
{
"path": "internal/pretty/inlineable_test.go",
"chars": 11150,
"preview": "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"
},
{
"path": "internal/pretty/pretty_print.go",
"chars": 2117,
"preview": "package pretty\n\nimport (\n\t\"strings\"\n\n\t\"github.com/antonmedv/fx/internal/ident\"\n\t\"github.com/antonmedv/fx/internal/jsonx\""
},
{
"path": "internal/pretty/pretty_print_test.go",
"chars": 11025,
"preview": "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/"
},
{
"path": "internal/shlex/shlex.go",
"chars": 9995,
"preview": "/*\nCopyright 2012 Google Inc. All Rights Reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
},
{
"path": "internal/shlex/shlex_test.go",
"chars": 3174,
"preview": "/*\nCopyright 2012 Google Inc. All Rights Reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou "
},
{
"path": "internal/theme/theme.go",
"chars": 11101,
"preview": "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.c"
},
{
"path": "internal/toml/toml.go",
"chars": 7138,
"preview": "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/"
},
{
"path": "internal/toml/toml_test.go",
"chars": 3109,
"preview": "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: comp"
},
{
"path": "internal/utils/image.go",
"chars": 1376,
"preview": "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/charm"
},
{
"path": "internal/utils/life.go",
"chars": 3193,
"preview": "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\nf"
},
{
"path": "internal/utils/utils.go",
"chars": 499,
"preview": "package utils\n\nimport (\n\t\"encoding/json\"\n)\n\nfunc IsHexDigit(ch byte) bool {\n\treturn (ch >= '0' && ch <= '9') || (ch >= '"
},
{
"path": "internal/utils/utils_test.go",
"chars": 2802,
"preview": "package utils\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsHexDigit(t *testing.T) {\n\ttests "
},
{
"path": "keymap.go",
"chars": 6101,
"preview": "package main\n\nimport \"github.com/charmbracelet/bubbles/key\"\n\ntype KeyMap struct {\n\tUp key.Binding `cate"
},
{
"path": "main.go",
"chars": 30199,
"preview": "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"
},
{
"path": "main_test.go",
"chars": 2648,
"preview": "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 \"gith"
},
{
"path": "npm/README.md",
"chars": 3038,
"preview": "# fx\n\nA non-interactive, JavaScript version of the [**fx**](https://fx.wtf). \nShort for _Function eXecution_ or _f(x)_.\n"
},
{
"path": "npm/index.js",
"chars": 153451,
"preview": "#!/usr/bin/env node\n'use strict'\n\nvoid async function main() {\n const os = await import('node:os')\n const fs = await i"
},
{
"path": "npm/package.json",
"chars": 334,
"preview": "{\n \"name\": \"fx\",\n \"version\": \"39.2.0\",\n \"bin\": {\n \"fx\": \"index.js\"\n },\n \"files\": [\n \"index.js\"\n ],\n \"script"
},
{
"path": "npm/test.js",
"chars": 7577,
"preview": "async function test(name, fn) {\n try {\n await fn(await import('node:assert/strict'))\n console.log(`✓ ${name}`)\n "
},
{
"path": "preview.go",
"chars": 5377,
"preview": "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/charmbracel"
},
{
"path": "scripts/build.mjs",
"chars": 741,
"preview": "$.verbose = true\n\nconst goos = [\n 'linux',\n 'darwin',\n 'windows',\n]\nconst goarch = [\n 'amd64',\n 'arm64',\n]\n\nconst n"
},
{
"path": "search.go",
"chars": 5152,
"preview": "package main\n\nimport (\n\t\"regexp\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t. \"github.com/antonmedv/fx/internal/jsonx\""
},
{
"path": "search_test.go",
"chars": 18953,
"preview": "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. \"git"
},
{
"path": "snap/snapcraft.yaml",
"chars": 510,
"preview": "name: fx\nversion: 39.2.0\nsummary: Terminal JSON viewer\ndescription: Terminal JSON viewer\nbase: core20\ngrade: stable\nconf"
},
{
"path": "testdata/TestCollapseRecursive.golden",
"chars": 887,
"preview": "\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"
},
{
"path": "testdata/TestCollapseRecursiveWithSizes.golden",
"chars": 915,
"preview": "\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 a"
},
{
"path": "testdata/TestGotoLine.golden",
"chars": 1163,
"preview": "\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 s"
},
{
"path": "testdata/TestGotoLineCollapsed.golden",
"chars": 1094,
"preview": "\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 s"
},
{
"path": "testdata/TestGotoLineInputGreaterThanTotalLines.golden",
"chars": 1163,
"preview": "\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 s"
},
{
"path": "testdata/TestGotoLineInputInvalid.golden",
"chars": 1013,
"preview": "\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 s"
},
{
"path": "testdata/TestGotoLineInputLessThanOne.golden",
"chars": 1163,
"preview": "\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"
},
{
"path": "testdata/TestGotoLineKeepsHistory.golden",
"chars": 1163,
"preview": "\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 s"
},
{
"path": "testdata/TestNavigation.golden",
"chars": 946,
"preview": "\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, s"
},
{
"path": "testdata/TestOutput.golden",
"chars": 946,
"preview": "\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"
},
{
"path": "testdata/blog.json",
"chars": 1123,
"preview": "{\n \"title\": \"Lorem ipsum\",\n \"body\": \"# Lorem Ipsum Dolor Sit Amet\\n\\n## Consectetur Adipiscing Elit\\n\\nLorem ipsum dol"
},
{
"path": "testdata/example.json",
"chars": 644,
"preview": "{\n \"title\": \"Lorem ipsum\",\n \"text\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor in"
},
{
"path": "utils.go",
"chars": 2231,
"preview": "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/goc"
},
{
"path": "version.go",
"chars": 39,
"preview": "package main\n\nconst version = \"39.2.0\"\n"
},
{
"path": "view.go",
"chars": 6202,
"preview": "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/a"
},
{
"path": "vim.go",
"chars": 851,
"preview": "package main\n\nimport (\n\t\"strconv\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t. \"github.com/antonmedv/fx/internal/jsonx"
},
{
"path": "vim_test.go",
"chars": 3550,
"preview": "package main\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss"
}
]
About this extraction
This page contains the full source code of the antonmedv/fx GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 103 files (518.4 KB), approximately 168.0k tokens, and a symbol index with 840 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.