Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html style="height: 100%">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<script type="text/javascript" src="qweb.js"></script>
<script type="text/javascript" src="qweb2.js"></script>
<script type="text/javascript">
(function (c) {
if (c.time) { return; }
var d = {};
c.time = function (key) {
d[key] = Date.now();
};
c.timeEnd = function (key) {
var end = Date.now(),
origin = d[key];
delete d[key];
if (!origin) { return; }
console.log(key + ': ' + (end - origin) + 'ms');
};
})(window.console);
var dict = {
session : true,
testing : 'yes',
name : 'AGR'
};
console.time("Load template with QWeb");
QWeb.add_template("qweb-benchmark.xml");
console.timeEnd("Load template with QWeb");
console.time("Load template with QWeb2");
var engine = new QWeb2.Engine("qweb-benchmark.xml")
engine.debug = true;
console.timeEnd("Load template with QWeb2")
var iter = 1000;
console.log("Rendering...");
console.time("Render " + iter + " templates with QWeb");
for (var i = 0; i < iter; i++) {
var qweb = QWeb.render('benchmark', dict);
}
console.timeEnd("Render " + iter + " templates with QWeb");
console.time("Render " + iter + " templates with QWeb2");
for (var i = 0; i < iter; i++) {
var qweb2 = engine.render('benchmark', dict);
}
console.timeEnd("Render " + iter + " templates with QWeb2");
</script>
</head>
<body>
Please, check your console for results
</body>
</html>

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template">
<t t-name="benchmark"><div id="oe_notification" class="oe_notification">
<div id="oe_notification_default">
<a class="ui-notify-cross ui-notify-close" href="#">x</a>
<h1>title</h1>
<p>text</p>
</div>
<div id="oe_notification_alert" class="ui-state-error">
<a class="ui-notify-cross ui-notify-close" href="#">x</a>
<span style="float:left; margin:2px 5px 0 0;" class="ui-icon ui-icon-alert"></span>
<h1>title</h1>
<p>text</p>
</div>
</div>
<t t-js="d">
d.iter = 'one,two,three,four,five'.split(',')
</t>
<t t-foreach="iter" t-as="i">
<t t-call="benchmark_call">
+ <t t-esc="i"/>
</t>
</t>
<t t-set="enplus">1</t>
<t t-set="novar">true</t>
<div t-attf-class="id_#{enplus}"/>
<div t-if="testing || true" t-att-class="novar || 'yes'" style="display: none">
<t t-set="novar"></t>
<t t-set="style">height: 200px; border: 1px solid red;</t>
<div t-att="{ 'style' : style, 'disabled' : 'false', 'readonly' : novar or undefined }"/>
<t t-foreach="{'my': 'first', 'my2': 'second' }" t-as="v">
* <t t-esc="v"/> : <t t-esc="v_value"/>
</t>
Ok this is good <t t-esc="name"/>!
<t t-set="myvar">Hi there !</t>
[<t t-raw="myvar"/>]
<t t-set="myvar2" t-value="'a,b,c,d,e'.split(',')"/>
<t t-foreach="myvar2" t-as="i">
(<t t-esc="i"/>)
</t>
</div>
<div id="oe_notification" class="oe_notification">
<div id="oe_notification_default">
<a class="ui-notify-cross ui-notify-close" href="#">x</a>
<h1>title</h1>
<p>text</p>
</div>
</div>
</t>
<t t-name="benchmark_call">
<div id="oe_notification_alert" class="ui-state-error">
<a class="ui-notify-cross ui-notify-close" href="#">x</a>
<span style="float:left; margin:2px 5px 0 0;" class="ui-icon ui-icon-alert"></span>
<h1>Here's your value : (<t t-esc="0"/>) !!</h1>
</div>
</t>
</templates>

View file

@ -0,0 +1,83 @@
<templates>
<t t-name="static">
<div foo="a" bar="b" baz="c"/>
</t>
<result id="static"><![CDATA[<div foo="a" bar="b" baz="c"></div>]]></result>
<t t-name="static-void">
<img src="/test.jpg" alt="Test" loading="lazy"/>
</t>
<result id="static-void"><![CDATA[<img src="/test.jpg" alt="Test" loading="lazy"/>]]></result>
<t t-name="fixed-literal">
<div t-att-foo="'bar'"/>
</t>
<result id="fixed-literal"><![CDATA[<div foo="bar"></div>]]></result>
<t t-name="fixed-variable">
<div t-att-foo="value"/>
</t>
<params id="fixed-variable">{"value": "ok"}</params>
<result id="fixed-variable"><![CDATA[<div foo="ok"></div>]]></result>
<t t-name="tuple-literal">
<div t-att="['foo', 'bar']"/>
</t>
<result id="tuple-literal"><![CDATA[<div foo="bar"></div>]]></result>
<t t-name="tuple-variable">
<div t-att="value"/>
</t>
<params id="tuple-variable">{"value": ["foo", "bar"]}</params>
<result id="tuple-variable"><![CDATA[<div foo="bar"></div>]]></result>
<t t-name="object">
<div t-att="value"/>
</t>
<params id="object">{"value": {"a": 1, "b": 2, "c": 3}}</params>
<result id="object"><![CDATA[<div a="1" b="2" c="3"></div>]]></result>
<t t-name="format-literal">
<div t-attf-foo="bar"/>
</t>
<result id="format-literal"><![CDATA[<div foo="bar"></div>]]></result>
<t t-name="format-value">
<div t-attf-foo="b{{value}}r"/>
</t>
<params id="format-value">{"value": "a"}</params>
<result id="format-value"><![CDATA[<div foo="bar"></div>]]></result>
<t t-name="format-expression">
<div t-attf-foo="{{value + 37}}"/>
</t>
<params id="format-expression">{"value": 5}</params>
<result id="format-expression"><![CDATA[<div foo="42"></div>]]></result>
<t t-name="format-multiple">
<div t-attf-foo="a {{value1}} is {{value2}} of {{value3}} ]"/>
</t>
<params id="format-multiple">{
"value1": 0,
"value2": 1,
"value3": 2
}</params>
<result id="format-multiple"><![CDATA[
<div foo="a 0 is 1 of 2 ]"></div>
]]></result>
<t t-name="various-escapes">
<div foo="&lt;foo"
t-att-bar="bar"
t-attf-baz="&lt;{{baz}}&gt;"
t-att="qux"/>
</t>
<params id="various-escapes"><![CDATA[{
"bar": "<bar>",
"baz": "\"<baz>\"",
"qux": {"qux": "<>"}
}]]></params>
<result id="various-escapes"><![CDATA[
<div foo="&lt;foo" bar="&lt;bar&gt;" baz="&lt;&quot;&lt;baz&gt;&quot;&gt;" qux="&lt;&gt;"></div>
]]></result>
</templates>

