Add new json parser

pull/268/head
Anton Medvedev 9 months ago
parent ce2e3a4bad
commit 260e7cc444
No known key found for this signature in database

@ -0,0 +1,26 @@
module github.com/antonmedv/fx/new
go 1.20
require (
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.24.2
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.3.8 // indirect
)

@ -0,0 +1,37 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=

@ -0,0 +1,336 @@
package main
import (
"fmt"
"strconv"
)
type jsonParser struct {
data []byte
end int
lastChar byte
lineNumber uint
sourceTail *ring
depth uint8
skipFirstIdent bool
}
func parse(data []byte) (line *node, err error) {
p := &jsonParser{
data: data,
lineNumber: 1,
sourceTail: &ring{},
}
defer func() {
if r := recover(); r != nil {
err = p.errorSnippet(fmt.Sprintf("%v", r))
}
}()
p.next()
line = p.parseValue()
if p.lastChar != 0 {
panic(fmt.Sprintf("Unexpected character %q after root node", p.lastChar))
}
return
}
func (p *jsonParser) next() {
if p.end < len(p.data) {
p.lastChar = p.data[p.end]
p.end++
} else {
p.lastChar = 0
}
p.sourceTail.writeByte(p.lastChar)
}
func (p *jsonParser) parseValue() *node {
p.skipWhitespace()
var l *node
switch p.lastChar {
case '"':
l = p.parseString()
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-':
l = p.parseNumber()
case '{':
l = p.parseObject()
case '[':
l = p.parseArray()
case 't':
l = p.parseKeyword("true")
case 'f':
l = p.parseKeyword("false")
case 'n':
l = p.parseKeyword("null")
default:
panic(fmt.Sprintf("Unexpected character %q", p.lastChar))
}
p.skipWhitespace()
return l
}
func (p *jsonParser) parseString() *node {
str := &node{depth: p.depth}
start := p.end - 1
p.next()
escaped := false
for {
if escaped {
switch p.lastChar {
case 'u':
var unicode string
for i := 0; i < 4; i++ {
p.next()
if !isHexDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s%c'", unicode, p.lastChar))
}
unicode += string(p.lastChar)
}
_, err := strconv.ParseInt(unicode, 16, 32)
if err != nil {
panic(fmt.Sprintf("Invalid Unicode escape sequence '\\u%s'", unicode))
}
case '"', '\\', '/', 'b', 'f', 'n', 'r', 't':
default:
panic(fmt.Sprintf("Invalid escape sequence '\\%c'", p.lastChar))
}
escaped = false
} else if p.lastChar == '\\' {
escaped = true
} else if p.lastChar == '"' {
break
} else if p.lastChar == 0 {
panic("Unexpected end of input in string")
} else if p.lastChar < 0x1F {
panic(fmt.Sprintf("Invalid character %q in string", p.lastChar))
}
p.next()
}
str.value = p.data[start:p.end]
p.next()
return str
}
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) {
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
}
}
// Leading zero
if p.lastChar == '0' {
p.next()
} else {
for isDigit(p.lastChar) {
p.next()
}
}
// Decimal portion
if p.lastChar == '.' {
p.next()
if !isDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
}
for isDigit(p.lastChar) {
p.next()
}
}
// Exponent
if p.lastChar == 'e' || p.lastChar == 'E' {
p.next()
if p.lastChar == '+' || p.lastChar == '-' {
p.next()
}
if !isDigit(p.lastChar) {
panic(fmt.Sprintf("Invalid character %q in number", p.lastChar))
}
for isDigit(p.lastChar) {
p.next()
}
}
num.value = p.data[start : p.end-1]
return num
}
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, '}')
p.next()
return object
}
for {
// Expecting a key which should be a string
if p.lastChar != '"' {
panic(fmt.Sprintf("Expected object key to be a string, got %q", p.lastChar))
}
p.depth++
key := p.parseString()
key.key, key.value = key.value, nil
p.skipWhitespace()
// Expecting colon after key
if p.lastChar != ':' {
panic(fmt.Sprintf("Expected colon after object key, got %q", p.lastChar))
}
p.next()
p.skipFirstIdent = true
value := p.parseValue()
p.depth--
key.value = value.value
key.next = value.next
if key.next != nil {
key.next.prev = key
}
key.end = value.end
object.append(key)
p.skipWhitespace()
// End of object
if p.lastChar == '}' {
closeBracket := &node{depth: p.depth}
closeBracket.value = []byte{'}'}
object.append(closeBracket)
p.next()
return object
}
// Multiple key-value pairs separated by comma
if p.lastChar == ',' {
object.end.comma = true
p.next()
p.skipWhitespace()
continue
}
// Unexpected character
panic(fmt.Sprintf("Unexpected character %q in object", p.lastChar))
}
}
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, ']')
p.next()
return arr
}
for {
p.depth++
value := p.parseValue()
p.depth--
arr.append(value)
p.skipWhitespace()
if p.lastChar == ']' {
closeBracket := &node{depth: p.depth}
closeBracket.value = []byte{']'}
arr.append(closeBracket)
p.next()
return arr
} else if p.lastChar == ',' {
arr.end.comma = true
p.next()
p.skipWhitespace()
continue
} else {
panic(fmt.Sprintf("Invalid character %q in array", p.lastChar))
}
}
}
func (p *jsonParser) parseKeyword(name string) *node {
for i := 1; i < len(name); i++ {
p.next()
if p.lastChar != name[i] {
panic(fmt.Sprintf("Unexpected character %q in keyword", p.lastChar))
}
}
p.next()
nextCharIsSpecial := isWhitespace(p.lastChar) || p.lastChar == ',' || p.lastChar == '}' || p.lastChar == ']' || p.lastChar == 0
if nextCharIsSpecial {
keyword := &node{depth: p.depth}
keyword.value = []byte(name)
return keyword
}
panic(fmt.Sprintf("Unexpected character %q in keyword", p.lastChar))
}
func isWhitespace(ch byte) bool {
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}
func (p *jsonParser) skipWhitespace() {
for {
switch p.lastChar {
case ' ', '\t', '\n', '\r':
p.next()
case '/':
p.skipComment()
default:
return
}
}
}
func (p *jsonParser) skipComment() {
p.next()
switch p.lastChar {
case '/':
for p.lastChar != '\n' && p.lastChar != 0 {
p.next()
}
case '*':
for {
p.next()
if p.lastChar == '*' {
p.next()
if p.lastChar == '/' {
p.next()
return
}
}
if p.lastChar == 0 {
panic("Unexpected end of input in comment")
}
}
default:
panic(fmt.Sprintf("Invalid comment: '/%c'", p.lastChar))
}
}
func (p *jsonParser) errorSnippet(message string) error {
if p.lastChar == 0 {
message = "Unexpected end of input"
}
return fmt.Errorf("%s on node %d.\n%s\n", message, p.lineNumber, p.sourceTail.string())
}

