/*
 * Fast[1], Powerful[2] Javascript Template Player.
 *
 * [1] Templates are compiled to native javascript when stored.
 * [2] Has control structures + allows arbitrary javascript.
 */

// ensure CVS revision number survives minification
var dummy = '$Id: tmpl.js,v 1.1.6.2 2009-10-05 15:48:59 mjack Exp $'

/*
 * tmpl_store - Store (and compile) a template.
 *
 * Params:
 *   tmpl_name = Name to use when referring to the template.
 *   template  = A string which may contain template directives (see below).
 *               Anything outside a template directive is copied to the output.
 *
 * Template Directives:
 *
 *   <%IF boolExpr%> body <%ELSE IF boolExpr%> body <%ELSE%> body <%END%>
 *     The javascript expression boolExpr in each IF and ELSE IF clause will
 *     be evaluated in turn until one is true; if so the following body will
 *     be played. Otherwise, the body after the ELSE clause will be played.
 *     There can be zero or more ELSE IF clauses and at most one ELSE clause.
 *
 *   <%LOOP varName endExpr%> body <%END%>
 *     body will be played with the local variable given by varName set in
 *     sequence to 0 up to one less than the result of the javascript
 *     expression endExpr. endExpr is evaluated on each iteration.
 *
 *   <%BREAK%>
 *     Only valid within a LOOP body - ends iteration.
 *
 *   <%CONTINUE%>
 *     Only valid within a LOOP body - skip to next iteration.
 *
 *   <%COMMENT ignored%>
 *     Self-explanatory.
 *
 *   <%JS jsStmt%>
 *     The javascript statement jsStmt will be executed and the result
 *     discarded.
 *
 *   <%jsExpr%>
 *     The result of the javascript expression jsExpr will appear in the
 *     output. The expression can of course simply be a global variable name
 *     or property of the dict object supplied when playing the template.
 * 
 *   <%XL code%>
 *     The result of tmpl_xl(code) will appear in the output.
 *     Note that the translation will occur when the template is stored if
 *     a translation for the code has already been stored with tmpl_xl_set().
 *
 * Returns:
 *   Nothing, or throws an exception if the template has errors.
 *
 * Bugs:
 *   Syntax / Error checking is terrible.
 * 
 * Example:
 *
 *   var template =
 *     '<%COMMENT Example Template%>'
 *   + '<pre>\n'
 *   + 'x + 1 = <%x + 1%>\n'
 *   + '<%IF foo%>'
 *   + 'branch1\n'
 *   + '<%ELSE%>'
 *   + 'branch2\n'
 *   + '<%END%>'
 *   + '<%LOOP i limit%>'
 *   + '<%IF i == 3%>skipping 3\n<%CONTINUE%><%END%>'
 *   + '<%IF i == 5%>breaking at 5\n<%BREAK%><%END%>'
 *   + '2i = <%i * 2%>\n'
 *   + '<%END%>'
 *   + '<%LOOP j stuff.length%>'
 *   + '<div<%IF j % 2%> style="color:green"<%END%>>'
 *   + '<%stuff[j]%>'
 *   + '</div>'
 *   + '<%END%>\n'
 *   + '<%XL FIN%>\n'
 *   + '</pre>'
 *   ;
 *   tmpl_xl_set('FIN', 'That\'s all folks');
 *   tmpl_store('test', template);
 *   var dict = {x: 123, limit: 10, foo: true, stuff: [1,2,3,4,5]};
 *   var out = tmpl_play('test', dict);
 *   print('Played:\n' + out);
 *
 */
