瀏覽代碼

- added templo template system

Laurent Bedubourg 19 年之前
父節點
當前提交
be719a0fd6
共有 5 個文件被更改,包括 1308 次插入0 次删除
  1. 188 0
      std/mtwin/templo/Generator.hx
  2. 144 0
      std/mtwin/templo/Macro.hx
  3. 594 0
      std/mtwin/templo/Parser.hx
  4. 228 0
      std/mtwin/templo/Preprocessor.hx
  5. 154 0
      std/mtwin/templo/Template.hx

+ 188 - 0
std/mtwin/templo/Generator.hx

@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2006, Motion-Twin
+ * 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 MOTION-TWIN "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.
+ */
+
+package mtwin.templo;
+
+/**
+	Generates template .neko file.
+**/
+class Generator {
+
+	var out : StringBuf;
+	var htmlBuf : StringBuf;
+
+	public function new(){
+		out = new StringBuf();
+		htmlBuf = null;
+	}
+
+	public function toString() : String {
+		flushHtml();
+	
+		var result = new StringBuf();
+		result.add('
+			String = $loader.String;
+			iter = $loader.iter;
+			buffer_new = $loader.loadprim("std@buffer_new", 0);
+			buffer_add = $loader.loadprim("std@buffer_add", 2);
+			buffer_string = $loader.loadprim("std@buffer_string", 1);
+
+			html_escape = function( data ){
+				var str = String.new($string(data));
+				if (str == null){
+					$throw("String.new returned null\n");
+				}
+				return str.split(String.new("&")).join(String.new("&amp;")).split(String.new("<")).join(String.new("&lt;")).split(String.new(">")).join(String.new("&gt;")).split(String.new("\\\"")).join(String.new("&quot;"));
+			}
+
+			is_true = function( data ){
+				if (data == "") return false;
+				return $istrue(data);
+			}
+
+			new_repeat = function( data ){
+				var result = $new(null);
+				result.data = data;
+				result.index = 0-1;
+				result.number = 0;
+				result.first = true;
+				result.last = false;
+				result.odd = true;
+				result.even = false;
+				if (data.get_length != null) result.size = data.get_length();
+				else if (data.size != null) result.size = data.size();
+				else result.size = null;
+				result.next = function(v){
+					this.current = v;
+					this.index = this.index + 1;
+					this.first = this.index == 0;
+					this.number = this.number + 1;
+					this.last = (this.number == this.size);
+					this.even = (this.number % 2) == 0;
+					this.odd = (this.even == false);
+				}
+				return result;
+			}
+
+			new_output_buffer = function( parent ){
+				var result = $new(null);
+				result.parent = parent;
+				result.buf = buffer_new();
+				result.add = function(str){ return buffer_add(this.buf, str); }
+				result.str = function(){ return buffer_string(this.buf); }
+				return result;
+			}
+
+			new_context = function( parent, vars ){
+				var result = $new(null);
+				result.parent = parent;
+				result.__isTemplateContext = true;
+				if (vars == null){
+					result.vars = $new(null);
+				}
+				else {
+					result.vars = vars;
+				}
+				result.get = function( field ){
+					if ($objfield(this.vars, field)) return $objget(this.vars, field);
+					if (this.parent == null) return null;
+					return this.parent.get(field);
+				}
+				result.set = function( field, v ){
+					$objset(this.vars, field, v);
+				}
+				return result;
+			}
+
+			template = function( macro, params ){
+				var __ctx = null;
+				if (params.__isTemplateContext) {
+					__ctx = new_context(params, null);
+				}
+				else {
+					__ctx = new_context(null, params);
+				}
+				var __glb = __ctx;
+				var __out = new_output_buffer(null);
+				
+//--- HERE COMES THE TEMPLATE CODE ---
+');
+		result.add(out.toString());
+		result.add('//--- END OF TEMPLATE CODE ---
+				return __out.str();
+			}
+
+			$exports.template = template;
+		'); //'
+		return result.toString();
+	}
+
+	public function writeHtml( str:String ){
+		if (htmlBuf == null){
+			htmlBuf = new StringBuf();
+		}
+		htmlBuf.add(str);
+	}
+
+	public function writeCode( str:String ){
+		if (htmlBuf != null){
+			flushHtml();
+		}
+		out.add("__out.add("+str+");\n");
+	}
+
+	public function writeEscapedCode( str:String ){
+		if (htmlBuf != null){
+			flushHtml();
+		}
+		out.add("__out.add(html_escape("+str+"));\n");
+	}
+
+	public function add( code:String ){
+		if (htmlBuf != null){
+			flushHtml();
+		}
+		out.add(code);
+	}
+
+	public function getVar( name:String ) : String {
+		return "__ctx.get($hash(\""+name+"\"))";
+	}
+
+	public function setVar( name:String, exp:String ){
+		add("__ctx.set($hash(\""+name+"\"), "+exp+");\n");
+	}
+
+	public function flushHtml(){
+		if (htmlBuf == null) return;
+		var html = htmlBuf.toString();
+		html = StringTools.replace(html, "\\", "\\\\");
+		html = StringTools.replace(html, "\\'", "\\'");
+		html = StringTools.replace(html, "\"", "\\\"");
+		html = StringTools.replace(html, "\n", "\\n");
+		out.add("__out.add(\"" + html + "\");\n");
+		htmlBuf = null;
+	}
+}

+ 144 - 0
std/mtwin/templo/Macro.hx

@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2006, Motion-Twin
+ * 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 MOTION-TWIN "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.
+ */
+
+package mtwin.templo;
+
+class Macro {
+
+	static var R_PARAMS = ~/^([a-zA-Z0-9_]+)\((.*?)\)$/g;
+	static var R_NOTVAR = ~/[^a-zA-Z0-9_.]/;
+	static var R_NUM    = ~/^[0-9.]+$/;
+	static var R_VAR    = ~/^::(.*?)::$/;
+
+	public var name : String;
+	var params : Array<String>;
+	var source : String;
+
+	public function new( n:String, c:String ){
+		params = new Array();
+		name = n;
+		if (R_PARAMS.match(name)){
+			name = R_PARAMS.matched(1);	
+			var self = this;
+			var i = 0;
+			for (param in R_PARAMS.matched(2).split(",")){
+				params[i++] = StringTools.trim(param);
+			}
+		}
+		source = c;
+	}
+
+	public function expand( p:Array<String> ) : String {
+		if (params.length != p.length){
+			throw "macro "+name+" takes "+params.length+" arguments"+params.toString()+"\ngot: "+p.toString();
+		}
+		var res = source;
+		for (i in 0...params.length){
+			var replace = new EReg("::\\s*?"+params[i]+"\\s*?::", "g");
+			var tmp = replace.replace(res, StringTools.replace(p[i], "$","$$"));
+			res = tmp;
+			var isNum = R_NUM.match(p[i]);
+			var isVar = (R_VAR.match(p[i]) && R_VAR.matched(1).indexOf("::", 0) == -1);
+			var pos = res.indexOf("::", 0);
+			while (pos != -1){
+				var end = res.indexOf("::", pos+2);
+				if (end == null){
+					neko.Lib.print(res);
+					throw "Unable to find matching ::";
+				}
+				var exp = res.substr(pos+2, end-pos-2);
+				var rep = replaceArgumentInExpression(exp, params[i], p[i], isVar, isNum);
+				res = res.substr(0, pos+2) + rep + res.substr(end, res.length - end);
+				pos = end - exp.length + rep.length + 2;
+				pos = res.indexOf("::", pos);
+			}
+		}
+		return res;
+	}
+
+	static function replaceArgumentInExpression( exp:String, paramName:String, value:String, isVar:Bool, isNum:Bool ) : String {	
+		var repl = if (isNum) value 
+			else if (isVar) "(" + value.substr(2, value.length-4) + ")" 
+			else stringArgumentToExpressionCompound(value);
+		var res = exp;
+		var pos = res.indexOf(paramName, 0);
+		while (pos != -1){
+			var end = pos + paramName.length;
+			var before = if (pos > 0) res.charAt(pos-1) else " ";
+			var after = if (end < res.length) res.charAt(end) else " ";
+			if (R_NOTVAR.match(before) && (after == "." || R_NOTVAR.match(after))){
+				res = res.substr(0, pos) + repl + res.substr(end, res.length-end);
+				end = end - paramName.length + repl.length;
+			}
+			pos = res.indexOf(paramName, end+1);
+		}
+		return res;
+	}
+
+	static function xmlToString( xml:Xml ) : String {
+		if (xml.nodeType != Xml.Element){
+			return xml.nodeValue;
+		}
+		var res = new StringBuf();
+		res.add("<");
+		res.add(xml.nodeName);
+		for (i in xml.attributes()){
+			res.add(" ");
+			res.add(i);
+			res.add("=\"");
+			res.add(StringTools.htmlEscape(xml.get(i)));
+			res.add("\"");
+		}
+		if (xml.firstChild() != null){
+			res.add(">");
+			for (x in xml)
+				res.add(xmlToString(x));
+			res.add("</");
+			res.add(xml.nodeName);
+			res.add(">");
+		}
+		else {
+			res.add("/>");
+		}
+		return res.toString();
+	}
+
+	static function stringArgumentToExpressionCompound( str:String ) : String {
+		var res = StringTools.replace(str,"'","\\'");
+		var pos = res.indexOf("::",0);
+		while ( pos != -1 ){
+			var end = res.indexOf("::", pos+2);
+			if (end == -1)
+				throw "Malformed expression '"+str+"'";
+			var left = res.substr(0, pos);
+			var data = res.substr(pos+2, end-pos-2);
+			var right = res.substr(end+2, res.length-end-2);
+			res = left + "\'+" + data + "+\'" + right;
+			pos = end + 2;
+			pos = res.indexOf("::",pos);
+		}
+		return "(\'" + res + "\')";
+	}
+}

+ 594 - 0
std/mtwin/templo/Parser.hx

@@ -0,0 +1,594 @@
+/*
+ * Copyright (c) 2006, Motion-Twin
+ * 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 MOTION-TWIN "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.
+ */
+
+package mtwin.templo;
+
+class Parser {
+
+	static var REGEX_EXP = ~/^([a-zA-Z][A-Za-z0-9_]{0,})[ \n\r\t]+(.*?)$/gsm;
+	static var REGEX_DXP = ~/::([^:].*?)::/gsm;
+
+	static var MT = "mt";
+	static var MT_IF = "mt:if";
+	static var MT_ELSEIF = "mt:elseif";
+	static var MT_ELSE = "mt:else";
+	static var MT_CONTENT = "mt:content";
+	static var MT_REPLACE = "mt:replace";
+	static var MT_FOREACH = "mt:foreach";
+	static var MT_ATTRIBUTES = "mt:attr";
+	static var MT_SET = "mt:set";
+	static var MT_FILL = "mt:fill";
+	static var MT_OMIT_TAG = "mt:omit-tag";
+	static var MT_MACRO = "mt:macro";
+	static var MT_USE = "mt:use";
+
+	static var XHTML_EMPTY = ["area","base","basefont","br","col","frame","hr","img","input","isindex","link","meta","param"];
+	static var XHTML_ATTRS = ["compact","nowrap","ismap","declare","noshade","checked","disabled","readonly","multiple","selected","noresize","defer"];
+
+	var out : mtwin.templo.Generator;
+
+	public function new(){
+		out = new mtwin.templo.Generator();
+	}
+
+	public function parse( xml:Xml ) : String {
+		parseNode(xml);
+		return out.toString();
+	}
+
+	function parseNode( xml:Xml ){
+		switch (untyped xml.nodeType){
+			case Xml.Document:
+				for (child in xml) parseNode(child);
+			case Xml.CData:
+				parseCDATA(xml);
+			case Xml.Comment:
+				parseComment(xml);
+			case Xml.Element:
+				parseElement(xml);
+			default:
+				echoString(xml.toString());
+		}
+	}
+
+	function parseElement( xml:Xml ){
+		var mtSet = extractAttribute(xml, MT_SET);
+		if (mtSet != null){
+			var parts = Lambda.array(mtSet.split("=").iterator());
+			var dest = StringTools.trim(parts.shift());
+			var exp = parseExpression( StringTools.trim(parts.join("=")) );
+			out.add("__ctx.set($hash(\""+dest+"\"), ("+exp+"));");
+			return;
+		}
+
+		var mtUse = extractAttribute(xml, MT_USE);
+		if (mtUse != null){
+			var f = mtwin.templo.Template.fromFile(mtUse); // ensure template is parsed (beware of cycle)
+			out.add("tmp = __ctx.get($hash(\"__content__\"));\n");
+			out.add("__out = new_output_buffer(__out);\n");
+			parseNode(xml);
+			out.add("__ctx.set($hash(\"__content__\"), __out.str());\n");
+			out.add("__out = __out.parent;\n");
+			out.add("mcr = macro(\""+mtUse+"\");\n");
+			out.add("__out.add(mcr.template(macro, __ctx));\n");
+			out.add("__ctx.set($hash(\"__content__\"), tmp);\n");
+			return;
+		}
+		
+		var mtFill = extractAttribute(xml, MT_FILL);
+		if (mtFill != null){
+			out.add("__out = new_output_buffer(__out);\n");
+			parseNode(xml);
+			out.add("__ctx.set($hash(\""+StringTools.trim(mtFill)+"\"), String.new(__out.str()));\n");
+			out.add("__out = __out.parent;\n");
+			return;
+		}
+		
+		var mtIf = extractAttribute(xml, MT_IF);
+		if (mtIf != null){
+			out.add("if (is_true("+parseExpression(mtIf)+")){\n");
+			parseNode(xml);
+			out.add("}\n");
+			return;
+		}
+		
+		var mtElseIf = extractAttribute(xml, MT_ELSEIF);
+		if (mtElseIf != null){
+			out.add("else if (is_true("+parseExpression(mtElseIf)+")){\n");
+			parseNode(xml);
+			out.add("}\n");
+			return;
+		}
+		
+		var mtElse = extractAttribute(xml, MT_ELSE);
+		if (mtElse != null){
+			out.add("else {\n");
+			parseNode(xml);
+			out.add("}\n");
+			return;
+		}
+
+		var mtForeach = extractAttribute(xml, MT_FOREACH);
+		if (mtForeach != null){
+			var o = extractExpressionTarget(mtForeach);		
+			out.add("var loop = "+parseExpression(o.exp)+";\n");
+			out.add("__ctx.vars.repeat_"+o.target+" = new_repeat(loop);\n");
+			out.add("iter(loop, function(__item){\n");
+			out.add("__ctx.vars.repeat_"+o.target+".next(__item);\n");
+			out.setVar(o.target, "__item");
+			parseNode(xml);
+			out.add("});\n");
+			return;
+		}
+
+		var mtReplace = extractAttribute(xml, MT_REPLACE);
+		if (mtReplace != null){
+			echoExpression(mtReplace);
+			return;
+		}
+
+		var mtOmitTag = extractAttribute(xml, MT_OMIT_TAG);
+		if (mtOmitTag == null && xml.nodeName == MT){
+			mtOmitTag = "true";
+		}
+		
+		var mtAttributes = extractAttribute(xml, MT_ATTRIBUTES);
+		var mtContent = extractAttribute(xml, MT_CONTENT);
+
+		var hasContent = (mtContent != null || xml.firstChild() != null);
+
+		var xhtmlEmpty = isXHTMLEmptyTag(xml.nodeName);
+		if (hasContent && xhtmlEmpty){
+			hasContent = false;
+		}
+		if (!hasContent && !xhtmlEmpty){
+			hasContent = true;
+		}
+		
+		if (mtOmitTag == null){
+			out.writeHtml("<"+xml.nodeName);
+			if (mtAttributes != null){
+				doMtAttributes(mtAttributes, xml);
+			}
+			else {
+				echoAttributes(xml);
+			}
+			if (hasContent){ 
+				out.writeHtml(">");
+			}
+			else {
+				out.writeHtml("/>");
+				return;
+			}
+		}
+		
+		if (mtContent != null){
+			echoExpression(mtContent);
+		}
+		else {
+			for (child in xml)
+				parseNode(child);
+		}
+		
+		if (mtOmitTag == null && hasContent){
+			out.writeHtml("</" + xml.nodeName + ">");
+		}
+	}
+
+	function doMtAttributes( att:String, xml:Xml ){
+		var overwritten = new Hash();
+		var parts = Lambda.array(splitExpression(att).iterator());
+		for (i in 0...parts.length){
+			var x = StringTools.trim(parts[i]);
+			var o = extractExpressionTarget(x);
+			var exp = parseExpression(o.exp);
+			if (isBooleanAttribute(o.target)){
+				out.add("if ("+exp+"){\n");
+				out.add("__out.add(\" "+o.target+"=\\\""+o.target+"\\\"\");\n");
+				out.add("}");
+			}
+			else {
+				out.add("var value = "+exp+";\n");
+				out.add("if (value != false && value != null){\n");
+				out.add("__out.add(\" "+o.target+"=\\\"\");\n");
+				out.add("__out.add(value);\n");
+				out.add("__out.add(\"\\\"\");\n");
+				out.add("}");
+			}
+			overwritten.set(o.target, true);
+		}
+
+		for (field in xml.attributes()){
+			var attName = field;
+			if (!overwritten.exists(attName)){
+				var attVal = xml.get(field);
+				if (attVal != null){ // mt attributes
+					out.add("__out.add(\" "+attName+"=\\\"\");\n");
+					echoString(attVal);
+					out.add("__out.add(\"\\\"\");\n");
+				}
+			}
+		}
+	}
+
+	function splitExpression( exp:String ) : List<String> {
+		var result = new List();
+		var start = 0;
+		var len = exp.length;
+		for (i in 0...len){
+			if (exp.charAt(i) == ";"){
+				if (exp.charAt(i+1) == ";"){
+					++i;
+				}
+				else {
+					result.push(exp.substr(start, i-start));
+					start = i+1;
+				}
+			}
+		}
+		if (start < len){
+			result.push(exp.substr(start, len-start));
+		}
+		return result;
+	}
+
+	function echoAttributes( xml:Xml ){
+		if (xml == null)
+			return;
+		for (att in xml.attributes()){
+			var value = xml.get(att);
+			if (value == null)
+				continue;
+			out.writeHtml(" "+att);
+			out.writeHtml("=\"");
+			echoString( StringTools.replace(value, "\"", "&quot;") );
+			out.writeHtml("\"");
+		}
+	}
+
+	function parseComment( xml:Xml ){
+		out.writeHtml(StringTools.htmlUnescape(xml.nodeValue));
+	}
+
+	function echoExpression( exp:String ){
+		exp = StringTools.trim(exp);
+		if (exp.indexOf("raw ", 0) == 0){
+			out.writeCode(parseExpression(exp.substr(4, exp.length-4)));
+		}
+		else {
+			out.writeEscapedCode(parseExpression(exp));
+		}
+	}
+
+	function echoString( str:String ){
+		var source = str;
+		while (REGEX_DXP.match(source)){
+			var pos = REGEX_DXP.matchedPos();
+			if (pos.pos > 0){
+				out.writeHtml(source.substr(0,pos.pos));
+			}
+			echoExpression(StringTools.htmlUnescape(REGEX_DXP.matched(1)));
+			source = source.substr(pos.pos + pos.len, source.length - pos.pos - pos.len);
+		}
+		if (source.length > 0){
+			out.writeHtml(source);
+		}
+	}
+
+	function parseCDATA( xml:Xml ){
+		out.writeHtml("<![CDATA[\n");
+		var cdataSrc = xml.nodeValue;
+		cdataSrc = StringTools.htmlUnescape(cdataSrc);
+		cdataSrc = "<mt>" + cdataSrc + "</mt>";
+		var cdataxml = null;
+		try {
+			cdataxml = Xml.parse(cdataSrc);
+			if (cdataxml == null)
+				throw "Unable to parse CDATA content";
+		}
+		catch (e:Dynamic){
+			throw { error:e, xmlsource:cdataSrc, cdatasource:xml.nodeValue };
+		}
+		restoreCDATAHtmlEncoding(cdataxml);
+		parseNode(cdataxml);
+		out.writeHtml("]]>");
+	}
+
+	static function isBooleanAttribute( attName:String ) : Bool {
+		for (f in XHTML_ATTRS){
+			if (f == attName) return true;
+		}
+		return false;
+	}
+
+	static function isXHTMLEmptyTag( tag:String ) : Bool {
+		for (f in XHTML_EMPTY){
+			if (f == tag) return true;
+		}
+		return false;
+	}
+
+	static function splitArguments( str:String ) : List<String> {
+		var res = new List();
+		var arg = "";
+		var len = str.length;
+		var cto = 0;
+		for (i in 0...len){
+			var c = str.charAt(i);
+			if (c == "("){ 
+				cto++; 
+			}
+			else if (c == ")"){ 
+				cto--; 
+			}
+			if (c == "," && cto == 0) {
+				res.add(StringTools.trim(arg));
+				arg = "";
+			}
+			else {
+				arg += c;
+			}
+		}
+		if (arg != ""){
+			res.add(StringTools.trim(arg));
+		}
+		return res;
+	}
+
+	static function findEndOfBracket( str:String, pos:Int ) : Int {
+		var len = str.length;
+		var ctopen = 0;
+		for (i in (pos+1)...(len)){
+			var c = str.charAt(i);
+			if (c == "("){ 
+				ctopen++; 
+			}
+			else if (c == ")"){
+				if (ctopen == 0){
+					return i-1;
+				}
+				else {
+					ctopen--;
+				}
+			}
+		}
+		return -1;
+	}
+
+	static function extractAttribute( xml:Xml, id:String ) : String {
+		var res = xml.get(id);
+		if (res == null){
+			return null;
+		}
+		xml.set(id, null);
+		return StringTools.trim(res.split("&quot;").join("\"").split("&amp;").join("&"));
+	}
+
+	static function isExpressionKeyword( varName:String ) : Bool {
+		return varName == "true" || varName == "false" || varName == "null" || varName == "if" || varName == "else";
+	}
+
+	static function restoreCDATAHtmlEncoding( xml:Xml ){
+		if (xml.nodeType == Xml.Element || xml.nodeType == Xml.Document)
+			for (x in xml) restoreCDATAHtmlEncoding(x);
+		else
+			xml.nodeValue = StringTools.htmlUnescape(xml.nodeValue);
+	}
+
+	static function extractExpressionTarget( exp:String ) : { target:String, exp:String } {
+		if (REGEX_EXP.match(exp)){
+			return {target:REGEX_EXP.matched(1), exp:REGEX_EXP.matched(2)};
+		}
+		return {target:null, exp:exp};
+	}
+
+	public static function parseExpression( exp:String ) : String {
+		var r_num = ~/[0-9]+/;
+		var r_digit = ~/[0-9.]+/;
+		var r_var = ~/[\$a-zA-Z0-9_]/;
+		var r_op = ~/[!+-\/*<>=&|%]+/;  //*/
+		var result = new StringBuf();
+		var states = { none:0, string:1, dstring:2, variable:3, num:4, member:5 };
+		var str = StringTools.trim(exp);
+		var state = states.none;
+		var mark = 0;
+		var getter = false;
+		var len = str.length;
+		for (i in 0...len+1){
+			var skip = false;
+			var c = if (i == len) "\n" else str.charAt(i);
+			var n = if (i+1 >= len) "\n" else str.charAt(i+1);
+			switch (state){
+				case states.none:
+					if (r_num.match(c)){
+						state = states.num;
+					}
+					else if (c == "\""){
+						result.add("String.new(");
+						state = states.dstring;
+					}
+					else if (c == "'"){
+						result.add("String.new(\"");
+						state = states.string;
+						skip = true;
+					}
+					else if (c == "."){
+						state = states.member;
+					}
+					else if (r_op.match(c)){
+						if (c == '!'){
+							if (n == "="){
+							}
+							else {
+								result.add("false == ");
+								skip = true;
+							}
+						}
+					}
+					else if (c == "("){
+						var end = findEndOfBracket(str, i);
+						var sub = str.substr(i+1, end-i);
+						result.add("(");
+						result.add(parseExpression(sub));
+						//result.add(")");
+						i = end;
+						skip = true;
+					}
+					else if (r_var.match(c)){
+						state = states.variable;
+						mark = i;
+					}
+					
+				case states.string:
+					if (c == "\\" && n == "'"){ 
+						result.add("'");
+						skip = true;
+						++i; 
+					}
+					else if (c == "\""){
+						result.add("\\");
+					}						
+					else if (c == "'"){
+						state = states.none;
+						result.add("\")");
+						//++i;
+						skip = true;
+					}
+					
+				case states.dstring:
+					if (c == "\\" && n == "'"){
+						result.add("\\'");
+						skip = true;
+						++i;
+					}
+					else if (c == "\\" && n == "\""){
+						++i;
+					}
+					else if (c == "\""){
+						state = states.none;
+						result.add("\")");
+						//++i;
+						skip = true;
+					}
+
+				case states.variable:
+					if (r_var.match(c)){
+					}
+					else if (c == "."){
+						var variable = str.substr(mark, i-mark);
+						if (variable == "repeat"){
+							result.add("__ctx.vars.repeat_");
+							state = states.member;
+							skip = true;
+						}
+						else {
+							result.add("__ctx.get($hash(\"");
+							result.add(variable);
+							result.add("\"))");
+							state = states.member;
+							if (i < len && str.charAt(i+1) == "_"){
+								result.add(".get");
+								getter = true;
+								skip = true;
+							}
+						}
+					}
+					else {
+						var variable = str.substr(mark, i-mark);
+						if (isExpressionKeyword(variable)){
+							result.add(variable);
+						}
+						else {									
+							result.add("__ctx.get($hash(\"");
+							result.add(variable);
+							result.add("\"))");
+						}
+						state = states.none;
+					}
+
+				case states.num:
+					if (r_digit.match(c)){
+					}
+					else {
+						state = states.none;
+					}
+					
+				case states.member:
+					if (r_var.match(c)){
+					}
+					else if (c == "("){
+						if (getter){
+							result.add("()");
+							getter = false;
+						}
+						var end = findEndOfBracket(str, i);
+						var sub = str.substr(i+1, end-i);
+						var argStr = Lambda.array(splitArguments(sub).iterator());
+						for (j in 0...argStr.length){
+							argStr[j] = parseExpression(argStr[j]);
+						}
+						result.add("(");
+						result.add(argStr.join(","));
+						result.add(")");
+						i = end+1; skip = true;
+						/*
+						if (i < len && str.charAt(i) == "."){
+							state = states.member;
+						}
+						else {
+							state = states.none; // test a.foo().bar
+						}
+						*/
+					}
+					else if (c == "."){
+						if (getter){
+							result.add("()");
+							getter = false;
+						}
+						if (i < len && str.charAt(i+1) == "_"){
+							getter = true;
+							result.add(".get");
+							skip = true;
+						}
+					}
+					else {
+						if (getter){
+							result.add("()");
+							getter = false;
+						}
+						state = states.none;
+						if (i != len){
+							i--;
+						}
+						skip = true;
+					}
+			}
+			if (!skip && i < len && state != states.variable){
+				result.add(str.charAt(i));
+			}
+		}
+		return result.toString();
+	}
+}

+ 228 - 0
std/mtwin/templo/Preprocessor.hx

@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2006, Motion-Twin
+ * 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 MOTION-TWIN "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.
+ */
+
+package mtwin.templo;
+
+class Preprocessor {
+
+	static var r_if      = ~/::if (.*?)::/g;
+	static var r_elseif  = ~/::elseif (.*?)::/;
+	static var r_else    = ~/::else *?::/;
+	static var r_foreach = ~/::foreach (.*?)::/g;
+	static var r_fill    = ~/::fill (.*?)::/g;
+	static var r_use     = ~/::use (.*?)::/g;
+	static var r_set     = ~/::set (.*?)::/g;
+
+	static var r_cond    = ~/::cond (.*?)::/g;
+	static var r_repeat  = ~/::repeat (.*?)::/g;
+	static var r_attr    = ~/::attr (.*?)::/g;
+
+	static var r_cdata     = ~/<!\[CDATA\[([^\0]*?)]]>/g;
+	static var r_comment   = ~/<!--([^\0]*?)-->/g;
+	static var r_macroCall = ~/\$\$([a-zA-Z0-9_]+)\(/g;
+	static var r_print     = ~/::(.*?)::/g;
+
+	public static var macros : Hash<mtwin.templo.Macro> = new Hash();
+	public static var macroFileStamp : Float;
+
+	public static function process( str:String ) : String {
+		if (macroFileStamp == null && mtwin.templo.Template.MACROS != null)
+			registerMacroFile(mtwin.templo.Template.BASE_DIR+mtwin.templo.Template.MACROS);
+
+		var res = expandMacros(str);
+		res = escapeCdata1(res);
+		res = res.split("::else::").join("</mt><mt mt:else=\"\">");
+		res = res.split("::end::").join("</mt>");
+		while (r_if.match(res)){
+			res = res.split(r_if.matched(0)).join("<mt mt:if=\""+quote(r_if.matched(1))+"\">");
+		}
+		while (r_elseif.match(res)){
+			res = res.split(r_elseif.matched(0)).join("</mt><mt mt:elseif=\""+quote(r_elseif.matched(1))+"\">");
+		}
+		while (r_foreach.match(res)){
+			res = res.split(r_foreach.matched(0)).join("<mt mt:foreach=\""+quote(r_foreach.matched(1))+"\">");
+		}
+		while (r_fill.match(res)){
+			res = res.split(r_fill.matched(0)).join("<mt mt:fill=\""+quote(r_fill.matched(1))+"\">");
+		}
+		while (r_set.match(res)){
+			res = res.split(r_set.matched(0)).join("<mt mt:set=\""+quote(r_set.matched(1))+"\"/>");
+		}
+		while (r_use.match(res)){
+			res = res.split(r_use.matched(0)).join("<mt mt:use=\""+quote(r_use.matched(1))+"\">");
+		}
+		res = escapeComments(res);
+
+		while (r_cond.match(res)){
+			res = res.split(r_cond.matched(0)).join("mt:if=\""+quote(r_cond.matched(1))+"\"");
+		}
+		while (r_repeat.match(res)){
+			res = res.split(r_repeat.matched(0)).join("mt:foreach=\""+quote(r_repeat.matched(1))+"\"");
+		}
+		while (r_attr.match(res)){
+			res = res.split(r_attr.matched(0)).join("mt:attr=\""+quote(r_attr.matched(1))+"\"");
+		}
+		res = unescapeCdata1(res);
+		res = escapePrints(res);
+		return "<mt>" + res + "</mt>";
+	}
+
+	public static function expandMacros( str:String ) : String {
+		var res = str;
+		while (r_macroCall.match(res)){
+			var macroName = r_macroCall.matched(1);
+			var macroCallPos = r_macroCall.matchedPos();
+			var i = macroCallPos.pos + macroCallPos.len;
+			var args = new Array();
+			var nargs = 0;
+			var startArg = i;
+			var endArg = -1;
+			var oAccos = 0;
+			var oParas = 0;
+			var end = 0;
+			var forceArg = false;
+			while (i < res.length){
+				var c = res.charAt(i);
+				if (c == "("){
+					if (oAccos == 0){
+						oParas++;
+						++i;
+						continue;
+					}
+				}
+				if (c == ")" && oParas > 0){
+					oParas--;
+					++i;
+					continue;
+				}
+				if (c == "{"){
+					if (oAccos == 0){ 
+						startArg = i+1;
+						forceArg = true; 
+					}
+					oAccos++;
+				}
+				if (c == "}"){
+					if (oAccos > 0){ 
+						oAccos--;
+						if (oAccos == 0){ endArg = i; }
+					}
+				}
+				if (oAccos == 0 && oParas == 0 && (c == "," || c == ")")){
+					if (endArg == -1){ endArg = i; }
+					var p = res.substr(startArg, endArg - startArg);
+					p = StringTools.trim(p);
+					if (p.length > 0 || forceArg){
+						args[ nargs ] = p;
+						nargs++;
+					}
+					startArg = i+1;
+					endArg = -1;
+					forceArg = false;
+				}
+				if (oAccos == 0 && (c == ")")){
+					end = i+1;
+					break;
+				}
+				++i;
+			}
+			var mcr = macros.get(macroName);
+			if (mcr == null){
+				throw "Unknown macro "+macroName;
+			}
+			var src = res.substr(r_macroCall.matchedPos().pos, end - r_macroCall.matchedPos().pos);
+			res = res.split(src).join(mcr.expand(args));
+		}
+		return res;
+	}
+
+	public static function registerMacroFile( path:String ){
+		if (!neko.FileSystem.exists(path)){
+			throw "Macro file "+path+" does not exists";
+		}
+		macroFileStamp = neko.FileSystem.stat(path).mtime.getTime();
+		registerMacros(neko.File.getContent(path));
+	}
+
+	public static function registerMacros( str:String ){
+		var src = str;
+		var rFind = ~/<macro\s+name=['"](.*?)['"]\s*?>([^\0]*?)<\/macro>/g; //"
+		while (rFind.match(src)){
+			var pos = rFind.matchedPos();
+			var name = rFind.matched(1);
+			var content = rFind.matched(2);
+			var macro = new mtwin.templo.Macro(name, content);
+			mtwin.templo.Preprocessor.macros.set( macro.name, macro );
+			var end = pos.pos + pos.len;
+			src = src.substr(end, src.length - end);
+		}
+	}
+
+	static function quote( str:String ) : String {
+		return str.split("&").join("&amp;").split("\"").join("&quot;");
+	}
+
+	static function escapeCdata1( str:String ) : String {
+		var res = str;
+		while (r_cdata.match(res)){
+			var pos   = r_cdata.matchedPos();
+			var cdata = r_cdata.matched(1);
+			cdata = cdata.split("&").join("&amp;").split("<").join("&lt;").split(">").join("&gt;");
+			res = res.substr(0, pos.pos) + "<![CDATAX[" + cdata + "]]>" + res.substr(pos.pos+pos.len, res.length-(pos.pos+pos.len));
+		}
+		return res;
+	}
+
+	static function escapeComments( str:String ) : String {
+		var res = str;
+		while (r_comment.match(res)){
+			var pos = r_comment.matchedPos();
+			var comment = r_comment.matched(1);
+			comment = comment.split("&").join("&amp;").split("<").join("&lt;").split(">").join("&gt;");
+			res = res.substr(0, pos.pos) + "<![COMMENT[" + comment + "]]C>" + res.substr(pos.pos+pos.len, res.length-(pos.pos+pos.len));
+		}
+		return res;
+	}
+
+	static function unescapeCdata1( str:String ) : String {
+		var res = str.split("<![CDATAX[").join("<![CDATA[");
+		res = escapeCdata1(res);
+		res = res.split("<![CDATAX[").join("<![CDATA[").split("]]>").join("]]>");
+		res = res.split("<![COMMENT[").join("<!--").split("]]C>").join("-->");
+		return res;
+	}
+
+	static function escapePrints( str:String ) : String {
+		var buf = new StringBuf();
+		while (r_print.match(str)){
+			var pos = r_print.matchedPos();
+			buf.add(str.substr(0,pos.pos));
+			buf.add(StringTools.htmlEscape(r_print.matched(0)));
+			str = str.substr(pos.pos+pos.len, str.length);
+		}
+		buf.add(str);
+		return buf.toString();
+	}
+}

+ 154 - 0
std/mtwin/templo/Template.hx

@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2006, Motion-Twin
+ * 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 MOTION-TWIN "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.
+ */
+
+package mtwin.templo;
+
+class Template {
+
+	/** Enable production mode (no file check). */
+	public static var OPTIMIZED = false;
+	/** Destination of .neko and .n files */
+	public static var TMP_DIR = "/tmp/";
+	/** Templates repository */
+	public static var BASE_DIR = null;
+	/** Macros file */
+	public static var MACROS = "macros.mtt";
+
+	public var execute : Dynamic -> String;
+
+	public function new( file:String ){
+		execute = if (OPTIMIZED) loadTemplate(nekoBin(file)) else fromFile(file);
+	}
+
+	static function nekoId( path:String ) : String {
+		var rpath = path;
+		var temp = path;
+		if (temp.charAt(0) == "/") temp = temp.substr(1, temp.length-1);
+		temp = StringTools.replace(temp, "/", "__");
+		temp = StringTools.replace(temp, "\\", "__");
+		temp = StringTools.replace(temp, ":", "__");
+		temp = StringTools.replace(temp, "____", "__");
+		return temp;
+	}
+
+	static function nekoBin( path:String ) : String {
+		return TMP_DIR + nekoId(path) + ".n";
+	}
+
+	static function nekoSrc( path:String ) : String {
+		return TMP_DIR + nekoId(path) + ".neko";
+	}
+
+	public static function fromFile( path:String ) : Dynamic -> String {
+		if (OPTIMIZED)
+			return loadTemplate(nekoBin(path));
+		if (MACROS != null && mtwin.templo.Preprocessor.macroFileStamp == null)
+			mtwin.templo.Preprocessor.registerMacroFile(BASE_DIR+MACROS);
+		var binPath = nekoBin(path);
+		if (neko.FileSystem.exists(binPath)){
+			var macroStamp  = mtwin.templo.Preprocessor.macroFileStamp;
+			var sourceStamp = neko.FileSystem.stat(BASE_DIR+"/"+path).mtime.getTime();
+			var stamp       = neko.FileSystem.stat(binPath).mtime.getTime();
+			if ((stamp >= sourceStamp) && (macroStamp == null || macroStamp < stamp)){
+				return loadTemplate(binPath);
+			}
+		}
+		var content = neko.File.getContent(BASE_DIR+"/"+path);
+		return fromString(content, nekoId(path));
+	}
+
+	public static function fromString( src:String, ?id:String ) : Dynamic -> String {
+		if (id == null){
+			id = Md5.encode(src);
+		}
+		src = mtwin.templo.Preprocessor.process(src);
+
+		var path = nekoSrc(id);
+		var x = null;
+		try {
+			x = Xml.parse(src);
+		}
+		catch (e:Dynamic){
+			throw { message:"Error in "+id+"\n"+Std.string(e), source:src };
+		}
+
+		var p = new mtwin.templo.Parser();
+		var s = p.parse(x);
+
+		s = "// generated from " + id + "\n//" + src.split("\n").join("//") + "\n" + s;
+
+		var f = neko.File.write(path, false);
+		f.write(s);
+		f.close();
+
+		var r = neko.Sys.command("nekoc -o "+TMP_DIR+" "+path+" 2> "+TMP_DIR+"/nekoc.out");
+		if (r != 0){
+			if (neko.FileSystem.exists(TMP_DIR+"/nekoc.out")){
+				throw "nekoc compilation of "+path+" failed ("+r+") : "+neko.File.getContent(TMP_DIR+"/nekoc.out");
+			}
+			else {
+				throw "nekoc compilation of "+path+" failed ("+r+") -- no nekoc.out available";
+			}
+		}
+
+		return loadTemplate(nekoBin(id));
+	}
+
+	static function loadTemplate( nPath:String ) : Dynamic -> String {
+		return untyped {
+			var loader = __dollar__loader;
+			var oldCache = loader.cache;
+			loader.cache = __dollar__new(oldCache);
+			loader.String = String;
+			loader.iter = function(loop, fnc){
+				if (loop == null){
+					throw "repeat or foreach called on null value";
+				}
+				if (loop.iterator != null){
+					for (v in loop.iterator()){ fnc(v); }
+				}
+				else if (loop.hasNext != null && loop.next != null){
+					for (v in loop){ fnc(v); }
+				}
+				else {
+					throw "repeat or foreach called on non iterable object";
+				}
+			};
+			var code = loader.loadmodule(nPath.__s, loader);
+			loader.cache = oldCache;
+			function(context){
+				var wrapCache = loader.cache;
+				loader.cache = __dollar__new(wrapCache);
+				var macro = function(path){
+					mtwin.templo.Template.fromFile(new String(path));
+					return loader.loadmodule(mtwin.templo.Template.nekoBin(new String(path)).__s, loader);
+				}
+				var result = new String(code.template(macro, context));
+				loader.cache = wrapCache;
+				return result;
+			}
+		}
+	}
+}