@ -0,0 +1,123 @@
package main
import "github.com/charmbracelet/bubbles/key"
type KeyMap struct {
Quit key.Binding
Help key.Binding
PageDown key.Binding
PageUp key.Binding
HalfPageUp key.Binding
HalfPageDown key.Binding
GotoTop key.Binding
GotoBottom key.Binding
Down key.Binding
Up key.Binding
Expand key.Binding
Collapse key.Binding
ExpandRecursively key.Binding
CollapseRecursively key.Binding
ExpandAll key.Binding
CollapseAll key.Binding
NextSibling key.Binding
PrevSibling key.Binding
ToggleWrap key.Binding
Search key.Binding
Next key.Binding
Prev key.Binding
}
var keyMap KeyMap
func init() {
keyMap = KeyMap{
Quit: key.NewBinding(
key.WithKeys("q", "ctrl+c", "esc"),
key.WithHelp("", "exit program"),
),
Help: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("", "show help"),
),
PageDown: key.NewBinding(
key.WithKeys("pgdown", " ", "f"),
key.WithHelp("pgdown, space, f", "page down"),
),
PageUp: key.NewBinding(
key.WithKeys("pgup", "b"),
key.WithHelp("pgup, b", "page up"),
),
HalfPageUp: key.NewBinding(
key.WithKeys("u", "ctrl+u"),
key.WithHelp("", "half page up"),
),
HalfPageDown: key.NewBinding(
key.WithKeys("d", "ctrl+d"),
key.WithHelp("", "half page down"),
),
GotoTop: key.NewBinding(
key.WithKeys("g"),
key.WithHelp("", "goto top"),
),
GotoBottom: key.NewBinding(
key.WithKeys("G"),
key.WithHelp("", "goto bottom"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("", "down"),
),
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("", "up"),
),
Expand: key.NewBinding(
key.WithKeys("right", "l"),
key.WithHelp("", "expand"),
),
Collapse: key.NewBinding(
key.WithKeys("left", "h"),
key.WithHelp("", "collapse"),
),
ExpandRecursively: key.NewBinding(
key.WithKeys("L"),
key.WithHelp("", "expand recursively"),
),
CollapseRecursively: key.NewBinding(
key.WithKeys("H"),
key.WithHelp("", "collapse recursively"),
),
ExpandAll: key.NewBinding(
key.WithKeys("e"),
key.WithHelp("", "expand all"),
),
CollapseAll: key.NewBinding(
key.WithKeys("E"),
key.WithHelp("", "collapse all"),
),
NextSibling: key.NewBinding(
key.WithKeys("J"),
key.WithHelp("", "next sibling"),
),
PrevSibling: key.NewBinding(
key.WithKeys("K"),
key.WithHelp("", "previous sibling"),
),
ToggleWrap: key.NewBinding(
key.WithKeys("z"),
key.WithHelp("", "toggle strings wrap"),
),
Search: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("", "search regexp"),
),
Next: key.NewBinding(
key.WithKeys("n"),
key.WithHelp("", "next search result"),
),
Prev: key.NewBinding(
key.WithKeys("N"),
key.WithHelp("", "prev search result"),
),
}
}

