509 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Haxe
		
	
	
	
	
	
			
		
		
	
	
			509 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Haxe
		
	
	
	
	
	
/*
 | 
						|
 * Copyright (C)2005-2019 Haxe Foundation
 | 
						|
 *
 | 
						|
 * Permission is hereby granted, free of charge, to any person obtaining a
 | 
						|
 * copy of this software and associated documentation files (the "Software"),
 | 
						|
 * to deal in the Software without restriction, including without limitation
 | 
						|
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 | 
						|
 * and/or sell copies of the Software, and to permit persons to whom the
 | 
						|
 * Software is furnished to do so, subject to the following conditions:
 | 
						|
 *
 | 
						|
 * The above copyright notice and this permission notice shall be included in
 | 
						|
 * all copies or substantial portions of the Software.
 | 
						|
 *
 | 
						|
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
						|
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
						|
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
						|
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
						|
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 | 
						|
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 | 
						|
 * DEALINGS IN THE SOFTWARE.
 | 
						|
 */
 | 
						|
 | 
						|
package haxe;
 | 
						|
 | 
						|
import haxe.ds.List;
 | 
						|
 | 
						|
using StringTools;
 | 
						|
 | 
						|
private enum TemplateExpr {
 | 
						|
	OpVar(v:String);
 | 
						|
	OpExpr(expr:Void->Dynamic);
 | 
						|
	OpIf(expr:Void->Dynamic, eif:TemplateExpr, eelse:TemplateExpr);
 | 
						|
	OpStr(str:String);
 | 
						|
	OpBlock(l:List<TemplateExpr>);
 | 
						|
	OpForeach(expr:Void->Dynamic, loop:TemplateExpr);
 | 
						|
	OpMacro(name:String, params:List<TemplateExpr>);
 | 
						|
}
 | 
						|
 | 
						|
private typedef Token = {
 | 
						|
	var s:Bool;
 | 
						|
	var p:String;
 | 
						|
	var l:Array<String>;
 | 
						|
}
 | 
						|
 | 
						|
