300 lines
9.8 KiB
Go
300 lines
9.8 KiB
Go
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",
|
|
}
|