var _tmpl = {};
function tmpl_store(tmpl_name, template) {
	// We construct a function body that will play the template.
	var body = '';
	// The played template strings will be built up in this var.
	body += "var __ss = [];\n";
	// Make the properties of the object passed when the template is
	// played available in the local scope.
	body += "with (_dict) {\n"
	// Parse the template, looking for <% and %> to indicate the
	// start and end of directives.
	var i = 0;
	var text = '';
	var directive = '';
	var state = 'TEXT';
	for (; i < template.length; i++) {
		var c = template.charAt(i);
		if (state == 'TEXT') {
			if (c == '<') {
				state = 'MAYBE_OPEN';
			} else {
				text += c;
			}
		} else if (state == 'MAYBE_OPEN') {
			if (c == '%') {
				state = 'DIRECTIVE';
			} else {
				text += '<' + c;
				state = 'TEXT';
			}
		} else if (state == 'DIRECTIVE') {
			if (c == '%') {
				state = 'MAYBE_CLOSE';
			} else {
				directive += c;
			}
		} else if (state == 'MAYBE_CLOSE') {
			if (c == '>') {
				// Output the text that led up to the directive.
				if (text.length > 0) {
					body += "__ss.push('" + tmpl_esc_js(text) + "');\n";
				}
				// We've found a directive. See if it is a keyword.
				var m;
				if (       m = directive.match(/^IF\s+(.+)$/)) {
					var expr = m[1];
					body += "if (" + expr + ") {\n";
				} else if (m = directive.match(/^ELSE\s+IF\s+(.+)$/)) {
					var expr = m[1];
					body += "} else if (" + expr + ") {\n";
				} else if (m = directive.match(/^ELSE$/)) {
					body += "} else {\n";
				} else if (m = directive.match(/^LOOP\s+(\w+)\s+(.+)$/)) {
					var varName = m[1];
					var endExpr = m[2];
					body += "var __e = " + endExpr + ";";
					body += "for (";
					body += "var " + varName + " = 0; ";
					body += varName + " < __e; ";
					body += varName + "++) {\n"
				} else if (m = directive.match(/^CONTINUE$/)) {
					body += "continue;\n";
				} else if (m = directive.match(/^BREAK$/)) {
					body += "break;\n";
				} else if (m = directive.match(/^END$/)) {
					body += "}\n";
				} else if (m = directive.match(/^COMMENT\s+/)) {
					// No-op.
				} else if (m = directive.match(/^JS\s+(.+)$/)) {
					var stmt = m[1];
					body += stmt + ";\n";
				} else if (m = directive.match(/^XL\s+(.+)$/)) {
					var code = m[1];
					xl = _tmpl_xl['c' + code];
					if (xl != undefined) {
						// store-time xlation
						if (xl.length > 0) {
							body += "__ss.push('" + tmpl_esc_js(xl) + "');\n";
						}
					} else {
						// play-time xlation
						body += "__ss.push(tmpl_xl('" + tmpl_esc_js(code) + "'));\n";
					}
				} else if (directive.length > 0) {
					// Treat the directive as a JS expression.
					body += "__ss.push(" + directive + ");\n";
				} else {
					throw('Empty directive');
				}
				text = '';
				directive = '';
				state = 'TEXT';
			} else {
				directive += '%' + c;
				state = 'DIRECTIVE'
			}
		} else {
			throw('bad state ' + state);
		}
	}
	// Output any trailing text.
	if (text.length > 0) {
		body += "__ss.push('" + tmpl_esc_js(text) + "');\n";
	}
	if (directive.length > 0) {
		throw('Unterminated directive ' + directive);
	}
	// Close the 'with' statement.
	body += "}\n";
	body += "return __ss.join('');";
	try {
		_tmpl['t' + tmpl_name] = new Function(['_dict'], body);
	} catch (e) {
		throw('Could not compile template: ' + e + '; body was: \n' + body);
	}
	return;
}

/*
 * tmpl_play - Play a previously stored template.
 *
 * Params:
 *   tmpl_name - The name of a template passed to tmpl_store.
 *   dict      - A javascript object whose properties will become
 *               available as local variables within the template.
 *
 *  Returns:
 *   The output from the template, or throws an exception if the template
 *   has an error.
 */
function tmpl_play(tmpl_name, dict) {
	return _tmpl['t' + tmpl_name](dict);
}

/*
 * Store one or more translations for codes.
 * Usage:
 *   tmpl_xl_set(code1, xlation1, ... codeN, xlationN)
 * Call this before storing templates for greater efficiency.
 */
var _tmpl_xl = {};
function tmpl_xl_set(vargs) {
	for (var i = 0; i < arguments.length - 1; i += 2) {
		_tmpl_xl['c' + arguments[i]] = arguments[i+1];
	}
}

/*
 * Get translation stored with tmpl_xl_set for code, or code itself if none.
 */
function tmpl_xl(code) {
	return ((xl = _tmpl_xl['c' + code]) != undefined) ? xl : code;
}

/*
 * Get translation stored with tmpl_xl_set for code, and substitute any
 * "%s" placeholders in the translations with the other arguments.
 * Returns code if no translation found.
 */
function tmpl_ml_printf(code, variable_number_of_other_args) {
	var xl = _tmpl_xl['c' + code];
	if (xl == undefined) return code;
	var i = 1; var last_posn = 0; var s = "";
	var placeholder = "%s";
	while (true) {
		var placeholder_posn = xl.indexOf(placeholder, last_posn);
		if (placeholder_posn < 0) {
			s += xl.substr(last_posn);
			break;
		}
		s += xl.substr(last_posn, placeholder_posn - last_posn);
		if (i < arguments.length) {
			s += arguments[i];
		} else {
			s += placeholder;
		}
		last_posn = placeholder_posn + placeholder.length;
		i++;
	}
	return s;
}

/*
 * Escape a string so it can safely be used as a JS string literal.
 */ 
function tmpl_esc_js(s) {
	var p = '';
	for (var i = 0; i < s.length; i++) {
		var c = s.charAt(i);
		switch (c) {
			case '\\':
				p += '\\\\';
				break;
			case '\'':
				p += '\\\'';
				break;
			case '"':
				p += '\\"';
				break;
			case '\n':
				p += '\\n';
				break;
			case '\r':
				p += '\\r';
				break;
			default:
				p += c;
		}
	}
	return p;
}

/*
 * Escape a string so it can safely be used in HTML.
 */ 
function tmpl_esc_html(s) {
	var p = '';
	for (var i = 0; i < s.length; i++) {
		var c = s.charAt(i);
		switch (c) {
			case '<':
				p += '&lt;';
				break;
			case '>':
				p += '&gt;';
				break;
			case '&':
				p += '&amp;';
				break;
			case '"':
				p += '&quot;';
				break;
			case '\'':
				p += '&#39;';
				break;
			default:
				p += c;
		}
	}
	return p;
}
