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">&times;</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">&times;</span>
157       </button>
158     </div>`).join;
159   }
160 }
161 
162 /// Eschape html special chars: & " ' < >
163 string escapeHtmlString(string data) {
164   return data
165     .replace("&", "&amp;")
166     .replace("\"", "&quot;")
167     .replace("'", "&#039;")
168     .replace("<", "&lt;")
169     .replace(">", "&gt;");
170 }
171 
172 /// escape html strings
173 unittest {
174   "&\"'<>".escapeHtmlString.should.equal("&amp;&quot;&#039;&lt;&gt;");
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 }