Skip to content

Commit 3435771

Browse files
committed
Adds gron2shell tool
1 parent e7d60ad commit 3435771

File tree

5 files changed

+1081
-0
lines changed

5 files changed

+1081
-0
lines changed

gron2shell/identifier.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package main
2+
3+
import "unicode"
4+
5+
// The javascript reserved words cannot be used as unquoted keys
6+
var reservedWords = map[string]bool{
7+
"break": true,
8+
"case": true,
9+
"catch": true,
10+
"class": true,
11+
"const": true,
12+
"continue": true,
13+
"debugger": true,
14+
"default": true,
15+
"delete": true,
16+
"do": true,
17+
"else": true,
18+
"export": true,
19+
"extends": true,
20+
"false": true,
21+
"finally": true,
22+
"for": true,
23+
"function": true,
24+
"if": true,
25+
"import": true,
26+
"in": true,
27+
"instanceof": true,
28+
"new": true,
29+
"null": true,
30+
"return": true,
31+
"super": true,
32+
"switch": true,
33+
"this": true,
34+
"throw": true,
35+
"true": true,
36+
"try": true,
37+
"typeof": true,
38+
"var": true,
39+
"void": true,
40+
"while": true,
41+
"with": true,
42+
"yield": true,
43+
}
44+
45+
// validIdentifier checks to see if a string is a valid
46+
// JavaScript identifier
47+
// E.g:
48+
// justLettersAndNumbers1 -> true
49+
// a key with spaces -> false
50+
// 1startsWithANumber -> false
51+
func validIdentifier(s string) bool {
52+
if reservedWords[s] || s == "" {
53+
return false
54+
}
55+
56+
for i, r := range s {
57+
if i == 0 && !validFirstRune(r) {
58+
return false
59+
}
60+
if i != 0 && !validSecondaryRune(r) {
61+
return false
62+
}
63+
}
64+
65+
return true
66+
}
67+
68+
// validFirstRune returns true for runes that are valid
69+
// as the first rune in an identifier.
70+
// E.g:
71+
// 'r' -> true
72+
// '7' -> false
73+
func validFirstRune(r rune) bool {
74+
return unicode.In(r,
75+
unicode.Lu,
76+
unicode.Ll,
77+
unicode.Lm,
78+
unicode.Lo,
79+
unicode.Nl,
80+
) || r == '$' || r == '_'
81+
}
82+
83+
// validSecondaryRune returns true for runes that are valid
84+
// as anything other than the first rune in an identifier.
85+
func validSecondaryRune(r rune) bool {
86+
return validFirstRune(r) ||
87+
unicode.In(r, unicode.Mn, unicode.Mc, unicode.Nd, unicode.Pc)
88+
}

gron2shell/main.go

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"os"
8+
)
9+
10+
func main() {
11+
scanner := bufio.NewScanner(os.Stdin)
12+
13+
for scanner.Scan() {
14+
s := statementFromString(scanner.Text())
15+
f := formatStatement(s)
16+
if f == "" {
17+
continue
18+
}
19+
fmt.Println(f)
20+
}
21+
}
22+
23+
func formatStatement(s statement) string {
24+
out := &bytes.Buffer{}
25+
26+
// strip off the leading 'json' bare key
27+
if s[0].typ == typBare && s[0].text == "json" {
28+
s = s[1:]
29+
}
30+
31+
// strip off the leading dots
32+
if s[0].typ == typDot || s[0].typ == typLBrace {
33+
s = s[1:]
34+
}
35+
36+
for _, t := range s {
37+
switch t.typ {
38+
case typBare:
39+
out.WriteString(t.text)
40+
41+
case typNumericKey:
42+
out.WriteString(t.text)
43+
44+
case typQuotedKey:
45+
out.WriteString(t.text[1 : len(t.text)-1])
46+
47+
case typDot:
48+
out.WriteString(t.text)
49+
50+
case typLBrace:
51+
out.WriteRune('.')
52+
53+
case typRBrace:
54+
// nothing
55+
56+
case typEquals:
57+
out.WriteString(t.text)
58+
59+
case typSemi:
60+
// nothing
61+
62+
case typString:
63+
out.WriteString(t.text[1 : len(t.text)-1])
64+
65+
case typNumber:
66+
out.WriteString(t.text)
67+
68+
case typTrue:
69+
out.WriteString(t.text)
70+
71+
case typFalse:
72+
out.WriteString(t.text)
73+
74+
case typNull:
75+
out.WriteString(t.text)
76+
77+
case typEmptyArray:
78+
// ignore line
79+
return ""
80+
81+
case typEmptyObject:
82+
// ignore line
83+
return ""
84+
85+
default:
86+
// Nothing
87+
}
88+
}
89+
90+
return out.String()
91+
}