View file

@ -0,0 +1,84 @@
<templates>
<t t-name="_basic-callee">ok</t>
<t t-name="_callee-printsbody"><t t-esc="0"/></t>
<t t-name="_callee-uses-foo"><t t-esc="foo"/></t>
<t t-name="basic-caller">
<t t-call="_basic-callee"/>
</t>
<result id="basic-caller">ok</result>
<t t-name="with-unused-body">
<t t-call="_basic-callee">WHEEE</t>
</t>
<result id="with-unused-body">ok</result>
<t t-name="with-unused-setbody">
<t t-call="_basic-callee">
<t t-set="qux" t-value="3"/>
</t>
</t>
<result id="with-unused-setbody">ok</result>
<t t-name="with-used-body">
<t t-call="_callee-printsbody">ok</t>
</t>
<result id="with-used-body">ok</result>
<t t-name="with-used-setbody">
<t t-call="_callee-uses-foo">
<t t-set="foo" t-value="'ok'"/>
</t>
</t>
<result id="with-used-setbody">ok</result>
<t t-name="_call-with-body-arch-lookup">
<section t-raw="0"/>
</t>
<t t-name="call-without-body-arch-lookup">
<t t-call="_call-with-body-arch-lookup"/>
</t>
<result id="call-without-body-arch-lookup"><![CDATA[<section></section>]]></result>
<t t-name="call-with-body-arch-lookup">
<t t-call="_call-with-body-arch-lookup">
<div><span class="toto" t-esc="value"/></div>
</t>
</t>
<params id="call-with-body-arch-lookup">
{"value": "ok"}
</params>
<result id="call-with-body-arch-lookup"><![CDATA[<section>
<div><span class="toto">ok</span></div>
</section>]]></result>
<!--
postfix to call removed because Python impl appends all whitespace
following called template's root to template result (+= element.tail)
-> ends up with bunch of extra whitespace in the middle of the
generated content. Could normalize, not sure current impl can be
fixed as-is
-->
<t t-name="inherit-context">
<t t-set="foo" t-value="1"/>
<t t-call="_callee-uses-foo"/><!-- - <t t-esc="foo"/> -->
</t>
<result id="inherit-context">1<!-- - 1 --></result>
<t t-name="scoped-parameter">
<t t-call="_basic-callee">
<t t-set="foo" t-value="42"/>
</t>
<!-- should not print anything -->
<t t-esc="foo"/>
</t>
<result id="scoped-parameter">
ok
</result>
<t t-name="expression-caller">
<t t-call="{{True and '_basic-callee' or 'other'}}"/>
</t>
<result id="expression-caller">ok</result>
</templates>

View file

@ -0,0 +1,68 @@
<templates>
<t t-name="boolean-value-condition">
<t t-if="condition">ok</t>
</t>
<params id="boolean-value-condition">{"condition": true}</params>
<result id="boolean-value-condition">ok</result>
<t t-name="boolean-value-condition-false">
<t t-if="condition">fail</t>
</t>
<params id="boolean-value-condition-false">{"condition": false}</params>
<result id="boolean-value-condition-false"/>
<t t-name="boolean-value-condition-missing">
<t t-if="condition">fail</t>
</t>
<result id="boolean-value-condition-missing"/>
<t t-name="boolean-value-condition-elif">
<t t-if="color == 'black'">black pearl</t>
<t t-elif="color == 'yellow'">yellow submarine</t>
<t t-elif="color == 'red'">red is dead</t>
<t t-else="">beer</t>
</t>
<params id="boolean-value-condition-elif">{"color": "red"}</params>
<result id="boolean-value-condition-elif">red is dead</result>
<t t-name="boolean-value-condition-else">
<div><span>begin</span><t t-if="condition">ok</t>
<t t-else="">ok-else</t><span>end</span></div>
</t>
<params id="boolean-value-condition-else">{"condition": true}</params>
<result id="boolean-value-condition-else"><![CDATA[<div><span>begin</span>ok<span>end</span></div>]]></result>
<t t-name="boolean-value-condition-false-else">
<div><span>begin</span><t t-if="condition">fail</t>
<t t-else="">fail-else</t><span>end</span></div>
</t>
<params id="boolean-value-condition-false-else">{"condition": false}</params>
<result id="boolean-value-condition-false-else"><![CDATA[<div><span>begin</span>fail-else<span>end</span></div>]]></result>
<t t-name="comment-branching">
<t t-if="condition == 'if'">if</t>
<t t-elif="condition == 'elif1'">elif1</t>
<!-- Comment ignored PART OF THE TEST !!! -->
<t t-elif="condition == 'elif2'">elif2</t>
<t t-else="">else</t>
</t>
<params id="comment-branching">{"condition": "elif1"}</params>
<result id="comment-branching"><![CDATA[elif1]]></result>
<t t-name="comment-branching-1">
<t t-if="condition == 'if'">if</t>
<t t-elif="condition == 'elif1'">elif1</t>
<!-- Comment ignored PART OF THE TEST !!! -->
<t t-elif="condition == 'elif2'">elif2</t>
<t t-else="">else</t>
</t>
<params id="comment-branching-1">{"condition": "elif2"}</params>
<result id="comment-branching-1"><![CDATA[elif2]]></result>
<t t-name="comment-branching-2">
<div t-if="condition == 'if'">if</div><!-- Comment ignored PART OF THE TEST !!! --><div>sometext</div>
</t>
<params id="comment-branching-2">{"condition": "if"}</params>
<result id="comment-branching-2"><![CDATA[<div>if</div><div>sometext</div>]]></result>
</templates>

