package request
import (
"bytes"
"encoding/json"
"strings"
"unicode/utf8"
"mercemay.top/httptap/internal/parser"
"mercemay.top/httptap/internal/parser/content"
"mercemay.top/httptap/internal/tui/theme"
)
// RenderBody produces a display string for the message body. JSON is
// pretty-printed, text/* is shown verbatim, binary is rendered as a
// compact hex dump.
func RenderBody(pal theme.Palette, msg parser.Message) string {
body := msg.Body
if enc := parser.Header(msg, "Content-Encoding"); enc != "" {
if decoded, err := content.Decode(body, enc); err == nil {
body = decoded
}
}
if len(body) == 0 {
return pal.Detail.Render("(empty body)")
}
ct := parser.Header(msg, "Content-Type")
switch {
case strings.Contains(ct, "application/json"):
return prettyJSON(body)
case strings.HasPrefix(ct, "text/"),
strings.Contains(ct, "application/javascript"),
strings.Contains(ct, "application/xml"):
return string(body)
case utf8.Valid(body) && !isMostlyControl(body):
return string(body)
default:
return hexDump(body)
}
}
// prettyJSON calls json.Indent. On failure it returns the raw string so
// the UI never looks blank.
func prettyJSON(body []byte) string {
var buf bytes.Buffer
if err := json.Indent(&buf, body, "", " "); err != nil {
return string(body)
}
return buf.String()
}
// hexDump is a compact xxd-style dump with 16 bytes per line.
func hexDump(body []byte) string {
const width = 16
var b strings.Builder
for i := 0; i < len(body); i += width {
end := i + width
if end > len(body) {
end = len(body)
}
line := body[i:end]
b.WriteString(offsetHex(i))
b.WriteString(" ")
for j := 0; j < width; j++ {
if j < len(line) {
b.WriteString(byteHex(line[j]))
} else {
b.WriteString(" ")
}
if j == 7 {
b.WriteByte(' ')
}
}
b.WriteString(" |")
for _, c := range line {
if c >= 0x20 && c < 0x7f {
b.WriteByte(c)
} else {
b.WriteByte('.')
}
}
b.WriteString("|\n")
}
return strings.TrimRight(b.String(), "\n")
}
func offsetHex(n int) string {
const digits = "0123456789abcdef"
var out [8]byte
for i := 7; i >= 0; i-- {
out[i] = digits[n&0xf]
n >>= 4
}
return string(out[:])
}
func byteHex(b byte) string {
const digits = "0123456789abcdef"
return string([]byte{digits[b>>4], digits[b&0xf], ' '})
}
func isMostlyControl(b []byte) bool {
n := 0
for _, c := range b {
if c < 0x09 || (c > 0x0d && c < 0x20) {
n++
}
}
return len(b) > 0 && n*4 > len(b)
}