File: vida/config.go 1 package main 2 3 import ( 4 "errors" 5 "flag" 6 "io" 7 "strings" 8 ) 9 10 const ( 11 mb = 1024 * 1024 12 gb = 1024 * mb 13 14 lineBufferLimit = 8 * gb 15 ) 16 17 const ( 18 // columnGap is how many spaces show between adjacent output-columns 19 columnGap = 2 20 21 // bullet is the symbol used to show tallies 22 bullet = `•` 23 24 // emptyTile is used to show empty row values 25 emptyTile = `○` 26 27 // usualTile is ANSI-styled to show non-empty row values, its color 28 // telling various kinds of values apart 29 usualTile = `■` 30 ) 31 32 const ( 33 resetStyleANSI = "\x1b[0m" 34 35 altDigitGroupStyleANSI = "\x1b[38;5;249m" 36 37 swappedStyleANSI = "\x1b[7m" 38 39 // emptyStyleANSI uses a background-color style, and thus forces the 40 // tile-display logic to reset the style after each tile, to avoid 41 // this background from carrying over all later tiles 42 // emptyStyleANSI = "\x1b[48;5;252m" 43 44 emptyStyleANSI = "\x1b[0m" 45 46 paddedStyleANSI = "\x1b[38;5;226m" 47 normalStyleANSI = "\x1b[38;5;244m" 48 49 positiveDecimalStyleANSI = "\x1b[38;5;29m" 50 positiveIntegerStyleANSI = "\x1b[38;5;22m" 51 negativeDecimalStyleANSI = "\x1b[38;5;167m" 52 negativeIntegerStyleANSI = "\x1b[38;5;88m" 53 zeroStyleANSI = "\x1b[38;5;33m" 54 55 // emptyStyleANSI = "\x1b[48;5;248m" 56 // positiveIntegerStyleANSI = "\x1b[38;5;2m" 57 // positiveIntegerStyleANSI = "\x1b[38;5;28m" 58 // negativeDecimalStyleANSI = "\x1b[38;5;166m" 59 // negativeIntegerStyleANSI = "\x1b[38;5;168m" 60 // linkStyleANSI = "\x1b[38;5;75m" 61 62 // linkStyleANSI = "\x1b[38;5;33m" 63 ) 64 65 const ( 66 // swappedClassHTML = `` 67 68 emptyClassHTML = `empty` 69 70 paddedClassHTML = `padded` 71 normalClassHTML = `` 72 73 positiveDecimalClassHTML = `pos` 74 positiveIntegerClassHTML = `pos` 75 negativeDecimalClassHTML = `neg` 76 negativeIntegerClassHTML = `neg` 77 zeroClassHTML = `zero` 78 ) 79 80 const ( 81 // jsonLevelIndent is how many spaces to use for each nesting-level when 82 // showing JSON data 83 jsonLevelIndent = 2 84 85 nullStyleANSI = "\x1b[38;5;243m" 86 boolStyleANSI = "\x1b[38;5;74m" 87 keyStyleANSI = "\x1b[38;5;99m" 88 stringStyleANSI = "\x1b[38;5;24m" 89 syntaxStyleANSI = "\x1b[38;5;249m" 90 91 // stringStyleANSI = resetStyleANSI 92 ) 93 94 const ( 95 inputUsage = `input type: can be csv, tsv, or json; ` + 96 `only needed when filepath extensions don't help or when reading from stdin` 97 outputUsage = `output type: can be ansi, plain, text, html, json, or jsons` 98 colorsUsage = `which (if any) column to color-code by unique values` 99 altDigitsUsage = `style/color name to use for groups of alternating digits` 100 linksUsage = `style/color name to use for detected (hyper)links` 101 bulletsUsage = `which (if any) column to use to show bullet-talllies` 102 tilesUsage = `whether to show tiles for table rows; used for ANSI output` 103 titleUsage = `the (optional) title to use for HTML output` 104 ) 105 106 // config is this app's general configuration, parsed from cmd-line args 107 type config struct { 108 // From is the input-format to read the output as: it can be `csv`, 109 // `tsv`, or `json` 110 From string 111 112 // To is the output-format to use for the output: it can be `plain`, 113 // `ansi`, or `html` 114 To string 115 116 // Inputs has all named inputs/files given to this app 117 Inputs []string 118 119 // Title is used when Output is `html` and isn't empty 120 Title string 121 122 // BulletColumn is the 1-based index of the column to use (if any), to 123 // show bullets after each row; negative numbers are allowed and are 124 // meant to be fixed with the first/header row; 0 means disabled 125 126 // Bullets is the column name/index to use to show bullet-tallies after 127 // each output row: it can be a column name, a 1-based index, or even a 128 // negative index, counting backwards from the last column 129 Bullets string 130 131 // Colors is the column name/index to color-code by unique values: it 132 // be a column name, a 1-based index, or even a negative index, counting 133 // backwards from the last column 134 Colors string 135 136 // DigitsStyle is the name of the style/color to use for alternating 137 // groups of digits, when rendering long-enough numbers 138 DigitsStyle string 139 140 // LinksStyle is the name of the style/color to use for detected links 141 LinksStyle string 142 143 // UseTiles controls whether to show row-tiles for tabular inputs 144 UseTiles bool 145 146 // NiceNumbers causes output to either alternate ANSI-styles, or put 147 // separator-commas every 3-digit group 148 NiceNumbers bool 149 } 150 151 func parseArgs(usage string) (config, error) { 152 cfg := config{ 153 From: `auto`, 154 To: `ansi`, 155 DigitsStyle: `lightgray`, 156 LinksStyle: `lightblue`, 157 UseTiles: true, 158 NiceNumbers: true, 159 } 160 161 flag.Usage = func() { 162 w := flag.CommandLine.Output() 163 io.WriteString(w, usage) 164 io.WriteString(w, "\n\nOptions\n\n") 165 flag.PrintDefaults() 166 } 167 168 flag.StringVar(&cfg.From, `from`, cfg.From, inputUsage) 169 flag.StringVar(&cfg.To, `to`, cfg.To, outputUsage) 170 flag.StringVar(&cfg.Bullets, `bullets`, cfg.Bullets, bulletsUsage) 171 flag.StringVar(&cfg.Bullets, `tally`, cfg.Bullets, bulletsUsage) 172 flag.StringVar(&cfg.Colors, `color`, cfg.Colors, colorsUsage) 173 flag.StringVar(&cfg.Colors, `colors`, cfg.Colors, colorsUsage) 174 flag.StringVar(&cfg.DigitsStyle, `digits`, cfg.DigitsStyle, altDigitsUsage) 175 flag.StringVar(&cfg.LinksStyle, `links`, cfg.LinksStyle, linksUsage) 176 flag.StringVar(&cfg.Title, `title`, cfg.Title, titleUsage) 177 flag.BoolVar(&cfg.UseTiles, `tiles`, cfg.UseTiles, tilesUsage) 178 flag.Parse() 179 180 cfg.From = conform(typeAliases, cfg.From) 181 cfg.To = conform(typeAliases, cfg.To) 182 183 cfg.Inputs = flag.Args() 184 185 if cfg.From == `` { 186 return cfg, errors.New(`not given an input-format`) 187 } 188 if cfg.To == `` { 189 return cfg, errors.New(`not given an output-format`) 190 } 191 192 switch cfg.To { 193 case `csv`, `tsv`, `json`, `jsons`: 194 if len(cfg.Inputs) > 1 { 195 out := strings.ToUpper(cfg.To) 196 msg := `converting multiple inputs into ` + out + ` isn't allowed` 197 return cfg, errors.New(msg) 198 } 199 } 200 201 return cfg, nil 202 } 203 204 var typeAliases = map[string]string{ 205 `a`: `ansi`, 206 `ansi`: `ansi`, 207 `color`: `ansi`, 208 `colored`: `ansi`, 209 `colors`: `ansi`, 210 `styled`: `ansi`, 211 212 `b64`: `base64`, 213 `b-64`: `base64`, 214 `base64`: `base64`, 215 `base-64`: `base64`, 216 `datahref`: `base64`, 217 `data-href`: `base64`, 218 `datalink`: `base64`, 219 `data-link`: `base64`, 220 `datauri`: `base64`, 221 `data-uri`: `base64`, 222 `dataurl`: `base64`, 223 `data-url`: `base64`, 224 `href`: `base64`, 225 `link`: `base64`, 226 227 `b`: `bytes`, 228 `bytes`: `bytes`, 229 230 `,`: `csv`, 231 `c`: `csv`, 232 `cs`: `csv`, 233 `csv`: `csv`, 234 `text/csv`: `csv`, 235 `text/csv; charset=ascii`: `csv`, 236 `text/csv; charset=utf-8`: `csv`, 237 238 `h`: `html`, 239 `htm`: `html`, 240 `html`: `html`, 241 `page`: `html`, 242 `w`: `html`, 243 `web`: `html`, 244 `webpage`: `html`, 245 `web-page`: `html`, 246 `wp`: `html`, 247 248 `application/json`: `json`, 249 `application/json; charset=ascii`: `json`, 250 `application/json; charset=utf-8`: `json`, 251 `j`: `json`, 252 `json`: `json`, 253 `text/json`: `json`, 254 `text/json; charset=ascii`: `json`, 255 `text/json; charset=utf-8`: `json`, 256 257 `jl`: `jsonl`, 258 `jsonl`: `jsonl`, 259 260 `js`: `jsons`, 261 `jsons`: `jsons`, 262 `jsonstrings`: `jsons`, 263 `json-s`: `jsons`, 264 `json-strings`: `jsons`, 265 266 `l`: `lines`, 267 `li`: `lines`, 268 `lines`: `lines`, 269 `ln`: `lines`, 270 271 `p`: `plain`, 272 `pl`: `plain`, 273 `plain`: `plain`, 274 `plaintext`: `plain`, 275 `plain-text`: `plain`, 276 `prose`: `plain`, 277 `te`: `plain`, 278 `text`: `plain`, 279 `text/plain`: `plain`, 280 `text/plain; charset=ascii`: `plain`, 281 `text/plain; charset=utf-8`: `plain`, 282 `tx`: `plain`, 283 `txt`: `plain`, 284 285 `n`: `null`, 286 `no`: `null`, 287 `nothing`: `null`, 288 `nu`: `null`, 289 `null`: `null`, 290 291 `ta`: `tsv`, 292 `tab`: `tsv`, 293 `table`: `tsv`, 294 `tabs`: `tsv`, 295 `text/tsv`: `tsv`, 296 `text/tsv; charset=ascii`: `tsv`, 297 `text/tsv; charset=utf-8`: `tsv`, 298 `ts`: `tsv`, 299 `tsv`: `tsv`, 300 } 301 302 // conform tries to mold format-type names into canonical keys this app's 303 // dispatch tables have entries for, handling many variations letter-casing, 304 // MIME types, filepath extensions, shortcuts, and aliases can introduce 305 func conform(aliases map[string]string, k string) string { 306 k = strings.TrimSpace(k) 307 k = strings.TrimPrefix(k, `.`) 308 k = strings.ToLower(k) 309 if alias, ok := aliases[k]; ok { 310 return alias 311 } 312 return k 313 } 314 315 var outFormat2handler = map[string]func(c config) outputHandler{ 316 `ansi`: newOutputHandlerNoWrap, 317 `binary`: newOutputHandlerNoWrap, 318 `csv`: newOutputHandlerNoWrap, 319 `html`: newOutputHandlerHTML, 320 `json`: newOutputHandlerNoWrap, 321 `jsons`: newOutputHandlerNoWrap, 322 `plain`: newOutputHandlerNoWrap, 323 `tsv`: newOutputHandlerNoWrap, 324 } 325 326 // formatPair is the lookup-key-type for all converters 327 type formatPair struct { 328 From string 329 To string 330 } 331 332 // converter is a general data-format converter: it's so general it includes 333 // both whole-input-blocking converters, as well as streaming ones 334 type converter func(w writer, r io.Reader, name string, cfg config) error 335 336 var io2Converter = map[formatPair]converter{ 337 // {`json`, `csv`} 338 // {`json`, `tsv`} 339 340 {`base64`, `binary`}: decodeBase64, 341 342 {`bytes`, `ansi`}: viewHex, 343 {`csv`, `ansi`}: convertWholeTable, 344 {`csv`, `html`}: convertWholeTable, 345 {`csv`, `json`}: csv2json, 346 {`csv`, `jsons`}: csv2jsons, 347 {`csv`, `plain`}: convertWholeTable, 348 {`csv`, `tsv`}: csv2tsv, 349 {`json`, `ansi`}: renderJSON, 350 {`json`, `html`}: renderJSON, 351 {`json`, `jsons`}: renderJSON, 352 {`json`, `plain`}: renderJSON, 353 {`jsonl`, `json`}: jsonl2json, 354 {`lines`, `ansi`}: text2ansi, 355 {`lines`, `html`}: prose2html, 356 {`lines`, `json`}: lines2jsons, 357 {`lines`, `jsons`}: lines2jsons, 358 {`null`, `html`}: null2anything, 359 {`plain`, `ansi`}: text2ansi, 360 {`plain`, `html`}: prose2html, 361 {`plain`, `json`}: text2jsons, 362 {`plain`, `jsons`}: text2jsons, 363 {`tsv`, `ansi`}: convertWholeTable, 364 {`tsv`, `csv`}: tsv2csv, 365 {`tsv`, `html`}: convertWholeTable, 366 {`tsv`, `json`}: tsv2json, 367 {`tsv`, `jsons`}: tsv2jsons, 368 {`tsv`, `plain`}: convertWholeTable, 369 } 370 371 func adaptName(s string) string { 372 if s == `` || s == `-` { 373 return `<stdin>` 374 } 375 return s 376 } 377 378 var styleAliases = map[string]string{ 379 `dark-gray`: `darkgray`, 380 `light-blue`: `lightblue`, 381 `light-gray`: `lightgray`, 382 383 `l`: `lightblue`, 384 `light`: `lightblue`, 385 `b`: `blue`, 386 `blue`: `blue`, 387 `g`: `green`, 388 `inv`: `inverse`, 389 `invert`: `inverse`, 390 `swap`: `inverse`, 391 `rev`: `inverse`, 392 `reverse`: `inverse`, 393 `m`: `magenta`, 394 `mag`: `magenta`, 395 `o`: `orange`, 396 `p`: `purple`, 397 `r`: `red`, 398 `t`: `teal`, 399 } 400 401 var ansiStyles = map[string]string{ 402 `blue`: "\x1b[38;5;26m", 403 `bold`: "\x1b[1m", 404 `darkgray`: "\x1b[38;5;243m", 405 `gray`: "\x1b[38;5;246m", 406 `green`: "\x1b[38;5;29m", 407 `inverse`: "\x1b[7m", 408 `lightblue`: "\x1b[38;5;33m", 409 `lightgray`: "\x1b[38;5;249m", 410 `magenta`: "\x1b[38;5;165m", 411 `orange`: "\x1b[38;5;166m", 412 `pink`: "\x1b[38;5;213m", 413 `plain`: "\x1b[0m", 414 `purple`: "\x1b[38;5;99m", 415 `red`: "\x1b[31m", 416 `teal`: "\x1b[38;5;6m", 417 `underline`: "\x1b[4m", 418 } 419 420 func lookupStyleANSI(name string) (ansi string, ok bool) { 421 if alias, ok := styleAliases[name]; ok { 422 name = alias 423 } 424 ansi, ok = ansiStyles[name] 425 return ansi, ok 426 } File: vida/go.mod 1 module vida 2 3 go 1.18 File: vida/hex.go 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io" 8 "math" 9 "os" 10 "strconv" 11 "strings" 12 ) 13 14 // hexViewPadding is the padding/spacing emitted across each output line, 15 // except for the breather/ruler lines 16 const hexViewPadding = ` ` 17 18 const ( 19 hexUnknownStyle = 0 20 hexZeroStyle = 1 21 hexOtherStyle = 2 22 hexStyleASCII = 3 23 hexAllOnStyle = 4 24 ) 25 26 // hexByteStyles turns bytes into one of several distinct visual types, which 27 // allows quickly telling when ANSI styles codes are repetitive and when 28 // they're actually needed 29 var hexByteStyles = [256]int{ 30 hexZeroStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 31 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 32 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 33 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 34 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 35 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 36 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 37 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 38 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 39 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 40 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 41 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 42 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 43 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 44 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 45 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 46 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 47 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 48 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 49 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 50 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 51 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 52 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 53 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 54 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 55 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 56 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 57 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 58 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 59 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 60 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexStyleASCII, 61 hexStyleASCII, hexStyleASCII, hexStyleASCII, hexOtherStyle, 62 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 63 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 64 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 65 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 66 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 67 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 68 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 69 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 70 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 71 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 72 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 73 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 74 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 75 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 76 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 77 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 78 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 79 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 80 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 81 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 82 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 83 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 84 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 85 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 86 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 87 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 88 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 89 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 90 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 91 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 92 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexOtherStyle, 93 hexOtherStyle, hexOtherStyle, hexOtherStyle, hexAllOnStyle, 94 } 95 96 // hexSymbols is a direct lookup table combining 2 hex digits with either a 97 // space or a displayable ASCII symbol matching the byte's own ASCII value; 98 // this table was autogenerated by running the command 99 // 100 // seq 0 255 | ./hex-symbols.awk 101 var hexSymbols = [256]string{ 102 `00 `, `01 `, `02 `, `03 `, `04 `, `05 `, `06 `, `07 `, 103 `08 `, `09 `, `0a `, `0b `, `0c `, `0d `, `0e `, `0f `, 104 `10 `, `11 `, `12 `, `13 `, `14 `, `15 `, `16 `, `17 `, 105 `18 `, `19 `, `1a `, `1b `, `1c `, `1d `, `1e `, `1f `, 106 `20 `, `21!`, `22"`, `23#`, `24$`, `25%`, `26&`, `27'`, 107 `28(`, `29)`, `2a*`, `2b+`, `2c,`, `2d-`, `2e.`, `2f/`, 108 `300`, `311`, `322`, `333`, `344`, `355`, `366`, `377`, 109 `388`, `399`, `3a:`, `3b;`, `3c<`, `3d=`, `3e>`, `3f?`, 110 `40@`, `41A`, `42B`, `43C`, `44D`, `45E`, `46F`, `47G`, 111 `48H`, `49I`, `4aJ`, `4bK`, `4cL`, `4dM`, `4eN`, `4fO`, 112 `50P`, `51Q`, `52R`, `53S`, `54T`, `55U`, `56V`, `57W`, 113 `58X`, `59Y`, `5aZ`, `5b[`, `5c\`, `5d]`, `5e^`, `5f_`, 114 "60`", `61a`, `62b`, `63c`, `64d`, `65e`, `66f`, `67g`, 115 `68h`, `69i`, `6aj`, `6bk`, `6cl`, `6dm`, `6en`, `6fo`, 116 `70p`, `71q`, `72r`, `73s`, `74t`, `75u`, `76v`, `77w`, 117 `78x`, `79y`, `7az`, `7b{`, `7c|`, `7d}`, `7e~`, `7f `, 118 `80 `, `81 `, `82 `, `83 `, `84 `, `85 `, `86 `, `87 `, 119 `88 `, `89 `, `8a `, `8b `, `8c `, `8d `, `8e `, `8f `, 120 `90 `, `91 `, `92 `, `93 `, `94 `, `95 `, `96 `, `97 `, 121 `98 `, `99 `, `9a `, `9b `, `9c `, `9d `, `9e `, `9f `, 122 `a0 `, `a1 `, `a2 `, `a3 `, `a4 `, `a5 `, `a6 `, `a7 `, 123 `a8 `, `a9 `, `aa `, `ab `, `ac `, `ad `, `ae `, `af `, 124 `b0 `, `b1 `, `b2 `, `b3 `, `b4 `, `b5 `, `b6 `, `b7 `, 125 `b8 `, `b9 `, `ba `, `bb `, `bc `, `bd `, `be `, `bf `, 126 `c0 `, `c1 `, `c2 `, `c3 `, `c4 `, `c5 `, `c6 `, `c7 `, 127 `c8 `, `c9 `, `ca `, `cb `, `cc `, `cd `, `ce `, `cf `, 128 `d0 `, `d1 `, `d2 `, `d3 `, `d4 `, `d5 `, `d6 `, `d7 `, 129 `d8 `, `d9 `, `da `, `db `, `dc `, `dd `, `de `, `df `, 130 `e0 `, `e1 `, `e2 `, `e3 `, `e4 `, `e5 `, `e6 `, `e7 `, 131 `e8 `, `e9 `, `ea `, `eb `, `ec `, `ed `, `ee `, `ef `, 132 `f0 `, `f1 `, `f2 `, `f3 `, `f4 `, `f5 `, `f6 `, `f7 `, 133 `f8 `, `f9 `, `fa `, `fb `, `fc `, `fd `, `fe `, `ff `, 134 } 135 136 // styledHexBytes is a super-fast direct byte-to-result lookup table, and was 137 // autogenerated by running the command 138 // 139 // seq 0 255 | ./hex-styles.awk 140 var styledHexBytes = [256]string{ 141 "\x1b[38;5;111m00 ", "\x1b[38;5;246m01 ", 142 "\x1b[38;5;246m02 ", "\x1b[38;5;246m03 ", 143 "\x1b[38;5;246m04 ", "\x1b[38;5;246m05 ", 144 "\x1b[38;5;246m06 ", "\x1b[38;5;246m07 ", 145 "\x1b[38;5;246m08 ", "\x1b[38;5;246m09 ", 146 "\x1b[38;5;246m0a ", "\x1b[38;5;246m0b ", 147 "\x1b[38;5;246m0c ", "\x1b[38;5;246m0d ", 148 "\x1b[38;5;246m0e ", "\x1b[38;5;246m0f ", 149 "\x1b[38;5;246m10 ", "\x1b[38;5;246m11 ", 150 "\x1b[38;5;246m12 ", "\x1b[38;5;246m13 ", 151 "\x1b[38;5;246m14 ", "\x1b[38;5;246m15 ", 152 "\x1b[38;5;246m16 ", "\x1b[38;5;246m17 ", 153 "\x1b[38;5;246m18 ", "\x1b[38;5;246m19 ", 154 "\x1b[38;5;246m1a ", "\x1b[38;5;246m1b ", 155 "\x1b[38;5;246m1c ", "\x1b[38;5;246m1d ", 156 "\x1b[38;5;246m1e ", "\x1b[38;5;246m1f ", 157 "\x1b[38;5;72m20\x1b[38;5;239m ", "\x1b[38;5;72m21\x1b[38;5;239m!", 158 "\x1b[38;5;72m22\x1b[38;5;239m\"", "\x1b[38;5;72m23\x1b[38;5;239m#", 159 "\x1b[38;5;72m24\x1b[38;5;239m$", "\x1b[38;5;72m25\x1b[38;5;239m%", 160 "\x1b[38;5;72m26\x1b[38;5;239m&", "\x1b[38;5;72m27\x1b[38;5;239m'", 161 "\x1b[38;5;72m28\x1b[38;5;239m(", "\x1b[38;5;72m29\x1b[38;5;239m)", 162 "\x1b[38;5;72m2a\x1b[38;5;239m*", "\x1b[38;5;72m2b\x1b[38;5;239m+", 163 "\x1b[38;5;72m2c\x1b[38;5;239m,", "\x1b[38;5;72m2d\x1b[38;5;239m-", 164 "\x1b[38;5;72m2e\x1b[38;5;239m.", "\x1b[38;5;72m2f\x1b[38;5;239m/", 165 "\x1b[38;5;72m30\x1b[38;5;239m0", "\x1b[38;5;72m31\x1b[38;5;239m1", 166 "\x1b[38;5;72m32\x1b[38;5;239m2", "\x1b[38;5;72m33\x1b[38;5;239m3", 167 "\x1b[38;5;72m34\x1b[38;5;239m4", "\x1b[38;5;72m35\x1b[38;5;239m5", 168 "\x1b[38;5;72m36\x1b[38;5;239m6", "\x1b[38;5;72m37\x1b[38;5;239m7", 169 "\x1b[38;5;72m38\x1b[38;5;239m8", "\x1b[38;5;72m39\x1b[38;5;239m9", 170 "\x1b[38;5;72m3a\x1b[38;5;239m:", "\x1b[38;5;72m3b\x1b[38;5;239m;", 171 "\x1b[38;5;72m3c\x1b[38;5;239m<", "\x1b[38;5;72m3d\x1b[38;5;239m=", 172 "\x1b[38;5;72m3e\x1b[38;5;239m>", "\x1b[38;5;72m3f\x1b[38;5;239m?", 173 "\x1b[38;5;72m40\x1b[38;5;239m@", "\x1b[38;5;72m41\x1b[38;5;239mA", 174 "\x1b[38;5;72m42\x1b[38;5;239mB", "\x1b[38;5;72m43\x1b[38;5;239mC", 175 "\x1b[38;5;72m44\x1b[38;5;239mD", "\x1b[38;5;72m45\x1b[38;5;239mE", 176 "\x1b[38;5;72m46\x1b[38;5;239mF", "\x1b[38;5;72m47\x1b[38;5;239mG", 177 "\x1b[38;5;72m48\x1b[38;5;239mH", "\x1b[38;5;72m49\x1b[38;5;239mI", 178 "\x1b[38;5;72m4a\x1b[38;5;239mJ", "\x1b[38;5;72m4b\x1b[38;5;239mK", 179 "\x1b[38;5;72m4c\x1b[38;5;239mL", "\x1b[38;5;72m4d\x1b[38;5;239mM", 180 "\x1b[38;5;72m4e\x1b[38;5;239mN", "\x1b[38;5;72m4f\x1b[38;5;239mO", 181 "\x1b[38;5;72m50\x1b[38;5;239mP", "\x1b[38;5;72m51\x1b[38;5;239mQ", 182 "\x1b[38;5;72m52\x1b[38;5;239mR", "\x1b[38;5;72m53\x1b[38;5;239mS", 183 "\x1b[38;5;72m54\x1b[38;5;239mT", "\x1b[38;5;72m55\x1b[38;5;239mU", 184 "\x1b[38;5;72m56\x1b[38;5;239mV", "\x1b[38;5;72m57\x1b[38;5;239mW", 185 "\x1b[38;5;72m58\x1b[38;5;239mX", "\x1b[38;5;72m59\x1b[38;5;239mY", 186 "\x1b[38;5;72m5a\x1b[38;5;239mZ", "\x1b[38;5;72m5b\x1b[38;5;239m[", 187 "\x1b[38;5;72m5c\x1b[38;5;239m\\", "\x1b[38;5;72m5d\x1b[38;5;239m]", 188 "\x1b[38;5;72m5e\x1b[38;5;239m^", "\x1b[38;5;72m5f\x1b[38;5;239m_", 189 "\x1b[38;5;72m60\x1b[38;5;239m`", "\x1b[38;5;72m61\x1b[38;5;239ma", 190 "\x1b[38;5;72m62\x1b[38;5;239mb", "\x1b[38;5;72m63\x1b[38;5;239mc", 191 "\x1b[38;5;72m64\x1b[38;5;239md", "\x1b[38;5;72m65\x1b[38;5;239me", 192 "\x1b[38;5;72m66\x1b[38;5;239mf", "\x1b[38;5;72m67\x1b[38;5;239mg", 193 "\x1b[38;5;72m68\x1b[38;5;239mh", "\x1b[38;5;72m69\x1b[38;5;239mi", 194 "\x1b[38;5;72m6a\x1b[38;5;239mj", "\x1b[38;5;72m6b\x1b[38;5;239mk", 195 "\x1b[38;5;72m6c\x1b[38;5;239ml", "\x1b[38;5;72m6d\x1b[38;5;239mm", 196 "\x1b[38;5;72m6e\x1b[38;5;239mn", "\x1b[38;5;72m6f\x1b[38;5;239mo", 197 "\x1b[38;5;72m70\x1b[38;5;239mp", "\x1b[38;5;72m71\x1b[38;5;239mq", 198 "\x1b[38;5;72m72\x1b[38;5;239mr", "\x1b[38;5;72m73\x1b[38;5;239ms", 199 "\x1b[38;5;72m74\x1b[38;5;239mt", "\x1b[38;5;72m75\x1b[38;5;239mu", 200 "\x1b[38;5;72m76\x1b[38;5;239mv", "\x1b[38;5;72m77\x1b[38;5;239mw", 201 "\x1b[38;5;72m78\x1b[38;5;239mx", "\x1b[38;5;72m79\x1b[38;5;239my", 202 "\x1b[38;5;72m7a\x1b[38;5;239mz", "\x1b[38;5;72m7b\x1b[38;5;239m{", 203 "\x1b[38;5;72m7c\x1b[38;5;239m|", "\x1b[38;5;72m7d\x1b[38;5;239m}", 204 "\x1b[38;5;72m7e\x1b[38;5;239m~", "\x1b[38;5;246m7f ", 205 "\x1b[38;5;246m80 ", "\x1b[38;5;246m81 ", 206 "\x1b[38;5;246m82 ", "\x1b[38;5;246m83 ", 207 "\x1b[38;5;246m84 ", "\x1b[38;5;246m85 ", 208 "\x1b[38;5;246m86 ", "\x1b[38;5;246m87 ", 209 "\x1b[38;5;246m88 ", "\x1b[38;5;246m89 ", 210 "\x1b[38;5;246m8a ", "\x1b[38;5;246m8b ", 211 "\x1b[38;5;246m8c ", "\x1b[38;5;246m8d ", 212 "\x1b[38;5;246m8e ", "\x1b[38;5;246m8f ", 213 "\x1b[38;5;246m90 ", "\x1b[38;5;246m91 ", 214 "\x1b[38;5;246m92 ", "\x1b[38;5;246m93 ", 215 "\x1b[38;5;246m94 ", "\x1b[38;5;246m95 ", 216 "\x1b[38;5;246m96 ", "\x1b[38;5;246m97 ", 217 "\x1b[38;5;246m98 ", "\x1b[38;5;246m99 ", 218 "\x1b[38;5;246m9a ", "\x1b[38;5;246m9b ", 219 "\x1b[38;5;246m9c ", "\x1b[38;5;246m9d ", 220 "\x1b[38;5;246m9e ", "\x1b[38;5;246m9f ", 221 "\x1b[38;5;246ma0 ", "\x1b[38;5;246ma1 ", 222 "\x1b[38;5;246ma2 ", "\x1b[38;5;246ma3 ", 223 "\x1b[38;5;246ma4 ", "\x1b[38;5;246ma5 ", 224 "\x1b[38;5;246ma6 ", "\x1b[38;5;246ma7 ", 225 "\x1b[38;5;246ma8 ", "\x1b[38;5;246ma9 ", 226 "\x1b[38;5;246maa ", "\x1b[38;5;246mab ", 227 "\x1b[38;5;246mac ", "\x1b[38;5;246mad ", 228 "\x1b[38;5;246mae ", "\x1b[38;5;246maf ", 229 "\x1b[38;5;246mb0 ", "\x1b[38;5;246mb1 ", 230 "\x1b[38;5;246mb2 ", "\x1b[38;5;246mb3 ", 231 "\x1b[38;5;246mb4 ", "\x1b[38;5;246mb5 ", 232 "\x1b[38;5;246mb6 ", "\x1b[38;5;246mb7 ", 233 "\x1b[38;5;246mb8 ", "\x1b[38;5;246mb9 ", 234 "\x1b[38;5;246mba ", "\x1b[38;5;246mbb ", 235 "\x1b[38;5;246mbc ", "\x1b[38;5;246mbd ", 236 "\x1b[38;5;246mbe ", "\x1b[38;5;246mbf ", 237 "\x1b[38;5;246mc0 ", "\x1b[38;5;246mc1 ", 238 "\x1b[38;5;246mc2 ", "\x1b[38;5;246mc3 ", 239 "\x1b[38;5;246mc4 ", "\x1b[38;5;246mc5 ", 240 "\x1b[38;5;246mc6 ", "\x1b[38;5;246mc7 ", 241 "\x1b[38;5;246mc8 ", "\x1b[38;5;246mc9 ", 242 "\x1b[38;5;246mca ", "\x1b[38;5;246mcb ", 243 "\x1b[38;5;246mcc ", "\x1b[38;5;246mcd ", 244 "\x1b[38;5;246mce ", "\x1b[38;5;246mcf ", 245 "\x1b[38;5;246md0 ", "\x1b[38;5;246md1 ", 246 "\x1b[38;5;246md2 ", "\x1b[38;5;246md3 ", 247 "\x1b[38;5;246md4 ", "\x1b[38;5;246md5 ", 248 "\x1b[38;5;246md6 ", "\x1b[38;5;246md7 ", 249 "\x1b[38;5;246md8 ", "\x1b[38;5;246md9 ", 250 "\x1b[38;5;246mda ", "\x1b[38;5;246mdb ", 251 "\x1b[38;5;246mdc ", "\x1b[38;5;246mdd ", 252 "\x1b[38;5;246mde ", "\x1b[38;5;246mdf ", 253 "\x1b[38;5;246me0 ", "\x1b[38;5;246me1 ", 254 "\x1b[38;5;246me2 ", "\x1b[38;5;246me3 ", 255 "\x1b[38;5;246me4 ", "\x1b[38;5;246me5 ", 256 "\x1b[38;5;246me6 ", "\x1b[38;5;246me7 ", 257 "\x1b[38;5;246me8 ", "\x1b[38;5;246me9 ", 258 "\x1b[38;5;246mea ", "\x1b[38;5;246meb ", 259 "\x1b[38;5;246mec ", "\x1b[38;5;246med ", 260 "\x1b[38;5;246mee ", "\x1b[38;5;246mef ", 261 "\x1b[38;5;246mf0 ", "\x1b[38;5;246mf1 ", 262 "\x1b[38;5;246mf2 ", "\x1b[38;5;246mf3 ", 263 "\x1b[38;5;246mf4 ", "\x1b[38;5;246mf5 ", 264 "\x1b[38;5;246mf6 ", "\x1b[38;5;246mf7 ", 265 "\x1b[38;5;246mf8 ", "\x1b[38;5;246mf9 ", 266 "\x1b[38;5;246mfa ", "\x1b[38;5;246mfb ", 267 "\x1b[38;5;246mfc ", "\x1b[38;5;246mfd ", 268 "\x1b[38;5;246mfe ", "\x1b[38;5;209mff ", 269 } 270 271 // hexRenderConfig groups several arguments given to the hex-rendering funcs 272 type hexRenderConfig struct { 273 // out is writer to send all output to 274 out *bufio.Writer 275 276 // offset is the byte-offset of the first byte shown on the current output 277 // line: if shown at all, it's shown at the start the line 278 offset uint 279 280 // chunks is the 0-based counter for byte-chunks/lines shown so far, which 281 // indirectly keeps track of when it's time to show a `breather` line 282 chunks uint 283 284 // ruler is the `ruler` content to show on `breather` lines 285 ruler []byte 286 287 // perLine is how many hex-encoded bytes are shown per line 288 perLine uint 289 290 // offsetWidth is the max string-width for the byte-offsets shown at the 291 // start of output lines, and determines those values' left-padding 292 offsetWidth uint 293 294 // showOffsets determines whether byte-offsets are shown at all 295 showOffsets bool 296 297 // showASCII determines whether the ASCII-panels are shown at all 298 showASCII bool 299 } 300 301 // viewHex renders bytes as lines/rows of ANSI-styled hex values 302 func viewHex(w writer, r io.Reader, name string, cfg config) error { 303 size := -1 304 if f, ok := r.(*os.File); ok && r != os.Stdin { 305 if st, err := f.Stat(); err == nil { 306 size = int(st.Size()) 307 } 308 } 309 310 if cfg.Title != "" { 311 fmt.Fprintf(w, "\x1b[4m%s\x1b[0m\n", cfg.Title) 312 w.WriteString("\n") 313 } 314 315 if name == `-` { 316 name = `<stdin>` 317 } 318 319 if size < 0 { 320 fmt.Fprintf(w, "• %s\n", name) 321 } else { 322 const fs = "• %s \x1b[38;5;245m(%s bytes)\x1b[0m\n" 323 fmt.Fprintf(w, fs, name, sprintCommas(size)) 324 } 325 w.WriteString("\n") 326 327 // when done, emit a new line in case only part of the last line is 328 // shown, which means no newline was emitted for it 329 defer w.WriteString("\x1b[0m\n") 330 331 rc := hexRenderConfig{ 332 out: w, 333 offset: 0, 334 chunks: 0, 335 perLine: 16, 336 ruler: nil, 337 338 offsetWidth: 8, 339 showOffsets: true, 340 showASCII: true, 341 } 342 rc.ruler = makeRuler(int(rc.perLine)) 343 344 // calling func Read directly can sometimes result in chunks shorter 345 // than the max chunk-size, even when there are plenty of bytes yet 346 // to read; to avoid that, use a buffered-reader to explicitly fill 347 // a slice instead 348 br := bufio.NewReader(r) 349 350 // to show ASCII up to 1 full chunk ahead, 2 chunks are needed 351 cur := make([]byte, 0, rc.perLine) 352 ahead := make([]byte, 0, rc.perLine) 353 354 // the ASCII-panel's wide output requires staying 1 step/chunk behind, 355 // so to speak 356 cur, err := fillChunk(cur[:0], int(rc.perLine), br) 357 if len(cur) == 0 { 358 if err == io.EOF { 359 err = nil 360 } 361 return err 362 } 363 364 for { 365 ahead, err := fillChunk(ahead[:0], int(rc.perLine), br) 366 if err != nil && err != io.EOF { 367 return err 368 } 369 370 if len(ahead) == 0 { 371 // done, maybe except for an extra line of output 372 break 373 } 374 375 // show the byte-chunk on its own output line 376 err = hexWriteBufferANSI(rc, cur, ahead) 377 if err != nil { 378 // probably a pipe was closed 379 return nil 380 } 381 382 rc.chunks++ 383 rc.offset += uint(len(cur)) 384 cur = cur[:copy(cur, ahead)] 385 } 386 387 // don't forget the last output line 388 if rc.chunks > 0 && len(cur) > 0 { 389 return hexWriteBufferANSI(rc, cur, nil) 390 } 391 return nil 392 } 393 394 // loopThousandsGroups comes from my lib/package `mathplus`: that's why it 395 // handles negatives, even though this app only uses it with non-negatives. 396 func loopThousandsGroups(n int, fn func(i, n int)) { 397 // 0 doesn't have a log10 398 if n == 0 { 399 fn(0, 0) 400 return 401 } 402 403 sign := +1 404 if n < 0 { 405 n = -n 406 sign = -1 407 } 408 409 intLog1000 := int(math.Log10(float64(n)) / 3) 410 remBase := int(math.Pow10(3 * intLog1000)) 411 412 for i := 0; remBase > 0; i++ { 413 group := (1000 * n) / remBase / 1000 414 fn(i, sign*group) 415 // if original number was negative, ensure only first 416 // group gives a negative input to the callback 417 sign = +1 418 419 n %= remBase 420 remBase /= 1000 421 } 422 } 423 424 // hexWriteBufferANSI shows the hex byte-view using ANSI colors/styles 425 func hexWriteBufferANSI(rc hexRenderConfig, first, second []byte) error { 426 // show a ruler every few lines to make eye-scanning easier 427 if rc.chunks%5 == 0 && rc.chunks > 0 { 428 hexWriteRulerANSI(rc) 429 } 430 431 return hexWriteLineANSI(rc, first, second) 432 } 433 434 // hexWriteRulerANSI emits an indented ANSI-styled line showing spaced-out 435 // dots, so as to help eye-scan items on nearby output lines 436 func hexWriteRulerANSI(rc hexRenderConfig) { 437 w := rc.out 438 if len(rc.ruler) == 0 { 439 w.WriteByte('\n') 440 return 441 } 442 443 w.WriteString("\x1b[38;5;245m") 444 indent := int(rc.offsetWidth) + len(hexViewPadding) 445 writeSpaces(w, indent) 446 w.Write(rc.ruler) 447 w.WriteString("\x1b[0m\n") 448 } 449 450 func hexWriteLineANSI(rc hexRenderConfig, first, second []byte) error { 451 w := rc.out 452 453 // start each line with the byte-offset for the 1st item shown on it 454 if rc.showOffsets { 455 writeStyledCounter(w, int(rc.offsetWidth), rc.offset) 456 w.WriteString(hexViewPadding + "\x1b[48;5;254m") 457 } else { 458 w.WriteString(hexViewPadding) 459 } 460 461 prevStyle := hexUnknownStyle 462 for _, b := range first { 463 // using the slow/generic fmt.Fprintf is a performance bottleneck, 464 // since it's called for each input byte 465 // w.WriteString(styledHexBytes[b]) 466 467 // this more complicated way of emitting output avoids repeating 468 // ANSI styles when dealing with bytes which aren't displayable 469 // ASCII symbols, thus emitting fewer bytes when dealing with 470 // general binary datasets 471 style := hexByteStyles[b] 472 if style != prevStyle { 473 w.WriteString(styledHexBytes[b]) 474 if style == hexStyleASCII { 475 // styling displayable ASCII symbols uses multiple different 476 // styles each time it happens, always forcing ANSI-style 477 // updates 478 style = hexUnknownStyle 479 } 480 } else { 481 w.WriteString(hexSymbols[b]) 482 } 483 prevStyle = style 484 } 485 486 w.WriteString("\x1b[0m") 487 if rc.showASCII { 488 hexWritePlainASCII(w, first, second, int(rc.perLine)) 489 } 490 491 return w.WriteByte('\n') 492 } 493 494 // sprintCommas turns the non-negative number given into a readable string, 495 // where digits are grouped-separated by commas 496 func sprintCommas(n int) string { 497 var sb strings.Builder 498 loopThousandsGroups(n, func(i, n int) { 499 if i == 0 { 500 var buf [4]byte 501 sb.Write(strconv.AppendInt(buf[:0], int64(n), 10)) 502 return 503 } 504 sb.WriteByte(',') 505 writePad0Sub1000Counter(&sb, uint(n)) 506 }) 507 return sb.String() 508 } 509 510 func writeStyledCounter(w *bufio.Writer, width int, n uint) { 511 var buf [32]byte 512 str := strconv.AppendUint(buf[:0], uint64(n), 10) 513 514 // left-pad the final result with leading spaces 515 writeSpaces(w, width-len(str)) 516 517 var style bool 518 // emit leading part with 1 or 2 digits unstyled, ensuring the 519 // rest or the rendered number's string is a multiple of 3 long 520 if rem := len(str) % 3; rem != 0 { 521 w.Write(str[:rem]) 522 str = str[rem:] 523 // next digit-group needs some styling 524 style = true 525 } else { 526 style = false 527 } 528 529 // alternate between styled/unstyled 3-digit groups 530 for len(str) >= 3 { 531 if !style { 532 w.Write(str[:3]) 533 } else { 534 // w.WriteString("\x1b[38;5;243m") 535 w.WriteString("\x1b[38;5;249m") 536 w.Write(str[:3]) 537 w.WriteString("\x1b[0m") 538 } 539 540 style = !style 541 str = str[3:] 542 } 543 } 544 545 // hexWritePlainASCII emits the side-panel showing all ASCII runs for each line 546 func hexWritePlainASCII(w *bufio.Writer, first, second []byte, perline int) { 547 // prev keeps track of the previous byte, so spaces are added 548 // when bytes change from non-visible-ASCII to visible-ASCII 549 prev := byte(0) 550 551 spaces := 3*(perline-len(first)) + len(hexViewPadding) 552 553 for _, b := range first { 554 if 32 < b && b < 127 { 555 if !(32 < prev && prev < 127) { 556 writeSpaces(w, spaces) 557 spaces = 1 558 } 559 w.WriteByte(b) 560 } 561 prev = b 562 } 563 564 for _, b := range second { 565 if 32 < b && b < 127 { 566 if !(32 < prev && prev < 127) { 567 writeSpaces(w, spaces) 568 spaces = 1 569 } 570 w.WriteByte(b) 571 } 572 prev = b 573 } 574 } 575 576 // writePad0Sub1000Counter is an alternative to fmt.Fprintf(w, `%03d`, n) 577 func writePad0Sub1000Counter(w io.Writer, n uint) { 578 // precondition is 0...999 579 if n > 999 { 580 w.Write([]byte(`???`)) 581 return 582 } 583 584 var buf [3]byte 585 buf[0] = byte(n/100) + '0' 586 n %= 100 587 buf[1] = byte(n/10) + '0' 588 buf[2] = byte(n%10) + '0' 589 w.Write(buf[:]) 590 } 591 592 // makeRuler prerenders a ruler-line, used to make the output lines breathe 593 func makeRuler(numitems int) []byte { 594 if n := numitems / 4; n > 0 { 595 return bytes.Repeat([]byte(` ·`), n) 596 } 597 return nil 598 } 599 600 // fillChunk tries to read the number of bytes given, appending them to the 601 // byte-slice given; this func returns an EOF error only when no bytes are 602 // read, which somewhat simplifies error-handling for the func caller 603 func fillChunk(chunk []byte, n int, br *bufio.Reader) ([]byte, error) { 604 // read buffered-bytes up to the max chunk-size 605 for i := 0; i < n; i++ { 606 b, err := br.ReadByte() 607 if err == nil { 608 chunk = append(chunk, b) 609 continue 610 } 611 612 if err == io.EOF && i > 0 { 613 return chunk, nil 614 } 615 return chunk, err 616 } 617 618 // got the full byte-count asked for 619 return chunk, nil 620 } File: vida/hex-styles.awk 1 #!/usr/bin/awk -f 2 3 # all 0 bits 4 $0 == 0 { 5 print "\"\\x1b[38;5;111m00 \"," 6 next 7 } 8 9 # ascii symbol which need backslashing 10 $0 == 34 || $0 == 92 { 11 printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m\\%c\",\n", $0 + 0, $0 12 next 13 } 14 15 # all other ascii symbol 16 32 <= $0 && $0 <= 126 { 17 printf "\"\\x1b[38;5;72m%02x\\x1b[38;5;239m%c\",\n", $0 + 0, $0 18 next 19 } 20 21 # all 1 bits 22 $0 == 255 { 23 print "\"\\x1b[38;5;209mff \"," 24 next 25 } 26 27 # all other bytes 28 1 { 29 printf "\"\\x1b[38;5;246m%02x \",\n", $0 + 0 30 next 31 } File: vida/hex-symbols.awk 1 #!/usr/bin/awk -f 2 3 # ascii symbol which need backslashing 4 $0 == 34 || $0 == 92 { 5 printf "\"%02x\\%c\",\n", $0 + 0, $0 6 next 7 } 8 9 # all other ascii symbol 10 32 <= $0 && $0 <= 126 { 11 printf "\"%02x%c\",\n", $0 + 0, $0 12 next 13 } 14 15 # all other bytes 16 1 { 17 printf "\"%02x \",\n", $0 + 0 18 next 19 } File: vida/html.go 1 package main 2 3 import ( 4 "fmt" 5 "html" 6 "io" 7 "math" 8 "strconv" 9 "strings" 10 11 _ "embed" 12 ) 13 14 //go:embed style.css 15 var styleCSS string 16 17 const ( 18 htmlViewportContent = `content="width=device-width, initial-scale=1.0"` 19 htmlViewportTag = `<meta name="viewport" ` + htmlViewportContent + `>` 20 ) 21 22 type htmlOutputHandler struct { 23 Title string 24 NumInputs int 25 } 26 27 func newOutputHandlerHTML(c config) outputHandler { 28 return htmlOutputHandler{ 29 Title: c.Title, 30 NumInputs: len(c.Inputs), 31 } 32 } 33 34 func (h htmlOutputHandler) Begin(w writer) error { 35 w.WriteString(`<!DOCTYPE html>` + "\n") 36 // w.WriteString(`<html lang="en">` + "\n") 37 w.WriteString(`<html>` + "\n") 38 39 w.WriteString(`<head>` + "\n") 40 w.WriteString(` <meta charset="UTF-8">` + "\n") 41 w.WriteString(` ` + htmlViewportTag + "\n") 42 w.WriteString(` <link rel="icon" href="data:,">` + "\n") 43 if len(h.Title) > 0 { 44 w.WriteString(` <title>`) 45 w.WriteString(html.EscapeString(h.Title)) 46 w.WriteString(`</title>` + "\n") 47 } 48 w.WriteString(` <style>` + "\n") 49 w.WriteString(styleCSS) 50 w.WriteString(` </style>` + "\n") 51 w.WriteString(`</head>` + "\n") 52 53 _, err := w.WriteString(`<body>` + "\n") 54 return adaptWriterError(err) 55 } 56 57 func (h htmlOutputHandler) Before(w writer, i int, name string) error { 58 if i > 0 { 59 return writeString(w, "\n\n") 60 } 61 62 if h.NumInputs > 1 { 63 w.WriteString(`<h1>`) 64 w.WriteString(html.EscapeString(adaptName(name))) 65 w.WriteString(`</h1>`) 66 return endLine(w) 67 } 68 return nil 69 } 70 71 func (h htmlOutputHandler) After(w writer, i int, name string) error { 72 return nil 73 } 74 75 func (h htmlOutputHandler) End(w writer) error { 76 w.WriteString(`</body>` + "\n") 77 w.WriteString(`</html>` + "\n") 78 return nil 79 } 80 81 func (h htmlOutputHandler) Error(w writer, err error) error { 82 if err == nil { 83 return nil 84 } 85 86 w.WriteString(`<p class="error">`) 87 w.WriteString(html.EscapeString(err.Error())) 88 w.WriteString(`</p>`) 89 return err 90 } 91 92 type htmlTableWriter struct { 93 out writer 94 cfg rowParams 95 } 96 97 func (tw htmlTableWriter) BeforeTable() error { 98 w := tw.out 99 names := make([]string, 0, len(tw.cfg.Columns)) 100 for _, c := range tw.cfg.Columns { 101 names = append(names, c.Name) 102 } 103 104 w.WriteString(`<table>` + "\n") 105 106 w.WriteString(`<thead>` + "\n") 107 w.WriteString(`<tr>`) 108 tw.WriteTiles(names) 109 tw.WriteRowIndex(-1, names) 110 for _, c := range tw.cfg.Columns { 111 w.WriteString(`<th>`) 112 w.WriteString(html.EscapeString(c.Name)) 113 w.WriteString(`</th>`) 114 } 115 w.WriteString(`</tr>` + "\n") 116 w.WriteString(`</thead>` + "\n") 117 118 w.WriteString(`<tbody>` + "\n") 119 return nil 120 } 121 122 func (tw htmlTableWriter) AfterTable() error { 123 w := tw.out 124 names := make([]string, 0, len(tw.cfg.Columns)) 125 for _, c := range tw.cfg.Columns { 126 names = append(names, c.Name) 127 } 128 129 w.WriteString(`</tbody>` + "\n") 130 131 w.WriteString(`<tfoot>` + "\n") 132 w.WriteString(`<tr>`) 133 tw.WriteTiles(names) 134 tw.WriteRowIndex(-1, names) 135 for _, c := range tw.cfg.Columns { 136 w.WriteString(`<th>`) 137 w.WriteString(html.EscapeString(c.Name)) 138 w.WriteString(`</th>`) 139 } 140 w.WriteString(`</tr>` + "\n") 141 w.WriteString(`</tfoot>` + "\n") 142 143 w.WriteString(`</table>` + "\n") 144 return nil 145 } 146 147 func (tw htmlTableWriter) WriteHeaderRow(cols []columnMeta) error { 148 return nil 149 } 150 151 func (tw htmlTableWriter) WriteTiles(values []string) error { 152 w := tw.out 153 w.WriteString(`<th class="t">`) 154 155 for _, s := range values { 156 style, tile := matchTileHTML(s) 157 w.WriteString(`<span class="`) 158 w.WriteString(style) 159 w.WriteString(`">`) 160 w.WriteString(tile) 161 w.WriteString(`</span>`) 162 } 163 164 w.WriteString(`</th>`) 165 return nil 166 } 167 168 func (tw htmlTableWriter) WriteRowIndex(i int, values []string) error { 169 w := tw.out 170 if i < 0 { 171 w.WriteString(`<th class="ri"></th>`) 172 return nil 173 } 174 175 var buf [32]byte 176 s := strconv.AppendInt(buf[:0], int64(i), 10) 177 w.WriteString(`<th class="ri">`) 178 w.Write(s) 179 w.WriteString(`</th>`) 180 return nil 181 } 182 183 func (tw htmlTableWriter) WriteRowValues(values []string) error { 184 w := tw.out 185 186 for i, s := range values { 187 if f, ok := tryNumeric(s); ok { 188 style := numeric2class(f) 189 w.WriteString(`<td class="f `) 190 w.WriteString(style) 191 w.WriteString(`" style="`) 192 c := tw.cfg.Columns[i] 193 writeNumericStyleHTML(w, f, c.Min, c.Max) 194 w.WriteString(`">`) 195 writeFloat64(w, f) 196 w.WriteString(`</td>`) 197 continue 198 } 199 200 if len(s) == 0 { 201 w.WriteString(`<td class="empty"></td>`) 202 continue 203 } 204 205 w.WriteString(`<td>`) 206 207 if ok, err := tryLinkHTML(w, s); ok || err != nil { 208 w.WriteString(`</td>`) 209 if err != nil { 210 return err 211 } 212 continue 213 } 214 215 if ok, err := tryBase64HTML(w, s); ok || err != nil { 216 w.WriteString(`</td>`) 217 if err != nil { 218 return err 219 } 220 continue 221 } 222 223 w.WriteString(html.EscapeString(s)) 224 w.WriteString(`</td>`) 225 } 226 227 return nil 228 } 229 230 func (w htmlTableWriter) StartRow(values []string) error { 231 c := w.cfg.Colors 232 i := w.cfg.CategoryIndex 233 234 if 0 <= i && i < len(values) && len(values[i]) > 0 && c != nil { 235 cat := w.cfg.matchColor(values[i], paletteHTML) 236 w.out.WriteString(`<tr class="`) 237 w.out.WriteString(cat) 238 w.out.WriteString(`">`) 239 return nil 240 } 241 242 w.out.WriteString(`<tr>`) 243 return nil 244 } 245 246 func (w htmlTableWriter) EndRow() error { 247 w.out.WriteString(`</tr>`) 248 return endLine(w.out) 249 } 250 251 func writeNumericStyleHTML(w writer, f float64, min, max float64) { 252 fprintCellBackground(w, f, min, max, `transparent`) 253 } 254 255 // constants used in func FprintCellBackground 256 const ( 257 poscol = `#c1dfc1` // #b9dfb9 258 negcol = `#e7c6c6` // #d8aaaa 259 bgcol = `transparent` 260 left = 0.0 261 right = 100.0 262 263 // format-strings for 2 or 3 decimal digits 264 fs2 = `background:linear-gradient(to right,%s,%s %.2f%%,%s %.2f%%,%s %.2f%%,%s %.2f%%,%s)` 265 fs3 = `background:linear-gradient(to right,%s,%s %.3f%%,%s %.3f%%,%s %.3f%%,%s %.3f%%,%s)` 266 ) 267 268 func fprintCellBackground(w io.Writer, x float64, min, max float64, bgColor string) { 269 // avoid nonsense CSS for nonsense inputs 270 if math.IsNaN(x) || min >= max { 271 return 272 } 273 274 // ensure zero-based proportional bars for values from all-positive domains 275 if min > 0 { 276 min = 0 277 } 278 // ensure zero-based proportional bars for values from all-negative domains 279 if max < 0 { 280 max = 0 281 } 282 zero := scale(0, min, max, left, right) 283 p := scale(x, min, max, left, right) 284 285 if x > 0 { 286 fmt.Fprintf(w, fs2, bgColor, bgColor, zero, poscol, zero, poscol, p, bgColor, p, bgColor) 287 } 288 if x < 0 { 289 fmt.Fprintf(w, fs2, bgColor, bgColor, p, negcol, p, negcol, zero, bgColor, zero, bgColor) 290 } 291 if x == 0 { 292 fmt.Fprintf(w, `background-color:%s`, bgColor) 293 } 294 } 295 296 func scale(x float64, xmin, xmax, ymin, ymax float64) float64 { 297 k := (x - xmin) / (xmax - xmin) 298 return (ymax-ymin)*k + ymin 299 } 300 301 func tryBase64HTML(w writer, s string) (ok bool, err error) { 302 const ( 303 prefix = `data:` 304 suffix = `;base64,` 305 ) 306 if !strings.HasPrefix(s, prefix) { 307 return false, nil 308 } 309 310 start := s 311 if len(start) > 32 { 312 start = s[:32] 313 } 314 315 semicolon := strings.Index(start, suffix) 316 if semicolon < 0 { 317 return false, nil 318 } 319 320 slash := strings.IndexByte(start, '/') 321 if slash < len(prefix) { 322 return false, nil 323 } 324 325 for _, b := range s[semicolon+len(suffix):] { 326 if isValidBase64[b] { 327 continue 328 } 329 return false, nil 330 } 331 332 switch s[len(prefix):slash] { 333 case `image`: 334 w.WriteString(`<img src="`) 335 w.WriteString(s) 336 err := writeString(w, `">`) 337 return true, err 338 339 case `audio`: 340 w.WriteString(`<audio controls src="`) 341 w.WriteString(s) 342 err := writeString(w, `"></audio>`) 343 return true, err 344 345 case `video`: 346 w.WriteString(`<video controls src="`) 347 w.WriteString(s) 348 err := writeString(w, `"></video>`) 349 return true, err 350 351 default: 352 return false, nil 353 } 354 } 355 356 func tryLinkHTML(w writer, s string) (ok bool, err error) { 357 if f := strings.HasPrefix; !f(s, `https://`) && !f(s, `http://`) { 358 return false, nil 359 } 360 361 if len(s) == 0 || strings.ContainsRune(s, '"') { 362 return false, nil 363 } 364 365 w.WriteString(`<a rel="noopener noreferrer" href="`) 366 w.WriteString(s) 367 w.WriteString(`">`) 368 w.WriteString(html.EscapeString(s)) 369 err = writeString(w, `</a>`) 370 return true, err 371 } 372 373 func prose2html(w writer, r io.Reader, name string, cfg config) error { 374 inParagraph := false 375 376 err := loopLines(r, func(line string) error { 377 line = strings.TrimSpace(line) 378 if len(line) == 0 { 379 if inParagraph { 380 inParagraph = false 381 return writeString(w, `</p>`+"\n") 382 } 383 return nil 384 } 385 386 if !inParagraph { 387 inParagraph = true 388 w.WriteString(`<p>` + "\n") 389 } 390 391 if ok, err := tryBase64HTML(w, line); ok || err != nil { 392 return err 393 } 394 return proseLine2html(w, line) 395 }) 396 397 if err != nil { 398 return err 399 } 400 401 if inParagraph { 402 w.WriteString(`</p>` + "\n") 403 } 404 return nil 405 } 406 407 func proseLine2html(w writer, line string) error { 408 for len(line) > 0 { 409 i, j := indexLink(line) 410 if i < 0 { 411 break 412 } 413 414 w.WriteString(html.EscapeString(line[:i])) 415 416 href := line[i:j] 417 w.WriteString(`<a rel="noopener noreferrer" href="`) 418 w.WriteString(href) 419 w.WriteString(`">`) 420 w.WriteString(html.EscapeString(href)) 421 w.WriteString(`</a>`) 422 423 line = line[j:] 424 } 425 426 w.WriteString(html.EscapeString(line)) 427 return writeString(w, `<br>`+"\n") 428 } File: vida/info.txt 1 vida [options...] [filepath...] 2 3 VIew DAta emits/reformats table-like data or JSON as either ANSI-styled text, 4 or even self-contained HTML. Output can be embellished/enriched using various 5 options. 6 7 Some of its supported input-output format permutations are data-converters, 8 rather than data-viewers. 9 10 Supported input-output format-pairs: 11 12 base64 binary 13 bytes ansi 14 csv ansi 15 csv html 16 csv json 17 csv jsons 18 csv plain 19 csv tsv 20 json ansi 21 json html 22 json jsons 23 json plain 24 jsonl json 25 lines json 26 lines jsons 27 null html 28 plain ansi 29 plain html 30 plain json 31 tsv ansi 32 tsv csv 33 tsv html 34 tsv json 35 tsv jsons 36 tsv plain 37 38 Special formats: 39 40 bytes input-format which shows colored-hex when output is `ansi` 41 42 jsonl `JSON Lines` is plain-text lines, each of which is valid JSON 43 44 jsons `JSON Strings` is JSON where nulls, booleans, and numbers are 45 always turned into strings; it's an output-only format 46 47 lines input-format used to convert plain-text lines into JSON arrays 48 of strings; output-formats `json` and `jsons` give the same 49 output, in this case 50 51 null read nothing, or write nothing; filepaths and data are still 52 opened, read, and checked, when used as an output-format 53 54 Data-format aliases: 55 56 datauri alias for the `base64` format 57 jsonlines alias for the `jsonl` format 58 jsonstrings alias for the `jsonl` format 59 prose alias for the `plain` input-output format 60 text alias for the `plain` input-output format 61 web alias for the `html` output-format 62 webpage alias for the `html` output-format File: vida/io.go 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "io" 8 "os" 9 "strconv" 10 "strings" 11 ) 12 13 type writer = *bufio.Writer 14 15 // errNoMoreOutput is a dummy error which is ignored deliberately: it allows 16 // quitting this app right away and successfully 17 var errNoMoreOutput = errors.New(`no more output`) 18 19 // bullets is a cache trying to minimize string (re)allocations when showing 20 // tallies after data rows, when that option is enabled 21 var bullets = `` 22 23 func loopLines(r io.Reader, fn func(line string) error) error { 24 sc := bufio.NewScanner(r) 25 sc.Buffer(nil, lineBufferLimit) 26 27 for i := 0; sc.Scan(); i++ { 28 line := sc.Text() 29 if i == 0 { 30 line = strings.TrimPrefix(line, utf8BOM) 31 } 32 if err := fn(line); err != nil { 33 return err 34 } 35 } 36 37 return sc.Err() 38 } 39 40 func adaptWriterError(err error) error { 41 if err == nil { 42 return nil 43 } 44 return errNoMoreOutput 45 } 46 47 func prefaceError(intro string, err error) error { 48 if err == nil || err == errNoMoreOutput { 49 return nil 50 } 51 return errors.New(intro + `: ` + err.Error()) 52 } 53 54 func showError(err error) { 55 if err == nil || err == errNoMoreOutput { 56 return 57 } 58 59 os.Stderr.WriteString("\x1b[31m") 60 os.Stderr.WriteString(err.Error()) 61 os.Stderr.WriteString("\x1b[0m\n") 62 } 63 64 // writeBullets tries to minimize write-like operations 65 func writeBullets(w writer, n int) { 66 if n < 1 { 67 return 68 } 69 70 i := len(bullet) * n 71 if i > len(bullets) { 72 // round-up to chunk-sizes of multiple kilobytes 73 c := ceilIntBy(n, (10*1024)/len(bullet)) 74 c = maxInt(c, 1024/len(bullet)) 75 bullets = strings.Repeat(bullet, c) 76 } 77 78 w.WriteString(bullets[:i]) 79 } 80 81 // writeSpaces tries to minimize write-like operations 82 func writeSpaces(w writer, n int) { 83 const spaces = ` ` 84 if n < 1 { 85 return 86 } 87 88 for n >= len(spaces) { 89 w.WriteString(spaces) 90 n -= len(spaces) 91 } 92 w.WriteString(spaces[:n]) 93 } 94 95 func writeString(w writer, s string) error { 96 _, err := w.WriteString(s) 97 return adaptWriterError(err) 98 } 99 100 func endLine(w writer) error { 101 return adaptWriterError(w.WriteByte('\n')) 102 } 103 104 func padWriteInt64(w writer, n int64, pad int) { 105 var buf [32]byte 106 s := strconv.AppendInt(buf[:0], n, 10) 107 writeSpaces(w, pad-len(s)) 108 w.Write(s) 109 } 110 111 func numeric2style(f float64) string { 112 if f > 0 { 113 if float64(int64(f)) != f { 114 return positiveDecimalStyleANSI 115 } 116 return positiveIntegerStyleANSI 117 } 118 if f < 0 { 119 if float64(int64(f)) != f { 120 return negativeDecimalStyleANSI 121 } 122 return negativeIntegerStyleANSI 123 } 124 if f == 0 { 125 return zeroStyleANSI 126 } 127 return `` 128 } 129 130 func numeric2class(f float64) string { 131 if f > 0 { 132 if float64(int64(f)) != f { 133 return positiveDecimalClassHTML 134 } 135 return positiveIntegerClassHTML 136 } 137 if f < 0 { 138 if float64(int64(f)) != f { 139 return negativeDecimalClassHTML 140 } 141 return negativeIntegerClassHTML 142 } 143 if f == 0 { 144 return zeroClassHTML 145 } 146 return `` 147 } 148 149 func writeNumericANSI(w writer, f float64, cm columnMeta) { 150 const alt = altDigitGroupStyleANSI 151 const reset = resetStyleANSI 152 153 var buf [32]byte 154 s := strconv.AppendFloat(buf[:0], f, 'f', -1, 64) 155 pre := cm.MaxWidth - len(s) 156 157 post := 0 158 if cm.MaxDecimals > 0 { 159 decs := countDecimals(s) 160 post = cm.MaxDecimals - decs 161 pre -= post 162 } 163 164 // writeSpaces(w, pre) 165 // w.WriteString(numeric2style(f)) 166 // w.Write(s) 167 // w.WriteString(resetStyleANSI) 168 // writeSpaces(w, post) 169 170 style := numeric2style(f) 171 writeSpaces(w, pre) 172 w.WriteString(style) 173 if dot := bytes.IndexByte(s, '.'); dot >= 0 { 174 alternateDigitGroups(w, s[:dot], alt, reset) 175 w.WriteString(style) 176 w.Write(s[dot:]) 177 w.WriteString(reset) 178 } else { 179 alternateDigitGroups(w, s, alt, reset) 180 } 181 w.WriteString(resetStyleANSI) 182 writeSpaces(w, post) 183 } 184 185 func writeFloat64(w writer, f float64) { 186 var buf [32]byte 187 s := strconv.AppendFloat(buf[:0], f, 'f', -1, 64) 188 w.Write(s) 189 } 190 191 func writeNumericPlain(w writer, f float64, cm columnMeta) { 192 var buf [32]byte 193 s := strconv.AppendFloat(buf[:0], f, 'f', -1, 64) 194 pre := cm.MaxWidth - len(s) 195 196 post := 0 197 if cm.MaxDecimals > 0 { 198 decs := countDecimals(s) 199 post = cm.MaxDecimals - decs 200 pre -= post 201 } 202 203 writeSpaces(w, pre) 204 w.Write(s) 205 writeSpaces(w, post) 206 } 207 208 func writeStringANSI(w writer, s string) { 209 if isPadded(s) { 210 w.WriteString(paddedStyleANSI) 211 w.WriteString(s) 212 w.WriteString(resetStyleANSI) 213 return 214 } 215 216 w.WriteString(s) 217 } 218 219 func alternateDigitGroups(w writer, s []byte, alt string, reset string) { 220 if len(s) > 0 && s[0] == '-' { 221 w.WriteByte('-') 222 s = s[1:] 223 } 224 225 if len(s) < 4 { 226 w.Write(s) 227 return 228 } 229 230 lead := len(s) % 3 231 w.Write(s[:lead]) 232 s = s[lead:] 233 234 for style := lead > 0; len(s) > 0; s = s[3:] { 235 if style { 236 w.WriteString(alt) 237 w.Write(s[:3]) 238 w.WriteString(reset) 239 } else { 240 w.Write(s[:3]) 241 } 242 style = !style 243 } 244 } 245 246 func alternateDigitGroupsString(w writer, s string, alt string, reset string) { 247 if len(s) > 0 && s[0] == '-' { 248 w.WriteByte('-') 249 s = s[1:] 250 } 251 252 if len(s) < 4 { 253 w.WriteString(s) 254 return 255 } 256 257 lead := len(s) % 3 258 w.WriteString(s[:lead]) 259 s = s[lead:] 260 261 for style := lead > 0; len(s) > 0; s = s[3:] { 262 if style { 263 w.WriteString(alt) 264 w.WriteString(s[:3]) 265 w.WriteString(reset) 266 } else { 267 w.WriteString(s[:3]) 268 } 269 style = !style 270 } 271 } 272 273 func matchTileANSI(s string) (style, tile string) { 274 if f, ok := tryNumeric(s); ok { 275 return numeric2style(f), usualTile 276 } 277 278 if len(s) == 0 { 279 return emptyStyleANSI, emptyTile 280 } 281 282 if isPadded(s) { 283 return paddedStyleANSI, usualTile 284 } 285 286 return normalStyleANSI, usualTile 287 } 288 289 func matchTileHTML(s string) (style, tile string) { 290 if f, ok := tryNumeric(s); ok { 291 return numeric2class(f), usualTile 292 } 293 294 if len(s) == 0 { 295 return emptyClassHTML, emptyTile 296 } 297 298 if isPadded(s) { 299 return paddedClassHTML, usualTile 300 } 301 302 return normalClassHTML, usualTile 303 } 304 305 type outputHandler interface { 306 Begin(w writer) error 307 Before(w writer, i int, name string) error 308 After(w writer, i int, name string) error 309 End(w writer) error 310 Error(w writer, err error) error 311 } 312 313 type noWrapOutputHandler struct { 314 NumInputs int 315 } 316 317 func newOutputHandlerNoWrap(c config) outputHandler { 318 return noWrapOutputHandler{ 319 NumInputs: len(c.Inputs), 320 } 321 } 322 323 func (h noWrapOutputHandler) Begin(w writer) error { return nil } 324 func (h noWrapOutputHandler) End(w writer) error { return nil } 325 func (h noWrapOutputHandler) Error(w writer, err error) error { return nil } 326 327 func (h noWrapOutputHandler) Before(w writer, i int, name string) error { 328 if i > 0 { 329 return writeString(w, "\n\n") 330 } 331 332 if h.NumInputs > 1 { 333 w.WriteString(bullet + ` `) 334 w.WriteString(adaptName(name)) 335 return endLine(w) 336 } 337 return nil 338 } 339 340 func (h noWrapOutputHandler) After(w writer, i int, name string) error { 341 return nil 342 } 343 344 func null2anything(w writer, r io.Reader, name string, cfg config) error { 345 return nil 346 } File: vida/io_test.go 1 package main 2 3 import ( 4 "bufio" 5 "strings" 6 "testing" 7 "unicode/utf8" 8 ) 9 10 func TestWriteBullets(t *testing.T) { 11 const max = 2_000 12 var sb strings.Builder 13 sb.Grow(len(bullet) * max) 14 15 for c := -100; c < max; c++ { 16 sb.Reset() 17 w := bufio.NewWriter(&sb) 18 writeBullets(w, c) 19 w.Flush() 20 21 got := sb.String() 22 exp := strings.Repeat(bullet, maxInt(c, 0)) 23 m := utf8.RuneCountInString(exp) 24 n := utf8.RuneCountInString(got) 25 if m != n { 26 const fs = `%d: expected len(%d), instead of len(%d)` 27 t.Fatalf(fs, c, m, n) 28 return 29 } 30 } 31 } 32 33 func TestWriteSpaces(t *testing.T) { 34 const max = 2_000 35 var sb strings.Builder 36 sb.Grow(max) 37 38 for c := -100; c < max; c++ { 39 sb.Reset() 40 w := bufio.NewWriter(&sb) 41 writeSpaces(w, c) 42 w.Flush() 43 44 got := sb.String() 45 exp := strings.Repeat(` `, maxInt(c, 0)) 46 if len(exp) != len(got) { 47 const fs = `%d: expected len(%d), instead of len(%d)` 48 t.Fatalf(fs, c, len(exp), len(got)) 49 return 50 } 51 } 52 } File: vida/json.go 1 package main 2 3 import ( 4 "encoding/json" 5 "errors" 6 "html" 7 "io" 8 "strconv" 9 "strings" 10 ) 11 12 var type2jsonWalker = map[string]func(w writer) jsonWalker{ 13 `ansi`: func(w writer) jsonWalker { return &jsonWalkerANSI{w} }, 14 `html`: func(w writer) jsonWalker { return &jsonWalkerHTML{w} }, 15 `jsons`: func(w writer) jsonWalker { return &jsonWalkerJSONS{jsonWalkerPlain{w}} }, 16 `plain`: func(w writer) jsonWalker { return &jsonWalkerPlain{w} }, 17 `null`: func(w writer) jsonWalker { return &jsonWalkerNull{} }, 18 } 19 20 func renderJSON(w writer, r io.Reader, name string, cfg config) error { 21 makeWalker, ok := type2jsonWalker[cfg.To] 22 if !ok { 23 return convTypesError(name, cfg.From, cfg.To) 24 } 25 26 var h jsonHandler 27 h.dec = json.NewDecoder(r) 28 h.dec.UseNumber() 29 return h.walk(makeWalker(w)) 30 } 31 32 func jsonl2json(w writer, r io.Reader, name string, cfg config) error { 33 i := 0 34 var h jsonHandler 35 36 err := loopLines(r, func(line string) error { 37 line = strings.TrimSpace(line) 38 if len(line) == 0 || line[0] == '#' || strings.HasPrefix(line, `//`) { 39 return nil 40 } 41 42 if err := writeString(w, bool2str(i == 0, "[\n", ",\n")); err != nil { 43 return err 44 } 45 46 i++ 47 writeSpaces(w, jsonLevelIndent) 48 h.dec = json.NewDecoder(strings.NewReader(line)) 49 h.dec.UseNumber() 50 return h.walk(jsonWalkerSingleLine{jsonWalkerPlain{w}}) 51 }) 52 53 if err != nil { 54 return err 55 } 56 return writeString(w, bool2str(i == 0, "[]\n", "\n]\n")) 57 } 58 59 func lines2jsons(w writer, r io.Reader, name string, cfg config) error { 60 i := 0 61 62 err := loopLines(r, func(line string) error { 63 if err := writeString(w, bool2str(i == 0, "[\n", ",\n")); err != nil { 64 return err 65 } 66 67 i++ 68 writeSpaces(w, jsonLevelIndent) 69 w.WriteByte('"') 70 writeInnerStringJSON(w, line) 71 w.WriteByte('"') 72 return nil 73 }) 74 75 if err != nil { 76 return err 77 } 78 return writeString(w, bool2str(i == 0, "[]\n", "\n]\n")) 79 } 80 81 func text2jsons(w writer, r io.Reader, name string, cfg config) error { 82 b, err := io.ReadAll(r) 83 if err != nil { 84 return err 85 } 86 87 w.WriteByte('"') 88 writeInnerStringJSON(w, string(b)) 89 w.WriteByte('"') 90 return endLine(w) 91 } 92 93 type jsonWalker interface { 94 End() error 95 96 Indent(level int) error 97 98 Null() error 99 Bool(b bool) error 100 Number(f float64, s string) error 101 String(s string) error 102 103 ArrayItem(i, level int) error 104 EndArray(i, level int) error 105 106 Key(i int, k string, level int) error 107 EndObject(i, level int) error 108 } 109 110 // jsonHandler recursively digs into JSON input, and renders it according to 111 // the callbacks/funcs it's configured with; it assumes the json.Decoder it 112 // uses is in numbers-mode, activated by calling json.Decoder.UseNumber() 113 type jsonHandler struct { 114 dec *json.Decoder 115 116 // level is the current nesting-level 117 level int 118 } 119 120 func (h *jsonHandler) walk(w jsonWalker) error { 121 t, err := h.dec.Token() 122 if err == io.EOF { 123 return errors.New(`input has no JSON values`) 124 } 125 if err != nil { 126 return err 127 } 128 129 err = h.token(w, t) 130 endErr := w.End() 131 132 if err != nil { 133 return err 134 } 135 if endErr != nil { 136 return endErr 137 } 138 139 _, err = h.dec.Token() 140 if err == io.EOF { 141 return nil 142 } 143 return errors.New(`unexpected trailing JSON data`) 144 } 145 146 // token handles read/parsed tokens; this func doesn't read tokens 147 func (h *jsonHandler) token(w jsonWalker, t json.Token) error { 148 switch t := t.(type) { 149 case nil: 150 return w.Null() 151 152 case bool: 153 return w.Bool(t) 154 155 case json.Number: 156 f, _ := t.Float64() 157 return w.Number(f, t.String()) 158 159 case string: 160 return w.String(t) 161 162 case json.Delim: 163 switch t { 164 case json.Delim('['): 165 return h.array(w) 166 case json.Delim('{'): 167 return h.object(w) 168 default: 169 return errors.New(`unsupported JSON delimiter`) 170 } 171 172 default: 173 return errors.New(`unsupported JSON token`) 174 } 175 } 176 177 // array handles arrays for func token 178 func (h *jsonHandler) array(w jsonWalker) error { 179 level := h.level 180 h.level++ 181 defer func() { h.level = level }() 182 183 for i := 0; true; i++ { 184 t, err := h.dec.Token() 185 if err != nil { 186 return err 187 } 188 189 if t == json.Delim(']') { 190 return w.EndArray(i, h.level-1) 191 } 192 193 if err := w.ArrayItem(i, h.level); err != nil { 194 return err 195 } 196 197 if err := h.token(w, t); err != nil { 198 return err 199 } 200 } 201 202 // make the compiler happy 203 return nil 204 } 205 206 // object handles objects for func token 207 func (h *jsonHandler) object(w jsonWalker) error { 208 level := h.level 209 h.level++ 210 defer func() { h.level = level }() 211 212 for i := 0; true; i++ { 213 t, err := h.dec.Token() 214 if err != nil { 215 return err 216 } 217 218 if t == json.Delim('}') { 219 return w.EndObject(i, level) 220 } 221 222 // the stdlib JSON decoder is supposed to fail with non-string keys 223 k, _ := t.(string) 224 if err := w.Key(i, k, h.level); err != nil { 225 return err 226 } 227 228 t, err = h.dec.Token() 229 if err != nil { 230 return err 231 } 232 233 if err := h.token(w, t); err != nil { 234 return err 235 } 236 } 237 238 // make the compiler happy 239 return nil 240 } 241 242 type jsonWalkerANSI struct { 243 out writer 244 } 245 246 func (w jsonWalkerANSI) End() error { 247 w.out.WriteByte('\n') 248 return nil 249 } 250 251 func (w jsonWalkerANSI) Indent(level int) error { 252 writeSpaces(w.out, jsonLevelIndent*level) 253 return nil 254 } 255 256 func (w jsonWalkerANSI) Null() error { 257 w.out.WriteString(nullStyleANSI + `null` + resetStyleANSI) 258 return nil 259 } 260 261 func (w jsonWalkerANSI) Bool(b bool) error { 262 if b { 263 w.out.WriteString(boolStyleANSI + `true` + resetStyleANSI) 264 } else { 265 w.out.WriteString(boolStyleANSI + `false` + resetStyleANSI) 266 } 267 return nil 268 } 269 270 func (w jsonWalkerANSI) Number(f float64, s string) error { 271 style := numeric2style(f) 272 w.out.WriteString(style) 273 w.out.WriteString(s) 274 w.out.WriteString(resetStyleANSI) 275 return nil 276 } 277 278 func (w jsonWalkerANSI) String(s string) error { 279 w.out.WriteString(syntaxStyleANSI + `"` + resetStyleANSI) 280 w.out.WriteString(stringStyleANSI) 281 writeInnerStringJSON(w.out, s) 282 w.out.WriteString(syntaxStyleANSI + `"` + resetStyleANSI) 283 return nil 284 } 285 286 func (w jsonWalkerANSI) EndArray(i, level int) error { 287 if i == 0 { 288 w.out.WriteString(syntaxStyleANSI + `[]` + resetStyleANSI) 289 } else { 290 w.out.WriteByte('\n') 291 w.Indent(level) 292 w.out.WriteString(syntaxStyleANSI + `]` + resetStyleANSI) 293 } 294 return nil 295 } 296 297 func (w jsonWalkerANSI) ArrayItem(i, level int) error { 298 var err error 299 if i == 0 { 300 err = writeString(w.out, syntaxStyleANSI+`[`+resetStyleANSI+"\n") 301 } else { 302 err = writeString(w.out, syntaxStyleANSI+`,`+resetStyleANSI+"\n") 303 } 304 305 if err != nil { 306 return err 307 } 308 return w.Indent(level) 309 } 310 311 func (w jsonWalkerANSI) EndObject(i, level int) error { 312 if i == 0 { 313 w.out.WriteString(syntaxStyleANSI + `{}` + resetStyleANSI) 314 } else { 315 w.out.WriteByte('\n') 316 w.Indent(level) 317 w.out.WriteString(syntaxStyleANSI + `}` + resetStyleANSI) 318 } 319 return nil 320 } 321 322 func (w jsonWalkerANSI) Key(i int, s string, level int) error { 323 var err error 324 if i == 0 { 325 err = writeString(w.out, syntaxStyleANSI+`{`+resetStyleANSI+"\n") 326 } else { 327 err = writeString(w.out, syntaxStyleANSI+`,`+resetStyleANSI+"\n") 328 } 329 330 if err != nil { 331 return err 332 } 333 334 w.Indent(level) 335 w.out.WriteString(syntaxStyleANSI + `"` + keyStyleANSI) 336 writeInnerStringJSON(w.out, s) 337 w.out.WriteString(syntaxStyleANSI + `":` + resetStyleANSI + ` `) 338 return nil 339 } 340 341 type jsonWalkerJSONS struct { 342 jsonWalkerPlain 343 } 344 345 func (w jsonWalkerJSONS) Null() error { 346 w.out.WriteString(`"null"`) 347 return nil 348 } 349 350 func (w jsonWalkerJSONS) Bool(b bool) error { 351 if b { 352 w.out.WriteString(`"true"`) 353 } else { 354 w.out.WriteString(`"false"`) 355 } 356 return nil 357 } 358 359 func (w jsonWalkerJSONS) Number(f float64, s string) error { 360 w.out.WriteByte('"') 361 writeFloat64(w.out, f) 362 w.out.WriteByte('"') 363 return nil 364 } 365 366 type jsonWalkerHTML struct { 367 out writer 368 } 369 370 func (w jsonWalkerHTML) End() error { 371 return nil 372 } 373 374 func (w jsonWalkerHTML) Indent(level int) error { 375 return nil 376 } 377 378 func (w jsonWalkerHTML) Null() error { 379 return writeString(w.out, `<span class="n">null</span>`) 380 } 381 382 func (w jsonWalkerHTML) Bool(b bool) error { 383 if b { 384 return writeString(w.out, `<span class="b">true</span>`) 385 } else { 386 return writeString(w.out, `<span class="b">false</span>`) 387 } 388 } 389 390 func (w jsonWalkerHTML) Number(f float64, s string) error { 391 w.out.WriteString(`<span class="f `) 392 w.out.WriteString(numeric2class(f)) 393 w.out.WriteString(`">`) 394 writeFloat64(w.out, f) 395 return writeString(w.out, `</span>`) 396 } 397 398 func (w jsonWalkerHTML) String(s string) error { 399 if len(s) == 0 { 400 return writeString(w.out, `<span class="empty"></span>`) 401 } 402 403 w.out.WriteString(`<span>`) 404 defer w.out.WriteString(`</span>`) 405 406 if ok, err := tryLinkHTML(w.out, s); ok || err != nil { 407 return err 408 } 409 410 if ok, err := tryBase64HTML(w.out, s); ok || err != nil { 411 return err 412 } 413 414 return writeString(w.out, html.EscapeString(s)) 415 } 416 417 func (w jsonWalkerHTML) EndArray(i, level int) error { 418 if i == 0 { 419 w.out.WriteString(`<details open>` + "\n") 420 w.out.WriteString(`<summary>empty array</summary>` + "\n") 421 return writeString(w.out, `</details>`+"\n") 422 } 423 424 w.out.WriteString(`</li>` + "\n") 425 w.out.WriteString(`</ul>` + "\n") 426 return writeString(w.out, `</details>`+"\n") 427 } 428 429 func (w jsonWalkerHTML) ArrayItem(i, level int) error { 430 if i == 0 { 431 w.out.WriteString(`<details open>` + "\n") 432 w.out.WriteString(`<summary>array</summary>` + "\n") 433 return writeString(w.out, `<ul>`+"\n"+`<li>`) 434 } 435 436 return writeString(w.out, `</li>`+"\n"+`<li>`) 437 } 438 439 func (w jsonWalkerHTML) EndObject(i, level int) error { 440 if i == 0 { 441 w.out.WriteString(`<details open>` + "\n") 442 w.out.WriteString(`<summary>empty object</summary>` + "\n") 443 return writeString(w.out, `</details">`+"\n") 444 } 445 446 w.out.WriteString(`</div>` + "\n") 447 return writeString(w.out, `</details>`+"\n") 448 } 449 450 func (w jsonWalkerHTML) Key(i int, s string, level int) error { 451 if i == 0 { 452 w.out.WriteString(`<details open>` + "\n") 453 w.out.WriteString(`<summary>object</summary>` + "\n") 454 w.out.WriteString(`<div class="pairs">` + "\n") 455 } 456 457 w.out.WriteString(`<span class="key">`) 458 w.out.WriteString(html.EscapeString(s)) 459 return writeString(w.out, `</span>`) 460 } 461 462 type jsonWalkerNull struct{} 463 464 func (w jsonWalkerNull) End() error { return nil } 465 func (w jsonWalkerNull) Indent(level int) error { return nil } 466 func (w jsonWalkerNull) Null() error { return nil } 467 func (w jsonWalkerNull) Bool(b bool) error { return nil } 468 func (w jsonWalkerNull) Number(f float64, s string) error { return nil } 469 func (w jsonWalkerNull) String(s string) error { return nil } 470 func (w jsonWalkerNull) EndArray(i, level int) error { return nil } 471 func (w jsonWalkerNull) ArrayItem(i, level int) error { return nil } 472 func (w jsonWalkerNull) EndObject(i, level int) error { return nil } 473 func (w jsonWalkerNull) Key(i int, s string, level int) error { return nil } 474 475 type jsonWalkerPlain struct { 476 out writer 477 } 478 479 func (w jsonWalkerPlain) End() error { 480 w.out.WriteByte('\n') 481 return nil 482 } 483 484 func (w jsonWalkerPlain) Indent(level int) error { 485 writeSpaces(w.out, jsonLevelIndent*level) 486 return nil 487 } 488 489 func (w jsonWalkerPlain) Null() error { 490 w.out.WriteString(`null`) 491 return nil 492 } 493 494 func (w jsonWalkerPlain) Bool(b bool) error { 495 if b { 496 w.out.WriteString(`true`) 497 } else { 498 w.out.WriteString(`false`) 499 } 500 return nil 501 } 502 503 func (w jsonWalkerPlain) Number(f float64, s string) error { 504 writeFloat64(w.out, f) 505 return nil 506 } 507 508 func (w jsonWalkerPlain) String(s string) error { 509 w.out.WriteString(`"`) 510 writeInnerStringJSON(w.out, s) 511 w.out.WriteString(`"`) 512 return nil 513 } 514 515 func (w jsonWalkerPlain) EndArray(i, level int) error { 516 if i == 0 { 517 w.out.WriteString(`[]`) 518 } else { 519 w.out.WriteByte('\n') 520 w.Indent(level) 521 w.out.WriteString(`]`) 522 } 523 return nil 524 } 525 526 func (w jsonWalkerPlain) ArrayItem(i, level int) error { 527 var err error 528 if i == 0 { 529 err = writeString(w.out, "[\n") 530 } else { 531 err = writeString(w.out, ",\n") 532 } 533 534 if err != nil { 535 return err 536 } 537 return w.Indent(level) 538 } 539 540 func (w jsonWalkerPlain) EndObject(i, level int) error { 541 if i == 0 { 542 w.out.WriteString(`{}`) 543 } else { 544 w.out.WriteByte('\n') 545 w.Indent(level) 546 w.out.WriteString(`}`) 547 } 548 return nil 549 } 550 551 func (w jsonWalkerPlain) Key(i int, s string, level int) error { 552 var err error 553 if i == 0 { 554 err = writeString(w.out, "{\n") 555 } else { 556 err = writeString(w.out, ",\n") 557 } 558 559 if err != nil { 560 return err 561 } 562 563 w.Indent(level) 564 w.out.WriteString(`"`) 565 writeInnerStringJSON(w.out, s) 566 w.out.WriteString(`": `) 567 return nil 568 } 569 570 type jsonWalkerSingleLine struct { 571 jsonWalkerPlain 572 } 573 574 func (w jsonWalkerSingleLine) End() error { 575 return nil 576 } 577 578 func (w jsonWalkerSingleLine) Indent(level int) error { 579 return nil 580 } 581 582 func (w jsonWalkerSingleLine) EndArray(i, level int) error { 583 if i == 0 { 584 w.out.WriteString(`[]`) 585 } else { 586 w.out.WriteString(`]`) 587 } 588 return nil 589 } 590 591 func (w jsonWalkerSingleLine) ArrayItem(i, level int) error { 592 if i == 0 { 593 w.out.WriteString(`[`) 594 } else { 595 w.out.WriteString(`, `) 596 } 597 return nil 598 } 599 600 func (w jsonWalkerSingleLine) EndObject(i, level int) error { 601 if i == 0 { 602 w.out.WriteString(`{}`) 603 } else { 604 w.out.WriteString(`}`) 605 } 606 return nil 607 } 608 609 func (w jsonWalkerSingleLine) Key(i int, s string, level int) error { 610 if i == 0 { 611 w.out.WriteString(`{`) 612 } else { 613 w.out.WriteString(`, `) 614 } 615 616 w.out.WriteString(`"`) 617 writeInnerStringJSON(w.out, s) 618 w.out.WriteString(`": `) 619 return nil 620 } 621 622 // writeInnerStringJSON ensures this app's JSON-sourced output has all its 623 // strings properly escaped: this allows plain-text output to be used as 624 // valid JSON, along with its ANSI-styled variant, if styles are filtered 625 // out by other tools 626 func writeInnerStringJSON(w writer, s string) { 627 if !strings.ContainsAny(s, "\"\\\t\r\n") { 628 w.WriteString(s) 629 return 630 } 631 632 for _, r := range s { 633 switch r { 634 case '"': 635 w.WriteString(`\"`) 636 case '\\': 637 w.WriteString(`\\`) 638 case '\t': 639 w.WriteString(`\t`) 640 case '\r': 641 w.WriteString(`\r`) 642 case '\n': 643 w.WriteString(`\n`) 644 default: 645 w.WriteRune(r) 646 } 647 } 648 } 649 650 type kvRowWriter func(w writer, keys, values []string) 651 652 func writeRowJSON(w writer, keys, values []string) { 653 w.WriteByte('{') 654 655 for i, k := range keys { 656 // treat missing trailing values as null ones 657 s := `` 658 if i < len(values) { 659 s = values[i] 660 } 661 662 if i > 0 { 663 w.WriteString(`, "`) 664 } else { 665 w.WriteByte('"') 666 } 667 668 writeInnerStringJSON(w, k) 669 w.WriteString(`": `) 670 if len(s) == 0 { 671 w.WriteString(`null`) 672 continue 673 } 674 675 if f, ok := tryNumeric(s); ok { 676 var buf [32]byte 677 s := strconv.AppendFloat(buf[:0], f, 'f', -1, 64) 678 w.Write(s) 679 continue 680 } 681 682 w.WriteByte('"') 683 writeInnerStringJSON(w, s) 684 w.WriteByte('"') 685 } 686 687 // don't forget to treat missing trailing values as null ones 688 for i := len(values); i < len(keys); i++ { 689 if i > 0 { 690 w.WriteString(`, "`) 691 } else { 692 w.WriteByte('"') 693 } 694 695 writeInnerStringJSON(w, keys[i]) 696 w.WriteString(`": null`) 697 } 698 699 w.WriteByte('}') 700 } 701 702 func writeRowJSONS(w writer, keys, values []string) { 703 w.WriteByte('{') 704 705 for i, k := range keys { 706 // treat missing trailing values as null ones 707 v := `` 708 if i < len(values) { 709 v = values[i] 710 } 711 712 if i > 0 { 713 w.WriteString(`, "`) 714 } else { 715 w.WriteByte('"') 716 } 717 718 writeInnerStringJSON(w, k) 719 w.WriteString(`": "`) 720 writeInnerStringJSON(w, v) 721 w.WriteByte('"') 722 } 723 724 w.WriteByte('}') 725 } File: vida/main.go 1 package main 2 3 import ( 4 "bufio" 5 "compress/bzip2" 6 "compress/gzip" 7 "errors" 8 "io" 9 "os" 10 "path/filepath" 11 "strings" 12 13 _ "embed" 14 ) 15 16 //go:embed info.txt 17 var info string 18 19 func main() { 20 cfg, err := parseArgs(info) 21 if err != nil { 22 showError(err) 23 os.Exit(1) 24 } 25 26 err = run(os.Stdout, cfg.Inputs, cfg) 27 if err != nil && err != errNoMoreOutput { 28 showError(err) 29 os.Exit(1) 30 } 31 } 32 33 func run(w io.Writer, paths []string, cfg config) error { 34 if len(paths) == 0 { 35 paths = []string{`-`} 36 } 37 38 // refuse to read from stdin more than once 39 dashes := 0 40 for _, p := range paths { 41 if p == `-` { 42 dashes++ 43 } 44 if dashes > 1 { 45 const msg = `can't use stdin/single-dash as input more than once` 46 return errors.New(msg) 47 } 48 } 49 50 bw := bufio.NewWriter(w) 51 defer bw.Flush() 52 53 makeHandler, ok := outFormat2handler[cfg.To] 54 if !ok { 55 return errors.New(cfg.To + ` isn't a supported output-format`) 56 } 57 58 h := makeHandler(cfg) 59 if err := h.Begin(bw); err != nil { 60 return err 61 } 62 defer h.End(bw) 63 64 for i, path := range paths { 65 if err := h.Before(bw, i, path); err != nil { 66 return err 67 } 68 if err := handleInput(bw, path, cfg); err != nil { 69 return prefaceError(adaptName(path), err) 70 } 71 if err := h.After(bw, i, path); err != nil { 72 return err 73 } 74 } 75 return nil 76 } 77 78 func convTypesError(path, from, to string) error { 79 from = strings.TrimSuffix(from, `.gz`) 80 81 if from == `` { 82 const s = `: name doesn't suggest any supported input-type` 83 return errors.New(path + s) 84 } 85 86 msg := `can't convert from ` + from + ` to ` + to 87 return errors.New(msg) 88 } 89 90 func open(path string) (rc io.ReadCloser, kind string, err error) { 91 if path == `-` { 92 return io.NopCloser(os.Stdin), `tsv`, nil 93 } 94 95 // if f := strings.HasPrefix; f(path, `https://`) || f(path, `http://`) { 96 // resp, err := http.Get(path) 97 // if err != nil { 98 // return nil, ``, err 99 // } 100 // kind = resp.Header.Get(`Content-Type`) 101 // if i := strings.IndexByte(kind, ';'); i >= 0 { 102 // kind = kind[:i] 103 // kind = strings.TrimSpace(kind) 104 // } 105 // return resp.Body, strings.ToLower(kind), err 106 // } 107 108 ext := fileExtension(path) 109 kind = strings.TrimPrefix(ext, `.`) 110 kind = strings.ToLower(kind) 111 112 f, err := os.Open(path) 113 if err != nil { 114 return f, kind, errors.New(`can't open file named ` + path) 115 } 116 return f, kind, nil 117 } 118 119 func handleInput(w writer, path string, cfg config) error { 120 rc, typeHint, err := open(path) 121 if err != nil { 122 return err 123 } 124 defer rc.Close() 125 126 // the dummy `auto` input-format is meant to be overridden 127 if cfg.From == `auto` { 128 cfg.From = typeHint 129 } 130 cfg.From = strings.ToLower(cfg.From) 131 132 // auto-handle gzipped input 133 if strings.HasSuffix(cfg.From, `.gz`) { 134 cfg.From = strings.TrimSuffix(cfg.From, `.gz`) 135 dec, err := gzip.NewReader(rc) 136 if err != nil { 137 return err 138 } 139 defer dec.Close() 140 return handleReader(w, dec, path, cfg) 141 } 142 143 // auto-handle bzipped input 144 ext := filepath.Ext(cfg.From) 145 if isAnyFold(ext, `.bz`, `.bz2`, `.bzip`, `.bzip2`) { 146 cfg.From = cfg.From[:len(cfg.From)-len(ext)] 147 dec := bzip2.NewReader(rc) 148 return handleReader(w, dec, path, cfg) 149 } 150 151 return handleReader(w, rc, path, cfg) 152 } 153 154 func handleReader(w writer, r io.Reader, path string, cfg config) error { 155 // don't rely on the type-aliases table having keys for file extensions 156 from := conform(typeAliases, cfg.From) 157 158 if conv, ok := io2Converter[formatPair{from, cfg.To}]; ok { 159 return conv(w, r, path, cfg) 160 } 161 return convTypesError(path, from, cfg.To) 162 } File: vida/math.go 1 package main 2 3 func maxInt(x, y int) int { 4 if x > y { 5 return x 6 } 7 return y 8 } 9 10 // ceilIntBy calculates a sort of `modular ceiling` for integer, given a main 11 // number and a period; this func expects its 2nd argument to be positive 12 func ceilIntBy(n int, by int) int { 13 if rem := n % by; rem > 0 { 14 return n - rem + by 15 } 16 return n 17 } File: vida/mit-license.txt 1 The MIT License (MIT) 2 3 Copyright © 2024 pacman64 4 5 Permission is hereby granted, free of charge, to any person obtaining a copy of 6 this software and associated documentation files (the “Software”), to deal 7 in the Software without restriction, including without limitation the rights to 8 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 of the Software, and to permit persons to whom the Software is furnished to do 10 so, subject to the following conditions: 11 12 The above copyright notice and this permission notice shall be included in all 13 copies or substantial portions of the Software. 14 15 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 SOFTWARE. File: vida/strings.go 1 package main 2 3 import ( 4 "bytes" 5 "math" 6 "path/filepath" 7 "strconv" 8 "strings" 9 "unicode" 10 "unicode/utf8" 11 ) 12 13 // utf8BOM is used by some windows apps to start plain-text output, but it's 14 // useless as a `byte order mark` (BOM), since the UTF-8 format always has a 15 // single/known byte-order by design 16 const utf8BOM = "\xef\xbb\xbf" 17 18 // paletteANSI is constrained in the colors it can use by the colors which 19 // the tiles-panel uses, to avoid casually associating colored rows with the 20 // colors of numeric tiles 21 var paletteANSI = []string{ 22 "\x1b[38;5;166m", // orange 23 "\x1b[38;5;99m", // purple 24 "\x1b[38;5;30m", // teal 25 "\x1b[38;5;169m", // pink 26 "\x1b[38;5;94m", // brown? 27 "\x1b[38;5;245m", // gray 28 "\x1b[38;5;142m", // yellowish 29 30 // green looks like the tile colors for positive numbers, which use 31 // a simple green colorscale 32 // "\x1b[38;5;28m", // green 33 34 // can't use reds or most blues either to avoid looking like any 35 // colored tiles for zeros or negative numbers 36 37 // "\x1b[38;5;39m", // cyan 38 // "\x1b[38;5;207m", // pink 39 } 40 41 // paletteHTML is the HTML-class analog to the ANSI-values palette 42 var paletteHTML = []string{ 43 "c1", // orange 44 "c2", // purple 45 "c3", // teal 46 "c4", // pink 47 "c5", // brown? 48 "c6", // gray 49 "c7", // yellowish 50 } 51 52 var isValidBase64 = [256]bool{ 53 '0': true, '1': true, '2': true, '3': true, '4': true, 54 '5': true, '6': true, '7': true, '8': true, '9': true, 55 56 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 57 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 58 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true, 59 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true, 60 'Y': true, 'Z': true, 61 62 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 63 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 64 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true, 65 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true, 66 'y': true, 'z': true, 67 68 '+': true, '/': true, '=': true, 69 } 70 71 // isValidLinkDomainByte helps check the domain part of a web link 72 var isValidLinkDomainByte = [256]bool{ 73 '0': true, '1': true, '2': true, '3': true, '4': true, 74 '5': true, '6': true, '7': true, '8': true, '9': true, 75 76 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 77 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 78 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true, 79 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true, 80 'Y': true, 'Z': true, 81 82 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 83 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 84 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true, 85 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true, 86 'y': true, 'z': true, 87 88 '+': true, '-': true, '_': true, '.': true, ':': true, '/': true, 89 '%': true, 90 } 91 92 // isValidLinkPathByte helps check bytes in a web link after the domain part; 93 // among the few differences with isValidLinkDomainByte are semicolons, which 94 // in this table aren't considered valid: this correctly excludes them from 95 // links 96 var isValidLinkPathByte = [256]bool{ 97 '0': true, '1': true, '2': true, '3': true, '4': true, 98 '5': true, '6': true, '7': true, '8': true, '9': true, 99 100 'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 101 'G': true, 'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 102 'M': true, 'N': true, 'O': true, 'P': true, 'Q': true, 'R': true, 103 'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true, 104 'Y': true, 'Z': true, 105 106 'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 107 'g': true, 'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 108 'm': true, 'n': true, 'o': true, 'p': true, 'q': true, 'r': true, 109 's': true, 't': true, 'u': true, 'v': true, 'w': true, 'x': true, 110 'y': true, 'z': true, 111 112 '.': true, '+': true, '-': true, '_': true, '/': true, 113 '#': true, '%': true, '?': true, '&': true, '=': true, 114 } 115 116 // appendTSV allows reusing previously-allocated slices, while never allocating 117 // values on its own 118 func appendTSV(dest []string, items string) []string { 119 loopTSV(items, func(i int, s string) { 120 dest = append(dest, s) 121 }) 122 return dest 123 } 124 125 func bool2str(cond bool, yes, no string) string { 126 if cond { 127 return yes 128 } else { 129 return no 130 } 131 } 132 133 func countDecimals(s []byte) int { 134 if i := bytes.IndexByte(s, '.'); i >= 0 { 135 return len(s) - i 136 } 137 return 0 138 } 139 140 func countItemsTSV(line string) int { 141 if len(line) == 0 { 142 return 0 143 } 144 145 n := strings.Count(line, "\t") 146 return n + 1 147 } 148 149 // loopTSV helps avoid using strings.Split, which both minimizes allocations 150 // and reduces memory-use overall 151 func loopTSV(line string, fn func(i int, s string)) { 152 if len(line) == 0 { 153 return 154 } 155 156 for i := 0; true; i++ { 157 j := strings.IndexByte(line, '\t') 158 if j < 0 { 159 fn(i, line) 160 return 161 } 162 163 fn(i, line[:j]) 164 line = line[j+1:] 165 } 166 } 167 168 func hasPrefixFold(s, prefix string) bool { 169 n := len(prefix) 170 return len(s) >= n && strings.EqualFold(s[:n], prefix) 171 } 172 173 func float64width(f float64) (span, decimals int) { 174 var buf [32]byte 175 s := strconv.AppendFloat(buf[:0], f, 'f', -1, 64) 176 return len(s), countDecimals(s) 177 } 178 179 func int64width(n int64) int { 180 var buf [32]byte 181 s := strconv.AppendInt(buf[:0], n, 10) 182 return len(s) 183 } 184 185 func isPadded(s string) bool { 186 return len(s) > 0 && s[0] == ' ' || s[len(s)-1] == ' ' 187 } 188 189 // stringWidth makes it easy to change how the whole app finds string-widths 190 func stringWidth(s string) int { 191 return utf8.RuneCountInString(s) 192 } 193 194 // tryNumeric checks if a string is useable as a number, returning both the 195 // parsed value and its success/suitability 196 func tryNumeric(s string) (f float64, ok bool) { 197 f, err := strconv.ParseFloat(s, 64) 198 if err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) { 199 return f, true 200 } 201 return 0, false 202 } 203 204 func fileExtension(path string) string { 205 ext := filepath.Ext(path) 206 if !isAnyFold(ext, `.gz`, `.bz`, `.bz2`, `.bzip`, `.bzip2`) { 207 return ext 208 } 209 210 // handle gzip double-extension 211 j := len(path) - len(ext) 212 if i := strings.LastIndexByte(path[:j], '.'); i >= 0 { 213 return path[i:] 214 } 215 216 // extension is just .gz 217 return ext 218 } 219 220 // isAnyFold generalizes func strings.EqualFold to multiple values, which are 221 // all compared to the first string given 222 func isAnyFold(s string, options ...string) bool { 223 for _, t := range options { 224 if strings.EqualFold(s, t) { 225 return true 226 } 227 } 228 return false 229 } 230 231 // indexLinkOrDigits is alternative to calling both indexLink and indexDigits, 232 // having to figure out if these overlap, which comes first when both occur, 233 // and implying multiple loops, which slow processing down unnecessarily 234 func indexLinkOrDigits(s string) (isLink bool, i, j int) { 235 for k := 0; k < len(s); k++ { 236 switch s[k] { 237 case 'h': 238 i, j = indexLink(s[k:]) 239 if i >= 0 { 240 return true, k + i, k + j 241 } 242 // not a link, so just keep going 243 244 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 245 n := indexNonDigit(s[k:]) 246 if n >= 0 { 247 return false, k, k + n 248 } 249 return false, k, len(s) 250 } 251 } 252 253 return false, -1, -1 254 } 255 256 // indexLink tries to find a slice for the next (if any) detected web link in 257 // the string given; lack of detected links results in negative indices 258 func indexLink(s string) (i, j int) { 259 i, k := indexWebProtocol(s) 260 if i < 0 || k == len(s) { 261 return -1, -1 262 } 263 264 r, _ := utf8.DecodeRuneInString(s[k:]) 265 if !unicode.IsLetter(r) { 266 return -1, -1 267 } 268 269 isValidByte := isValidLinkDomainByte 270 271 for j = k + 1; j < len(s); j++ { 272 b := s[j] 273 274 // byte-checking logic changes with the first non-protocol slash 275 if b == '/' { 276 isValidByte = isValidLinkPathByte 277 } 278 279 if isValidByte[b] { 280 continue 281 } 282 283 // found end of the link 284 break 285 } 286 287 // exclude trailing dots, which presumably end sentences with trailing 288 // links in them 289 if s[j-1] == '.' { 290 j-- 291 } 292 293 // just a protocol, even if followed by a dot, isn't a valid link 294 if k == j { 295 return -1, -1 296 } 297 298 // found what seems a valid link 299 return i, j 300 } 301 302 // indexWebProtocol simplifies the control-flow of func indexLink 303 func indexWebProtocol(s string) (i, j int) { 304 i = strings.Index(s, `http`) 305 if i < 0 { 306 return -1, -1 307 } 308 309 t := s[i+len(`http`):] 310 if strings.HasPrefix(t, `s://`) { 311 return i, i + len(`https://`) 312 } 313 if strings.HasPrefix(t, `://`) { 314 return i, i + len(`http://`) 315 } 316 return -1, -1 317 } 318 319 func indexNonDigit(s string) int { 320 for i := 0; i < len(s); i++ { 321 switch s[i] { 322 case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 323 continue 324 default: 325 return i 326 } 327 } 328 329 return -1 330 } File: vida/strings_test.go 1 package main 2 3 import ( 4 "strings" 5 "testing" 6 ) 7 8 func TestLoopTSV(t *testing.T) { 9 var tests = []struct { 10 Input string 11 Expected []string 12 }{ 13 {``, nil}, 14 {` `, []string{}}, 15 {"abc\tdef", []string{`abc`, `def`}}, 16 {"abc\tdef\t", []string{`abc`, `def`, ``}}, 17 {"\t\tabc\tdef\t", []string{``, ``, `abc`, `def`, ``}}, 18 } 19 20 for _, tc := range tests { 21 t.Run(tc.Input, func(t *testing.T) { 22 var got []string 23 var expected []string 24 if tc.Input != `` { 25 expected = strings.Split(tc.Input, "\t") 26 } 27 28 loopTSV(tc.Input, func(i int, s string) { 29 if exp := expected[i]; expected[i] != s { 30 const fs = `item @ %d: expected %q, instead of %q` 31 t.Fatalf(fs, i, exp, s) 32 } 33 got = append(got, s) 34 }) 35 36 if len(got) != len(expected) { 37 const fs = `expected %d pieces, instead of %d` 38 t.Fatalf(fs, len(expected), len(got)) 39 } 40 }) 41 } 42 } 43 44 func TestIndexLink(t *testing.T) { 45 var tests = []struct { 46 Input string 47 Start int 48 Stop int 49 }{ 50 {``, -1, -1}, 51 {` `, -1, -1}, 52 {`abc, 123`, -1, -1}, 53 {`abc https:`, -1, -1}, 54 { 55 `https://www.duckduckgo.com/, blah blah`, 56 0, 57 len(`https://www.duckduckgo.com/`), 58 }, 59 { 60 `a link to https://www.duckduckgo.com/, blah blah`, 61 len(`a link to `), 62 len(`a link to https://www.duckduckgo.com/`), 63 }, 64 { 65 `Here's a link to https://www.duckduckgo.com/.`, 66 len(`Here's a link to `), 67 len(`Here's a link to https://www.duckduckgo.com/`), 68 }, 69 { 70 `Here's a link to https://www.duckduckgo.com/: enjoy.`, 71 len(`Here's a link to `), 72 len(`Here's a link to https://www.duckduckgo.com/`), 73 }, 74 { 75 `Here's a link to http://localhost:1234/: try it.`, 76 len(`Here's a link to `), 77 len(`Here's a link to http://localhost:1234/`), 78 }, 79 } 80 81 for _, tc := range tests { 82 t.Run(tc.Input, func(t *testing.T) { 83 i, j := indexLink(tc.Input) 84 if i != tc.Start || j != tc.Stop { 85 const fs = `expected %d and %d, instead of %d and %d` 86 t.Fatalf(fs, tc.Start, tc.Stop, i, j) 87 } 88 }) 89 } 90 } File: vida/style.css 1 body { 2 font-size: 15px; 3 font-family: system-ui, -apple-system, sans-serif; 4 line-height: 1.6rem; 5 } 6 7 p { 8 margin: auto; 9 margin-top: 1rem; 10 margin-bottom: 1rem; 11 12 max-width: 85ch; 13 } 14 15 a { 16 color: steelblue; 17 text-decoration: none; 18 } 19 20 audio, 21 video { 22 height: fit-content; 23 } 24 25 ul, 26 ol { 27 margin: 0.25rem; 28 line-height: 1.45rem; 29 } 30 31 li { 32 padding-right: 1rem; 33 } 34 35 pre { 36 margin: auto; 37 } 38 39 section { 40 width: fit-content; 41 margin: 0.5rem auto 0.5rem auto; 42 } 43 44 details { 45 margin: auto; 46 47 min-width: 10ch; 48 max-width: fit-content; 49 border: solid thin #ddd; 50 } 51 52 li>details { 53 margin-bottom: 1rem; 54 } 55 56 summary { 57 min-width: 10ch; 58 padding: 0 0.5rem; 59 background-color: #eee; 60 } 61 62 td { 63 padding: 0.25rem; 64 vertical-align: top; 65 border-bottom: solid thin transparent; 66 } 67 68 tbody tr:nth-child(5n) td { 69 border-bottom: solid thin lightgray; 70 } 71 72 .pairs { 73 display: grid; 74 padding: 0.5rem; 75 grid-template-columns: max-content max-content; 76 } 77 78 .inline { 79 margin: 0.25rem; 80 display: inline-block; 81 vertical-align: text-top; 82 } 83 84 /* img.inline { 85 vertical-align: middle; 86 } */ 87 88 .array { 89 border: solid thin #ddd; 90 } 91 92 .object { 93 border: solid thin #e0e0e0; 94 } 95 96 .key { 97 color: blueviolet; 98 } 99 100 .key+* { 101 margin-left: 1rem; 102 } 103 104 .padded { 105 /* color: cyan; */ 106 background-color: yellow; 107 } 108 109 .null { 110 color: darkred; 111 } 112 113 .nothing { 114 color: #777; 115 font-style: italic; 116 } 117 118 .i, 119 .integer { 120 color: royalblue; 121 } 122 123 .f, 124 .float64 { 125 color: darkgreen; 126 } 127 128 /* t is a tile cell used in tables */ 129 .t { 130 font-size: small; 131 background-color: white; 132 } 133 134 /* ri is a row-index cell used in tables */ 135 .ri { 136 padding: 0.25rem; 137 padding-right: 0.5rem; 138 padding-left: 1rem; 139 text-align: right; 140 vertical-align: top; 141 border-bottom: solid thin transparent; 142 } 143 144 td>span.i, 145 td>span.f, 146 td>span.integer, 147 td>span.float64 { 148 width: 100%; 149 text-align: right; 150 display: inline-block; 151 } 152 153 td.i, 154 td.f { 155 text-align: right; 156 } 157 158 .bigint, 159 .fraction { 160 color: darkcyan; 161 } 162 163 abbr { 164 color: #444; 165 } 166 167 section a { 168 /* padding: 0.35rem 0.8rem; */ 169 padding: 0rem 0.8rem; 170 } 171 172 img { 173 display: block; 174 margin: auto; 175 } 176 177 hr { 178 margin: auto; 179 color: #eee; 180 } 181 182 183 .spacer { 184 width: 5ch; 185 display: inline-block; 186 } 187 188 /* center-aligned */ 189 .c { 190 text-align: center; 191 } 192 193 /* right-aligned: style for td cells */ 194 .r { 195 text-align: right; 196 } 197 198 /* wide: style for thead th items above numeric columns */ 199 .w { 200 width: 15ch; 201 } 202 203 /* tight container */ 204 .t { 205 padding: 0; 206 } 207 208 /* trendline cells */ 209 .trend { 210 /* border: solid thin #f0f0f0; */ 211 padding: 0; 212 } 213 214 /* give consistent width to columns with counts in them */ 215 .count { 216 width: 7ch; 217 } 218 219 .dim { 220 color: #bbb; 221 } 222 223 .pos { 224 color: green; 225 } 226 227 .zero { 228 color: darkcyan; 229 background-color: #dcf8f8; 230 /* lighter than lightcyan */ 231 background-color: #eaf7f7; 232 /* lighter than lightcyan */ 233 } 234 235 .neg { 236 color: indianred; 237 } 238 239 td.neg { 240 color: firebrick; 241 /* color: tomato; */ 242 } 243 244 .index { 245 color: #aaa; 246 background-color: white; 247 } 248 249 .container { 250 padding-left: 0.2rem; 251 background-color: #eee; 252 border: dotted solid black; 253 } 254 255 /* don't let the left side of summaries touch any dotted borders */ 256 /* details { 257 width: 100%; 258 padding: 0.5rem; 259 } */ 260 261 .general-options, 262 .bubbleplot-options, 263 .dotplot-options { 264 gap: 1ch; 265 display: grid; 266 grid-template-columns: 2fr repeat(3, 3fr); 267 } 268 269 table { 270 margin: 3rem auto; 271 border-collapse: collapse; 272 } 273 274 thead th { 275 top: 0; 276 position: sticky; 277 background-color: white; 278 279 text-align: center; 280 font-weight: bolder; 281 padding: 0.1rem 0.5rem; 282 } 283 284 tfoot th { 285 text-align: center; 286 font-weight: bolder; 287 padding: 0.1rem 0.5rem; 288 user-select: none; 289 } 290 291 /* allow multiline cells */ 292 th { 293 white-space: pre-wrap; 294 } 295 296 td { 297 max-width: 85ch; 298 border: solid thin #f0f0f0; 299 } 300 301 tr:first-child td { 302 border-top: solid medium #bbb; 303 } 304 305 tr:nth-child(5n) td { 306 border-bottom: solid thin #bbb; 307 } 308 309 td.inv { 310 background-color: #ddd; 311 } 312 313 thead th.cat1 { 314 color: #eee; 315 background-color: #444; 316 } 317 318 thead th.cat2 { 319 font-weight: 900; 320 } 321 322 tbody th { 323 background-color: white; 324 } 325 326 .error { 327 padding: 1.5rem; 328 color: whitesmoke; 329 background-color: firebrick; 330 } 331 332 .divider { 333 color: #ccc; 334 } 335 336 .n, 337 .nothing { 338 font-style: italic; 339 color: #ccc; 340 } 341 342 .b { 343 font-style: italic; 344 color: cornflowerblue; 345 } 346 347 .i, 348 .integer { 349 color: cornflowerblue; 350 } 351 352 .f, 353 .float64 { 354 color: forestgreen; 355 } 356 357 .k, 358 .key { 359 color: mediumpurple; 360 } 361 362 .kv { 363 margin: 0; 364 } 365 366 .kv thead { 367 display: none; 368 } 369 370 tr.c1 td { 371 background-color: #ffa50040; 372 } 373 374 tr.c1 th.ri { 375 color: #ffa500; 376 } 377 378 tr.c2 td { 379 background-color: #4b008230; 380 } 381 382 tr.c2 th.ri { 383 color: #4b008280; 384 } 385 386 tr.c3 td { 387 background-color: #5f9ea040; 388 } 389 390 tr.c3 th.ri { 391 color: #5f9ea0; 392 } 393 394 tr.c4 td { 395 background-color: #ffc0cb40; 396 } 397 398 tr.c4 th.ri { 399 color: #ffc0cb; 400 } 401 402 tr.c5 td { 403 background-color: #8b451340; 404 } 405 406 tr.c5 th.ri { 407 color: #8b4513; 408 } 409 410 tr.c6 td { 411 background-color: #c3c3c340; 412 } 413 414 tr.c6 th.ri { 415 color: #c3c3c3; 416 } 417 418 tr.c7 td { 419 background-color: #ffff0040; 420 } 421 422 tr.c7 th.ri { 423 color: #ffff0080; 424 } File: vida/tables.go 1 package main 2 3 import ( 4 "encoding/csv" 5 "errors" 6 "io" 7 "math" 8 "strconv" 9 "strings" 10 ) 11 12 type columnMeta struct { 13 // Name is the column's name, taken from the first row/line 14 Name string 15 16 // Empty is how many items in the column are empty/missing 17 Empty int 18 19 // Padded is how many items in the column start/end with spaces 20 Padded int 21 22 // Numeric is how many number-like values the column has 23 Numeric int 24 25 // MaxDecimals is used to right-pad the column's number-like values 26 MaxDecimals int 27 28 // MaxWidth is the column's max width in runes, and is used to 29 // pad the column's values 30 MaxWidth int 31 32 Min float64 33 Max float64 34 Sum float64 35 } 36 37 // newColumnMeta is columnMeta's constructor, which is needed as zero-like 38 // values are the wrong way to start some of its fields 39 func newColumnMeta() columnMeta { 40 return columnMeta{ 41 Min: math.Inf(+1), 42 Max: math.Inf(-1), 43 } 44 } 45 46 // update is used to update the column's stats; don't use with the 1st row 47 func (cm *columnMeta) update(s string) { 48 if len(s) == 0 { 49 cm.Empty++ 50 return 51 } 52 53 if isPadded(s) { 54 cm.Padded++ 55 } 56 57 n := stringWidth(s) 58 cm.MaxWidth = maxInt(cm.MaxWidth, n) 59 60 if f, ok := tryNumeric(s); ok { 61 cm.Sum += f 62 cm.Min = math.Min(cm.Min, f) 63 cm.Max = math.Max(cm.Max, f) 64 cm.Numeric++ 65 66 // recheck widths in case regular (non-exponential) number-notation 67 // is wider than the original string 68 span, decs := float64width(f) 69 cm.MaxWidth = maxInt(cm.MaxWidth, span) 70 cm.MaxDecimals = maxInt(cm.MaxDecimals, decs) 71 } 72 } 73 74 // matchColumn tries to find which column a string may refer to, using various 75 // fairly-reliable heuristics, which are used when there are no exact matches 76 func matchColumn(k string, cols []columnMeta) int { 77 // empty strings match as prefixes to any string, according to both func 78 // strings.HasPrefix and func hasPrefixFold 79 if len(k) == 0 { 80 return -1 81 } 82 83 // try non-zero number indices 84 if i, err := strconv.ParseInt(k, 10, 64); err == nil { 85 return matchColumnIndex(int(i), cols) 86 } 87 88 // try exact matches 89 for i, c := range cols { 90 if k == c.Name { 91 return i 92 } 93 } 94 95 // try case-insensitive matches 96 for i, c := range cols { 97 if strings.EqualFold(k, c.Name) { 98 return i 99 } 100 } 101 102 // try exact prefix matches 103 for i, c := range cols { 104 if strings.HasPrefix(c.Name, k) { 105 return i 106 } 107 } 108 109 // try case-insensitive prefix matches 110 for i, c := range cols { 111 if hasPrefixFold(c.Name, k) { 112 return i 113 } 114 } 115 116 return -1 117 } 118 119 func matchColumnIndex(i int, cols []columnMeta) int { 120 if 1 <= i && i <= len(cols) { 121 return i - 1 122 } 123 if -len(cols) <= i && i < 0 { 124 return i + len(cols) 125 } 126 return -1 127 } 128 129 type visualTallier func(w writer, row []string) 130 131 func (rp *rowParams) configBullets(k string) { 132 if k == `` { 133 rp.WriteBullets = func(w writer, row []string) {} 134 rp.BulletsIndex = -1 135 return 136 } 137 138 cols := rp.Columns 139 i := matchColumn(k, cols) 140 if i < 0 { 141 rp.WriteBullets = func(w writer, row []string) {} 142 rp.BulletsIndex = -1 143 return 144 } 145 146 c := cols[i] 147 if c.Numeric == 0 || c.Max <= 0 { 148 rp.WriteBullets = func(w writer, row []string) {} 149 rp.BulletsIndex = -1 150 return 151 } 152 153 div := 1.0 154 if k := 25.0; c.Max > k { 155 log := math.Log10(c.Max) 156 div = math.Pow10(int(log)) / k 157 } 158 159 maxWidthLast := cols[len(cols)-1].MaxWidth 160 needPadding := rp.CategoryIndex != len(cols)-1 161 162 rp.BulletsIndex = i 163 rp.WriteBullets = func(w writer, row []string) { 164 if i >= len(row) { 165 return 166 } 167 168 f, ok := tryNumeric(row[i]) 169 if !ok || f <= 0 || math.IsNaN(f) || math.IsInf(f, 0) { 170 return 171 } 172 173 n := 0 174 if needPadding { 175 n = stringWidth(row[len(row)-1]) 176 n = maxInt(maxWidthLast-n, 0) 177 } 178 179 writeSpaces(w, n+columnGap) 180 writeBullets(w, int(f/div)) 181 } 182 } 183 184 func writeTilesANSI(w writer, row []string) { 185 for _, s := range row { 186 style, tile := matchTileANSI(s) 187 w.WriteString(style) 188 w.WriteString(tile) 189 } 190 191 w.WriteString(resetStyleANSI) 192 writeSpaces(w, columnGap) 193 } 194 195 type rowParams struct { 196 // Columns has metadata for all table columns 197 Columns []columnMeta 198 199 RowCountMaxWidth int 200 201 // CategoryIndex marks which column to color-code, if any 202 CategoryIndex int 203 204 BulletsIndex int 205 206 // Colors keeps track of which color-table index to use for which unique 207 // value 208 Colors map[string]int 209 210 WriteBullets visualTallier 211 } 212 213 func newRowParams(nrows int, cols []columnMeta, cfg config) rowParams { 214 var rp rowParams 215 rp.Columns = cols 216 rp.CategoryIndex = matchColumn(cfg.Colors, cols) 217 rp.Colors = make(map[string]int) 218 rp.RowCountMaxWidth = int64width(int64(nrows)) 219 rp.configBullets(cfg.Bullets) 220 return rp 221 } 222 223 func (rp rowParams) matchColor(s string, with []string) string { 224 n, ok := rp.Colors[s] 225 if !ok { 226 n = len(rp.Colors) % len(with) 227 rp.Colors[s] = n 228 } 229 return with[n] 230 } 231 232 func writeRowValuesANSI(w writer, row []string, p rowParams) { 233 gapDue := 0 234 235 for i, s := range row { 236 col := p.Columns[i] 237 writeSpaces(w, columnGap+gapDue) 238 239 if i == p.CategoryIndex && p.Colors != nil { 240 w.WriteString(p.matchColor(s, paletteANSI)) 241 w.WriteString(s) 242 writeSpaces(w, col.MaxWidth-stringWidth(s)) 243 w.WriteString(resetStyleANSI) 244 gapDue = 0 245 continue 246 } 247 248 if f, ok := tryNumeric(s); ok { 249 writeNumericANSI(w, f, col) 250 gapDue = 0 251 continue 252 } 253 254 if len(s) == 0 { 255 w.WriteString(emptyStyleANSI) 256 writeSpaces(w, col.MaxWidth) 257 w.WriteString(resetStyleANSI) 258 gapDue = 0 259 continue 260 } 261 262 writeStringANSI(w, s) 263 n := stringWidth(s) 264 gapDue = col.MaxWidth - n 265 } 266 } 267 268 func writeRowValuesPlain(w writer, row []string, p rowParams) { 269 gapDue := 0 270 271 for i, s := range row { 272 col := p.Columns[i] 273 writeSpaces(w, columnGap+gapDue) 274 275 if f, ok := tryNumeric(s); ok { 276 writeNumericPlain(w, f, col) 277 gapDue = 0 278 continue 279 } 280 281 w.WriteString(s) 282 n := stringWidth(s) 283 gapDue = col.MaxWidth - n 284 } 285 } 286 287 func newReaderCSV(r io.Reader) *csv.Reader { 288 rr := csv.NewReader(r) 289 rr.Comma = ',' 290 rr.LazyQuotes = true 291 rr.ReuseRecord = true 292 rr.FieldsPerRecord = -1 293 return rr 294 } 295 296 func newWriterCSV(w io.Writer) *csv.Writer { 297 rw := csv.NewWriter(w) 298 rw.UseCRLF = false 299 rw.Comma = ',' 300 return rw 301 } 302 303 func csv2tsv(w writer, r io.Reader, name string, cfg config) error { 304 rr := newReaderCSV(r) 305 306 for { 307 row, err := rr.Read() 308 if err == io.EOF { 309 return nil 310 } 311 if err != nil { 312 return err 313 } 314 315 for i, s := range row { 316 if i > 0 { 317 w.WriteByte('\t') 318 } 319 w.WriteString(s) 320 } 321 322 if err := endLine(w); err != nil { 323 return err 324 } 325 } 326 } 327 328 func csv2json(w writer, r io.Reader, name string, cfg config) error { 329 return csv2jsonImpl(w, r, writeRowJSON) 330 } 331 332 func csv2jsons(w writer, r io.Reader, name string, cfg config) error { 333 return csv2jsonImpl(w, r, writeRowJSONS) 334 } 335 336 func csv2jsonImpl(w writer, r io.Reader, fn kvRowWriter) error { 337 var keys []string 338 rr := newReaderCSV(r) 339 340 for i := 0; true; i++ { 341 row, err := rr.Read() 342 if err == io.EOF { 343 if i > 0 { 344 w.WriteString("\n]\n") 345 } else { 346 w.WriteString("[]\n") 347 } 348 return nil 349 } 350 if err != nil { 351 return err 352 } 353 354 if i == 0 { 355 keys = append(keys, row...) 356 continue 357 } 358 359 if i > 1 { 360 _, err = w.WriteString(",\n") 361 writeSpaces(w, jsonLevelIndent) 362 } else { 363 _, err = w.WriteString("[\n") 364 writeSpaces(w, jsonLevelIndent) 365 } 366 367 if err = adaptWriterError(err); err != nil { 368 return err 369 } 370 371 fn(w, keys, row) 372 } 373 374 // make the compiler happy 375 return nil 376 } 377 378 func tsv2csv(w writer, r io.Reader, name string, cfg config) error { 379 var row []string 380 rw := newWriterCSV(w) 381 defer rw.Flush() 382 383 return loopLines(r, func(line string) error { 384 if len(line) == 0 { 385 return nil 386 } 387 388 row = appendTSV(row[:0], line) 389 return rw.Write(row) 390 }) 391 } 392 393 func tsv2json(w writer, r io.Reader, name string, cfg config) error { 394 return tsv2jsonImpl(w, r, writeRowJSON) 395 } 396 397 func tsv2jsons(w writer, r io.Reader, name string, cfg config) error { 398 return tsv2jsonImpl(w, r, writeRowJSONS) 399 } 400 401 func tsv2jsonImpl(w writer, r io.Reader, fn kvRowWriter) error { 402 var row []string 403 var keys []string 404 405 rows := 0 406 err := loopLines(r, func(line string) error { 407 if len(line) == 0 { 408 return nil 409 } 410 411 row = appendTSV(row[:0], line) 412 if len(keys) == 0 { 413 keys = append(keys, row...) 414 return nil 415 } 416 417 var err error 418 if rows > 0 { 419 _, err = w.WriteString(",\n") 420 writeSpaces(w, jsonLevelIndent) 421 } else { 422 _, err = w.WriteString("[\n") 423 writeSpaces(w, jsonLevelIndent) 424 } 425 426 if err = adaptWriterError(err); err != nil { 427 return err 428 } 429 430 rows++ 431 fn(w, keys, row) 432 return nil 433 }) 434 435 if rows > 0 { 436 w.WriteString("\n]\n") 437 } else { 438 w.WriteString("[]\n") 439 } 440 return err 441 } 442 443 func makeWholeTableReader(cfg config) (wholeTableReader, error) { 444 from := strings.TrimSuffix(cfg.From, `.gz`) 445 446 switch from { 447 case `csv`: 448 return &wholeReaderCSV{}, nil 449 case `tsv`: 450 return &wholeReaderTSV{}, nil 451 default: 452 msg := `internal bug: invalid table input-type ` + from 453 return nil, errors.New(msg) 454 } 455 } 456 457 func makeTableWriter(w writer, rp rowParams, cfg config) (tableWriter, error) { 458 switch cfg.To { 459 case `ansi`: 460 return &ansiTableWriter{out: w, cfg: rp}, nil 461 462 case `plain`: 463 return &plainTableWriter{out: w, cfg: rp}, nil 464 465 case `html`: 466 return &htmlTableWriter{out: w, cfg: rp}, nil 467 468 default: 469 msg := `internal bug: invalid output-type ` + cfg.From 470 return nil, errors.New(msg) 471 } 472 } 473 474 func convertWholeTable(w writer, r io.Reader, name string, cfg config) error { 475 wtr, err := makeWholeTableReader(cfg) 476 if err != nil { 477 return err 478 } 479 480 nrows, cols, err := wtr.Read(r) 481 if err != nil { 482 return err 483 } 484 485 rp := newRowParams(nrows, cols, cfg) 486 487 tw, err := makeTableWriter(w, rp, cfg) 488 if err != nil { 489 return err 490 } 491 492 tw.BeforeTable() 493 defer tw.AfterTable() 494 495 tw.WriteHeaderRow(cols) 496 if err := endLine(w); err != nil { 497 return err 498 } 499 500 return wtr.Loop(func(i int, row []string) error { 501 tw.StartRow(row) 502 tw.WriteTiles(row) 503 tw.WriteRowIndex(i+1, row) 504 tw.WriteRowValues(row) 505 rp.WriteBullets(w, row) 506 return tw.EndRow() 507 }) 508 } 509 510 type wholeTableReader interface { 511 Read(r io.Reader) (nrows int, cols []columnMeta, err error) 512 Loop(fn func(i int, row []string) error) error 513 } 514 515 type wholeReaderCSV struct { 516 // dataRows has all split-rows, excluding the first/header row 517 dataRows [][]string 518 } 519 520 func (wrc *wholeReaderCSV) Read(r io.Reader) (nrows int, cols []columnMeta, err error) { 521 rr := newReaderCSV(r) 522 523 lines, err := rr.ReadAll() 524 if err != nil { 525 return -1, nil, err 526 } 527 if len(lines) == 0 { 528 return -1, nil, nil 529 } 530 531 header := lines[0] 532 cols = make([]columnMeta, 0, len(header)) 533 for _, s := range header { 534 cm := newColumnMeta() 535 cm.Name = s 536 cm.MaxWidth = stringWidth(s) 537 cols = append(cols, cm) 538 } 539 540 rows := lines[1:] 541 for _, row := range lines { 542 for j, s := range row { 543 if j == len(cols) { 544 cols = append(cols, newColumnMeta()) 545 } 546 cols[j].update(s) 547 } 548 } 549 nrows = len(rows) 550 551 if len(cols) == 0 { 552 return nrows, cols, errors.New(`empty table`) 553 } 554 555 wrc.dataRows = rows 556 return nrows, cols, err 557 } 558 559 func (wrc wholeReaderCSV) Loop(fn func(i int, row []string) error) error { 560 for i, row := range wrc.dataRows { 561 if err := fn(i, row); err != nil { 562 return err 563 } 564 } 565 return nil 566 } 567 568 type wholeReaderTSV struct { 569 // lines has all unsplit lines, except the first one 570 lines []string 571 } 572 573 func (wrt *wholeReaderTSV) Read(r io.Reader) (nrows int, cols []columnMeta, err error) { 574 var lines []string 575 err = loopLines(r, func(line string) error { 576 if len(line) == 0 { 577 return nil 578 } 579 580 if len(cols) == 0 { 581 n := countItemsTSV(line) 582 cols = make([]columnMeta, 0, n) 583 loopTSV(line, func(i int, s string) { 584 cm := newColumnMeta() 585 cm.Name = s 586 cm.MaxWidth = stringWidth(s) 587 cols = append(cols, cm) 588 }) 589 return nil 590 } 591 592 loopTSV(line, func(i int, s string) { 593 if i == len(cols) { 594 cols = append(cols, newColumnMeta()) 595 } 596 cols[i].update(s) 597 }) 598 599 lines = append(lines, line) 600 return nil 601 }) 602 603 nrows = len(lines) 604 if err != nil { 605 return nrows, cols, err 606 } 607 608 if len(cols) == 0 { 609 return nrows, cols, errors.New(`empty table`) 610 } 611 wrt.lines = lines 612 return nrows, cols, nil 613 } 614 615 func (wrt wholeReaderTSV) Loop(fn func(i int, row []string) error) error { 616 var items []string 617 for i, row := range wrt.lines { 618 items = appendTSV(items[:0], row) 619 if err := fn(i, items); err != nil { 620 return err 621 } 622 } 623 return nil 624 } 625 626 type tableWriter interface { 627 BeforeTable() error 628 AfterTable() error 629 WriteHeaderRow(cols []columnMeta) error 630 WriteTiles(values []string) error 631 WriteRowIndex(i int, values []string) error 632 WriteRowValues(values []string) error 633 634 StartRow(values []string) error 635 EndRow() error 636 } 637 638 type ansiTableWriter struct { 639 out writer 640 cfg rowParams 641 } 642 643 func (w ansiTableWriter) BeforeTable() error { 644 return nil 645 } 646 647 func (w ansiTableWriter) AfterTable() error { 648 return nil 649 } 650 651 func (tw ansiTableWriter) WriteHeaderRow(cols []columnMeta) error { 652 names := make([]string, 0, len(cols)) 653 for _, c := range cols { 654 names = append(names, c.Name) 655 } 656 tw.WriteTiles(names) 657 tw.WriteRowIndex(-1, nil) 658 return writeHeaderRowANSI(tw.out, tw.cfg) 659 } 660 661 func (w ansiTableWriter) WriteTiles(values []string) error { 662 writeTilesANSI(w.out, values) 663 return nil 664 } 665 666 func (w ansiTableWriter) WriteRowIndex(i int, values []string) error { 667 if i < 0 { 668 writeSpaces(w.out, w.cfg.RowCountMaxWidth) 669 return nil 670 } 671 672 j := w.cfg.CategoryIndex 673 ok := w.cfg.Colors != nil && 0 <= j && j < len(values) 674 675 if !ok || len(values[j]) == 0 { 676 padWriteInt64(w.out, int64(i), w.cfg.RowCountMaxWidth) 677 return nil 678 } 679 680 style := w.cfg.matchColor(values[j], paletteANSI) 681 w.out.WriteString(style) 682 padWriteInt64(w.out, int64(i), w.cfg.RowCountMaxWidth) 683 w.out.WriteString(resetStyleANSI) 684 return nil 685 } 686 687 func (w ansiTableWriter) WriteRowValues(values []string) error { 688 writeRowValuesANSI(w.out, values, w.cfg) 689 return nil 690 } 691 692 func (w ansiTableWriter) StartRow(values []string) error { 693 return nil 694 } 695 696 func (w ansiTableWriter) EndRow() error { 697 return endLine(w.out) 698 } 699 700 type plainTableWriter struct { 701 out writer 702 cfg rowParams 703 } 704 705 func (w plainTableWriter) BeforeTable() error { 706 return nil 707 } 708 709 func (w plainTableWriter) AfterTable() error { 710 return nil 711 } 712 713 func (tw plainTableWriter) WriteHeaderRow(cols []columnMeta) error { 714 names := make([]string, 0, len(cols)) 715 for _, c := range cols { 716 names = append(names, c.Name) 717 } 718 tw.WriteTiles(names) 719 tw.WriteRowIndex(-1, nil) 720 return writeHeaderRowPlain(tw.out, tw.cfg) 721 } 722 723 func (tw plainTableWriter) WriteTiles(values []string) error { 724 w := tw.out 725 726 for _, s := range values { 727 if len(s) == 0 { 728 w.WriteString(emptyTile) 729 continue 730 } 731 732 if f, ok := tryNumeric(s); ok { 733 if f > 0 { 734 w.WriteByte('+') 735 continue 736 } 737 if f < 0 { 738 w.WriteByte('-') 739 continue 740 } 741 w.WriteByte('0') 742 continue 743 } 744 745 w.WriteString(usualTile) 746 } 747 748 writeSpaces(w, columnGap) 749 return nil 750 } 751 752 func (w plainTableWriter) WriteRowIndex(i int, values []string) error { 753 if i >= 0 { 754 padWriteInt64(w.out, int64(i), w.cfg.RowCountMaxWidth) 755 return nil 756 } 757 writeSpaces(w.out, w.cfg.RowCountMaxWidth) 758 return nil 759 } 760 761 func (w plainTableWriter) WriteRowValues(values []string) error { 762 writeRowValuesPlain(w.out, values, w.cfg) 763 return nil 764 } 765 766 func (w plainTableWriter) StartRow(values []string) error { 767 return nil 768 } 769 770 func (w plainTableWriter) EndRow() error { 771 return endLine(w.out) 772 } 773 774 func writeHeaderRowANSI(w writer, cfg rowParams) error { 775 gapDue := 0 776 cols := cfg.Columns 777 778 for i, c := range cols { 779 s := c.Name 780 writeSpaces(w, columnGap+gapDue) 781 782 if i == cfg.CategoryIndex { 783 w.WriteString(swappedStyleANSI) 784 w.WriteString(s) 785 n := stringWidth(s) 786 writeSpaces(w, c.MaxWidth-n) 787 w.WriteString(resetStyleANSI) 788 gapDue = 0 789 continue 790 } 791 792 w.WriteString(s) 793 n := stringWidth(s) 794 gapDue = c.MaxWidth - n 795 } 796 797 extraCol := cfg.BulletsIndex 798 if len(cols) > 0 && 0 <= extraCol && extraCol < len(cols) { 799 n := 0 800 if cfg.CategoryIndex != len(cols)-1 { 801 last := cols[len(cols)-1] 802 n = last.MaxWidth - stringWidth(last.Name) 803 } 804 writeSpaces(w, columnGap+maxInt(n, 0)) 805 w.WriteString(cols[extraCol].Name) 806 } 807 return nil 808 } 809 810 func writeHeaderRowPlain(w writer, cfg rowParams) error { 811 cfg.CategoryIndex = -1 812 return writeHeaderRowANSI(w, cfg) 813 } File: vida/text.go 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/base64" 7 "errors" 8 "io" 9 ) 10 11 func decodeBase64(w writer, r io.Reader, name string, cfg config) error { 12 var chunk [4096]byte 13 n, err := r.Read(chunk[:]) 14 if err != nil { 15 return err 16 } 17 read := chunk[:n] 18 19 // handle direct base64 input, instead of prefixed base64 data-URIs 20 if !bytes.HasPrefix(read, []byte{'d', 'a', 't', 'a', ':'}) { 21 r = io.MultiReader(bytes.NewReader(read), r) 22 dec := base64.NewDecoder(base64.RawStdEncoding, r) 23 _, err := io.Copy(w, dec) 24 return err 25 } 26 27 // don't bother searching for base64-markers far from the start 28 start := read 29 if len(start) > 32 { 30 start = read[:32] 31 } 32 33 var marker = []byte{';', 'b', 'a', 's', 'e', '6', '4', ','} 34 i := bytes.Index(start, marker) 35 if i < 0 { 36 return errors.New(`data-URI doesn't have base64-encoding marker`) 37 } 38 39 // handle data-URIs by skipping their leading metadata 40 r = io.MultiReader(bytes.NewReader(read[i+len(marker):]), r) 41 dec := base64.NewDecoder(base64.RawStdEncoding, r) 42 _, err = io.Copy(w, dec) 43 return err 44 } 45 46 func text2ansi(w writer, r io.Reader, name string, cfg config) error { 47 const reset = resetStyleANSI 48 49 var digitsStyle string 50 s, ok := lookupStyleANSI(cfg.DigitsStyle) 51 if !ok { 52 return errors.New(`unsupported style name ` + cfg.DigitsStyle) 53 } 54 digitsStyle = s 55 56 var linksStyle string 57 s, ok = lookupStyleANSI(cfg.LinksStyle) 58 if !ok { 59 return errors.New(`unsupported style name ` + cfg.LinksStyle) 60 } 61 linksStyle = s 62 63 if digitsStyle == reset { 64 digitsStyle = `` 65 } 66 if linksStyle == reset { 67 linksStyle = `` 68 } 69 70 if len(digitsStyle) == 0 && len(linksStyle) == 0 { 71 return loopLines(r, func(line string) error { 72 w.WriteString(line) 73 return endLine(w) 74 }) 75 } 76 77 return loopLines(r, func(line string) error { 78 return line2ansi(w, line, digitsStyle, linksStyle) 79 }) 80 } 81 82 func line2ansi(w *bufio.Writer, line string, digits, links string) error { 83 const reset = resetStyleANSI 84 85 for len(line) > 0 { 86 isLink, i, j := indexLinkOrDigits(line) 87 if i < 0 { 88 w.WriteString(line) 89 return endLine(w) 90 } 91 92 if isLink { 93 if len(links) != 0 { 94 w.WriteString(line[:i]) 95 w.WriteString(links) 96 w.WriteString(line[i:j]) 97 w.WriteString(reset) 98 } else { 99 w.WriteString(line[:j]) 100 } 101 } else { 102 if len(digits) != 0 { 103 w.WriteString(line[:i]) 104 alternateDigitGroupsString(w, line[i:j], digits, reset) 105 } else { 106 w.WriteString(line[:j]) 107 } 108 } 109 110 line = line[j:] 111 } 112 113 return endLine(w) 114 }