1 /*
2  * hunt-console eases the creation of beautiful and testable command line interfaces.
3  *
4  * Copyright (C) 2018-2019, HuntLabs
5  *
6  * Website: https://www.huntlabs.net
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11  
12 module hunt.console.formatter.DefaultOutputFormatter;
13 
14 import hunt.console.error.InvalidArgumentException;
15 
16 import hunt.collection.HashMap;
17 import hunt.collection.Map;
18 import hunt.console.formatter.OutputFormatter;
19 import hunt.console.formatter.OutputFormatterStyle;
20 import hunt.console.formatter.OutputFormatterStyleStack;
21 import hunt.console.formatter.DefaultOutputFormatterStyle;
22 import hunt.text.Common;
23 import hunt.logging;
24 import std.regex;
25 import std.string;
26 import hunt.util.StringBuilder;
27 
28 class DefaultOutputFormatter : OutputFormatter
29 {
30     private bool decorated;
31 
32     private Map!(string, OutputFormatterStyle) styles ;
33 
34     private OutputFormatterStyleStack styleStack;
35 
36     public const string TAG_REGEX = "[a-z][a-z0-9_=;-]*";
37 
38     public const string TAG_PATTERN = "<((" ~ TAG_REGEX ~ ")|/(" ~ TAG_REGEX ~ ")?)>";
39 
40     public const string STYLE_PATTERN = "([^=]+)=([^;]+)(;|$)";
41 
42     public static string escape(string text)
43     {
44         string pattern = "([^\\\\\\\\]?)<";
45 
46         // return pattern.matcher(text).replaceAll("$1\\\\<");
47         auto re = regex(pattern);
48         return text.replaceAll(re,"$1\\\\<");
49     }
50 
51     public this()
52     {
53         this(false);
54     }
55 
56     public this(bool decorated)
57     {
58         this(decorated, new HashMap!(string, OutputFormatterStyle)());
59     }
60 
61     public this(bool decorated, Map!(string, OutputFormatterStyle) styles)
62     {
63         this.styles = new HashMap!(string, OutputFormatterStyle)();
64         this.decorated = decorated;
65 
66         setStyle("error", new DefaultOutputFormatterStyle("white", "red"));
67         setStyle("info", new DefaultOutputFormatterStyle("green"));
68         setStyle("comment", new DefaultOutputFormatterStyle("yellow"));
69         setStyle("question", new DefaultOutputFormatterStyle("black", "cyan"));
70 
71         // this.styles.putAll(styles);
72         foreach(string k ,OutputFormatterStyle v ; styles) {
73             this.styles.put(k,v);
74         }
75 
76         styleStack = new OutputFormatterStyleStack();
77     }
78 
79     override public void setDecorated(bool decorated)
80     {
81         this.decorated = decorated;
82     }
83 
84     override public bool isDecorated()
85     {
86         return decorated;
87     }
88 
89     override public void setStyle(string name, OutputFormatterStyle style)
90     {
91         styles.put(name.toLower(), style);
92     }
93 
94     override public bool hasStyle(string name)
95     {
96         return styles.containsKey(name.toLower());
97     }
98 
99     override public OutputFormatterStyle getStyle(string name)
100     {
101         if (!hasStyle(name)) {
102             throw new InvalidArgumentException(std..string.format("Undefined style: %s", name));
103         }
104 
105         return styles.get(name);
106     }
107 
108     override public string format(string message)
109     {
110         if (message is null) {
111             return "";
112         }
113 
114         int offset = 0;
115         StringBuilder output = new StringBuilder();
116 
117         auto  matchers = matchAll(message,regex(TAG_PATTERN,"im"));
118 
119         bool open;
120         string tag;
121         OutputFormatterStyle style;
122         // logInfo("mesg : ",message);
123         foreach(matcher; matchers) {
124             string text = matcher.captures[0];
125             int pos = cast(int)(message[offset..$].indexOf(text))+offset;
126 
127             // add the text up to the next tag
128             // logInfo("pos : ",pos, " offset: ",offset," text :",text);
129             output.append(applyCurrentStyle(message.substring(offset, pos)));
130             offset = pos + cast(int)(text.length);
131 
132             // opening tag?
133             open = (text[1] != '/');
134             if (open) {
135                 tag = matcher.captures[2];
136             } else {
137                 tag = matcher.captures[3];
138             }
139             // logInfo("tag --- > : ",tag, " len:",tag.length);
140             if (!open && (tag is null || tag.length == 0)) {
141                 // </>
142                 styleStack.pop();
143             } else if (pos > 0 && message.charAt(pos - 1) == '\\') {
144                 // escaped tag
145                 output.append(applyCurrentStyle(text));
146             } else {
147                 style = createStyleFromString(tag.toLower());
148                 if (style is null) {
149                     output.append(applyCurrentStyle(text));
150                 } else {
151                     if (open) {
152                         styleStack.push(style);
153                     } else {
154                         styleStack.pop(style);
155                     }
156                 }
157             }
158         }
159 
160         output.append(applyCurrentStyle(message.substring(offset)));
161 
162         return output.toString().replace("\\\\<", "<");
163     }
164 
165     private OutputFormatterStyle createStyleFromString(string str)
166     {
167         if (styles.containsKey(str)) {
168             return styles.get(str);
169         }
170 
171         auto matchers = matchAll(str.toLower(),STYLE_PATTERN);
172 
173         OutputFormatterStyle style = new DefaultOutputFormatterStyle();
174 
175         string type;
176         foreach(matcher; matchers){
177             type = matcher.captures[1];
178             switch (type) {
179                 case "fg":
180                     style.setForeground(matcher.captures[2]);
181                     break;
182                 case "bg":
183                     style.setBackground(matcher.captures[2]);
184                     break;
185                 default:
186                     try {
187                         style.setOption(matcher.captures[2]);
188                     } catch (InvalidArgumentException e) {
189                         return null;
190                     }
191                     break;
192             }
193         }
194 
195         return style;
196     }
197 
198     private string applyCurrentStyle(string text)
199     {
200         if (isDecorated() && text.length > 0) {
201             return styleStack.getCurrent().apply(text);
202         }
203 
204         return text;
205     }
206 }