// TODO: trim support // TODO: line number -> https://bugzilla.mozilla.org/show_bug.cgi?id=618650 // TODO: templates orverwritten could be called by t-call="__super__" ? // TODO: t-set + t-value + children node == scoped variable ? var QWeb2 = { expressions_cache: {}, RESERVED_WORDS: 'true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date'.split(','), ACTIONS_PRECEDENCE: 'foreach,if,call,set,esc,escf,raw,rawf,js,debug,log'.split(','), WORD_REPLACEMENT: { 'and': '&&', 'or': '||', 'gt': '>', 'gte': '>=', 'lt': '<', 'lte': '<=' }, tools: { exception: function(message, context) { context = context || {}; var prefix = 'QWeb2'; if (context.template) { prefix += " - template['" + context.template + "']"; } throw new Error(prefix + ": " + message); }, warning : function(message) { if (typeof(window) !== 'undefined' && window.console) { window.console.warn(message); } }, trim: function(s, mode) { switch (mode) { case "left": return s.replace(/^\s*/, ""); case "right": return s.replace(/\s*$/, ""); default: return s.replace(/^\s*|\s*$/g, ""); } }, js_escape: function(s, noquotes) { return (noquotes ? '' : "'") + s.replace(/\r?\n/g, "\\n").replace(/'/g, "\\'") + (noquotes ? '' : "'"); }, html_escape: function(s, attribute) { if (s == null) { return ''; } s = String(s).replace(/&/g, '&').replace(//g, '>'); if (attribute) { s = s.replace(/"/g, '"'); } return s; }, gen_attribute: function(o) { if (o !== null && o !== undefined) { if (o.constructor === Array) { if (o[1] !== null && o[1] !== undefined) { return this.format_attribute(o[0], o[1]); } } else if (typeof o === 'object') { var r = ''; for (var k in o) { if (o.hasOwnProperty(k)) { r += this.gen_attribute([k, o[k]]); } } return r; } } return ''; }, format_attribute: function(name, value) { return ' ' + name + '="' + this.html_escape(value, true) + '"'; }, extend: function(dst, src, exclude) { for (var p in src) { if (src.hasOwnProperty(p) && !(exclude && this.arrayIndexOf(exclude, p) !== -1)) { dst[p] = src[p]; } } return dst; }, arrayIndexOf : function(array, item) { for (var i = 0, ilen = array.length; i < ilen; i++) { if (array[i] === item) { return i; } } return -1; }, xml_node_to_string : function(node, childs_only) { if (childs_only) { var childs = node.childNodes, r = []; for (var i = 0, ilen = childs.length; i < ilen; i++) { r.push(this.xml_node_to_string(childs[i])); } return r.join(''); } else { if (typeof XMLSerializer !== 'undefined') { return (new XMLSerializer()).serializeToString(node); } else { switch(node.nodeType) { case 1: return node.outerHTML; case 3: return node.data; case 4: return ''; case 8: return ''; } throw new Error('Unknown node type ' + node.nodeType); } } }, call: function(context, template, old_dict, _import, callback) { var new_dict = this.extend({}, old_dict); new_dict['__caller__'] = old_dict['__template__']; if (callback) { new_dict['__content__'] = callback(context, new_dict); } var r = context.engine._render(template, new_dict); if (_import) { if (_import === '*') { this.extend(old_dict, new_dict, ['__caller__', '__template__']); } else { _import = _import.split(','); for (var i = 0, ilen = _import.length; i < ilen; i++) { var v = _import[i]; old_dict[v] = new_dict[v]; } } } return r; }, foreach: function(context, enu, as, old_dict, callback) { if (enu != null) { var size, new_dict = this.extend({}, old_dict); new_dict[as + "_all"] = enu; var as_value = as + "_value", as_index = as + "_index", as_first = as + "_first", as_last = as + "_last", as_parity = as + "_parity"; if (size = enu.length) { new_dict[as + "_size"] = size; for (var j = 0, jlen = enu.length; j < jlen; j++) { var cur = enu[j]; new_dict[as_value] = cur; new_dict[as_index] = j; new_dict[as_first] = j === 0; new_dict[as_last] = j + 1 === size; new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even'); if (cur.constructor === Object) { this.extend(new_dict, cur); } new_dict[as] = cur; callback(context, new_dict); } } else if (enu.constructor == Number) { var _enu = []; for (var i = 0; i < enu; i++) { _enu.push(i); } this.foreach(context, _enu, as, old_dict, callback); } else { var index = 0; for (var k in enu) { if (enu.hasOwnProperty(k)) { var v = enu[k]; new_dict[as_value] = v; new_dict[as_index] = index; new_dict[as_first] = index === 0; new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even'); new_dict[as] = k; callback(context, new_dict); index += 1; } } } } else { this.exception("No enumerator given to foreach", context); } } } }; QWeb2.Engine = (function() { function Engine() { // TODO: handle prefix at template level : t-prefix="x", don't forget to lowercase it this.prefix = 't'; this.debug = false; this.templates_resources = []; // TODO: implement this.reload() this.templates = {}; this.compiled_templates = {}; this.extend_templates = {}; this.default_dict = {}; this.tools = QWeb2.tools; this.jQuery = window.jQuery; this.reserved_words = QWeb2.RESERVED_WORDS.slice(0); this.actions_precedence = QWeb2.ACTIONS_PRECEDENCE.slice(0); this.word_replacement = QWeb2.tools.extend({}, QWeb2.WORD_REPLACEMENT); this.preprocess_node = null; for (var i = 0; i < arguments.length; i++) { this.add_template(arguments[i]); } } QWeb2.tools.extend(Engine.prototype, { add_template : function(template) { this.templates_resources.push(template); if (template.constructor === String) { template = this.load_xml(template); } var ec = (template.documentElement && template.documentElement.childNodes) || template.childNodes || []; for (var i = 0; i < ec.length; i++) { var node = ec[i]; if (node.nodeType === 1) { if (node.nodeName == 'parsererror') { return this.tools.exception(node.innerText); } var name = node.getAttribute(this.prefix + '-name'); var extend = node.getAttribute(this.prefix + '-extend'); if (name && extend) { // Clone template and extend it if (!this.templates[extend]) { return this.tools.exception("Can't clone undefined template " + extend); } this.templates[name] = this.templates[extend].cloneNode(true); extend = name; name = undefined; } if (name) { this.templates[name] = node; } else if (extend) { delete(this.compiled_templates[extend]); if (this.extend_templates[extend]) { this.extend_templates[extend].push(node); } else { this.extend_templates[extend] = [node]; } } } } return true; }, load_xml : function(s) { s = this.tools.trim(s); if (s.charAt(0) === '<') { return this.load_xml_string(s); } else { var req = this.get_xhr(); if (req) { // TODO: third parameter is async : https://developer.mozilla.org/en/XMLHttpRequest#open() // do an on_ready in QWeb2{} that could be passed to add_template if (this.debug) { s += '?debug=' + (new Date()).getTime(); // TODO fme: do it properly in case there's already url parameters } req.open('GET', s, false); req.send(null); var xDoc = req.responseXML; if (xDoc) { if (!xDoc.documentElement) { throw new Error("QWeb2: This xml document has no root document : " + xDoc.responseText); } if (xDoc.documentElement.nodeName == "parsererror") { return this.tools.exception(xDoc.documentElement.childNodes[0].nodeValue); } return xDoc; } else { return this.load_xml_string(req.responseText); } } } }, load_xml_string : function(s) { if (window.DOMParser) { var dp = new DOMParser(); var r = dp.parseFromString(s, "text/xml"); if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') { return this.tools.exception(r.body.innerText); } return r; } var xDoc; try { // new ActiveXObject("Msxml2.DOMDocument.4.0"); xDoc = new ActiveXObject("MSXML2.DOMDocument"); } catch (e) { return this.tools.exception( "Could not find a DOM Parser: " + e.message); } xDoc.async = false; xDoc.preserveWhiteSpace = true; xDoc.loadXML(s); return xDoc; }, has_template : function(template) { return !!this.templates[template]; }, get_xhr : function() { if (window.XMLHttpRequest) { return new window.XMLHttpRequest(); } try { return new ActiveXObject('MSXML2.XMLHTTP.3.0'); } catch (e) { return null; } }, compile : function(node) { var e = new QWeb2.Element(this, node); var template = node.getAttribute(this.prefix + '-name'); return " /* 'this' refers to Qweb2.Engine instance */\n" + " var context = { engine : this, template : " + (this.tools.js_escape(template)) + " };\n" + " dict = dict || {};\n" + " dict['__template__'] = '" + template + "';\n" + " var r = [];\n" + " /* START TEMPLATE */" + (this.debug ? "" : " try {\n") + (e.compile()) + "\n" + " /* END OF TEMPLATE */" + (this.debug ? "" : " } catch(error) {\n" + " if (console && console.exception) console.exception(error);\n" + " context.engine.tools.exception('Runtime Error: ' + error, context);\n") + (this.debug ? "" : " }\n") + " return r.join('');"; }, render : function(template, dict) { dict = dict || {}; QWeb2.tools.extend(dict, this.default_dict); /*if (this.debug && window['console'] !== undefined) { console.time("QWeb render template " + template); }*/ var r = this._render(template, dict); /*if (this.debug && window['console'] !== undefined) { console.timeEnd("QWeb render template " + template); }*/ return r; }, _render : function(template, dict) { if (this.compiled_templates[template]) { return this.compiled_templates[template].apply(this, [dict || {}]); } else if (this.templates[template]) { var ext; if (ext = this.extend_templates[template]) { var extend_node; while (extend_node = ext.shift()) { this.extend(template, extend_node); } } var code = this.compile(this.templates[template]), tcompiled; try { tcompiled = new Function(['dict'], code); } catch (error) { if (this.debug && window.console) { console.log(code); } this.tools.exception("Error evaluating template: " + error, { template: name }); } if (!tcompiled) { this.tools.exception("Error evaluating template: (IE?)" + error, { template: name }); } this.compiled_templates[template] = tcompiled; return this.render(template, dict); } else { return this.tools.exception("Template '" + template + "' not found"); } }, extend : function(template, extend_node) { if (!this.jQuery) { return this.tools.exception("Can't extend template " + template + " without jQuery"); } var template_dest = this.templates[template]; for (var i = 0, ilen = extend_node.childNodes.length; i < ilen; i++) { var child = extend_node.childNodes[i]; if (child.nodeType === 1) { var jquery = child.getAttribute(this.prefix + '-jquery'), operation = child.getAttribute(this.prefix + '-operation'), target, error_msg = "Error while extending template '" + template; if (jquery) { target = this.jQuery(jquery, template_dest); } else { this.tools.exception(error_msg + "No expression given"); } error_msg += "' (expression='" + jquery + "') : "; if (operation) { var allowed_operations = "append,prepend,before,after,replace,inner".split(','); if (this.tools.arrayIndexOf(allowed_operations, operation) == -1) { this.tools.exception(error_msg + "Invalid operation : '" + operation + "'"); } operation = {'replace' : 'replaceWith', 'inner' : 'html'}[operation] || operation; target[operation](child.cloneNode(true).childNodes); } else { try { var f = new Function(['$', 'document'], this.tools.xml_node_to_string(child, true)); } catch(error) { return this.tools.exception("Parse " + error_msg + error); } try { f.apply(target, [this.jQuery, template_dest.ownerDocument]); } catch(error) { return this.tools.exception("Runtime " + error_msg + error); } } } } } }); return Engine; })(); QWeb2.Element = (function() { function Element(engine, node) { this.engine = engine; this.node = node; this.tag = node.tagName; this.actions = {}; this.actions_done = []; this.attributes = {}; this.children = []; this._top = []; this._bottom = []; this._indent = 1; this.process_children = true; var childs = this.node.childNodes; if (childs) { for (var i = 0, ilen = childs.length; i < ilen; i++) { this.children.push(new QWeb2.Element(this.engine, childs[i])); } } var attrs = this.node.attributes; if (attrs) { for (var j = 0, jlen = attrs.length; j < jlen; j++) { var attr = attrs[j]; var name = attr.name; var m = name.match(new RegExp("^" + this.engine.prefix + "-(.+)")); if (m) { name = m[1]; if (name === 'name') { continue; } this.actions[name] = attr.value; } else { this.attributes[name] = attr.value; } } } if (this.engine.preprocess_node) { this.engine.preprocess_node.call(this); } } QWeb2.tools.extend(Element.prototype, { compile : function() { var r = [], instring = false, lines = this._compile().split('\n'); for (var i = 0, ilen = lines.length; i < ilen; i++) { var m, line = lines[i]; if (m = line.match(/^(\s*)\/\/@string=(.*)/)) { if (instring) { if (this.engine.debug) { // Split string lines in indented r.push arguments r.push((m[2].indexOf("\\n") != -1 ? "',\n\t" + m[1] + "'" : '') + m[2]); } else { r.push(m[2]); } } else { r.push(m[1] + "r.push('" + m[2]); instring = true; } } else { if (instring) { r.push("');\n"); } instring = false; r.push(line + '\n'); } } return r.join(''); }, _compile : function() { switch (this.node.nodeType) { case 3: case 4: this.top_string(this.node.data); break; case 1: this.compile_element(); } var r = this._top.join(''); if (this.process_children) { for (var i = 0, ilen = this.children.length; i < ilen; i++) { var child = this.children[i]; child._indent = this._indent; r += child._compile(); } } r += this._bottom.join(''); return r; }, format_expression : function(e) { /* Naive format expression builder. Replace reserved words and variables to dict[variable] * Does not handle spaces before dot yet, and causes problems for anonymous functions. Use t-js="" for that */ if (QWeb2.expressions_cache[e]) { return QWeb2.expressions_cache[e]; } var chars = e.split(''), instring = '', invar = '', invar_pos = 0, r = ''; chars.push(' '); for (var i = 0, ilen = chars.length; i < ilen; i++) { var c = chars[i]; if (instring.length) { if (c === instring && chars[i - 1] !== "\\") { instring = ''; } } else if (c === '"' || c === "'") { instring = c; } else if (c.match(/[a-zA-Z_\$]/) && !invar.length) { invar = c; invar_pos = i; continue; } else if (c.match(/\W/) && invar.length) { // TODO: Should check for possible spaces before dot if (chars[invar_pos - 1] !== '.' && QWeb2.tools.arrayIndexOf(this.engine.reserved_words, invar) < 0) { invar = this.engine.word_replacement[invar] || ("dict['" + invar + "']"); } r += invar; invar = ''; } else if (invar.length) { invar += c; continue; } r += c; } r = r.slice(0, -1); QWeb2.expressions_cache[e] = r; return r; }, string_interpolation : function(s) { if (!s) { return "''"; } var regex = /^{(.*)}(.*)/, src = s.split(/#/), r = []; for (var i = 0, ilen = src.length; i < ilen; i++) { var val = src[i], m = val.match(regex); if (m) { r.push("(" + this.format_expression(m[1]) + ")"); if (m[2]) { r.push(this.engine.tools.js_escape(m[2])); } } else if (!(i === 0 && val === '')) { r.push(this.engine.tools.js_escape((i === 0 ? '' : '#') + val)); } } return r.join(' + '); }, indent : function() { return this._indent++; }, dedent : function() { if (this._indent !== 0) { return this._indent--; } }, get_indent : function() { return new Array(this._indent + 1).join("\t"); }, top : function(s) { return this._top.push(this.get_indent() + s + '\n'); }, top_string : function(s) { return this._top.push(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n'); }, bottom : function(s) { return this._bottom.unshift(this.get_indent() + s + '\n'); }, bottom_string : function(s) { return this._bottom.unshift(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n'); }, compile_element : function() { for (var i = 0, ilen = this.engine.actions_precedence.length; i < ilen; i++) { var a = this.engine.actions_precedence[i]; if (a in this.actions) { var value = this.actions[a]; var key = 'compile_action_' + a; if (this[key]) { this[key](value); } else if (this.engine[key]) { this.engine[key].call(this, value); } else { this.engine.tools.exception("No handler method for action '" + a + "'"); } } } if (this.tag.toLowerCase() !== this.engine.prefix) { var tag = "<" + this.tag; for (var a in this.attributes) { tag += this.engine.tools.gen_attribute([a, this.attributes[a]]); } this.top_string(tag); if (this.actions.att) { this.top("r.push(context.engine.tools.gen_attribute(" + (this.format_expression(this.actions.att)) + "));"); } for (var a in this.actions) { var v = this.actions[a]; var m = a.match(/att-(.+)/); if (m) { this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.format_expression(v)) + ")]));"); } var m = a.match(/attf-(.+)/); if (m) { this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.string_interpolation(v)) + ")]));"); } } if (this.children.length || this.actions.opentag === 'true') { this.top_string(">"); this.bottom_string(""); } else { this.top_string("/>"); } } }, compile_action_if : function(value) { this.top("if (" + (this.format_expression(value)) + ") {"); this.bottom("}"); this.indent(); }, compile_action_foreach : function(value) { var as = this.actions['as'] || value.replace(/[^a-zA-Z0-9]/g, '_'); //TODO: exception if t-as not valid this.top("context.engine.tools.foreach(context, " + (this.format_expression(value)) + ", " + (this.engine.tools.js_escape(as)) + ", dict, function(context, dict) {"); this.bottom("});"); this.indent(); }, compile_action_call : function(value) { var _import = this.actions['import'] || ''; if (this.children.length === 0) { return this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + "));"); } else { this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + ", function(context, dict) {"); this.bottom("}));"); this.indent(); this.top("var r = [];"); return this.bottom("return r.join('');"); } }, compile_action_set : function(value) { var variable = this.format_expression(value); if (this.actions['value']) { if (this.children.length) { this.engine.tools.warning("@set with @value plus node chidren found. Children are ignored."); } this.top(variable + " = (" + (this.format_expression(this.actions['value'])) + ");"); this.process_children = false; } else { if (this.children.length === 0) { this.top(variable + " = '';"); } else if (this.children.length === 1 && this.children[0].node.nodeType === 3) { this.top(variable + " = " + (this.engine.tools.js_escape(this.children[0].node.data)) + ";"); this.process_children = false; } else { this.top(variable + " = (function(dict) {"); this.bottom("})(dict);"); this.indent(); this.top("var r = [];"); this.bottom("return r.join('');"); } } }, compile_action_esc : function(value) { this.top("r.push(context.engine.tools.html_escape(" + (this.format_expression(value)) + "));"); }, compile_action_escf : function(value) { this.top("r.push(context.engine.tools.html_escape(" + (this.string_interpolation(value)) + "));"); }, compile_action_raw : function(value) { this.top("r.push(" + (this.format_expression(value)) + ");"); }, compile_action_rawf : function(value) { this.top("r.push(" + (this.string_interpolation(value)) + ");"); }, compile_action_js : function(value) { this.top("(function(" + value + ") {"); this.bottom("})(dict);"); this.indent(); var lines = this.engine.tools.xml_node_to_string(this.node, true).split(/\r?\n/); for (var i = 0, ilen = lines.length; i < ilen; i++) { this.top(lines[i]); } this.process_children = false; }, compile_action_debug : function(value) { this.top("debugger;"); }, compile_action_log : function(value) { this.top("console.log(" + this.format_expression(value) + ");"); } }); return Element; })();