gron2shell/statements.go

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"reflect"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/pkg/errors"
11+
)
12+
13+
// A statement is a slice of tokens representing an assignment statement.
14+
// An assignment statement is something like:
15+
//
16+
// json.city = "Leeds";
17+
//
18+
// Where 'json', '.', 'city', '=', '"Leeds"' and ';' are discrete tokens.
19+
// Statements are stored as tokens to make sorting more efficient, and so
20+
// that the same type can easily be used when gronning and ungronning.
21+
type statement []token
22+
23+
// String returns the string form of a statement rather than the
24+
// underlying slice of tokens
25+
func (s statement) String() string {
26+
out := make([]string, 0, len(s)+2)
27+
for _, t := range s {
28+
out = append(out, t.format())
29+
}
30+
return strings.Join(out, "")
31+
}
32+
33+
// withBare returns a copy of a statement with a new bare
34+
// word token appended to it
35+
func (s statement) withBare(k string) statement {
36+
new := make(statement, len(s), len(s)+2)
37+
copy(new, s)
38+
return append(
39+
new,
40+
token{".", typDot},
41+
token{k, typBare},
42+
)
43+
}
44+
45+
// withQuotedKey returns a copy of a statement with a new
46+
// quoted key token appended to it
47+
func (s statement) withQuotedKey(k string) statement {
48+
new := make(statement, len(s), len(s)+3)
49+
copy(new, s)
50+
return append(
51+
new,
52+
token{"[", typLBrace},
53+
token{quoteString(k), typQuotedKey},
54+
token{"]", typRBrace},
55+
)
56+
}
57+
58+
// withNumericKey returns a copy of a statement with a new
59+
// numeric key token appended to it
60+
func (s statement) withNumericKey(k int) statement {
61+
new := make(statement, len(s), len(s)+3)
62+
copy(new, s)
63+
return append(
64+
new,
65+
token{"[", typLBrace},
66+
token{strconv.Itoa(k), typNumericKey},
67+
token{"]", typRBrace},
68+
)
69+
}
70+
71+
// statements is a list of assignment statements.
72+
// E.g statement: json.foo = "bar";
73+
type statements []statement
74+
75+
// addWithValue takes a statement representing a path, copies it,
76+
// adds a value token to the end of the statement and appends
77+
// the new statement to the list of statements
78+
func (ss *statements) addWithValue(path statement, value token) {
79+
s := make(statement, len(path), len(path)+3)
80+
copy(s, path)
81+
s = append(s, token{"=", typEquals}, value, token{";", typSemi})
82+
*ss = append(*ss, s)
83+
}
84+
85+
// add appends a new complete statement to list of statements
86+
func (ss *statements) add(s statement) {
87+
*ss = append(*ss, s)
88+
}
89+
90+
// Len returns the number of statements for sort.Sort
91+
func (ss statements) Len() int {
92+
return len(ss)
93+
}
94+
95+
// Swap swaps two statements for sort.Sort
96+
func (ss statements) Swap(i, j int) {
97+
ss[i], ss[j] = ss[j], ss[i]
98+
}
99+
100+
// statementFromString takes statement string, lexes it and returns
101+
// the corresponding statement
102+
func statementFromString(str string) statement {
103+
l := newLexer(str)
104+
s := l.lex()
105+
return s
106+
}
107+
108+
// ungron turns statements into a proper datastructure
109+
func (ss statements) toInterface() (interface{}, error) {
110+
111+
// Get all the individually parsed statements
112+
var parsed []interface{}
113+
for _, s := range ss {
114+
u, err := ungronTokens(s)
115+
116+
switch err.(type) {
117+
case nil:
118+
// no problem :)
119+
case errRecoverable:
120+
continue
121+
default:
122+
return nil, errors.Wrapf(err, "ungron failed for `%s`", s)
123+
}
124+
125+
parsed = append(parsed, u)
126+
}
127+
128+
if len(parsed) == 0 {
129+
return nil, fmt.Errorf("no statements were parsed")
130+
}
131+
132+
merged := parsed[0]
133+
for _, p := range parsed[1:] {
134+
m, err := recursiveMerge(merged, p)
135+
if err != nil {
136+
return nil, errors.Wrap(err, "failed to merge statements")
137+
}
138+
merged = m
139+
}
140+
return merged, nil
141+
142+
}
143+
144+
// Less compares two statements for sort.Sort
145+
// Implements a natural sort to keep array indexes in order
146+
func (ss statements) Less(a, b int) bool {
147+
148+
// ss[a] and ss[b] are both slices of tokens. The first
149+
// thing we need to do is find the first token (if any)
150+
// that differs, then we can use that token to decide
151+
// if ss[a] or ss[b] should come first in the sort.
152+
diffIndex := -1
153+
for i := range ss[a] {
154+
155+
if len(ss[b]) < i+1 {
156+
// b must be shorter than a, so it
157+
// should come first
158+
return false
159+
}
160+
161+
// The tokens match, so just carry on
162+
if ss[a][i] == ss[b][i] {
163+
continue
164+
}
165+
166+
// We've found a difference
167+
diffIndex = i
168+
break
169+
}
170+
171+
// If diffIndex is still -1 then the only difference must be
172+
// that ss[b] is longer than ss[a], so ss[a] should come first
173+
if diffIndex == -1 {
174+
return true
175+
}
176+
177+
// Get the tokens that differ
178+
ta := ss[a][diffIndex]
179+
tb := ss[b][diffIndex]
180+
181+
// An equals always comes first
182+
if ta.typ == typEquals {
183+
return true
184+
}
185+
if tb.typ == typEquals {
186+
return false
187+
}
188+
189+
// If both tokens are numeric keys do an integer comparison
190+
if ta.typ == typNumericKey && tb.typ == typNumericKey {
191+
ia, _ := strconv.Atoi(ta.text)
192+
ib, _ := strconv.Atoi(tb.text)
193+
return ia < ib
194+
}
195+
196+
// If neither token is a number, just do a string comparison
197+
if ta.typ != typNumber || tb.typ != typNumber {
198+
return ta.text < tb.text
199+
}
200+
201+
// We have two numbers to compare so turn them into json.Number
202+
// for comparison
203+
na, _ := json.Number(ta.text).Float64()
204+
nb, _ := json.Number(tb.text).Float64()
205+
return na < nb
206+
207+
}
208+
209+
// Contains searches the statements for a given statement
210+
// Mostly to make testing things easier
211+
func (ss statements) Contains(search statement) bool {
212+
for _, i := range ss {
213+
if reflect.DeepEqual(i, search) {
214+
return true
215+
}
216+
}
217+
return false
218+
}

0 commit comments

Comments
 (0)