cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / e2e / stillcells_test.go
stillcells_test.go 4.4K
1package e2e23import (4 "fmt"5 "html"6 "image/color"7 "strconv"8 "strings"910 "golang.org/x/text/width"11)1213// A golden screen read back into cells, for drawing it as a still. The14// HTML is what renderHTML writes: one line of text per row, runs of15// cells with the same style in a span, colors as palette variables.1617// ink is a color as the golden names it: the terminal's default18// foreground or background, a palette entry or an RGB value.19type ink struct {20 kind inkKind21 palette int22 rgb color.RGBA23}2425type inkKind uint82627const (28 inkNone inkKind = iota // the default: fg or bg as the slot says29 inkFg30 inkBg31 inkPalette32 inkRGB33)3435// underline styles, as the golden's text-decoration names them.36const (37 ulNone = iota38 ulSingle39 ulDouble40 ulWavy41 ulDotted42 ulDashed43)4445// stillCell is one cell of the screen.46type stillCell struct {47 r rune // 0 for the second half of a wide character48 fg, bg, ulInk ink49 bold, italic, faint bool50 underline int51 strike, wide, filled bool52}5354// parseScreen reads a golden's HTML into rows of cols cells.55func parseScreen(src string, cols, rows int) ([][]stillCell, error) {56 src = strings.TrimPrefix(src, `<pre class="screen">`)57 src = strings.TrimSuffix(src, "</pre>")58 grid := make([][]stillCell, rows)59 for y := range grid {60 grid[y] = make([]stillCell, cols)61 }62 for y, line := range strings.Split(src, "\n") {63 if y >= rows {64 return nil, fmt.Errorf("more than %d rows", rows)65 }66 if err := parseLine(grid[y], line); err != nil {67 return nil, fmt.Errorf("row %d: %w", y, err)68 }69 }70 return grid, nil71}7273// parseLine fills row from one line of the golden.74func parseLine(row []stillCell, line string) error {75 x := 076 var st stillCell77 for line != "" {78 switch {79 case strings.HasPrefix(line, `<span style="`):80 end := strings.Index(line, `">`)81 if end < 0 {82 return fmt.Errorf("unclosed span")83 }84 st = styleOf(line[len(`<span style="`):end])85 line = line[end+2:]86 case strings.HasPrefix(line, "</span>"):87 st = stillCell{}88 line = line[len("</span>"):]89 default:90 end := strings.IndexByte(line, '<')91 if end < 0 {92 end = len(line)93 }94 var err error95 if x, err = putText(row, x, html.UnescapeString(line[:end]), st); err != nil {96 return err97 }98 line = line[end:]99 }100 }101 return nil102}103104// putText writes text into row from column x in style st and returns105// the column after it.106func putText(row []stillCell, x int, text string, st stillCell) (int, error) {107 for _, r := range text {108 w := 1109 if k := width.LookupRune(r).Kind(); k == width.EastAsianWide || k == width.EastAsianFullwidth {110 w = 2111 }112 if x+w > len(row) {113 return x, fmt.Errorf("wider than %d columns", len(row))114 }115 c := st116 c.r, c.wide, c.filled = r, w == 2, true117 row[x] = c118 if w == 2 {119 row[x+1] = st120 row[x+1].filled = true121 }122 x += w123 }124 return x, nil125}126127// styleOf reads a span's CSS.128func styleOf(css string) stillCell {129 var c stillCell130 for _, decl := range strings.Split(css, ";") {131 prop, val, _ := strings.Cut(decl, ":")132 switch prop {133 case "color":134 c.fg = inkOf(val)135 case "background":136 c.bg = inkOf(val)137 case "font-weight":138 c.bold = val == "bold"139 case "font-style":140 c.italic = val == "italic"141 case "opacity":142 c.faint = true143 case "text-decoration":144 decorate(&c, val)145 }146 }147 return c148}149150// decorate reads a text-decoration: underline with its style and color,151// or line-through.152func decorate(c *stillCell, val string) {153 f := strings.Fields(val)154 if len(f) == 0 {155 return156 }157 if f[0] == "line-through" {158 c.strike = true159 return160 }161 c.underline = ulSingle162 for _, w := range f[1:] {163 switch w {164 case "double":165 c.underline = ulDouble166 case "wavy":167 c.underline = ulWavy168 case "dotted":169 c.underline = ulDotted170 case "dashed":171 c.underline = ulDashed172 default:173 c.ulInk = inkOf(w)174 }175 }176}177178// inkOf reads a CSS color as the goldens write them.179func inkOf(v string) ink {180 switch {181 case v == "var(--fg)":182 return ink{kind: inkFg}183 case v == "var(--bg)":184 return ink{kind: inkBg}185 case strings.HasPrefix(v, "var(--vt-palette-"):186 n, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(v, "var(--vt-palette-"), ")"))187 return ink{kind: inkPalette, palette: n}188 case strings.HasPrefix(v, "#") && len(v) == 7:189 n, _ := strconv.ParseUint(v[1:], 16, 32)190 return ink{kind: inkRGB, rgb: rgba(uint32(n))}191 }192 return ink{}193}194195func rgba(v uint32) color.RGBA {196 return color.RGBA{R: uint8(v >> 16), G: uint8(v >> 8), B: uint8(v), A: 0xff}197}