1package main
2
3import (
4	"bytes"
5	"encoding/json"
6	"fmt"
7	"unicode"
8)
9
10// A token is a chunk of text from a statement with a type
11type token struct {
12	text string
13	typ  tokenTyp
14}
15
16// A tokenTyp identifies what kind of token something is
17type tokenTyp int
18
19const (
20	// A bare word is a unquoted key; like 'foo' in json.foo = 1;
21	typBare tokenTyp = iota
22
23	// Numeric key; like '2' in json[2] = "foo";
24	typNumericKey
25
26	// A quoted key; like 'foo bar' in json["foo bar"] = 2;
27	typQuotedKey
28
29	// Punctuation types
30	typDot    // .
31	typLBrace // [
32	typRBrace // ]
33	typEquals // =
34	typSemi   // ;
35	typComma  // ,
36
37	// Value types
38	typString      // "foo"
39	typNumber      // 4
40	typTrue        // true
41	typFalse       // false
42	typNull        // null
43	typEmptyArray  // []
44	typEmptyObject // {}
45
46	// Ignored token
47	typIgnored
48
49	// Error token
50	typError
51)
52
53// a sprintFn adds color to its input
54type sprintFn func(...interface{}) string
55
56// mapping of token types to the appropriate color sprintFn
57var sprintFns = map[tokenTyp]sprintFn{
58	typBare:        bareColor.SprintFunc(),
59	typNumericKey:  numColor.SprintFunc(),
60	typQuotedKey:   strColor.SprintFunc(),
61	typLBrace:      braceColor.SprintFunc(),
62	typRBrace:      braceColor.SprintFunc(),
63	typString:      strColor.SprintFunc(),
64	typNumber:      numColor.SprintFunc(),
65	typTrue:        boolColor.SprintFunc(),
66	typFalse:       boolColor.SprintFunc(),
67	typNull:        boolColor.SprintFunc(),
68	typEmptyArray:  braceColor.SprintFunc(),
69	typEmptyObject: braceColor.SprintFunc(),
70}
71
72// isValue returns true if the token is a valid value type
73func (t token) isValue() bool {
74	switch t.typ {
75	case typString, typNumber, typTrue, typFalse, typNull, typEmptyArray, typEmptyObject:
76		return true
77	default:
78		return false
79	}
80}
81
82// isPunct returns true if the token is a punctuation type
83func (t token) isPunct() bool {
84	switch t.typ {
85	case typDot, typLBrace, typRBrace, typEquals, typSemi, typComma:
86		return true
87	default:
88		return false
89	}
90}
91
92// format returns the formatted version of the token text
93func (t token) format() string {
94	if t.typ == typEquals {
95		return " " + t.text + " "
96	}
97	return t.text
98}
99
100// formatColor returns the colored formatted version of the token text
101func (t token) formatColor() string {
102	text := t.text
103	if t.typ == typEquals {
104		text = " " + text + " "
105	}
106	fn, ok := sprintFns[t.typ]
107	if ok {
108		return fn(text)
109	}
110	return text
111
112}
113
114// valueTokenFromInterface takes any valid value and
115// returns a value token to represent it
116func valueTokenFromInterface(v interface{}) token {
117	switch vv := v.(type) {
118
119	case map[string]interface{}:
120		return token{"{}", typEmptyObject}
121	case []interface{}:
122		return token{"[]", typEmptyArray}
123	case json.Number:
124		return token{vv.String(), typNumber}
125	case string:
126		return token{quoteString(vv), typString}
127	case bool:
128		if vv {
129			return token{"true", typTrue}
130		}
131		return token{"false", typFalse}
132	case nil:
133		return token{"null", typNull}
134	default:
135		return token{"", typError}
136	}
137}
138
139// quoteString takes a string and returns a quoted and
140// escaped string valid for use in gron output
141func quoteString(s string) string {
142
143	out := &bytes.Buffer{}
144	// bytes.Buffer never returns errors on these methods.
145	// errors are explicitly ignored to keep the linter
146	// happy. A price worth paying so that the linter
147	// remains useful.
148	_ = out.WriteByte('"')
149
150	for _, r := range s {
151
152		if r == '\\' || r == '"' {
153			_ = out.WriteByte('\\')
154			_, _ = out.WriteRune(r)
155			continue
156		}
157
158		// \u2028 and \u2029 are separator runes that are not valid
159		// in javascript strings so they must be escaped.
160		// See http://timelessrepo.com/json-isnt-a-javascript-subset
161		if r == '\u2028' {
162			_, _ = out.WriteString(`\u2028`)
163			continue
164		}
165		if r == '\u2029' {
166			_, _ = out.WriteString(`\u2029`)
167			continue
168		}
169
170		// Any other control runes must be escaped
171		if unicode.IsControl(r) {
172
173			switch r {
174			case '\b':
175				_ = out.WriteByte('\\')
176				_ = out.WriteByte('b')
177			case '\f':
178				_ = out.WriteByte('\\')
179				_ = out.WriteByte('f')
180			case '\n':
181				_ = out.WriteByte('\\')
182				_ = out.WriteByte('n')
183			case '\r':
184				_ = out.WriteByte('\\')
185				_ = out.WriteByte('r')
186			case '\t':
187				_ = out.WriteByte('\\')
188				_ = out.WriteByte('t')
189			default:
190				_, _ = out.WriteString(fmt.Sprintf(`\u%04X`, r))
191			}
192
193			continue
194		}
195
196		// Unescaped rune
197		_, _ = out.WriteRune(r)
198	}
199
200	_ = out.WriteByte('"')
201	return out.String()
202
203}
204