package freezelib import ( "fmt" "strings" "github.com/beevik/etree" "github.com/charmbracelet/x/ansi" "github.com/mattn/go-runewidth" ) // dispatcher handles ANSI escape sequences and converts them to SVG type dispatcher struct { lines []*etree.Element svg *etree.Element config *Config scale float64 row int col int bg *etree.Element bgWidth int } // newDispatcher creates a new ANSI dispatcher func newDispatcher(lines []*etree.Element, svg *etree.Element, config *Config, scale float64) *dispatcher { return &dispatcher{ lines: lines, svg: svg, config: config, scale: scale, row: 0, col: 0, } } // Print handles printable characters func (p *dispatcher) Print(r rune) { p.row = clamp(p.row, 0, len(p.lines)-1) // insert the rune in the last tspan children := p.lines[p.row].ChildElements() var lastChild *etree.Element isFirstChild := len(children) == 0 if isFirstChild { lastChild = etree.NewElement("tspan") lastChild.CreateAttr("xml:space", "preserve") p.lines[p.row].AddChild(lastChild) } else { lastChild = children[len(children)-1] } if runewidth.RuneWidth(r) > 1 { newChild := lastChild.Copy() newChild.SetText(string(r)) newChild.CreateAttr("dx", fmt.Sprintf("%.2fpx", (p.config.Font.Size/5)*p.scale)) p.lines[p.row].AddChild(newChild) } else { lastChild.SetText(lastChild.Text() + string(r)) } p.col += runewidth.RuneWidth(r) if p.bg != nil { p.bgWidth += runewidth.RuneWidth(r) } } // Execute handles control characters func (p *dispatcher) Execute(code byte) { if code == '\t' { for p.col%16 != 0 { p.Print(' ') } } if code == '\n' { p.endBackground() p.row++ p.col = 0 } } // endBackground ends the current background span func (p *dispatcher) endBackground() { if p.bg == nil { return } p.bg.CreateAttr("width", fmt.Sprintf("%.2fpx", float64(p.bgWidth)*(p.config.Font.Size/fontHeightToWidthRatio)*p.scale)) p.bg = nil p.bgWidth = 0 } // CsiDispatch handles CSI (Control Sequence Introducer) sequences func (p *dispatcher) CsiDispatch(cmd ansi.Cmd, params ansi.Params) { if cmd != 'm' { // ignore incomplete or non Style (SGR) sequences return } span := etree.NewElement("tspan") span.CreateAttr("xml:space", "preserve") reset := func() { // reset ANSI, this is done by creating a new empty tspan, // which would reset all the styles such that when text is appended to the last // child of this line there is no styling applied. if p.row < len(p.lines) { p.lines[p.row].AddChild(span) } p.endBackground() } if len(params) == 0 { // zero params means reset reset() return } var i int for i < len(params) { v := params[i].Param(0) switch v { case 0: reset() case 1: // Bold - not implemented in SVG for now p.lines[p.row].AddChild(span) case 9: span.CreateAttr("text-decoration", "line-through") p.lines[p.row].AddChild(span) case 3: span.CreateAttr("font-style", "italic") p.lines[p.row].AddChild(span) case 4: span.CreateAttr("text-decoration", "underline") p.lines[p.row].AddChild(span) case 30, 31, 32, 33, 34, 35, 36, 37, 90, 91, 92, 93, 94, 95, 96, 97: span.CreateAttr("fill", ansiPalette[v]) p.lines[p.row].AddChild(span) case 38: i++ if i < len(params) { switch params[i].Param(0) { case 5: if i+1 < len(params) { n := params[i+1].Param(0) i++ fill := palette[n] span.CreateAttr("fill", fill) p.lines[p.row].AddChild(span) } case 2: if i+3 < len(params) { r := params[i+1].Param(0) g := params[i+2].Param(0) b := params[i+3].Param(0) i += 3 fill := fmt.Sprintf("rgb(%d,%d,%d)", r, g, b) span.CreateAttr("fill", fill) p.lines[p.row].AddChild(span) } } } case 40, 41, 42, 43, 44, 45, 46, 47, 100, 101, 102, 103, 104, 105, 106, 107: // Background colors p.endBackground() p.bg = etree.NewElement("rect") p.bg.CreateAttr("fill", ansiPalette[v-10]) p.bg.CreateAttr("height", fmt.Sprintf("%.2fpx", p.config.Font.Size*p.config.LineHeight)) p.bg.CreateAttr("x", fmt.Sprintf("%.2fpx", float64(p.col)*(p.config.Font.Size/fontHeightToWidthRatio)*p.scale)) p.bg.CreateAttr("y", fmt.Sprintf("%.2fpx", float64(p.row)*p.config.Font.Size*p.config.LineHeight)) p.svg.InsertChildAt(0, p.bg) case 48: i++ if i < len(params) { switch params[i].Param(0) { case 5: if i+1 < len(params) { n := params[i+1].Param(0) i++ p.endBackground() p.bg = etree.NewElement("rect") p.bg.CreateAttr("fill", palette[n]) p.bg.CreateAttr("height", fmt.Sprintf("%.2fpx", p.config.Font.Size*p.config.LineHeight)) p.bg.CreateAttr("x", fmt.Sprintf("%.2fpx", float64(p.col)*(p.config.Font.Size/fontHeightToWidthRatio)*p.scale)) p.bg.CreateAttr("y", fmt.Sprintf("%.2fpx", float64(p.row)*p.config.Font.Size*p.config.LineHeight)) p.svg.InsertChildAt(0, p.bg) } case 2: if i+3 < len(params) { r := params[i+1].Param(0) g := params[i+2].Param(0) b := params[i+3].Param(0) i += 3 p.endBackground() p.bg = etree.NewElement("rect") p.bg.CreateAttr("fill", fmt.Sprintf("rgb(%d,%d,%d)", r, g, b)) p.bg.CreateAttr("height", fmt.Sprintf("%.2fpx", p.config.Font.Size*p.config.LineHeight)) p.bg.CreateAttr("x", fmt.Sprintf("%.2fpx", float64(p.col)*(p.config.Font.Size/fontHeightToWidthRatio)*p.scale)) p.bg.CreateAttr("y", fmt.Sprintf("%.2fpx", float64(p.row)*p.config.Font.Size*p.config.LineHeight)) p.svg.InsertChildAt(0, p.bg) } } } } i++ } } // processANSI processes ANSI escape sequences in the input text func processANSI(input string, lines []*etree.Element, svg *etree.Element, config *Config, scale float64) { d := newDispatcher(lines, svg, config, scale) parser := ansi.NewParser() parser.SetHandler(ansi.Handler{ Print: d.Print, HandleCsi: d.CsiDispatch, Execute: d.Execute, }) for _, line := range strings.Split(input, "\n") { parser.Parse([]byte(line)) d.Execute(ansi.LF) // simulate a newline } } // stripANSI removes ANSI escape sequences from text func stripANSI(input string) string { return ansi.Strip(input) } // isANSI checks if the input contains ANSI escape sequences func isANSI(input string) bool { return stripANSI(input) != input } // clamp constrains a value between min and max func clamp(value, min, max int) int { if value < min { return min } if value > max { return max } return value } const fontHeightToWidthRatio = 1.68 // ANSI color palette var ansiPalette = map[int]string{ 30: "#000000", // black 31: "#FF0000", // red 32: "#00FF00", // green 33: "#FFFF00", // yellow 34: "#0000FF", // blue 35: "#FF00FF", // magenta 36: "#00FFFF", // cyan 37: "#FFFFFF", // white 90: "#808080", // bright black (gray) 91: "#FF8080", // bright red 92: "#80FF80", // bright green 93: "#FFFF80", // bright yellow 94: "#8080FF", // bright blue 95: "#FF80FF", // bright magenta 96: "#80FFFF", // bright cyan 97: "#FFFFFF", // bright white } // 256-color palette var palette = []string{ "#000000", "#800000", "#008000", "#808000", "#000080", "#800080", "#008080", "#c0c0c0", "#808080", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff", "#000000", "#00005f", "#000087", "#0000af", "#0000d7", "#0000ff", "#005f00", "#005f5f", "#005f87", "#005faf", "#005fd7", "#005fff", "#008700", "#00875f", "#008787", "#0087af", "#0087d7", "#0087ff", "#00af00", "#00af5f", "#00af87", "#00afaf", "#00afd7", "#00afff", "#00d700", "#00d75f", "#00d787", "#00d7af", "#00d7d7", "#00d7ff", "#00ff00", "#00ff5f", "#00ff87", "#00ffaf", "#00ffd7", "#00ffff", "#5f0000", "#5f005f", "#5f0087", "#5f00af", "#5f00d7", "#5f00ff", "#5f5f00", "#5f5f5f", "#5f5f87", "#5f5faf", "#5f5fd7", "#5f5fff", "#5f8700", "#5f875f", "#5f8787", "#5f87af", "#5f87d7", "#5f87ff", "#5faf00", "#5faf5f", "#5faf87", "#5fafaf", "#5fafd7", "#5fafff", "#5fd700", "#5fd75f", "#5fd787", "#5fd7af", "#5fd7d7", "#5fd7ff", "#5fff00", "#5fff5f", "#5fff87", "#5fffaf", "#5fffd7", "#5fffff", "#870000", "#87005f", "#870087", "#8700af", "#8700d7", "#8700ff", "#875f00", "#875f5f", "#875f87", "#875faf", "#875fd7", "#875fff", "#878700", "#87875f", "#878787", "#8787af", "#8787d7", "#8787ff", "#87af00", "#87af5f", "#87af87", "#87afaf", "#87afd7", "#87afff", "#87d700", "#87d75f", "#87d787", "#87d7af", "#87d7d7", "#87d7ff", "#87ff00", "#87ff5f", "#87ff87", "#87ffaf", "#87ffd7", "#87ffff", "#af0000", "#af005f", "#af0087", "#af00af", "#af00d7", "#af00ff", "#af5f00", "#af5f5f", "#af5f87", "#af5faf", "#af5fd7", "#af5fff", "#af8700", "#af875f", "#af8787", "#af87af", "#af87d7", "#af87ff", "#afaf00", "#afaf5f", "#afaf87", "#afafaf", "#afafd7", "#afafff", "#afd700", "#afd75f", "#afd787", "#afd7af", "#afd7d7", "#afd7ff", "#afff00", "#afff5f", "#afff87", "#afffaf", "#afffd7", "#afffff", "#d70000", "#d7005f", "#d70087", "#d700af", "#d700d7", "#d700ff", "#d75f00", "#d75f5f", "#d75f87", "#d75faf", "#d75fd7", "#d75fff", "#d78700", "#d7875f", "#d78787", "#d787af", "#d787d7", "#d787ff", "#d7af00", "#d7af5f", "#d7af87", "#d7afaf", "#d7afd7", "#d7afff", "#d7d700", "#d7d75f", "#d7d787", "#d7d7af", "#d7d7d7", "#d7d7ff", "#d7ff00", "#d7ff5f", "#d7ff87", "#d7ffaf", "#d7ffd7", "#d7ffff", "#ff0000", "#ff005f", "#ff0087", "#ff00af", "#ff00d7", "#ff00ff", "#ff5f00", "#ff5f5f", "#ff5f87", "#ff5faf", "#ff5fd7", "#ff5fff", "#ff8700", "#ff875f", "#ff8787", "#ff87af", "#ff87d7", "#ff87ff", "#ffaf00", "#ffaf5f", "#ffaf87", "#ffafaf", "#ffafd7", "#ffafff", "#ffd700", "#ffd75f", "#ffd787", "#ffd7af", "#ffd7d7", "#ffd7ff", "#ffff00", "#ffff5f", "#ffff87", "#ffffaf", "#ffffd7", "#ffffff", "#080808", "#121212", "#1c1c1c", "#262626", "#303030", "#3a3a3a", "#444444", "#4e4e4e", "#585858", "#626262", "#6c6c6c", "#767676", "#808080", "#8a8a8a", "#949494", "#9e9e9e", "#a8a8a8", "#b2b2b2", "#bcbcbc", "#c6c6c6", "#d0d0d0", "#dadada", "#e4e4e4", "#eeeeee", }