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:], "") 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:], "") 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: if strings.HasPrefix(tag, "") 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() }