1 package formatter
2
3 import (
4 "fmt"
5 "os"
6 "regexp"
7 "strconv"
8 "strings"
9 )
10
11
12 var ColorableStdOut = newColorable(os.Stdout)
13 var ColorableStdErr = newColorable(os.Stderr)
14
15 const COLS = 80
16
17 type ColorMode uint8
18
19 const (
20 ColorModeNone ColorMode = iota
21 ColorModeTerminal
22 ColorModePassthrough
23 )
24
25 var SingletonFormatter = New(ColorModeTerminal)
26
27 func F(format string, args ...interface{}) string {
28 return SingletonFormatter.F(format, args...)
29 }
30
31 func Fi(indentation uint, format string, args ...interface{}) string {
32 return SingletonFormatter.Fi(indentation, format, args...)
33 }
34
35 func Fiw(indentation uint, maxWidth uint, format string, args ...interface{}) string {
36 return SingletonFormatter.Fiw(indentation, maxWidth, format, args...)
37 }
38
39 type Formatter struct {
40 ColorMode ColorMode
41 colors map[string]string
42 styleRe *regexp.Regexp
43 preserveColorStylingTags bool
44 }
45
46 func NewWithNoColorBool(noColor bool) Formatter {
47 if noColor {
48 return New(ColorModeNone)
49 }
50 return New(ColorModeTerminal)
51 }
52
53 func New(colorMode ColorMode) Formatter {
54 colorAliases := map[string]int{
55 "black": 0,
56 "red": 1,
57 "green": 2,
58 "yellow": 3,
59 "blue": 4,
60 "magenta": 5,
61 "cyan": 6,
62 "white": 7,
63 }
64 for colorAlias, n := range colorAliases {
65 colorAliases[fmt.Sprintf("bright-%s", colorAlias)] = n + 8
66 }
67
68 getColor := func(color, defaultEscapeCode string) string {
69 color = strings.ToUpper(strings.ReplaceAll(color, "-", "_"))
70 envVar := fmt.Sprintf("GINKGO_CLI_COLOR_%s", color)
71 envVarColor := os.Getenv(envVar)
72 if envVarColor == "" {
73 return defaultEscapeCode
74 }
75 if colorCode, ok := colorAliases[envVarColor]; ok {
76 return fmt.Sprintf("\x1b[38;5;%dm", colorCode)
77 }
78 colorCode, err := strconv.Atoi(envVarColor)
79 if err != nil || colorCode < 0 || colorCode > 255 {
80 return defaultEscapeCode
81 }
82 return fmt.Sprintf("\x1b[38;5;%dm", colorCode)
83 }
84
85 f := Formatter{
86 ColorMode: colorMode,
87 colors: map[string]string{
88 "/": "\x1b[0m",
89 "bold": "\x1b[1m",
90 "underline": "\x1b[4m",
91
92 "red": getColor("red", "\x1b[38;5;9m"),
93 "orange": getColor("orange", "\x1b[38;5;214m"),
94 "coral": getColor("coral", "\x1b[38;5;204m"),
95 "magenta": getColor("magenta", "\x1b[38;5;13m"),
96 "green": getColor("green", "\x1b[38;5;10m"),
97 "dark-green": getColor("dark-green", "\x1b[38;5;28m"),
98 "yellow": getColor("yellow", "\x1b[38;5;11m"),
99 "light-yellow": getColor("light-yellow", "\x1b[38;5;228m"),
100 "cyan": getColor("cyan", "\x1b[38;5;14m"),
101 "gray": getColor("gray", "\x1b[38;5;243m"),
102 "light-gray": getColor("light-gray", "\x1b[38;5;246m"),
103 "blue": getColor("blue", "\x1b[38;5;12m"),
104 },
105 }
106 colors := []string{}
107 for color := range f.colors {
108 colors = append(colors, color)
109 }
110 f.styleRe = regexp.MustCompile("{{(" + strings.Join(colors, "|") + ")}}")
111 return f
112 }
113
114 func (f Formatter) F(format string, args ...interface{}) string {
115 return f.Fi(0, format, args...)
116 }
117
118 func (f Formatter) Fi(indentation uint, format string, args ...interface{}) string {
119 return f.Fiw(indentation, 0, format, args...)
120 }
121
122 func (f Formatter) Fiw(indentation uint, maxWidth uint, format string, args ...interface{}) string {
123 out := f.style(format)
124 if len(args) > 0 {
125 out = fmt.Sprintf(out, args...)
126 }
127
128 if indentation == 0 && maxWidth == 0 {
129 return out
130 }
131
132 lines := strings.Split(out, "\n")
133
134 if maxWidth != 0 {
135 outLines := []string{}
136
137 maxWidth = maxWidth - indentation*2
138 for _, line := range lines {
139 if f.length(line) <= maxWidth {
140 outLines = append(outLines, line)
141 continue
142 }
143 words := strings.Split(line, " ")
144 outWords := []string{words[0]}
145 length := uint(f.length(words[0]))
146 for _, word := range words[1:] {
147 wordLength := f.length(word)
148 if length+wordLength+1 <= maxWidth {
149 length += wordLength + 1
150 outWords = append(outWords, word)
151 continue
152 }
153 outLines = append(outLines, strings.Join(outWords, " "))
154 outWords = []string{word}
155 length = wordLength
156 }
157 if len(outWords) > 0 {
158 outLines = append(outLines, strings.Join(outWords, " "))
159 }
160 }
161
162 lines = outLines
163 }
164
165 if indentation == 0 {
166 return strings.Join(lines, "\n")
167 }
168
169 padding := strings.Repeat(" ", int(indentation))
170 for i := range lines {
171 if lines[i] != "" {
172 lines[i] = padding + lines[i]
173 }
174 }
175
176 return strings.Join(lines, "\n")
177 }
178
179 func (f Formatter) length(styled string) uint {
180 n := uint(0)
181 inStyle := false
182 for _, b := range styled {
183 if inStyle {
184 if b == 'm' {
185 inStyle = false
186 }
187 continue
188 }
189 if b == '\x1b' {
190 inStyle = true
191 continue
192 }
193 n += 1
194 }
195 return n
196 }
197
198 func (f Formatter) CycleJoin(elements []string, joiner string, cycle []string) string {
199 if len(elements) == 0 {
200 return ""
201 }
202 n := len(cycle)
203 out := ""
204 for i, text := range elements {
205 out += cycle[i%n] + text
206 if i < len(elements)-1 {
207 out += joiner
208 }
209 }
210 out += "{{/}}"
211 return f.style(out)
212 }
213
214 func (f Formatter) style(s string) string {
215 switch f.ColorMode {
216 case ColorModeNone:
217 return f.styleRe.ReplaceAllString(s, "")
218 case ColorModePassthrough:
219 return s
220 case ColorModeTerminal:
221 return f.styleRe.ReplaceAllStringFunc(s, func(match string) string {
222 if out, ok := f.colors[strings.Trim(match, "{}")]; ok {
223 return out
224 }
225 return match
226 })
227 }
228
229 return ""
230 }
231
View as plain text