From 58b646c3f43873ef8736de8db25d538e307e2b89 Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Wed, 20 Mar 2024 21:12:39 +0100 Subject: [PATCH 1/6] Add goja --- go.mod | 4 +- go.sum | 6 +- internal/complete/complete.go | 22 ++-- internal/complete/prelude.js | 10 ++ internal/engine/engine.go | 108 ++++++++++++++++++ internal/engine/prelude.js | 9 ++ .../{complete/prelude.go => engine/stdlib.js} | 32 ++---- .../transpile.go => engine/transform.go} | 2 +- internal/engine/utils.go | 36 ++++++ reduce.go | 10 +- 10 files changed, 200 insertions(+), 39 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 diff --git a/go.mod b/go.mod index 6b40041cb..5a0fe8fbc 100644 --- a/go.mod +++ b/go.mod @@ -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 c25296c87..aeec72a68 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= @@ -40,8 +41,9 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5Nq 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/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= diff --git a/internal/complete/complete.go b/internal/complete/complete.go index 9af8ed969..2e6e128b8 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 000000000..bcfdb528b --- /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 000000000..3881aeedb --- /dev/null +++ b/internal/engine/engine.go @@ -0,0 +1,108 @@ +package engine + +import ( + _ "embed" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/dop251/goja" + "github.com/goccy/go-yaml" +) + +//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 { + + } + + 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 + } + + fmt.Println(output) +} diff --git a/internal/engine/prelude.js b/internal/engine/prelude.js new file mode 100644 index 000000000..b1fb47e4d --- /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 a51adacc2..1a95e0e20 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 b82780b8a..9c7457fce 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 000000000..eec60742e --- /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/reduce.go b/reduce.go index b428ad682..5321fdcda 100644 --- a/reduce.go +++ b/reduce.go @@ -6,6 +6,8 @@ import ( "os" "os/exec" "path" + + "github.com/antonmedv/fx/internal/engine" ) //go:embed npm/index.js @@ -22,12 +24,12 @@ func reduce(fns []string) { } deno := false - bin, err := exec.LookPath("node") + bin, err := exec.LookPath("node1") if err != nil { - bin, err = exec.LookPath("deno") + bin, err = exec.LookPath("deno1") 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 } From 1bb28031864ab73ee898150988fb0b91b0a41c97 Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Wed, 20 Mar 2024 21:31:53 +0100 Subject: [PATCH 2/6] Separate jsonx package --- go.mod | 2 +- go.sum | 7 + json.go => internal/jsonx/json.go | 86 +++--- internal/jsonx/node.go | 259 ++++++++++++++++++ node_test.go => internal/jsonx/node_test.go | 18 +- ring.go => internal/jsonx/ring.go | 2 +- internal/jsonx/utils.go | 9 + wrap.go => internal/jsonx/wrap.go | 50 ++-- main.go | 279 ++++++++++---------- main_test.go | 4 +- node.go | 259 ------------------ search.go | 16 +- utils.go | 11 - 13 files changed, 507 insertions(+), 495 deletions(-) rename json.go => internal/jsonx/json.go (82%) 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/utils.go rename wrap.go => internal/jsonx/wrap.go (52%) delete mode 100644 node.go diff --git a/go.mod b/go.mod index 5a0fe8fbc..6993f3e56 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 diff --git a/go.sum b/go.sum index aeec72a68..a00fd6a19 100644 --- a/go.sum +++ b/go.sum @@ -34,13 +34,17 @@ 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/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= @@ -54,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= @@ -91,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/json.go b/internal/jsonx/json.go similarity index 82% rename from json.go rename to internal/jsonx/json.go index 248bad6a0..c0357cdb4 100644 --- a/json.go +++ b/internal/jsonx/json.go @@ -1,4 +1,4 @@ -package main +package jsonx import ( "fmt" @@ -17,7 +17,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 +29,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 +57,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 +84,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 @@ -122,13 +122,13 @@ 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 @@ -173,20 +173,20 @@ func (p *jsonParser) parseNumber() *node { } } - 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 +199,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 +216,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 +253,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 +270,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 +302,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 +313,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 000000000..0d0a881b2 --- /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 9f37edd1b..9a49f3586 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 86f4b6399..73f4f1aa0 100644 --- a/ring.go +++ b/internal/jsonx/ring.go @@ -1,4 +1,4 @@ -package main +package jsonx import ( "strings" diff --git a/internal/jsonx/utils.go b/internal/jsonx/utils.go new file mode 100644 index 000000000..442895fa4 --- /dev/null +++ b/internal/jsonx/utils.go @@ -0,0 +1,9 @@ +package jsonx + +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/wrap.go b/internal/jsonx/wrap.go similarity index 52% rename from wrap.go rename to internal/jsonx/wrap.go index 4b6ad8e73..43c6690c6 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/main.go b/main.go index 4268cc1bb..4ce122985 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ import ( "github.com/sahilm/fuzzy" "github.com/antonmedv/fx/internal/complete" + . "github.com/antonmedv/fx/internal/jsonx" jsonpath "github.com/antonmedv/fx/path" ) @@ -143,7 +144,7 @@ func main() { } } - head, err := parse(data) + head, err := Parse(data) if err != nil { fmt.Print(err.Error()) os.Exit(1) @@ -200,7 +201,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 +230,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 +258,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 +491,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 +503,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 +519,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 +564,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 +586,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 +633,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 +649,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 +660,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,10 +670,10 @@ 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 } } @@ -695,7 +696,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,7 +705,7 @@ 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...) isSelected = false // don't highlight the key's value @@ -712,35 +713,35 @@ func (m *model) View() string { 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)...) + if n.IsCollapsed() { + if n.Value[0] == '{' { + if n.Collapsed.Key != nil { + screen = append(screen, currentTheme.Preview(n.Collapsed.Key)...) screen = append(screen, colonPreview...) } screen = append(screen, dot3...) screen = append(screen, closeCurlyBracket...) - } else if n.value[0] == '[' { + } else if n.Value[0] == '[' { screen = append(screen, dot3...) screen = append(screen, closeSquareBracket...) } - if n.end != nil && n.end.comma { + if n.End != nil && n.End.Comma { screen = append(screen, comma...) } } - if n.comma { + if n.Comma { screen = append(screen, 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 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)))...) } } screen = append(screen, '\n') printedLines++ - n = n.next + n = n.Next } for i := printedLines; i < m.viewHeight(); i++ { @@ -781,8 +782,8 @@ 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 if selected { @@ -806,19 +807,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 := valueStyle(b, selected, node.Chunk != nil) if indexes, ok := m.search.values[node]; ok { var out []byte @@ -847,34 +848,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 +887,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 +902,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 +916,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 +927,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 +953,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 +1013,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 +1030,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 +1070,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 +1080,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 +1112,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 +1146,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 +1168,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 24e8049ec..952cd28e9 100644 --- a/main_test.go +++ b/main_test.go @@ -13,6 +13,8 @@ import ( "github.com/charmbracelet/x/exp/teatest" "github.com/muesli/termenv" "github.com/stretchr/testify/require" + + "github.com/antonmedv/fx/internal/jsonx" ) func init() { @@ -26,7 +28,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{ diff --git a/node.go b/node.go deleted file mode 100644 index e96043e55..000000000 --- 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/search.go b/search.go index ac0e69d98..fb10c8b59 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 af354a1b0..c79a09929 100644 --- a/utils.go +++ b/utils.go @@ -4,21 +4,10 @@ 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 From 8ca74b870da0814161f53419f8e0c90ad9e29400 Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Wed, 20 Mar 2024 21:41:40 +0100 Subject: [PATCH 3/6] Parse in engine --- internal/engine/engine.go | 9 ++++++++- internal/jsonx/string.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 internal/jsonx/string.go diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 3881aeedb..de841ee52 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -10,6 +10,8 @@ import ( "github.com/dop251/goja" "github.com/goccy/go-yaml" + + "github.com/antonmedv/fx/internal/jsonx" ) //go:embed stdlib.js @@ -74,7 +76,12 @@ func Reduce(args []string) { 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 diff --git a/internal/jsonx/string.go b/internal/jsonx/string.go new file mode 100644 index 000000000..87136823b --- /dev/null +++ b/internal/jsonx/string.go @@ -0,0 +1,30 @@ +package jsonx + +import ( + "strings" +) + +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() +} From 6f6c9e3585e4c92669d7812a8f82f9e5c77b6377 Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Wed, 20 Mar 2024 22:03:04 +0100 Subject: [PATCH 4/6] Separate theme pkg --- internal/engine/engine.go | 13 +++- internal/jsonx/json.go | 16 +++-- internal/jsonx/string.go | 31 +++++++++ theme.go => internal/theme/theme.go | 102 ++++++++++++++-------------- internal/{jsonx => utils}/utils.go | 6 +- main.go | 51 +++++++------- main_test.go | 5 +- utils.go | 4 -- 8 files changed, 136 insertions(+), 92 deletions(-) rename theme.go => internal/theme/theme.go (81%) rename internal/{jsonx => utils}/utils.go (62%) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index de841ee52..60e4b2f44 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -111,5 +111,16 @@ func Reduce(args []string) { return } - fmt.Println(output) + 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/jsonx/json.go b/internal/jsonx/json.go index c0357cdb4..721c137dd 100644 --- a/internal/jsonx/json.go +++ b/internal/jsonx/json.go @@ -5,6 +5,8 @@ import ( "strconv" "strings" "unicode/utf8" + + "github.com/antonmedv/fx/internal/utils" ) type jsonParser struct { @@ -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) @@ -134,7 +136,7 @@ func (p *jsonParser) parseNumber() *Node { // 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,10 +167,10 @@ 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() } } diff --git a/internal/jsonx/string.go b/internal/jsonx/string.go index 87136823b..b29601151 100644 --- a/internal/jsonx/string.go +++ b/internal/jsonx/string.go @@ -2,6 +2,8 @@ package jsonx import ( "strings" + + "github.com/antonmedv/fx/internal/theme" ) func (n *Node) String() string { @@ -28,3 +30,32 @@ func (n *Node) String() string { 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/theme.go b/internal/theme/theme.go similarity index 81% rename from theme.go rename to internal/theme/theme.go index 9ec156720..8e2659f4c 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/jsonx/utils.go b/internal/utils/utils.go similarity index 62% rename from internal/jsonx/utils.go rename to internal/utils/utils.go index 442895fa4..dc6ab8af9 100644 --- a/internal/jsonx/utils.go +++ b/internal/utils/utils.go @@ -1,9 +1,9 @@ -package jsonx +package utils -func isHexDigit(ch byte) bool { +func IsHexDigit(ch byte) bool { return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F') } -func isDigit(ch byte) bool { +func IsDigit(ch byte) bool { return ch >= '0' && ch <= '9' } diff --git a/main.go b/main.go index 4ce122985..0783b7172 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ import ( "github.com/antonmedv/fx/internal/complete" . "github.com/antonmedv/fx/internal/jsonx" + "github.com/antonmedv/fx/internal/theme" jsonpath "github.com/antonmedv/fx/path" ) @@ -72,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) @@ -177,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 { @@ -680,12 +681,12 @@ func (m *model) scrollIntoView() { 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 @@ -707,7 +708,7 @@ func (m *model) View() string { 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 } @@ -716,26 +717,26 @@ func (m *model) View() string { if n.IsCollapsed() { if n.Value[0] == '{' { if n.Collapsed.Key != nil { - screen = append(screen, currentTheme.Preview(n.Collapsed.Key)...) - screen = append(screen, colonPreview...) + screen = append(screen, theme.CurrentTheme.Preview(n.Collapsed.Key)...) + screen = append(screen, theme.ColonPreview...) } - screen = append(screen, dot3...) - screen = append(screen, closeCurlyBracket...) + screen = append(screen, theme.Dot3...) + screen = append(screen, theme.CloseCurlyBracket...) } else if n.Value[0] == '[' { - screen = append(screen, dot3...) - screen = append(screen, closeSquareBracket...) + screen = append(screen, theme.Dot3...) + screen = append(screen, theme.CloseSquareBracket...) } if n.End != nil && n.End.Comma { - screen = append(screen, comma...) + screen = append(screen, theme.Comma...) } } if n.Comma { - screen = append(screen, comma...) + screen = append(screen, theme.Comma...) } - if showSizes && len(n.Value) > 0 && (n.Value[0] == '{' || n.Value[0] == '[') { + if theme.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)))...) + screen = append(screen, theme.CurrentTheme.Size([]byte(fmt.Sprintf(" // %d", n.Size)))...) } } @@ -745,7 +746,7 @@ func (m *model) View() string { } for i := printedLines; i < m.viewHeight(); i++ { - screen = append(screen, empty...) + screen = append(screen, theme.Empty...) screen = append(screen, '\n') } @@ -753,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 { @@ -785,9 +786,9 @@ func (m *model) View() string { 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 { @@ -796,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 @@ -819,7 +820,7 @@ func (m *model) prettyPrint(node *Node, selected bool) []byte { 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 @@ -827,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 diff --git a/main_test.go b/main_test.go index 952cd28e9..de3bcdae4 100644 --- a/main_test.go +++ b/main_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/antonmedv/fx/internal/jsonx" + "github.com/antonmedv/fx/internal/theme" ) func init() { @@ -105,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/utils.go b/utils.go index c79a09929..c813319ed 100644 --- a/utils.go +++ b/utils.go @@ -4,10 +4,6 @@ import ( "strings" ) -func isDigit(ch byte) bool { - return ch >= '0' && ch <= '9' -} - func regexCase(code string) (string, bool) { if strings.HasSuffix(code, "/i") { return code[:len(code)-2], true From 2d20f07c68a5a3e0baf8918c16f145ad6138f6e8 Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Wed, 20 Mar 2024 22:11:39 +0100 Subject: [PATCH 5/6] Refactor reduce.go --- reduce.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/reduce.go b/reduce.go index 5321fdcda..0a5b4d29f 100644 --- a/reduce.go +++ b/reduce.go @@ -14,19 +14,11 @@ import ( 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("node1") + bin, err := exec.LookPath("node") if err != nil { - bin, err = exec.LookPath("deno1") + bin, err = exec.LookPath("deno") if err != nil { engine.Reduce(fns) return @@ -34,6 +26,15 @@ func reduce(fns []string) { 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 From 6a3608265df48bd107bfbba312e6ef87036f956a Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Thu, 21 Mar 2024 20:38:50 +0100 Subject: [PATCH 6/6] Fix tests --- .github/workflows/snap.yml | 2 +- .github/workflows/test.yml | 2 +- main_test.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 47b35418d..a7321e368 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 c41ab1318..69ce36144 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/main_test.go b/main_test.go index de3bcdae4..f74f0ca4d 100644 --- a/main_test.go +++ b/main_test.go @@ -106,8 +106,8 @@ func TestCollapseRecursive(t *testing.T) { } func TestCollapseRecursiveWithSizes(t *testing.T) { - theme.showSizes = true - defer func() { theme.showSizes = true }() + theme.ShowSizes = true + defer func() { theme.ShowSizes = true }() tm := prepare(t)