[IMP] qweb-js: reimplement string interpolation compilation as a single pass
[odoo/odoo.git] / addons / web / static / lib / qweb / qweb2.js
1 /*
2 Copyright (c) 2013, Fabien Meghazi
3
4 Released under the MIT license
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy of
7 this software and associated documentation files (the "Software"), to deal in
8 the Software without restriction, including without limitation the rights to use,
9 copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
10 Software, and to permit persons to whom the Software is furnished to do so,
11 subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18 FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19 COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
20 AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 */
23
24 // TODO: trim support
25 // TODO: line number -> https://bugzilla.mozilla.org/show_bug.cgi?id=618650
26 // TODO: templates orverwritten could be called by t-call="__super__" ?
27 // TODO: t-set + t-value + children node == scoped variable ?
28 var QWeb2 = {
29     expressions_cache: {},
30     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(','),
31     ACTIONS_PRECEDENCE: 'foreach,if,call,set,esc,raw,js,debug,log'.split(','),
32     WORD_REPLACEMENT: {
33         'and': '&&',
34         'or': '||',
35         'gt': '>',
36         'gte': '>=',
37         'lt': '<',
38         'lte': '<='
39     },
40     VOID_ELEMENTS: 'area,base,br,col,embed,hr,img,input,keygen,link,menuitem,meta,param,source,track,wbr'.split(','),
41     tools: {
42         exception: function(message, context) {
43             context = context || {};
44             var prefix = 'QWeb2';
45             if (context.template) {
46                 prefix += " - template['" + context.template + "']";
47             }
48             throw new Error(prefix + ": " + message);
49         },
50         warning : function(message) {
51             if (typeof(window) !== 'undefined' && window.console) {
52                 window.console.warn(message);
53             }
54         },
55         trim: function(s, mode) {
56             switch (mode) {
57                 case "left":
58                     return s.replace(/^\s*/, "");
59                 case "right":
60                     return s.replace(/\s*$/, "");
61                 default:
62                     return s.replace(/^\s*|\s*$/g, "");
63             }
64         },
65         js_escape: function(s, noquotes) {
66             return (noquotes ? '' : "'") + s.replace(/\r?\n/g, "\\n").replace(/'/g, "\\'") + (noquotes ? '' : "'");
67         },
68         html_escape: function(s, attribute) {
69             if (s == null) {
70                 return '';
71             }
72             s = String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
73             if (attribute) {
74                 s = s.replace(/"/g, '&quot;');
75             }
76             return s;
77         },
78         gen_attribute: function(o) {
79             if (o !== null && o !== undefined) {
80                 if (o.constructor === Array) {
81                     if (o[1] !== null && o[1] !== undefined) {
82                         return this.format_attribute(o[0], o[1]);
83                     }
84                 } else if (typeof o === 'object') {
85                     var r = '';
86                     for (var k in o) {
87                         if (o.hasOwnProperty(k)) {
88                             r += this.gen_attribute([k, o[k]]);
89                         }
90                     }
91                     return r;
92                 }
93             }
94             return '';
95         },
96         format_attribute: function(name, value) {
97             return ' ' + name + '="' + this.html_escape(value, true) + '"';
98         },
99         extend: function(dst, src, exclude) {
100             for (var p in src) {
101                 if (src.hasOwnProperty(p) && !(exclude && this.arrayIndexOf(exclude, p) !== -1)) {
102                     dst[p] = src[p];
103                 }
104             }
105             return dst;
106         },
107         arrayIndexOf : function(array, item) {
108             for (var i = 0, ilen = array.length; i < ilen; i++) {
109                 if (array[i] === item) {
110                     return i;
111                 }
112             }
113             return -1;
114         },
115         xml_node_to_string : function(node, childs_only) {
116             if (childs_only) {
117                 var childs = node.childNodes, r = [];
118                 for (var i = 0, ilen = childs.length; i < ilen; i++) {
119                     r.push(this.xml_node_to_string(childs[i]));
120                 }
121                 return r.join('');
122             } else {
123                 if (typeof XMLSerializer !== 'undefined') {
124                     return (new XMLSerializer()).serializeToString(node);
125                 } else {
126                     switch(node.nodeType) {
127                     case 1: return node.outerHTML;
128                     case 3: return node.data;
129                     case 4: return '<![CDATA[' + node.data + ']]>';
130                     case 8: return '<!-- ' + node.data + '-->';
131                     }
132                     throw new Error('Unknown node type ' + node.nodeType);
133                 }
134             }
135         },
136         call: function(context, template, old_dict, _import, callback) {
137             var new_dict = this.extend({}, old_dict);
138             new_dict['__caller__'] = old_dict['__template__'];
139             if (callback) {
140                 new_dict['__content__'] = callback(context, new_dict);
141             }
142             var r = context.engine._render(template, new_dict);
143             if (_import) {
144                 if (_import === '*') {
145                     this.extend(old_dict, new_dict, ['__caller__', '__template__']);
146                 } else {
147                     _import = _import.split(',');
148                     for (var i = 0, ilen = _import.length; i < ilen; i++) {
149                         var v = _import[i];
150                         old_dict[v] = new_dict[v];
151                     }
152                 }
153             }
154             return r;
155         },
156         foreach: function(context, enu, as, old_dict, callback) {
157             if (enu != null) {
158                 var size, new_dict = this.extend({}, old_dict);
159                 new_dict[as + "_all"] = enu;
160                 var as_value = as + "_value",
161                     as_index = as + "_index",
162                     as_first = as + "_first",
163                     as_last = as + "_last",
164                     as_parity = as + "_parity";
165                 if (size = enu.length) {
166                     new_dict[as + "_size"] = size;
167                     for (var j = 0, jlen = enu.length; j < jlen; j++) {
168                         var cur = enu[j];
169                         new_dict[as_value] = cur;
170                         new_dict[as_index] = j;
171                         new_dict[as_first] = j === 0;
172                         new_dict[as_last] = j + 1 === size;
173                         new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even');
174                         if (cur.constructor === Object) {
175                             this.extend(new_dict, cur);
176                         }
177                         new_dict[as] = cur;
178                         callback(context, new_dict);
179                     }
180                 } else if (enu.constructor == Number) {
181                     var _enu = [];
182                     for (var i = 0; i < enu; i++) {
183                         _enu.push(i);
184                     }
185                     this.foreach(context, _enu, as, old_dict, callback);
186                 } else {
187                     var index = 0;
188                     for (var k in enu) {
189                         if (enu.hasOwnProperty(k)) {
190                             var v = enu[k];
191                             new_dict[as_value] = v;
192                             new_dict[as_index] = index;
193                             new_dict[as_first] = index === 0;
194                             new_dict[as_parity] = (j % 2 == 1 ? 'odd' : 'even');
195                             new_dict[as] = k;
196                             callback(context, new_dict);
197                             index += 1;
198                         }
199                       }
200                 }
201             } else {
202                 this.exception("No enumerator given to foreach", context);
203             }
204         }
205     }
206 };
207
208 QWeb2.Engine = (function() {
209     function Engine() {
210         // TODO: handle prefix at template level : t-prefix="x", don't forget to lowercase it
211         this.prefix = 't';
212         this.debug = false;
213         this.templates_resources = []; // TODO: implement this.reload()
214         this.templates = {};
215         this.compiled_templates = {};
216         this.extend_templates = {};
217         this.default_dict = {};
218         this.tools = QWeb2.tools;
219         this.jQuery = window.jQuery;
220         this.reserved_words = QWeb2.RESERVED_WORDS.slice(0);
221         this.actions_precedence = QWeb2.ACTIONS_PRECEDENCE.slice(0);
222         this.void_elements = QWeb2.VOID_ELEMENTS.slice(0);
223         this.word_replacement = QWeb2.tools.extend({}, QWeb2.WORD_REPLACEMENT);
224         this.preprocess_node = null;
225         for (var i = 0; i < arguments.length; i++) {
226             this.add_template(arguments[i]);
227         }
228     }
229
230     QWeb2.tools.extend(Engine.prototype, {
231         /**
232          * Add a template to the engine
233          *
234          * @param {String|Document} template Template as string or url or DOM Document
235          * @param {Function} [callback] Called when the template is loaded, force async request
236          */
237         add_template : function(template, callback) {
238             var self = this;
239             this.templates_resources.push(template);
240             if (template.constructor === String) {
241                 return this.load_xml(template, function (err, xDoc) {
242                     if (err) {
243                         if (callback) {
244                             return callback(err);
245                         } else {
246                             throw err;
247                         }
248                     }
249                     self.add_template(xDoc, callback);
250                 });
251             }
252             var ec = (template.documentElement && template.documentElement.childNodes) || template.childNodes || [];
253             for (var i = 0; i < ec.length; i++) {
254                 var node = ec[i];
255                 if (node.nodeType === 1) {
256                     if (node.nodeName == 'parsererror') {
257                         return this.tools.exception(node.innerText);
258                     }
259                     var name = node.getAttribute(this.prefix + '-name');
260                     var extend = node.getAttribute(this.prefix + '-extend');
261                     if (name && extend) {
262                         // Clone template and extend it
263                         if (!this.templates[extend]) {
264                             return this.tools.exception("Can't clone undefined template " + extend);
265                         }
266                         this.templates[name] = this.templates[extend].cloneNode(true);
267                         extend = name;
268                         name = undefined;
269                     }
270                     if (name) {
271                         this.templates[name] = node;
272                         this.compiled_templates[name] = null;
273                     } else if (extend) {
274                         delete(this.compiled_templates[extend]);
275                         if (this.extend_templates[extend]) {
276                             this.extend_templates[extend].push(node);
277                         } else {
278                             this.extend_templates[extend] = [node];
279                         }
280                     }
281                 }
282             }
283             if (callback) {
284                 callback(null, template);
285             }
286             return true;
287         },
288         load_xml : function(s, callback) {
289             var self = this;
290             var async = !!callback;
291             s = this.tools.trim(s);
292             if (s.charAt(0) === '<') {
293                 var tpl = this.load_xml_string(s);
294                 if (callback) {
295                     callback(null, tpl);
296                 }
297                 return tpl;
298             } else {
299                 var req = this.get_xhr();
300                 if (this.debug) {
301                     s += '?debug=' + (new Date()).getTime(); // TODO fme: do it properly in case there's already url parameters
302                 }
303                 req.open('GET', s, async);
304                 if (async) {
305                     req.onreadystatechange = function() {
306                         if (req.readyState == 4) {
307                             if (req.status == 200) {
308                                 callback(null, self._parse_from_request(req));
309                             } else {
310                                 callback(new Error("Can't load template, http status " + req.status));
311                             }
312                         }
313                     };
314                 }
315                 req.send(null);
316                 if (!async) {
317                     return this._parse_from_request(req);
318                 }
319             }
320         },
321         _parse_from_request: function(req) {
322             var xDoc = req.responseXML;
323             if (xDoc) {
324                 if (!xDoc.documentElement) {
325                     throw new Error("QWeb2: This xml document has no root document : " + xDoc.responseText);
326                 }
327                 if (xDoc.documentElement.nodeName == "parsererror") {
328                     throw new Error("QWeb2: Could not parse document :" + xDoc.documentElement.childNodes[0].nodeValue);
329                 }
330                 return xDoc;
331             } else {
332                 return this.load_xml_string(req.responseText);
333             }
334         },
335         load_xml_string : function(s) {
336             if (window.DOMParser) {
337                 var dp = new DOMParser();
338                 var r = dp.parseFromString(s, "text/xml");
339                 if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') {
340                     throw new Error("QWeb2: Could not parse document :" + r.body.innerText);
341                 }
342                 return r;
343             }
344             var xDoc;
345             try {
346                 xDoc = new ActiveXObject("MSXML2.DOMDocument");
347             } catch (e) {
348                 throw new Error("Could not find a DOM Parser: " + e.message);
349             }
350             xDoc.async = false;
351             xDoc.preserveWhiteSpace = true;
352             xDoc.loadXML(s);
353             return xDoc;
354         },
355         has_template : function(template) {
356             return !!this.templates[template];
357         },
358         get_xhr : function() {
359             if (window.XMLHttpRequest) {
360                 return new window.XMLHttpRequest();
361             }
362             try {
363                 return new ActiveXObject('MSXML2.XMLHTTP.3.0');
364             } catch (e) {
365                 throw new Error("Could not get XHR");
366             }
367         },
368         compile : function(node) {
369             var e = new QWeb2.Element(this, node);
370             var template = node.getAttribute(this.prefix + '-name');
371             return  "   /* 'this' refers to Qweb2.Engine instance */\n" +
372                     "   var context = { engine : this, template : " + (this.tools.js_escape(template)) + " };\n" +
373                     "   dict = dict || {};\n" +
374                     "   dict['__template__'] = '" + template + "';\n" +
375                     "   var r = [];\n" +
376                     "   /* START TEMPLATE */" +
377                     (this.debug ? "" : " try {\n") +
378                     (e.compile()) + "\n" +
379                     "   /* END OF TEMPLATE */" +
380                     (this.debug ? "" : " } catch(error) {\n" +
381                     "       if (console && console.exception) console.exception(error);\n" +
382                     "       context.engine.tools.exception('Runtime Error: ' + error, context);\n") +
383                     (this.debug ? "" : "   }\n") +
384                     "   return r.join('');";
385         },
386         render : function(template, dict) {
387             dict = dict || {};
388             QWeb2.tools.extend(dict, this.default_dict);
389             /*if (this.debug && window['console'] !== undefined) {
390                 console.time("QWeb render template " + template);
391             }*/
392             var r = this._render(template, dict);
393             /*if (this.debug && window['console'] !== undefined) {
394                 console.timeEnd("QWeb render template " + template);
395             }*/
396             return r;
397         },
398         _render : function(template, dict) {
399             if (this.compiled_templates[template]) {
400                 return this.compiled_templates[template].apply(this, [dict || {}]);
401             } else if (this.templates[template]) {
402                 var ext;
403                 if (ext = this.extend_templates[template]) {
404                     var extend_node;
405                     while (extend_node = ext.shift()) {
406                         this.extend(template, extend_node);
407                     }
408                 }
409                 var code = this.compile(this.templates[template]), tcompiled;
410                 try {
411                     tcompiled = new Function(['dict'], code);
412                 } catch (error) {
413                     if (this.debug && window.console) {
414                         console.log(code);
415                     }
416                     this.tools.exception("Error evaluating template: " + error, { template: name });
417                 }
418                 if (!tcompiled) {
419                     this.tools.exception("Error evaluating template: (IE?)" + error, { template: name });
420                 }
421                 this.compiled_templates[template] = tcompiled;
422                 return this.render(template, dict);
423             } else {
424                 return this.tools.exception("Template '" + template + "' not found");
425             }
426         },
427         extend : function(template, extend_node) {
428             if (!this.jQuery) {
429                 return this.tools.exception("Can't extend template " + template + " without jQuery");
430             }
431             var template_dest = this.templates[template];
432             for (var i = 0, ilen = extend_node.childNodes.length; i < ilen; i++) {
433                 var child = extend_node.childNodes[i];
434                 if (child.nodeType === 1) {
435                     var jquery = child.getAttribute(this.prefix + '-jquery'),
436                         operation = child.getAttribute(this.prefix + '-operation'),
437                         target,
438                         error_msg = "Error while extending template '" + template;
439                     if (jquery) {
440                         target = this.jQuery(jquery, template_dest);
441                     } else {
442                         this.tools.exception(error_msg + "No expression given");
443                     }
444                     error_msg += "' (expression='" + jquery + "') : ";
445                     if (operation) {
446                         var allowed_operations = "append,prepend,before,after,replace,inner".split(',');
447                         if (this.tools.arrayIndexOf(allowed_operations, operation) == -1) {
448                             this.tools.exception(error_msg + "Invalid operation : '" + operation + "'");
449                         }
450                         operation = {'replace' : 'replaceWith', 'inner' : 'html'}[operation] || operation;
451                         target[operation](child.cloneNode(true).childNodes);
452                     } else {
453                         try {
454                             var f = new Function(['$', 'document'], this.tools.xml_node_to_string(child, true));
455                         } catch(error) {
456                             return this.tools.exception("Parse " + error_msg + error);
457                         }
458                         try {
459                             f.apply(target, [this.jQuery, template_dest.ownerDocument]);
460                         } catch(error) {
461                             return this.tools.exception("Runtime " + error_msg + error);
462                         }
463                     }
464                 }
465             }
466         }
467     });
468     return Engine;
469 })();
470
471 QWeb2.Element = (function() {
472     function Element(engine, node) {
473         this.engine = engine;
474         this.node = node;
475         this.tag = node.tagName;
476         this.actions = {};
477         this.actions_done = [];
478         this.attributes = {};
479         this.children = [];
480         this._top = [];
481         this._bottom = [];
482         this._indent = 1;
483         this.process_children = true;
484         this.is_void_element = ~QWeb2.tools.arrayIndexOf(this.engine.void_elements, this.tag);
485         var childs = this.node.childNodes;
486         if (childs) {
487             for (var i = 0, ilen = childs.length; i < ilen; i++) {
488                 this.children.push(new QWeb2.Element(this.engine, childs[i]));
489             }
490         }
491         var attrs = this.node.attributes;
492         if (attrs) {
493             for (var j = 0, jlen = attrs.length; j < jlen; j++) {
494                 var attr = attrs[j];
495                 var name = attr.name;
496                 var m = name.match(new RegExp("^" + this.engine.prefix + "-(.+)"));
497                 if (m) {
498                     name = m[1];
499                     if (name === 'name') {
500                         continue;
501                     }
502                     this.actions[name] = attr.value;
503                 } else {
504                     this.attributes[name] = attr.value;
505                 }
506             }
507         }
508         if (this.engine.preprocess_node) {
509             this.engine.preprocess_node.call(this);
510         }
511     }
512
513     QWeb2.tools.extend(Element.prototype, {
514         compile : function() {
515             var r = [],
516                 instring = false,
517                 lines = this._compile().split('\n');
518             for (var i = 0, ilen = lines.length; i < ilen; i++) {
519                 var m, line = lines[i];
520                 if (m = line.match(/^(\s*)\/\/@string=(.*)/)) {
521                     if (instring) {
522                         if (this.engine.debug) {
523                             // Split string lines in indented r.push arguments
524                             r.push((m[2].indexOf("\\n") != -1 ? "',\n\t" + m[1] + "'" : '') + m[2]);
525                         } else {
526                             r.push(m[2]);
527                         }
528                     } else {
529                         r.push(m[1] + "r.push('" + m[2]);
530                         instring = true;
531                     }
532                 } else {
533                     if (instring) {
534                         r.push("');\n");
535                     }
536                     instring = false;
537                     r.push(line + '\n');
538                 }
539             }
540             return r.join('');
541         },
542         _compile : function() {
543             switch (this.node.nodeType) {
544                 case 3:
545                 case 4:
546                     this.top_string(this.node.data);
547                 break;
548                 case 1:
549                     this.compile_element();
550             }
551             var r = this._top.join('');
552             if (this.process_children) {
553                 for (var i = 0, ilen = this.children.length; i < ilen; i++) {
554                     var child = this.children[i];
555                     child._indent = this._indent;
556                     r += child._compile();
557                 }
558             }
559             r += this._bottom.join('');
560             return r;
561         },
562         format_expression : function(e) {
563             /* Naive format expression builder. Replace reserved words and variables to dict[variable]
564              * Does not handle spaces before dot yet, and causes problems for anonymous functions. Use t-js="" for that */
565             if (QWeb2.expressions_cache[e]) {
566               return QWeb2.expressions_cache[e];
567             }
568             var chars = e.split(''),
569                 instring = '',
570                 invar = '',
571                 invar_pos = 0,
572                 r = '';
573             chars.push(' ');
574             for (var i = 0, ilen = chars.length; i < ilen; i++) {
575                 var c = chars[i];
576                 if (instring.length) {
577                     if (c === instring && chars[i - 1] !== "\\") {
578                         instring = '';
579                     }
580                 } else if (c === '"' || c === "'") {
581                     instring = c;
582                 } else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
583                     invar = c;
584                     invar_pos = i;
585                     continue;
586                 } else if (c.match(/\W/) && invar.length) {
587                     // TODO: Should check for possible spaces before dot
588                     if (chars[invar_pos - 1] !== '.' && QWeb2.tools.arrayIndexOf(this.engine.reserved_words, invar) < 0) {
589                         invar = this.engine.word_replacement[invar] || ("dict['" + invar + "']");
590                     }
591                     r += invar;
592                     invar = '';
593                 } else if (invar.length) {
594                     invar += c;
595                     continue;
596                 }
597                 r += c;
598             }
599             r = r.slice(0, -1);
600             QWeb2.expressions_cache[e] = r;
601             return r;
602         },
603         string_interpolation : function(s) {
604             var _this = this;
605             if (!s) {
606               return "''";
607             }
608             function append_literal(s) {
609                 s && r.push(_this.engine.tools.js_escape(s));
610             }
611
612             var re = /#{(.*?)}/g, start = 0, r = [], m;
613             while (m = re.exec(s)) {
614                 // extract literal string between previous and current match
615                 append_literal(s.slice(start, re.lastIndex - m[0].length));
616                 // extract matched expression
617                 r.push('(' + this.format_expression(m[1]) + ')');
618                 // update position of new matching
619                 start = re.lastIndex;
620             }
621             // remaining text after last expression
622             append_literal(s.slice(start));
623
624             return r.join(' + ');
625         },
626         indent : function() {
627             return this._indent++;
628         },
629         dedent : function() {
630             if (this._indent !== 0) {
631                 return this._indent--;
632             }
633         },
634         get_indent : function() {
635             return new Array(this._indent + 1).join("\t");
636         },
637         top : function(s) {
638             return this._top.push(this.get_indent() + s + '\n');
639         },
640         top_string : function(s) {
641             return this._top.push(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n');
642         },
643         bottom : function(s) {
644             return this._bottom.unshift(this.get_indent() + s + '\n');
645         },
646         bottom_string : function(s) {
647             return this._bottom.unshift(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n');
648         },
649         compile_element : function() {
650             for (var i = 0, ilen = this.engine.actions_precedence.length; i < ilen; i++) {
651                 var a = this.engine.actions_precedence[i];
652                 if (a in this.actions) {
653                     var value = this.actions[a];
654                     var key = 'compile_action_' + a;
655                     if (this[key]) {
656                         this[key](value);
657                     } else if (this.engine[key]) {
658                         this.engine[key].call(this, value);
659                     } else {
660                         this.engine.tools.exception("No handler method for action '" + a + "'");
661                     }
662                 }
663             }
664             if (this.tag.toLowerCase() !== this.engine.prefix) {
665                 var tag = "<" + this.tag;
666                 for (var a in this.attributes) {
667                     tag += this.engine.tools.gen_attribute([a, this.attributes[a]]);
668                 }
669                 this.top_string(tag);
670                 if (this.actions.att) {
671                     this.top("r.push(context.engine.tools.gen_attribute(" + (this.format_expression(this.actions.att)) + "));");
672                 }
673                 for (var a in this.actions) {
674                     var v = this.actions[a];
675                     var m = a.match(/att-(.+)/);
676                     if (m) {
677                         this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.format_expression(v)) + ")]));");
678                     }
679                     var m = a.match(/attf-(.+)/);
680                     if (m) {
681                         this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.string_interpolation(v)) + ")]));");
682                     }
683                 }
684                 if (this.actions.opentag === 'true' || (!this.children.length && this.is_void_element)) {
685                     // We do not enforce empty content on void elements
686                     // because QWeb rendering is not necessarily html.
687                     this.top_string("/>");
688                 } else {
689                     this.top_string(">");
690                     this.bottom_string("</" + this.tag + ">");
691                 }
692             }
693         },
694         compile_action_if : function(value) {
695             this.top("if (" + (this.format_expression(value)) + ") {");
696             this.bottom("}");
697             this.indent();
698         },
699         compile_action_foreach : function(value) {
700             var as = this.actions['as'] || value.replace(/[^a-zA-Z0-9]/g, '_');
701             //TODO: exception if t-as not valid
702             this.top("context.engine.tools.foreach(context, " + (this.format_expression(value)) + ", " + (this.engine.tools.js_escape(as)) + ", dict, function(context, dict) {");
703             this.bottom("});");
704             this.indent();
705         },
706         compile_action_call : function(value) {
707             var _import = this.actions['import'] || '';
708             if (this.children.length === 0) {
709                 return this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, " + (this.engine.tools.js_escape(_import)) + "));");
710             } else {
711                 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) {");
712                 this.bottom("}));");
713                 this.indent();
714                 this.top("var r = [];");
715                 return this.bottom("return r.join('');");
716             }
717         },
718         compile_action_set : function(value) {
719             var variable = this.format_expression(value);
720             if (this.actions['value']) {
721                 if (this.children.length) {
722                     this.engine.tools.warning("@set with @value plus node chidren found. Children are ignored.");
723                 }
724                 this.top(variable + " = (" + (this.format_expression(this.actions['value'])) + ");");
725                 this.process_children = false;
726             } else {
727                 if (this.children.length === 0) {
728                     this.top(variable + " = '';");
729                 } else if (this.children.length === 1 && this.children[0].node.nodeType === 3) {
730                     this.top(variable + " = " + (this.engine.tools.js_escape(this.children[0].node.data)) + ";");
731                     this.process_children = false;
732                 } else {
733                     this.top(variable + " = (function(dict) {");
734                     this.bottom("})(dict);");
735                     this.indent();
736                     this.top("var r = [];");
737                     this.bottom("return r.join('');");
738                 }
739             }
740         },
741         compile_action_esc : function(value) {
742             this.top("r.push(context.engine.tools.html_escape(" + (this.format_expression(value)) + "));");
743         },
744         compile_action_raw : function(value) {
745             this.top("r.push(" + (this.format_expression(value)) + ");");
746         },
747         compile_action_js : function(value) {
748             this.top("(function(" + value + ") {");
749             this.bottom("})(dict);");
750             this.indent();
751             var lines = this.engine.tools.xml_node_to_string(this.node, true).split(/\r?\n/);
752             for (var i = 0, ilen = lines.length; i < ilen; i++) {
753                 this.top(lines[i]);
754             }
755             this.process_children = false;
756         },
757         compile_action_debug : function(value) {
758             this.top("debugger;");
759         },
760         compile_action_log : function(value) {
761             this.top("console.log(" + this.format_expression(value) + ");");
762         }
763     });
764     return Element;
765 })();