@ 261101.7

261026.5 · e0e318febdcbbba34e53f28d52928a8ba9a2206e

JSON files have a gron view

A code page for a .json file offers gron at the left end of the view row. It shows one line per node as a JS assignment, eg json.tags[0] = "a";, so each value reads and searches with its full path. The conversion runs in-process, scans once and builds no tree. Keys keep input order and values show as written. A file that is not valid JSON shows the flat code under a line naming where parsing stopped.

reading declarations

Changes to gron.go. All files

gron.go

new file mode 100644@@ -0,0 +1,331 @@+package main++import (+	"bytes"+	"encoding/json"+	"fmt"+	"path"+	"strconv"+	"strings"+	"unicode"+	"unicode/utf8"++	"github.com/alecthomas/chroma/v2/lexers"+)++// isJSON says whether a code page offers the gron view.+func isJSON(name string) bool {+	return strings.EqualFold(path.Ext(name), ".json")+}++// gronLexer colours the gron view: its lines are JavaScript.+var gronLexer = lexers.Get("javascript")++// gron writes each node of a JSON document on its own line as a JS+// assignment, json.a.b[0] = "x";, so the page reads and searches by path.+// It scans the input once and builds no tree. Keys keep their input order+// and values print as written. Malformed input returns an error that names+// its line and column.+func gron(b []byte) (out []byte, err error) {+	g := &gronner{buf: b, path: []byte("json")}+	defer func() {+		if r := recover(); r != nil {+			e, ok := r.(gronError)+			if !ok {+				panic(r)+			}+			out, err = nil, e+		}+	}()+	g.ws()+	if g.pos == len(b) {+		g.fail("no value")+	}+	g.value()+	g.ws()+	if g.pos != len(b) {+		g.fail("data after the value")+	}+	return g.out.Bytes(), nil+}++type gronError struct {+	line, col int+	msg       string+}++func (e gronError) Error() string {+	return fmt.Sprintf("line %d, column %d: %s", e.line, e.col, e.msg)+}++// gronMaxDepth bounds nesting, so a hostile file cannot exhaust the stack.+const gronMaxDepth = 10000++type gronner struct {+	buf   []byte+	pos   int+	depth int+	path  []byte+	out   bytes.Buffer+}++func (g *gronner) fail(msg string) {+	line, col := 1, 1+	for _, c := range g.buf[:min(g.pos, len(g.buf))] {+		if c == '\n' {+			line, col = line+1, 1+		} else {+			col+++		}+	}+	panic(gronError{line, col, msg})+}++func (g *gronner) ws() {+	for g.pos < len(g.buf) {+		switch g.buf[g.pos] {+		case ' ', '\t', '\n', '\r':+			g.pos+++		default:+			return+		}+	}+}++func (g *gronner) peek() byte {+	if g.pos < len(g.buf) {+		return g.buf[g.pos]+	}+	return 0+}++func (g *gronner) emit(v []byte) {+	g.out.Write(g.path)+	g.out.WriteString(" = ")+	g.out.Write(v)+	g.out.WriteString(";\n")+}++func (g *gronner) value() {+	g.ws()+	start := g.pos+	switch c := g.peek(); {+	case c == '{':+		g.object()+	case c == '[':+		g.array()+	case c == '"':+		g.str()+		g.emit(g.buf[start:g.pos])+	case c == '-' || isDigit(c):+		g.number()+		g.emit(g.buf[start:g.pos])+	case c == 't':+		g.literal("true")+	case c == 'f':+		g.literal("false")+	case c == 'n':+		g.literal("null")+	case g.pos >= len(g.buf):+		g.fail("unexpected end")+	default:+		g.fail(fmt.Sprintf("unexpected %q", c))+	}+}++func (g *gronner) enter() {+	g.depth+++	if g.depth > gronMaxDepth {+		g.fail(fmt.Sprintf("nested deeper than %d", gronMaxDepth))+	}+	g.pos+++	g.ws()+}++func (g *gronner) object() {+	g.emit([]byte("{}"))+	g.enter()+	defer func() { g.depth-- }()+	if g.peek() == '}' {+		g.pos+++		return+	}+	for {+		g.ws()+		if g.peek() != '"' {+			g.fail("expected a string key")+		}+		start := g.pos+		esc := g.str()+		n := len(g.path)+		g.path = appendKey(g.path, g.buf[start:g.pos], esc)+		g.ws()+		if g.peek() != ':' {+			g.fail("expected ':'")+		}+		g.pos+++		g.value()+		g.path = g.path[:n]+		g.ws()+		switch g.peek() {+		case ',':+			g.pos+++		case '}':+			g.pos+++			return+		default:+			g.fail("expected ',' or '}'")+		}+	}+}++func (g *gronner) array() {+	g.emit([]byte("[]"))+	g.enter()+	defer func() { g.depth-- }()+	if g.peek() == ']' {+		g.pos+++		return+	}+	for i := 0; ; i++ {+		n := len(g.path)+		g.path = append(g.path, '[')+		g.path = strconv.AppendInt(g.path, int64(i), 10)+		g.path = append(g.path, ']')+		g.value()+		g.path = g.path[:n]+		g.ws()+		switch g.peek() {+		case ',':+			g.pos+++		case ']':+			g.pos+++			return+		default:+			g.fail("expected ',' or ']'")+		}+	}+}++// str scans a string and reports whether it holds escapes.+func (g *gronner) str() (esc bool) {+	g.pos+++	for g.pos < len(g.buf) {+		c := g.buf[g.pos]+		switch {+		case c == '"':+			g.pos+++			return esc+		case c == '\\':+			esc = true+			g.pos+++			switch g.peek() {+			case '"', '\\', '/', 'b', 'f', 'n', 'r', 't':+			case 'u':+				for range 4 {+					g.pos+++					if !isHex(g.peek()) {+						g.fail(`bad \u escape`)+					}+				}+			default:+				g.fail("bad escape")+			}+		case c < 0x20:+			g.fail("control character in a string")+		}+		g.pos+++	}+	g.fail("unterminated string")+	return+}++func (g *gronner) digits() {+	if !isDigit(g.peek()) {+		g.fail("expected a digit")+	}+	for isDigit(g.peek()) {+		g.pos+++	}+}++func (g *gronner) number() {+	if g.peek() == '-' {+		g.pos+++	}+	if g.peek() == '0' {+		g.pos+++	} else {+		g.digits()+	}+	if g.peek() == '.' {+		g.pos+++		g.digits()+	}+	if c := g.peek(); c == 'e' || c == 'E' {+		g.pos+++		if c := g.peek(); c == '+' || c == '-' {+			g.pos+++		}+		g.digits()+	}+}++func (g *gronner) literal(s string) {+	if !bytes.HasPrefix(g.buf[g.pos:], []byte(s)) {+		g.fail("bad literal")+	}+	g.pos += len(s)+	g.emit([]byte(s))+}++func isHex(c byte) bool {+	return isDigit(c) || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'+}++// appendKey adds a key to the path: .name when the key is a JS identifier,+// otherwise ["key"] with the key as written.+func appendKey(path, quoted []byte, esc bool) []byte {+	name := quoted[1 : len(quoted)-1]+	if esc {+		var s string+		if json.Unmarshal(quoted, &s) == nil {+			name = []byte(s)+		}+	}+	if utf8.Valid(name) && isIdent(name) {+		path = append(path, '.')+		return append(path, name...)+	}+	path = append(path, '[')+	path = append(path, quoted...)+	return append(path, ']')+}++// isIdent follows gron's rule: a JS identifier that is not a reserved word.+func isIdent(b []byte) bool {+	if len(b) == 0 || gronReserved[string(b)] {+		return false+	}+	for i, r := range string(b) {+		if r == '$' || r == '_' || unicode.In(r, unicode.Lu, unicode.Ll, unicode.Lm, unicode.Lo, unicode.Nl) {+			continue+		}+		if i > 0 && unicode.In(r, unicode.Mn, unicode.Mc, unicode.Nd, unicode.Pc) {+			continue+		}+		return false+	}+	return true+}++var gronReserved = map[string]bool{+	"break": true, "case": true, "catch": true, "class": true, "const": true,+	"continue": true, "debugger": true, "default": true, "delete": true,+	"do": true, "else": true, "export": true, "extends": true, "false": true,+	"finally": true, "for": true, "function": true, "if": true, "import": true,+	"in": true, "instanceof": true, "new": true, "null": true, "return": true,+	"super": true, "switch": true, "this": true, "throw": true, "true": true,+	"try": true, "typeof": true, "var": true, "void": true, "while": true,+	"with": true, "yield": true,+}