1 module simpletpl; 2 3 import std.range : isOutputRange; 4 import core.stdc.string; 5 6 version (WASI) { 7 version (LDC) { 8 pragma(LDC_no_moduleinfo); 9 pragma(LDC_no_typeinfo); 10 pragma(LDC_alloca) 11 void* alloca(size_t size) pure; 12 } 13 } 14 15 private: 16 17 enum Error(string s) = s; 18 19 extern (C): 20 21 bool isAllowedChar(char c) => 22 ('0' <= c && c <= '9') || 23 ('a' <= c && c <= 'z') || 24 ('A' <= c && c <= 'Z') || 25 c == '_' || c == '.'; 26 // || c == '[' || c == ']'; 27 28 /++ 29 Params: c = The character to test. 30 Returns: Whether or not `c` is a whitespace character. That includes the 31 space, tab, vertical tab, form feed, carriage return, and linefeed 32 characters. 33 +/ 34 bool isWhite(dchar c) @safe pure nothrow @nogc 35 => c == ' ' || (c >= 0x09 && c <= 0x0D); 36 37 size_t search(string s, char c) { 38 size_t i; 39 for (; i < s.length; i++) 40 if (s[i] == c) 41 return i; 42 return i; 43 } 44 45 bool isTrue(string str) { 46 size_t i; 47 while (i < str.length && str[i].isWhite) 48 i++; 49 str = str[i .. $]; 50 for (i = str.length; i;) 51 if (!str[--i].isWhite) 52 break; 53 if (!str.length) 54 return false; 55 str = str[0 .. i + 1]; 56 if (str.length == 5 && str[4] == 'e') { 57 if (*cast(int*)str.ptr == 1936482662) 58 return false; 59 } 60 return str.length != 1 || str[0] != '0'; 61 } 62 63 unittest { 64 assert(!"".isTrue); 65 assert(!"0".isTrue); 66 assert(!"false".isTrue); 67 assert("2".isTrue); 68 assert("str".isTrue); 69 } 70 71 bool findSplit(ref string str, ref string slice, char c) { 72 size_t i; 73 for (; i < str.length; i++) { 74 if (str[i] == c) { 75 slice = str[0 .. i]; 76 str = str[i + 1 .. $]; 77 return true; 78 } 79 } 80 if (i == 0) 81 return false; 82 slice = str; 83 str = ""; 84 return true; 85 } 86 87 unittest { 88 auto str = "x\0yaa\0c\0d"; 89 string ele; 90 assert(str.findSplit(ele, '\0'), str); 91 assert(ele == "x", ele); 92 assert(str.findSplit(ele, '\0'), str); 93 assert(ele == "yaa", ele); 94 assert(str.findSplit(ele, '\0'), str); 95 assert(ele == "c", ele); 96 str = "\0"; 97 assert(str.findSplit(ele, '\0'), str); 98 } 99 100 size_t intToStr(char* buf, size_t value) { 101 char* p = buf; 102 for (;;) { 103 *p++ = (value % 10) ^ '0'; 104 if (value < 10) 105 break; 106 value /= 10; 107 } 108 for (char* i = buf, j = p - 1; i < j; i++, j--) { 109 char t = *i; 110 *i = *j; 111 *j = t; 112 } 113 return p - buf; 114 } 115 116 unittest { 117 char[10] buf = void; 118 auto str = buf[]; 119 assert(intToStr(buf.ptr, 0)); 120 assert(str[0 .. 1] == "0", str); 121 assert(intToStr(buf.ptr, 255)); 122 assert(str[0 .. 3] == "255", str); 123 assert(intToStr(buf.ptr, 20170), str); 124 assert(str[0 .. 5] == "20170", str); 125 assert(intToStr(buf.ptr, 4294967295), str); 126 assert(str == "4294967295", str); 127 } 128 129 // get start pos & end pos of contents inside [ … ] 130 // start == end means not found 131 void getBlock(char bs = '[', char be = ']', C)(const(C[]) str, ref size_t start, ref size_t end) { 132 end = start; 133 size_t j = start; 134 while (j < str.length) { 135 if (str[j].isWhite) 136 j++; 137 else { 138 if (str[j] != bs) 139 return; 140 break; 141 } 142 } 143 if (++j >= str.length) 144 return; 145 start = j; 146 for (uint level; j < str.length; j++) { 147 if (str[j] == bs) 148 level++; 149 else if (str[j] == be) { 150 if (level == 0) 151 break; 152 else 153 level--; 154 } 155 } 156 end = j < str.length ? j : start; 157 } 158 159 /+ 160 syntax: \$(var)?:(key )?(value)?\?\{\} 161 162 max variable name: 256 163 max inputlength: 24kb 164 max variable value length: 24kb 165 166 output 167 $var 168 169 raw output 170 $:var 171 172 condition 173 $cond?[ … ] 174 $cond?[ … ]:[ … ] 175 176 loop 177 $map:value[ … ] 178 $map:key value[ … ] 179 180 expression evaluation 181 ${expr} 182 183 returns: error message (empty if succeed) 184 +/ 185 186 string renderImpl(alias getContent, String, Sink)(ref Sink sink, String tpl, uint maxDepth = 5) 187 if (isOutputRange!(Sink, String)) 188 in (maxDepth < 24) { 189 do { 190 auto i = tpl.search('$'); 191 if (i + 1 >= tpl.length) { 192 sink.put(tpl); 193 break; 194 } 195 sink.put(tpl[0 .. i]); 196 i++; 197 size_t bs, be, j = void; 198 String var = void; 199 bool flag; 200 if (tpl[i] == '{') { // expr 201 bs = i; 202 getBlock!('{', '}')(tpl, bs, be); 203 if (bs == be) { 204 sink.put(tpl[i .. $]); 205 return null; 206 } 207 if (maxDepth == 0) 208 return Error!"MaxDepth exceeded"; 209 push(); 210 renderImpl!getContent(sink, getContent(tpl[bs .. be]), maxDepth - 1); 211 popFront(); 212 i = be + 1; 213 goto next; 214 } 215 j = i + (i < tpl.length && tpl[i] == ':'); 216 while (j < tpl.length && tpl[j].isAllowedChar) 217 j++; 218 var = tpl[i .. j]; 219 i = j; 220 while (j < tpl.length && tpl[j].isWhite) 221 j++; 222 if (j + 3 < tpl.length) { 223 if (tpl[j] == '?') { // cond 224 bs = ++j; 225 getBlock(tpl, bs, be); 226 if (bs == be) 227 goto output; 228 alias cond = flag; 229 cond = getVal(var).isTrue; 230 if (cond) { 231 renderImpl!getContent(sink, tpl[bs .. be], maxDepth); 232 } 233 j = be + 1; 234 while (j < tpl.length && tpl[j].isWhite) 235 j++; 236 if (j == tpl.length) 237 return null; 238 if (tpl[j] == ':') { 239 bs = j + 1; 240 getBlock(tpl, bs, be); 241 if (bs == be) { 242 i = j; 243 goto next; 244 } 245 if (!cond) { 246 renderImpl!getContent(sink, tpl[bs .. be], maxDepth); 247 } 248 } 249 i = be + 1; 250 goto next; 251 } 252 if (tpl[j] == ':') { // loop 253 ++j; 254 while (j < tpl.length && tpl[j].isWhite) 255 j++; 256 if (j == tpl.length) 257 goto output; 258 auto str = var; 259 alias mapname = str; 260 auto map = cast(String)getVal(mapname); 261 if (mapname.length > 254) 262 return Error!"Name too long"; 263 version (WASI) { 264 auto mbuf = cast(char*)alloca(map.length); 265 memcpy(mbuf, map.ptr, map.length); 266 map = cast(String)mbuf[0 .. map.length]; 267 } 268 auto k = j; 269 while (j < tpl.length && tpl[j].isAllowedChar) 270 j++; 271 auto keyname = tpl[k .. j]; 272 while (j < tpl.length && tpl[j].isWhite) 273 j++; 274 if (j == tpl.length) 275 goto output; 276 k = j; 277 while (j < tpl.length && tpl[j].isAllowedChar) 278 j++; 279 bs = j; 280 getBlock(tpl, bs, be); 281 if (bs == be) 282 goto output; 283 if (k < j) { 284 var = tpl[k .. j]; 285 } else { 286 var = keyname; 287 keyname = null; 288 } 289 if (map.length) { 290 auto block = tpl[bs .. be]; 291 char[256] buf = void; 292 auto p = buf.ptr; 293 alias isArray = flag; 294 isArray = map[0] == '\0'; 295 if (isArray) 296 map = map[1 .. $]; 297 else { 298 memcpy(p, mapname.ptr, mapname.length); 299 p += mapname.length; 300 *(p++) = '.'; 301 } 302 if (map.length) { 303 push(); 304 alias index = k, 305 keystr = str; 306 for (index = 0; map.findSplit(keystr, '\0'); index++) { 307 if (isArray) { 308 if (keyname.length) { 309 auto keylen = intToStr(p, index); 310 assert(keylen <= 10); 311 setValue(keyname, cast(string)buf[0 .. keylen]); 312 } 313 if (var.length) 314 setValue(var, keystr); 315 } else { 316 memcpy(p, keystr.ptr, keystr.length); 317 auto keylen = p - buf.ptr + keystr.length; 318 if (keylen > 256) 319 return Error!"Name too long"; 320 auto fullkey = cast(string)buf[0 .. keylen]; 321 if (keyname.length) 322 setValue(keyname, fullkey); 323 if (var.length) 324 setValue(var, getVal(fullkey)); 325 } 326 renderImpl!getContent(sink, block, maxDepth); 327 } 328 popFront(); 329 } 330 } 331 i = be + 1; 332 goto next; 333 } 334 } 335 output: 336 auto value = cast(String)getVal(var); 337 if (!value.length) { 338 sink.put("$"); 339 value = var; 340 } 341 sink.put(value); 342 next: 343 tpl = tpl[i .. $]; 344 } 345 while (tpl.length); 346 return null; 347 } 348 349 public: 350 351 version (WASI) { 352 export byte[24 << 10] buf, inbuf; 353 byte[8 << 10] outbuf; 354 size_t pos; 355 356 private void put(void[], string s) { 357 auto remain = pos + s.length; 358 for (;;) { 359 auto outlen = remain < outbuf.length ? remain : outbuf.length; 360 outlen -= pos; 361 memcpy(outbuf.ptr + pos, s.ptr, outlen); 362 s = s[outlen .. $]; 363 if (outlen + pos != outbuf.length) 364 break; 365 pos = 0; 366 remain -= outbuf.length; 367 flushBuffer(outbuf[]); 368 } 369 pos = remain; 370 } 371 // get variable value 372 size_t getValue(in string key); 373 string getVal(in string key) { 374 size_t len = getValue(key); 375 return cast(string)inbuf[0 .. len]; 376 } 377 378 void setValue(in string key, in string value); 379 size_t evalExpr(in string expr); 380 string getContent(in string expr) { 381 auto len = evalExpr(expr); 382 return cast(string)inbuf[0 .. len]; 383 } 384 //size_t inputBuffer(byte[] buf); 385 void flushBuffer(void[] buf); 386 void push(); 387 void popFront(); 388 export const(char*) render(string tpl, uint maxDepth) { 389 auto arr = cast(char[])outbuf[]; 390 auto errmsg = renderImpl!getContent(arr, tpl, maxDepth); 391 if (pos) { 392 flushBuffer(outbuf[0 .. pos]); 393 pos = 0; 394 } 395 return errmsg.ptr; 396 } 397 } else { 398 alias 399 getVal = getValue, 400 render = renderImpl; 401 402 struct Context { 403 const(char)[][string] data; 404 alias data this; 405 406 auto opIndex(const string key) { 407 if (auto p = key in data) 408 return *p; 409 return null; 410 } 411 412 void opIndexAssign(const char[] value, const string key) pure { 413 data[key] = value; 414 } 415 416 auto opDispatch(string key)() => this[key]; 417 418 auto opDispatch(string key)(const char[] value) => data[key] = value; 419 } 420 421 Context[] data; 422 423 // helper for HTML escape(original function from std.xml.encode) 424 void escape(String, Sink)(const String text, ref Sink sink) 425 if (isOutputRange!(Sink, String)) { 426 size_t index; 427 428 foreach (i, c; text) { 429 String temp = void; 430 // dfmt off 431 switch (c) { 432 case '&': temp = "&"; break; 433 case '"': temp = """; break; 434 case '<': temp = "<"; break; 435 case '>': temp = ">"; break; 436 default: continue; 437 } 438 // dfmt on 439 440 sink.put(text[index .. i]); 441 sink.put(temp); 442 index = i + 1; 443 } 444 445 sink.put(text[index .. $]); 446 } 447 448 string getValue(scope const(char)[] key) { 449 import std.array; 450 451 string value; 452 if (!key.length) 453 return value; 454 455 bool flag = key[0] == ':'; 456 if (flag) 457 key = key[1 .. $]; 458 459 foreach_reverse (x; data) 460 if (auto p = key in x) { 461 value = cast(string)*p; 462 break; 463 } 464 if (flag) 465 return value; 466 467 auto app = appender!string; 468 escape(value, app); 469 return app[]; 470 } 471 472 void setValue(scope const(char[]) key, scope const(char[]) value) 473 in (data.length) { 474 data[$ - 1][cast(string)key] = cast(string)value; 475 } 476 477 void push() { 478 data ~= Context(); 479 } 480 481 void popFront() { 482 data.length--; 483 } 484 485 T render(alias getContent, T)(T tpl, Context data, uint maxDepth = 5) { 486 import std.array; 487 488 auto app = appender!T; 489 .data = [data]; 490 renderImpl!getContent(app, tpl, maxDepth); 491 return app[]; 492 } 493 } 494 495 unittest { 496 void test(string tpl, string expected) { 497 auto result = tpl.render!getVal(data[0]); 498 assert(result == expected, tpl ~ ": " ~ result); 499 } 500 501 Context data; // @suppress(dscanner.suspicious.label_var_same_name) 502 data.a = "55"; 503 data.b = "<"; 504 data.m = "x\0y\0n"; 505 data.l = "\0b\0c\0d\0e"; 506 data["m.x"] = "a"; 507 data["m.y"] = "foo"; 508 data["m.n"] = "bar"; 509 510 .data = [data]; 511 test("a $a $n $b $:b", "a 55 $n < <"); 512 test("$a?bcd", "55?bcd"); 513 test("$a?[x]:bcd", "x:bcd"); 514 test("$a?[foo]:[bar]", "foo"); 515 test("$x?[foo]:[bar]", "bar"); 516 test("$l:k v[$k=$v ]", "0=b 1=c 2=d 3=e "); 517 test("$a:[bcd]", "bcd"); 518 test("$a:x foo", "55:x foo"); 519 test("u$m:k v[$k=$v ]123", "um.x=a m.y=foo m.n=bar 123"); 520 test("w$m:k v[$k=$v $l:k v[$k=$v ]]", 521 "wm.x=a 0=b 1=c 2=d 3=e m.y=foo 0=b 1=c 2=d 3=e m.n=bar 0=b 1=c 2=d 3=e "); 522 }