1 module handlebars.tokens;
2 
3 import std.array;
4 import std.string;
5 import std.traits;
6 import std.conv;
7 import std.algorithm;
8 
9 import handlebars.properties;
10 
11 version(unittest) {
12   import fluent.asserts;
13 }
14 
15 ///
16 struct Token {
17   static Token[] empty;
18   ///
19   enum Type {
20     /// tokens that should not be processed
21     plain,
22     value,
23     helper,
24     openBlock,
25     closeBlock,
26   }
27 
28   ///
29   Type type;
30 
31   ///
32   string value;
33 
34   ///
35   Properties properties;
36 }
37 
38 /// split the input into tokens
39 class TokenRange {
40   private {
41     string tpl;
42     size_t index;
43     size_t nextIndex;
44 
45     Token token;
46   }
47 
48   static opCall(string tpl) {
49     return new TokenRange(tpl);
50   }
51 
52   ///
53   this(string tpl) {
54     this.tpl = tpl;
55     next;
56   }
57 
58   ///
59   private void next() {
60     long pos;
61     token.properties = Properties("");
62 
63     if(index+2 < tpl.length && tpl[index..index+2] == "{{") {
64       token.type = Token.Type.value;
65       pos = tpl[index..$].indexOf("}}");
66 
67       token.value = tpl[index+2..index+pos];
68       nextIndex = index+pos+2;
69 
70       auto paramPos = token.value.indexOf(' ');
71 
72       if(paramPos >= 0) {
73         token.type = Token.Type.helper;
74         token.properties = Properties(token.value[paramPos+1..$]);
75         token.value = token.value[0..paramPos];
76       }
77 
78       if(token.value[0] == '#') {
79         token.type = Token.Type.openBlock;
80         token.value = token.value[1..$];
81       }
82 
83       if(token.value[0] == '/') {
84         token.type = Token.Type.closeBlock;
85         token.value = token.value[1..$];
86       }
87 
88       return;
89     }
90 
91     if(index < tpl.length) {
92       token.type = Token.Type.plain;
93       pos = tpl[index..$].indexOf("{{");
94 
95       if(pos == -1) {
96         token.value = tpl[index..$];
97       } else {
98         token.value = tpl[index..index+pos];
99       }
100 
101       nextIndex = index + token.value.length;
102 
103       if( token.type == Token.Type.plain ) {
104         token.value = token.value.stripBeginToken.stripEndToken;
105       }
106     }
107   }
108 
109   ///
110   Token front() {
111     return this.token;
112   }
113 
114   ///
115   bool empty() {
116     return index >= tpl.length || index == nextIndex;
117   }
118 
119   ///
120   void popFront() {
121     index = nextIndex;
122     next();
123   }
124 }
125 
126 /// It should parse a token value
127 unittest {
128   enum tpl = "{{value}}";
129 
130   auto range = TokenRange(tpl);
131 
132   range.front.should.equal(Token(Token.Type.value, "value", Properties("")));
133 }
134 
135 /// It should parse an helper token
136 unittest {
137   enum tpl = "{{helper value}}";
138 
139   auto range = TokenRange(tpl);
140 
141   range.front.should.equal(Token(Token.Type.helper, "helper", Properties("value")));
142 }
143 
144 /// It should parse block tokens
145 unittest {
146   enum tpl = "{{#if condition}}{{else}}{{/if}}";
147 
148   auto range = TokenRange(tpl);
149 
150   range.array.should.equal([
151     Token(Token.Type.openBlock, "if", Properties("condition")),
152     Token(Token.Type.value, "else", Properties("")),
153     Token(Token.Type.closeBlock, "if", Properties(""))]);
154 }
155 
156 /// It should parse two value tokens
157 unittest {
158   enum tpl = "{{value1}}{{value2}}";
159 
160   auto range = TokenRange(tpl);
161 
162   range.array.should.equal([
163     Token(Token.Type.value, "value1", Properties("")),
164     Token(Token.Type.value, "value2", Properties(""))]);
165 }
166 
167 /// It should parse value and text tokens
168 unittest {
169   enum tpl = "1{{value1}}2{{value2}}3";
170 
171   auto range = TokenRange(tpl);
172 
173   range.array.should.equal([
174     Token(Token.Type.plain, "1", Properties("")),
175     Token(Token.Type.value, "value1", Properties("")),
176     Token(Token.Type.plain, "2", Properties("")),
177     Token(Token.Type.value, "value2", Properties("")),
178     Token(Token.Type.plain, "3", Properties(""))]);
179 }
180 
181 ///
182 class TokenLevelRange(T) {
183   private {
184     T range;
185     Token[] items;
186   }
187 
188   ///
189   this(T range) {
190     this.range = range;
191     next();
192   }
193 
194   ///
195   private void next() {
196     if(range.empty) {
197       this.items = [ ];
198       return;
199     }
200 
201     auto token = range.front;
202     range.popFront;
203 
204     this.items = [ token ];
205 
206     if(token.type != Token.Type.openBlock) {
207       return;
208     }
209 
210     size_t level = 1;
211     while(!range.empty) {
212       this.items ~= range.front;
213 
214       if(range.front.type == Token.Type.closeBlock && range.front.value == token.value) {
215         level--;
216       }
217 
218       if(range.front.type == Token.Type.openBlock && range.front.value == token.value) {
219         level++;
220       }
221 
222       range.popFront;
223       if(level == 0) {
224         break;
225       }
226     }
227   }
228 
229   ///
230   Token[] front() {
231     return items;
232   }
233 
234   ///
235   bool empty() {
236     return range.empty && items.length == 0;
237   }
238 
239   ///
240   void popFront() {
241     next();
242   }
243 }
244 
245 ///
246 auto tokenLevelRange(T)(T range) {
247   return new TokenLevelRange!(T)(range);
248 }
249 
250 /// It should return all tokens if there is one level
251 unittest {
252   enum tpl = "1{{a}}2{{b}}3";
253 
254   auto range = TokenRange(tpl);
255   auto levelRange = tokenLevelRange(range);
256 
257   levelRange.array.should.equal([
258     [ Token(Token.Type.plain, "1", Properties("")) ],
259     [ Token(Token.Type.value, "a", Properties("")) ],
260     [ Token(Token.Type.plain, "2", Properties("")) ],
261     [ Token(Token.Type.value, "b", Properties("")) ],
262     [ Token(Token.Type.plain, "3", Properties("")) ]
263   ]);
264 }
265 
266 /// It should group the tokens by levels
267 unittest {
268   enum tpl = "1{{value1}}{{#value2}}3{{/value2}}";
269 
270   auto range = TokenRange(tpl);
271   auto levelRange = tokenLevelRange(range);
272 
273   levelRange.array.should.equal([
274     [ Token(Token.Type.plain, "1", Properties("")) ],
275     [ Token(Token.Type.value, "value1", Properties("")) ],
276     [ Token(Token.Type.openBlock, "value2", Properties("")),
277         Token(Token.Type.plain, "3", Properties("")),
278         Token(Token.Type.closeBlock, "value2", Properties("")) ]
279   ]);
280 }
281 
282 /// It should group the tokens by levels when the same component is used in the block
283 unittest {
284   enum tpl = "{{#a}}{{#a}}{{#a}}3{{/a}}{{/a}}{{/a}}";
285 
286   auto range = TokenRange(tpl);
287   auto levelRange = tokenLevelRange(range);
288 
289   levelRange.array.should.equal([
290     [ Token(Token.Type.openBlock, "a", Properties("")),
291         Token(Token.Type.openBlock, "a", Properties("")),
292           Token(Token.Type.openBlock, "a", Properties("")),
293             Token(Token.Type.plain, "3", Properties("")),
294           Token(Token.Type.closeBlock, "a", Properties("")),
295         Token(Token.Type.closeBlock, "a", Properties("")),
296       Token(Token.Type.closeBlock, "a", Properties("")) ]
297   ]);
298 }
299 
300 /// It should strip ws from plain tokens
301 unittest {
302   enum tpl = "{{a}}\ntest\n{{a}}";
303   auto range = TokenRange(tpl);
304   auto levelRange = tokenLevelRange(range);
305 
306   levelRange.array.should.equal([
307     [ Token(Token.Type.value, "a", Properties("")) ],
308     [ Token(Token.Type.plain, "test", Properties("")) ],
309     [ Token(Token.Type.value, "a", Properties("")) ]]);
310 }
311 
312 ///
313 T evaluate(T,U)(U value, string fieldName) {
314   static immutable ignoredMembers = [ __traits(allMembers, Object), "render", "this" ];
315   auto pieces = fieldName.splitMemberAccess;
316 
317   static foreach (memberName; __traits(allMembers, U)) {{
318     static if(__traits(hasMember, U, memberName)) {
319       enum protection = __traits(getProtection, __traits(getMember, U, memberName));
320     } else {
321       enum protection = "";
322     }
323 
324     static if(protection == "public" && !ignoredMembers.canFind(memberName)) {
325       mixin(`alias field = U.` ~ memberName ~ `;`);
326 
327       static if (isCallable!(field)) {
328         alias FieldType = ReturnType!field;
329       } else {
330         alias FieldType = typeof(field);
331       }
332 
333       static if(isArray!FieldType && !isSomeString!FieldType) {
334         static if(isBasicType!T || isSomeString!T) {
335           if(pieces.length == 2 && pieces[0] == memberName && pieces[1] == "length") {
336             mixin(`return value.` ~ memberName ~ `.length.to!(T);`);
337           }
338         }
339 
340         if(pieces.length == 2 && pieces[0] == memberName && pieces[1][0] == '[') {
341           auto k = pieces[1][1..$-1].to!size_t;
342 
343           static if(__traits(compiles, FieldType.init[0].to!(T))) {
344             mixin(`return value.` ~ memberName ~ `[k].to!(T);`);
345           }
346         }
347 
348         if(pieces.length == 1 && pieces[0] == memberName) {
349           static if(is(T == FieldType)) {
350             mixin(`return value.` ~ memberName ~ `;`);
351           } else static if(__traits(compiles, FieldType.init.to!(T))) {
352             mixin(`return value.` ~ memberName ~ `.to!(T);`);
353           } else {
354             throw new Exception("Can't assign `"~fieldName~"` because can't transform " ~ T.stringof ~ " from " ~ FieldType.stringof);
355           }
356         }
357       } else static if((isCallable!(field) && arity!field == 0) || !isCallable!(field)) {
358         if(pieces.length == 1 && pieces[0] == memberName) {
359           mixin(`auto tmp = value.` ~ memberName ~ `;`);
360           static if(is(T == bool)) {
361             static if(is(typeof(tmp) == bool)) {
362               return tmp;
363             } else static if(is(typeof(tmp) == class)) {
364               return tmp !is null;
365             } else static if(std.traits.isNumeric!(typeof(tmp))) {
366               return tmp != 0;
367             } else static if(is(typeof(tmp) == string)) {
368               return tmp != "";
369             } else {
370               return true;
371             }
372           } else static if(__traits(compiles, tmp.to!string.to!T)) {
373             return tmp.to!string.to!T;
374           } else {
375             throw new Exception("Can't evaluate `"~fieldName~"` as `"~T.stringof~"`");
376           }
377         }
378       }
379     }
380   }}
381 
382   return T.init;
383 }
384 
385 string[] splitMemberAccess(string memberName) {
386   string[] result;
387 
388   foreach(item; memberName.split(".")) {
389     auto pos = item.indexOf("[");
390 
391     if(pos != -1) {
392       result ~= item[0..pos];
393       result ~= item[pos..$];
394     } else {
395       result ~= item;
396     }
397   }
398 
399   return result;
400 }
401 
402 ///
403 string stripBeginToken(string value) {
404   long begin;
405   auto pos = value.indexOf("\n");
406   auto charPos = getFirstCharPos(value);
407 
408   if(charPos != -1 && pos > charPos) {
409     return value;
410   }
411 
412   if(pos >= 0) {
413     begin = pos + 1;
414   }
415 
416   return value[begin..$];
417 }
418 
419 /// Remove the white spaces until the first new line
420 unittest {
421   "  \n\n\n".stripBeginToken.should.equal("\n\n");
422   "  \r\n\r\n\r\n".stripBeginToken.should.equal("\r\n\r\n");
423   " a \n\n\n".stripBeginToken.should.equal(" a \n\n\n");
424   " b \r\n\r\n\r\n".stripBeginToken.should.equal(" b \r\n\r\n\r\n");
425   "\ntest\n".stripBeginToken.should.equal("test\n");
426   "  ".stripBeginToken.should.equal("  ");
427 }
428 
429 ///
430 long getFirstCharPos(const string value) {
431   foreach(long index, char ch; value) {
432     if(ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r') {
433       return index;
434     }
435   }
436 
437   return -1;
438 }
439 
440 /// Get the position of the first non ws char
441 unittest {
442   "  \r\n\r\n\r\n".getFirstCharPos.should.equal(-1);
443   "  \r\n\t".getFirstCharPos.should.equal(-1);
444   " b \r\n\r\n\r\n".getFirstCharPos.should.equal(1);
445 }
446 
447 
448 ///
449 string stripEndToken(string value) {
450   long end = value.length;
451   auto pos = value.lastIndexOf("\n");
452   if(value.lastIndexOf("\r") == pos - 1) {
453     pos--;
454   }
455 
456   auto charPos = getLastCharPos(value);
457 
458   if(charPos != -1 && pos < charPos) {
459     return value;
460   }
461 
462   if(pos >= 0) {
463     end = pos;
464   }
465 
466   return value[0..end];
467 }
468 
469 /// Remove the white spaces after the last new line
470 unittest {
471   "  \n\n\n  ".stripEndToken.should.equal("  \n\n");
472   "  \r\n\r\n\r\n  ".stripEndToken.should.equal("  \r\n\r\n");
473   " a \n\n\n a ".stripEndToken.should.equal(" a \n\n\n a ");
474   " b \r\n\r\n\r\n a ".stripEndToken.should.equal(" b \r\n\r\n\r\n a ");
475   "\ntest\n".stripEndToken.should.equal("\ntest");
476   "  ".stripEndToken.should.equal("  ");
477 }
478 
479 
480 ///
481 long getLastCharPos(const string value) {
482   foreach_reverse(long index, char ch; value) {
483     if(ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r') {
484       return index;
485     }
486   }
487 
488   return -1;
489 }
490 
491 /// Get the position of the last non ws char
492 unittest {
493   "  \r\n\r\n\r\n".getLastCharPos.should.equal(-1);
494   "  \r\n\t".getLastCharPos.should.equal(-1);
495   " a \r\n\r\n\r\n b ".getLastCharPos.should.equal(10);
496 }