diff --git a/haxe/Template.hx b/haxe/Template.hx new file mode 100644 index 000000000..b1545732a --- /dev/null +++ b/haxe/Template.hx @@ -0,0 +1,417 @@ +/* + * Copyright (C)2005-2016 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; + +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 = {}; + + 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; + var value = Reflect.getProperty(context, v); + if( value != null || Reflect.hasField(context,v) ) + return value; + for( ctx in stack ) { + 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); + } + // 'end' , 'else', 'elseif' can't be found here + if( p.substr(0,3) == "if " ) { + p = p.substr(3,p.length - 3); + 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); + } + if( p.substr(0,8) == "foreach " ) { + p = p.substr(8,p.length - 8); + 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 ) + l.add({ p : data, s : true }); + 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 makeExpr2( l : List ) : Void -> Dynamic { + var p = l.pop(); + if( p == null ) + throw ""; + if( p.s ) + return makeConst(p.p); + switch( p.p ) { + case "(": + var e1:Dynamic = makeExpr(l); + var p = l.pop(); + if( p == null || p.s ) + throw p; + if( p.p == ")" ) + return e1; + var e2:Dynamic = makeExpr(l); + var p2 = l.pop(); + 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 + } + } + } + +}