View file

@ -0,0 +1,62 @@
<templates>
<!-- js-only -->
<t t-name="jquery-extend">
<ul><li>one</li></ul>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="append"><li>3</li></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul li:first-child" t-operation="replace"><li>2</li></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="prepend"><li>1</li></t>
<t t-jquery="ul" t-operation="before"><hr/></t>
<t t-jquery="ul" t-operation="after"><hr/></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul">this.attr('class', 'main');</t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="attributes"><attribute name="title" value="Main Title" /></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="ul" t-operation="attributes"><attribute name="name">main-ul</attribute></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="hr:eq(1)" t-operation="replace"><footer></footer></t>
</t>
<t t-extend="jquery-extend">
<t t-jquery="footer" t-operation="inner"><b>[[end]]</b></t>
</t>
<result id="jquery-extend"><![CDATA[
<hr/><ul class="main" title="Main Title" name="main-ul"><li>1</li><li>2</li><li>3</li></ul><footer><b>[[end]]</b></footer>
]]></result>
<t t-name="jquery-extend-clone" t-extend="jquery-extend">
<t t-jquery="ul" t-operation="append"><li>[[cloned template]]</li></t>
</t>
<result id="jquery-extend-clone"><![CDATA[
<hr/><ul class="main" title="Main Title" name="main-ul"><li>1</li><li>2</li><li>3</li><li>[[cloned template]]</li></ul><footer><b>[[end]]</b></footer>
]]></result>
<t t-name="a">
<div><span>Hi</span></div>
</t>
<t t-name="b" t-extend="a">
<t t-jquery="span" t-operation="after"><i>World</i></t>
</t>
<t t-name="c" t-extend="b">
<t t-jquery="span" t-operation="replace"><span>Hello</span></t>
</t>
<result id="a"><![CDATA[
<div><span>Hi</span></div>
]]></result>
<result id="b"><![CDATA[
<div><span>Hi</span><i>World</i></div>
]]></result>
<result id="c"><![CDATA[
<div><span>Hello</span><i>World</i></div>
]]></result>
</templates>

View file

@ -0,0 +1,46 @@
<templates xml:space="preserve">
<t t-name="iter-items">
<t t-foreach="[3, 2, 1]" t-as="item">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]</t>
</t>
<result id="iter-items">
[0: 3 3]
[1: 2 2]
[2: 1 1]
</result>
<t t-name="iter-position">
<t t-foreach="5" t-as="item">
-<t t-if="item_first"> first</t><t t-if="item_last"> last</t> (<t t-esc="item_parity"/>)</t>
</t>
<result id="iter-position">
- first (even)
- (odd)
- (even)
- (odd)
- last (even)
</result>
<!-- test integer param -->
<t t-name="iter-int">
<t t-foreach="3" t-as="item">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]</t>
</t>
<result id="iter-int">
[0: 0 0]
[1: 1 1]
[2: 2 2]
</result>
<!-- test dict param -->
<t t-name="iter-dict">
<t t-foreach="value" t-as="item">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/> - <t t-esc="item_parity"/>]</t>
</t>
<params id="iter-dict">{"value": {"a": 1, "b": 2, "c": 3}}</params>
<result id="iter-dict">
[0: a 1 - even]
[1: b 2 - odd]
[2: c 3 - even]
</result>
</templates>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="_callee-asc"><Año t-att-falló="'agüero'" t-raw="0"/></t>
<t t-name="_callee-uses-foo"><span t-esc="foo">foo default</span></t>
<t t-name="_callee-asc-toto"><div t-raw="toto">toto default</div></t>
<t t-name="caller">
<t t-foreach="[4,5,6]" t-as="value">
<span t-esc="value"/>
<t t-call="_callee-asc">
<t t-call="_callee-uses-foo">
<t t-set="foo" t-value="'aaa'"/>
</t>
<t t-call="_callee-uses-foo"/>
<t t-set="foo" t-value="'bbb'"/>
<t t-call="_callee-uses-foo"/>
</t>
</t>
<t t-call="_callee-asc-toto"/>
<t t-set="toto"><t t-set="truc" t-value="'bbb'"/><i t-att-notruc="not truc or None" t-att-truc="bool(truc)">i</i></t>
<t t-call="_callee-asc-toto"/>
</t>
<result id="caller"><![CDATA[
<span>4</span>
<Año falló="agüero">
<span>aaa</span>
<span>foo default</span>
<span>bbb</span>
</Año>
<span>5</span>
<Año falló="agüero">
<span>aaa</span>
<span>foo default</span>
<span>bbb</span>
</Año>
<span>6</span>
<Año falló="agüero">
<span>aaa</span>
<span>foo default</span>
<span>bbb</span>
</Año>
<div>toto default</div>
<div><i truc="True">i</i></div>
]]></result>
</templates>

View file

@ -0,0 +1,47 @@
<templates>
<!-- esc, evaluates and returns @t-esc after having xml-escaped it -->
<t t-name="esc-literal">
<t t-esc="'ok'"/>
</t>
<result id="esc-literal">ok</result>
<t t-name="esc-variable">
<t t-esc="var"/>
</t>
<params id="esc-variable">{"var": "ok"}</params>
<result id="esc-variable">ok</result>
<t t-name="esc-toescape">
<t t-esc="var"/>
</t>
<params id="esc-toescape"><![CDATA[{"var": "<ok>"}]]></params>
<result id="esc-toescape"><![CDATA[&lt;ok&gt;]]></result>
<t t-name="esc-node">
<span t-esc="'ok'"/>
</t>
<result id="esc-node"><![CDATA[<span>ok</span>]]></result>
<!-- raw, evaluates and returns @t-raw directly (no escaping) -->
<t t-name="raw-literal">
<t t-raw="'ok'"/>
</t>
<result id="raw-literal">ok</result>
<t t-name="raw-number">
<t t-raw="1"/>
</t>
<result id="raw-number">1</result>
<t t-name="raw-variable">
<t t-raw="var"/>
</t>
<params id="raw-variable">{"var": "ok"}</params>
<result id="raw-variable">ok</result>
<t t-name="raw-notescaped">
<t t-raw="var"/>
</t>
<params id="raw-notescaped"><![CDATA[{"var": "<ok>"}]]></params>
<result id="raw-notescaped"><![CDATA[<ok>]]></result>
</templates>

