diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/lib/qweb | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/lib/qweb')
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-benchmark.html | 57 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-benchmark.xml | 58 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test-attributes.xml | 83 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test-call.xml | 63 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test-conditionals.xml | 68 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test-extend.xml | 62 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test-foreach.xml | 46 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test-global.xml | 54 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test-output.xml | 42 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test-set.xml | 53 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test-utf8tags.xml | 8 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test-widgets.xml | 26 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb-test.js.html | 73 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb.js | 435 | ||||
| -rw-r--r-- | addons/web/static/lib/qweb/qweb2.js | 861 |
15 files changed, 1989 insertions, 0 deletions
diff --git a/addons/web/static/lib/qweb/qweb-benchmark.html b/addons/web/static/lib/qweb/qweb-benchmark.html new file mode 100644 index 00000000..4f7fb56a --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-benchmark.html @@ -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> + diff --git a/addons/web/static/lib/qweb/qweb-benchmark.xml b/addons/web/static/lib/qweb/qweb-benchmark.xml new file mode 100644 index 00000000..a499b4f1 --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-benchmark.xml @@ -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> + diff --git a/addons/web/static/lib/qweb/qweb-test-attributes.xml b/addons/web/static/lib/qweb/qweb-test-attributes.xml new file mode 100644 index 00000000..b599b76b --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-attributes.xml @@ -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"/> + </t> + <result id="static-void"><![CDATA[<img src="/test.jpg" alt="Test"/>]]></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="<foo" + t-att-bar="bar" + t-attf-baz="<{{baz}}>" + t-att="qux"/> + </t> + <params id="various-escapes"><![CDATA[{ + "bar": "<bar>", + "baz": "\"<baz>\"", + "qux": {"qux": "<>"} + }]]></params> + <result id="various-escapes"><![CDATA[ + <div foo="<foo" bar="<bar>" baz="<"<baz>">" qux="<>"></div> + ]]></result> +</templates> diff --git a/addons/web/static/lib/qweb/qweb-test-call.xml b/addons/web/static/lib/qweb/qweb-test-call.xml new file mode 100644 index 00000000..8cf1d1d6 --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-call.xml @@ -0,0 +1,63 @@ +<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> + + <!-- + 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> diff --git a/addons/web/static/lib/qweb/qweb-test-conditionals.xml b/addons/web/static/lib/qweb/qweb-test-conditionals.xml new file mode 100644 index 00000000..f4b95b69 --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-conditionals.xml @@ -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> diff --git a/addons/web/static/lib/qweb/qweb-test-extend.xml b/addons/web/static/lib/qweb/qweb-test-extend.xml new file mode 100644 index 00000000..f8a24c4e --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-extend.xml @@ -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> diff --git a/addons/web/static/lib/qweb/qweb-test-foreach.xml b/addons/web/static/lib/qweb/qweb-test-foreach.xml new file mode 100644 index 00000000..9a1e8ecb --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-foreach.xml @@ -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> diff --git a/addons/web/static/lib/qweb/qweb-test-global.xml b/addons/web/static/lib/qweb/qweb-test-global.xml new file mode 100644 index 00000000..8bdefc6a --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-global.xml @@ -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> diff --git a/addons/web/static/lib/qweb/qweb-test-output.xml b/addons/web/static/lib/qweb/qweb-test-output.xml new file mode 100644 index 00000000..cbe7f051 --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-output.xml @@ -0,0 +1,42 @@ +<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[<ok>]]></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-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> diff --git a/addons/web/static/lib/qweb/qweb-test-set.xml b/addons/web/static/lib/qweb/qweb-test-set.xml new file mode 100644 index 00000000..945fd4a2 --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-set.xml @@ -0,0 +1,53 @@ +<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-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> diff --git a/addons/web/static/lib/qweb/qweb-test-utf8tags.xml b/addons/web/static/lib/qweb/qweb-test-utf8tags.xml new file mode 100644 index 00000000..dd51635b --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-utf8tags.xml @@ -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> diff --git a/addons/web/static/lib/qweb/qweb-test-widgets.xml b/addons/web/static/lib/qweb/qweb-test-widgets.xml new file mode 100644 index 00000000..503880bc --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test-widgets.xml @@ -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> diff --git a/addons/web/static/lib/qweb/qweb-test.js.html b/addons/web/static/lib/qweb/qweb-test.js.html new file mode 100644 index 00000000..0ee5f27c --- /dev/null +++ b/addons/web/static/lib/qweb/qweb-test.js.html @@ -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> diff --git a/addons/web/static/lib/qweb/qweb.js b/addons/web/static/lib/qweb/qweb.js new file mode 100644 index 00000000..cd652485 --- /dev/null +++ b/addons/web/static/lib/qweb/qweb.js @@ -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, "&").replace(/</g, "<").replace(/>/g, ">"); + }, + escape_att:function(s) { + return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); + }, + 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"; + } +}; + diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js new file mode 100644 index 00000000..d2a367f1 --- /dev/null +++ b/addons/web/static/lib/qweb/qweb2.js @@ -0,0 +1,861 @@ +/* +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,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, attribute) { + if (s == null) { + return ''; + } + s = String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); + if (attribute) { + s = s.replace(/"/g, '"'); + } + return s; + }, + gen_attribute: function(o) { + if (o !== null && o !== undefined) { + if (o.constructor === Array) { + if (o[1] !== null && o[1] !== undefined) { + return this.format_attribute(o[0], o[1]); + } + } else if (typeof o === 'object') { + var r = ''; + for (var k in o) { + if (o.hasOwnProperty(k)) { + r += this.gen_attribute([k, o[k]]); + } + } + return r; + } + } + return ''; + }, + format_attribute: function(name, value) { + return ' ' + name + '="' + this.html_escape(value, true) + '"'; + }, + extend: function(dst, src, exclude) { + for (var p in src) { + if (src.hasOwnProperty(p) && !(exclude && this.arrayIndexOf(exclude, p) !== -1)) { + dst[p] = src[p]; + } + } + return dst; + }, + arrayIndexOf : function(array, item) { + for (var i = 0, ilen = array.length; i < ilen; i++) { + if (array[i] === item) { + return i; + } + } + return -1; + }, + 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] = callback(context, new_dict); + } + return 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); + } + 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.debug('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 r.join('');"); + } + }, + compile_action_set : function(value) { + var variable = this.format_expression(value); + if (this.actions['value']) { + if (this.children.length) { + this.engine.tools.warning("@set with @value plus node chidren found. Children are ignored."); + } + this.top(variable + " = (" + (this.format_expression(this.actions['value'])) + ");"); + this.process_children = false; + } else { + if (this.children.length === 0) { + this.top(variable + " = '';"); + } else if (this.children.length === 1 && this.children[0].node.nodeType === 3) { + this.top(variable + " = " + (this.engine.tools.js_escape(this.children[0].node.data)) + ";"); + this.process_children = false; + } else { + this.top(variable + " = (function(dict) {"); + this.bottom("})(dict);"); + this.indent(); + this.top("var r = [];"); + this.bottom("return r.join('');"); + } + } + }, + compile_action_esc : function(value) { + this.top("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) { + 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; +})(); |
