1 /++ 2 A module containing helper functions to ease the template usage 3 4 Copyright: © 2018 Szabo Bogdan 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Szabo Bogdan 7 +/ 8 module vibeauth.mvc.templatedata; 9 10 import std.string; 11 import std.algorithm; 12 import std.conv; 13 import std.stdio; 14 15 import vibe.data.json; 16 17 18 version(unittest) { 19 import fluent.asserts; 20 } 21 22 /// Structure used to store the data that will rendered inside a template 23 struct TemplateData { 24 private { 25 Json[] options; 26 string[string] variables; 27 string[] messages; 28 string[] errors; 29 } 30 31 /*************************************************************************************************** 32 33 Adds a set of options. The strings that will match these options will be replaced in the rendered 34 string. 35 36 eg. for this data set: 37 38 { 39 "key": "value" 40 "level1": { 41 "key": "value" 42 } 43 } 44 45 #{key} and #{level1.key} will be replaced 46 47 ***************************************************************************************************/ 48 void add(Json options) { 49 this.options ~= options; 50 } 51 52 /// ditto 53 void add(string key, Json options) { 54 Json obj = Json.emptyObject; 55 obj[key] = options; 56 57 this.options ~= obj; 58 } 59 60 /// ditto 61 void add(string key, string value) { 62 Json obj = Json.emptyObject; 63 obj[key] = value; 64 65 this.options ~= obj; 66 } 67 68 /// Add a notification to the user 69 void addMessage(string message) { 70 messages ~= message; 71 } 72 73 /// Add an error to the user 74 void addError(string error) { 75 errors ~= error; 76 } 77 78 79 /*************************************************************************************************** 80 81 Set a variable that will be replaced inside the options. 82 83 eg. for this data set: 84 85 { 86 "key": "value :id" 87 "level1": { 88 "key": "value :id" 89 } 90 } 91 92 after set(":id", "/user/:id", "/user/3") 93 94 the options will be: 95 { 96 "key": "value 3" 97 "level1": { 98 "key": "value 3" 99 } 100 } 101 102 ***************************************************************************************************/ 103 void set(string variable, string route, string path) { 104 auto pathValue = getValue(variable, route, path); 105 variables[variable] = pathValue; 106 } 107 108 /// 109 private void replaceJson(ref Json json, string variable, string value) { 110 if(json.type == Json.Type..string) { 111 json = json.to!string.replace(variable, value); 112 } 113 114 if(json.type == Json.Type.object) { 115 foreach(string key, ref jsonValue; json) { 116 replaceJson(jsonValue, variable, value); 117 } 118 } 119 } 120 121 /// Get a variable 122 string get(string variable) { 123 return variables[variable]; 124 } 125 126 /// Render a template 127 string render(string page) { 128 foreach(key, value; variables) { 129 foreach(ref item; options) { 130 replaceJson(item, key, value); 131 } 132 } 133 134 foreach(item; options) { 135 page = page.replaceVariables(item); 136 } 137 138 page = page.replace("#{messages}", renderErrors ~ renderMessages); 139 140 return page; 141 } 142 143 private string renderMessages() { 144 return messages.map!(a => `<div class="alert alert-info alert-dismissible fade show" role="alert"> 145 ` ~ a ~ ` 146 <button type="button" class="close" data-dismiss="alert" aria-label="Close"> 147 <span aria-hidden="true">×</span> 148 </button> 149 </div>`).join; 150 } 151 152 private string renderErrors() { 153 return errors.map!(a => `<div class="alert alert-danger alert-dismissible fade show" role="alert"> 154 ` ~ a ~ ` 155 <button type="button" class="close" data-dismiss="alert" aria-label="Close"> 156 <span aria-hidden="true">×</span> 157 </button> 158 </div>`).join; 159 } 160 } 161 162 /// Eschape html special chars: & " ' < > 163 string escapeHtmlString(string data) { 164 return data 165 .replace("&", "&") 166 .replace("\"", """) 167 .replace("'", "'") 168 .replace("<", "<") 169 .replace(">", ">"); 170 } 171 172 /// escape html strings 173 unittest { 174 "&\"'<>".escapeHtmlString.should.equal("&"'<>"); 175 } 176 177 /// Return true if the route path matches listpath/:id 178 string getValue(string variable, string routePath, string path) { 179 auto pieces = routePath.split(variable); 180 auto routePieces = path.split("/"); 181 182 if(!routePath.startsWith(pieces[0]) || !routePath.endsWith(pieces[1])) { 183 return ""; 184 } 185 186 auto prefixLen = pieces[0].length; 187 auto postfixLen = pieces[1].length; 188 189 if(path.length < prefixLen) { 190 return ""; 191 } 192 193 if(routePath[prefixLen .. $-postfixLen].canFind("/")) { 194 return ""; 195 } 196 197 return path[prefixLen .. $-postfixLen]; 198 } 199 200 /// getValue usage 201 unittest { 202 getValue(":id", "/users/:id", "/users/some page").should.equal("some page"); 203 getValue(":id", "/users/:id", "/users").should.equal(""); 204 } 205 206 /// Search variables `#{variable_name}` and replace them with the values from json 207 string replaceVariables(const string data, const Json variables, const string prefix = "") { 208 string result = data.dup; 209 210 if(variables.type == Json.Type.object) { 211 foreach(string key, value; variables) { 212 213 if(value.type == Json.Type.object) { 214 result = result.replaceVariables(value, prefix ~ key ~ "."); 215 } else { 216 result = result.replace("#{" ~ prefix ~ key ~ "}", value.to!string); 217 } 218 } 219 } 220 221 return result; 222 } 223 224 /// replace variables 225 unittest { 226 Json data = Json.emptyObject; 227 data["one"] = "1"; 228 data["second"] = Json.emptyObject; 229 data["second"]["value"] = "2"; 230 231 "#{one}-#{second.value}".replaceVariables(data).should.startWith("1-"); 232 "#{one}-#{second.value}".replaceVariables(data).should.endWith("-2"); 233 } 234 235 236 /// should not replace variables on undefined data 237 unittest { 238 Json data; 239 240 "#{one}-#{second.value}".replaceVariables(data).should.startWith("#{one}-"); 241 "#{one}-#{second.value}".replaceVariables(data).should.endWith("-#{second.value}"); 242 } 243 244 245 /// Return true if the route path matches listpath/:id 246 bool isUserPage(string routePath, string path) { 247 auto pieces = routePath.split(":id"); 248 249 if(!path.startsWith(pieces[0]) || !path.endsWith(pieces[1])) { 250 return false; 251 } 252 253 auto prefixLen = pieces[0].length; 254 auto postfixLen = pieces[1].length; 255 256 if(path[prefixLen .. $-postfixLen].canFind("/")) { 257 return false; 258 } 259 260 return true; 261 } 262 263 /// isUserPage tests 264 unittest { 265 isUserPage("/users/:id", "/users/some page").should.equal(true); 266 isUserPage("/users/:id", "/users").should.equal(false); 267 isUserPage("/users/:id", "/other/some").should.equal(false); 268 isUserPage("/users/:id", "/other").should.equal(false); 269 }