[ADD] QWeb2: add support for operation='attributes' in a t-extend
[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[0] = callback(context, new_dict);
141             }
142             return context.engine._render(template, new_dict);
143         },
144         foreach: function(context, enu, as, old_dict, callback) {
145             if (enu != null) {
146                 var index, jlen, cur;
147                 var size, new_dict = this.extend({}, old_dict);
148                 new_dict[as + "_all"] = enu;
149                 var as_value = as + "_value",
150                     as_index = as + "_index",
151                     as_first = as + "_first",
152                     as_last = as + "_last",
153                     as_parity = as + "_parity";
154                 if (size = enu.length) {
155                     new_dict[as + "_size"] = size;
156                     for (index = 0, jlen = enu.length; index < jlen; index++) {
157                         cur = enu[index];
158                         new_dict[as_value] = cur;
159                         new_dict[as_index] = index;
160                         new_dict[as_first] = index === 0;
161                         new_dict[as_last] = index + 1 === size;
162                         new_dict[as_parity] = (index % 2 == 1 ? 'odd' : 'even');
163                         if (cur.constructor === Object) {
164                             this.extend(new_dict, cur);
165                         }
166                         new_dict[as] = cur;
167                         callback(context, new_dict);
168                     }
169                 } else if (enu.constructor == Number) {
170                     var _enu = [];
171                     for (var i = 0; i < enu; i++) {
172                         _enu.push(i);
173                     }
174                     this.foreach(context, _enu, as, old_dict, callback);
175                 } else {
176                     index = 0;
177                     for (var k in enu) {
178                         if (enu.hasOwnProperty(k)) {
179                             cur = enu[k];
180                             new_dict[as_value] = cur;
181                             new_dict[as_index] = index;
182                             new_dict[as_first] = index === 0;
183                             new_dict[as_parity] = (index % 2 == 1 ? 'odd' : 'even');
184                             new_dict[as] = k;
185                             callback(context, new_dict);
186                             index += 1;
187                         }
188                       }
189                 }
190             } else {
191                 this.exception("No enumerator given to foreach", context);
192             }
193         }
194     }
195 };
196
197 QWeb2.Engine = (function() {
198     function Engine() {
199         // TODO: handle prefix at template level : t-prefix="x", don't forget to lowercase it
200         this.prefix = 't';
201         this.debug = false;
202         this.templates_resources = []; // TODO: implement this.reload()
203         this.templates = {};
204         this.compiled_templates = {};
205         this.extend_templates = {};
206         this.default_dict = {};
207         this.tools = QWeb2.tools;
208         this.jQuery = window.jQuery;
209         this.reserved_words = QWeb2.RESERVED_WORDS.slice(0);
210         this.actions_precedence = QWeb2.ACTIONS_PRECEDENCE.slice(0);
211         this.void_elements = QWeb2.VOID_ELEMENTS.slice(0);
212         this.word_replacement = QWeb2.tools.extend({}, QWeb2.WORD_REPLACEMENT);
213         this.preprocess_node = null;
214         for (var i = 0; i < arguments.length; i++) {
215             this.add_template(arguments[i]);
216         }
217     }
218
219     QWeb2.tools.extend(Engine.prototype, {
220         /**
221          * Add a template to the engine
222          *
223          * @param {String|Document} template Template as string or url or DOM Document
224          * @param {Function} [callback] Called when the template is loaded, force async request
225          */
226         add_template : function(template, callback) {
227             var self = this;
228             this.templates_resources.push(template);
229             if (template.constructor === String) {
230                 return this.load_xml(template, function (err, xDoc) {
231                     if (err) {
232                         if (callback) {
233                             return callback(err);
234                         } else {
235                             throw err;
236                         }
237                     }
238                     self.add_template(xDoc, callback);
239                 });
240             }
241             var ec = (template.documentElement && template.documentElement.childNodes) || template.childNodes || [];
242             for (var i = 0; i < ec.length; i++) {
243                 var node = ec[i];
244                 if (node.nodeType === 1) {
245                     if (node.nodeName == 'parsererror') {
246                         return this.tools.exception(node.innerText);
247                     }
248                     var name = node.getAttribute(this.prefix + '-name');
249                     var extend = node.getAttribute(this.prefix + '-extend');
250                     if (name && extend) {
251                         // Clone template and extend it
252                         if (!this.templates[extend]) {
253                             return this.tools.exception("Can't clone undefined template " + extend);
254                         }
255                         this.templates[name] = this.templates[extend].cloneNode(true);
256                         extend = name;
257                         name = undefined;
258                     }
259                     if (name) {
260                         this.templates[name] = node;
261                         this.compiled_templates[name] = null;
262                     } else if (extend) {
263                         delete(this.compiled_templates[extend]);
264                         if (this.extend_templates[extend]) {
265                             this.extend_templates[extend].push(node);
266                         } else {
267                             this.extend_templates[extend] = [node];
268                         }
269                     }
270                 }
271             }
272             if (callback) {
273                 callback(null, template);
274             }
275             return true;
276         },
277         load_xml : function(s, callback) {
278             var self = this;
279             var async = !!callback;
280             s = this.tools.trim(s);
281             if (s.charAt(0) === '<') {
282                 var tpl = this.load_xml_string(s);
283                 if (callback) {
284                     callback(null, tpl);
285                 }
286                 return tpl;
287             } else {
288                 var req = this.get_xhr();
289                 if (this.debug) {
290                     s += '?debug=' + (new Date()).getTime(); // TODO fme: do it properly in case there's already url parameters
291                 }
292                 req.open('GET', s, async);
293                 if (async) {
294                     req.onreadystatechange = function() {
295                         if (req.readyState == 4) {
296                             if (req.status == 200) {
297                                 callback(null, self._parse_from_request(req));
298                             } else {
299                                 callback(new Error("Can't load template, http status " + req.status));
300                             }
301                         }
302                     };
303                 }
304                 req.send(null);
305                 if (!async) {
306                     return this._parse_from_request(req);
307                 }
308             }
309         },
310         _parse_from_request: function(req) {
311             var xDoc = req.responseXML;
312             if (xDoc) {
313                 if (!xDoc.documentElement) {
314                     throw new Error("QWeb2: This xml document has no root document : " + xDoc.responseText);
315                 }
316                 if (xDoc.documentElement.nodeName == "parsererror") {
317                     throw new Error("QWeb2: Could not parse document :" + xDoc.documentElement.childNodes[0].nodeValue);
318                 }
319                 return xDoc;
320             } else {
321                 return this.load_xml_string(req.responseText);
322             }
323         },
324         load_xml_string : function(s) {
325             if (window.DOMParser) {
326                 var dp = new DOMParser();
327                 var r = dp.parseFromString(s, "text/xml");
328                 if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') {
329                     throw new Error("QWeb2: Could not parse document :" + r.body.innerText);
330                 }
331                 return r;
332             }
333             var xDoc;
334             try {
335                 xDoc = new ActiveXObject("MSXML2.DOMDocument");
336             } catch (e) {
337                 throw new Error("Could not find a DOM Parser: " + e.message);
338             }
339             xDoc.async = false;
340             xDoc.preserveWhiteSpace = true;
341             xDoc.loadXML(s);
342             return xDoc;
343         },
344         has_template : function(template) {
345             return !!this.templates[template];
346         },
347         get_xhr : function() {
348             if (window.XMLHttpRequest) {
349                 return new window.XMLHttpRequest();
350             }
351             try {
352                 return new ActiveXObject('MSXML2.XMLHTTP.3.0');
353             } catch (e) {
354                 throw new Error("Could not get XHR");
355             }
356         },
357         compile : function(node) {
358             var e = new QWeb2.Element(this, node);
359             var template = node.getAttribute(this.prefix + '-name');
360             return  "   /* 'this' refers to Qweb2.Engine instance */\n" +
361                     "   var context = { engine : this, template : " + (this.tools.js_escape(template)) + " };\n" +
362                     "   dict = dict || {};\n" +
363                     "   dict['__template__'] = '" + template + "';\n" +
364                     "   var r = [];\n" +
365                     "   /* START TEMPLATE */" +
366                     (this.debug ? "" : " try {\n") +
367                     (e.compile()) + "\n" +
368                     "   /* END OF TEMPLATE */" +
369                     (this.debug ? "" : " } catch(error) {\n" +
370                     "       if (console && console.exception) console.exception(error);\n" +
371                     "       context.engine.tools.exception('Runtime Error: ' + error, context);\n") +
372                     (this.debug ? "" : "   }\n") +
373                     "   return r.join('');";
374         },
375         render : function(template, dict) {
376             dict = dict || {};
377             QWeb2.tools.extend(dict, this.default_dict);
378             /*if (this.debug && window['console'] !== undefined) {
379                 console.time("QWeb render template " + template);
380             }*/
381             var r = this._render(template, dict);
382             /*if (this.debug && window['console'] !== undefined) {
383                 console.timeEnd("QWeb render template " + template);
384             }*/
385             return r;
386         },
387         _render : function(template, dict) {
388             if (this.compiled_templates[template]) {
389                 return this.compiled_templates[template].apply(this, [dict || {}]);
390             } else if (this.templates[template]) {
391                 var ext;
392                 if (ext = this.extend_templates[template]) {
393                     var extend_node;
394                     while (extend_node = ext.shift()) {
395                         this.extend(template, extend_node);
396                     }
397                 }
398                 var code = this.compile(this.templates[template]), tcompiled;
399                 try {
400                     tcompiled = new Function(['dict'], code);
401                 } catch (error) {
402                     if (this.debug && window.console) {
403                         console.log(code);
404                     }
405                     this.tools.exception("Error evaluating template: " + error, { template: name });
406                 }
407                 if (!tcompiled) {
408                     this.tools.exception("Error evaluating template: (IE?)" + error, { template: name });
409                 }
410                 this.compiled_templates[template] = tcompiled;
411                 return this.render(template, dict);
412             } else {
413                 return this.tools.exception("Template '" + template + "' not found");
414             }
415         },
416         extend : function(template, extend_node) {
417             var jQuery = this.jQuery;
418             if (!jQuery) {
419                 return this.tools.exception("Can't extend template " + template + " without jQuery");
420             }
421             var template_dest = this.templates[template];
422             for (var i = 0, ilen = extend_node.childNodes.length; i < ilen; i++) {
423                 var child = extend_node.childNodes[i];
424                 if (child.nodeType === 1) {
425                     var jquery = child.getAttribute(this.prefix + '-jquery'),
426                         operation = child.getAttribute(this.prefix + '-operation'),
427                         target,
428                         error_msg = "Error while extending template '" + template;
429                     if (jquery) {
430                         target = jQuery(jquery, template_dest);
431                     } else {
432                         this.tools.exception(error_msg + "No expression given");
433                     }
434                     error_msg += "' (expression='" + jquery + "') : ";
435                     if (operation) {
436                         var allowed_operations = "append,prepend,before,after,replace,inner,attributes".split(',');
437                         if (this.tools.arrayIndexOf(allowed_operations, operation) == -1) {
438                             this.tools.exception(error_msg + "Invalid operation : '" + operation + "'");
439                         }
440                         operation = {'replace' : 'replaceWith', 'inner' : 'html'}[operation] || operation;
441                         if (operation === 'attributes') {
442                             jQuery('attribute', child).each(function () {
443                                 var attrib = jQuery(this);
444                                 target.attr(attrib.attr('name'), attrib.text());
445                             });
446                         } else {
447                             target[operation](child.cloneNode(true).childNodes);
448                         }
449                     } else {
450                         try {
451                             var f = new Function(['$', 'document'], this.tools.xml_node_to_string(child, true));
452                         } catch(error) {
453                             return this.tools.exception("Parse " + error_msg + error);
454                         }
455                         try {
456                             f.apply(target, [jQuery, template_dest.ownerDocument]);
457                         } catch(error) {
458                             return this.tools.exception("Runtime " + error_msg + error);
459                         }
460                     }
461                 }
462             }
463         }
464     });
465     return Engine;
466 })();
467
468 QWeb2.Element = (function() {
469     function Element(engine, node) {
470         this.engine = engine;
471         this.node = node;
472         this.tag = node.tagName;
473         this.actions = {};
474         this.actions_done = [];
475         this.attributes = {};
476         this.children = [];
477         this._top = [];
478         this._bottom = [];
479         this._indent = 1;
480         this.process_children = true;
481         this.is_void_element = ~QWeb2.tools.arrayIndexOf(this.engine.void_elements, this.tag);
482         var childs = this.node.childNodes;
483         if (childs) {
484             for (var i = 0, ilen = childs.length; i < ilen; i++) {
485                 this.children.push(new QWeb2.Element(this.engine, childs[i]));
486             }
487         }
488         var attrs = this.node.attributes;
489         if (attrs) {
490             for (var j = 0, jlen = attrs.length; j < jlen; j++) {
491                 var attr = attrs[j];
492                 var name = attr.name;
493                 var m = name.match(new RegExp("^" + this.engine.prefix + "-(.+)"));
494                 if (m) {
495                     name = m[1];
496                     if (name === 'name') {
497                         continue;
498                     }
499                     this.actions[name] = attr.value;
500                 } else {
501                     this.attributes[name] = attr.value;
502                 }
503             }
504         }
505         if (this.engine.preprocess_node) {
506             this.engine.preprocess_node.call(this);
507         }
508     }
509
510     QWeb2.tools.extend(Element.prototype, {
511         compile : function() {
512             var r = [],
513                 instring = false,
514                 lines = this._compile().split('\n');
515             for (var i = 0, ilen = lines.length; i < ilen; i++) {
516                 var m, line = lines[i];
517                 if (m = line.match(/^(\s*)\/\/@string=(.*)/)) {
518                     if (instring) {
519                         if (this.engine.debug) {
520                             // Split string lines in indented r.push arguments
521                             r.push((m[2].indexOf("\\n") != -1 ? "',\n\t" + m[1] + "'" : '') + m[2]);
522                         } else {
523                             r.push(m[2]);
524                         }
525                     } else {
526                         r.push(m[1] + "r.push('" + m[2]);
527                         instring = true;
528                     }
529                 } else {
530                     if (instring) {
531                         r.push("');\n");
532                     }
533                     instring = false;
534                     r.push(line + '\n');
535                 }
536             }
537             return r.join('');
538         },
539         _compile : function() {
540             switch (this.node.nodeType) {
541                 case 3:
542                 case 4:
543                     this.top_string(this.node.data);
544                 break;
545                 case 1:
546                     this.compile_element();
547             }
548             var r = this._top.join('');
549             if (this.process_children) {
550                 for (var i = 0, ilen = this.children.length; i < ilen; i++) {
551                     var child = this.children[i];
552                     child._indent = this._indent;
553                     r += child._compile();
554                 }
555             }
556             r += this._bottom.join('');
557             return r;
558         },
559         format_expression : function(e) {
560             /* Naive format expression builder. Replace reserved words and variables to dict[variable]
561              * Does not handle spaces before dot yet, and causes problems for anonymous functions. Use t-js="" for that */
562              if (QWeb2.expressions_cache[e]) {
563               return QWeb2.expressions_cache[e];
564             }
565             var chars = e.split(''),
566                 instring = '',
567                 invar = '',
568                 invar_pos = 0,
569                 r = '';
570             chars.push(' ');
571             for (var i = 0, ilen = chars.length; i < ilen; i++) {
572                 var c = chars[i];
573                 if (instring.length) {
574                     if (c === instring && chars[i - 1] !== "\\") {
575                         instring = '';
576                     }
577                 } else if (c === '"' || c === "'") {
578                     instring = c;
579                 } else if (c.match(/[a-zA-Z_\$]/) && !invar.length) {
580                     invar = c;
581                     invar_pos = i;
582                     continue;
583                 } else if (c.match(/\W/) && invar.length) {
584                     // TODO: Should check for possible spaces before dot
585                     if (chars[invar_pos - 1] !== '.' && QWeb2.tools.arrayIndexOf(this.engine.reserved_words, invar) < 0) {
586                         invar = this.engine.word_replacement[invar] || ("dict['" + invar + "']");
587                     }
588                     r += invar;
589                     invar = '';
590                 } else if (invar.length) {
591                     invar += c;
592                     continue;
593                 }
594                 r += c;
595             }
596             r = r.slice(0, -1);
597             QWeb2.expressions_cache[e] = r;
598             return r;
599         },
600         format_str: function (e) {
601             if (e == '0') {
602                 return 'dict[0]';
603             }
604             return this.format_expression(e);
605         },
606         string_interpolation : function(s) {
607             var _this = this;
608             if (!s) {
609               return "''";
610             }
611             function append_literal(s) {
612                 s && r.push(_this.engine.tools.js_escape(s));
613             }
614
615             var re = /(?:#{(.+?)}|{{(.+?)}})/g, start = 0, r = [], m;
616             while (m = re.exec(s)) {
617                 // extract literal string between previous and current match
618                 append_literal(s.slice(start, re.lastIndex - m[0].length));
619                 // extract matched expression
620                 r.push('(' + this.format_str(m[2] || m[1]) + ')');
621                 // update position of new matching
622                 start = re.lastIndex;
623             }
624             // remaining text after last expression
625             append_literal(s.slice(start));
626
627             return r.join(' + ');
628         },
629         indent : function() {
630             return this._indent++;
631         },
632         dedent : function() {
633             if (this._indent !== 0) {
634                 return this._indent--;
635             }
636         },
637         get_indent : function() {
638             return new Array(this._indent + 1).join("\t");
639         },
640         top : function(s) {
641             return this._top.push(this.get_indent() + s + '\n');
642         },
643         top_string : function(s) {
644             return this._top.push(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n');
645         },
646         bottom : function(s) {
647             return this._bottom.unshift(this.get_indent() + s + '\n');
648         },
649         bottom_string : function(s) {
650             return this._bottom.unshift(this.get_indent() + "//@string=" + this.engine.tools.js_escape(s, true) + '\n');
651         },
652         compile_element : function() {
653             for (var i = 0, ilen = this.engine.actions_precedence.length; i < ilen; i++) {
654                 var a = this.engine.actions_precedence[i];
655                 if (a in this.actions) {
656                     var value = this.actions[a];
657                     var key = 'compile_action_' + a;
658                     if (this[key]) {
659                         this[key](value);
660                     } else if (this.engine[key]) {
661                         this.engine[key].call(this, value);
662                     } else {
663                         this.engine.tools.exception("No handler method for action '" + a + "'");
664                     }
665                 }
666             }
667             if (this.tag.toLowerCase() !== this.engine.prefix) {
668                 var tag = "<" + this.tag;
669                 for (var a in this.attributes) {
670                     tag += this.engine.tools.gen_attribute([a, this.attributes[a]]);
671                 }
672                 this.top_string(tag);
673                 if (this.actions.att) {
674                     this.top("r.push(context.engine.tools.gen_attribute(" + (this.format_expression(this.actions.att)) + "));");
675                 }
676                 for (var a in this.actions) {
677                     var v = this.actions[a];
678                     var m = a.match(/att-(.+)/);
679                     if (m) {
680                         this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.format_expression(v)) + ")]));");
681                     }
682                     var m = a.match(/attf-(.+)/);
683                     if (m) {
684                         this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.string_interpolation(v)) + ")]));");
685                     }
686                 }
687                 if (this.actions.opentag === 'true' || (!this.children.length && this.is_void_element)) {
688                     // We do not enforce empty content on void elements
689                     // because QWeb rendering is not necessarily html.
690                     this.top_string("/>");
691                 } else {
692                     this.top_string(">");
693                     this.bottom_string("</" + this.tag + ">");
694                 }
695             }
696         },
697         compile_action_if : function(value) {
698             this.top("if (" + (this.format_expression(value)) + ") {");
699             this.bottom("}");
700             this.indent();
701         },
702         compile_action_foreach : function(value) {
703             var as = this.actions['as'] || value.replace(/[^a-zA-Z0-9]/g, '_');
704             //TODO: exception if t-as not valid
705             this.top("context.engine.tools.foreach(context, " + (this.format_expression(value)) + ", " + (this.engine.tools.js_escape(as)) + ", dict, function(context, dict) {");
706             this.bottom("});");
707             this.indent();
708         },
709         compile_action_call : function(value) {
710             if (this.children.length === 0) {
711                 return this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict));");
712             } else {
713                 this.top("r.push(context.engine.tools.call(context, " + (this.engine.tools.js_escape(value)) + ", dict, null, function(context, dict) {");
714                 this.bottom("}));");
715                 this.indent();
716                 this.top("var r = [];");
717                 return this.bottom("return r.join('');");
718             }
719         },
720         compile_action_set : function(value) {
721             var variable = this.format_expression(value);
722             if (this.actions['value']) {
723                 if (this.children.length) {
724                     this.engine.tools.warning("@set with @value plus node chidren found. Children are ignored.");
725                 }
726                 this.top(variable + " = (" + (this.format_expression(this.actions['value'])) + ");");
727                 this.process_children = false;
728             } else {
729                 if (this.children.length === 0) {
730                     this.top(variable + " = '';");
731                 } else if (this.children.length === 1 && this.children[0].node.nodeType === 3) {
732                     this.top(variable + " = " + (this.engine.tools.js_escape(this.children[0].node.data)) + ";");
733                     this.process_children = false;
734                 } else {
735                     this.top(variable + " = (function(dict) {");
736                     this.bottom("})(dict);");
737                     this.indent();
738                     this.top("var r = [];");
739                     this.bottom("return r.join('');");
740                 }
741             }
742         },
743         compile_action_esc : function(value) {
744             this.top("r.push(context.engine.tools.html_escape("
745                     + this.format_expression(value)
746                     + "));");
747         },
748         compile_action_raw : function(value) {
749             this.top("r.push(" + (this.format_str(value)) + ");");
750         },
751         compile_action_js : function(value) {
752             this.top("(function(" + value + ") {");
753             this.bottom("})(dict);");
754             this.indent();
755             var lines = this.engine.tools.xml_node_to_string(this.node, true).split(/\r?\n/);
756             for (var i = 0, ilen = lines.length; i < ilen; i++) {
757                 this.top(lines[i]);
758             }
759             this.process_children = false;
760         },
761         compile_action_debug : function(value) {
762             this.top("debugger;");
763         },
764         compile_action_log : function(value) {
765             this.top("console.log(" + this.format_expression(value) + ");");
766         }
767     });
768     return Element;
769 })();