cat / 012
clone

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

main
012 / internal / fileio / htmlstyle.go
htmlstyle.go 7.8K
1package fileio23import (4 "embed"5 "encoding/base64"6 "fmt"7 "math"8 "strings"9 "sync"1011 "src.rest/cat/012/internal/chart"12 "src.rest/cat/012/internal/sheet"13)1415// HTML pages are drawn in 012's look: the 16 ANSI colors of the16// reference palettes the golden screens use (e2e/palette_test.go),17// mapped to the roles the terminal theme gives them18// (internal/ui/theme), dark or light as the reader's system prefers,19// in JetBrains Mono, the docs site's font, embedded so the page needs20// nothing else. One cell is htmlCellW by htmlCellH pixels, the size of21// a cell of an SVG chart, so charts sit on the grid as on the screen.2223const (24 htmlCellW = chart.SVGCellW25 htmlCellH = chart.SVGCellH26)2728//go:embed fonts/*.woff229var fonts embed.FS3031// htmlPalette is a reference palette and the roles drawn in it.32type htmlPalette struct {33 ansi [16]uint3234 bg, fg uint3235 grid uint32 // the faint lines between cells36 // ANSI slots of the roles.37 headerBg, headerFg, bar, muted, border, note, link int38 series [chart.Colors]int39}4041var htmlPalettes = [2]htmlPalette{{42 ansi: [16]uint32{0x1d1f21, 0xcc6666, 0xb5bd68, 0xf0c674, 0x81a2be, 0xb294bb, 0x8abeb7, 0xc5c8c6,43 0x666666, 0xd54e53, 0xb9ca4a, 0xe7c547, 0x7aa6da, 0xc397d8, 0x70c0b1, 0xeaeaea},44 bg: 0x1d1f21, fg: 0xc5c8c6, grid: 0x2c2e30,45 headerBg: 8, headerFg: 15, bar: 6, muted: 8, border: 7, note: 3, link: 12,46 series: [chart.Colors]int{6, 5, 3, 2, 12, 9},47}, {48 ansi: [16]uint32{0x1d1f21, 0xc82829, 0x718c00, 0xb58900, 0x4271ae, 0x8959a8, 0x3e999f, 0xd6d6d6,49 0x8e908c, 0xc82829, 0x718c00, 0xb58900, 0x4271ae, 0x8959a8, 0x3e999f, 0xffffff},50 bg: 0xfafafa, fg: 0x1d1f21, grid: 0xe8e8e8,51 headerBg: 7, headerFg: 0, bar: 4, muted: 0, border: 0, note: 5, link: 4,52 series: [chart.Colors]int{4, 5, 2, 1, 6, 8},53}}5455// ruleSlots are the ANSI slots of sheet.Color, as the theme maps them.56var ruleSlots = [sheet.NumColors]int{0, 1, 3, 2, 6, 4, 5}5758// vars writes the palette's CSS variables.59func (p htmlPalette) vars() string {60 var b strings.Builder61 hex := func(v uint32) string { return fmt.Sprintf("#%06x", v) }62 slot := func(i int) string { return hex(p.ansi[i]) }63 fmt.Fprintf(&b, "--bg:%s;--fg:%s;--grid:%s;--hbg:%s;--hfg:%s;--bar:%s;--muted:%s;--border:%s;--note:%s;--link:%s;--axis:%s;--acc:%s;--accfg:%s;",64 hex(p.bg), hex(p.fg), hex(p.grid), slot(p.headerBg), slot(p.headerFg), slot(p.bar), slot(p.muted),65 slot(p.border), slot(p.note), slot(p.link), slot(8), slot(6), slot(0))66 for i, s := range p.series {67 fmt.Fprintf(&b, "--s%d:%s;", i, slot(s))68 }69 for c := 1; c < sheet.NumColors; c++ {70 fill := p.ansi[ruleSlots[c]]71 fmt.Fprintf(&b, "--r%d:%s;--i%d:%s;", c, hex(fill), c, hex(p.ink(fill)))72 }73 return b.String()74}7576// ink is black or bright white, whichever reads better on bg.77func (p htmlPalette) ink(bg uint32) uint32 {78 if contrast(p.ansi[0], bg) >= contrast(p.ansi[15], bg) {79 return p.ansi[0]80 }81 return p.ansi[15]82}8384// contrast is the WCAG contrast ratio of two colors.85func contrast(a, b uint32) float64 {86 la, lb := luminance(a), luminance(b)87 return (max(la, lb) + 0.05) / (min(la, lb) + 0.05)88}8990func luminance(c uint32) float64 {91 ch := func(v uint32) float64 {92 s := float64(v&0xff) / 25593 if s <= 0.03928 {94 return s / 12.9295 }96 return math.Pow((s+0.055)/1.055, 2.4)97 }98 return 0.2126*ch(c>>16) + 0.7152*ch(c>>8) + 0.0722*ch(c)99}100101// htmlCSS is the style sheet every page and view shares, fonts102// included.103var htmlCSS = sync.OnceValue(func() string {104 var b strings.Builder105 for _, f := range []struct{ file, weight, style string }{106 {"jetbrains-mono-latin-400-normal.woff2", "400", "normal"},107 {"jetbrains-mono-latin-600-normal.woff2", "600", "normal"},108 {"jetbrains-mono-latin-400-italic.woff2", "400", "italic"},109 } {110 data, _ := fonts.ReadFile("fonts/" + f.file)111 fmt.Fprintf(&b, "@font-face{font-family:'JetBrains Mono';font-weight:%s;font-style:%s;src:url(data:font/woff2;base64,%s) format('woff2')}\n",112 f.weight, f.style, base64.StdEncoding.EncodeToString(data))113 }114 dark, light := htmlPalettes[0].vars(), htmlPalettes[1].vars()115 fmt.Fprintf(&b, ":root{%s}\n@media (prefers-color-scheme:light){:root:not(.dark){%s}}\n:root.light{%s}\n", dark, light, light)116 b.WriteString(htmlRules)117 return b.String()118})119120// HTMLStyle is the style sheet of 012's HTML pages, for a page built121// around HTMLGrid or HTMLChart; the root element takes the class dark122// or light to choose a palette over the reader's preference.123func HTMLStyle() string { return htmlCSS() }124125// htmlRules are the page's rules, after its fonts and palettes.126const htmlRules = `*{box-sizing:border-box}127html,body{margin:0;background:var(--bg);color:var(--fg)}128body{font:15px/21px 'JetBrains Mono',ui-monospace,'SF Mono',Menlo,Consolas,monospace;font-variant-ligatures:none}129.o12{min-width:100%;width:max-content}130.panel{position:sticky;left:0;top:0;z-index:3;background:var(--bg);white-space:pre;width:100vw;max-width:100%}131.panel>div{height:21px;display:flex;gap:9px;overflow:hidden}132.panel .book{flex:1;overflow:hidden;text-overflow:ellipsis}133.mode{background:var(--acc);color:var(--accfg);font-weight:600;padding:0 9px}134.namebox{background:var(--hbg);color:var(--hfg);min-width:90px;padding:0 9px}135.input{flex:1;overflow:hidden;text-overflow:ellipsis}136.context{color:var(--muted)}137.sheet{position:relative}138table.grid{border-collapse:collapse;table-layout:fixed;width:max-content}139.grid th{background:var(--hbg);color:var(--hfg);font-weight:400;height:21px;padding:0;text-align:center;position:sticky;z-index:1}140.grid thead th{top:63px}141.grid tbody th{left:0;text-align:right;padding-right:9px;vertical-align:bottom}142.grid thead th.corner{left:0;z-index:2}143.grid th.on{background:var(--acc);color:var(--accfg);font-weight:600}144.grid td{height:21px;padding:0 4px;white-space:pre;overflow:visible;box-shadow:inset -1px -1px 0 var(--grid);vertical-align:bottom;position:relative}145.grid td.clip{overflow:hidden}146.grid td.wrap{white-space:pre-wrap;overflow-wrap:anywhere}147.grid td.r{text-align:right}.grid td.c{text-align:center}.grid td.l{text-align:left}148.grid td.t{vertical-align:top}.grid td.m{vertical-align:middle}149.grid td.b{font-weight:600}.grid td.i{font-style:italic}150.grid td.u{text-decoration:underline}.grid td.s{text-decoration:line-through}.grid td.u.s{text-decoration:underline line-through}151.grid td.err{text-decoration:underline wavy var(--r1)}152.grid td.bad{text-decoration:underline dotted var(--r2)}153.grid td.th{font-weight:600;text-decoration:underline;color:var(--bar)}154.grid td.band{background:var(--hbg);color:var(--hfg)}155.grid td.note::after{content:'\259D';position:absolute;right:0;top:-3px;color:var(--note);font-size:12px}156.grid td.ptr{outline:2px solid var(--acc);outline-offset:-2px}157.grid a{color:var(--link)}158.grid .icon{margin-right:9px}159figure.chart{position:absolute;margin:0;border:1px solid var(--axis);background:var(--bg);padding:20px 17px;z-index:1}160figure.chart figcaption{position:absolute;top:-11px;left:9px;background:var(--bg);padding:0 9px;font-weight:600;white-space:pre}161figure.chart .range{position:absolute;bottom:-11px;right:9px;background:var(--bg);padding:0 9px;color:var(--muted);white-space:pre}162figure.solo{position:relative;display:inline-block;margin:21px 18px}163svg.chart{display:block;overflow:visible;font:15px 'JetBrains Mono',ui-monospace,monospace}164.ct-ax{fill:var(--axis);color:var(--axis)}.ct-lb{fill:var(--muted);color:var(--muted)}.ct-mu{fill:var(--muted);color:var(--muted)}165.cs-box{stroke:currentColor;stroke-width:1}.cs-gl{stroke:var(--grid);stroke-width:1}.cs-gap{stroke:var(--bg);stroke-width:1.5}166.ct-s0,.cf-0{fill:var(--s0)}.ct-s1,.cf-1{fill:var(--s1)}.ct-s2,.cf-2{fill:var(--s2)}.ct-s3,.cf-3{fill:var(--s3)}.ct-s4,.cf-4{fill:var(--s4)}.ct-s5,.cf-5{fill:var(--s5)}167.cs-0{stroke:var(--s0)}.cs-1{stroke:var(--s1)}.cs-2{stroke:var(--s2)}.cs-3{stroke:var(--s3)}.cs-4{stroke:var(--s4)}.cs-5{stroke:var(--s5)}168.status{white-space:pre;color:var(--muted);padding:0 9px;height:21px}169`