File: coma.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2025 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the “Software”), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 To compile a smaller-sized command-line app, you can use the `go` command as
  27 follows:
  28 
  29 go build -ldflags "-s -w" -trimpath coma.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "bytes"
  37     "os"
  38     "regexp"
  39 )
  40 
  41 var styleAliases = map[string]string{
  42     `b`: `blue`,
  43     `g`: `green`,
  44     `m`: `magenta`,
  45     `o`: `orange`,
  46     `p`: `purple`,
  47     `r`: `red`,
  48     `u`: `underline`,
  49 
  50     `bb`: `blueback`,
  51     `bg`: `greenback`,
  52     `bm`: `magentaback`,
  53     `bo`: `orangeback`,
  54     `bp`: `purpleback`,
  55     `br`: `redback`,
  56 
  57     `gb`: `greenback`,
  58     `mb`: `magentaback`,
  59     `ob`: `orangeback`,
  60     `pb`: `purpleback`,
  61     `rb`: `redback`,
  62 
  63     `hi`:  `inverse`,
  64     `inv`: `inverse`,
  65     `mag`: `magenta`,
  66 
  67     `du`: `doubleunderline`,
  68 
  69     `flip`: `inverse`,
  70     `swap`: `inverse`,
  71 
  72     `reset`:     `plain`,
  73     `highlight`: `inverse`,
  74     `hilite`:    `inverse`,
  75     `invert`:    `inverse`,
  76     `inverted`:  `inverse`,
  77     `swapped`:   `inverse`,
  78 
  79     `dunderline`:  `doubleunderline`,
  80     `dunderlined`: `doubleunderline`,
  81 
  82     `strikethrough`: `strike`,
  83     `strikethru`:    `strike`,
  84     `struck`:        `strike`,
  85 
  86     `underlined`: `underline`,
  87 
  88     `bblue`:    `blueback`,
  89     `bgray`:    `grayback`,
  90     `bgreen`:   `greenback`,
  91     `bmagenta`: `magentaback`,
  92     `borange`:  `orangeback`,
  93     `bpurple`:  `purpleback`,
  94     `bred`:     `redback`,
  95 
  96     `bgblue`:    `blueback`,
  97     `bggray`:    `grayback`,
  98     `bggreen`:   `greenback`,
  99     `bgmag`:     `magentaback`,
 100     `bgmagenta`: `magentaback`,
 101     `bgorange`:  `orangeback`,
 102     `bgpurple`:  `purpleback`,
 103     `bgred`:     `redback`,
 104 
 105     `bluebg`:    `blueback`,
 106     `graybg`:    `grayback`,
 107     `greenbg`:   `greenback`,
 108     `magbg`:     `magentaback`,
 109     `magentabg`: `magentaback`,
 110     `orangebg`:  `orangeback`,
 111     `purplebg`:  `purpleback`,
 112     `redbg`:     `redback`,
 113 
 114     `backblue`:    `blueback`,
 115     `backgray`:    `grayback`,
 116     `backgreen`:   `greenback`,
 117     `backmag`:     `magentaback`,
 118     `backmagenta`: `magentaback`,
 119     `backorange`:  `orangeback`,
 120     `backpurple`:  `purpleback`,
 121     `backred`:     `redback`,
 122 }
 123 
 124 var styles = map[string]string{
 125     `blue`:            "\x1b[38;2;0;95;215m",
 126     `bold`:            "\x1b[1m",
 127     `doubleunderline`: "\x1b[21m",
 128     `gray`:            "\x1b[38;2;168;168;168m",
 129     `green`:           "\x1b[38;2;0;135;95m",
 130     `inverse`:         "\x1b[7m",
 131     `magenta`:         "\x1b[38;2;215;0;255m",
 132     `orange`:          "\x1b[38;2;215;95;0m",
 133     `plain`:           "\x1b[0m",
 134     `purple`:          "\x1b[38;2;135;95;255m",
 135     `red`:             "\x1b[38;2;204;0;0m",
 136     `strike`:          "\x1b[9m",
 137     `underline`:       "\x1b[4m",
 138 
 139     `blueback`:    "\x1b[48;2;0;95;215m\x1b[38;2;238;238;238m",
 140     `grayback`:    "\x1b[48;2;168;168;168m\x1b[38;2;238;238;238m",
 141     `greenback`:   "\x1b[48;2;0;135;95m\x1b[38;2;238;238;238m",
 142     `magentaback`: "\x1b[48;2;215;0;255m\x1b[38;2;238;238;238m",
 143     `orangeback`:  "\x1b[48;2;215;95;0m\x1b[38;2;238;238;238m",
 144     `purpleback`:  "\x1b[48;2;135;95;255m\x1b[38;2;238;238;238m",
 145     `redback`:     "\x1b[48;2;204;0;0m\x1b[38;2;238;238;238m",
 146 }
 147 
 148 type patternStylePair struct {
 149     expr  *regexp.Regexp
 150     style string
 151 }
 152 
 153 func main() {
 154     if len(os.Args)%2 != 1 {
 155         const msg = `you forgot the style-name for/after the last regex`
 156         os.Stderr.WriteString("\x1b[31m")
 157         os.Stderr.WriteString(msg)
 158         os.Stderr.WriteString("\x1b[0m\n")
 159         os.Exit(1)
 160     }
 161 
 162     nerr := 0
 163     pairs := make([]patternStylePair, 0, len(os.Args[1:])/2)
 164 
 165     for args := os.Args[1:]; len(args) >= 2; args = args[2:] {
 166         resrc := args[0]
 167         sname := args[1]
 168 
 169         e, err := regexp.Compile(resrc)
 170         if err != nil {
 171             os.Stderr.WriteString("\x1b[31m")
 172             os.Stderr.WriteString(err.Error())
 173             os.Stderr.WriteString("\x1b[0m\n")
 174             nerr++
 175         }
 176 
 177         if alias, ok := styleAliases[sname]; ok {
 178             sname = alias
 179         }
 180 
 181         style, ok := styles[sname]
 182         if !ok {
 183             os.Stderr.WriteString("\x1b[31mno style named `")
 184             os.Stderr.WriteString(args[1])
 185             os.Stderr.WriteString("`\x1b[0m\n")
 186             nerr++
 187         }
 188 
 189         pairs = append(pairs, patternStylePair{expr: e, style: style})
 190     }
 191 
 192     if nerr > 0 {
 193         os.Exit(1)
 194     }
 195 
 196     sc := bufio.NewScanner(os.Stdin)
 197     sc.Buffer(nil, 8*1024*1024*1024)
 198     bw := bufio.NewWriter(os.Stdout)
 199 
 200     for sc.Scan() {
 201         handleLine(bw, sc.Bytes(), pairs)
 202         bw.WriteByte('\n')
 203         if err := bw.Flush(); err != nil {
 204             return
 205         }
 206     }
 207 }
 208 
 209 // indexEscapeSequence finds the first ANSI-style escape-sequence, which is
 210 // either the alert/bell byte, or the multi-byte sequences starting either
 211 // with ESC[ or ESC]; either returned index can be negative
 212 func indexEscapeSequence(s []byte) (int, int) {
 213     var prev byte
 214 
 215     for i, b := range s {
 216         if b == '\a' {
 217             return i, i + 1
 218         }
 219 
 220         if prev == '\x1b' && b == '[' {
 221             j := indexLetter(s[i+1:])
 222             if j < 0 {
 223                 return i, -1
 224             }
 225             return i - 1, i + 1 + j + 1
 226         }
 227 
 228         if prev == '\x1b' && b == ']' {
 229             j := bytes.IndexByte(s[i+1:], ':')
 230             if j < 0 {
 231                 return i, -1
 232             }
 233             return i - 1, i + 1 + j + 1
 234         }
 235 
 236         if prev == '\x1b' && b == '\\' {
 237             return i - 1, i + 1
 238         }
 239 
 240         prev = b
 241     }
 242 
 243     return -1, -1
 244 }
 245 
 246 func indexLetter(s []byte) int {
 247     for i, b := range s {
 248         if 'A' <= b && b <= 'Z' {
 249             return i
 250         }
 251         if 'a' <= b && b <= 'z' {
 252             return i
 253         }
 254     }
 255 
 256     return -1
 257 }
 258 
 259 func handleLine(w *bufio.Writer, s []byte, with []patternStylePair) {
 260     for len(s) > 0 {
 261         i, j := indexEscapeSequence(s)
 262         if i < 0 {
 263             handleLineChunk(w, s, with)
 264             return
 265         }
 266 
 267         handleLineChunk(w, s[:i], with)
 268         w.Write(s[i:j])
 269 
 270         if j < 0 {
 271             break
 272         }
 273         s = s[j:]
 274     }
 275 }
 276 
 277 func handleLineChunk(w *bufio.Writer, s []byte, with []patternStylePair) {
 278     start := -1
 279     end := -1
 280     which := -1
 281 
 282     for len(s) > 0 {
 283         start = -1
 284         for i, pair := range with {
 285             span := pair.expr.FindIndex(s)
 286             if span != nil && (span[0] < start || start < 0) {
 287                 start = span[0]
 288                 end = span[1]
 289                 which = i
 290             }
 291         }
 292 
 293         if start < 0 {
 294             w.Write(s)
 295             return
 296         }
 297 
 298         w.Write(s[:start])
 299         w.WriteString(with[which].style)
 300         w.Write(s[start:end])
 301         w.WriteString("\x1b[0m")
 302         s = s[end:]
 303     }
 304 }