private typedef ExprToken = {
 | 
						|
	var s:Bool;
 | 
						|
	var p:String;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
	`Template` provides a basic templating mechanism to replace values in a source
 | 
						|
	String, and to have some basic logic.
 | 
						|
 | 
						|
	A complete documentation of the supported syntax is available at:
 | 
						|
	<https://haxe.org/manual/std-template.html>
 | 
						|
**/
 | 
						|
class Template {
 | 
						|
	static var splitter = ~/(::[A-Za-z0-9_ ()&|!+=\/><*."-]+::|\$\$([A-Za-z0-9_-]+)\()/;
 | 
						|
	static var expr_splitter = ~/(\(|\)|[ \r\n\t]*"[^"]*"[ \r\n\t]*|[!+=\/><*.&|-]+)/;
 | 
						|
	static var expr_trim = ~/^[ ]*([^ ]+)[ ]*$/;
 | 
						|
	static var expr_int = ~/^[0-9]+$/;
 | 
						|
	static var expr_float = ~/^([+-]?)(?=\d|,\d)\d*(,\d*)?([Ee]([+-]?\d+))?$/;
 | 
						|
 | 
						|
	/**
 | 
						|
		Global replacements which are used across all `Template` instances. This
 | 
						|
		has lower priority than the context argument of `execute()`.
 | 
						|
	**/
 | 
						|
	public static var globals:Dynamic = {};
 | 
						|
 | 
						|
	// To avoid issues with DCE, keep the array iterator.
 | 
						|
	@:ifFeature("haxe.Template.run") static var hxKeepArrayIterator = [].iterator();
 | 
						|
 | 
						|
	var expr:TemplateExpr;
 | 
						|
	var context:Dynamic;
 | 
						|
	var macros:Dynamic;
 | 
						|
	var stack:List<Dynamic>;
 | 
						|
	var buf:StringBuf;
 | 
						|
 | 
						|
	/**
 | 
						|
		Creates a new `Template` instance from `str`.
 | 
						|
 | 
						|
		`str` is parsed into tokens, which are stored for internal use. This
 | 
						|
		means that multiple `execute()` operations on a single `Template` instance
 | 
						|
		are more efficient than one `execute()` operations on multiple `Template`
 | 
						|
		instances.
 | 
						|
 | 
						|
		If `str` is `null`, the result is unspecified.
 | 
						|
	**/
 | 
						|
	public function new(str:String) {
 | 
						|
		var tokens = parseTokens(str);
 | 
						|
		expr = parseBlock(tokens);
 | 
						|
		if (!tokens.isEmpty())
 | 
						|
			throw "Unexpected '" + tokens.first().s + "'";
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
		Executes `this` `Template`, taking into account `context` for
 | 
						|
		replacements and `macros` for callback functions.
 | 
						|
 | 
						|
		If `context` has a field `name`, its value replaces all occurrences of
 | 
						|
		`::name::` in the `Template`. Otherwise `Template.globals` is checked instead,
 | 
						|
		If `name` is not a field of that either, `::name::` is replaced with `null`.
 | 
						|
 | 
						|
		If `macros` has a field `name`, all occurrences of `$$name(args)` are
 | 
						|
		replaced with the result of calling that field. The first argument is
 | 
						|
		always the `resolve()` method, followed by the given arguments.
 | 
						|
		If `macros` has no such field, the result is unspecified.
 | 
						|
 | 
						|
		If `context` is `null`, the result is unspecified. If `macros` is `null`,
 | 
						|
		no macros are used.
 | 
						|
	**/
 | 
						|
	public function execute(context:Dynamic, ?macros:Dynamic):String {
 | 
						|
		this.macros = if (macros == null) {} else macros;
 | 
						|
		this.context = context;
 | 
						|
		stack = new List();
 | 
						|
		buf = new StringBuf();
 | 
						|
		run(expr);
 | 
						|
		return buf.toString();
 | 
						|
	}
 | 
						|
 | 
						|
	function resolve(v:String):Dynamic {
 | 
						|
		if (v == "__current__")
 | 
						|
			return context;
 | 
						|
		if (Reflect.isObject(context)) {
 | 
						|
			var value = Reflect.getProperty(context, v);
 | 
						|
			if (value != null || Reflect.hasField(context, v))
 | 
						|
				return value;
 | 
						|
		}
 | 
						|
		for (ctx in stack) {
 | 
						|
			var value = Reflect.getProperty(ctx, v);
 | 
						|
			if (value != null || Reflect.hasField(ctx, v))
 | 
						|
				return value;
 | 
						|
		}
 | 
						|
		return Reflect.field(globals, v);
 | 
						|
	}
 | 
						|
 | 
						|
	function parseTokens(data:String) {
 | 
						|
		var tokens = new List<Token>();
 | 
						|
		while (splitter.match(data)) {
 | 
						|
			var p = splitter.matchedPos();
 | 
						|
			if (p.pos > 0)
 | 
						|
				tokens.add({p: data.substr(0, p.pos), s: true, l: null});
 | 
						|
 | 
						|
			// : ?
 | 
						|
			if (data.charCodeAt(p.pos) == 58) {
 | 
						|
				tokens.add({p: data.substr(p.pos + 2, p.len - 4), s: false, l: null});
 | 
						|
				data = splitter.matchedRight();
 | 
						|
				continue;
 | 
						|
			}
 | 
						|
 | 
						|
			// macro parse
 | 
						|
			var parp = p.pos + p.len;
 | 
						|
			var npar = 1;
 | 
						|
			var params = [];
 | 
						|
			var part = "";
 | 
						|
			while (true) {
 | 
						|
				var c = data.charCodeAt(parp);
 | 
						|
				parp++;
 | 
						|
				if (c == 40) {
 | 
						|
					npar++;
 | 
						|
				} else if (c == 41) {
 | 
						|
					npar--;
 | 
						|
					if (npar <= 0)
 | 
						|
						break;
 | 
						|
				} else if (c == null) {
 | 
						|
					throw "Unclosed macro parenthesis";
 | 
						|
				}
 | 
						|
				if (c == 44 && npar == 1) {
 | 
						|
					params.push(part);
 | 
						|
					part = "";
 | 
						|
				} else {
 | 
						|
					part += String.fromCharCode(c);
 | 
						|
				}
 | 
						|
			}
 | 
						|
			params.push(part);
 | 
						|
			tokens.add({p: splitter.matched(2), s: false, l: params});
 | 
						|
			data = data.substr(parp, data.length - parp);
 | 
						|
		}
 | 
						|
		if (data.length > 0)
 | 
						|
			tokens.add({p: data, s: true, l: null});
 | 
						|
		return tokens;
 | 
						|
	}
 | 
						|
 | 
						|
	function parseBlock(tokens:List<Token>) {
 | 
						|
		var l = new List();
 | 
						|
		while (true) {
 | 
						|
			var t = tokens.first();
 | 
						|
			if (t == null)
 | 
						|
				break;
 | 
						|
			if (!t.s && (t.p == "end" || t.p == "else" || t.p.substr(0, 7) == "elseif "))
 | 
						|
				break;
 | 
						|
			l.add(parse(tokens));
 | 
						|
		}
 | 
						|
		if (l.length == 1)
 | 
						|
			return l.first();
 | 
						|
		return OpBlock(l);
 | 
						|
	}
 | 
						|
 | 
						|
	function parse(tokens:List<Token>) {
 | 
						|
		var t = tokens.pop();
 | 
						|
		var p = t.p;
 | 
						|
		if (t.s)
 | 
						|
			return OpStr(p);
 | 
						|
		// macro
 | 
						|
		if (t.l != null) {
 | 
						|
			var pe = new List();
 | 
						|
			for (p in t.l)
 | 
						|
				pe.add(parseBlock(parseTokens(p)));
 | 
						|
			return OpMacro(p, pe);
 | 
						|
		}
 | 
						|
		function kwdEnd(kwd:String):Int {
 | 
						|
			var pos = -1;
 | 
						|
			var length = kwd.length;
 | 
						|
			if (p.substr(0, length) == kwd) {
 | 
						|
				pos = length;
 | 
						|
				for (c in p.substr(length)) {
 | 
						|
					switch c {
 | 
						|
						case ' '.code: pos++;
 | 
						|
						case _: break;
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
			return pos;
 | 
						|
		}
 | 
						|
		// 'end' , 'else', 'elseif' can't be found here
 | 
						|
		var pos = kwdEnd("if");
 | 
						|
		if (pos > 0) {
 | 
						|
			p = p.substr(pos, p.length - pos);
 | 
						|
			var e = parseExpr(p);
 | 
						|
			var eif = parseBlock(tokens);
 | 
						|
			var t = tokens.first();
 | 
						|
			var eelse;
 | 
						|
			if (t == null)
 | 
						|
				throw "Unclosed 'if'";
 | 
						|
			if (t.p == "end") {
 | 
						|
				tokens.pop();
 | 
						|
				eelse = null;
 | 
						|
			} else if (t.p == "else") {
 | 
						|
				tokens.pop();
 | 
						|
				eelse = parseBlock(tokens);
 | 
						|
				t = tokens.pop();
 | 
						|
				if (t == null || t.p != "end")
 | 
						|
					throw "Unclosed 'else'";
 | 
						|
			} else { // elseif
 | 
						|
				t.p = t.p.substr(4, t.p.length - 4);
 | 
						|
				eelse = parse(tokens);
 | 
						|
			}
 | 
						|
			return OpIf(e, eif, eelse);
 | 
						|
		}
 | 
						|
		var pos = kwdEnd("foreach");
 | 
						|
		if (pos >= 0) {
 | 
						|
			p = p.substr(pos, p.length - pos);
 | 
						|
			var e = parseExpr(p);
 | 
						|
			var efor = parseBlock(tokens);
 | 
						|
			var t = tokens.pop();
 | 
						|
			if (t == null || t.p != "end")
 | 
						|
				throw "Unclosed 'foreach'";
 | 
						|
			return OpForeach(e, efor);
 | 
						|
		}
 | 
						|
		if (expr_splitter.match(p))
 | 
						|
			return OpExpr(parseExpr(p));
 | 
						|
		return OpVar(p);
 | 
						|
	}
 | 
						|
 | 
						|
	function parseExpr(data:String) {
 | 
						|
		var l = new List<ExprToken>();
 | 
						|
		var expr = data;
 | 
						|
		while (expr_splitter.match(data)) {
 | 
						|
			var p = expr_splitter.matchedPos();
 | 
						|
			var k = p.pos + p.len;
 | 
						|
			if (p.pos != 0)
 | 
						|
				l.add({p: data.substr(0, p.pos), s: true});
 | 
						|
			var p = expr_splitter.matched(0);
 | 
						|
			l.add({p: p, s: p.indexOf('"') >= 0});
 | 
						|
			data = expr_splitter.matchedRight();
 | 
						|
		}
 | 
						|
		if (data.length != 0) {
 | 
						|
			for (i => c in data) {
 | 
						|
				switch c {
 | 
						|
					case ' '.code:
 | 
						|
					case _:
 | 
						|
						l.add({p: data.substr(i), s: true});
 | 
						|
						break;
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		var e:Void->Dynamic;
 | 
						|
		try {
 | 
						|
			e = makeExpr(l);
 | 
						|
			if (!l.isEmpty())
 | 
						|
				throw l.first().p;
 | 
						|
		} catch (s:String) {
 | 
						|
			throw "Unexpected '" + s + "' in " + expr;
 | 
						|
		}
 | 
						|
		return function() {
 | 
						|
			try {
 | 
						|
				return e();
 | 
						|
			} catch (exc:Dynamic) {
 | 
						|
				throw "Error : " + Std.string(exc) + " in " + expr;
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	function makeConst(v:String):Void->Dynamic {
 | 
						|
		expr_trim.match(v);
 | 
						|
		v = expr_trim.matched(1);
 | 
						|
		if (v.charCodeAt(0) == 34) {
 | 
						|
			var str = v.substr(1, v.length - 2);
 | 
						|
			return function() return str;
 | 
						|
		}
 | 
						|
		if (expr_int.match(v)) {
 | 
						|
			var i = Std.parseInt(v);
 | 
						|
			return function() {
 | 
						|
				return i;
 | 
						|
			};
 | 
						|
		}
 | 
						|
		if (expr_float.match(v)) {
 | 
						|
			var f = Std.parseFloat(v);
 | 
						|
			return function() {
 | 
						|
				return f;
 | 
						|
			};
 | 
						|
		}
 | 
						|
		var me = this;
 | 
						|
		return function() {
 | 
						|
			return me.resolve(v);
 | 
						|
		};
 | 
						|
	}
 | 
						|
 | 
						|
	function makePath(e:Void->Dynamic, l:List<ExprToken>) {
 | 
						|
		var p = l.first();
 | 
						|
		if (p == null || p.p != ".")
 | 
						|
			return e;
 | 
						|
		l.pop();
 | 
						|
		var field = l.pop();
 | 
						|
		if (field == null || !field.s)
 | 
						|
			throw field.p;
 | 
						|
		var f = field.p;
 | 
						|
		expr_trim.match(f);
 | 
						|
		f = expr_trim.matched(1);
 | 
						|
		return makePath(function() {
 | 
						|
			return Reflect.field(e(), f);
 | 
						|
		}, l);
 | 
						|
	}
 | 
						|
 | 
						|
	function makeExpr(l) {
 | 
						|
		return makePath(makeExpr2(l), l);
 | 
						|
	}
 | 
						|
 | 
						|
	function skipSpaces(l:List<ExprToken>) {
 | 
						|
		var p = l.first();
 | 
						|
		while (p != null) {
 | 
						|
			for (c in p.p) {
 | 
						|
				if (c != " ".code) {
 | 
						|
					return;
 | 
						|
				}
 | 
						|
			}
 | 
						|
			l.pop();
 | 
						|
			p = l.first();
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	function makeExpr2(l:List<ExprToken>):Void->Dynamic {
 | 
						|
		skipSpaces(l);
 | 
						|
		var p = l.pop();
 | 
						|
		skipSpaces(l);
 | 
						|
		if (p == null)
 | 
						|
			throw "<eof>";
 | 
						|
		if (p.s)
 | 
						|
			return makeConst(p.p);
 | 
						|
		switch (p.p) {
 | 
						|
			case "(":
 | 
						|
				skipSpaces(l);
 | 
						|
				var e1:Dynamic = makeExpr(l);
 | 
						|
				skipSpaces(l);
 | 
						|
				var p = l.pop();
 | 
						|
				if (p == null || p.s)
 | 
						|
					throw p;
 | 
						|
				if (p.p == ")")
 | 
						|
					return e1;
 | 
						|
				skipSpaces(l);
 | 
						|
				var e2:Dynamic = makeExpr(l);
 | 
						|
				skipSpaces(l);
 | 
						|
				var p2 = l.pop();
 | 
						|
				skipSpaces(l);
 | 
						|
				if (p2 == null || p2.p != ")")
 | 
						|
					throw p2;
 | 
						|
				return switch (p.p) {
 | 
						|
					case "+": function() {
 | 
						|
							return cast e1() + e2();
 | 
						|
						};
 | 
						|
					case "-": function() {
 | 
						|
							return cast e1() - e2();
 | 
						|
						};
 | 
						|
					case "*": function() {
 | 
						|
							return cast e1() * e2();
 | 
						|
						};
 | 
						|
					case "/": function() {
 | 
						|
							return cast e1() / e2();
 | 
						|
						};
 | 
						|
					case ">": function() {
 | 
						|
							return cast e1() > e2();
 | 
						|
						};
 | 
						|
					case "<": function() {
 | 
						|
							return cast e1() < e2();
 | 
						|
						};
 | 
						|
					case ">=": function() {
 | 
						|
							return cast e1() >= e2();
 | 
						|
						};
 | 
						|
					case "<=": function() {
 | 
						|
							return cast e1() <= e2();
 | 
						|
						};
 | 
						|
					case "==": function() {
 | 
						|
							return cast e1() == e2();
 | 
						|
						};
 | 
						|
					case "!=": function() {
 | 
						|
							return cast e1() != e2();
 | 
						|
						};
 | 
						|
					case "&&": function() {
 | 
						|
							return cast e1() && e2();
 | 
						|
						};
 | 
						|
					case "||": function() {
 | 
						|
							return cast e1() || e2();
 | 
						|
						};
 | 
						|
					default: throw "Unknown operation " + p.p;
 | 
						|
				}
 | 
						|
			case "!":
 | 
						|
				var e:Void->Dynamic = makeExpr(l);
 | 
						|
				return function() {
 | 
						|
					var v:Dynamic = e();
 | 
						|
					return (v == null || v == false);
 | 
						|
				};
 | 
						|
			case "-":
 | 
						|
				var e = makeExpr(l);
 | 
						|
				return function() {
 | 
						|
					return -e();
 | 
						|
				};
 | 
						|
		}
 | 
						|
		throw p.p;
 | 
						|
	}
 | 
						|
 | 
						|
	function run(e:TemplateExpr) {
 | 
						|
		switch (e) {
 | 
						|
			case OpVar(v):
 | 
						|
				buf.add(Std.string(resolve(v)));
 | 
						|
			case OpExpr(e):
 | 
						|
				buf.add(Std.string(e()));
 | 
						|
			case OpIf(e, eif, eelse):
 | 
						|
				var v:Dynamic = e();
 | 
						|
				if (v == null || v == false) {
 | 
						|
					if (eelse != null)
 | 
						|
						run(eelse);
 | 
						|
				} else
 | 
						|
					run(eif);
 | 
						|
			case OpStr(str):
 | 
						|
				buf.add(str);
 | 
						|
			case OpBlock(l):
 | 
						|
				for (e in l)
 | 
						|
					run(e);
 | 
						|
			case OpForeach(e, loop):
 | 
						|
				var v:Dynamic = e();
 | 
						|
				try {
 | 
						|
					var x:Dynamic = v.iterator();
 | 
						|
					if (x.hasNext == null)
 | 
						|
						throw null;
 | 
						|
					v = x;
 | 
						|
				} catch (e:Dynamic)
 | 
						|
					try {
 | 
						|
						if (v.hasNext == null)
 | 
						|
							throw null;
 | 
						|
					} catch (e:Dynamic) {
 | 
						|
						throw "Cannot iter on " + v;
 | 
						|
					}
 | 
						|
				stack.push(context);
 | 
						|
				var v:Iterator<Dynamic> = v;
 | 
						|
				for (ctx in v) {
 | 
						|
					context = ctx;
 | 
						|
					run(loop);
 | 
						|
				}
 | 
						|
				context = stack.pop();
 | 
						|
			case OpMacro(m, params):
 | 
						|
				var v:Dynamic = Reflect.field(macros, m);
 | 
						|
				var pl = new Array<Dynamic>();
 | 
						|
				var old = buf;
 | 
						|
				pl.push(resolve);
 | 
						|
				for (p in params) {
 | 
						|
					switch (p) {
 | 
						|
						case OpVar(v): pl.push(resolve(v));
 | 
						|
						default:
 | 
						|
							buf = new StringBuf();
 | 
						|
							run(p);
 | 
						|
							pl.push(buf.toString());
 | 
						|
					}
 | 
						|
				}
 | 
						|
				buf = old;
 | 
						|
				try {
 | 
						|
					buf.add(Std.string(Reflect.callMethod(macros, v, pl)));
 | 
						|
				} catch (e:Dynamic) {
 | 
						|
					var plstr = try pl.join(",") catch (e:Dynamic) "???";
 | 
						|
					var msg = "Macro call " + m + "(" + plstr + ") failed (" + Std.string(e) + ")";
 | 
						|
					#if neko
 | 
						|
					neko.Lib.rethrow(msg);
 | 
						|
					#else
 | 
						|
					throw msg;
 | 
						|
					#end
 | 
						|
				}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |