color.go

v0.1.0
Doc Versions Source
1
package format
2
3
import (
4
	"strings"
5
)
6
7
// ANSI color codes.
8
const (
9
	reset   = "\033[0m"
10
	red     = "\033[31m"
11
	green   = "\033[32m"
12
	yellow  = "\033[33m"
13
	blue    = "\033[34m"
14
	magenta = "\033[35m"
15
	cyan    = "\033[36m"
16
	gray    = "\033[90m"
17
)
18
19
// Colorize applies syntax highlighting to formatted Confluence XML.
20
func Colorize(input string) string {
21
	var buf strings.Builder
22
	i := 0
23
	for i < len(input) {
24
		if input[i] != '<' {
25
			// Text content — no color
26
			end := strings.Index(input[i:], "<")
27
			if end == -1 {
28
				buf.WriteString(input[i:])
29
				break
30
			}
31
			buf.WriteString(input[i : i+end])
32
			i += end
33
			continue
34
		}
35
36
		// CDATA
37
		if strings.HasPrefix(input[i:], "<![CDATA[") {
38
			end := strings.Index(input[i:], "]]>")
39
			if end == -1 {
40
				buf.WriteString(gray)
41
				buf.WriteString(input[i:])
42
				buf.WriteString(reset)
43
				break
44
			}
45
			buf.WriteString(gray)
46
			buf.WriteString(input[i : i+end+3])
47
			buf.WriteString(reset)
48
			i += end + 3
49
			continue
50
		}
51
52
		// Comment
53
		if strings.HasPrefix(input[i:], "<!--") {
54
			end := strings.Index(input[i:], "-->")
55
			if end == -1 {
56
				buf.WriteString(gray)
57
				buf.WriteString(input[i:])
58
				buf.WriteString(reset)
59
				break
60
			}
61
			buf.WriteString(gray)
62
			buf.WriteString(input[i : i+end+3])
63
			buf.WriteString(reset)
64
			i += end + 3
65
			continue
66
		}
67
68
		// XML tag
69
		end := strings.Index(input[i:], ">")
70
		if end == -1 {
71
			buf.WriteString(input[i:])
72
			break
73
		}
74
		tag := input[i : i+end+1]
75
		buf.WriteString(colorizeTag(tag))
76
		i += end + 1
77
	}
78
	return buf.String()
79
}
80
81
func colorizeTag(tag string) string {
82
	var buf strings.Builder
83
84
	// Closing tag: </name>
85
	if strings.HasPrefix(tag, "</") {
86
		name := tag[2 : len(tag)-1]
87
		buf.WriteString(gray)
88
		buf.WriteString("</")
89
		buf.WriteString(reset)
90
		buf.WriteString(tagNameColor(name))
91
		buf.WriteString(name)
92
		buf.WriteString(reset)
93
		buf.WriteString(gray)
94
		buf.WriteString(">")
95
		buf.WriteString(reset)
96
		return buf.String()
97
	}
98
99
	// Opening or self-closing tag
100
	selfClosing := strings.HasSuffix(tag, "/>")
101
	inner := tag[1:]
102
	if selfClosing {
103
		inner = inner[:len(inner)-2]
104
	} else {
105
		inner = inner[:len(inner)-1]
106
	}
107
108
	// Split tag name from attributes
109
	nameEnd := strings.IndexAny(inner, " \t\n")
110
	var name, attrs string
111
	if nameEnd == -1 {
112
		name = inner
113
	} else {
114
		name = inner[:nameEnd]
115
		attrs = inner[nameEnd:]
116
	}
117
118
	buf.WriteString(gray)
119
	buf.WriteString("<")
120
	buf.WriteString(reset)
121
	buf.WriteString(tagNameColor(name))
122
	buf.WriteString(name)
123
	buf.WriteString(reset)
124
125
	if attrs != "" {
126
		buf.WriteString(colorizeAttrs(attrs))
127
	}
128
129
	if selfClosing {
130
		buf.WriteString(gray)
131
		buf.WriteString("/>")
132
		buf.WriteString(reset)
133
	} else {
134
		buf.WriteString(gray)
135
		buf.WriteString(">")
136
		buf.WriteString(reset)
137
	}
138
139
	return buf.String()
140
}
141
142
func tagNameColor(name string) string {
143
	lower := strings.ToLower(name)
144
	switch {
145
	case strings.HasPrefix(lower, "ac:"):
146
		return magenta
147
	case strings.HasPrefix(lower, "ri:"):
148
		return cyan
149
	default:
150
		return blue
151
	}
152
}
153
154
func colorizeAttrs(attrs string) string {
155
	var buf strings.Builder
156
	rest := attrs
157
	for len(rest) > 0 {
158
		// Find next attribute: name="value" or name='value'
159
		eqIdx := strings.Index(rest, "=")
160
		if eqIdx == -1 {
161
			// No more attributes, just whitespace or text
162
			buf.WriteString(rest)
163
			break
164
		}
165
166
		// Everything before = is whitespace + attr name
167
		before := rest[:eqIdx]
168
		rest = rest[eqIdx+1:]
169
170
		// Split leading whitespace from attr name
171
		trimmed := strings.TrimLeft(before, " \t\n")
172
		ws := before[:len(before)-len(trimmed)]
173
174
		buf.WriteString(ws)
175
		buf.WriteString(yellow)
176
		buf.WriteString(trimmed)
177
		buf.WriteString(reset)
178
		buf.WriteString(gray)
179
		buf.WriteString("=")
180
		buf.WriteString(reset)
181
182
		// Read quoted value
183
		if len(rest) > 0 && (rest[0] == '"' || rest[0] == '\'') {
184
			quote := rest[0]
185
			endQ := strings.IndexByte(rest[1:], quote)
186
			if endQ == -1 {
187
				buf.WriteString(green)
188
				buf.WriteString(rest)
189
				buf.WriteString(reset)
190
				break
191
			}
192
			buf.WriteString(green)
193
			buf.WriteString(rest[:endQ+2])
194
			buf.WriteString(reset)
195
			rest = rest[endQ+2:]
196
		}
197
	}
198
	return buf.String()
199
}
200

Source Files