package formatter import ( "fmt" "regexp" "strings" ) const COLS = 80 type ColorMode uint8 const ( ColorModeNone ColorMode = iota ColorModeTerminal ColorModePassthrough ) var SingletonFormatter = New(ColorModeTerminal) func F(format string, args ...interface{}) string { return SingletonFormatter.F(format, args...) } func Fi(indentation uint, format string, args ...interface{}) string { return SingletonFormatter.Fi(indentation, format, args...) } func Fiw(indentation uint, maxWidth uint, format string, args ...interface{}) string { return SingletonFormatter.Fiw(indentation, maxWidth, format, args...) } type Formatter struct { ColorMode ColorMode colors map[string]string styleRe *regexp.Regexp preserveColorStylingTags bool } func NewWithNoColorBool(noColor bool) Formatter { if noColor { return New(ColorModeNone) } return New(ColorModeTerminal) } func New(colorMode ColorMode) Formatter { f := Formatter{ ColorMode: colorMode, colors: map[string]string{ "/": "\x1b[0m", "bold": "\x1b[1m", "underline": "\x1b[4m", "red": "\x1b[38;5;9m", "orange": "\x1b[38;5;214m", "coral": "\x1b[38;5;204m", "magenta": "\x1b[38;5;13m", "green": "\x1b[38;5;10m", "dark-green": "\x1b[38;5;28m", "yellow": "\x1b[38;5;11m", "light-yellow": "\x1b[38;5;228m", "cyan": "\x1b[38;5;14m", "gray": "\x1b[38;5;243m", "light-gray": "\x1b[38;5;246m", "blue": "\x1b[38;5;12m", }, } colors := []string{} for color := range f.colors { colors = append(colors, color) } f.styleRe = regexp.MustCompile("{{(" + strings.Join(colors, "|") + ")}}") return f } func (f Formatter) F(format string, args ...interface{}) string { return f.Fi(0, format, args...) } func (f Formatter) Fi(indentation uint, format string, args ...interface{}) string { return f.Fiw(indentation, 0, format, args...) } func (f Formatter) Fiw(indentation uint, maxWidth uint, format string, args ...interface{}) string { out := fmt.Sprintf(f.style(format), args...) if indentation == 0 && maxWidth == 0 { return out } lines := strings.Split(out, "\n") if maxWidth != 0 { outLines := []string{} maxWidth = maxWidth - indentation*2 for _, line := range lines { if f.length(line) <= maxWidth { outLines = append(outLines, line) continue } outWords := []string{} length := uint(0) words := strings.Split(line, " ") for _, word := range words { wordLength := f.length(word) if length+wordLength <= maxWidth { length += wordLength outWords = append(outWords, word) continue } outLines = append(outLines, strings.Join(outWords, " ")) outWords = []string{word} length = wordLength } if len(outWords) > 0 { outLines = append(outLines, strings.Join(outWords, " ")) } } lines = outLines } if indentation == 0 { return strings.Join(lines, "\n") } padding := strings.Repeat(" ", int(indentation)) for i := range lines { if lines[i] != "" { lines[i] = padding + lines[i] } } return strings.Join(lines, "\n") } func (f Formatter) length(styled string) uint { n := uint(0) inStyle := false for _, b := range styled { if inStyle { if b == 'm' { inStyle = false } continue } if b == '\x1b' { inStyle = true continue } n += 1 } return n } func (f Formatter) CycleJoin(elements []string, joiner string, cycle []string) string { if len(elements) == 0 { return "" } n := len(cycle) out := "" for i, text := range elements { out += cycle[i%n] + text if i < len(elements)-1 { out += joiner } } out += "{{/}}" return f.style(out) } func (f Formatter) style(s string) string { switch f.ColorMode { case ColorModeNone: return f.styleRe.ReplaceAllString(s, "") case ColorModePassthrough: return s case ColorModeTerminal: return f.styleRe.ReplaceAllStringFunc(s, func(match string) string { if out, ok := f.colors[strings.Trim(match, "{}")]; ok { return out } return match }) } return "" }