View file

@ -0,0 +1,62 @@
<templates>
<t t-name="set-from-attribute-literal">
<t t-set="value" t-value="'ok'"/>
<t t-esc="value"/>
</t>
<result id="set-from-attribute-literal">
ok
</result>
<t t-name="set-from-body-literal">
<t t-set="value">ok</t>
<t t-esc="value"/>
</t>
<result id="set-from-body-literal">
ok
</result>
<t t-name="set-from-attribute-lookup">
<t t-set="stuff" t-value="value"/>
<t t-esc="stuff"/>
</t>
<params id="set-from-attribute-lookup">
{"value": "ok"}
</params>
<result id="set-from-attribute-lookup">
ok
</result>
<t t-name="set-from-body-lookup">
<t t-set="stuff">
<t t-esc="value"/>
</t>
<t t-esc="stuff"/>
</t>
<params id="set-from-body-lookup">
{"value": "ok"}
</params>
<result id="set-from-body-lookup">
ok
</result>
<t t-name="set-from-body-arch-lookup">
<t t-set="stuff"><div><span t-esc="value"/></div></t>
<t t-raw="stuff"/>
</t>
<params id="set-from-body-arch-lookup">
{"value": "ok"}
</params>
<result id="set-from-body-arch-lookup"><![CDATA[<div><span>ok</span></div>]]></result>
<t t-name="set-empty-body">
<t t-set="stuff"/>
<t t-esc="stuff"/>
</t>
<result id="set-empty-body"/>
<t t-name="t-value-priority">
<t t-set="value" t-value="1">2</t>
<t t-esc="value"/>
</t>
<result id="t-value-priority">1</result>
</templates>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="fixed-literal">
<Año t-att-falló="'agüero'"/>
</t>
<result id="fixed-literal"><![CDATA[<Año falló="agüero"></Año>]]></result>
</templates>

View file

@ -0,0 +1,26 @@
<templates>
<t t-name="date-simple"><t t-esc='value' /></t>
<params id="date-simple">{"value": "1988-09-16"}</params>
<result id="date-simple">1988-09-16</result>
<t t-name="datetime-simple"><t t-esc='value' /></t>
<params id="datetime-simple">{"value": "1988-09-16 14:00:00"}</params>
<result id="datetime-simple">1988-09-16 14:00:00</result>
<t t-name="datetime-widget-datetime"><t t-esc='value' t-options="{'widget': 'datetime'}" /></t>
<params id="datetime-widget-datetime">{"value": "1988-09-16 14:00:00"}</params>
<result id="datetime-widget-datetime">09/16/1988 16:00:00</result>
<t t-name="datetime-widget-date"><t t-esc='value' t-options="{'widget': 'date'}" /></t>
<params id="datetime-widget-date">{"value": "1988-09-16 14:00:00"}</params>
<result id="datetime-widget-date">09/16/1988</result>
<t t-name="datetime-widget-date-tz2"><t t-esc='value' t-options="{'widget': 'date'}" /></t>
<params id="datetime-widget-date-tz2">{"value": "1988-09-16 01:00:00"}</params>
<result id="datetime-widget-date-tz2">09/16/1988</result>
<t t-name="datetime-widget-date-tz"><t t-esc='value' t-options="{'widget': 'date'}" /></t>
<params id="datetime-widget-date-tz">{"value": "1988-09-16 23:00:00"}</params>
<result id="datetime-widget-date-tz">09/17/1988</result>
</templates>

View file

@ -0,0 +1,73 @@
<!doctype html>
<html>
<head>
<script src="/web/static/lib/jquery/jquery.js"></script>
<link rel="stylesheet" href="/web/static/lib/qunit/qunit.css" type="text/css" media="screen"/>
<script type="text/javascript" src="/web/static/lib/qunit/qunit.js"></script>
<script type="text/javascript" src="qweb2.js"></script>
<script>
QWeb = new QWeb2.Engine();
function trim(s) {
return s.replace(/(^\s+|\s+$)/g, '');
}
function render(template, context) {
return trim(QWeb.render(template, context)).toLowerCase();
}
/**
* Loads the template file, and executes all the test template in a
* qunit module $title
*/
function test(title, template) {
QUnit.module(title, {
setup: function () {
var self = this;
this.qweb = new QWeb2.Engine();
QUnit.stop();
this.qweb.add_template(template, function (_, doc) {
self.doc = doc;
QUnit.start();
})
}
});
QUnit.test('autotest', function (assert) {
var templates = this.qweb.templates;
for (var template in templates) {
if (!templates.hasOwnProperty(template)) { continue; }
// ignore templates whose name starts with _, they're
// helpers/internal
if (/^_/.test(template)) { continue; }
var params = this.doc.querySelector('params#' + template);
var args = params ? JSON.parse(params.textContent) : {};
var results = this.doc.querySelector('result#' + template);
assert.equal(
trim(this.qweb.render(template, args)),
trim(results.textContent),
template);
}
});
}
$(document).ready(function() {
test("Output", 'qweb-test-output.xml');
test("Context-setting", 'qweb-test-set.xml');
test("Conditionals", 'qweb-test-conditionals.xml');
test("Attributes manipulation", 'qweb-test-attributes.xml');
test("Template calling (to the faraway pages)",
'qweb-test-call.xml');
test("Foreach", 'qweb-test-foreach.xml');
test("Global", 'qweb-test-global.xml');
test('Template inheritance', 'qweb-test-extend.xml');
});
</script>
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
</html>

View file

