| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508 |
- /*
- * 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
- }
- }
- }
- }
|