/* * 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); OpForeach(expr:Void->Dynamic, loop:TemplateExpr); OpMacro(name:String, params:List); } private typedef Token = { var s:Bool; var p:String; var l:Array; } 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: **/ 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; 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(); 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) { 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) { 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(); 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) { 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) { 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):Void->Dynamic { skipSpaces(l); var p = l.pop(); skipSpaces(l); if (p == null) throw ""; 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 = 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(); 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 } } } }