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