@ -0,0 +1,121 @@
package main
import (
"fmt"
"io"
"os"
"runtime/pprof"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
)
func main() {
cpuProfile := os.Getenv("CPU_PROFILE")
if cpuProfile != "" {
f, err := os.Create(cpuProfile)
if err != nil {
panic(err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
panic(err)
}
}
data, err := io.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
head, err := parse(data)
if err != nil {
fmt.Print(err.Error())
os.Exit(1)
return
}
m := &model{
head: head,
}
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
_, err = p.Run()
if err != nil {
panic(err)
}
if cpuProfile != "" {
pprof.StopCPUProfile()
}
}
type model struct {
windowWidth, windowHeight int
head *node
}
func (m *model) Init() tea.Cmd {
return nil
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.windowWidth = msg.Width
m.windowHeight = msg.Height
case tea.MouseMsg:
switch msg.Type {
case tea.MouseWheelUp:
case tea.MouseWheelDown:
}
case tea.KeyMsg:
return m.handleKey(msg)
}
return m, nil
}
func (m *model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, keyMap.Quit):
return m, tea.Quit
case key.Matches(msg, keyMap.Up):
m.head = m.head.prev
return m, nil
case key.Matches(msg, keyMap.Down):
m.head = m.head.next
return m, nil
}
return m, nil
}
func (m *model) View() string {
var screen []byte
head := m.head
for i := 0; i < m.windowHeight; i++ {
if head == nil {
break
}
for ident := 0; ident < int(head.depth); ident++ {
screen = append(screen, ' ', ' ')
}
if head.key != nil {
screen = append(screen, head.key...)
screen = append(screen, ':', ' ')
}
screen = append(screen, head.value...)
if head.comma {
screen = append(screen, ',')
}
screen = append(screen, '\n')
head = head.next
}
if len(screen) > 0 && screen[len(screen)-1] == '\n' {
screen = screen[:len(screen)-1]
}
return string(screen)
}

@ -0,0 +1,22 @@
package main
type node struct {
prev, next, end *node
depth uint8
key []byte
value []byte
comma bool
}
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
}
}

@ -0,0 +1,32 @@
package main
type ring struct {
buf [100]byte
start, end int
}
func (r *ring) writeByte(b byte) {
if b == '\n' {
r.end = 0
r.start = r.end
return
}
r.buf[r.end] = b
r.end++
if r.end >= len(r.buf) {
r.end = 0
}
if r.end == r.start {
r.start++
if r.start >= len(r.buf) {
r.start = 0
}
}
}
func (r *ring) string() string {
if r.start < r.end {
return string(r.buf[r.start:r.end])
}
return string(r.buf[r.start:]) + string(r.buf[:r.end])
}

@ -0,0 +1,9 @@
package main
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'
}
Loading…
Cancel
Save