jsoncolor.go

v0.7.0
Doc Versions Source
1
package logreader
2
3
import (
4
	"bytes"
5
	"encoding/json"
6
	"fmt"
7
	"strings"
8
9
	"github.com/charmbracelet/lipgloss"
10
)
11
12
var (
13
	jsonKeyStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("117"))            // light blue
14
	jsonStrStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("107"))            // green
15
	jsonNumStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("214"))            // orange
16
	jsonBoolStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("170")).Bold(true) // purple
17
	jsonNullStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Italic(true)
18
	jsonBracStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252")) // white
19
)
20
21
// prettifyBody detects SSE "data: {json}" lines in the body and pretty-prints
22
// each JSON payload. Non-SSE bodies (already pretty JSON) are returned as-is.
23
func prettifyBody(body string) string {
24
	// Check if this looks like SSE data.
25
	hasSSE := false
26
	for line := range strings.SplitSeq(body, "\n") {
27
		if strings.HasPrefix(line, "data: {") {
28
			hasSSE = true
29
			break
30
		}
31
	}
32
	if !hasSSE {
33
		return body
34
	}
35
36
	var b strings.Builder
37
	chunkNum := 0
38
	for line := range strings.SplitSeq(body, "\n") {
39
		line = strings.TrimSpace(line)
40
		if line == "" {
41
			continue
42
		}
43
		if !strings.HasPrefix(line, "data: ") {
44
			// Non-data lines like [END OF STREAM], [Total chunks...]
45
			b.WriteString(line)
46
			b.WriteByte('\n')
47
			continue
48
		}
49
50
		payload := strings.TrimPrefix(line, "data: ")
51
		if payload == "[DONE]" {
52
			b.WriteString("data: [DONE]\n")
53
			continue
54
		}
55
56
		// Try to pretty-print JSON.
57
		var pretty bytes.Buffer
58
		if json.Indent(&pretty, []byte(payload), "  ", "  ") == nil {
59
			chunkNum++
60
			fmt.Fprintf(&b, "chunk #%d:\n", chunkNum)
61
			b.WriteString("  ")
62
			b.WriteString(pretty.String())
63
			b.WriteByte('\n')
64
		} else {
65
			// Not valid JSON — show as-is.
66
			b.WriteString(line)
67
			b.WriteByte('\n')
68
		}
69
	}
70
	return b.String()
71
}
72
73
// colorizeJSON applies syntax highlighting to pretty-printed JSON.
74
// Input must already be wrapped/formatted to fit the display width.
75
func colorizeJSON(s string) string {
76
	var b strings.Builder
77
	b.Grow(len(s) * 2)
78
79
	for line := range strings.SplitSeq(s, "\n") {
80
		b.WriteString(colorizeLine(line))
81
		b.WriteByte('\n')
82
	}
83
	return b.String()
84
}
85
86
func colorizeLine(line string) string {
87
	trimmed := strings.TrimLeft(line, " ")
88
	indent := line[:len(line)-len(trimmed)]
89
90
	if trimmed == "" {
91
		return ""
92
	}
93
94
	// Key-value line: "key": value
95
	if strings.HasPrefix(trimmed, `"`) {
96
		colonIdx := strings.Index(trimmed, `": `)
97
		if colonIdx > 0 {
98
			key := trimmed[:colonIdx+1]  // includes closing quote
99
			rest := trimmed[colonIdx+1:] // ": value..."
100
			return indent + jsonKeyStyle.Render(key) + colorizeValue(rest)
101
		}
102
		// Standalone string (e.g. array element)
103
		return indent + colorizeValue(trimmed)
104
	}
105
106
	// Pure value lines (in arrays, or closing brackets).
107
	return indent + colorizeValue(trimmed)
108
}
109
110
func colorizeValue(s string) string {
111
	// Handle ": value" prefix.
112
	prefix := ""
113
	val := s
114
	if strings.HasPrefix(s, ": ") {
115
		prefix = ": "
116
		val = s[2:]
117
	}
118
119
	val = strings.TrimRight(val, ",")
120
	comma := ""
121
	if len(val) < len(strings.TrimRight(s[len(prefix):], ",")) || strings.HasSuffix(s, ",") {
122
		comma = ","
123
	}
124
125
	var styled string
126
	switch {
127
	case val == "{" || val == "}" || val == "[" || val == "]" ||
128
		val == "{}" || val == "[]" ||
129
		strings.HasPrefix(val, "{") || strings.HasPrefix(val, "["):
130
		styled = jsonBracStyle.Render(val)
131
	case val == "null":
132
		styled = jsonNullStyle.Render(val)
133
	case val == "true" || val == "false":
134
		styled = jsonBoolStyle.Render(val)
135
	case strings.HasPrefix(val, `"`):
136
		styled = jsonStrStyle.Render(val)
137
	case len(val) > 0 && (val[0] >= '0' && val[0] <= '9' || val[0] == '-'):
138
		styled = jsonNumStyle.Render(val)
139
	default:
140
		styled = val
141
	}
142
143
	return prefix + styled + comma
144
}
145

Source Files