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 = "&amp;";  break;
433 			case '"': temp = "&quot;"; break;
434 			case '<': temp = "&lt;";   break;
435 			case '>': temp = "&gt;";   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 &lt; <");
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 }