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 }