package format
import (
"strings"
)
// ANSI color codes.
const (
reset = "\033[0m"
red = "\033[31m"
green = "\033[32m"
yellow = "\033[33m"
blue = "\033[34m"
magenta = "\033[35m"
cyan = "\033[36m"
gray = "\033[90m"
)
// Colorize applies syntax highlighting to formatted Confluence XML.
func Colorize(input string) string {
var buf strings.Builder
i := 0
for i < len(input) {
if input[i] != '<' {
// Text content — no color
end := strings.Index(input[i:], "<")
if end == -1 {
buf.WriteString(input[i:])
break
}
buf.WriteString(input[i : i+end])
i += end
continue
}
// CDATA
if strings.HasPrefix(input[i:], "<![CDATA[") {
end := strings.Index(input[i:], "]]>")
if end == -1 {
buf.WriteString(gray)
buf.WriteString(input[i:])
buf.WriteString(reset)
break
}
buf.WriteString(gray)
buf.WriteString(input[i : i+end+3])
buf.WriteString(reset)
i += end + 3
continue
}
// Comment
if strings.HasPrefix(input[i:], "<!--") {
end := strings.Index(input[i:], "-->")
if end == -1 {
buf.WriteString(gray)
buf.WriteString(input[i:])
buf.WriteString(reset)
break
}
buf.WriteString(gray)
buf.WriteString(input[i : i+end+3])
buf.WriteString(reset)
i += end + 3
continue
}
// XML tag
end := strings.Index(input[i:], ">")
if end == -1 {
buf.WriteString(input[i:])
break
}
tag := input[i : i+end+1]
buf.WriteString(colorizeTag(tag))
i += end + 1
}
return buf.String()
}
func colorizeTag(tag string) string {
var buf strings.Builder
// Closing tag: </name>
if strings.HasPrefix(tag, "</") {
name := tag[2 : len(tag)-1]
buf.WriteString(gray)
buf.WriteString("</")
buf.WriteString(reset)
buf.WriteString(tagNameColor(name))
buf.WriteString(name)
buf.WriteString(reset)
buf.WriteString(gray)
buf.WriteString(">")
buf.WriteString(reset)
return buf.String()
}
// Opening or self-closing tag
selfClosing := strings.HasSuffix(tag, "/>")
inner := tag[1:]
if selfClosing {
inner = inner[:len(inner)-2]
} else {
inner = inner[:len(inner)-1]
}
// Split tag name from attributes
nameEnd := strings.IndexAny(inner, " \t\n")
var name, attrs string
if nameEnd == -1 {
name = inner
} else {
name = inner[:nameEnd]
attrs = inner[nameEnd:]
}
buf.WriteString(gray)
buf.WriteString("<")
buf.WriteString(reset)
buf.WriteString(tagNameColor(name))
buf.WriteString(name)
buf.WriteString(reset)
if attrs != "" {
buf.WriteString(colorizeAttrs(attrs))
}
if selfClosing {
buf.WriteString(gray)
buf.WriteString("/>")
buf.WriteString(reset)
} else {
buf.WriteString(gray)
buf.WriteString(">")
buf.WriteString(reset)
}
return buf.String()
}
func tagNameColor(name string) string {
lower := strings.ToLower(name)
switch {
case strings.HasPrefix(lower, "ac:"):
return magenta
case strings.HasPrefix(lower, "ri:"):
return cyan
default:
return blue
}
}
func colorizeAttrs(attrs string) string {
var buf strings.Builder
rest := attrs
for len(rest) > 0 {
// Find next attribute: name="value" or name='value'
eqIdx := strings.Index(rest, "=")
if eqIdx == -1 {
// No more attributes, just whitespace or text
buf.WriteString(rest)
break
}
// Everything before = is whitespace + attr name
before := rest[:eqIdx]
rest = rest[eqIdx+1:]
// Split leading whitespace from attr name
trimmed := strings.TrimLeft(before, " \t\n")
ws := before[:len(before)-len(trimmed)]
buf.WriteString(ws)
buf.WriteString(yellow)
buf.WriteString(trimmed)
buf.WriteString(reset)
buf.WriteString(gray)
buf.WriteString("=")
buf.WriteString(reset)
// Read quoted value
if len(rest) > 0 && (rest[0] == '"' || rest[0] == '\'') {
quote := rest[0]
endQ := strings.IndexByte(rest[1:], quote)
if endQ == -1 {
buf.WriteString(green)
buf.WriteString(rest)
buf.WriteString(reset)
break
}
buf.WriteString(green)
buf.WriteString(rest[:endQ+2])
buf.WriteString(reset)
rest = rest[endQ+2:]
}
}
return buf.String()
}