package logging import ( "errors" "fmt" "time" "github.com/rs/zerolog" "github.com/yusing/go-proxy/internal/common" ) var levelHTMLFormats = [][]byte{ []byte(` TRC `), []byte(` DBG `), []byte(` INF `), []byte(` WRN `), []byte(` ERR `), []byte(` FTL `), []byte(` PAN `), } var colorToClass = map[string]string{ "1": "log-bold", "3": "log-italic", "4": "log-underline", "30": "log-black", "31": "log-red", "32": "log-green", "33": "log-yellow", "34": "log-blue", "35": "log-magenta", "36": "log-cyan", "37": "log-white", "90": "log-bright-black", "91": "log-red", "92": "log-bright-green", "93": "log-bright-yellow", "94": "log-bright-blue", "95": "log-bright-magenta", "96": "log-bright-cyan", "97": "log-bright-white", } // FormatMessageToHTMLBytes converts text with ANSI color codes to HTML with class names. // ANSI codes are mapped to classes via a static map, and reset codes ([0m) close all spans. // Time complexity is O(n) with minimal allocations. func FormatMessageToHTMLBytes(msg string, buf []byte) ([]byte, error) { buf = append(buf, ""...) var stack []string lastPos := 0 for i := 0; i < len(msg); { if msg[i] == '\x1b' && i+1 < len(msg) && msg[i+1] == '[' { if lastPos < i { escapeAndAppend(msg[lastPos:i], &buf) } i += 2 // Skip \x1b[ start := i for ; i < len(msg) && msg[i] != 'm'; i++ { if !isANSICodeChar(msg[i]) { return nil, fmt.Errorf("invalid ANSI char: %c", msg[i]) } } if i >= len(msg) { return nil, errors.New("unterminated ANSI sequence") } codeStr := msg[start:i] i++ // Skip 'm' lastPos = i startPart := 0 for j := 0; j <= len(codeStr); j++ { if j == len(codeStr) || codeStr[j] == ';' { part := codeStr[startPart:j] if part == "" { return nil, errors.New("empty code part") } if part == "0" { for range stack { buf = append(buf, ""...) } stack = stack[:0] } else { className, ok := colorToClass[part] if !ok { return nil, fmt.Errorf("invalid ANSI code: %s", part) } stack = append(stack, className) buf = append(buf, ``...) } startPart = j + 1 } } } else { i++ } } if lastPos < len(msg) { escapeAndAppend(msg[lastPos:], &buf) } for range stack { buf = append(buf, ""...) } buf = append(buf, ""...) return buf, nil } func isANSICodeChar(c byte) bool { return (c >= '0' && c <= '9') || c == ';' } func escapeAndAppend(s string, buf *[]byte) { for i, r := range s { switch r { case '•': *buf = append(*buf, "·"...) case '&': *buf = append(*buf, "&"...) case '<': *buf = append(*buf, "<"...) case '>': *buf = append(*buf, ">"...) case '\t': *buf = append(*buf, " "...) case '\n': *buf = append(*buf, "
"...) *buf = append(*buf, prefixHTML...) default: *buf = append(*buf, s[i]) } } } func timeNowHTML() []byte { if !common.IsTest { return []byte(time.Now().Format(timeFmt)) } return []byte(time.Date(2024, 1, 1, 1, 1, 1, 1, time.UTC).Format(timeFmt)) } func FormatLogEntryHTML(level zerolog.Level, message string, buf []byte) []byte { buf = append(buf, []byte(`
`)...)
	buf = append(buf, timeNowHTML()...)
	if level < zerolog.NoLevel {
		buf = append(buf, levelHTMLFormats[level+1]...)
	}
	buf, _ = FormatMessageToHTMLBytes(message, buf)
	buf = append(buf, []byte("
")...) return buf }