From 04f79a71d8f1f6e7da2674192d8279b231631341 Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Thu, 21 Mar 2024 20:48:09 +0100 Subject: [PATCH] Add fallback engine (#300) --- .github/workflows/snap.yml | 2 +- .github/workflows/test.yml | 2 +- go.mod | 6 +- go.sum | 13 +- internal/complete/complete.go | 22 +- internal/complete/prelude.js | 10 + internal/engine/engine.go | 126 +++++++ internal/engine/prelude.js | 9 + .../{complete/prelude.go => engine/stdlib.js} | 32 +- .../transpile.go => engine/transform.go} | 2 +- internal/engine/utils.go | 36 ++ json.go => internal/jsonx/json.go | 102 +++--- internal/jsonx/node.go | 259 ++++++++++++++ node_test.go => internal/jsonx/node_test.go | 18 +- ring.go => internal/jsonx/ring.go | 2 +- internal/jsonx/string.go | 61 ++++ wrap.go => internal/jsonx/wrap.go | 50 +-- theme.go => internal/theme/theme.go | 102 +++--- internal/utils/utils.go | 9 + main.go | 322 +++++++++--------- main_test.go | 9 +- node.go | 259 -------------- reduce.go | 25 +- search.go | 16 +- utils.go | 15 - 25 files changed, 882 insertions(+), 627 deletions(-) create mode 100644 internal/complete/prelude.js create mode 100644 internal/engine/engine.go create mode 100644 internal/engine/prelude.js rename internal/{complete/prelude.go => engine/stdlib.js} (76%) rename internal/{complete/transpile.go => engine/transform.go} (98%) create mode 100644 internal/engine/utils.go rename json.go => internal/jsonx/json.go (78%) create mode 100644 internal/jsonx/node.go rename node_test.go => internal/jsonx/node_test.go (58%) rename ring.go => internal/jsonx/ring.go (98%) create mode 100644 internal/jsonx/string.go rename wrap.go => internal/jsonx/wrap.go (52%) rename theme.go => internal/theme/theme.go (81%) create mode 100644 internal/utils/utils.go delete mode 100644 node.go diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 47b3541..a7321e3 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.21 - uses: snapcore/action-build@v1 id: build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c41ab13..69ce361 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.21 - name: Test run: go test ./... diff --git a/go.mod b/go.mod index 6b40041..6993f3e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/antonmedv/fx -go 1.20 +go 1.21 require ( github.com/antonmedv/clipboard v1.0.1 @@ -23,10 +23,10 @@ require ( github.com/aymanbagabas/go-udiff v0.1.3 // indirect github.com/containerd/console v1.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.7.0 // indirect + github.com/dlclark/regexp2 v1.10.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect - github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect + github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-localereader v0.0.1 // indirect diff --git a/go.sum b/go.sum index c25296c..a00fd6a 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,9 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 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.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 h1:O7I1iuzEA7SG+dK8ocOBSlYAA9jBUmCYl/Qa7ey7JAM= github.com/dop251/goja v0.0.0-20240220182346-e401ed450204/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= @@ -33,15 +34,20 @@ github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ= +github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -52,7 +58,9 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 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-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -89,6 +97,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= diff --git a/internal/complete/complete.go b/internal/complete/complete.go index 9af8ed9..2e6e128 100644 --- a/internal/complete/complete.go +++ b/internal/complete/complete.go @@ -1,6 +1,7 @@ package complete import ( + _ "embed" "fmt" "os" "path/filepath" @@ -10,6 +11,7 @@ import ( "github.com/dop251/goja" "github.com/goccy/go-yaml" + "github.com/antonmedv/fx/internal/engine" "github.com/antonmedv/fx/internal/shlex" ) @@ -53,6 +55,9 @@ var globals = []string{ "skip", } +//go:embed prelude.js +var prelude string + func Complete() bool { compLine, ok := os.LookupEnv("COMP_LINE") @@ -145,7 +150,7 @@ func doComplete(compLine string, compWord string) { } } - codeComplete(string(input), args, compWord) + codeComplete(input, args, compWord) } } @@ -163,7 +168,7 @@ func globalsComplete(compWord string) bool { return false } -func codeComplete(input string, args []string, compWord string) { +func codeComplete(input []byte, args []string, compWord string) { args = args[2:] // Drop binary & file from the args. if compWord == "" { @@ -180,21 +185,24 @@ func codeComplete(input string, args []string, compWord string) { var code strings.Builder code.WriteString(prelude) - code.WriteString(fmt.Sprintf("let json = %s\n", input)) + code.WriteString(engine.Stdlib) + code.WriteString("let json = ") + code.Write(input) for _, arg := range args { - if arg == "" { + if arg == "" { // After dropTail, we can have empty strings. continue } - code.WriteString(Transform(arg)) + code.WriteString(engine.Transform(arg)) } code.WriteString("\n__keys\n") - out, err := goja.New().RunString(code.String()) + vm := goja.New() + value, err := vm.RunString(code.String()) if err != nil { return } - if array, ok := out.Export().([]interface{}); ok { + if array, ok := value.Export().([]interface{}); ok { prefix := dropTail(compWord) var reply []string for _, key := range array { diff --git a/internal/complete/prelude.js b/internal/complete/prelude.js new file mode 100644 index 0000000..bcfdb52 --- /dev/null +++ b/internal/complete/prelude.js @@ -0,0 +1,10 @@ +const __keys = new Set() + +Object.prototype.__keys = function () { + if (Array.isArray(this)) return + if (typeof this === 'string') return + if (this instanceof String) return + if (typeof this === 'object' && this !== null) + Object.keys(this).forEach(x => __keys.add(x)) +} + diff --git a/internal/engine/engine.go b/internal/engine/engine.go new file mode 100644 index 0000000..60e4b2f --- /dev/null +++ b/internal/engine/engine.go @@ -0,0 +1,126 @@ +package engine + +import ( + _ "embed" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/dop251/goja" + "github.com/goccy/go-yaml" + + "github.com/antonmedv/fx/internal/jsonx" +) + +//go:embed stdlib.js +var Stdlib string + +//go:embed prelude.js +var prelude string + +func Reduce(args []string) { + if len(args) < 1 { + panic("args must have at least one element") + } + + var ( + flagYaml bool + flagRaw bool + flagSlurp bool + ) + + var src io.Reader = os.Stdin + if isFile(args[0]) { + src = open(args[0], &flagYaml) + args = args[1:] + } else if isFile(args[len(args)-1]) { + src = open(args[len(args)-1], &flagYaml) + args = args[:len(args)-1] + } + + var fns []string + for _, arg := range args { + switch arg { + case "--yaml": + flagYaml = true + case "--raw", "-r": + flagRaw = true + case "--slurp", "-s": + flagSlurp = true + case "-rs", "-sr": + flagRaw = true + flagSlurp = true + default: + fns = append(fns, arg) + } + } + + if flagSlurp { + println("Error: Built-in JS engine does not support \"--slurp\" flag. Install Node.js or Deno to use this flag.") + os.Exit(1) + } + + data, err := io.ReadAll(src) + if err != nil { + panic(err) + } + + if flagRaw { + data = []byte(strconv.Quote(string(data))) + } else if flagYaml { + data, err = yaml.YAMLToJSON(data) + if err != nil { + println(err.Error()) + os.Exit(1) + } + } else { + node, err := jsonx.Parse(data) + if err != nil { + println(err.Error()) + os.Exit(1) + } + data = []byte(node.String()) + } + + var code strings.Builder + code.WriteString(prelude) + code.WriteString(Stdlib) + code.WriteString(fmt.Sprintf("let json = JSON.parse(%q)\n", data)) + for _, fn := range fns { + code.WriteString(Transform(fn)) + } + code.WriteString("JSON.stringify(json)") + + vm := goja.New() + vm.Set("println", func(s string) any { + fmt.Println(s) + return nil + }) + + value, err := vm.RunString(code.String()) + if err != nil { + println(err.Error()) + os.Exit(1) + } + + output, ok := value.Export().(string) + if !ok { + println("undefined") + return + } + + node, err := jsonx.Parse([]byte(output)) + if err != nil { + println(err.Error()) + os.Exit(1) + } + + if len(node.Value) > 0 && node.Value[0] == '"' { + s, _ := strconv.Unquote(string(node.Value)) + fmt.Println(s) + return + } + fmt.Print(node.PrettyPrint()) +} diff --git a/internal/engine/prelude.js b/internal/engine/prelude.js new file mode 100644 index 0000000..b1fb47e --- /dev/null +++ b/internal/engine/prelude.js @@ -0,0 +1,9 @@ +const console = { + log: function (...args) { + const parts = [] + for (const arg of args) { + parts.push(typeof arg === 'string' ? arg : JSON.stringify(arg, null, 2)) + } + println(parts.join(' ')) + }, +} diff --git a/internal/complete/prelude.go b/internal/engine/stdlib.js similarity index 76% rename from internal/complete/prelude.go rename to internal/engine/stdlib.js index a51adac..1a95e0e 100644 --- a/internal/complete/prelude.go +++ b/internal/engine/stdlib.js @@ -1,16 +1,3 @@ -package complete - -const prelude = ` -const __keys = new Set() - -Object.prototype.__keys = function () { - if (Array.isArray(this)) return - if (typeof this === 'string') return - if (this instanceof String) return - if (typeof this === 'object' && this !== null) - Object.keys(this).forEach(x => __keys.add(x)) -} - function apply(fn, ...args) { if (typeof fn === 'function') return fn(...args) return fn @@ -20,23 +7,23 @@ 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() + throw new Error(`Cannot get length of ${typeof x}`) } function uniq(x) { if (Array.isArray(x)) return [...new Set(x)] - throw new Error() + throw new Error(`Cannot get unique values of ${typeof x}`) } function sort(x) { if (Array.isArray(x)) return x.sort() - throw new Error() + throw new Error(`Cannot sort ${typeof x}`) } function map(fn) { return function (x) { if (Array.isArray(x)) return x.map((v, i) => fn(v, i)) - throw new Error() + throw new Error(`Cannot map ${typeof x}`) } } @@ -47,7 +34,7 @@ function sortBy(fn) { const fb = fn(b) return fa < fb ? -1 : fa > fb ? 1 : 0 }) - throw new Error() + throw new Error(`Cannot sort ${typeof x}`) } } @@ -85,21 +72,20 @@ function zip(...x) { function flatten(x) { if (Array.isArray(x)) return x.flat() - throw new Error() + throw new Error(`Cannot flatten ${typeof x}`) } function reverse(x) { if (Array.isArray(x)) return x.reverse() - throw new Error() + throw new Error(`Cannot reverse ${typeof x}`) } function keys(x) { if (typeof x === 'object' && x !== null) return Object.keys(x) - throw new Error() + 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() + throw new Error(`Cannot get values of ${typeof x}`) } -` diff --git a/internal/complete/transpile.go b/internal/engine/transform.go similarity index 98% rename from internal/complete/transpile.go rename to internal/engine/transform.go index b82780b..9c7457f 100644 --- a/internal/complete/transpile.go +++ b/internal/engine/transform.go @@ -1,4 +1,4 @@ -package complete +package engine import ( "fmt" diff --git a/internal/engine/utils.go b/internal/engine/utils.go new file mode 100644 index 0000000..eec6074 --- /dev/null +++ b/internal/engine/utils.go @@ -0,0 +1,36 @@ +package engine + +import ( + "errors" + "io/fs" + "os" + "path" + "regexp" +) + +func isFile(name string) bool { + stat, err := os.Stat(name) + if err != nil { + return false + } + return !stat.IsDir() +} + +func open(filePath string, flagYaml *bool) *os.File { + f, err := os.Open(filePath) + if err != nil { + var pathError *fs.PathError + if errors.As(err, &pathError) { + println(err.Error()) + os.Exit(1) + } else { + panic(err) + } + } + fileName := path.Base(filePath) + hasYamlExt, _ := regexp.MatchString(`(?i)\.ya?ml$`, fileName) + if !*flagYaml && hasYamlExt { + *flagYaml = true + } + return f +} diff --git a/json.go b/internal/jsonx/json.go similarity index 78% rename from json.go rename to internal/jsonx/json.go index 248bad6..721c137 100644 --- a/json.go +++ b/internal/jsonx/json.go @@ -1,10 +1,12 @@ -package main +package jsonx import ( "fmt" "strconv" "strings" "unicode/utf8" + + "github.com/antonmedv/fx/internal/utils" ) type jsonParser struct { @@ -17,7 +19,7 @@ type jsonParser struct { skipFirstIdent bool } -func parse(data []byte) (head *node, err error) { +func Parse(data []byte) (head *Node, err error) { p := &jsonParser{ data: data, lineNumber: 1, @@ -29,14 +31,14 @@ func parse(data []byte) (head *node, err error) { } }() p.next() - var next *node + var next *Node for p.lastChar != 0 { value := p.parseValue() if head == nil { head = value next = head } else { - value.index = -1 + value.Index = -1 next.adjacent(value) next = value } @@ -57,10 +59,10 @@ func (p *jsonParser) next() { p.sourceTail.writeByte(p.lastChar) } -func (p *jsonParser) parseValue() *node { +func (p *jsonParser) parseValue() *Node { p.skipWhitespace() - var l *node + var l *Node switch p.lastChar { case '"': l = p.parseString() @@ -84,8 +86,8 @@ func (p *jsonParser) parseValue() *node { return l } -func (p *jsonParser) parseString() *node { - str := &node{depth: p.depth} +func (p *jsonParser) parseString() *Node { + str := &Node{Depth: p.depth} start := p.end - 1 p.next() escaped := false @@ -96,7 +98,7 @@ func (p *jsonParser) parseString() *node { var unicode string for i := 0; i < 4; i++ { p.next() - if !isHexDigit(p.lastChar) { + if !utils.IsHexDigit(p.lastChar) { panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s%c'", unicode, p.lastChar)) } unicode += string(p.lastChar) @@ -122,19 +124,19 @@ func (p *jsonParser) parseString() *node { p.next() } - str.value = p.data[start:p.end] + str.Value = p.data[start:p.end] p.next() return str } -func (p *jsonParser) parseNumber() *node { - num := &node{depth: p.depth} +func (p *jsonParser) parseNumber() *Node { + num := &Node{Depth: p.depth} start := p.end - 1 // Handle negative numbers if p.lastChar == '-' { p.next() - if !isDigit(p.lastChar) { + if !utils.IsDigit(p.lastChar) { panic(fmt.Sprintf("Invalid character %q in number", p.lastChar)) } } @@ -143,7 +145,7 @@ func (p *jsonParser) parseNumber() *node { if p.lastChar == '0' { p.next() } else { - for isDigit(p.lastChar) { + for utils.IsDigit(p.lastChar) { p.next() } } @@ -151,10 +153,10 @@ func (p *jsonParser) parseNumber() *node { // Decimal portion if p.lastChar == '.' { p.next() - if !isDigit(p.lastChar) { + if !utils.IsDigit(p.lastChar) { panic(fmt.Sprintf("Invalid character %q in number", p.lastChar)) } - for isDigit(p.lastChar) { + for utils.IsDigit(p.lastChar) { p.next() } } @@ -165,28 +167,28 @@ func (p *jsonParser) parseNumber() *node { if p.lastChar == '+' || p.lastChar == '-' { p.next() } - if !isDigit(p.lastChar) { + if !utils.IsDigit(p.lastChar) { panic(fmt.Sprintf("Invalid character %q in number", p.lastChar)) } - for isDigit(p.lastChar) { + for utils.IsDigit(p.lastChar) { p.next() } } - num.value = p.data[start : p.end-1] + num.Value = p.data[start : p.end-1] return num } -func (p *jsonParser) parseObject() *node { - object := &node{depth: p.depth} - object.value = []byte{'{'} +func (p *jsonParser) parseObject() *Node { + object := &Node{Depth: p.depth} + object.Value = []byte{'{'} p.next() p.skipWhitespace() // Empty object if p.lastChar == '}' { - object.value = append(object.value, '}') + object.Value = append(object.Value, '}') p.next() return object } @@ -199,8 +201,8 @@ func (p *jsonParser) parseObject() *node { p.depth++ key := p.parseString() - key.key, key.value = key.value, nil - object.size += 1 + key.Key, key.Value = key.Value, nil + object.Size += 1 key.directParent = object p.skipWhitespace() @@ -216,34 +218,34 @@ func (p *jsonParser) parseObject() *node { value := p.parseValue() p.depth-- - key.value = value.value - key.size = value.size - key.next = value.next - if key.next != nil { - key.next.prev = key + key.Value = value.Value + key.Size = value.Size + key.Next = value.Next + if key.Next != nil { + key.Next.Prev = key } - key.end = value.end + key.End = value.End value.indirectParent = key object.append(key) p.skipWhitespace() if p.lastChar == ',' { - object.end.comma = true + object.End.Comma = true p.next() p.skipWhitespace() if p.lastChar == '}' { - object.end.comma = false + object.End.Comma = false } else { continue } } if p.lastChar == '}' { - closeBracket := &node{depth: p.depth} - closeBracket.value = []byte{'}'} + closeBracket := &Node{Depth: p.depth} + closeBracket.Value = []byte{'}'} closeBracket.directParent = object - closeBracket.index = -1 + closeBracket.Index = -1 object.append(closeBracket) p.next() return object @@ -253,15 +255,15 @@ func (p *jsonParser) parseObject() *node { } } -func (p *jsonParser) parseArray() *node { - arr := &node{depth: p.depth} - arr.value = []byte{'['} +func (p *jsonParser) parseArray() *Node { + arr := &Node{Depth: p.depth} + arr.Value = []byte{'['} p.next() p.skipWhitespace() if p.lastChar == ']' { - arr.value = append(arr.value, ']') + arr.Value = append(arr.Value, ']') p.next() return arr } @@ -270,29 +272,29 @@ func (p *jsonParser) parseArray() *node { p.depth++ value := p.parseValue() value.directParent = arr - arr.size += 1 - value.index = i + arr.Size += 1 + value.Index = i p.depth-- arr.append(value) p.skipWhitespace() if p.lastChar == ',' { - arr.end.comma = true + arr.End.Comma = true p.next() p.skipWhitespace() if p.lastChar == ']' { - arr.end.comma = false + arr.End.Comma = false } else { continue } } if p.lastChar == ']' { - closeBracket := &node{depth: p.depth} - closeBracket.value = []byte{']'} + closeBracket := &Node{Depth: p.depth} + closeBracket.Value = []byte{']'} closeBracket.directParent = arr - closeBracket.index = -1 + closeBracket.Index = -1 arr.append(closeBracket) p.next() return arr @@ -302,7 +304,7 @@ func (p *jsonParser) parseArray() *node { } } -func (p *jsonParser) parseKeyword(name string) *node { +func (p *jsonParser) parseKeyword(name string) *Node { for i := 1; i < len(name); i++ { p.next() if p.lastChar != name[i] { @@ -313,8 +315,8 @@ func (p *jsonParser) parseKeyword(name string) *node { nextCharIsSpecial := isWhitespace(p.lastChar) || p.lastChar == ',' || p.lastChar == '}' || p.lastChar == ']' || p.lastChar == 0 if nextCharIsSpecial { - keyword := &node{depth: p.depth} - keyword.value = []byte(name) + keyword := &Node{Depth: p.depth} + keyword.Value = []byte(name) return keyword } diff --git a/internal/jsonx/node.go b/internal/jsonx/node.go new file mode 100644 index 0000000..0d0a881 --- /dev/null +++ b/internal/jsonx/node.go @@ -0,0 +1,259 @@ +package jsonx + +import ( + "strconv" + + jsonpath "github.com/antonmedv/fx/path" +) + +type Node struct { + Prev, Next, End *Node + directParent *Node + indirectParent *Node + Collapsed *Node + Depth uint8 + Key []byte + Value []byte + Size int + Chunk []byte + ChunkEnd *Node + Comma bool + Index 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 +} + +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 = nil + + 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) Parent() *Node { + if n.directParent == nil { + return nil + } + parent := n.directParent + if parent.indirectParent != nil { + parent = parent.indirectParent + } + return parent +} + +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 { + it = it.Next + } + } +} + +func (n *Node) FindChildByKey(key string) *Node { + it := n.Next + for it != nil && it != n.End { + if it.Key != nil { + k, err := strconv.Unquote(string(it.Key)) + if err != nil { + return nil + } + if k == key { + return it + } + } + if it.ChunkEnd != nil { + it = it.ChunkEnd.Next + } else if it.End != nil { + it = it.End.Next + } else { + it = it.Next + } + } + return nil +} + +func (n *Node) FindChildByIndex(index int) *Node { + for at := n.Next; at != nil && at != n.End; { + if at.Index == index { + return at + } + if at.End != nil { + at = at.End.Next + } else { + at = at.Next + } + } + return nil +} + +func (n *Node) paths(prefix string, paths *[]string, nodes *[]*Node) { + it := n.Next + for it != nil && it != n.End { + var path string + + if it.Key != nil { + quoted := string(it.Key) + unquoted, err := strconv.Unquote(quoted) + if err == nil && jsonpath.Identifier.MatchString(unquoted) { + path = prefix + "." + unquoted + } else { + path = prefix + "[" + quoted + "]" + } + } else if it.Index >= 0 { + path = prefix + "[" + strconv.Itoa(it.Index) + "]" + } + + *paths = append(*paths, path) + *nodes = append(*nodes, it) + + if it.HasChildren() { + it.paths(path, paths, nodes) + it = it.End.Next + } else { + it = it.Next + } + } +} + +func (n *Node) Children() ([]string, []*Node) { + if !n.HasChildren() { + return nil, nil + } + + var paths []string + var nodes []*Node + + var it *Node + if n.IsCollapsed() { + it = n.Collapsed + } else { + it = n.Next + } + + for it != nil && it != n.End { + if it.Key != nil { + key := string(it.Key) + unquoted, err := strconv.Unquote(key) + if err == nil { + key = unquoted + } + paths = append(paths, key) + nodes = append(nodes, it) + } + + if it.HasChildren() { + it = it.End.Next + } else { + it = it.Next + } + } + + return paths, nodes +} diff --git a/node_test.go b/internal/jsonx/node_test.go similarity index 58% rename from node_test.go rename to internal/jsonx/node_test.go index 9f37edd..9a49f35 100644 --- a/node_test.go +++ b/internal/jsonx/node_test.go @@ -1,4 +1,4 @@ -package main +package jsonx import ( "testing" @@ -8,28 +8,28 @@ import ( ) func TestNode_paths(t *testing.T) { - n, err := parse([]byte(`{"a": 1, "b": {"f": 2}, "c": [3, 4]}`)) + n, err := Parse([]byte(`{"a": 1, "b": {"f": 2}, "c": [3, 4]}`)) require.NoError(t, err) var paths []string - var nodes []*node + var nodes []*Node n.paths("", &paths, &nodes) assert.Equal(t, []string{".a", ".b", ".b.f", ".c", ".c[0]", ".c[1]"}, paths) } func TestNode_children(t *testing.T) { - n, err := parse([]byte(`{"a": 1, "b": {"f": 2}, "c": [3, 4]}`)) + n, err := Parse([]byte(`{"a": 1, "b": {"f": 2}, "c": [3, 4]}`)) require.NoError(t, err) - paths, _ := n.children() + paths, _ := n.Children() assert.Equal(t, []string{"a", "b", "c"}, paths) } func TestNode_expandRecursively(t *testing.T) { - n, err := parse([]byte(`{"a": {"b": {"c": 1}}}`)) + n, err := Parse([]byte(`{"a": {"b": {"c": 1}}}`)) require.NoError(t, err) - n.collapseRecursively() - n.expandRecursively(0, 3) - assert.Equal(t, `"c"`, string(n.next.next.next.key)) + n.CollapseRecursively() + n.ExpandRecursively(0, 3) + assert.Equal(t, `"c"`, string(n.Next.Next.Next.Key)) } diff --git a/ring.go b/internal/jsonx/ring.go similarity index 98% rename from ring.go rename to internal/jsonx/ring.go index 86f4b63..73f4f1a 100644 --- a/ring.go +++ b/internal/jsonx/ring.go @@ -1,4 +1,4 @@ -package main +package jsonx import ( "strings" diff --git a/internal/jsonx/string.go b/internal/jsonx/string.go new file mode 100644 index 0000000..b296011 --- /dev/null +++ b/internal/jsonx/string.go @@ -0,0 +1,61 @@ +package jsonx + +import ( + "strings" + + "github.com/antonmedv/fx/internal/theme" +) + +func (n *Node) String() string { + var out strings.Builder + + it := n + for it != nil { + if it.Key != nil { + out.Write(it.Key) + out.WriteByte(':') + } + if it.Value != nil { + out.Write(it.Value) + } + if it.Comma { + out.WriteByte(',') + } + if it.IsCollapsed() { + it = it.Collapsed + } else { + it = it.Next + } + } + + return out.String() +} + +func (n *Node) PrettyPrint() string { + var out strings.Builder + + it := n + for it != nil { + for ident := 0; ident < int(it.Depth); ident++ { + out.WriteString(" ") + } + if it.Key != nil { + out.Write(theme.CurrentTheme.Key(it.Key)) + out.Write(theme.Colon) + } + if it.Value != nil { + out.Write(theme.Value(it.Value, false, false)(it.Value)) + } + if it.Comma { + out.Write(theme.Comma) + } + out.WriteByte('\n') + if it.IsCollapsed() { + it = it.Collapsed + } else { + it = it.Next + } + } + + return out.String() +} diff --git a/wrap.go b/internal/jsonx/wrap.go similarity index 52% rename from wrap.go rename to internal/jsonx/wrap.go index 4b6ad8e..43c6690 100644 --- a/wrap.go +++ b/internal/jsonx/wrap.go @@ -1,4 +1,4 @@ -package main +package jsonx import ( "unicode/utf8" @@ -6,56 +6,56 @@ import ( "github.com/mattn/go-runewidth" ) -func dropWrapAll(n *node) { +func DropWrapAll(n *Node) { for n != nil { - if n.value != nil && n.value[0] == '"' { + if n.Value != nil && n.Value[0] == '"' { n.dropChunks() } - if n.isCollapsed() { - n = n.collapsed + if n.IsCollapsed() { + n = n.Collapsed } else { - n = n.next + n = n.Next } } } -func wrapAll(n *node, termWidth int) { +func WrapAll(n *Node, termWidth int) { if termWidth <= 0 { return } for n != nil { - if n.value != nil && n.value[0] == '"' { + if n.Value != nil && n.Value[0] == '"' { n.dropChunks() lines, count := doWrap(n, termWidth) if count > 1 { - n.chunk = lines[0] + n.Chunk = lines[0] for i := 1; i < count; i++ { - child := &node{ + child := &Node{ directParent: n, - depth: n.depth, - chunk: lines[i], + Depth: n.Depth, + Chunk: lines[i], } - if n.comma && i == count-1 { - child.comma = true + if n.Comma && i == count-1 { + child.Comma = true } n.insertChunk(child) } } } - if n.isCollapsed() { - n = n.collapsed + if n.IsCollapsed() { + n = n.Collapsed } else { - n = n.next + n = n.Next } } } -func doWrap(n *node, termWidth int) ([][]byte, int) { +func doWrap(n *Node, termWidth int) ([][]byte, int) { lines := make([][]byte, 0, 1) - width := int(n.depth) * 2 + width := int(n.Depth) * 2 - if n.key != nil { - for _, ch := range string(n.key) { + if n.Key != nil { + for _, ch := range string(n.Key) { width += runewidth.RuneWidth(ch) } width += 2 // for ": " @@ -63,15 +63,15 @@ func doWrap(n *node, termWidth int) ([][]byte, int) { linesCount := 0 start, end := 0, 0 - b := n.value + b := n.Value for len(b) > 0 { r, size := utf8.DecodeRune(b) w := runewidth.RuneWidth(r) if width+w > termWidth { - lines = append(lines, n.value[start:end]) + lines = append(lines, n.Value[start:end]) start = end - width = int(n.depth) * 2 + width = int(n.Depth) * 2 linesCount++ } width += w @@ -80,7 +80,7 @@ func doWrap(n *node, termWidth int) ([][]byte, int) { } if start < end { - lines = append(lines, n.value[start:]) + lines = append(lines, n.Value[start:]) linesCount++ } diff --git a/theme.go b/internal/theme/theme.go similarity index 81% rename from theme.go rename to internal/theme/theme.go index 9ec1567..8e2659f 100644 --- a/theme.go +++ b/internal/theme/theme.go @@ -1,4 +1,4 @@ -package main +package theme import ( "encoding/json" @@ -10,42 +10,44 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" + + "github.com/antonmedv/fx/internal/utils" ) -type theme struct { - Cursor color - Syntax color - Preview color - StatusBar color - Search color - Key color - String color - Null color - Boolean color - Number color - Size color +type Theme struct { + Cursor Color + Syntax Color + Preview Color + StatusBar Color + Search Color + Key Color + String Color + Null Color + Boolean Color + Number Color + Size Color } -type color func(s []byte) []byte +type Color func(s []byte) []byte -func valueStyle(b []byte, selected, chunk bool) color { +func Value(b []byte, selected, chunk bool) Color { if selected { - return currentTheme.Cursor + return CurrentTheme.Cursor } else if chunk { - return currentTheme.String + return CurrentTheme.String } else { switch b[0] { case '"': - return currentTheme.String + return CurrentTheme.String case 't', 'f': - return currentTheme.Boolean + return CurrentTheme.Boolean case 'n': - return currentTheme.Null + return CurrentTheme.Null case '{', '[', '}', ']': - return currentTheme.Syntax + return CurrentTheme.Syntax default: - if isDigit(b[0]) || b[0] == '-' { - return currentTheme.Number + if utils.IsDigit(b[0]) || b[0] == '-' { + return CurrentTheme.Number } return noColor } @@ -53,7 +55,7 @@ func valueStyle(b []byte, selected, chunk bool) color { } var ( - termOutput = termenv.NewOutput(os.Stderr) + TermOutput = termenv.NewOutput(os.Stderr) ) func init() { @@ -71,51 +73,51 @@ func init() { showSizesValue, ok := os.LookupEnv("FX_SHOW_SIZE") if ok { showSizesValue := strings.ToLower(showSizesValue) - showSizes = showSizesValue == "true" || showSizesValue == "yes" || showSizesValue == "on" || showSizesValue == "1" + ShowSizes = showSizesValue == "true" || showSizesValue == "yes" || showSizesValue == "on" || showSizesValue == "1" } - currentTheme, ok = themes[themeId] + CurrentTheme, ok = themes[themeId] if !ok { _, _ = fmt.Fprintf(os.Stderr, "fx: unknown theme %q, available themes: %v\n", themeId, themeNames) os.Exit(1) } - if termOutput.ColorProfile() == termenv.Ascii { - currentTheme = themes["0"] + if TermOutput.ColorProfile() == termenv.Ascii { + CurrentTheme = themes["0"] } - colon = currentTheme.Syntax([]byte{':', ' '}) - colonPreview = currentTheme.Preview([]byte{':'}) - comma = currentTheme.Syntax([]byte{','}) - empty = currentTheme.Preview([]byte{'~'}) - dot3 = currentTheme.Preview([]byte("…")) - closeCurlyBracket = currentTheme.Syntax([]byte{'}'}) - closeSquareBracket = currentTheme.Syntax([]byte{']'}) + Colon = CurrentTheme.Syntax([]byte{':', ' '}) + ColonPreview = CurrentTheme.Preview([]byte{':'}) + Comma = CurrentTheme.Syntax([]byte{','}) + Empty = CurrentTheme.Preview([]byte{'~'}) + Dot3 = CurrentTheme.Preview([]byte("…")) + CloseCurlyBracket = CurrentTheme.Syntax([]byte{'}'}) + CloseSquareBracket = CurrentTheme.Syntax([]byte{']'}) } var ( themeNames []string - currentTheme theme + CurrentTheme Theme defaultCursor = toColor(lipgloss.NewStyle().Reverse(true).Render) defaultPreview = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render) defaultStatusBar = toColor(lipgloss.NewStyle().Background(lipgloss.Color("7")).Foreground(lipgloss.Color("0")).Render) defaultSearch = toColor(lipgloss.NewStyle().Background(lipgloss.Color("11")).Foreground(lipgloss.Color("16")).Render) defaultNull = fg("243") defaultSize = toColor(lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render) - showSizes = false + ShowSizes = false ) var ( - colon []byte - colonPreview []byte - comma []byte - empty []byte - dot3 []byte - closeCurlyBracket []byte - closeSquareBracket []byte + Colon []byte + ColonPreview []byte + Comma []byte + Empty []byte + Dot3 []byte + CloseCurlyBracket []byte + CloseSquareBracket []byte ) -var themes = map[string]theme{ +var themes = map[string]Theme{ "0": { Cursor: defaultCursor, Syntax: noColor, @@ -268,21 +270,21 @@ func noColor(s []byte) []byte { return s } -func toColor(f func(s ...string) string) color { +func toColor(f func(s ...string) string) Color { return func(s []byte) []byte { return []byte(f(string(s))) } } -func fg(color string) color { +func fg(color string) Color { return toColor(lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render) } -func boldFg(color string) color { +func boldFg(color string) Color { return toColor(lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(color)).Render) } -func themeTester() { +func ThemeTester() { title := lipgloss.NewStyle().Bold(true) for _, name := range themeNames { t := themes[name] @@ -319,7 +321,7 @@ func themeTester() { } } -func exportThemes() { +func ExportThemes() { lipgloss.SetColorProfile(termenv.ANSI256) // Export in Terminal.app compatible colors placeholder := []byte{'_'} extract := func(b []byte) string { diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..dc6ab8a --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,9 @@ +package utils + +func IsHexDigit(ch byte) bool { + return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F') +} + +func IsDigit(ch byte) bool { + return ch >= '0' && ch <= '9' +} diff --git a/main.go b/main.go index 4268cc1..0783b71 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,8 @@ import ( "github.com/sahilm/fuzzy" "github.com/antonmedv/fx/internal/complete" + . "github.com/antonmedv/fx/internal/jsonx" + "github.com/antonmedv/fx/internal/theme" jsonpath "github.com/antonmedv/fx/path" ) @@ -71,10 +73,10 @@ func main() { fmt.Println(version) return case "--themes": - themeTester() + theme.ThemeTester() return case "--export-themes": - exportThemes() + theme.ExportThemes() return default: args = append(args, arg) @@ -143,7 +145,7 @@ func main() { } } - head, err := parse(data) + head, err := Parse(data) if err != nil { fmt.Print(err.Error()) os.Exit(1) @@ -176,7 +178,7 @@ func main() { search: newSearch(), } - lipgloss.SetColorProfile(termOutput.ColorProfile()) + lipgloss.SetColorProfile(theme.TermOutput.ColorProfile()) withMouse := tea.WithMouseCellMotion() if _, ok := os.LookupEnv("FX_NO_MOUSE"); ok { @@ -200,7 +202,7 @@ func main() { type model struct { termWidth, termHeight int - head, top *node + head, top *Node cursor int // cursor position [0, termHeight) showCursor bool wrap bool @@ -229,7 +231,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.help.Height = m.termHeight - 1 m.preview.Width = m.termWidth m.preview.Height = m.termHeight - 1 - wrapAll(m.top, m.termWidth) + WrapAll(m.top, m.termWidth) m.redoSearch() } @@ -257,18 +259,18 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.cursor == msg.Y { to := m.cursorPointsTo() if to != nil { - if to.isCollapsed() { - to.expand() + if to.IsCollapsed() { + to.Expand() } else { - to.collapse() + to.Collapse() } } } else { to := m.at(msg.Y) if to != nil { m.cursor = msg.Y - if to.isCollapsed() { - to.expand() + if to.IsCollapsed() { + to.Expand() } } } @@ -490,11 +492,11 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case key.Matches(msg, keyMap.NextSibling): pointsTo := m.cursorPointsTo() - var nextSibling *node - if pointsTo.end != nil && pointsTo.end.next != nil { - nextSibling = pointsTo.end.next + var nextSibling *Node + if pointsTo.End != nil && pointsTo.End.Next != nil { + nextSibling = pointsTo.End.Next } else { - nextSibling = pointsTo.next + nextSibling = pointsTo.Next } if nextSibling != nil { m.selectNode(nextSibling) @@ -502,13 +504,13 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case key.Matches(msg, keyMap.PrevSibling): pointsTo := m.cursorPointsTo() - var prevSibling *node - if pointsTo.parent() != nil && pointsTo.parent().end == pointsTo { - prevSibling = pointsTo.parent() - } else if pointsTo.prev != nil { - prevSibling = pointsTo.prev - parent := prevSibling.parent() - if parent != nil && parent.end == prevSibling { + var prevSibling *Node + if pointsTo.Parent() != nil && pointsTo.Parent().End == pointsTo { + prevSibling = pointsTo.Parent() + } else if pointsTo.Prev != nil { + prevSibling = pointsTo.Prev + parent := prevSibling.Parent() + if parent != nil && parent.End == prevSibling { prevSibling = parent } } @@ -518,41 +520,41 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case key.Matches(msg, keyMap.Collapse): n := m.cursorPointsTo() - if n.hasChildren() && !n.isCollapsed() { - n.collapse() + if n.HasChildren() && !n.IsCollapsed() { + n.Collapse() } else { - if n.parent() != nil { - n = n.parent() + if n.Parent() != nil { + n = n.Parent() } } m.selectNode(n) case key.Matches(msg, keyMap.Expand): - m.cursorPointsTo().expand() + m.cursorPointsTo().Expand() m.showCursor = true case key.Matches(msg, keyMap.CollapseRecursively): n := m.cursorPointsTo() - if n.hasChildren() { - n.collapseRecursively() + if n.HasChildren() { + n.CollapseRecursively() } m.showCursor = true case key.Matches(msg, keyMap.ExpandRecursively): n := m.cursorPointsTo() - if n.hasChildren() { - n.expandRecursively(0, math.MaxInt) + if n.HasChildren() { + n.ExpandRecursively(0, math.MaxInt) } m.showCursor = true case key.Matches(msg, keyMap.CollapseAll): n := m.top for n != nil { - n.collapseRecursively() - if n.end == nil { + n.CollapseRecursively() + if n.End == nil { n = nil } else { - n = n.end.next + n = n.End.Next } } m.cursor = 0 @@ -563,21 +565,21 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { at := m.cursorPointsTo() n := m.top for n != nil { - n.expandRecursively(0, math.MaxInt) - if n.end == nil { + n.ExpandRecursively(0, math.MaxInt) + if n.End == nil { n = nil } else { - n = n.end.next + n = n.End.Next } } m.selectNode(at) case key.Matches(msg, keyMap.CollapseLevel): at := m.cursorPointsTo() - if at != nil && at.hasChildren() { + if at != nil && at.HasChildren() { toLevel, _ := strconv.Atoi(msg.String()) - at.collapseRecursively() - at.expandRecursively(0, toLevel) + at.CollapseRecursively() + at.ExpandRecursively(0, toLevel) m.showCursor = true } @@ -585,12 +587,12 @@ func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { at := m.cursorPointsTo() m.wrap = !m.wrap if m.wrap { - wrapAll(m.top, m.termWidth) + WrapAll(m.top, m.termWidth) } else { - dropWrapAll(m.top) + DropWrapAll(m.top) } - if at.chunk != nil && at.value == nil { - at = at.parent() + if at.Chunk != nil && at.Value == nil { + at = at.Parent() } m.redoSearch() m.selectNode(at) @@ -632,8 +634,8 @@ func (m *model) up() { m.cursor-- if m.cursor < 0 { m.cursor = 0 - if m.head.prev != nil { - m.head = m.head.prev + if m.head.Prev != nil { + m.head = m.head.Prev } } } @@ -648,8 +650,8 @@ func (m *model) down() { } if m.cursor >= m.viewHeight() { m.cursor = m.viewHeight() - 1 - if m.head.next != nil { - m.head = m.head.next + if m.head.Next != nil { + m.head = m.head.Next } } } @@ -659,7 +661,7 @@ func (m *model) visibleLines() int { n := m.head for n != nil && visibleLines < m.viewHeight() { visibleLines++ - n = n.next + n = n.Next } return visibleLines } @@ -669,22 +671,22 @@ func (m *model) scrollIntoView() { if m.cursor >= visibleLines { m.cursor = visibleLines - 1 } - for visibleLines < m.viewHeight() && m.head.prev != nil { + for visibleLines < m.viewHeight() && m.head.Prev != nil { visibleLines++ m.cursor++ - m.head = m.head.prev + m.head = m.head.Prev } } func (m *model) View() string { if m.showHelp { statusBar := flex(m.termWidth, ": press q or ? to close help", "") - return m.help.View() + "\n" + string(currentTheme.StatusBar([]byte(statusBar))) + return m.help.View() + "\n" + string(theme.CurrentTheme.StatusBar([]byte(statusBar))) } if m.showPreview { statusBar := flex(m.termWidth, m.cursorPath(), m.fileName) - return m.preview.View() + "\n" + string(currentTheme.StatusBar([]byte(statusBar))) + return m.preview.View() + "\n" + string(theme.CurrentTheme.StatusBar([]byte(statusBar))) } var screen []byte @@ -695,7 +697,7 @@ func (m *model) View() string { if n == nil { break } - for ident := 0; ident < int(n.depth); ident++ { + for ident := 0; ident < int(n.Depth); ident++ { screen = append(screen, ' ', ' ') } @@ -704,47 +706,47 @@ func (m *model) View() string { isSelected = false // don't highlight the cursor while iterating search results } - if n.key != nil { + if n.Key != nil { screen = append(screen, m.prettyKey(n, isSelected)...) - screen = append(screen, colon...) + screen = append(screen, theme.Colon...) isSelected = false // don't highlight the key's value } screen = append(screen, m.prettyPrint(n, isSelected)...) - if n.isCollapsed() { - if n.value[0] == '{' { - if n.collapsed.key != nil { - screen = append(screen, currentTheme.Preview(n.collapsed.key)...) - screen = append(screen, colonPreview...) + if n.IsCollapsed() { + if n.Value[0] == '{' { + if n.Collapsed.Key != nil { + screen = append(screen, theme.CurrentTheme.Preview(n.Collapsed.Key)...) + screen = append(screen, theme.ColonPreview...) } - screen = append(screen, dot3...) - screen = append(screen, closeCurlyBracket...) - } else if n.value[0] == '[' { - screen = append(screen, dot3...) - screen = append(screen, closeSquareBracket...) + screen = append(screen, theme.Dot3...) + screen = append(screen, theme.CloseCurlyBracket...) + } else if n.Value[0] == '[' { + screen = append(screen, theme.Dot3...) + screen = append(screen, theme.CloseSquareBracket...) } - if n.end != nil && n.end.comma { - screen = append(screen, comma...) + if n.End != nil && n.End.Comma { + screen = append(screen, theme.Comma...) } } - if n.comma { - screen = append(screen, comma...) + if n.Comma { + screen = append(screen, theme.Comma...) } - if showSizes && len(n.value) > 0 && (n.value[0] == '{' || n.value[0] == '[') { - if n.isCollapsed() || n.size > 1 { - screen = append(screen, currentTheme.Size([]byte(fmt.Sprintf(" // %d", n.size)))...) + if theme.ShowSizes && len(n.Value) > 0 && (n.Value[0] == '{' || n.Value[0] == '[') { + if n.IsCollapsed() || n.Size > 1 { + screen = append(screen, theme.CurrentTheme.Size([]byte(fmt.Sprintf(" // %d", n.Size)))...) } } screen = append(screen, '\n') printedLines++ - n = n.next + n = n.Next } for i := printedLines; i < m.viewHeight(); i++ { - screen = append(screen, empty...) + screen = append(screen, theme.Empty...) screen = append(screen, '\n') } @@ -752,7 +754,7 @@ func (m *model) View() string { screen = append(screen, m.digInput.View()...) } else { statusBar := flex(m.termWidth, m.cursorPath(), m.fileName) - screen = append(screen, currentTheme.StatusBar([]byte(statusBar))...) + screen = append(screen, theme.CurrentTheme.StatusBar([]byte(statusBar))...) } if m.yank { @@ -781,12 +783,12 @@ func (m *model) View() string { return string(screen) } -func (m *model) prettyKey(node *node, selected bool) []byte { - b := node.key +func (m *model) prettyKey(node *Node, selected bool) []byte { + b := node.Key - style := currentTheme.Key + style := theme.CurrentTheme.Key if selected { - style = currentTheme.Cursor + style = theme.CurrentTheme.Cursor } if indexes, ok := m.search.keys[node]; ok { @@ -795,9 +797,9 @@ func (m *model) prettyKey(node *node, selected bool) []byte { if i%2 == 0 { out = append(out, style(p.b)...) } else if p.index == m.search.cursor { - out = append(out, currentTheme.Cursor(p.b)...) + out = append(out, theme.CurrentTheme.Cursor(p.b)...) } else { - out = append(out, currentTheme.Search(p.b)...) + out = append(out, theme.CurrentTheme.Search(p.b)...) } } return out @@ -806,19 +808,19 @@ func (m *model) prettyKey(node *node, selected bool) []byte { } } -func (m *model) prettyPrint(node *node, selected bool) []byte { +func (m *model) prettyPrint(node *Node, selected bool) []byte { var b []byte - if node.chunk != nil { - b = node.chunk + if node.Chunk != nil { + b = node.Chunk } else { - b = node.value + b = node.Value } if len(b) == 0 { return b } - style := valueStyle(b, selected, node.chunk != nil) + style := theme.Value(b, selected, node.Chunk != nil) if indexes, ok := m.search.values[node]; ok { var out []byte @@ -826,9 +828,9 @@ func (m *model) prettyPrint(node *node, selected bool) []byte { if i%2 == 0 { out = append(out, style(p.b)...) } else if p.index == m.search.cursor { - out = append(out, currentTheme.Cursor(p.b)...) + out = append(out, theme.CurrentTheme.Cursor(p.b)...) } else { - out = append(out, currentTheme.Search(p.b)...) + out = append(out, theme.CurrentTheme.Search(p.b)...) } } return out @@ -847,34 +849,34 @@ func (m *model) viewHeight() int { return m.termHeight - 1 } -func (m *model) cursorPointsTo() *node { +func (m *model) cursorPointsTo() *Node { return m.at(m.cursor) } -func (m *model) at(pos int) *node { +func (m *model) at(pos int) *Node { head := m.head for i := 0; i < pos; i++ { if head == nil { break } - head = head.next + head = head.Next } return head } -func (m *model) findBottom() *node { +func (m *model) findBottom() *Node { n := m.head - for n.next != nil { - if n.end != nil { - n = n.end + for n.Next != nil { + if n.End != nil { + n = n.End } else { - n = n.next + n = n.Next } } return n } -func (m *model) nodeInsideView(n *node) bool { +func (m *model) nodeInsideView(n *Node) bool { if n == nil { return false } @@ -886,12 +888,12 @@ func (m *model) nodeInsideView(n *node) bool { if head == n { return true } - head = head.next + head = head.Next } return false } -func (m *model) selectNodeInView(n *node) { +func (m *model) selectNodeInView(n *Node) { head := m.head for i := 0; i < m.viewHeight(); i++ { if head == nil { @@ -901,11 +903,11 @@ func (m *model) selectNodeInView(n *node) { m.cursor = i return } - head = head.next + head = head.Next } } -func (m *model) selectNode(n *node) { +func (m *model) selectNode(n *Node) { m.showCursor = true if m.nodeInsideView(n) { m.selectNodeInView(n) @@ -915,10 +917,10 @@ func (m *model) selectNode(n *node) { m.head = n m.scrollIntoView() } - parent := n.parent() + parent := n.Parent() for parent != nil { - parent.expand() - parent = parent.parent() + parent.Expand() + parent = parent.Parent() } } @@ -926,23 +928,23 @@ func (m *model) cursorPath() string { path := "" at := m.cursorPointsTo() for at != nil { - if at.prev != nil { - if at.chunk != nil && at.value == nil { - at = at.parent() + if at.Prev != nil { + if at.Chunk != nil && at.Value == nil { + at = at.Parent() } - if at.key != nil { - quoted := string(at.key) + if at.Key != nil { + quoted := string(at.Key) unquoted, err := strconv.Unquote(quoted) if err == nil && jsonpath.Identifier.MatchString(unquoted) { path = "." + unquoted + path } else { path = "[" + quoted + "]" + path } - } else if at.index >= 0 { - path = "[" + strconv.Itoa(at.index) + "]" + path + } else if at.Index >= 0 { + path = "[" + strconv.Itoa(at.Index) + "]" + path } } - at = at.parent() + at = at.Parent() } return path } @@ -952,55 +954,55 @@ func (m *model) cursorValue() string { if at == nil { return "" } - parent := at.parent() + parent := at.Parent() if parent != nil { // wrapped string part - if at.chunk != nil && at.value == nil { + if at.Chunk != nil && at.Value == nil { at = parent } - if len(at.value) == 1 && at.value[0] == '}' || at.value[0] == ']' { + if len(at.Value) == 1 && at.Value[0] == '}' || at.Value[0] == ']' { at = parent } } - if len(at.value) > 0 && at.value[0] == '"' { - str, err := strconv.Unquote(string(at.value)) + if len(at.Value) > 0 && at.Value[0] == '"' { + str, err := strconv.Unquote(string(at.Value)) if err == nil { return str } - return string(at.value) + return string(at.Value) } var out strings.Builder - out.Write(at.value) + out.Write(at.Value) out.WriteString("\n") - if at.hasChildren() { - it := at.next - if at.isCollapsed() { - it = at.collapsed + if at.HasChildren() { + it := at.Next + if at.IsCollapsed() { + it = at.Collapsed } for it != nil { - out.WriteString(strings.Repeat(" ", int(it.depth-at.depth))) - if it.key != nil { - out.Write(it.key) + out.WriteString(strings.Repeat(" ", int(it.Depth-at.Depth))) + if it.Key != nil { + out.Write(it.Key) out.WriteString(": ") } - if it.value != nil { - out.Write(it.value) + if it.Value != nil { + out.Write(it.Value) } - if it == at.end { + if it == at.End { break } - if it.comma { + if it.Comma { out.WriteString(",") } out.WriteString("\n") - if it.chunkEnd != nil { - it = it.chunkEnd.next - } else if it.isCollapsed() { - it = it.collapsed + if it.ChunkEnd != nil { + it = it.ChunkEnd.Next + } else if it.IsCollapsed() { + it = it.Collapsed } else { - it = it.next + it = it.Next } } } @@ -1012,16 +1014,16 @@ func (m *model) cursorKey() string { if at == nil { return "" } - if at.key != nil { + if at.Key != nil { var v string - _ = json.Unmarshal(at.key, &v) + _ = json.Unmarshal(at.Key, &v) return v } - return strconv.Itoa(at.index) + return strconv.Itoa(at.Index) } -func (m *model) selectByPath(path []any) *node { +func (m *model) selectByPath(path []any) *Node { n := m.currentTopNode() for _, part := range path { if n == nil { @@ -1029,21 +1031,21 @@ func (m *model) selectByPath(path []any) *node { } switch part := part.(type) { case string: - n = n.findChildByKey(part) + n = n.FindChildByKey(part) case int: - n = n.findChildByIndex(part) + n = n.FindChildByIndex(part) } } return n } -func (m *model) currentTopNode() *node { +func (m *model) currentTopNode() *Node { at := m.cursorPointsTo() if at == nil { return nil } - for at.parent() != nil { - at = at.parent() + for at.Parent() != nil { + at = at.Parent() } return at } @@ -1069,8 +1071,8 @@ func (m *model) doSearch(s string) { n := m.top searchIndex := 0 for n != nil { - if n.key != nil { - indexes := re.FindAllIndex(n.key, -1) + if n.Key != nil { + indexes := re.FindAllIndex(n.Key, -1) if len(indexes) > 0 { for i, pair := range indexes { m.search.results = append(m.search.results, n) @@ -1079,24 +1081,24 @@ func (m *model) doSearch(s string) { searchIndex += len(indexes) } } - indexes := re.FindAllIndex(n.value, -1) + indexes := re.FindAllIndex(n.Value, -1) if len(indexes) > 0 { for range indexes { m.search.results = append(m.search.results, n) } - if n.chunk != nil { + if n.Chunk != nil { // String can be split into chunks, so we need to map the indexes to the chunks. - chunks := [][]byte{n.chunk} - chunkNodes := []*node{n} + chunks := [][]byte{n.Chunk} + chunkNodes := []*Node{n} - it := n.next + it := n.Next for it != nil { chunkNodes = append(chunkNodes, it) - chunks = append(chunks, it.chunk) - if it == n.chunkEnd { + chunks = append(chunks, it.Chunk) + if it == n.ChunkEnd { break } - it = it.next + it = it.Next } chunkMatches := splitIndexesToChunks(chunks, indexes, searchIndex) @@ -1111,10 +1113,10 @@ func (m *model) doSearch(s string) { searchIndex += len(indexes) } - if n.isCollapsed() { - n = n.collapsed + if n.IsCollapsed() { + n = n.Collapsed } else { - n = n.next + n = n.Next } } @@ -1145,7 +1147,7 @@ func (m *model) redoSearch() { } } -func (m *model) dig(v string) *node { +func (m *model) dig(v string) *Node { p, ok := jsonpath.Split(v) if !ok { return nil @@ -1167,7 +1169,7 @@ func (m *model) dig(v string) *node { return nil } - keys, nodes := at.children() + keys, nodes := at.Children() matches := fuzzy.Find(searchTerm, keys) if len(matches) == 0 { diff --git a/main_test.go b/main_test.go index 24e8049..f74f0ca 100644 --- a/main_test.go +++ b/main_test.go @@ -13,6 +13,9 @@ import ( "github.com/charmbracelet/x/exp/teatest" "github.com/muesli/termenv" "github.com/stretchr/testify/require" + + "github.com/antonmedv/fx/internal/jsonx" + "github.com/antonmedv/fx/internal/theme" ) func init() { @@ -26,7 +29,7 @@ func prepare(t *testing.T) *teatest.TestModel { json, err := io.ReadAll(file) require.NoError(t, err) - head, err := parse(json) + head, err := jsonx.Parse(json) require.NoError(t, err) m := &model{ @@ -103,8 +106,8 @@ func TestCollapseRecursive(t *testing.T) { } func TestCollapseRecursiveWithSizes(t *testing.T) { - showSizes = true - defer func() { showSizes = true }() + theme.ShowSizes = true + defer func() { theme.ShowSizes = true }() tm := prepare(t) diff --git a/node.go b/node.go deleted file mode 100644 index e96043e..0000000 --- a/node.go +++ /dev/null @@ -1,259 +0,0 @@ -package main - -import ( - "strconv" - - jsonpath "github.com/antonmedv/fx/path" -) - -type node struct { - prev, next, end *node - directParent *node - indirectParent *node - collapsed *node - depth uint8 - key []byte - value []byte - size int - chunk []byte - chunkEnd *node - comma bool - index 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 -} - -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 = nil - - 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) parent() *node { - if n.directParent == nil { - return nil - } - parent := n.directParent - if parent.indirectParent != nil { - parent = parent.indirectParent - } - return parent -} - -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 { - it = it.next - } - } -} - -func (n *node) findChildByKey(key string) *node { - it := n.next - for it != nil && it != n.end { - if it.key != nil { - k, err := strconv.Unquote(string(it.key)) - if err != nil { - return nil - } - if k == key { - return it - } - } - if it.chunkEnd != nil { - it = it.chunkEnd.next - } else if it.end != nil { - it = it.end.next - } else { - it = it.next - } - } - return nil -} - -func (n *node) findChildByIndex(index int) *node { - for at := n.next; at != nil && at != n.end; { - if at.index == index { - return at - } - if at.end != nil { - at = at.end.next - } else { - at = at.next - } - } - return nil -} - -func (n *node) paths(prefix string, paths *[]string, nodes *[]*node) { - it := n.next - for it != nil && it != n.end { - var path string - - if it.key != nil { - quoted := string(it.key) - unquoted, err := strconv.Unquote(quoted) - if err == nil && jsonpath.Identifier.MatchString(unquoted) { - path = prefix + "." + unquoted - } else { - path = prefix + "[" + quoted + "]" - } - } else if it.index >= 0 { - path = prefix + "[" + strconv.Itoa(it.index) + "]" - } - - *paths = append(*paths, path) - *nodes = append(*nodes, it) - - if it.hasChildren() { - it.paths(path, paths, nodes) - it = it.end.next - } else { - it = it.next - } - } -} - -func (n *node) children() ([]string, []*node) { - if !n.hasChildren() { - return nil, nil - } - - var paths []string - var nodes []*node - - var it *node - if n.isCollapsed() { - it = n.collapsed - } else { - it = n.next - } - - for it != nil && it != n.end { - if it.key != nil { - key := string(it.key) - unquoted, err := strconv.Unquote(key) - if err == nil { - key = unquoted - } - paths = append(paths, key) - nodes = append(nodes, it) - } - - if it.hasChildren() { - it = it.end.next - } else { - it = it.next - } - } - - return paths, nodes -} diff --git a/reduce.go b/reduce.go index b428ad6..0a5b4d2 100644 --- a/reduce.go +++ b/reduce.go @@ -6,32 +6,35 @@ import ( "os" "os/exec" "path" + + "github.com/antonmedv/fx/internal/engine" ) //go:embed npm/index.js var src []byte func reduce(fns []string) { - script := path.Join(os.TempDir(), fmt.Sprintf("fx-%v.js", version)) - _, err := os.Stat(script) - if os.IsNotExist(err) { - err := os.WriteFile(script, src, 0644) - if err != nil { - panic(err) - } - } + var deno bool - deno := false bin, err := exec.LookPath("node") if err != nil { bin, err = exec.LookPath("deno") if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "Node.js or Deno is required to run fx with reducers.\n") - os.Exit(1) + engine.Reduce(fns) + return } deno = true } + script := path.Join(os.TempDir(), fmt.Sprintf("fx-%v.js", version)) + _, err = os.Stat(script) + if os.IsNotExist(err) { + err := os.WriteFile(script, src, 0644) + if err != nil { + panic(err) + } + } + env := os.Environ() var args []string diff --git a/search.go b/search.go index ac0e69d..fb10c8b 100644 --- a/search.go +++ b/search.go @@ -1,18 +1,22 @@ package main +import ( + . "github.com/antonmedv/fx/internal/jsonx" +) + type search struct { err error - results []*node + results []*Node cursor int - values map[*node][]match - keys map[*node][]match + values map[*Node][]match + keys map[*Node][]match } func newSearch() *search { return &search{ - results: make([]*node, 0), - values: make(map[*node][]match), - keys: make(map[*node][]match), + results: make([]*Node, 0), + values: make(map[*Node][]match), + keys: make(map[*Node][]match), } } diff --git a/utils.go b/utils.go index af354a1..c813319 100644 --- a/utils.go +++ b/utils.go @@ -4,21 +4,6 @@ import ( "strings" ) -func isHexDigit(ch byte) bool { - return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F') -} - -func isDigit(ch byte) bool { - return ch >= '0' && ch <= '9' -} - -func max(i, j int) int { - if i > j { - return i - } - return j -} - func regexCase(code string) (string, bool) { if strings.HasSuffix(code, "/i") { return code[:len(code)-2], true