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 }