package tml import ( "fmt" "io" "strings" ) // Parser is used to parse a TML string into an output string containing ANSI escape codes type Parser struct { writer io.Writer IncludeLeadingResets bool IncludeTrailingResets bool state parserState } type parserState struct { fg string bg string attrs attrs } type attrs uint8 const ( bold uint8 = 1 << iota dim underline blink reverse hidden italic strikethrough ) var resetAll = "\x1b[0m" var resetFg = "\x1b[39m" var resetBg = "\x1b[49m" var attrMap = map[uint8]string{ bold: "\x1b[1m", dim: "\x1b[2m", italic: "\x1b[3m", underline: "\x1b[4m", blink: "\x1b[5m", reverse: "\x1b[7m", hidden: "\x1b[8m", strikethrough: "\x1b[9m", } func (s *parserState) setFg(esc string) string { if s.fg == esc { return "" } s.fg = esc return esc } func (s *parserState) setBg(esc string) string { if s.bg == esc { return "" } s.bg = esc return esc } func (s *parserState) setAttr(attr int8) string { output := "" if attr < 0 { output = resetAll + s.fg + s.bg } s.attrs = attrs(uint8(s.attrs) + uint8(attr)) for attr, esc := range attrMap { if uint8(s.attrs)&attr > 0 { output += esc } } return output } // NewParser creates a new parser that writes to w func NewParser(w io.Writer) *Parser { return &Parser{ writer: w, IncludeLeadingResets: true, IncludeTrailingResets: true, } } func (p *Parser) handleTag(name string) bool { if strings.HasPrefix(name, "/") { name = name[1:] if _, isFg := fgTags[name]; isFg { if !disableFormatting { p.writer.Write([]byte(p.state.setFg(resetFg))) } return true } else if _, isBg := bgTags[name]; isBg { if !disableFormatting { p.writer.Write([]byte(p.state.setBg(resetBg))) } return true } else if attr, isAttr := attrTags[name]; isAttr { if !disableFormatting { p.writer.Write([]byte(p.state.setAttr(-int8(attr)))) } return true } return false } if esc, ok := fgTags[name]; ok { if !disableFormatting { p.writer.Write([]byte(p.state.setFg(esc))) } return true } if esc, ok := bgTags[name]; ok { if !disableFormatting { p.writer.Write([]byte(p.state.setBg(esc))) } return true } if attr, ok := attrTags[name]; ok { if !disableFormatting { p.writer.Write([]byte(p.state.setAttr(int8(attr)))) } return true } return false } // Parse takes input from the reader and converts any provided tags to the relevant ANSI escape codes for output to parser's writer. func (p *Parser) Parse(reader io.Reader) error { formattingLock.RLock() defer formattingLock.RUnlock() buffer := make([]byte, 1024) if p.IncludeLeadingResets && !disableFormatting { if _, err := p.writer.Write([]byte(resetAll)); err != nil { return err } } var inTag bool var tagName string for { n, err := reader.Read(buffer) if err != nil { if err == io.EOF { break } return err } for _, r := range string(buffer[:n]) { if inTag { if r == '>' { if !p.handleTag(tagName) { p.writer.Write([]byte(fmt.Sprintf("<%s>", tagName))) } tagName = "" inTag = false continue } tagName = fmt.Sprintf("%s%c", tagName, r) continue } if r == '<' { inTag = true continue } p.writer.Write([]byte(string([]rune{r}))) } } if p.IncludeTrailingResets && !disableFormatting { p.writer.Write([]byte(resetAll)) } return nil }