Skip to content

Commit 039966d

Browse files
committed
Add JSON stream highlighter
1 parent 9b20cd7 commit 039966d

File tree

5 files changed

+201
-10
lines changed

5 files changed

+201
-10
lines changed

cmd/root.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,18 @@ func NewRootCmd() *cobra.Command {
7272
} else if cssQuery != "" {
7373
err = utils.CSSQuery(reader, pw, cssQuery, cssAttr, options)
7474
} else {
75-
var isHtmlFormatter bool
76-
isHtmlFormatter, reader = isHTMLFormatterNeeded(cmd.Flags(), reader)
75+
var contentType utils.ContentType
76+
contentType, reader = detectFormat(cmd.Flags(), reader)
7777

78-
if isHtmlFormatter {
78+
switch contentType {
79+
case utils.ContentHtml:
7980
err = utils.FormatHtml(reader, pw, indent, colors)
80-
} else {
81+
case utils.ContentXml:
8182
err = utils.FormatXml(reader, pw, indent, colors)
83+
case utils.ContentJson:
84+
err = utils.FormatJson(reader, pw, indent, colors)
85+
default:
86+
err = fmt.Errorf("unknown content type: %v", contentType)
8287
}
8388
}
8489

@@ -198,18 +203,27 @@ func getColorMode(flags *pflag.FlagSet) int {
198203
return colors
199204
}
200205

201-
func isHTMLFormatterNeeded(flags *pflag.FlagSet, origReader io.Reader) (bool, io.Reader) {
206+
func detectFormat(flags *pflag.FlagSet, origReader io.Reader) (utils.ContentType, io.Reader) {
202207
isHtmlFormatter, _ := flags.GetBool("html")
203208
if isHtmlFormatter {
204-
return isHtmlFormatter, origReader
209+
return utils.ContentHtml, origReader
205210
}
206211

207212
buf := make([]byte, 10)
208213
length, err := origReader.Read(buf)
209214
if err != nil {
210-
return false, origReader
215+
return utils.ContentText, origReader
211216
}
212217

213218
reader := io.MultiReader(bytes.NewReader(buf[:length]), origReader)
214-
return utils.IsHTML(string(buf)), reader
219+
220+
if utils.IsJSON(string(buf)) {
221+
return utils.ContentJson, reader
222+
}
223+
224+
if utils.IsHTML(string(buf)) {
225+
return utils.ContentHtml, reader
226+
}
227+
228+
return utils.ContentXml, reader
215229
}

internal/utils/utils.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package utils
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"encoding/xml"
67
"errors"
78
"fmt"
@@ -14,6 +15,7 @@ import (
1415
"io"
1516
"os"
1617
"os/exec"
18+
"reflect"
1719
"regexp"
1820
"strings"
1921
)
@@ -24,12 +26,33 @@ const (
2426
ColorsDisabled
2527
)
2628

29+
type ContentType int
30+
31+
const (
32+
ContentXml ContentType = iota
33+
ContentHtml
34+
ContentJson
35+
ContentText
36+
)
37+
2738
type QueryOptions struct {
2839
WithTags bool
2940
Indent string
3041
Colors int
3142
}
3243

44+
const (
45+
jsonTokenTopValue = iota
46+
jsonTokenArrayStart
47+
jsonTokenArrayValue
48+
jsonTokenArrayComma
49+
jsonTokenObjectStart
50+
jsonTokenObjectKey
51+
jsonTokenObjectColon
52+
jsonTokenObjectValue
53+
jsonTokenObjectComma
54+
)
55+
3356
func FormatXml(reader io.Reader, writer io.Writer, indent string, colors int) error {
3457
decoder := xml.NewDecoder(reader)
3558
decoder.Strict = false
@@ -344,6 +367,90 @@ func FormatHtml(reader io.Reader, writer io.Writer, indent string, colors int) e
344367
return nil
345368
}
346369

370+
func FormatJson(reader io.Reader, writer io.Writer, indent string, colors int) error {
371+
decoder := json.NewDecoder(reader)
372+
decoder.UseNumber()
373+
374+
if ColorsDefault != colors {
375+
color.NoColor = colors == ColorsDisabled
376+
}
377+
378+
tagColor := color.New(color.FgYellow).SprintFunc()
379+
attrColor := color.New(color.FgHiBlue).SprintFunc()
380+
valueColor := color.New(color.FgGreen).SprintFunc()
381+
382+
level := 0
383+
suffix := ""
384+
prefix := ""
385+
386+
for {
387+
token, err := decoder.Token()
388+
389+
if err == io.EOF {
390+
break
391+
}
392+
393+
if err != nil {
394+
return err
395+
}
396+
397+
v := reflect.ValueOf(*decoder)
398+
tokenState := v.FieldByName("tokenState").Int()
399+
400+
switch tokenState {
401+
case jsonTokenObjectColon:
402+
suffix = ": "
403+
case jsonTokenObjectComma:
404+
suffix = ",\n" + strings.Repeat(indent, level)
405+
case jsonTokenArrayComma:
406+
suffix = ",\n" + strings.Repeat(indent, level)
407+
}
408+
409+
switch tokenType := token.(type) {
410+
case json.Delim:
411+
switch rune(tokenType) {
412+
case '{':
413+
_, _ = fmt.Fprintf(writer, "%s%s\n", prefix, tagColor("{"))
414+
level++
415+
suffix = strings.Repeat(indent, level)
416+
case '}':
417+
level--
418+
_, _ = fmt.Fprintf(writer, "\n%s%s", strings.Repeat(indent, level), tagColor("}"))
419+
if tokenState == jsonTokenArrayComma {
420+
suffix = ",\n" + strings.Repeat(indent, level)
421+
}
422+
case '[':
423+
_, _ = fmt.Fprintf(writer, "%s%s\n", prefix, tagColor("["))
424+
level++
425+
suffix = strings.Repeat(indent, level)
426+
case ']':
427+
level--
428+
_, _ = fmt.Fprintf(writer, "\n%s%s", strings.Repeat(indent, level), tagColor("]"))
429+
}
430+
case string:
431+
value := valueColor(token)
432+
if tokenState == jsonTokenObjectColon {
433+
value = attrColor(token)
434+
}
435+
_, _ = fmt.Fprintf(writer, "%s\"%s\"", prefix, value)
436+
case float64:
437+
_, _ = fmt.Fprintf(writer, "%s%v", prefix, valueColor(token))
438+
case json.Number:
439+
_, _ = fmt.Fprintf(writer, "%s%v", prefix, valueColor(token))
440+
case bool:
441+
_, _ = fmt.Fprintf(writer, "%s%v", prefix, valueColor(token))
442+
case nil:
443+
_, _ = fmt.Fprintf(writer, "%s%s", prefix, valueColor("null"))
444+
}
445+
446+
prefix = suffix
447+
}
448+
449+
_, _ = fmt.Fprint(writer, "\n")
450+
451+
return nil
452+
}
453+
347454
func IsHTML(input string) bool {
348455
input = strings.ToLower(input)
349456
htmlMarkers := []string{"html", "<!d", "<body"}
@@ -357,6 +464,12 @@ func IsHTML(input string) bool {
357464
return false
358465
}
359466

467+
func IsJSON(input string) bool {
468+
input = strings.ToLower(input)
469+
matched, _ := regexp.MatchString(`\s*[{\[]`, input)
470+
return matched
471+
}
472+
360473
func PagerPrint(reader io.Reader, writer io.Writer) error {
361474
pager := os.Getenv("PAGER")
362475

internal/utils/utils_test.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ func TestFormatHtml(t *testing.T) {
6464
for unformattedFile, expectedFile := range files {
6565
unformattedHtmlReader := getFileReader(path.Join("..", "..", "test", "data", "html", unformattedFile))
6666

67-
bytes, readErr := os.ReadFile(path.Join("..", "..", "test", "data", "html", expectedFile))
67+
data, readErr := os.ReadFile(path.Join("..", "..", "test", "data", "html", expectedFile))
6868
assert.Nil(t, readErr)
69-
expectedHtml := string(bytes)
69+
expectedHtml := string(data)
7070

7171
output := new(strings.Builder)
7272
formatErr := FormatHtml(unformattedHtmlReader, output, " ", ColorsDisabled)
@@ -75,6 +75,25 @@ func TestFormatHtml(t *testing.T) {
7575
}
7676
}
7777

78+
func TestFormatJson(t *testing.T) {
79+
files := map[string]string{
80+
"unformatted.json": "formatted.json",
81+
}
82+
83+
for unformattedFile, expectedFile := range files {
84+
unformattedJsonReader := getFileReader(path.Join("..", "..", "test", "data", "json", unformattedFile))
85+
86+
data, readErr := os.ReadFile(path.Join("..", "..", "test", "data", "json", expectedFile))
87+
assert.Nil(t, readErr)
88+
expectedJson := string(data)
89+
90+
output := new(strings.Builder)
91+
formatErr := FormatJson(unformattedJsonReader, output, " ", ColorsDisabled)
92+
assert.Nil(t, formatErr)
93+
assert.Equal(t, expectedJson, output.String())
94+
}
95+
}
96+
7897
func TestXPathQuery(t *testing.T) {
7998
type test struct {
8099
input string
@@ -134,6 +153,14 @@ func TestIsHTML(t *testing.T) {
134153
assert.False(t, IsHTML("<root></root>"))
135154
}
136155

156+
func TestIsJSON(t *testing.T) {
157+
assert.True(t, IsJSON(`{"key": "value"}`))
158+
assert.True(t, IsJSON(`{"key": "value", "key2": "value2"}`))
159+
assert.True(t, IsJSON(`[1, 2, 3]`))
160+
assert.True(t, IsJSON(` {}`))
161+
assert.False(t, IsJSON(`<html></html>`))
162+
}
163+
137164
func TestPagerPrint(t *testing.T) {
138165
var output bytes.Buffer
139166
fileReader := getFileReader(path.Join("..", "..", "test", "data", "html", "formatted.html"))

test/data/json/formatted.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"menu": {
3+
"id": "file",
4+
"value": 17,
5+
"price": 100.32,
6+
"popup": {
7+
"menuitem": [
8+
{
9+
"value": "New",
10+
"onclick": "CreateNewDoc()"
11+
},
12+
{
13+
"value": "Open",
14+
"onclick": "OpenDoc()",
15+
"new": true
16+
},
17+
{
18+
"value": "Close",
19+
"onclick": "CloseDoc()",
20+
"link": null
21+
}
22+
]
23+
}
24+
}
25+
}

test/data/json/unformatted.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{"menu": {
2+
"id": "file" ,
3+
"value": 17,
4+
"price": 100.32,
5+
"popup": {
6+
"menuitem": [
7+
{"value": "New", "onclick": "CreateNewDoc()"},
8+
{"value": "Open", "onclick": "OpenDoc()", "new": true},
9+
{"value": "Close", "onclick": "CloseDoc()", "link": null }
10+
]
11+
}
12+
}}

0 commit comments

Comments
 (0)