@ -0,0 +1,435 @@
/*
Copyright (c) 2013, Fabien Meghazi
Released under the MIT license
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
//---------------------------------------------------------
// QWeb javascript
//---------------------------------------------------------
/*
TODO
String parsing
if (window.DOMParser) {
parser=new DOMParser();
xmlDoc=parser.parseFromString(text,"text/xml");
} else {
xmlDoc=new ActiveXObject("Msxml2.DOMDocument.4.0");
xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
Which versions to try, it's confusing...
xmlDoc.async="false";
xmlDoc.async=false;
xmlDoc.preserveWhiteSpace=true;
xmlDoc.load("f.xml");
xmlDoc.loadXML(text); ?
}
Support space in IE by reparsing the responseText
xmlhttp.responseXML.loadXML(xmlhttp.responseText); ?
Preprocess: (nice optimization)
preprocess by flattening all non t- element to a TEXT_NODE.
count the number of "\n" in text nodes to give an aproximate LINE NUMBER on elements for error reporting
if from IE HTMLDOM use if(a[i].specified) to avoid 88 empty attributes per element during the preprocess,
implement t-trim 'left' 'right' 'both', is it needed ? inner=render_trim(l_inner.join(), t_att)
Ruby/python: to backport from javascript to python/ruby render_node to use regexp, factorize foreach %var, t-att test for tuple(attname,value)
DONE
we reintroduced t-att-id, no more t-esc-id because of the new convention t-att="["id","val"]"
*/
var QWeb = {
templates:{},
prefix:"t",
reg:new RegExp(),
tag:{},
att:{},
ValueException: function (value, message) {
this.value = value;
this.message = message;
},
eval_object:function(e, v) {
// TODO: Currently this will also replace and, or, ... in strings. Try
// 'hi boys and girls' != '' and 1 == 1 -- will be replaced to : 'hi boys && girls' != '' && 1 == 1
// try to find a solution without tokenizing
e = '(' + e + ')';
e = e.replace(/\band\b/g, " && ");
e = e.replace(/\bor\b/g, " || ");
e = e.replace(/\bgt\b/g, " > ");
e = e.replace(/\bgte\b/g, " >= ");
e = e.replace(/\blt\b/g, " < ");
e = e.replace(/\blte\b/g, " <= ");
if (v[e] != undefined) {
return v[e];
} else {
with (v) return eval(e);
}
},
eval_str:function(e, v) {
var r = this.eval_object(e, v);
r = (typeof(r) == "undefined" || r == null) ? "" : r.toString();
return e == "0" ? v["0"] : r;
},
eval_format:function(e, v) {
var m, src = e.split(/#/), r = src[0];
for (var i = 1; i < src.length; i++) {
if (m = src[i].match(/^{(.*)}(.*)/)) {
r += this.eval_str(m[1], v) + m[2];
} else {
r += "#" + src[i];
}
}
return r;
},
eval_bool:function(e, v) {
return !!this.eval_object(e, v);
},
trim : function(v, mode) {
if (!v || !mode) return v;
switch (mode) {
case 'both':
return v.replace(/^\s*|\s*$/g, "");
case "left":
return v.replace(/^\s*/, "");
case "right":
return v.replace(/\s*$/, "");
}
throw new QWeb.ValueException(
mode, "unknown trimming mode, trim mode must follow the pattern '[inner] (left|right|both)'");
},
escape_text:function(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
},
escape_att:function(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
},
render_node : function(e, v, inner_trim) {
if (e.nodeType == 3) {
return inner_trim ? this.trim(e.data, inner_trim) : e.data;
}
if (e.nodeType == 1) {
var g_att = {};
var t_att = {};
var t_render = null;
var a = e.attributes;
for (var i = 0; i < a.length; i++) {
var an = a[i].name,av = a[i].value;
var m;
if (m = an.match(this.reg)) {
var n = m[1];
if (n == "eval") {
n = m[2].substring(1);
av = this.eval_str(av, v);
}
var f;
if (f = this.att[n]) {
this[f](e, t_att, g_att, v, m[2], av);
} else if (f = this.tag[n]) {
t_render = f;
}
t_att[n] = av;
} else {
g_att[an] = av;
}
}
if (inner_trim && !t_att["trim"]) {
t_att["trim"] = "inner " + inner_trim;
}
if (t_render) {
return this[t_render](e, t_att, g_att, v);
}
return this.render_element(e, t_att, g_att, v);
}
return "";
},
render_element:function(e, t_att, g_att, v) {
var inner = "", ec = e.childNodes, trim = t_att["trim"], inner_trim;
if (trim) {
if (/\binner\b/.test(trim)) {
inner_trim = true;
if (trim == 'inner') {
trim = "both";
}
}
var tm = /\b(both|left|right)\b/.exec(trim);
if (tm) trim = tm[1];
}
for (var i = 0; i < ec.length; i++) {
inner += inner_trim ? this.trim(this.render_node(ec[i], v, inner_trim ? trim : null), trim) : this.render_node(ec[i], v, inner_trim ? trim : null);
}
if (trim && !inner_trim) {
inner = this.trim(inner, trim);
}
if (e.tagName == this.prefix) {
return inner;
}
var att = "";
for (var an in g_att) {
att += " " + an + '="' + this.escape_att(g_att[an]) + '"';
}
// Some IE versions have problems with closed tags
var opentag = !!t_att['opentag'] && this.eval_bool(t_att["opentag"], v);
return inner.length || opentag ? "<" + e.tagName + att + ">" + inner + "</" + e.tagName + ">" : "<" + e.tagName + att + "/>";
},
render_att_att:function(e, t_att, g_att, v, ext, av) {
if (ext) {
var attv = this.eval_object(av, v);
if (attv != null) {
g_att[ext.substring(1)] = attv.toString();
}
} else {
var o = this.eval_object(av, v);
if (o != null) {
// TODO: http://bonsaiden.github.com/JavaScript-Garden/#types.typeof
if (o.constructor == Array && o.length > 1 && o[1] != null) {
g_att[o[0]] = new String(o[1]);
} else if (o.constructor == Object) {
for (var i in o) {
if(o[i]!=null) {
g_att[i] = new String(o[i]);
}
}
}
}
}
},
render_att_attf:function(e, t_att, g_att, v, ext, av) {
g_att[ext.substring(1)] = this.eval_format(av, v);
},
render_tag_raw:function(e, t_att, g_att, v) {
return this.eval_str(t_att["raw"], v);
},
render_tag_rawf:function(e, t_att, g_att, v) {
return this.eval_format(t_att["rawf"], v);
},
/*
* Idea: if the name of the tag != t render the tag around the value <a name="a" t-esc="label"/>
*/
render_tag_esc:function(e, t_att, g_att, v) {
return this.escape_text(this.eval_str(t_att["esc"], v));
},
render_tag_escf:function(e, t_att, g_att, v) {
return this.escape_text(this.eval_format(t_att["escf"], v));
},
render_tag_if:function(e, t_att, g_att, v) {
return this.eval_bool(t_att["if"], v) ? this.render_element(e, t_att, g_att, v) : "";
},
render_tag_set:function(e, t_att, g_att, v) {
var ev = t_att["value"];
if (ev && ev.constructor != Function) {
v[t_att["set"]] = this.eval_object(ev, v);
} else {
v[t_att["set"]] = this.render_element(e, t_att, g_att, v);
}
return "";
},
render_tag_call:function(e, t_att, g_att, v) {
var d = v;
if (!t_att["import"]) {
d = {};
for (var i in v) {
d[i] = v[i];
}
}
d["0"] = this.render_element(e, t_att, g_att, d);
return this.render(t_att["call"], d);
},
render_tag_js:function(e, t_att, g_att, v) {
var dict_name = t_att["js"] || "dict";
v[dict_name] = v;
var r = this.eval_str(this.render_element(e, t_att, g_att, v), v);
delete(v[dict_name]);
return r || '';
},
/**
* Renders a foreach loop (@t-foreach).
*
* Adds the following elements to its context, where <code>${name}</code>
* is specified via <code>@t-as</code>:
* * <code>${name}</code> The current element itself
* * <code>${name}_value</code> Same as <code>${name}</code>
* * <code>${name}_index</code> The 0-based index of the current element
* * <code>${name}_first</code> Whether the current element is the first one
* * <code>${name}_parity</code> odd|even (as strings)
* * <code>${name}_all</code> The iterated collection itself
*
* If the collection being iterated is an array, also adds:
* * <code>${name}_last</code> Whether the current element is the last one
* * All members of the current object
*
* If the collection being iterated is an object, the value is actually the object's key
*
* @param e ?
* @param t_att attributes of the element being <code>t-foreach</code>'d
* @param g_att ?
* @param old_context the context in which the foreach is evaluated
*/
render_tag_foreach:function(e, t_att, g_att, old_context) {
var expr = t_att["foreach"];
var enu = this.eval_object(expr, old_context);
var ru = [];
if (enu) {
var val = t_att['as'] || expr.replace(/[^a-zA-Z0-9]/g, '_');
var context = {};
for (var i in old_context) {
context[i] = old_context[i];
}
context[val + "_all"] = enu;
var val_value = val + "_value",
val_index = val + "_index",
val_first = val + "_first",
val_last = val + "_last",
val_parity = val + "_parity";
var size = enu.length;
if (size) {
context[val + "_size"] = size;
for (var j = 0; j < size; j++) {
var cur = enu[j];
context[val_value] = cur;
context[val_index] = j;
context[val_first] = j == 0;
context[val_last] = j + 1 == size;
context[val_parity] = (j % 2 == 1 ? 'odd' : 'even');
if (cur.constructor == Object) {
for (var k in cur) {
context[k] = cur[k];
}
}
context[val] = cur;
var r = this.render_element(e, t_att, g_att, context);
ru.push(r);
}
} else {
var index = 0;
for (cur in enu) {
context[val_value] = cur;
context[val_index] = index;
context[val_first] = index == 0;
context[val_parity] = (index % 2 == 1 ? 'odd' : 'even');
context[val] = cur;
ru.push(this.render_element(e, t_att, g_att, context));
index += 1;
}
}
return ru.join("");
} else {
return "qweb: foreach " + expr + " not found.";
}
},
hash:function() {
var l = [], m;
for (var i in this) {
if (m = i.match(/render_tag_(.*)/)) {
this.tag[m[1]] = i;
l.push(m[1]);
} else if (m = i.match(/render_att_(.*)/)) {
this.att[m[1]] = i;
l.push(m[1]);
}
}
l.sort(function(a, b) {
return a.length > b.length ? -1 : 1;
});
var s = "^" + this.prefix + "-(eval|" + l.join("|") + "|.*)(.*)$";
this.reg = new RegExp(s);
},
/**
* returns the correct XMLHttpRequest instance for the browser, or null if
* it was not able to build any XHR instance.
*
* @returns XMLHttpRequest|MSXML2.XMLHTTP.3.0|null
*/
get_xhr:function () {
if (window.XMLHttpRequest) {
return new window.XMLHttpRequest();
}
try {
return new ActiveXObject('MSXML2.XMLHTTP.3.0');
} catch(e) {
return null;
}
},
load_xml:function(s) {
var xml;
if (s[0] == "<") {
/*
manque ca pour sarrisa
if(window.DOMParser){
mozilla
if(!window.DOMParser){
var doc = Sarissa.getDomDocument();
doc.loadXML(sXml);
return doc;
};
};
*/
} else {
var req = this.get_xhr();
if (req) {
req.open("GET", s, false);
req.send(null);
//if ie r.setRequestHeader("If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT");
xml = req.responseXML;
/*
TODO
if intsernetexploror
getdomimplmentation() for try catch
responseXML.getImplet
d=domimple()
d.preserverWhitespace=1
d.loadXML()
xml.preserverWhitespace=1
xml.loadXML(r.reponseText)
*/
return xml;
}
}
},
add_template:function(e) {
// TODO: keep sources so we can implement reload()
this.hash();
if (e.constructor == String) {
e = this.load_xml(e);
}
var ec = e.documentElement ? e.documentElement.childNodes : ( e.childNodes ? e.childNodes : [] );
for (var i = 0; i < ec.length; i++) {
var n = ec[i];
if (n.nodeType == 1) {
var name = n.getAttribute(this.prefix + "-name");
this.templates[name] = n;
}
}
},
render:function(name, v) {
var e;
if (e = this.templates[name]) {
return this.render_node(e, v);
}
return "template " + name + " not found";
}
};

View file

@ -0,0 +1,874 @@
/*
Copyright (c) 2013, Fabien Meghazi
Released under the MIT license
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// 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,elif,else,call,set,tag,out,esc,raw,js,debug,log'.split(','),
WORD_REPLACEMENT: {
'and': '&&',
'or': '||',
'gt': '>',
'gte': '>=',
'lt': '<',
'lte': '<='
},
VOID_ELEMENTS: 'area,base,br,col,embed,hr,img,input,keygen,link,menuitem,meta,param,source,track,wbr'.split(','),
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) {
if (s == null) {
return '';
}
return _.escape(s);
},
markup(s) {
return new _Markup(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) {
// we want to ensure `value` is *not* a `Markup`, because markup-safe
// strings are not necessarily attributes-safe.
const attrvalue = value == null ? '' : this.html_escape(String(value));
return ` ${name}="${attrvalue}"`;
},
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;
},
get_element_sibling: function(node, dom_attr) {
// This helper keeps support for IE8 which does not
// implement DOMNode.(previous|next)ElementSibling
var sibling = node[dom_attr];
while (sibling && sibling.nodeType !== 1) {
sibling = sibling[dom_attr];
}
return sibling;
},
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 {
// avoid XMLSerializer with text node for IE
if (node.nodeType == 3) {
return node.data;
}
if (typeof XMLSerializer !== 'undefined') {
return (new XMLSerializer()).serializeToString(node);
} else {
switch(node.nodeType) {
case 1: return node.outerHTML;
case 4: return '<![CDATA[' + node.data + ']]>';
case 8: return '<!-- ' + node.data + '-->';
}
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[0] = this.markup(callback(context, new_dict));
}
return this.markup(context.engine._render(template, new_dict));
},
foreach: function(context, enu, as, old_dict, callback) {
if (enu != null) {
var index, jlen, cur;
var 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 (enu instanceof Array) {
var size = enu.length;
new_dict[as + "_size"] = size;
for (index = 0, jlen = enu.length; index < jlen; index++) {
cur = enu[index];
new_dict[as_value] = cur;
new_dict[as_index] = index;
new_dict[as_first] = index === 0;
new_dict[as_last] = index + 1 === size;
new_dict[as_parity] = (index % 2 == 1 ? 'odd' : 'even');
if (cur && 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 {
index = 0;
for (var k in enu) {
if (enu.hasOwnProperty(k)) {
cur = enu[k];
new_dict[as_value] = cur;
new_dict[as_index] = index;
new_dict[as_first] = index === 0;
new_dict[as_parity] = (index % 2 == 1 ? 'odd' : 'even');
new_dict[as] = k;
callback(context, new_dict);
index += 1;
}
}
}
_.each(Object.keys(old_dict), function(z) {
old_dict[z] = new_dict[z];
});
} 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.void_elements = QWeb2.VOID_ELEMENTS.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 a template to the engine
*
* @param {String|Document} template Template as string or url or DOM Document
* @param {Function} [callback] Called when the template is loaded, force async request
*/
add_template : function(template, callback) {
var self = this;
this.templates_resources.push(template);
if (template.constructor === String) {
return this.load_xml(template, function (err, xDoc) {
if (err) {
if (callback) {
return callback(err);
} else {
throw err;
}
}
self.add_template(xDoc, callback);
});
}
template = this.preprocess(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) {
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 + "' to create '" + name + "'");
}
this.templates[name] = this.templates[extend].cloneNode(true);
this.extend_templates[name] = (this.extend_templates[extend] || []).slice();
extend = name;
name = undefined;
}
if (name) {
this.templates[name] = node;
this.compiled_templates[name] = null;
} 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];
}
}
}
}
if (callback) {
callback(null, template);
}
return true;
},
preprocess: function(doc) {
/**
* Preprocess a template's document at load time.
* This method is mostly used for template sanitization but could
* also be overloaded for extended features such as translations, ...
* Throws an exception if a template is invalid.
*
* @param {Document} doc Document containg the loaded templates
* @return {Document} Returns the pre-processed/sanitized template
*/
var self = this;
var childs = (doc.documentElement && doc.documentElement.childNodes) || doc.childNodes || [];
// Check for load errors
for (var i = 0; i < childs.length; i++) {
var node = childs[i];
if (node.nodeType === 1 && node.nodeName == 'parsererror') {
return this.tools.exception(node.innerText);
}
}
// Sanitize t-elif and t-else directives
var tbranch = doc.querySelectorAll('[t-elif], [t-else]');
for (var i = 0, ilen = tbranch.length; i < ilen; i++) {
var node = tbranch[i];
var prev_elem = self.tools.get_element_sibling(node, 'previousSibling');
var pattr = function(name) { return prev_elem.getAttribute(name); }
var nattr = function(name) { return +!!node.getAttribute(name); }
if (prev_elem && (pattr('t-if') || pattr('t-elif'))) {
if (pattr('t-foreach')) {
return self.tools.exception("Error: t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
}
if (['t-if', 't-elif', 't-else'].map(nattr).reduce(function(a, b) { return a + b; }) > 1) {
return self.tools.exception("Error: only one conditional branching directive is allowed per node");
}
// All text nodes between branch nodes are removed
var text_node;
while ((text_node = node.previousSibling) !== prev_elem) {
if (text_node.nodeType !== 8 && self.tools.trim(text_node.nodeValue)) {
return self.tools.exception("Error: text is not allowed between branching directives");
}
// IE <= 11.0 doesn't support ChildNode.remove
text_node.parentNode.removeChild(text_node);
}
} else {
return self.tools.exception("Error: t-elif and t-else directives must be preceded by a t-if or t-elif directive");
}
}
return doc;
},
load_xml : function(s, callback) {
var self = this;
var async = !!callback;
s = this.tools.trim(s);
if (s.charAt(0) === '<') {
var tpl = this.load_xml_string(s);
if (callback) {
callback(null, tpl);
}
return tpl;
} else {
var req = this.get_xhr();
if (this.debug) {
s += '?debug=' + (new Date()).getTime(); // TODO fme: do it properly in case there's already url parameters
}
req.open('GET', s, async);
if (async) {
req.addEventListener("load", function() {
// 0, not being a valid HTTP status code, is used by browsers
// to indicate success for a non-http xhr response
// (for example, using the file:// protocol)
// https://developer.mozilla.org/fr/docs/Web/API/XMLHttpRequest
// https://bugzilla.mozilla.org/show_bug.cgi?id=331610
if (req.status == 200 || req.status == 0) {
callback(null, self._parse_from_request(req));
} else {
callback(new Error("Can't load template " + s + ", http status " + req.status));
}
});
}
req.send(null);
if (!async) {
return this._parse_from_request(req);
}
}
},
_parse_from_request: function(req) {
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") {
throw new Error("QWeb2: Could not parse document :" + 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') {
throw new Error("QWeb2: Could not parse document :" + r.body.innerText);
}
return r;
}
var xDoc;
try {
xDoc = new ActiveXObject("MSXML2.DOMDocument");
} catch (e) {
throw new Error("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) {
throw new Error("Could not get XHR");
}
},
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: template });
}
if (!tcompiled) {
this.tools.exception("Error evaluating template: (IE?)" + error, { template: template });
}
this.compiled_templates[template] = tcompiled;
return this.render(template, dict);
} else {
return this.tools.exception("Template '" + template + "' not found");
}
},
extend : function(template, extend_node) {
var jQuery = this.jQuery;
if (!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 = jQuery(jquery, template_dest);
if (!target.length && window.console) {
console.error("Can't find '" + jquery + "' when extending template " + template);
}
} else {
this.tools.exception(error_msg + "No expression given");
}
error_msg += "' (expression='" + jquery + "') : ";
if (operation) {
var allowed_operations = "append,prepend,before,after,replace,inner,attributes".split(',');
if (this.tools.arrayIndexOf(allowed_operations, operation) == -1) {
this.tools.exception(error_msg + "Invalid operation : '" + operation + "'");
}
operation = {'replace' : 'replaceWith', 'inner' : 'html'}[operation] || operation;
if (operation === 'attributes') {
jQuery('attribute', child).each(function () {
var attrib = jQuery(this);
target.attr(attrib.attr('name'), attrib.text() || attrib.attr('value'));
});
} else {
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, [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 = {tag: this.tag};
this.actions_done = [];
this.attributes = {};
this.children = [];
this._top = [];
this._bottom = [];
this._indent = 1;
this.process_children = true;
this.is_void_element = ~QWeb2.tools.arrayIndexOf(this.engine.void_elements, this.tag);
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;
}
if (name.match(/^attf?(-.*)?/)) {
this.attributes[m[0]] = attr.value;
} else {
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;
},
format_str: function (e) {
if (e == '0') {
return 'dict[0]';
}
return this.format_expression(e);
},
string_interpolation : function(s) {
var _this = this;
if (!s) {
return "''";
}
function append_literal(s) {
s && r.push(_this.engine.tools.js_escape(s));
}
var re = /#{(.+?)}|{{(.+?)}}/g, start = 0, r = [], m;
while (m = re.exec(s)) {
// extract literal string between previous and current match
append_literal(s.slice(start, re.lastIndex - m[0].length));
// extract matched expression
r.push('(' + this.format_str(m[2] || m[1]) + ')');
// update position of new matching
start = re.lastIndex;
}
// remaining text after last expression
append_literal(s.slice(start));
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 + "'");
}
}
}
},
compile_action_tag : function() {
if (this.tag.toLowerCase() !== this.engine.prefix) {
this.top_string("<" + this.tag);
for (var a in this.attributes) {
var v = this.attributes[a];
var d = a.split('-');
if (d[0] === this.engine.prefix && d.length > 1) {
if (d.length === 2) {
this.top("r.push(context.engine.tools.gen_attribute(" + (this.format_expression(v)) + "));");
} else {
this.top("r.push(context.engine.tools.gen_attribute(['" + d.slice(2).join('-') + "', (" +
(d[1] === 'att' ? this.format_expression(v) : this.string_interpolation(v)) + ")]));");
}
} else {
this.top_string(this.engine.tools.gen_attribute([a, v]));
}
}
if (this.actions.opentag === 'true' || (!this.children.length && this.is_void_element)) {
// We do not enforce empty content on void elements
// because QWeb rendering is not necessarily html.
this.top_string("/>");
} else {
this.top_string(">");
this.bottom_string("</" + this.tag + ">");
}
}
},
compile_action_if : function(value) {
this.top("if (" + (this.format_expression(value)) + ") {");
this.bottom("}");
this.indent();
},
compile_action_elif : function(value) {
this.top("else if (" + (this.format_expression(value)) + ") {");
this.bottom("}");
this.indent();
},
compile_action_else : function(value) {
this.top("else {");
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) {
if (this.children.length === 0) {
return this.top("r.push(context.engine.tools.call(context, " + (this.string_interpolation(value)) + ", dict));");
} else {
this.top("r.push(context.engine.tools.call(context, " + (this.string_interpolation(value)) + ", dict, null, function(context, dict) {");
this.bottom("}));");
this.indent();
this.top("var r = [];");
return this.bottom("return context.engine.tools.markup(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 context.engine.tools.markup(r.join(''));");
}
}
},
compile_action_esc : function(value) {
return this.compile_action_out(value);
},
compile_action_out(value) {
this.top("var t = " + this.format_str(value) + ";");
this.top("if (t != null) r.push(context.engine.tools.html_escape(t));");
this.top("else {");
this.bottom("}");
this.indent();
},
compile_action_raw : function(value) {
let e = this.node;
while (e.parentElement && !e.getAttribute('t-name')) {
e = e.parentElement;
}
console.warn(
"Found deprecated directive '@t-raw=\"%s\"' in "
+ "template '%s'. Replace by @t-out, and explicitely wrap content in "
+ "utils.Markup if necessary.", value, e.getAttribute('t-name'));
this.top("var t = " + this.format_str(value) + ";");
this.top("if (t != null) r.push(t);");
this.top("else {");
this.bottom("}");
this.indent();
},
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;
})();