1 /++
2  + Simple coloring module for strings
3  +
4  + Copyright: Copyright © 2017, Christian Köstlin
5  + Authors: Christian Koestlin
6  + License: MIT
7  +/
8 module colored;
9 
10 @safe:
11 
12 import std..string;
13 
14 public import colored.packageversion;
15 
16 /// Available Colors
17 enum AnsiColor
18 {
19     black = 30,
20     red = 31,
21     green = 32,
22     yellow = 33,
23     blue = 34,
24     magenta = 35,
25     cyan = 36,
26     lightGray = 37,
27     defaultColor = 39,
28     darkGray = 90,
29     lightRed = 91,
30     lightGreen = 92,
31     lightYellow = 93,
32     lightBlue = 94,
33     lightMagenta = 95,
34     lightCyan = 96,
35     white = 97
36 }
37 
38 /// Available Styles
39 enum Style
40 {
41     bold = 1,
42     dim = 2,
43     underlined = 4,
44     blink = 5,
45     reverse = 7,
46     hidden = 8
47 }
48 
49 /// Internal structure to style a string
50 struct StyledString
51 {
52     public string unformatted;
53     private int[] befores;
54     private int[] afters;
55     public this(string unformatted)
56     {
57         this.unformatted = unformatted;
58     }
59 
60     private StyledString addPair(int before, int after)
61     {
62         befores ~= before;
63         afters ~= after;
64         return this;
65     }
66 
67     StyledString setForeground(int color)
68     {
69         return addPair(color, 0);
70     }
71 
72     StyledString setBackground(int color)
73     {
74         return addPair(color + 10, 0);
75     }
76 
77     StyledString addStyle(int style)
78     {
79         return addPair(style, 0);
80     }
81 
82     string toString() @safe
83     {
84         import std.algorithm;
85 
86         auto prefix = befores.map!(a => "\033[%dm".format(a)).join("");
87         auto suffix = afters.map!(a => "\033[%dm".format(a)).join("");
88         return "%s%s%s".format(prefix, unformatted, suffix);
89     }
90 }
91 
92 struct RGBString
93 {
94     string unformatted;
95     struct RGB
96     {
97         ubyte r;
98         ubyte g;
99         ubyte b;
100     }
101 
102     RGB* foreground;
103     RGB* background;
104     this(string unformatted)
105     {
106         this.unformatted = unformatted;
107     }
108 
109     auto rgb(ubyte r, ubyte g, ubyte b)
110     {
111         this.foreground = new RGB(r, g, b);
112         return this;
113     }
114 
115     auto onRgb(ubyte r, ubyte g, ubyte b)
116     {
117         this.background = new RGB(r, g, b);
118         return this;
119     }
120 
121     string toString() @safe
122     {
123         auto res = "";
124         if (foreground != null)
125         {
126             res = "\033[38;2;%s;%s;%sm".format(foreground.r, foreground.g, foreground.b) ~ res;
127         }
128         if (background != null)
129         {
130             res = "\033[48;2;%s;%s;%sm".format(background.r, background.g, background.b) ~ res;
131         }
132         res ~= unformatted;
133         if (foreground != null || background != null)
134         {
135             res ~= "\033[0m";
136         }
137         return res;
138     }
139 }
140 
141 string rgb(string s, ubyte r, ubyte g, ubyte b)
142 {
143     return RGBString(s).rgb(r, g, b).toString;
144 }
145 
146 string onRgb(string s, ubyte r, ubyte g, ubyte b)
147 {
148     return RGBString(s).onRgb(r, g, b).toString;
149 }
150 
151 @("rgb") unittest
152 {
153     import std.stdio;
154 
155     writeln("red: ", "r".rgb(255, 0, 0).onRgb(0, 255, 0));
156     writeln("green: ", "g".rgb(0, 255, 0).onRgb(0, 0, 255));
157     writeln("blue: ", "b".rgb(0, 0, 255).onRgb(255, 0, 0));
158 }
159 
160 @("styledstring") unittest
161 {
162     import unit_threaded;
163     import std.stdio;
164     import std.traits;
165 
166     foreach (immutable color; [EnumMembers!AnsiColor])
167     {
168         auto colorName = "%s".format(color);
169         writeln(StyledString(colorName).setForeground(color));
170     }
171     foreach (immutable color; [EnumMembers!AnsiColor])
172     {
173         auto colorName = "bg%s".format(color);
174         writeln(StyledString(colorName).setBackground(color));
175     }
176     foreach (immutable style; [EnumMembers!Style])
177     {
178         auto styleName = "%s".format(style);
179         writeln(StyledString(styleName).addStyle(style));
180     }
181 }
182 
183 auto colorMixin(T)()
184 {
185     import std.traits;
186 
187     string res = "";
188     foreach (immutable color; [EnumMembers!T])
189     {
190         auto t = typeof(T.init).stringof;
191         auto c = "%s".format(color);
192         res ~= "auto %1$s(string s) { return StyledString(s).setForeground(%2$s.%1$s); }\n".format(c,
193                 t);
194         res ~= "auto %1$s(StyledString s) { return s.setForeground(%2$s.%1$s); }\n".format(c, t);
195         string name = c[0 .. 1].toUpper ~ c[1 .. $];
196         res ~= "auto on%3$s(string s) { return StyledString(s).setBackground(%2$s.%1$s); }\n".format(c,
197                 t, name);
198         res ~= "auto on%3$s(StyledString s) { return s.setBackground(%2$s.%1$s); }\n".format(c,
199                 t, name);
200     }
201     return res;
202 }
203 
204 auto styleMixin(T)()
205 {
206     import std.traits;
207 
208     string res = "";
209     foreach (immutable style; [EnumMembers!T])
210     {
211         auto t = typeof(T.init).stringof;
212         auto s = "%s".format(style);
213         res ~= "auto %1$s(string s) { return StyledString(s).addStyle(%2$s.%1$s); }\n".format(s, t);
214         res ~= "auto %1$s(StyledString s) { return s.addStyle(%2$s.%1$s); }\n".format(s, t);
215     }
216     return res;
217 }
218 
219 mixin(colorMixin!AnsiColor);
220 mixin(styleMixin!Style);
221 
222 @("api") unittest
223 {
224     import std.stdio;
225 
226     "redOnGreen".red.onGreen.writeln;
227     "redOnYellowBoldUnderlined".red.onYellow.bold.underlined.writeln;
228     "bold".bold.writeln;
229     "test".writeln;
230 }
231 
232 /// Calculate length of string excluding all formatting escapes
233 ulong unformattedLength(string s)
234 {
235     enum State
236     {
237         NORMAL,
238         ESCAPED,
239     }
240 
241     auto state = State.NORMAL;
242     ulong count = 0;
243     foreach (c; s)
244     {
245         switch (state)
246         {
247         case State.NORMAL:
248             if (c == 0x1b)
249             {
250                 state = State.ESCAPED;
251             }
252             else
253             {
254                 count++;
255             }
256             break;
257         case State.ESCAPED:
258             if (c == 'm')
259             {
260                 state = State.NORMAL;
261             }
262             break;
263         default:
264             throw new Exception("Illegal state");
265         }
266     }
267     return count;
268 }
269 
270 // https://en.wikipedia.org/wiki/ANSI_escape_code
271 auto tokenize(Range)(Range parts)
272 {
273     import std.range;
274 
275     struct TokenizeResult(Range)
276     {
277         Range parts;
278         ElementType!(Range)[] next;
279         this(Range parts)
280         {
281             this.parts = parts;
282             tokenizeNext();
283         }
284 
285         private void tokenizeNext()
286         {
287             next = [];
288             if (parts.empty)
289             {
290                 return;
291             }
292             switch (parts.front)
293             {
294             case 38:
295             case 48:
296                 next ~= 38;
297                 parts.popFront;
298                 switch (parts.front)
299                 {
300                 case 2:
301                     next ~= 2;
302                     parts.popFront;
303                     next ~= parts.front;
304                     parts.popFront;
305                     next ~= parts.front;
306                     parts.popFront;
307                     next ~= parts.front;
308                     parts.popFront;
309                     break;
310                 case 5:
311                     next ~= 5;
312                     parts.popFront;
313                     next ~= parts.front;
314                     parts.popFront;
315                     break;
316                 default:
317                     throw new Exception("Only [38,48];[2,5] are supported but got %s;%s".format(next[0],
318                             parts.front));
319                 }
320                 break;
321             case 0: .. case 37:
322             case 39: .. case 47:
323             case 49:
324             case 51: .. case 55:
325             case 60: .. case 65:
326             case 90: .. case 97:
327             case 100: .. case 107:
328                 next ~= parts.front;
329                 parts.popFront;
330                 break;
331             default:
332                 throw new Exception("Only colors are supported");
333             }
334         }
335 
336         auto front()
337         {
338             return next;
339         }
340 
341         bool empty()
342         {
343             return next == null;
344         }
345 
346         void popFront()
347         {
348             tokenizeNext();
349         }
350     }
351 
352     return TokenizeResult!(Range)(parts);
353 }
354 
355 @("ansi tokenizer") unittest
356 {
357     import unit_threaded;
358 
359     [38, 5, 2, 38, 2, 1, 2, 3, 36, 1, 2, 3, 4].tokenize.shouldEqual([[38, 5,
360             2], [38, 2, 1, 2, 3], [36], [1], [2], [3], [4]]);
361 }
362 
363 string filterAnsiEscapes(alias predicate)(string s)
364 {
365     import std.regex;
366 
367     string withFilters(Captures!string c)
368     {
369         import std..string;
370         import std.algorithm;
371         import std.conv;
372         import std.array;
373 
374         auto parts = c[1].split(";").map!(a => a.to!uint).tokenize.filter!(p => predicate(p));
375         if (parts.empty)
376         {
377             return "";
378         }
379         else
380         {
381             return "\033[" ~ parts.joiner.map!(a => "%d".format(a)).join(";") ~ "m";
382         }
383     }
384 
385     alias r = ctRegex!"\033\\[(.*?)m";
386     return s.replaceAll!(withFilters)(r);
387 }
388 
389 bool foregroundColor(uint[] token)
390 {
391     return token[0] >= 30 && token[0] <= 38;
392 }
393 
394 bool backgroundColor(uint[] token)
395 {
396     return token[0] >= 40 && token[0] <= 48;
397 }
398 
399 bool style(uint[] token)
400 {
401     return token[0] >= 1 && token[0] <= 29;
402 }
403 
404 bool none(uint[] token)
405 {
406     return false;
407 }
408 
409 bool all(uint[] token)
410 {
411     return true;
412 }
413 
414 @("configurable strip") unittest
415 {
416     import unit_threaded;
417     import std.functional;
418 
419     "\033[1;31mtest".filterAnsiEscapes!(foregroundColor).shouldEqual("\033[31mtest");
420     "\033[1;31mtest".filterAnsiEscapes!(not!foregroundColor).shouldEqual("\033[1mtest");
421     "\033[1;31mtest".filterAnsiEscapes!(style).shouldEqual("\033[1mtest");
422     "\033[1;31mtest".filterAnsiEscapes!(none).shouldEqual("test");
423 }
424 
425 auto leftJustifyFormattedString(string s, ulong width, dchar fillChar = ' ')
426 {
427     auto res = s;
428     auto currentWidth = s.unformattedLength;
429     for (auto i = currentWidth; i < width; ++i)
430     {
431         res ~= fillChar;
432     }
433     return res;
434 }