Skip to content

Instantly share code, notes, and snippets.

@hotpotato
Created December 13, 2010 14:31
Show Gist options
  • Save hotpotato/739036 to your computer and use it in GitHub Desktop.
Save hotpotato/739036 to your computer and use it in GitHub Desktop.
/*
* Copyright (c) 2005, The haXe Project Contributors
* All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
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( name:String, expr : Void -> Dynamic, loop : TemplateExpr );
OpMacro( name : String, params : List<TemplateExpr> );
OpFill(name:String, efill : TemplateExpr);
OpSet(name:String, eset : Void -> Dynamic);
OpUnset(varName:String);
}
private typedef Token = {
var s : Bool;
var p : String;
var l : Array<String>;
}
private typedef ExprToken = {
var s : Bool;
var p : String;
}
class HxTemplate {
var splitter:EReg;
var expr_splitter:EReg;
var expr_trim:EReg;
var expr_int:EReg;
var expr_float:EReg;
var expr : TemplateExpr;
var context : Dynamic;
var macros : Dynamic;
var fills : Hash<TemplateExpr>;
var stack : List<Dynamic>;
var buf : StringBuf;
var parentResolve:String->Dynamic;
public function new( str : String ) {
splitter = ~/(::(?:[A-Za-z0-9_ ()&|!+=\/><*."?-]|(?:[:][A-Za-z0-9_ ()&|!+=\/><*."-]))+::|\$\$([A-Za-z0-9_-]+)\()/;
expr_splitter = ~/(\(|\)|[ \r\n\t]*"[^"]*"[ \r\n\t]*|[?!+=\/><*.&|:-]+)/;
expr_trim = ~/^[ ]*([^ ]+)[ ]*$/;
expr_int = ~/^[0-9]+$/;
expr_float = ~/^([+-]?)(?=\d|,\d)\d*(,\d*)?([Ee]([+-]?\d+))?$/;
var tokens = parseTokens(str);
expr = parseBlock(tokens);
if( !tokens.isEmpty() )
throw "Unexpected '"+tokens.first().s+"'";
}
public function execute( context : Dynamic, ?macros : Dynamic, ?parentResolve : String->Dynamic) {
this.macros = if( macros == null ) {} else macros;
this.context = context;
this.parentResolve = parentResolve;
stack = new List();
buf = new StringBuf();
fills = new Hash();
run(expr);
return buf.toString();
}
function resolve( v : String ) : Dynamic {
var e = fills.get(v);
if (e != null)
{
var old = buf;
buf = new StringBuf();
run(e);
var res = buf.toString();
buf = old;
return res;
}
if( Reflect.hasField(context,v) )
return Reflect.field(context,v);
for( ctx in stack )
if( Reflect.hasField(ctx,v) )
return Reflect.field(ctx,v);
if( v == "__current__" )
return context;
if (parentResolve != null) {
return parentResolve(v);
}
if (v.indexOf(".") != -1) {
var chunks = v.split(".");
var cur = resolve(chunks.shift());
while (chunks.length > 0) {
cur = Reflect.field(cur, chunks.shift());
}
return cur;
}
return null;
}
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;
while( npar > 0 ) {
var c = data.charCodeAt(parp);
if( c == 40 )
npar++;
else if( c == 41 )
npar--;
else if( c == null )
throw "Unclosed macro parenthesis";
parp++;
}
var params = data.substr(p.pos+p.len,parp - (p.pos+p.len) - 1).split(",");
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 && p.substr(0, 4) != "set ")
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, 5) == "fill " ) {
p = p.substr(5,p.length - 5);
var name = StringTools.trim(p);
var efill = parseBlock(tokens);
var t = tokens.first();
if (t == null || tokens.pop().p != "end") {
throw "Unclosed 'set'";
}
return OpFill(name, efill);
}
if ( p.substr(0, 6) == "unset " ) {
p = p.substr(6, p.length - 6);
var name = StringTools.trim(p.substr(0, p.length));
if (!~/^[a-zA-Z_][a-zA-Z_0-9]+$/.match(name)) throw "Invalid variableName " + name;
return OpUnset(name);
}
if ( p.substr(0, 4) == "set " ) {
p = p.substr(4, p.length - 4);
var equalSign = p.indexOf("=");
if (equalSign == -1) throw "Invalid set, format ::set varname=value::";
var name = StringTools.trim(p.substr(0, equalSign));
if (!~/^[a-zA-Z_][a-zA-Z_0-9]+$/.match(name)) throw "Invalid variableName " + name;
var eset = parseExpr(StringTools.trim(p.substr(equalSign+1)));
return OpSet(name, eset);
}
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 indexSplit = p.indexOf(" ");
if (indexSplit == -1) throw "Invalid 'foreach', format ::foreach user users::";
var name = p.substr(0, indexSplit);
p = p.substr(name.length + 1, p.length-name.length-1);
var e = parseExpr(p);
var efor = parseBlock(tokens);
var t = tokens.pop();
if( t == null || t.p != "end" )
throw "Unclosed 'foreach'";
return OpForeach(name, 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) {
var d = StringTools.trim(data.substr(0, p.pos));
if (d.length > 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;
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; };
}
if (v == "true") {
return function () { return true; };
}
if (v == "false") {
return function () { return false; };
}
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 makeExpr2( l : List<ExprToken> ) : Void -> Dynamic {
var p = l.pop();
if( p == null )
throw "<eof>";
if( p.s)
return makeConst(p.p);
switch( p.p ) {
case "(":
var e1 = makeExpr(l);
var p = l.pop();
if ( p == null) {
throw p.p;
}
if( p.p == ")" )
return e1;
var e2 = makeExpr(l);
var p2 = l.pop();
if ( p2 == null) {
throw p2.p;
}
var e3 = null;
if (p.p == "?" && p2.p == ":") {
e3 = makeExpr(l);
var p3 = l.pop();
if (p3 == null || p3.p != ")") {
throw p3.p;
}
} else if (p2.p != ")") {
throw p2.p;
}
return switch( p.p ) {
case "?": function() { return cast e1() ? e2() : e3(); };
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 = 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 copyContext(context:Dynamic) {
var c = { };
for (f in Reflect.fields(context)) {
Reflect.setField(c, f, Reflect.field(context, f));
}
return c;
}
function run( e : TemplateExpr ) {
switch( e ) {
case OpFill(name, efill):
fills.set(name, efill);
case OpSet(name, eset):
var old2 = buf;
buf = new StringBuf();
Reflect.setField(context, name, eset());
buf = old2;
case OpUnset(name):
Reflect.deleteField(context, name);
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(name, e,loop):
var v : Dynamic = e();
var length:Int = Reflect.hasField(v, "length") ? v.length : null;
try {
if( v.hasNext == null ) {
var x : Dynamic = v.iterator();
if( x.hasNext == null ) throw null;
v = x;
}
} catch( e : Dynamic ) {
throw "Cannot iter on " + v;
}
stack.push(context);
context = copyContext(context);
var repeat:Dynamic;
if (Reflect.hasField(context, "repeat")) {
repeat = Reflect.field(context, "repeat");
}
else {
repeat = { };
Reflect.setField(context, "repeat", repeat);
}
if (Reflect.hasField(repeat,name)) throw "foreach Context for variable " + name + " is already defined ";
var curCtx = { };
var foreachCtx = { };
Reflect.setField(repeat, name, foreachCtx);
var v : Iterator<Dynamic> = v;
var even = true;
var index = 0;
var first = true;
for ( ctx in v ) {
var last = (length != null) ? (index == length - 1) : null;
Reflect.setField(context, name, ctx);
var foreachCtx = {
first : first,
even : even,
odd : !even,
index : index,
last : last,
number : index + 1,
size : length
};
Reflect.setField(repeat, name, foreachCtx);
run(loop);
even = !even;
index++;
first = false;
}
Reflect.deleteField(repeat, name);
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
}
}
}
}
class HxTemplateTest extends TestCase
{
public function new()
{
super();
}
public function testSetAndUnsetVariable()
{
var txt = "::set test=1::"
+ "::unset test::"
+ "::(test==1)::";
var tmp = new HxTemplate(txt);
var res = tmp.execute( { } );
assertEquals("false", res);
}
public function testAccessArrayLength()
{
var txt = "::if (categories.length > 0)::"
+ "length>0"
+ "::else::"
+ "length=0"
+ "::end::";
var tmp = new HxTemplate(txt);
var categories = [];
var res = tmp.execute( { categories : categories } );
assertEquals("length=0", res);
}
public function testAccessBlockVariableFromOuterContext_VariableShouldNotBeDefined()
{
var txt = "::foreach cat categories::"
+ '::set style="myStyle"::'
+ "::style:: "
+ "::end::"
+ "::style::";
var tmp = new HxTemplate(txt);
var categories = ["one", "two"];
var res = tmp.execute( { categories : categories } );
assertEquals("myStyle myStyle null", res);
}
public function testComplexForeachWithSetAndTernaryOperator ()
{
var txt = "::foreach cat categories::"
+ '::set style=(repeat.cat.odd?"odd":"even")::'
+ "::style::,::cat:: "
+ "::end::";
var tmp = new HxTemplate(txt);
var categories = ["one", "two", "three"];
var res = tmp.execute( { categories : categories } );
assertEquals("even,one odd,two even,three ", res);
}
public function testSimpleIfAnd ()
{
var txt = "::if (a && b)::"
+ "true"
+ "::end::";
var tmp = new HxTemplate(txt);
var res = tmp.execute( { a:true, b:true } );
assertEquals("true", res);
}
public function testComplexIfAnd ()
{
var txt = "::if (((a == true) && (b == true)))::"
+ "true"
+ "::end::";
var tmp = new HxTemplate(txt);
var res = tmp.execute( { a:true, b:true } );
assertEquals("true", res);
}
public function testComplexIfAndWithTemplate ()
{
var txt = "::if (((a != null) && (b != null)))::"
+ "true"
+ "::end::";
var tmp = new HxTemplate(txt);
var res = tmp.execute( { a:true, b:true } );
assertEquals("true", res);
}
public function testComplexForeachWithSetAndTernaryOperator2 ()
{
var txt = "::foreach cat categories::"
+ '::set style=(repeat.cat.odd?"odd":"even")::'
+ "::cat:: "
+ "::end::";
var tmp = new HxTemplate(txt);
var categories = ["one", "two", "three"];
var res = tmp.execute( { categories : categories } );
assertEquals("one two three ", res);
}
public function testIfAndSetVariable_TypeComplexExpr ()
{
var tmp = new HxTemplate("::if repeat.cat.odd::::set style=0::::else::::set style=1::::end::::style::");
var repeat = {
cat : {
odd : true
}
}
var res = tmp.execute( { repeat: repeat } );
assertEquals("0", res);
}
public function testFullQualifiedVariableInMacro ()
{
var tmp = new HxTemplate("$$printvar(repeat.cat.odd)");
var repeat = {
cat : {
odd : true
}
}
var printvar = function (resolve:String->Void, what:Dynamic)
{
return resolve(what);
};
var res = tmp.execute( { repeat: repeat }, { printvar:printvar } );
assertEquals("true", res);
}
public function testTernaryIfElseOperatorInSetVariableWithString ()
{
var tmp = new HxTemplate('::set style=(even?"even":"odd")::::style::');
var res = tmp.execute( { even : true} );
assertEquals("even", res);
}
public function testTernaryIfElseOperatorInSetVariable_WithBool ()
{
var tmp = new HxTemplate('::set style=(true?"even":"odd")::::style::');
var res = tmp.execute({} );
assertEquals("even", res);
}
public function testTernaryIfElseOperatorInSetVariable_WithBoolAndVariablesToResolve_UseLeftSideExpression ()
{
var tmp = new HxTemplate('::set style=(true?one:two)::::style::');
var res = tmp.execute({one:"One", two:"Two"} );
assertEquals("One", res);
}
public function testTernaryIfElseOperatorInSetVariable_WithBoolAndVariablesToResolve_UseRightSideExpression ()
{
var tmp = new HxTemplate('::set style=(false?one:two)::::style::');
var res = tmp.execute({one:"One", two:"Two"} );
assertEquals("Two", res);
}
public function testSetVariable_TypeInt ()
{
var tmp = new HxTemplate("::set foo=1::::foo::");
var res = tmp.execute({});
assertEquals("1", res);
}
public function testSetVariable_TypeString ()
{
var tmp = new HxTemplate("::set foo=\"bar\"::::foo::");
var res = tmp.execute({});
assertEquals("bar", res);
}
public function testSetVariable_FalseSyntax_ThrowsException ()
{
var f = function () {
var tmp = new HxTemplate("::set foo 1::::foo::");
}
assertRaises(f, String);
}
public function testFillVariable ()
{
var tmp = new HxTemplate("::fill content::foo::end::::content::");
var res = tmp.execute({});
assertEquals("foo", res);
}
public function testFillVariable_Complex ()
{
var tmp = new HxTemplate("::fill content::::foo::::end::::content::");
var res = tmp.execute({foo:"bar"});
assertEquals("bar", res);
}
public function testForeach ()
{
var tmp = new HxTemplate("::foreach u users::::u.name::::end::");
var res = tmp.execute({users : [{name:"foo"}, {name:"bar"}]});
assertEquals("foobar", res);
}
public function testForeachContext_even ()
{
var tmp = new HxTemplate("::foreach u users::::repeat.u.even::::end::");
var res = tmp.execute({users : [{name:"foo"}, {name:"bar"}]});
assertEquals("truefalse", res);
}
public function testForeachContext_odd ()
{
var tmp = new HxTemplate("::foreach u users::::repeat.u.odd::::end::");
var res = tmp.execute({users : [{name:"foo"}, {name:"bar"}]});
assertEquals("falsetrue", res);
}
public function testForeachContext_size ()
{
var tmp = new HxTemplate("::foreach u users::::repeat.u.size::::end::");
var res = tmp.execute({users : [{name:"foo"}, {name:"bar"}]});
assertEquals("22", res);
}
public function testForeachContext_last ()
{
var tmp = new HxTemplate("::foreach u users::::repeat.u.last::::end::");
var res = tmp.execute({users : [{name:"foo"}, {name:"bar"}]});
assertEquals("falsetrue", res);
}
public function testForeachContext_index ()
{
var tmp = new HxTemplate("::foreach u users::::repeat.u.index::::end::");
var res = tmp.execute({users : [{name:"foo"}, {name:"bar"}]});
assertEquals("01", res);
}
public function testForeachContext_number ()
{
var tmp = new HxTemplate("::foreach u users::::repeat.u.number::::end::");
var res = tmp.execute({users : [{name:"foo"}, {name:"bar"}]});
assertEquals("12", res);
}
public function testForeachOuterContext ()
{
var t = "::foreach u users::";
t += "::bar::";
t += "::end::";
var tmp = new HxTemplate(t);
var res = tmp.execute({bar:"7", users : [{name:"foo"}, {name:"bar"}]});
assertEquals("77", res);
}
public function testIfForeachContextIsRemovedAfterForeach ()
{
var t = "::foreach u users::";
t += "::end::";
t += "::u.name::";
var tmp = new HxTemplate(t);
var res = tmp.execute({bar:"7", users : [{name:"foo"}, {name:"bar"}]});
assertEquals("null", res);
}
public function testNestedForeachContexts ()
{
var t = "::foreach u users::";
t += "::foreach i names::";
t += "::repeat.u.even::";
t += ",";
t += "::repeat.i.even::";
t += " ";
t += "::end::";
t += "::end::";
var tmp = new HxTemplate(t);
var res = tmp.execute( { bar:"7", users : [ { name:"foo" }, { name:"bar" } ],
names : ["jim", "tim"]});
assertEquals("true,true true,false false,true false,false ", res);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment