@odoo/owl 3.0.0-alpha.2 → 3.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +100 -110
  2. package/dist/compile_templates.mjs +2434 -2532
  3. package/dist/compiler.js +2371 -0
  4. package/dist/owl-devtools.zip +0 -0
  5. package/dist/owl.cjs.js +6709 -6616
  6. package/dist/owl.cjs.runtime.js +4480 -0
  7. package/dist/owl.es.js +6697 -6600
  8. package/dist/owl.es.runtime.js +4436 -0
  9. package/dist/owl.iife.js +6710 -6617
  10. package/dist/owl.iife.min.js +1 -1
  11. package/dist/owl.iife.runtime.js +4484 -0
  12. package/dist/owl.iife.runtime.min.js +1 -0
  13. package/dist/types/common/owl_error.d.ts +3 -3
  14. package/dist/types/common/types.d.ts +1 -29
  15. package/dist/types/common/utils.d.ts +8 -8
  16. package/dist/types/compiler/code_generator.d.ts +133 -152
  17. package/dist/types/compiler/index.d.ts +13 -13
  18. package/dist/types/compiler/inline_expressions.d.ts +41 -59
  19. package/dist/types/compiler/parser.d.ts +170 -178
  20. package/dist/types/compiler/standalone/index.d.ts +2 -2
  21. package/dist/types/compiler/standalone/setup_jsdom.d.ts +1 -1
  22. package/dist/types/index.d.ts +1 -1
  23. package/dist/types/owl.d.ts +709 -665
  24. package/dist/types/runtime/app.d.ts +55 -62
  25. package/dist/types/runtime/blockdom/attributes.d.ts +8 -6
  26. package/dist/types/runtime/blockdom/block_compiler.d.ts +21 -21
  27. package/dist/types/runtime/blockdom/config.d.ts +8 -8
  28. package/dist/types/runtime/blockdom/event_catcher.d.ts +7 -7
  29. package/dist/types/runtime/blockdom/events.d.ts +8 -8
  30. package/dist/types/runtime/blockdom/html.d.ts +17 -17
  31. package/dist/types/runtime/blockdom/index.d.ts +25 -26
  32. package/dist/types/runtime/blockdom/list.d.ts +18 -18
  33. package/dist/types/runtime/blockdom/multi.d.ts +17 -17
  34. package/dist/types/runtime/blockdom/text.d.ts +26 -26
  35. package/dist/types/runtime/blockdom/toggler.d.ts +17 -17
  36. package/dist/types/runtime/component.d.ts +17 -28
  37. package/dist/types/runtime/component_node.d.ts +59 -83
  38. package/dist/types/runtime/context.d.ts +36 -0
  39. package/dist/types/runtime/error_handling.d.ts +13 -13
  40. package/dist/types/runtime/event_handling.d.ts +1 -1
  41. package/dist/types/runtime/fibers.d.ts +37 -37
  42. package/dist/types/runtime/hooks.d.ts +32 -57
  43. package/dist/types/runtime/index.d.ts +46 -35
  44. package/dist/types/runtime/lifecycle_hooks.d.ts +10 -12
  45. package/dist/types/runtime/plugin_hooks.d.ts +6 -0
  46. package/dist/types/runtime/plugin_manager.d.ts +36 -0
  47. package/dist/types/runtime/props.d.ts +21 -0
  48. package/dist/types/runtime/reactivity/computations.d.ts +34 -0
  49. package/dist/types/runtime/reactivity/computed.d.ts +6 -0
  50. package/dist/types/runtime/reactivity/effect.d.ts +1 -0
  51. package/dist/types/runtime/reactivity/proxy.d.ts +47 -0
  52. package/dist/types/runtime/reactivity/signal.d.ts +31 -0
  53. package/dist/types/runtime/reactivity.d.ts +57 -46
  54. package/dist/types/runtime/registry.d.ts +24 -15
  55. package/dist/types/runtime/rendering/error_handling.d.ts +13 -0
  56. package/dist/types/runtime/rendering/fibers.d.ts +37 -0
  57. package/dist/types/runtime/rendering/scheduler.d.ts +21 -0
  58. package/dist/types/runtime/rendering/template_helpers.d.ts +51 -0
  59. package/dist/types/runtime/resource.d.ts +18 -0
  60. package/dist/types/runtime/scheduler.d.ts +21 -21
  61. package/dist/types/runtime/status.d.ts +11 -10
  62. package/dist/types/runtime/template_set.d.ts +36 -40
  63. package/dist/types/runtime/types.d.ts +70 -0
  64. package/dist/types/runtime/utils.d.ts +24 -25
  65. package/dist/types/runtime/validation.d.ts +19 -36
  66. package/dist/types/version.d.ts +1 -1
  67. package/package.json +45 -19
  68. package/dist/types/runtime/cancellableContext.d.ts +0 -15
  69. package/dist/types/runtime/cancellablePromise.d.ts +0 -15
  70. package/dist/types/runtime/executionContext.d.ts +0 -0
  71. package/dist/types/runtime/listOperation.d.ts +0 -1
  72. package/dist/types/runtime/plugins.d.ts +0 -39
  73. package/dist/types/runtime/relationalModel/discussModel.d.ts +0 -19
  74. package/dist/types/runtime/relationalModel/discussModelTypes.d.ts +0 -22
  75. package/dist/types/runtime/relationalModel/field.d.ts +0 -20
  76. package/dist/types/runtime/relationalModel/model.d.ts +0 -59
  77. package/dist/types/runtime/relationalModel/modelData.d.ts +0 -18
  78. package/dist/types/runtime/relationalModel/modelRegistry.d.ts +0 -3
  79. package/dist/types/runtime/relationalModel/modelUtils.d.ts +0 -4
  80. package/dist/types/runtime/relationalModel/store.d.ts +0 -16
  81. package/dist/types/runtime/relationalModel/types.d.ts +0 -83
  82. package/dist/types/runtime/relationalModel/util.d.ts +0 -1
  83. package/dist/types/runtime/relationalModel/web/WebDataPoint.d.ts +0 -25
  84. package/dist/types/runtime/relationalModel/web/WebRecord.d.ts +0 -131
  85. package/dist/types/runtime/relationalModel/web/WebStaticList.d.ts +0 -63
  86. package/dist/types/runtime/relationalModel/web/webModel.d.ts +0 -5
  87. package/dist/types/runtime/relationalModel/web/webModelTypes.d.ts +0 -139
  88. package/dist/types/runtime/signals.d.ts +0 -17
  89. package/dist/types/runtime/task.d.ts +0 -12
  90. package/dist/types/utils/registry.d.ts +0 -15
@@ -2,2552 +2,2454 @@ import { readFile, stat, readdir } from 'fs/promises';
2
2
  import path from 'path';
3
3
  import jsdom from 'jsdom';
4
4
 
5
- // -----------------------------------------------------------------------------
6
- // add global DOM stuff for compiler. Needs to be in a separate file so rollup
7
- // doesn't hoist the owl imports above this block of code.
8
- // -----------------------------------------------------------------------------
9
- var document$1 = new jsdom.JSDOM("", {});
10
- var window = document$1.window;
11
- global.document = window.document;
12
- global.window = window;
13
- global.DOMParser = window.DOMParser;
14
- global.Element = window.Element;
5
+ // -----------------------------------------------------------------------------
6
+ // add global DOM stuff for compiler. Needs to be in a separate file so rollup
7
+ // doesn't hoist the owl imports above this block of code.
8
+ // -----------------------------------------------------------------------------
9
+ var document$1 = new jsdom.JSDOM("", {});
10
+ var window = document$1.window;
11
+ global.document = window.document;
12
+ global.window = window;
13
+ global.DOMParser = window.DOMParser;
14
+ global.Element = window.Element;
15
15
  global.Node = window.Node;
16
16
 
17
- // Custom error class that wraps error that happen in the owl lifecycle
18
- class OwlError extends Error {
17
+ // Custom error class that wraps error that happen in the owl lifecycle
18
+ class OwlError extends Error {
19
+ cause;
19
20
  }
20
21
 
21
- /**
22
- * Owl QWeb Expression Parser
23
- *
24
- * Owl needs in various contexts to be able to understand the structure of a
25
- * string representing a javascript expression. The usual goal is to be able
26
- * to rewrite some variables. For example, if a template has
27
- *
28
- * ```xml
29
- * <t t-if="computeSomething({val: state.val})">...</t>
30
- * ```
31
- *
32
- * this needs to be translated in something like this:
33
- *
34
- * ```js
35
- * if (context["computeSomething"]({val: context["state"].val})) { ... }
36
- * ```
37
- *
38
- * This file contains the implementation of an extremely naive tokenizer/parser
39
- * and evaluator for javascript expressions. The supported grammar is basically
40
- * only expressive enough to understand the shape of objects, of arrays, and
41
- * various operators.
42
- */
43
- //------------------------------------------------------------------------------
44
- // Misc types, constants and helpers
45
- //------------------------------------------------------------------------------
46
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date,__globals__".split(",");
47
- const WORD_REPLACEMENT = Object.assign(Object.create(null), {
48
- and: "&&",
49
- or: "||",
50
- gt: ">",
51
- gte: ">=",
52
- lt: "<",
53
- lte: "<=",
54
- });
55
- const STATIC_TOKEN_MAP = Object.assign(Object.create(null), {
56
- "{": "LEFT_BRACE",
57
- "}": "RIGHT_BRACE",
58
- "[": "LEFT_BRACKET",
59
- "]": "RIGHT_BRACKET",
60
- ":": "COLON",
61
- ",": "COMMA",
62
- "(": "LEFT_PAREN",
63
- ")": "RIGHT_PAREN",
64
- });
65
- // note that the space after typeof is relevant. It makes sure that the formatted
66
- // expression has a space after typeof. Currently we don't support delete and void
67
- const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ,|,&,^,~".split(",");
68
- let tokenizeString = function (expr) {
69
- let s = expr[0];
70
- let start = s;
71
- if (s !== "'" && s !== '"' && s !== "`") {
72
- return false;
73
- }
74
- let i = 1;
75
- let cur;
76
- while (expr[i] && expr[i] !== start) {
77
- cur = expr[i];
78
- s += cur;
79
- if (cur === "\\") {
80
- i++;
81
- cur = expr[i];
82
- if (!cur) {
83
- throw new OwlError("Invalid expression");
84
- }
85
- s += cur;
86
- }
87
- i++;
88
- }
89
- if (expr[i] !== start) {
90
- throw new OwlError("Invalid expression");
91
- }
92
- s += start;
93
- if (start === "`") {
94
- return {
95
- type: "TEMPLATE_STRING",
96
- value: s,
97
- replace(replacer) {
98
- return s.replace(/\$\{(.*?)\}/g, (match, group) => {
99
- return "${" + replacer(group) + "}";
100
- });
101
- },
102
- };
103
- }
104
- return { type: "VALUE", value: s };
105
- };
106
- let tokenizeNumber = function (expr) {
107
- let s = expr[0];
108
- if (s && s.match(/[0-9]/)) {
109
- let i = 1;
110
- while (expr[i] && expr[i].match(/[0-9]|\./)) {
111
- s += expr[i];
112
- i++;
113
- }
114
- return { type: "VALUE", value: s };
115
- }
116
- else {
117
- return false;
118
- }
119
- };
120
- let tokenizeSymbol = function (expr) {
121
- let s = expr[0];
122
- if (s && s.match(/[a-zA-Z_\$]/)) {
123
- let i = 1;
124
- while (expr[i] && expr[i].match(/\w/)) {
125
- s += expr[i];
126
- i++;
127
- }
128
- if (s in WORD_REPLACEMENT) {
129
- return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
130
- }
131
- return { type: "SYMBOL", value: s };
132
- }
133
- else {
134
- return false;
135
- }
136
- };
137
- const tokenizeStatic = function (expr) {
138
- const char = expr[0];
139
- if (char && char in STATIC_TOKEN_MAP) {
140
- return { type: STATIC_TOKEN_MAP[char], value: char };
141
- }
142
- return false;
143
- };
144
- const tokenizeOperator = function (expr) {
145
- for (let op of OPERATORS) {
146
- if (expr.startsWith(op)) {
147
- return { type: "OPERATOR", value: op };
148
- }
149
- }
150
- return false;
151
- };
152
- const TOKENIZERS = [
153
- tokenizeString,
154
- tokenizeNumber,
155
- tokenizeOperator,
156
- tokenizeSymbol,
157
- tokenizeStatic,
158
- ];
159
- /**
160
- * Convert a javascript expression (as a string) into a list of tokens. For
161
- * example: `tokenize("1 + b")` will return:
162
- * ```js
163
- * [
164
- * {type: "VALUE", value: "1"},
165
- * {type: "OPERATOR", value: "+"},
166
- * {type: "SYMBOL", value: "b"}
167
- * ]
168
- * ```
169
- */
170
- function tokenize(expr) {
171
- const result = [];
172
- let token = true;
173
- let error;
174
- let current = expr;
175
- try {
176
- while (token) {
177
- current = current.trim();
178
- if (current) {
179
- for (let tokenizer of TOKENIZERS) {
180
- token = tokenizer(current);
181
- if (token) {
182
- result.push(token);
183
- current = current.slice(token.size || token.value.length);
184
- break;
185
- }
186
- }
187
- }
188
- else {
189
- token = false;
190
- }
191
- }
192
- }
193
- catch (e) {
194
- error = e; // Silence all errors and throw a generic error below
195
- }
196
- if (current.length || error) {
197
- throw new OwlError(`Tokenizer error: could not tokenize \`${expr}\``);
198
- }
199
- return result;
200
- }
201
- //------------------------------------------------------------------------------
202
- // Expression "evaluator"
203
- //------------------------------------------------------------------------------
204
- const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
205
- const isRightSeparator = (token) => token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
206
- /**
207
- * This is the main function exported by this file. This is the code that will
208
- * process an expression (given as a string) and returns another expression with
209
- * proper lookups in the context.
210
- *
211
- * Usually, this kind of code would be very simple to do if we had an AST (so,
212
- * if we had a javascript parser), since then, we would only need to find the
213
- * variables and replace them. However, a parser is more complicated, and there
214
- * are no standard builtin parser API.
215
- *
216
- * Since this method is applied to simple javasript expressions, and the work to
217
- * be done is actually quite simple, we actually can get away with not using a
218
- * parser, which helps with the code size.
219
- *
220
- * Here is the heuristic used by this method to determine if a token is a
221
- * variable:
222
- * - by default, all symbols are considered a variable
223
- * - unless the previous token is a dot (in that case, this is a property: `a.b`)
224
- * - or if the previous token is a left brace or a comma, and the next token is
225
- * a colon (in that case, this is an object key: `{a: b}`)
226
- *
227
- * Some specific code is also required to support arrow functions. If we detect
228
- * the arrow operator, then we add the current (or some previous tokens) token to
229
- * the list of variables so it does not get replaced by a lookup in the context
230
- */
231
- function compileExprToArray(expr) {
232
- const localVars = new Set();
233
- const tokens = tokenize(expr);
234
- let i = 0;
235
- let stack = []; // to track last opening (, [ or {
236
- while (i < tokens.length) {
237
- let token = tokens[i];
238
- let prevToken = tokens[i - 1];
239
- let nextToken = tokens[i + 1];
240
- let groupType = stack[stack.length - 1];
241
- switch (token.type) {
242
- case "LEFT_BRACE":
243
- case "LEFT_BRACKET":
244
- case "LEFT_PAREN":
245
- stack.push(token.type);
246
- break;
247
- case "RIGHT_BRACE":
248
- case "RIGHT_BRACKET":
249
- case "RIGHT_PAREN":
250
- stack.pop();
251
- }
252
- let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
253
- if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
254
- if (prevToken) {
255
- // normalize missing tokens: {a} should be equivalent to {a:a}
256
- if (groupType === "LEFT_BRACE" &&
257
- isLeftSeparator(prevToken) &&
258
- isRightSeparator(nextToken)) {
259
- tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
260
- nextToken = tokens[i + 1];
261
- }
262
- if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
263
- isVar = false;
264
- }
265
- else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
266
- if (nextToken && nextToken.type === "COLON") {
267
- isVar = false;
268
- }
269
- }
270
- }
271
- }
272
- if (token.type === "TEMPLATE_STRING") {
273
- token.value = token.replace((expr) => compileExpr(expr));
274
- }
275
- if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
276
- if (token.type === "RIGHT_PAREN") {
277
- let j = i - 1;
278
- while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
279
- if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
280
- tokens[j].value = tokens[j].originalValue;
281
- localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };
282
- }
283
- j--;
284
- }
285
- }
286
- else {
287
- localVars.add(token.value); //] = { id: token.value, expr: token.value };
288
- }
289
- }
290
- if (isVar) {
291
- token.varName = token.value;
292
- if (!localVars.has(token.value)) {
293
- token.originalValue = token.value;
294
- token.value = `ctx['${token.value}']`;
295
- }
296
- }
297
- i++;
298
- }
299
- // Mark all variables that have been used locally.
300
- // This assumes the expression has only one scope (incorrect but "good enough for now")
301
- for (const token of tokens) {
302
- if (token.type === "SYMBOL" && token.varName && localVars.has(token.value)) {
303
- token.originalValue = token.value;
304
- token.value = `_${token.value}`;
305
- token.isLocal = true;
306
- }
307
- }
308
- return tokens;
309
- }
310
- // Leading spaces are trimmed during tokenization, so they need to be added back for some values
311
- const paddedValues = new Map([["in ", " in "]]);
312
- function compileExpr(expr) {
313
- return compileExprToArray(expr)
314
- .map((t) => paddedValues.get(t.value) || t.value)
315
- .join("");
316
- }
317
- const INTERP_REGEXP = /\{\{.*?\}\}|\#\{.*?\}/g;
318
- function replaceDynamicParts(s, replacer) {
319
- let matches = s.match(INTERP_REGEXP);
320
- if (matches && matches[0].length === s.length) {
321
- return `(${replacer(s.slice(2, matches[0][0] === "{" ? -2 : -1))})`;
322
- }
323
- let r = s.replace(INTERP_REGEXP, (s) => "${" + replacer(s.slice(2, s[0] === "{" ? -2 : -1)) + "}");
324
- return "`" + r + "`";
325
- }
326
- function interpolate(s) {
327
- return replaceDynamicParts(s, compileExpr);
22
+ /**
23
+ * Owl QWeb Expression Parser
24
+ *
25
+ * Owl needs in various contexts to be able to understand the structure of a
26
+ * string representing a javascript expression. The usual goal is to be able
27
+ * to rewrite some variables. For example, if a template has
28
+ *
29
+ * ```xml
30
+ * <t t-if="computeSomething({val: state.val})">...</t>
31
+ * ```
32
+ *
33
+ * this needs to be translated in something like this:
34
+ *
35
+ * ```js
36
+ * if (context["computeSomething"]({val: context["state"].val})) { ... }
37
+ * ```
38
+ *
39
+ * This file contains the implementation of an extremely naive tokenizer/parser
40
+ * and evaluator for javascript expressions. The supported grammar is basically
41
+ * only expressive enough to understand the shape of objects, of arrays, and
42
+ * various operators.
43
+ */
44
+ //------------------------------------------------------------------------------
45
+ // Misc types, constants and helpers
46
+ //------------------------------------------------------------------------------
47
+ const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,eval,void,Math,RegExp,Array,Object,Date,__globals__".split(",");
48
+ const WORD_REPLACEMENT = Object.assign(Object.create(null), {
49
+ and: "&&",
50
+ or: "||",
51
+ gt: ">",
52
+ gte: ">=",
53
+ lt: "<",
54
+ lte: "<=",
55
+ });
56
+ const STATIC_TOKEN_MAP = Object.assign(Object.create(null), {
57
+ "{": "LEFT_BRACE",
58
+ "}": "RIGHT_BRACE",
59
+ "[": "LEFT_BRACKET",
60
+ "]": "RIGHT_BRACKET",
61
+ ":": "COLON",
62
+ ",": "COMMA",
63
+ "(": "LEFT_PAREN",
64
+ ")": "RIGHT_PAREN",
65
+ });
66
+ // note that the space after typeof is relevant. It makes sure that the formatted
67
+ // expression has a space after typeof. Currently we don't support delete and void
68
+ const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ,|,&,^,~".split(",");
69
+ let tokenizeString = function (expr) {
70
+ let s = expr[0];
71
+ let start = s;
72
+ if (s !== "'" && s !== '"' && s !== "`") {
73
+ return false;
74
+ }
75
+ let i = 1;
76
+ let cur;
77
+ while (expr[i] && expr[i] !== start) {
78
+ cur = expr[i];
79
+ s += cur;
80
+ if (cur === "\\") {
81
+ i++;
82
+ cur = expr[i];
83
+ if (!cur) {
84
+ throw new OwlError("Invalid expression");
85
+ }
86
+ s += cur;
87
+ }
88
+ i++;
89
+ }
90
+ if (expr[i] !== start) {
91
+ throw new OwlError("Invalid expression");
92
+ }
93
+ s += start;
94
+ if (start === "`") {
95
+ return {
96
+ type: "TEMPLATE_STRING",
97
+ value: s,
98
+ replace(replacer) {
99
+ return s.replace(/\$\{(.*?)\}/g, (match, group) => {
100
+ return "${" + replacer(group) + "}";
101
+ });
102
+ },
103
+ };
104
+ }
105
+ return { type: "VALUE", value: s };
106
+ };
107
+ let tokenizeNumber = function (expr) {
108
+ let s = expr[0];
109
+ if (s && s.match(/[0-9]/)) {
110
+ let i = 1;
111
+ while (expr[i] && expr[i].match(/[0-9]|\./)) {
112
+ s += expr[i];
113
+ i++;
114
+ }
115
+ return { type: "VALUE", value: s };
116
+ }
117
+ else {
118
+ return false;
119
+ }
120
+ };
121
+ let tokenizeSymbol = function (expr) {
122
+ let s = expr[0];
123
+ if (s && s.match(/[a-zA-Z_\$]/)) {
124
+ let i = 1;
125
+ while (expr[i] && expr[i].match(/[\w\$]/)) {
126
+ s += expr[i];
127
+ i++;
128
+ }
129
+ if (s in WORD_REPLACEMENT) {
130
+ return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
131
+ }
132
+ return { type: "SYMBOL", value: s };
133
+ }
134
+ else {
135
+ return false;
136
+ }
137
+ };
138
+ const tokenizeStatic = function (expr) {
139
+ const char = expr[0];
140
+ if (char && char in STATIC_TOKEN_MAP) {
141
+ return { type: STATIC_TOKEN_MAP[char], value: char };
142
+ }
143
+ return false;
144
+ };
145
+ const tokenizeOperator = function (expr) {
146
+ for (let op of OPERATORS) {
147
+ if (expr.startsWith(op)) {
148
+ return { type: "OPERATOR", value: op };
149
+ }
150
+ }
151
+ return false;
152
+ };
153
+ const TOKENIZERS = [
154
+ tokenizeString,
155
+ tokenizeNumber,
156
+ tokenizeOperator,
157
+ tokenizeSymbol,
158
+ tokenizeStatic,
159
+ ];
160
+ /**
161
+ * Convert a javascript expression (as a string) into a list of tokens. For
162
+ * example: `tokenize("1 + b")` will return:
163
+ * ```js
164
+ * [
165
+ * {type: "VALUE", value: "1"},
166
+ * {type: "OPERATOR", value: "+"},
167
+ * {type: "SYMBOL", value: "b"}
168
+ * ]
169
+ * ```
170
+ */
171
+ function tokenize(expr) {
172
+ const result = [];
173
+ let token = true;
174
+ let error;
175
+ let current = expr;
176
+ try {
177
+ while (token) {
178
+ current = current.trim();
179
+ if (current) {
180
+ for (let tokenizer of TOKENIZERS) {
181
+ token = tokenizer(current);
182
+ if (token) {
183
+ result.push(token);
184
+ current = current.slice(token.size || token.value.length);
185
+ break;
186
+ }
187
+ }
188
+ }
189
+ else {
190
+ token = false;
191
+ }
192
+ }
193
+ }
194
+ catch (e) {
195
+ error = e; // Silence all errors and throw a generic error below
196
+ }
197
+ if (current.length || error) {
198
+ throw new OwlError(`Tokenizer error: could not tokenize \`${expr}\``);
199
+ }
200
+ return result;
201
+ }
202
+ //------------------------------------------------------------------------------
203
+ // Expression "evaluator"
204
+ //------------------------------------------------------------------------------
205
+ const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
206
+ const isRightSeparator = (token) => token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
207
+ /**
208
+ * This is the main function exported by this file. This is the code that will
209
+ * process an expression (given as a string) and returns another expression with
210
+ * proper lookups in the context.
211
+ *
212
+ * Usually, this kind of code would be very simple to do if we had an AST (so,
213
+ * if we had a javascript parser), since then, we would only need to find the
214
+ * variables and replace them. However, a parser is more complicated, and there
215
+ * are no standard builtin parser API.
216
+ *
217
+ * Since this method is applied to simple javasript expressions, and the work to
218
+ * be done is actually quite simple, we actually can get away with not using a
219
+ * parser, which helps with the code size.
220
+ *
221
+ * Here is the heuristic used by this method to determine if a token is a
222
+ * variable:
223
+ * - by default, all symbols are considered a variable
224
+ * - unless the previous token is a dot (in that case, this is a property: `a.b`)
225
+ * - or if the previous token is a left brace or a comma, and the next token is
226
+ * a colon (in that case, this is an object key: `{a: b}`)
227
+ *
228
+ * Some specific code is also required to support arrow functions. If we detect
229
+ * the arrow operator, then we add the current (or some previous tokens) token to
230
+ * the list of variables so it does not get replaced by a lookup in the context
231
+ */
232
+ // Leading spaces are trimmed during tokenization, so they need to be added back for some values
233
+ const paddedValues = new Map([["in ", " in "]]);
234
+ /**
235
+ * Processes a javascript expression: compiles variable lookups and detects
236
+ * top-level arrow functions with their free variables, all in a single pass.
237
+ */
238
+ function processExpr(expr) {
239
+ const localVars = new Set();
240
+ const tokens = tokenize(expr);
241
+ let i = 0;
242
+ let stack = []; // to track last opening (, [ or {
243
+ let topLevelArrowIndex = -1;
244
+ while (i < tokens.length) {
245
+ let token = tokens[i];
246
+ let prevToken = tokens[i - 1];
247
+ let nextToken = tokens[i + 1];
248
+ let groupType = stack[stack.length - 1];
249
+ switch (token.type) {
250
+ case "LEFT_BRACE":
251
+ case "LEFT_BRACKET":
252
+ case "LEFT_PAREN":
253
+ stack.push(token.type);
254
+ break;
255
+ case "RIGHT_BRACE":
256
+ case "RIGHT_BRACKET":
257
+ case "RIGHT_PAREN":
258
+ stack.pop();
259
+ }
260
+ let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
261
+ if (isVar) {
262
+ if (prevToken) {
263
+ // normalize missing tokens: {a} should be equivalent to {a:a}
264
+ if (groupType === "LEFT_BRACE" &&
265
+ isLeftSeparator(prevToken) &&
266
+ isRightSeparator(nextToken)) {
267
+ tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
268
+ nextToken = tokens[i + 1];
269
+ }
270
+ if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
271
+ isVar = false;
272
+ }
273
+ else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
274
+ if (nextToken && nextToken.type === "COLON") {
275
+ isVar = false;
276
+ }
277
+ }
278
+ }
279
+ }
280
+ if (token.type === "TEMPLATE_STRING") {
281
+ token.value = token.replace((expr) => compileExpr(expr));
282
+ }
283
+ if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
284
+ if (stack.length === 0) {
285
+ topLevelArrowIndex = i + 1;
286
+ }
287
+ if (token.type === "RIGHT_PAREN") {
288
+ let j = i - 1;
289
+ while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
290
+ if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
291
+ tokens[j].value = tokens[j].originalValue;
292
+ localVars.add(tokens[j].value);
293
+ }
294
+ j--;
295
+ }
296
+ }
297
+ else {
298
+ localVars.add(token.value);
299
+ }
300
+ }
301
+ if (isVar) {
302
+ token.varName = token.value;
303
+ if (!localVars.has(token.value)) {
304
+ token.originalValue = token.value;
305
+ token.value = `ctx['${token.value}']`;
306
+ }
307
+ }
308
+ i++;
309
+ }
310
+ // Mark all variables that have been used locally.
311
+ // This assumes the expression has only one scope (incorrect but "good enough for now")
312
+ for (const token of tokens) {
313
+ if (token.type === "SYMBOL" && token.varName && localVars.has(token.value)) {
314
+ token.originalValue = token.value;
315
+ token.value = `_${token.value}`;
316
+ token.isLocal = true;
317
+ }
318
+ }
319
+ // Collect free variables from arrow function body
320
+ let freeVariables = null;
321
+ if (topLevelArrowIndex !== -1) {
322
+ freeVariables = [];
323
+ const seen = new Set();
324
+ for (let i = topLevelArrowIndex + 1; i < tokens.length; i++) {
325
+ const t = tokens[i];
326
+ if (t.varName && !t.isLocal && t.varName !== "this" && !seen.has(t.varName)) {
327
+ seen.add(t.varName);
328
+ freeVariables.push(t.varName);
329
+ }
330
+ }
331
+ }
332
+ const compiled = tokens.map((t) => paddedValues.get(t.value) || t.value).join("");
333
+ return { expr: compiled, freeVariables };
334
+ }
335
+ function compileExpr(expr) {
336
+ return processExpr(expr).expr;
337
+ }
338
+ const INTERP_REGEXP = /\{\{.*?\}\}|\#\{.*?\}/g;
339
+ function replaceDynamicParts(s, replacer) {
340
+ let matches = s.match(INTERP_REGEXP);
341
+ if (matches && matches[0].length === s.length) {
342
+ return `(${replacer(s.slice(2, matches[0][0] === "{" ? -2 : -1))})`;
343
+ }
344
+ let r = s.replace(INTERP_REGEXP, (s) => "${" + replacer(s.slice(2, s[0] === "{" ? -2 : -1)) + "}");
345
+ return "`" + r + "`";
346
+ }
347
+ function interpolate(s) {
348
+ return replaceDynamicParts(s, compileExpr);
328
349
  }
329
350
 
330
- const whitespaceRE = /\s+/g;
331
- // using a non-html document so that <inner/outer>HTML serializes as XML instead
332
- // of HTML (as we will parse it as xml later)
333
- const xmlDoc = document.implementation.createDocument(null, null, null);
334
- const MODS = new Set(["stop", "capture", "prevent", "self", "synthetic"]);
335
- let nextDataIds = {};
336
- function generateId(prefix = "") {
337
- nextDataIds[prefix] = (nextDataIds[prefix] || 0) + 1;
338
- return prefix + nextDataIds[prefix];
339
- }
340
- function isProp(tag, key) {
341
- switch (tag) {
342
- case "input":
343
- return (key === "checked" ||
344
- key === "indeterminate" ||
345
- key === "value" ||
346
- key === "readonly" ||
347
- key === "readOnly" ||
348
- key === "disabled");
349
- case "option":
350
- return key === "selected" || key === "disabled";
351
- case "textarea":
352
- return key === "value" || key === "readonly" || key === "readOnly" || key === "disabled";
353
- case "select":
354
- return key === "value" || key === "disabled";
355
- case "button":
356
- case "optgroup":
357
- return key === "disabled";
358
- }
359
- return false;
360
- }
361
- /**
362
- * Returns a template literal that evaluates to str. You can add interpolation
363
- * sigils into the string if required
364
- */
365
- function toStringExpression(str) {
366
- return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
367
- }
368
- // -----------------------------------------------------------------------------
369
- // BlockDescription
370
- // -----------------------------------------------------------------------------
371
- class BlockDescription {
372
- constructor(target, type) {
373
- this.dynamicTagName = null;
374
- this.isRoot = false;
375
- this.hasDynamicChildren = false;
376
- this.children = [];
377
- this.data = [];
378
- this.childNumber = 0;
379
- this.parentVar = "";
380
- this.id = BlockDescription.nextBlockId++;
381
- this.varName = "b" + this.id;
382
- this.blockName = "block" + this.id;
383
- this.target = target;
384
- this.type = type;
385
- }
386
- insertData(str, prefix = "d") {
387
- const id = generateId(prefix);
388
- this.target.addLine(`let ${id} = ${str};`);
389
- return this.data.push(id) - 1;
390
- }
391
- insert(dom) {
392
- if (this.currentDom) {
393
- this.currentDom.appendChild(dom);
394
- }
395
- else {
396
- this.dom = dom;
397
- }
398
- }
399
- generateExpr(expr) {
400
- if (this.type === "block") {
401
- const hasChildren = this.children.length;
402
- let params = this.data.length ? `[${this.data.join(", ")}]` : hasChildren ? "[]" : "";
403
- if (hasChildren) {
404
- params += ", [" + this.children.map((c) => c.varName).join(", ") + "]";
405
- }
406
- if (this.dynamicTagName) {
407
- return `toggler(${this.dynamicTagName}, ${this.blockName}(${this.dynamicTagName})(${params}))`;
408
- }
409
- return `${this.blockName}(${params})`;
410
- }
411
- else if (this.type === "list") {
412
- return `list(c_block${this.id})`;
413
- }
414
- return expr;
415
- }
416
- asXmlString() {
417
- // Can't use outerHTML on text/comment nodes
418
- // append dom to any element and use innerHTML instead
419
- const t = xmlDoc.createElement("t");
420
- t.appendChild(this.dom);
421
- return t.innerHTML;
422
- }
423
- }
424
- BlockDescription.nextBlockId = 1;
425
- function createContext(parentCtx, params) {
426
- return Object.assign({
427
- block: null,
428
- index: 0,
429
- forceNewBlock: true,
430
- translate: parentCtx.translate,
431
- translationCtx: parentCtx.translationCtx,
432
- tKeyExpr: null,
433
- nameSpace: parentCtx.nameSpace,
434
- tModelSelectedExpr: parentCtx.tModelSelectedExpr,
435
- }, params);
436
- }
437
- class CodeTarget {
438
- constructor(name, on) {
439
- this.indentLevel = 0;
440
- this.loopLevel = 0;
441
- this.code = [];
442
- this.hasRoot = false;
443
- this.hasCache = false;
444
- this.shouldProtectScope = false;
445
- this.hasRefWrapper = false;
446
- this.name = name;
447
- this.on = on || null;
448
- }
449
- addLine(line, idx) {
450
- const prefix = new Array(this.indentLevel + 2).join(" ");
451
- if (idx === undefined) {
452
- this.code.push(prefix + line);
453
- }
454
- else {
455
- this.code.splice(idx, 0, prefix + line);
456
- }
457
- }
458
- generateCode() {
459
- let result = [];
460
- result.push(`function ${this.name}(ctx, node, key = "") {`);
461
- if (this.shouldProtectScope) {
462
- result.push(` ctx = Object.create(ctx);`);
463
- result.push(` ctx[isBoundary] = 1`);
464
- }
465
- if (this.hasRefWrapper) {
466
- result.push(` let refWrapper = makeRefWrapper(this.__owl__);`);
467
- }
468
- if (this.hasCache) {
469
- result.push(` let cache = ctx.cache || {};`);
470
- result.push(` let nextCache = ctx.cache = {};`);
471
- }
472
- for (let line of this.code) {
473
- result.push(line);
474
- }
475
- if (!this.hasRoot) {
476
- result.push(`return text('');`);
477
- }
478
- result.push(`}`);
479
- return result.join("\n ");
480
- }
481
- currentKey(ctx) {
482
- let key = this.loopLevel ? `key${this.loopLevel}` : "key";
483
- if (ctx.tKeyExpr) {
484
- key = `${ctx.tKeyExpr} + ${key}`;
485
- }
486
- return key;
487
- }
488
- }
489
- const TRANSLATABLE_ATTRS = [
490
- "alt",
491
- "aria-label",
492
- "aria-placeholder",
493
- "aria-roledescription",
494
- "aria-valuetext",
495
- "label",
496
- "placeholder",
497
- "title",
498
- ];
499
- const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
500
- class CodeGenerator {
501
- constructor(ast, options) {
502
- this.blocks = [];
503
- this.nextBlockId = 1;
504
- this.isDebug = false;
505
- this.targets = [];
506
- this.target = new CodeTarget("template");
507
- this.translatableAttributes = TRANSLATABLE_ATTRS;
508
- this.staticDefs = [];
509
- this.slotNames = new Set();
510
- this.helpers = new Set();
511
- this.translateFn = options.translateFn || ((s) => s);
512
- if (options.translatableAttributes) {
513
- const attrs = new Set(TRANSLATABLE_ATTRS);
514
- for (let attr of options.translatableAttributes) {
515
- if (attr.startsWith("-")) {
516
- attrs.delete(attr.slice(1));
517
- }
518
- else {
519
- attrs.add(attr);
520
- }
521
- }
522
- this.translatableAttributes = [...attrs];
523
- }
524
- this.hasSafeContext = options.hasSafeContext || false;
525
- this.dev = options.dev || false;
526
- this.ast = ast;
527
- this.templateName = options.name;
528
- if (options.hasGlobalValues) {
529
- this.helpers.add("__globals__");
530
- }
531
- }
532
- generateCode() {
533
- const ast = this.ast;
534
- this.isDebug = ast.type === 12 /* TDebug */;
535
- BlockDescription.nextBlockId = 1;
536
- nextDataIds = {};
537
- this.compileAST(ast, {
538
- block: null,
539
- index: 0,
540
- forceNewBlock: false,
541
- isLast: true,
542
- translate: true,
543
- translationCtx: "",
544
- tKeyExpr: null,
545
- });
546
- // define blocks and utility functions
547
- let mainCode = [` let { text, createBlock, list, multi, html, toggler, comment } = bdom;`];
548
- if (this.helpers.size) {
549
- mainCode.push(`let { ${[...this.helpers].join(", ")} } = helpers;`);
550
- }
551
- if (this.templateName) {
552
- mainCode.push(`// Template name: "${this.templateName}"`);
553
- }
554
- for (let { id, expr } of this.staticDefs) {
555
- mainCode.push(`const ${id} = ${expr};`);
556
- }
557
- // define all blocks
558
- if (this.blocks.length) {
559
- mainCode.push(``);
560
- for (let block of this.blocks) {
561
- if (block.dom) {
562
- let xmlString = toStringExpression(block.asXmlString());
563
- if (block.dynamicTagName) {
564
- xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
565
- xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
566
- mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
567
- }
568
- else {
569
- mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
570
- }
571
- }
572
- }
573
- }
574
- // define all slots/defaultcontent function
575
- if (this.targets.length) {
576
- for (let fn of this.targets) {
577
- mainCode.push("");
578
- mainCode = mainCode.concat(fn.generateCode());
579
- }
580
- }
581
- // generate main code
582
- mainCode.push("");
583
- mainCode = mainCode.concat("return " + this.target.generateCode());
584
- const code = mainCode.join("\n ");
585
- if (this.isDebug) {
586
- const msg = `[Owl Debug]\n${code}`;
587
- console.log(msg);
588
- }
589
- return code;
590
- }
591
- compileInNewTarget(prefix, ast, ctx, on) {
592
- const name = generateId(prefix);
593
- const initialTarget = this.target;
594
- const target = new CodeTarget(name, on);
595
- this.targets.push(target);
596
- this.target = target;
597
- this.compileAST(ast, createContext(ctx));
598
- this.target = initialTarget;
599
- return name;
600
- }
601
- addLine(line, idx) {
602
- this.target.addLine(line, idx);
603
- }
604
- define(varName, expr) {
605
- this.addLine(`const ${varName} = ${expr};`);
606
- }
607
- insertAnchor(block, index = block.children.length) {
608
- const tag = `block-child-${index}`;
609
- const anchor = xmlDoc.createElement(tag);
610
- block.insert(anchor);
611
- }
612
- createBlock(parentBlock, type, ctx) {
613
- const hasRoot = this.target.hasRoot;
614
- const block = new BlockDescription(this.target, type);
615
- if (!hasRoot) {
616
- this.target.hasRoot = true;
617
- block.isRoot = true;
618
- }
619
- if (parentBlock) {
620
- parentBlock.children.push(block);
621
- if (parentBlock.type === "list") {
622
- block.parentVar = `c_block${parentBlock.id}`;
623
- }
624
- }
625
- return block;
626
- }
627
- insertBlock(expression, block, ctx) {
628
- let blockExpr = block.generateExpr(expression);
629
- if (block.parentVar) {
630
- let key = this.target.currentKey(ctx);
631
- this.helpers.add("withKey");
632
- this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${key});`);
633
- return;
634
- }
635
- if (ctx.tKeyExpr) {
636
- blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;
637
- }
638
- if (block.isRoot) {
639
- if (this.target.on) {
640
- blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
641
- }
642
- this.addLine(`return ${blockExpr};`);
643
- }
644
- else {
645
- this.define(block.varName, blockExpr);
646
- }
647
- }
648
- /**
649
- * Captures variables that are used inside of an expression. This is useful
650
- * because in compiled code, almost all variables are accessed through the ctx
651
- * object. In the case of functions, that lookup in the context can be delayed
652
- * which can cause issues if the value has changed since the function was
653
- * defined.
654
- *
655
- * @param expr the expression to capture
656
- * @param forceCapture whether the expression should capture its scope even if
657
- * it doesn't contain a function. Useful when the expression will be used as
658
- * a function body.
659
- * @returns a new expression that uses the captured values
660
- */
661
- captureExpression(expr, forceCapture = false) {
662
- if (!forceCapture && !expr.includes("=>")) {
663
- return compileExpr(expr);
664
- }
665
- const tokens = compileExprToArray(expr);
666
- const mapping = new Map();
667
- return tokens
668
- .map((tok) => {
669
- if (tok.varName && !tok.isLocal) {
670
- if (!mapping.has(tok.varName)) {
671
- const varId = generateId("v");
672
- mapping.set(tok.varName, varId);
673
- this.define(varId, tok.value);
674
- }
675
- tok.value = mapping.get(tok.varName);
676
- }
677
- return tok.value;
678
- })
679
- .join("");
680
- }
681
- translate(str, translationCtx) {
682
- const match = translationRE.exec(str);
683
- return match[1] + this.translateFn(match[2], translationCtx) + match[3];
684
- }
685
- /**
686
- * @returns the newly created block name, if any
687
- */
688
- compileAST(ast, ctx) {
689
- switch (ast.type) {
690
- case 1 /* Comment */:
691
- return this.compileComment(ast, ctx);
692
- case 0 /* Text */:
693
- return this.compileText(ast, ctx);
694
- case 2 /* DomNode */:
695
- return this.compileTDomNode(ast, ctx);
696
- case 4 /* TEsc */:
697
- return this.compileTEsc(ast, ctx);
698
- case 8 /* TOut */:
699
- return this.compileTOut(ast, ctx);
700
- case 5 /* TIf */:
701
- return this.compileTIf(ast, ctx);
702
- case 9 /* TForEach */:
703
- return this.compileTForeach(ast, ctx);
704
- case 10 /* TKey */:
705
- return this.compileTKey(ast, ctx);
706
- case 3 /* Multi */:
707
- return this.compileMulti(ast, ctx);
708
- case 7 /* TCall */:
709
- return this.compileTCall(ast, ctx);
710
- case 15 /* TCallBlock */:
711
- return this.compileTCallBlock(ast, ctx);
712
- case 6 /* TSet */:
713
- return this.compileTSet(ast, ctx);
714
- case 11 /* TComponent */:
715
- return this.compileComponent(ast, ctx);
716
- case 12 /* TDebug */:
717
- return this.compileDebug(ast, ctx);
718
- case 13 /* TLog */:
719
- return this.compileLog(ast, ctx);
720
- case 14 /* TSlot */:
721
- return this.compileTSlot(ast, ctx);
722
- case 16 /* TTranslation */:
723
- return this.compileTTranslation(ast, ctx);
724
- case 17 /* TTranslationContext */:
725
- return this.compileTTranslationContext(ast, ctx);
726
- case 18 /* TPortal */:
727
- return this.compileTPortal(ast, ctx);
728
- }
729
- }
730
- compileDebug(ast, ctx) {
731
- this.addLine(`debugger;`);
732
- if (ast.content) {
733
- return this.compileAST(ast.content, ctx);
734
- }
735
- return null;
736
- }
737
- compileLog(ast, ctx) {
738
- this.addLine(`console.log(${compileExpr(ast.expr)});`);
739
- if (ast.content) {
740
- return this.compileAST(ast.content, ctx);
741
- }
742
- return null;
743
- }
744
- compileComment(ast, ctx) {
745
- let { block, forceNewBlock } = ctx;
746
- const isNewBlock = !block || forceNewBlock;
747
- if (isNewBlock) {
748
- block = this.createBlock(block, "comment", ctx);
749
- this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
750
- ...ctx,
751
- forceNewBlock: forceNewBlock && !block,
752
- });
753
- }
754
- else {
755
- const text = xmlDoc.createComment(ast.value);
756
- block.insert(text);
757
- }
758
- return block.varName;
759
- }
760
- compileText(ast, ctx) {
761
- let { block, forceNewBlock } = ctx;
762
- let value = ast.value;
763
- if (value && ctx.translate !== false) {
764
- value = this.translate(value, ctx.translationCtx);
765
- }
766
- if (!ctx.inPreTag) {
767
- value = value.replace(whitespaceRE, " ");
768
- }
769
- if (!block || forceNewBlock) {
770
- block = this.createBlock(block, "text", ctx);
771
- this.insertBlock(`text(${toStringExpression(value)})`, block, {
772
- ...ctx,
773
- forceNewBlock: forceNewBlock && !block,
774
- });
775
- }
776
- else {
777
- const createFn = ast.type === 0 /* Text */ ? xmlDoc.createTextNode : xmlDoc.createComment;
778
- block.insert(createFn.call(xmlDoc, value));
779
- }
780
- return block.varName;
781
- }
782
- generateHandlerCode(rawEvent, handler) {
783
- const modifiers = rawEvent
784
- .split(".")
785
- .slice(1)
786
- .map((m) => {
787
- if (!MODS.has(m)) {
788
- throw new OwlError(`Unknown event modifier: '${m}'`);
789
- }
790
- return `"${m}"`;
791
- });
792
- let modifiersCode = "";
793
- if (modifiers.length) {
794
- modifiersCode = `${modifiers.join(",")}, `;
795
- }
796
- return `[${modifiersCode}${this.captureExpression(handler)}, ctx]`;
797
- }
798
- compileTDomNode(ast, ctx) {
799
- var _a;
800
- let { block, forceNewBlock } = ctx;
801
- const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;
802
- let codeIdx = this.target.code.length;
803
- if (isNewBlock) {
804
- if ((ast.dynamicTag || ctx.tKeyExpr || ast.ns) && ctx.block) {
805
- this.insertAnchor(ctx.block);
806
- }
807
- block = this.createBlock(block, "block", ctx);
808
- this.blocks.push(block);
809
- if (ast.dynamicTag) {
810
- const tagExpr = generateId("tag");
811
- this.define(tagExpr, compileExpr(ast.dynamicTag));
812
- block.dynamicTagName = tagExpr;
813
- }
814
- }
815
- // attributes
816
- const attrs = {};
817
- for (let key in ast.attrs) {
818
- let expr, attrName;
819
- if (key.startsWith("t-attf")) {
820
- expr = interpolate(ast.attrs[key]);
821
- const idx = block.insertData(expr, "attr");
822
- attrName = key.slice(7);
823
- attrs["block-attribute-" + idx] = attrName;
824
- }
825
- else if (key.startsWith("t-att")) {
826
- attrName = key === "t-att" ? null : key.slice(6);
827
- expr = compileExpr(ast.attrs[key]);
828
- if (attrName && isProp(ast.tag, attrName)) {
829
- if (attrName === "readonly") {
830
- // the property has a different name than the attribute
831
- attrName = "readOnly";
832
- }
833
- // we force a new string or new boolean to bypass the equality check in blockdom when patching same value
834
- if (attrName === "value") {
835
- // When the expression is falsy (except 0), fall back to an empty string
836
- expr = `new String((${expr}) === 0 ? 0 : ((${expr}) || ""))`;
837
- }
838
- else {
839
- expr = `new Boolean(${expr})`;
840
- }
841
- const idx = block.insertData(expr, "prop");
842
- attrs[`block-property-${idx}`] = attrName;
843
- }
844
- else {
845
- const idx = block.insertData(expr, "attr");
846
- if (key === "t-att") {
847
- attrs[`block-attributes`] = String(idx);
848
- }
849
- else {
850
- attrs[`block-attribute-${idx}`] = attrName;
851
- }
852
- }
853
- }
854
- else if (this.translatableAttributes.includes(key)) {
855
- const attrTranslationCtx = ((_a = ast.attrsTranslationCtx) === null || _a === void 0 ? void 0 : _a[key]) || ctx.translationCtx;
856
- attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
857
- }
858
- else {
859
- expr = `"${ast.attrs[key]}"`;
860
- attrName = key;
861
- attrs[key] = ast.attrs[key];
862
- }
863
- if (attrName === "value" && ctx.tModelSelectedExpr) {
864
- let selectedId = block.insertData(`${ctx.tModelSelectedExpr} === ${expr}`, "attr");
865
- attrs[`block-attribute-${selectedId}`] = "selected";
866
- }
867
- }
868
- // t-model
869
- let tModelSelectedExpr;
870
- if (ast.model) {
871
- const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;
872
- const baseExpression = compileExpr(baseExpr);
873
- const bExprId = generateId("bExpr");
874
- this.define(bExprId, baseExpression);
875
- const expression = compileExpr(expr);
876
- const exprId = generateId("expr");
877
- this.define(exprId, expression);
878
- const fullExpression = `${bExprId}[${exprId}]`;
879
- let idx;
880
- if (specialInitTargetAttr) {
881
- let targetExpr = targetAttr in attrs && `'${attrs[targetAttr]}'`;
882
- if (!targetExpr && ast.attrs) {
883
- // look at the dynamic attribute counterpart
884
- const dynamicTgExpr = ast.attrs[`t-att-${targetAttr}`];
885
- if (dynamicTgExpr) {
886
- targetExpr = compileExpr(dynamicTgExpr);
887
- }
888
- }
889
- idx = block.insertData(`${fullExpression} === ${targetExpr}`, "prop");
890
- attrs[`block-property-${idx}`] = specialInitTargetAttr;
891
- }
892
- else if (hasDynamicChildren) {
893
- const bValueId = generateId("bValue");
894
- tModelSelectedExpr = `${bValueId}`;
895
- this.define(tModelSelectedExpr, fullExpression);
896
- }
897
- else {
898
- idx = block.insertData(`${fullExpression}`, "prop");
899
- attrs[`block-property-${idx}`] = targetAttr;
900
- }
901
- this.helpers.add("toNumber");
902
- let valueCode = `ev.target.${targetAttr}`;
903
- valueCode = shouldTrim ? `${valueCode}.trim()` : valueCode;
904
- valueCode = shouldNumberize ? `toNumber(${valueCode})` : valueCode;
905
- const handler = `[(ev) => { ${fullExpression} = ${valueCode}; }]`;
906
- idx = block.insertData(handler, "hdlr");
907
- attrs[`block-handler-${idx}`] = eventType;
908
- }
909
- // event handlers
910
- for (let ev in ast.on) {
911
- const name = this.generateHandlerCode(ev, ast.on[ev]);
912
- const idx = block.insertData(name, "hdlr");
913
- attrs[`block-handler-${idx}`] = ev;
914
- }
915
- // t-ref
916
- if (ast.ref) {
917
- if (this.dev) {
918
- this.helpers.add("makeRefWrapper");
919
- this.target.hasRefWrapper = true;
920
- }
921
- const isDynamic = INTERP_REGEXP.test(ast.ref);
922
- let name = `\`${ast.ref}\``;
923
- if (isDynamic) {
924
- name = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
925
- }
926
- let setRefStr = `(el) => this.__owl__.setRef((${name}), el)`;
927
- if (this.dev) {
928
- setRefStr = `refWrapper(${name}, ${setRefStr})`;
929
- }
930
- const idx = block.insertData(setRefStr, "ref");
931
- attrs["block-ref"] = String(idx);
932
- }
933
- const nameSpace = ast.ns || ctx.nameSpace;
934
- const dom = nameSpace
935
- ? xmlDoc.createElementNS(nameSpace, ast.tag)
936
- : xmlDoc.createElement(ast.tag);
937
- for (const [attr, val] of Object.entries(attrs)) {
938
- if (!(attr === "class" && val === "")) {
939
- dom.setAttribute(attr, val);
940
- }
941
- }
942
- block.insert(dom);
943
- if (ast.content.length) {
944
- const initialDom = block.currentDom;
945
- block.currentDom = dom;
946
- const children = ast.content;
947
- for (let i = 0; i < children.length; i++) {
948
- const child = ast.content[i];
949
- const subCtx = createContext(ctx, {
950
- block,
951
- index: block.childNumber,
952
- forceNewBlock: false,
953
- isLast: ctx.isLast && i === children.length - 1,
954
- tKeyExpr: ctx.tKeyExpr,
955
- nameSpace,
956
- tModelSelectedExpr,
957
- inPreTag: ctx.inPreTag || ast.tag === "pre",
958
- });
959
- this.compileAST(child, subCtx);
960
- }
961
- block.currentDom = initialDom;
962
- }
963
- if (isNewBlock) {
964
- this.insertBlock(`${block.blockName}(ddd)`, block, ctx);
965
- // may need to rewrite code!
966
- if (block.children.length && block.hasDynamicChildren) {
967
- const code = this.target.code;
968
- const children = block.children.slice();
969
- let current = children.shift();
970
- for (let i = codeIdx; i < code.length; i++) {
971
- if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
972
- code[i] = code[i].replace(`const ${current.varName}`, current.varName);
973
- current = children.shift();
974
- if (!current)
975
- break;
976
- }
977
- }
978
- this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
979
- }
980
- }
981
- return block.varName;
982
- }
983
- compileTEsc(ast, ctx) {
984
- let { block, forceNewBlock } = ctx;
985
- let expr;
986
- if (ast.expr === "0") {
987
- this.helpers.add("zero");
988
- expr = `ctx[zero]`;
989
- }
990
- else {
991
- expr = compileExpr(ast.expr);
992
- if (ast.defaultValue) {
993
- this.helpers.add("withDefault");
994
- // FIXME: defaultValue is not translated
995
- expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
996
- }
997
- }
998
- if (!block || forceNewBlock) {
999
- block = this.createBlock(block, "text", ctx);
1000
- this.insertBlock(`text(${expr})`, block, { ...ctx, forceNewBlock: forceNewBlock && !block });
1001
- }
1002
- else {
1003
- const idx = block.insertData(expr, "txt");
1004
- const text = xmlDoc.createElement(`block-text-${idx}`);
1005
- block.insert(text);
1006
- }
1007
- return block.varName;
1008
- }
1009
- compileTOut(ast, ctx) {
1010
- let { block } = ctx;
1011
- if (block) {
1012
- this.insertAnchor(block);
1013
- }
1014
- block = this.createBlock(block, "html", ctx);
1015
- let blockStr;
1016
- if (ast.expr === "0") {
1017
- this.helpers.add("zero");
1018
- blockStr = `ctx[zero]`;
1019
- }
1020
- else if (ast.body) {
1021
- let bodyValue = null;
1022
- bodyValue = BlockDescription.nextBlockId;
1023
- const subCtx = createContext(ctx);
1024
- this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx);
1025
- this.helpers.add("safeOutput");
1026
- blockStr = `safeOutput(${compileExpr(ast.expr)}, b${bodyValue})`;
1027
- }
1028
- else {
1029
- this.helpers.add("safeOutput");
1030
- blockStr = `safeOutput(${compileExpr(ast.expr)})`;
1031
- }
1032
- this.insertBlock(blockStr, block, ctx);
1033
- return block.varName;
1034
- }
1035
- compileTIfBranch(content, block, ctx) {
1036
- this.target.indentLevel++;
1037
- let childN = block.children.length;
1038
- this.compileAST(content, createContext(ctx, { block, index: ctx.index }));
1039
- if (block.children.length > childN) {
1040
- // we have some content => need to insert an anchor at correct index
1041
- this.insertAnchor(block, childN);
1042
- }
1043
- this.target.indentLevel--;
1044
- }
1045
- compileTIf(ast, ctx, nextNode) {
1046
- let { block, forceNewBlock } = ctx;
1047
- const codeIdx = this.target.code.length;
1048
- const isNewBlock = !block || (block.type !== "multi" && forceNewBlock);
1049
- if (block) {
1050
- block.hasDynamicChildren = true;
1051
- }
1052
- if (!block || (block.type !== "multi" && forceNewBlock)) {
1053
- block = this.createBlock(block, "multi", ctx);
1054
- }
1055
- this.addLine(`if (${compileExpr(ast.condition)}) {`);
1056
- this.compileTIfBranch(ast.content, block, ctx);
1057
- if (ast.tElif) {
1058
- for (let clause of ast.tElif) {
1059
- this.addLine(`} else if (${compileExpr(clause.condition)}) {`);
1060
- this.compileTIfBranch(clause.content, block, ctx);
1061
- }
1062
- }
1063
- if (ast.tElse) {
1064
- this.addLine(`} else {`);
1065
- this.compileTIfBranch(ast.tElse, block, ctx);
1066
- }
1067
- this.addLine("}");
1068
- if (isNewBlock) {
1069
- // note: this part is duplicated from end of compiledomnode:
1070
- if (block.children.length) {
1071
- const code = this.target.code;
1072
- const children = block.children.slice();
1073
- let current = children.shift();
1074
- for (let i = codeIdx; i < code.length; i++) {
1075
- if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
1076
- code[i] = code[i].replace(`const ${current.varName}`, current.varName);
1077
- current = children.shift();
1078
- if (!current)
1079
- break;
1080
- }
1081
- }
1082
- this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
1083
- }
1084
- // note: this part is duplicated from end of compilemulti:
1085
- const args = block.children.map((c) => c.varName).join(", ");
1086
- this.insertBlock(`multi([${args}])`, block, ctx);
1087
- }
1088
- return block.varName;
1089
- }
1090
- compileTForeach(ast, ctx) {
1091
- let { block } = ctx;
1092
- if (block) {
1093
- this.insertAnchor(block);
1094
- }
1095
- block = this.createBlock(block, "list", ctx);
1096
- this.target.loopLevel++;
1097
- const loopVar = `i${this.target.loopLevel}`;
1098
- this.addLine(`ctx = Object.create(ctx);`);
1099
- const vals = `v_block${block.id}`;
1100
- const keys = `k_block${block.id}`;
1101
- const l = `l_block${block.id}`;
1102
- const c = `c_block${block.id}`;
1103
- this.helpers.add("prepareList");
1104
- this.define(`[${keys}, ${vals}, ${l}, ${c}]`, `prepareList(${compileExpr(ast.collection)});`);
1105
- // Throw errors on duplicate keys in dev mode
1106
- if (this.dev) {
1107
- this.define(`keys${block.id}`, `new Set()`);
1108
- }
1109
- this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
1110
- this.target.indentLevel++;
1111
- this.addLine(`ctx[\`${ast.elem}\`] = ${keys}[${loopVar}];`);
1112
- if (!ast.hasNoFirst) {
1113
- this.addLine(`ctx[\`${ast.elem}_first\`] = ${loopVar} === 0;`);
1114
- }
1115
- if (!ast.hasNoLast) {
1116
- this.addLine(`ctx[\`${ast.elem}_last\`] = ${loopVar} === ${keys}.length - 1;`);
1117
- }
1118
- if (!ast.hasNoIndex) {
1119
- this.addLine(`ctx[\`${ast.elem}_index\`] = ${loopVar};`);
1120
- }
1121
- if (!ast.hasNoValue) {
1122
- this.addLine(`ctx[\`${ast.elem}_value\`] = ${vals}[${loopVar}];`);
1123
- }
1124
- this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
1125
- if (this.dev) {
1126
- // Throw error on duplicate keys in dev mode
1127
- this.helpers.add("OwlError");
1128
- this.addLine(`if (keys${block.id}.has(String(key${this.target.loopLevel}))) { throw new OwlError(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`);
1129
- this.addLine(`keys${block.id}.add(String(key${this.target.loopLevel}));`);
1130
- }
1131
- let id;
1132
- if (ast.memo) {
1133
- this.target.hasCache = true;
1134
- id = generateId();
1135
- this.define(`memo${id}`, compileExpr(ast.memo));
1136
- this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);
1137
- this.addLine(`if (vnode${id}) {`);
1138
- this.target.indentLevel++;
1139
- this.addLine(`if (shallowEqual(vnode${id}.memo, memo${id})) {`);
1140
- this.target.indentLevel++;
1141
- this.addLine(`${c}[${loopVar}] = vnode${id};`);
1142
- this.addLine(`nextCache[key${this.target.loopLevel}] = vnode${id};`);
1143
- this.addLine(`continue;`);
1144
- this.target.indentLevel--;
1145
- this.addLine("}");
1146
- this.target.indentLevel--;
1147
- this.addLine("}");
1148
- }
1149
- const subCtx = createContext(ctx, { block, index: loopVar });
1150
- this.compileAST(ast.body, subCtx);
1151
- if (ast.memo) {
1152
- this.addLine(`nextCache[key${this.target.loopLevel}] = Object.assign(${c}[${loopVar}], {memo: memo${id}});`);
1153
- }
1154
- this.target.indentLevel--;
1155
- this.target.loopLevel--;
1156
- this.addLine(`}`);
1157
- if (!ctx.isLast) {
1158
- this.addLine(`ctx = ctx.__proto__;`);
1159
- }
1160
- this.insertBlock("l", block, ctx);
1161
- return block.varName;
1162
- }
1163
- compileTKey(ast, ctx) {
1164
- const tKeyExpr = generateId("tKey_");
1165
- this.define(tKeyExpr, compileExpr(ast.expr));
1166
- ctx = createContext(ctx, {
1167
- tKeyExpr,
1168
- block: ctx.block,
1169
- index: ctx.index,
1170
- });
1171
- return this.compileAST(ast.content, ctx);
1172
- }
1173
- compileMulti(ast, ctx) {
1174
- let { block, forceNewBlock } = ctx;
1175
- const isNewBlock = !block || forceNewBlock;
1176
- let codeIdx = this.target.code.length;
1177
- if (isNewBlock) {
1178
- const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
1179
- let result = null;
1180
- if (n <= 1) {
1181
- for (let child of ast.content) {
1182
- const blockName = this.compileAST(child, ctx);
1183
- result = result || blockName;
1184
- }
1185
- return result;
1186
- }
1187
- block = this.createBlock(block, "multi", ctx);
1188
- }
1189
- let index = 0;
1190
- for (let i = 0, l = ast.content.length; i < l; i++) {
1191
- const child = ast.content[i];
1192
- const forceNewBlock = !child.hasNoRepresentation;
1193
- const subCtx = createContext(ctx, {
1194
- block,
1195
- index,
1196
- forceNewBlock,
1197
- isLast: ctx.isLast && i === l - 1,
1198
- });
1199
- this.compileAST(child, subCtx);
1200
- if (forceNewBlock) {
1201
- index++;
1202
- }
1203
- }
1204
- if (isNewBlock) {
1205
- if (block.hasDynamicChildren && block.children.length) {
1206
- const code = this.target.code;
1207
- const children = block.children.slice();
1208
- let current = children.shift();
1209
- for (let i = codeIdx; i < code.length; i++) {
1210
- if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
1211
- code[i] = code[i].replace(`const ${current.varName}`, current.varName);
1212
- current = children.shift();
1213
- if (!current)
1214
- break;
1215
- }
1216
- }
1217
- this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
1218
- }
1219
- const args = block.children.map((c) => c.varName).join(", ");
1220
- this.insertBlock(`multi([${args}])`, block, ctx);
1221
- }
1222
- return block.varName;
1223
- }
1224
- compileTCall(ast, ctx) {
1225
- let { block, forceNewBlock } = ctx;
1226
- let ctxVar = ctx.ctxVar || "ctx";
1227
- if (ast.context) {
1228
- ctxVar = generateId("ctx");
1229
- this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);
1230
- }
1231
- const isDynamic = INTERP_REGEXP.test(ast.name);
1232
- const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`";
1233
- if (block && !forceNewBlock) {
1234
- this.insertAnchor(block);
1235
- }
1236
- block = this.createBlock(block, "multi", ctx);
1237
- if (ast.body) {
1238
- this.addLine(`${ctxVar} = Object.create(${ctxVar});`);
1239
- this.addLine(`${ctxVar}[isBoundary] = 1;`);
1240
- this.helpers.add("isBoundary");
1241
- const subCtx = createContext(ctx, { ctxVar });
1242
- const bl = this.compileMulti({ type: 3 /* Multi */, content: ast.body }, subCtx);
1243
- if (bl) {
1244
- this.helpers.add("zero");
1245
- this.addLine(`${ctxVar}[zero] = ${bl};`);
1246
- }
1247
- }
1248
- const key = this.generateComponentKey();
1249
- if (isDynamic) {
1250
- const templateVar = generateId("template");
1251
- if (!this.staticDefs.find((d) => d.id === "call")) {
1252
- this.staticDefs.push({ id: "call", expr: `app.callTemplate.bind(app)` });
1253
- }
1254
- this.define(templateVar, subTemplate);
1255
- this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block, {
1256
- ...ctx,
1257
- forceNewBlock: !block,
1258
- });
1259
- }
1260
- else {
1261
- const id = generateId(`callTemplate_`);
1262
- this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
1263
- this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block, {
1264
- ...ctx,
1265
- forceNewBlock: !block,
1266
- });
1267
- }
1268
- if (ast.body && !ctx.isLast) {
1269
- this.addLine(`${ctxVar} = ${ctxVar}.__proto__;`);
1270
- }
1271
- return block.varName;
1272
- }
1273
- compileTCallBlock(ast, ctx) {
1274
- let { block, forceNewBlock } = ctx;
1275
- if (block) {
1276
- if (!forceNewBlock) {
1277
- this.insertAnchor(block);
1278
- }
1279
- }
1280
- block = this.createBlock(block, "multi", ctx);
1281
- this.insertBlock(compileExpr(ast.name), block, { ...ctx, forceNewBlock: !block });
1282
- return block.varName;
1283
- }
1284
- compileTSet(ast, ctx) {
1285
- this.target.shouldProtectScope = true;
1286
- this.helpers.add("isBoundary").add("withDefault");
1287
- const expr = ast.value ? compileExpr(ast.value || "") : "null";
1288
- if (ast.body) {
1289
- this.helpers.add("LazyValue");
1290
- const bodyAst = { type: 3 /* Multi */, content: ast.body };
1291
- const name = this.compileInNewTarget("value", bodyAst, ctx);
1292
- let key = this.target.currentKey(ctx);
1293
- let value = `new LazyValue(${name}, ctx, this, node, ${key})`;
1294
- value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;
1295
- this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
1296
- }
1297
- else {
1298
- let value;
1299
- if (ast.defaultValue) {
1300
- const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue);
1301
- if (ast.value) {
1302
- value = `withDefault(${expr}, ${defaultValue})`;
1303
- }
1304
- else {
1305
- value = defaultValue;
1306
- }
1307
- }
1308
- else {
1309
- value = expr;
1310
- }
1311
- this.helpers.add("setContextValue");
1312
- this.addLine(`setContextValue(${ctx.ctxVar || "ctx"}, "${ast.name}", ${value});`);
1313
- }
1314
- return null;
1315
- }
1316
- generateComponentKey(currentKey = "key") {
1317
- const parts = [generateId("__")];
1318
- for (let i = 0; i < this.target.loopLevel; i++) {
1319
- parts.push(`\${key${i + 1}}`);
1320
- }
1321
- return `${currentKey} + \`${parts.join("__")}\``;
1322
- }
1323
- /**
1324
- * Formats a prop name and value into a string suitable to be inserted in the
1325
- * generated code. For example:
1326
- *
1327
- * Name Value Result
1328
- * ---------------------------------------------------------
1329
- * "number" "state" "number: ctx['state']"
1330
- * "something" "" "something: undefined"
1331
- * "some-prop" "state" "'some-prop': ctx['state']"
1332
- * "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
1333
- */
1334
- formatProp(name, value, attrsTranslationCtx, translationCtx) {
1335
- if (name.endsWith(".translate")) {
1336
- const attrTranslationCtx = (attrsTranslationCtx === null || attrsTranslationCtx === void 0 ? void 0 : attrsTranslationCtx[name]) || translationCtx;
1337
- value = toStringExpression(this.translateFn(value, attrTranslationCtx));
1338
- }
1339
- else {
1340
- value = this.captureExpression(value);
1341
- }
1342
- if (name.includes(".")) {
1343
- let [_name, suffix] = name.split(".");
1344
- name = _name;
1345
- switch (suffix) {
1346
- case "bind":
1347
- value = `(${value}).bind(this)`;
1348
- break;
1349
- case "alike":
1350
- case "translate":
1351
- break;
1352
- default:
1353
- throw new OwlError(`Invalid prop suffix: ${suffix}`);
1354
- }
1355
- }
1356
- name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
1357
- return `${name}: ${value || undefined}`;
1358
- }
1359
- formatPropObject(obj, attrsTranslationCtx, translationCtx) {
1360
- return Object.entries(obj).map(([k, v]) => this.formatProp(k, v, attrsTranslationCtx, translationCtx));
1361
- }
1362
- getPropString(props, dynProps) {
1363
- let propString = `{${props.join(",")}}`;
1364
- if (dynProps) {
1365
- propString = `Object.assign({}, ${compileExpr(dynProps)}${props.length ? ", " + propString : ""})`;
1366
- }
1367
- return propString;
1368
- }
1369
- compileComponent(ast, ctx) {
1370
- let { block } = ctx;
1371
- // props
1372
- const hasSlotsProp = "slots" in (ast.props || {});
1373
- const props = ast.props
1374
- ? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)
1375
- : [];
1376
- // slots
1377
- let slotDef = "";
1378
- if (ast.slots) {
1379
- let ctxStr = "ctx";
1380
- if (this.target.loopLevel || !this.hasSafeContext) {
1381
- ctxStr = generateId("ctx");
1382
- this.helpers.add("capture");
1383
- this.define(ctxStr, `capture(ctx)`);
1384
- }
1385
- let slotStr = [];
1386
- for (let slotName in ast.slots) {
1387
- const slotAst = ast.slots[slotName];
1388
- const params = [];
1389
- if (slotAst.content) {
1390
- const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
1391
- params.push(`__render: ${name}.bind(this), __ctx: ${ctxStr}`);
1392
- }
1393
- const scope = ast.slots[slotName].scope;
1394
- if (scope) {
1395
- params.push(`__scope: "${scope}"`);
1396
- }
1397
- if (ast.slots[slotName].attrs) {
1398
- params.push(...this.formatPropObject(ast.slots[slotName].attrs, ast.slots[slotName].attrsTranslationCtx, ctx.translationCtx));
1399
- }
1400
- const slotInfo = `{${params.join(", ")}}`;
1401
- slotStr.push(`'${slotName}': ${slotInfo}`);
1402
- }
1403
- slotDef = `{${slotStr.join(", ")}}`;
1404
- }
1405
- if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
1406
- this.helpers.add("markRaw");
1407
- props.push(`slots: markRaw(${slotDef})`);
1408
- }
1409
- let propString = this.getPropString(props, ast.dynamicProps);
1410
- let propVar;
1411
- if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
1412
- propVar = generateId("props");
1413
- this.define(propVar, propString);
1414
- propString = propVar;
1415
- }
1416
- if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
1417
- this.helpers.add("markRaw");
1418
- this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);
1419
- }
1420
- // cmap key
1421
- let expr;
1422
- if (ast.isDynamic) {
1423
- expr = generateId("Comp");
1424
- this.define(expr, compileExpr(ast.name));
1425
- }
1426
- else {
1427
- expr = `\`${ast.name}\``;
1428
- }
1429
- if (this.dev) {
1430
- this.addLine(`helpers.validateProps(${expr}, ${propVar}, this);`);
1431
- }
1432
- if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) {
1433
- // todo: check the forcenewblock condition
1434
- this.insertAnchor(block);
1435
- }
1436
- let keyArg = this.generateComponentKey();
1437
- if (ctx.tKeyExpr) {
1438
- keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
1439
- }
1440
- let id = generateId("comp");
1441
- const propList = [];
1442
- for (let p in ast.props || {}) {
1443
- let [name, suffix] = p.split(".");
1444
- if (!suffix) {
1445
- propList.push(`"${name}"`);
1446
- }
1447
- }
1448
- this.staticDefs.push({
1449
- id,
1450
- expr: `app.createComponent(${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, [${propList}])`,
1451
- });
1452
- if (ast.isDynamic) {
1453
- // If the component class changes, this can cause delayed renders to go
1454
- // through if the key doesn't change. Use the component name for now.
1455
- // This means that two component classes with the same name isn't supported
1456
- // in t-component. We can generate a unique id per class later if needed.
1457
- keyArg = `(${expr}).name + ${keyArg}`;
1458
- }
1459
- let blockExpr = `${id}(${propString}, ${keyArg}, node, this, ${ast.isDynamic ? expr : null})`;
1460
- if (ast.isDynamic) {
1461
- blockExpr = `toggler(${expr}, ${blockExpr})`;
1462
- }
1463
- // event handling
1464
- if (ast.on) {
1465
- blockExpr = this.wrapWithEventCatcher(blockExpr, ast.on);
1466
- }
1467
- block = this.createBlock(block, "multi", ctx);
1468
- this.insertBlock(blockExpr, block, ctx);
1469
- return block.varName;
1470
- }
1471
- wrapWithEventCatcher(expr, on) {
1472
- this.helpers.add("createCatcher");
1473
- let name = generateId("catcher");
1474
- let spec = {};
1475
- let handlers = [];
1476
- for (let ev in on) {
1477
- let handlerId = generateId("hdlr");
1478
- let idx = handlers.push(handlerId) - 1;
1479
- spec[ev] = idx;
1480
- const handler = this.generateHandlerCode(ev, on[ev]);
1481
- this.define(handlerId, handler);
1482
- }
1483
- this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` });
1484
- return `${name}(${expr}, [${handlers.join(",")}])`;
1485
- }
1486
- compileTSlot(ast, ctx) {
1487
- this.helpers.add("callSlot");
1488
- let { block } = ctx;
1489
- let blockString;
1490
- let slotName;
1491
- let dynamic = false;
1492
- let isMultiple = false;
1493
- if (ast.name.match(INTERP_REGEXP)) {
1494
- dynamic = true;
1495
- isMultiple = true;
1496
- slotName = interpolate(ast.name);
1497
- }
1498
- else {
1499
- slotName = "'" + ast.name + "'";
1500
- isMultiple = isMultiple || this.slotNames.has(ast.name);
1501
- this.slotNames.add(ast.name);
1502
- }
1503
- const attrs = { ...ast.attrs };
1504
- const dynProps = attrs["t-props"];
1505
- delete attrs["t-props"];
1506
- let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
1507
- if (isMultiple) {
1508
- key = this.generateComponentKey(key);
1509
- }
1510
- const props = ast.attrs
1511
- ? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
1512
- : [];
1513
- const scope = this.getPropString(props, dynProps);
1514
- if (ast.defaultContent) {
1515
- const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
1516
- blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope}, ${name}.bind(this))`;
1517
- }
1518
- else {
1519
- if (dynamic) {
1520
- let name = generateId("slot");
1521
- this.define(name, slotName);
1522
- blockString = `toggler(${name}, callSlot(ctx, node, ${key}, ${name}, ${dynamic}, ${scope}))`;
1523
- }
1524
- else {
1525
- blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope})`;
1526
- }
1527
- }
1528
- // event handling
1529
- if (ast.on) {
1530
- blockString = this.wrapWithEventCatcher(blockString, ast.on);
1531
- }
1532
- if (block) {
1533
- this.insertAnchor(block);
1534
- }
1535
- block = this.createBlock(block, "multi", ctx);
1536
- this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
1537
- return block.varName;
1538
- }
1539
- compileTTranslation(ast, ctx) {
1540
- if (ast.content) {
1541
- return this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));
1542
- }
1543
- return null;
1544
- }
1545
- compileTTranslationContext(ast, ctx) {
1546
- if (ast.content) {
1547
- return this.compileAST(ast.content, Object.assign({}, ctx, { translationCtx: ast.translationCtx }));
1548
- }
1549
- return null;
1550
- }
1551
- compileTPortal(ast, ctx) {
1552
- if (!this.staticDefs.find((d) => d.id === "Portal")) {
1553
- this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
1554
- }
1555
- let { block } = ctx;
1556
- const name = this.compileInNewTarget("slot", ast.content, ctx);
1557
- let ctxStr = "ctx";
1558
- if (this.target.loopLevel || !this.hasSafeContext) {
1559
- ctxStr = generateId("ctx");
1560
- this.helpers.add("capture");
1561
- this.define(ctxStr, `capture(ctx)`);
1562
- }
1563
- let id = generateId("comp");
1564
- this.staticDefs.push({
1565
- id,
1566
- expr: `app.createComponent(null, false, true, false, false)`,
1567
- });
1568
- const target = compileExpr(ast.target);
1569
- const key = this.generateComponentKey();
1570
- const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;
1571
- if (block) {
1572
- this.insertAnchor(block);
1573
- }
1574
- block = this.createBlock(block, "multi", ctx);
1575
- this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
1576
- return block.varName;
1577
- }
351
+ const zero = Symbol("zero");
352
+ const whitespaceRE = /\s+/g;
353
+ // using a non-html document so that <inner/outer>HTML serializes as XML instead
354
+ // of HTML (as we will parse it as xml later)
355
+ let xmlDoc;
356
+ if (typeof document !== "undefined") {
357
+ xmlDoc = document.implementation.createDocument(null, null, null);
358
+ }
359
+ const MODS = new Set(["stop", "capture", "prevent", "self", "synthetic", "passive"]);
360
+ let nextDataIds = {};
361
+ function generateId(prefix = "") {
362
+ nextDataIds[prefix] = (nextDataIds[prefix] || 0) + 1;
363
+ return prefix + nextDataIds[prefix];
364
+ }
365
+ function isProp(tag, key) {
366
+ switch (tag) {
367
+ case "input":
368
+ return (key === "checked" ||
369
+ key === "indeterminate" ||
370
+ key === "value" ||
371
+ key === "readonly" ||
372
+ key === "readOnly" ||
373
+ key === "disabled");
374
+ case "option":
375
+ return key === "selected" || key === "disabled";
376
+ case "textarea":
377
+ return key === "value" || key === "readonly" || key === "readOnly" || key === "disabled";
378
+ case "select":
379
+ return key === "value" || key === "disabled";
380
+ case "button":
381
+ case "optgroup":
382
+ return key === "disabled";
383
+ }
384
+ return false;
385
+ }
386
+ /**
387
+ * Returns a template literal that evaluates to str. You can add interpolation
388
+ * sigils into the string if required
389
+ */
390
+ function toStringExpression(str) {
391
+ return `\`${str.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/, "\\${")}\``;
392
+ }
393
+ // -----------------------------------------------------------------------------
394
+ // BlockDescription
395
+ // -----------------------------------------------------------------------------
396
+ class BlockDescription {
397
+ static nextBlockId = 1;
398
+ varName;
399
+ blockName;
400
+ dynamicTagName = null;
401
+ isRoot = false;
402
+ hasDynamicChildren = false;
403
+ children = [];
404
+ data = [];
405
+ dom;
406
+ currentDom;
407
+ childNumber = 0;
408
+ target;
409
+ type;
410
+ parentVar = "";
411
+ id;
412
+ constructor(target, type) {
413
+ this.id = BlockDescription.nextBlockId++;
414
+ this.varName = "b" + this.id;
415
+ this.blockName = "block" + this.id;
416
+ this.target = target;
417
+ this.type = type;
418
+ }
419
+ insertData(str, prefix = "d") {
420
+ const id = generateId(prefix);
421
+ this.target.addLine(`let ${id} = ${str};`);
422
+ return this.data.push(id) - 1;
423
+ }
424
+ insert(dom) {
425
+ if (this.currentDom) {
426
+ this.currentDom.appendChild(dom);
427
+ }
428
+ else {
429
+ this.dom = dom;
430
+ }
431
+ }
432
+ generateExpr(expr) {
433
+ if (this.type === "block") {
434
+ const hasChildren = this.children.length;
435
+ let params = this.data.length ? `[${this.data.join(", ")}]` : hasChildren ? "[]" : "";
436
+ if (hasChildren) {
437
+ params += ", [" + this.children.map((c) => c.varName).join(", ") + "]";
438
+ }
439
+ if (this.dynamicTagName) {
440
+ return `toggler(${this.dynamicTagName}, ${this.blockName}(${this.dynamicTagName})(${params}))`;
441
+ }
442
+ return `${this.blockName}(${params})`;
443
+ }
444
+ else if (this.type === "list") {
445
+ return `list(c_block${this.id})`;
446
+ }
447
+ return expr;
448
+ }
449
+ asXmlString() {
450
+ // Can't use outerHTML on text/comment nodes
451
+ // append dom to any element and use innerHTML instead
452
+ const t = xmlDoc.createElement("t");
453
+ t.appendChild(this.dom);
454
+ return t.innerHTML;
455
+ }
456
+ }
457
+ function createContext(parentCtx, params) {
458
+ return Object.assign({
459
+ block: null,
460
+ index: 0,
461
+ forceNewBlock: true,
462
+ translate: parentCtx.translate,
463
+ translationCtx: parentCtx.translationCtx,
464
+ tKeyExpr: null,
465
+ nameSpace: parentCtx.nameSpace,
466
+ tModelSelectedExpr: parentCtx.tModelSelectedExpr,
467
+ }, params);
468
+ }
469
+ class CodeTarget {
470
+ name;
471
+ indentLevel = 0;
472
+ loopLevel = 0;
473
+ loopCtxVars = [];
474
+ tSetVars = new Map();
475
+ code = [];
476
+ hasRoot = false;
477
+ needsScopeProtection = false;
478
+ on;
479
+ constructor(name, on) {
480
+ this.name = name;
481
+ this.on = on || null;
482
+ }
483
+ addLine(line, idx) {
484
+ const prefix = new Array(this.indentLevel + 2).join(" ");
485
+ if (idx === undefined) {
486
+ this.code.push(prefix + line);
487
+ }
488
+ else {
489
+ this.code.splice(idx, 0, prefix + line);
490
+ }
491
+ }
492
+ generateCode() {
493
+ let result = [];
494
+ result.push(`function ${this.name}(ctx, node, key = "") {`);
495
+ if (this.needsScopeProtection) {
496
+ result.push(` ctx = Object.create(ctx);`);
497
+ }
498
+ for (let line of this.code) {
499
+ result.push(line);
500
+ }
501
+ if (!this.hasRoot) {
502
+ result.push(`return text('');`);
503
+ }
504
+ result.push(`}`);
505
+ return result.join("\n ");
506
+ }
507
+ currentKey(ctx) {
508
+ let key = this.loopLevel ? `key${this.loopLevel}` : "key";
509
+ if (ctx.tKeyExpr) {
510
+ key = `${ctx.tKeyExpr} + ${key}`;
511
+ }
512
+ return key;
513
+ }
514
+ }
515
+ const TRANSLATABLE_ATTRS = [
516
+ "alt",
517
+ "aria-label",
518
+ "aria-placeholder",
519
+ "aria-roledescription",
520
+ "aria-valuetext",
521
+ "label",
522
+ "placeholder",
523
+ "title",
524
+ ];
525
+ const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
526
+ class CodeGenerator {
527
+ blocks = [];
528
+ nextBlockId = 1;
529
+ isDebug = false;
530
+ targets = [];
531
+ target = new CodeTarget("template");
532
+ templateName;
533
+ dev;
534
+ translateFn;
535
+ translatableAttributes = TRANSLATABLE_ATTRS;
536
+ ast;
537
+ staticDefs = [];
538
+ slotNames = new Set();
539
+ helpers = new Set();
540
+ constructor(ast, options) {
541
+ this.translateFn = options.translateFn || ((s) => s);
542
+ if (options.translatableAttributes) {
543
+ const attrs = new Set(TRANSLATABLE_ATTRS);
544
+ for (let attr of options.translatableAttributes) {
545
+ if (attr.startsWith("-")) {
546
+ attrs.delete(attr.slice(1));
547
+ }
548
+ else {
549
+ attrs.add(attr);
550
+ }
551
+ }
552
+ this.translatableAttributes = [...attrs];
553
+ }
554
+ this.dev = options.dev || false;
555
+ this.ast = ast;
556
+ this.templateName = options.name;
557
+ if (options.hasGlobalValues) {
558
+ this.helpers.add("__globals__");
559
+ }
560
+ }
561
+ generateCode() {
562
+ const ast = this.ast;
563
+ this.isDebug = ast.type === 11 /* ASTType.TDebug */;
564
+ BlockDescription.nextBlockId = 1;
565
+ nextDataIds = {};
566
+ this.compileAST(ast, {
567
+ block: null,
568
+ index: 0,
569
+ forceNewBlock: false,
570
+ translate: true,
571
+ translationCtx: "",
572
+ tKeyExpr: null,
573
+ });
574
+ // define blocks and utility functions
575
+ let mainCode = [` let { text, createBlock, list, multi, html, toggler, comment } = bdom;`];
576
+ if (this.helpers.size) {
577
+ mainCode.push(`let { ${[...this.helpers].join(", ")} } = helpers;`);
578
+ }
579
+ if (this.templateName) {
580
+ mainCode.push(`// Template name: "${this.templateName}"`);
581
+ }
582
+ for (let { id, expr } of this.staticDefs) {
583
+ mainCode.push(`const ${id} = ${expr};`);
584
+ }
585
+ // define all blocks
586
+ if (this.blocks.length) {
587
+ mainCode.push(``);
588
+ for (let block of this.blocks) {
589
+ if (block.dom) {
590
+ let xmlString = toStringExpression(block.asXmlString());
591
+ if (block.dynamicTagName) {
592
+ xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
593
+ xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
594
+ mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
595
+ }
596
+ else {
597
+ mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
598
+ }
599
+ }
600
+ }
601
+ }
602
+ // define all slots/defaultcontent function
603
+ if (this.targets.length) {
604
+ for (let fn of this.targets) {
605
+ mainCode.push("");
606
+ mainCode = mainCode.concat(fn.generateCode());
607
+ }
608
+ }
609
+ // generate main code
610
+ mainCode.push("");
611
+ mainCode = mainCode.concat("return " + this.target.generateCode());
612
+ const code = mainCode.join("\n ");
613
+ if (this.isDebug) {
614
+ const msg = `[Owl Debug]\n${code}`;
615
+ console.log(msg);
616
+ }
617
+ return code;
618
+ }
619
+ compileInNewTarget(prefix, ast, ctx, on) {
620
+ const name = generateId(prefix);
621
+ const initialTarget = this.target;
622
+ const target = new CodeTarget(name, on);
623
+ this.targets.push(target);
624
+ this.target = target;
625
+ this.compileAST(ast, createContext(ctx));
626
+ this.target = initialTarget;
627
+ return name;
628
+ }
629
+ addLine(line, idx) {
630
+ this.target.addLine(line, idx);
631
+ }
632
+ define(varName, expr) {
633
+ this.addLine(`const ${varName} = ${expr};`);
634
+ }
635
+ insertAnchor(block, index = block.children.length) {
636
+ const tag = `block-child-${index}`;
637
+ const anchor = xmlDoc.createElement(tag);
638
+ block.insert(anchor);
639
+ }
640
+ createBlock(parentBlock, type, ctx) {
641
+ const hasRoot = this.target.hasRoot;
642
+ const block = new BlockDescription(this.target, type);
643
+ if (!hasRoot) {
644
+ this.target.hasRoot = true;
645
+ block.isRoot = true;
646
+ }
647
+ if (parentBlock) {
648
+ parentBlock.children.push(block);
649
+ if (parentBlock.type === "list") {
650
+ block.parentVar = `c_block${parentBlock.id}`;
651
+ }
652
+ }
653
+ return block;
654
+ }
655
+ insertBlock(expression, block, ctx) {
656
+ let blockExpr = block.generateExpr(expression);
657
+ if (block.parentVar) {
658
+ let key = this.target.currentKey(ctx);
659
+ this.helpers.add("withKey");
660
+ this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${key});`);
661
+ return;
662
+ }
663
+ if (ctx.tKeyExpr) {
664
+ blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;
665
+ }
666
+ if (block.isRoot) {
667
+ if (this.target.on) {
668
+ blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
669
+ }
670
+ this.addLine(`return ${blockExpr};`);
671
+ }
672
+ else {
673
+ this.define(block.varName, blockExpr);
674
+ }
675
+ }
676
+ translate(str, translationCtx) {
677
+ const match = translationRE.exec(str);
678
+ return match[1] + this.translateFn(match[2], translationCtx) + match[3];
679
+ }
680
+ /**
681
+ * @returns the newly created block name, if any
682
+ */
683
+ compileAST(ast, ctx) {
684
+ switch (ast.type) {
685
+ case 1 /* ASTType.Comment */:
686
+ return this.compileComment(ast, ctx);
687
+ case 0 /* ASTType.Text */:
688
+ return this.compileText(ast, ctx);
689
+ case 2 /* ASTType.DomNode */:
690
+ return this.compileTDomNode(ast, ctx);
691
+ case 7 /* ASTType.TOut */:
692
+ return this.compileTOut(ast, ctx);
693
+ case 4 /* ASTType.TIf */:
694
+ return this.compileTIf(ast, ctx);
695
+ case 8 /* ASTType.TForEach */:
696
+ return this.compileTForeach(ast, ctx);
697
+ case 9 /* ASTType.TKey */:
698
+ return this.compileTKey(ast, ctx);
699
+ case 3 /* ASTType.Multi */:
700
+ return this.compileMulti(ast, ctx);
701
+ case 6 /* ASTType.TCall */:
702
+ return this.compileTCall(ast, ctx);
703
+ case 14 /* ASTType.TCallBlock */:
704
+ return this.compileTCallBlock(ast, ctx);
705
+ case 5 /* ASTType.TSet */:
706
+ return this.compileTSet(ast, ctx);
707
+ case 10 /* ASTType.TComponent */:
708
+ return this.compileComponent(ast, ctx);
709
+ case 11 /* ASTType.TDebug */:
710
+ return this.compileDebug(ast, ctx);
711
+ case 12 /* ASTType.TLog */:
712
+ return this.compileLog(ast, ctx);
713
+ case 13 /* ASTType.TCallSlot */:
714
+ return this.compileTCallSlot(ast, ctx);
715
+ case 15 /* ASTType.TTranslation */:
716
+ return this.compileTTranslation(ast, ctx);
717
+ case 16 /* ASTType.TTranslationContext */:
718
+ return this.compileTTranslationContext(ast, ctx);
719
+ }
720
+ }
721
+ compileDebug(ast, ctx) {
722
+ this.addLine(`debugger;`);
723
+ if (ast.content) {
724
+ return this.compileAST(ast.content, ctx);
725
+ }
726
+ return null;
727
+ }
728
+ compileLog(ast, ctx) {
729
+ this.addLine(`console.log(${compileExpr(ast.expr)});`);
730
+ if (ast.content) {
731
+ return this.compileAST(ast.content, ctx);
732
+ }
733
+ return null;
734
+ }
735
+ compileComment(ast, ctx) {
736
+ let { block, forceNewBlock } = ctx;
737
+ const isNewBlock = !block || forceNewBlock;
738
+ if (isNewBlock) {
739
+ block = this.createBlock(block, "comment", ctx);
740
+ this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
741
+ ...ctx,
742
+ forceNewBlock: forceNewBlock && !block,
743
+ });
744
+ }
745
+ else {
746
+ const text = xmlDoc.createComment(ast.value);
747
+ block.insert(text);
748
+ }
749
+ return block.varName;
750
+ }
751
+ compileText(ast, ctx) {
752
+ let { block, forceNewBlock } = ctx;
753
+ let value = ast.value;
754
+ if (value && ctx.translate !== false) {
755
+ value = this.translate(value, ctx.translationCtx);
756
+ }
757
+ if (!ctx.inPreTag) {
758
+ value = value.replace(whitespaceRE, " ");
759
+ }
760
+ if (!block || forceNewBlock) {
761
+ block = this.createBlock(block, "text", ctx);
762
+ this.insertBlock(`text(${toStringExpression(value)})`, block, {
763
+ ...ctx,
764
+ forceNewBlock: forceNewBlock && !block,
765
+ });
766
+ }
767
+ else {
768
+ const createFn = ast.type === 0 /* ASTType.Text */ ? xmlDoc.createTextNode : xmlDoc.createComment;
769
+ block.insert(createFn.call(xmlDoc, value));
770
+ }
771
+ return block.varName;
772
+ }
773
+ generateHandlerCode(rawEvent, handler) {
774
+ const modifiers = rawEvent
775
+ .split(".")
776
+ .slice(1)
777
+ .map((m) => {
778
+ if (!MODS.has(m)) {
779
+ throw new OwlError(`Unknown event modifier: '${m}'`);
780
+ }
781
+ return `"${m}"`;
782
+ });
783
+ let modifiersCode = "";
784
+ if (modifiers.length) {
785
+ modifiersCode = `${modifiers.join(",")}, `;
786
+ }
787
+ const compiled = compileExpr(handler);
788
+ if (!compiled.trim()) {
789
+ return `[${modifiersCode}, ctx]`;
790
+ }
791
+ let hoistedExpr;
792
+ const arrowMatch = compiled.match(/^(\([^)]*\))\s*=>/);
793
+ const bareArrowMatch = !arrowMatch && compiled.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/);
794
+ if (arrowMatch) {
795
+ const inner = arrowMatch[1].slice(1, -1).trim();
796
+ const rest = compiled.slice(arrowMatch[0].length);
797
+ hoistedExpr = inner ? `(ctx,${inner})=>${rest}` : `(ctx)=>${rest}`;
798
+ }
799
+ else if (bareArrowMatch) {
800
+ const rest = compiled.slice(bareArrowMatch[0].length);
801
+ hoistedExpr = `(ctx,${bareArrowMatch[1]})=>${rest}`;
802
+ }
803
+ else {
804
+ this.helpers.add("callHandler");
805
+ hoistedExpr = `(ctx, ev) => callHandler(${compiled}, ctx, ev)`;
806
+ }
807
+ const id = generateId("hdlr_fn");
808
+ this.staticDefs.push({ id, expr: hoistedExpr });
809
+ return `[${modifiersCode}${id}, ctx]`;
810
+ }
811
+ compileTDomNode(ast, ctx) {
812
+ let { block, forceNewBlock } = ctx;
813
+ const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;
814
+ let codeIdx = this.target.code.length;
815
+ if (isNewBlock) {
816
+ if ((ast.dynamicTag || ctx.tKeyExpr || ast.ns) && ctx.block) {
817
+ this.insertAnchor(ctx.block);
818
+ }
819
+ block = this.createBlock(block, "block", ctx);
820
+ this.blocks.push(block);
821
+ if (ast.dynamicTag) {
822
+ const tagExpr = generateId("tag");
823
+ this.define(tagExpr, compileExpr(ast.dynamicTag));
824
+ block.dynamicTagName = tagExpr;
825
+ }
826
+ }
827
+ // attributes
828
+ const attrs = {};
829
+ for (let key in ast.attrs) {
830
+ let expr, attrName;
831
+ if (key.startsWith("t-attf")) {
832
+ expr = interpolate(ast.attrs[key]);
833
+ const idx = block.insertData(expr, "attr");
834
+ attrName = key.slice(7);
835
+ attrs["block-attribute-" + idx] = attrName;
836
+ }
837
+ else if (key.startsWith("t-att")) {
838
+ attrName = key === "t-att" ? null : key.slice(6);
839
+ expr = compileExpr(ast.attrs[key]);
840
+ if (attrName && isProp(ast.tag, attrName)) {
841
+ if (attrName === "readonly") {
842
+ // the property has a different name than the attribute
843
+ attrName = "readOnly";
844
+ }
845
+ // we force a new string or new boolean to bypass the equality check in blockdom when patching same value
846
+ if (attrName === "value") {
847
+ // When the expression is falsy (except 0), fall back to an empty string
848
+ expr = `new String((${expr}) === 0 ? 0 : ((${expr}) || ""))`;
849
+ }
850
+ else {
851
+ expr = `new Boolean(${expr})`;
852
+ }
853
+ const idx = block.insertData(expr, "prop");
854
+ attrs[`block-property-${idx}`] = attrName;
855
+ }
856
+ else {
857
+ const idx = block.insertData(expr, "attr");
858
+ if (key === "t-att") {
859
+ attrs[`block-attributes`] = String(idx);
860
+ }
861
+ else {
862
+ attrs[`block-attribute-${idx}`] = attrName;
863
+ }
864
+ }
865
+ }
866
+ else if (this.translatableAttributes.includes(key)) {
867
+ const attrTranslationCtx = ast.attrsTranslationCtx?.[key] || ctx.translationCtx;
868
+ attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
869
+ }
870
+ else {
871
+ expr = `"${ast.attrs[key]}"`;
872
+ attrName = key;
873
+ attrs[key] = ast.attrs[key];
874
+ }
875
+ if (attrName === "value" && ctx.tModelSelectedExpr) {
876
+ let selectedId = block.insertData(`${ctx.tModelSelectedExpr} === ${expr}`, "attr");
877
+ attrs[`block-attribute-${selectedId}`] = "selected";
878
+ }
879
+ }
880
+ // t-model
881
+ let tModelSelectedExpr;
882
+ if (ast.model) {
883
+ const { hasDynamicChildren, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, isProxy, } = ast.model;
884
+ let readExpr;
885
+ let writeExpr;
886
+ if (isProxy) {
887
+ const expression = compileExpr(expr);
888
+ readExpr = expression;
889
+ writeExpr = (value) => `${expression} = ${value}`;
890
+ }
891
+ else {
892
+ const exprId = generateId("expr");
893
+ const expression = compileExpr(expr);
894
+ this.helpers.add("modelExpr");
895
+ this.define(exprId, `modelExpr(${expression})`);
896
+ readExpr = `${exprId}()`;
897
+ writeExpr = (value) => `${exprId}.set(${value})`;
898
+ }
899
+ let idx;
900
+ if (specialInitTargetAttr) {
901
+ let targetExpr = targetAttr in attrs && `'${attrs[targetAttr]}'`;
902
+ if (!targetExpr && ast.attrs) {
903
+ // look at the dynamic attribute counterpart
904
+ const dynamicTgExpr = ast.attrs[`t-att-${targetAttr}`];
905
+ if (dynamicTgExpr) {
906
+ targetExpr = compileExpr(dynamicTgExpr);
907
+ }
908
+ }
909
+ idx = block.insertData(`${readExpr} === ${targetExpr}`, "prop");
910
+ attrs[`block-property-${idx}`] = specialInitTargetAttr;
911
+ }
912
+ else if (hasDynamicChildren) {
913
+ const bValueId = generateId("bValue");
914
+ tModelSelectedExpr = `${bValueId}`;
915
+ this.define(tModelSelectedExpr, readExpr);
916
+ }
917
+ else {
918
+ idx = block.insertData(readExpr, "prop");
919
+ attrs[`block-property-${idx}`] = targetAttr;
920
+ }
921
+ this.helpers.add("toNumber");
922
+ let valueCode = `ev.target.${targetAttr}`;
923
+ valueCode = shouldTrim ? `${valueCode}.trim()` : valueCode;
924
+ valueCode = shouldNumberize ? `toNumber(${valueCode})` : valueCode;
925
+ const handler = `[(ctx, ev) => { ${writeExpr(valueCode)}; }, ctx]`;
926
+ idx = block.insertData(handler, "hdlr");
927
+ attrs[`block-handler-${idx}`] = eventType;
928
+ }
929
+ // event handlers
930
+ for (let ev in ast.on) {
931
+ const name = this.generateHandlerCode(ev, ast.on[ev]);
932
+ const idx = block.insertData(name, "hdlr");
933
+ attrs[`block-handler-${idx}`] = ev;
934
+ }
935
+ // t-ref
936
+ if (ast.ref) {
937
+ const refExpr = compileExpr(ast.ref);
938
+ this.helpers.add("createRef");
939
+ const setRefStr = `createRef(${refExpr})`;
940
+ const idx = block.insertData(setRefStr, "ref");
941
+ attrs["block-ref"] = String(idx);
942
+ }
943
+ const nameSpace = ast.ns || ctx.nameSpace;
944
+ const dom = nameSpace
945
+ ? xmlDoc.createElementNS(nameSpace, ast.tag)
946
+ : xmlDoc.createElement(ast.tag);
947
+ for (const [attr, val] of Object.entries(attrs)) {
948
+ if (!(attr === "class" && val === "")) {
949
+ dom.setAttribute(attr, val);
950
+ }
951
+ }
952
+ block.insert(dom);
953
+ if (ast.content.length) {
954
+ const initialDom = block.currentDom;
955
+ block.currentDom = dom;
956
+ const children = ast.content;
957
+ for (let i = 0; i < children.length; i++) {
958
+ const child = ast.content[i];
959
+ const subCtx = createContext(ctx, {
960
+ block,
961
+ index: block.childNumber,
962
+ forceNewBlock: false,
963
+ tKeyExpr: ctx.tKeyExpr,
964
+ nameSpace,
965
+ tModelSelectedExpr,
966
+ inPreTag: ctx.inPreTag || ast.tag === "pre",
967
+ });
968
+ this.compileAST(child, subCtx);
969
+ }
970
+ block.currentDom = initialDom;
971
+ }
972
+ if (isNewBlock) {
973
+ this.insertBlock(`${block.blockName}(ddd)`, block, ctx);
974
+ // may need to rewrite code!
975
+ if (block.children.length && block.hasDynamicChildren) {
976
+ const code = this.target.code;
977
+ const children = block.children.slice();
978
+ let current = children.shift();
979
+ for (let i = codeIdx; i < code.length; i++) {
980
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
981
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
982
+ current = children.shift();
983
+ if (!current)
984
+ break;
985
+ }
986
+ }
987
+ this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
988
+ }
989
+ }
990
+ return block.varName;
991
+ }
992
+ compileZero() {
993
+ this.helpers.add("zero");
994
+ const isMultiple = this.slotNames.has(zero);
995
+ this.slotNames.add(zero);
996
+ let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
997
+ if (isMultiple) {
998
+ key = this.generateComponentKey(key);
999
+ }
1000
+ return `ctx[zero]?.(node, ${key}) || text("")`;
1001
+ }
1002
+ compileTOut(ast, ctx) {
1003
+ let { block } = ctx;
1004
+ if (block) {
1005
+ this.insertAnchor(block);
1006
+ }
1007
+ block = this.createBlock(block, "html", ctx);
1008
+ let blockStr;
1009
+ if (ast.expr === "0") {
1010
+ blockStr = this.compileZero();
1011
+ }
1012
+ else if (ast.body) {
1013
+ let bodyValue = null;
1014
+ bodyValue = BlockDescription.nextBlockId;
1015
+ const subCtx = createContext(ctx);
1016
+ this.compileAST({ type: 3 /* ASTType.Multi */, content: ast.body }, subCtx);
1017
+ this.helpers.add("safeOutput");
1018
+ blockStr = `safeOutput(${compileExpr(ast.expr)}, b${bodyValue})`;
1019
+ }
1020
+ else {
1021
+ this.helpers.add("safeOutput");
1022
+ blockStr = `safeOutput(${compileExpr(ast.expr)})`;
1023
+ }
1024
+ this.insertBlock(blockStr, block, ctx);
1025
+ return block.varName;
1026
+ }
1027
+ compileTIfBranch(content, block, ctx) {
1028
+ this.target.indentLevel++;
1029
+ let childN = block.children.length;
1030
+ this.compileAST(content, createContext(ctx, { block, index: ctx.index }));
1031
+ if (block.children.length > childN) {
1032
+ // we have some content => need to insert an anchor at correct index
1033
+ this.insertAnchor(block, childN);
1034
+ }
1035
+ this.target.indentLevel--;
1036
+ }
1037
+ compileTIf(ast, ctx, nextNode) {
1038
+ let { block, forceNewBlock } = ctx;
1039
+ const codeIdx = this.target.code.length;
1040
+ const isNewBlock = !block || (block.type !== "multi" && forceNewBlock);
1041
+ if (block) {
1042
+ block.hasDynamicChildren = true;
1043
+ }
1044
+ if (!block || (block.type !== "multi" && forceNewBlock)) {
1045
+ block = this.createBlock(block, "multi", ctx);
1046
+ }
1047
+ this.addLine(`if (${compileExpr(ast.condition)}) {`);
1048
+ this.compileTIfBranch(ast.content, block, ctx);
1049
+ if (ast.tElif) {
1050
+ for (let clause of ast.tElif) {
1051
+ this.addLine(`} else if (${compileExpr(clause.condition)}) {`);
1052
+ this.compileTIfBranch(clause.content, block, ctx);
1053
+ }
1054
+ }
1055
+ if (ast.tElse) {
1056
+ this.addLine(`} else {`);
1057
+ this.compileTIfBranch(ast.tElse, block, ctx);
1058
+ }
1059
+ this.addLine("}");
1060
+ if (isNewBlock) {
1061
+ // note: this part is duplicated from end of compiledomnode:
1062
+ if (block.children.length) {
1063
+ const code = this.target.code;
1064
+ const children = block.children.slice();
1065
+ let current = children.shift();
1066
+ for (let i = codeIdx; i < code.length; i++) {
1067
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
1068
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
1069
+ current = children.shift();
1070
+ if (!current)
1071
+ break;
1072
+ }
1073
+ }
1074
+ this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
1075
+ }
1076
+ // note: this part is duplicated from end of compilemulti:
1077
+ const args = block.children.map((c) => c.varName).join(", ");
1078
+ this.insertBlock(`multi([${args}])`, block, ctx);
1079
+ }
1080
+ return block.varName;
1081
+ }
1082
+ compileTForeach(ast, ctx) {
1083
+ let { block } = ctx;
1084
+ if (block) {
1085
+ this.insertAnchor(block);
1086
+ }
1087
+ block = this.createBlock(block, "list", ctx);
1088
+ this.target.loopLevel++;
1089
+ const loopVar = `i${this.target.loopLevel}`;
1090
+ const ctxVar = generateId("ctx");
1091
+ this.addLine(`const ${ctxVar} = ctx;`);
1092
+ this.target.loopCtxVars.push(ctxVar);
1093
+ const vals = `v_block${block.id}`;
1094
+ const keys = `k_block${block.id}`;
1095
+ const l = `l_block${block.id}`;
1096
+ const c = `c_block${block.id}`;
1097
+ this.helpers.add("prepareList");
1098
+ this.define(`[${keys}, ${vals}, ${l}, ${c}]`, `prepareList(${compileExpr(ast.collection)});`);
1099
+ // Throw errors on duplicate keys in dev mode
1100
+ if (this.dev) {
1101
+ this.define(`keys${block.id}`, `new Set()`);
1102
+ }
1103
+ this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
1104
+ this.target.indentLevel++;
1105
+ this.addLine(`let ctx = Object.create(${ctxVar});`);
1106
+ this.addLine(`ctx[\`${ast.elem}\`] = ${keys}[${loopVar}];`);
1107
+ if (!(ast.noFlags & 1 /* ForEachNoFlag.First */)) {
1108
+ this.addLine(`ctx[\`${ast.elem}_first\`] = ${loopVar} === 0;`);
1109
+ }
1110
+ if (!(ast.noFlags & 2 /* ForEachNoFlag.Last */)) {
1111
+ this.addLine(`ctx[\`${ast.elem}_last\`] = ${loopVar} === ${keys}.length - 1;`);
1112
+ }
1113
+ if (!(ast.noFlags & 4 /* ForEachNoFlag.Index */)) {
1114
+ this.addLine(`ctx[\`${ast.elem}_index\`] = ${loopVar};`);
1115
+ }
1116
+ if (!(ast.noFlags & 8 /* ForEachNoFlag.Value */)) {
1117
+ this.addLine(`ctx[\`${ast.elem}_value\`] = ${vals}[${loopVar}];`);
1118
+ }
1119
+ this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
1120
+ if (this.dev) {
1121
+ // Throw error on duplicate keys in dev mode
1122
+ this.helpers.add("OwlError");
1123
+ this.addLine(`if (keys${block.id}.has(String(key${this.target.loopLevel}))) { throw new OwlError(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`);
1124
+ this.addLine(`keys${block.id}.add(String(key${this.target.loopLevel}));`);
1125
+ }
1126
+ const subCtx = createContext(ctx, { block, index: loopVar });
1127
+ this.compileAST(ast.body, subCtx);
1128
+ this.target.indentLevel--;
1129
+ this.target.loopLevel--;
1130
+ this.target.loopCtxVars.pop();
1131
+ this.addLine(`}`);
1132
+ this.insertBlock("l", block, ctx);
1133
+ return block.varName;
1134
+ }
1135
+ compileTKey(ast, ctx) {
1136
+ const tKeyExpr = generateId("tKey_");
1137
+ this.define(tKeyExpr, compileExpr(ast.expr));
1138
+ ctx = createContext(ctx, {
1139
+ tKeyExpr,
1140
+ block: ctx.block,
1141
+ index: ctx.index,
1142
+ });
1143
+ return this.compileAST(ast.content, ctx);
1144
+ }
1145
+ compileMulti(ast, ctx) {
1146
+ let { block, forceNewBlock } = ctx;
1147
+ const isNewBlock = !block || forceNewBlock;
1148
+ let codeIdx = this.target.code.length;
1149
+ if (isNewBlock) {
1150
+ const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
1151
+ let result = null;
1152
+ if (n <= 1) {
1153
+ for (let child of ast.content) {
1154
+ const blockName = this.compileAST(child, ctx);
1155
+ result = result || blockName;
1156
+ }
1157
+ return result;
1158
+ }
1159
+ block = this.createBlock(block, "multi", ctx);
1160
+ }
1161
+ let index = 0;
1162
+ for (let i = 0, l = ast.content.length; i < l; i++) {
1163
+ const child = ast.content[i];
1164
+ const forceNewBlock = !child.hasNoRepresentation;
1165
+ const subCtx = createContext(ctx, {
1166
+ block,
1167
+ index,
1168
+ forceNewBlock,
1169
+ });
1170
+ this.compileAST(child, subCtx);
1171
+ if (forceNewBlock) {
1172
+ index++;
1173
+ }
1174
+ }
1175
+ if (isNewBlock) {
1176
+ if (block.hasDynamicChildren && block.children.length) {
1177
+ const code = this.target.code;
1178
+ const children = block.children.slice();
1179
+ let current = children.shift();
1180
+ for (let i = codeIdx; i < code.length; i++) {
1181
+ if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
1182
+ code[i] = code[i].replace(`const ${current.varName}`, current.varName);
1183
+ current = children.shift();
1184
+ if (!current)
1185
+ break;
1186
+ }
1187
+ }
1188
+ this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
1189
+ }
1190
+ const args = block.children.map((c) => c.varName).join(", ");
1191
+ this.insertBlock(`multi([${args}])`, block, ctx);
1192
+ }
1193
+ return block.varName;
1194
+ }
1195
+ compileTCall(ast, ctx) {
1196
+ let { block, forceNewBlock } = ctx;
1197
+ const attrs = ast.attrs
1198
+ ? this.formatPropObject(ast.attrs, ast.attrsTranslationCtx, ctx.translationCtx)
1199
+ : [];
1200
+ const isDynamic = INTERP_REGEXP.test(ast.name);
1201
+ const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`";
1202
+ if (block && !forceNewBlock) {
1203
+ this.insertAnchor(block);
1204
+ }
1205
+ block = this.createBlock(block, "multi", ctx);
1206
+ if (ast.body) {
1207
+ const name = this.compileInNewTarget("callBody", ast.body, ctx);
1208
+ const zeroStr = generateId("lazyBlock");
1209
+ this.define(zeroStr, `${name}.bind(this, ctx)`);
1210
+ this.helpers.add("zero");
1211
+ attrs.push(`[zero]: ${zeroStr}`);
1212
+ }
1213
+ let ctxExpr;
1214
+ const ctxString = `{${attrs.join(", ")}}`;
1215
+ if (ast.context) {
1216
+ const dynCtxVar = generateId("ctx");
1217
+ this.addLine(`const ${dynCtxVar} = ${compileExpr(ast.context)};`);
1218
+ if (attrs.length) {
1219
+ ctxExpr = `Object.assign({this: ${dynCtxVar}}, ${ctxString})`;
1220
+ }
1221
+ else {
1222
+ ctxExpr = `{this: ${dynCtxVar}}`;
1223
+ }
1224
+ }
1225
+ else {
1226
+ if (attrs.length === 0) {
1227
+ ctxExpr = "ctx";
1228
+ }
1229
+ else {
1230
+ ctxExpr = `Object.assign(Object.create(ctx), ${ctxString})`;
1231
+ }
1232
+ }
1233
+ const key = this.generateComponentKey();
1234
+ this.helpers.add("callTemplate");
1235
+ this.insertBlock(`callTemplate(${subTemplate}, this, app, ${ctxExpr}, node, ${key})`, block, {
1236
+ ...ctx,
1237
+ forceNewBlock: !block,
1238
+ });
1239
+ return block.varName;
1240
+ }
1241
+ compileTCallBlock(ast, ctx) {
1242
+ let { block, forceNewBlock } = ctx;
1243
+ if (block) {
1244
+ if (!forceNewBlock) {
1245
+ this.insertAnchor(block);
1246
+ }
1247
+ }
1248
+ block = this.createBlock(block, "multi", ctx);
1249
+ this.insertBlock(compileExpr(ast.name), block, { ...ctx, forceNewBlock: !block });
1250
+ return block.varName;
1251
+ }
1252
+ compileTSet(ast, ctx) {
1253
+ const expr = ast.value ? compileExpr(ast.value || "") : "null";
1254
+ const isOuterScope = this.target.loopLevel === 0;
1255
+ const defLevel = this.target.tSetVars.get(ast.name);
1256
+ const isReassignment = defLevel !== undefined && this.target.loopLevel > defLevel;
1257
+ if (ast.body) {
1258
+ this.helpers.add("LazyValue");
1259
+ const bodyAst = { type: 3 /* ASTType.Multi */, content: ast.body };
1260
+ const name = this.compileInNewTarget("value", bodyAst, ctx);
1261
+ let key = this.target.currentKey(ctx);
1262
+ let value = `new LazyValue(${name}, ctx, this, node, ${key})`;
1263
+ value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;
1264
+ this.helpers.add("withDefault");
1265
+ if (isReassignment) {
1266
+ const ctxVar = this.target.loopCtxVars[defLevel];
1267
+ this.addLine(`${ctxVar}[\`${ast.name}\`] = ${value};`);
1268
+ }
1269
+ else if (isOuterScope) {
1270
+ this.target.needsScopeProtection = true;
1271
+ this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
1272
+ this.target.tSetVars.set(ast.name, 0);
1273
+ }
1274
+ else {
1275
+ this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
1276
+ this.target.tSetVars.set(ast.name, this.target.loopLevel);
1277
+ }
1278
+ }
1279
+ else {
1280
+ let value;
1281
+ if (ast.defaultValue) {
1282
+ const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue);
1283
+ if (ast.value) {
1284
+ this.helpers.add("withDefault");
1285
+ value = `withDefault(${expr}, ${defaultValue})`;
1286
+ }
1287
+ else {
1288
+ value = defaultValue;
1289
+ }
1290
+ }
1291
+ else {
1292
+ value = expr;
1293
+ }
1294
+ if (isReassignment) {
1295
+ const ctxVar = this.target.loopCtxVars[defLevel];
1296
+ this.addLine(`${ctxVar}["${ast.name}"] = ${value};`);
1297
+ }
1298
+ else if (isOuterScope) {
1299
+ this.target.needsScopeProtection = true;
1300
+ this.addLine(`ctx["${ast.name}"] = ${value};`);
1301
+ this.target.tSetVars.set(ast.name, 0);
1302
+ }
1303
+ else {
1304
+ this.addLine(`ctx["${ast.name}"] = ${value};`);
1305
+ this.target.tSetVars.set(ast.name, this.target.loopLevel);
1306
+ }
1307
+ }
1308
+ return null;
1309
+ }
1310
+ generateComponentKey(currentKey = "key") {
1311
+ const parts = [generateId("__")];
1312
+ for (let i = 0; i < this.target.loopLevel; i++) {
1313
+ parts.push(`\${key${i + 1}}`);
1314
+ }
1315
+ return `${currentKey} + \`${parts.join("__")}\``;
1316
+ }
1317
+ /**
1318
+ * Formats a prop name and value into a string suitable to be inserted in the
1319
+ * generated code. For example:
1320
+ *
1321
+ * Name Value Result
1322
+ * ---------------------------------------------------------
1323
+ * "number" "state" "number: ctx['state']"
1324
+ * "something" "" "something: undefined"
1325
+ * "some-prop" "state" "'some-prop': ctx['state']"
1326
+ * "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
1327
+ */
1328
+ formatProp(name, value, attrsTranslationCtx, translationCtx) {
1329
+ if (name.endsWith(".translate")) {
1330
+ const attrTranslationCtx = attrsTranslationCtx?.[name] || translationCtx;
1331
+ value = toStringExpression(this.translateFn(value, attrTranslationCtx));
1332
+ }
1333
+ else {
1334
+ value = compileExpr(value);
1335
+ }
1336
+ if (name.includes(".")) {
1337
+ let [_name, suffix] = name.split(".");
1338
+ name = _name;
1339
+ switch (suffix) {
1340
+ case "bind":
1341
+ value = `(${value}).bind(this)`;
1342
+ break;
1343
+ case "alike":
1344
+ case "translate":
1345
+ break;
1346
+ default:
1347
+ throw new OwlError(`Invalid prop suffix: ${suffix}`);
1348
+ }
1349
+ }
1350
+ name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
1351
+ return `${name}: ${value || undefined}`;
1352
+ }
1353
+ formatPropObject(obj, attrsTranslationCtx, translationCtx) {
1354
+ return Object.entries(obj).map(([k, v]) => this.formatProp(k, v, attrsTranslationCtx, translationCtx));
1355
+ }
1356
+ getPropString(props, dynProps) {
1357
+ let propString = `{${props.join(",")}}`;
1358
+ if (dynProps) {
1359
+ propString = `Object.assign({}, ${compileExpr(dynProps)}${props.length ? ", " + propString : ""})`;
1360
+ }
1361
+ return propString;
1362
+ }
1363
+ compileComponent(ast, ctx) {
1364
+ let { block } = ctx;
1365
+ // props
1366
+ const hasSlotsProp = "slots" in (ast.props || {});
1367
+ const props = [];
1368
+ const propList = [];
1369
+ for (let p in ast.props || {}) {
1370
+ let [name, suffix] = p.split(".");
1371
+ if (suffix) {
1372
+ // .alike, .bind, .translate — delegate to formatProp, no propList entry
1373
+ props.push(this.formatProp(p, ast.props[p], ast.propsTranslationCtx, ctx.translationCtx));
1374
+ continue;
1375
+ }
1376
+ const { expr: compiledValue, freeVariables } = processExpr(ast.props[p]);
1377
+ const propName = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
1378
+ props.push(`${propName}: ${compiledValue || undefined}`);
1379
+ if (freeVariables) {
1380
+ for (const varName of freeVariables) {
1381
+ const syntheticKey = `\x01${name}.${varName}`;
1382
+ propList.push(`"${syntheticKey}"`);
1383
+ props.push(`"${syntheticKey}": ctx['${varName}']`);
1384
+ }
1385
+ }
1386
+ else {
1387
+ propList.push(`"${name}"`);
1388
+ }
1389
+ }
1390
+ // slots
1391
+ let slotDef = "";
1392
+ if (ast.slots) {
1393
+ let slotStr = [];
1394
+ for (let slotName in ast.slots) {
1395
+ const slotAst = ast.slots[slotName];
1396
+ const params = [];
1397
+ if (slotAst.content) {
1398
+ const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
1399
+ params.push(`__render: ${name}.bind(this), __ctx: ctx`);
1400
+ }
1401
+ const scope = ast.slots[slotName].scope;
1402
+ if (scope) {
1403
+ params.push(`__scope: "${scope}"`);
1404
+ }
1405
+ if (ast.slots[slotName].attrs) {
1406
+ params.push(...this.formatPropObject(ast.slots[slotName].attrs, ast.slots[slotName].attrsTranslationCtx, ctx.translationCtx));
1407
+ }
1408
+ const slotInfo = `{${params.join(", ")}}`;
1409
+ slotStr.push(`'${slotName}': ${slotInfo}`);
1410
+ }
1411
+ slotDef = `{${slotStr.join(", ")}}`;
1412
+ }
1413
+ if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
1414
+ this.helpers.add("markRaw");
1415
+ props.push(`slots: markRaw(${slotDef})`);
1416
+ }
1417
+ let propString = this.getPropString(props, ast.dynamicProps);
1418
+ let propVar;
1419
+ if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
1420
+ propVar = generateId("props");
1421
+ this.define(propVar, propString);
1422
+ propString = propVar;
1423
+ }
1424
+ if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
1425
+ this.helpers.add("markRaw");
1426
+ this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);
1427
+ }
1428
+ // cmap key
1429
+ let expr;
1430
+ if (ast.isDynamic) {
1431
+ expr = generateId("Comp");
1432
+ this.define(expr, compileExpr(ast.name));
1433
+ }
1434
+ else {
1435
+ expr = `\`${ast.name}\``;
1436
+ }
1437
+ if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) {
1438
+ // todo: check the forcenewblock condition
1439
+ this.insertAnchor(block);
1440
+ }
1441
+ let keyArg = this.generateComponentKey();
1442
+ if (ctx.tKeyExpr) {
1443
+ keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
1444
+ }
1445
+ let id = generateId("comp");
1446
+ this.helpers.add("createComponent");
1447
+ this.staticDefs.push({
1448
+ id,
1449
+ expr: `createComponent(app, ${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, [${propList}])`,
1450
+ });
1451
+ if (ast.isDynamic) {
1452
+ // If the component class changes, this can cause delayed renders to go
1453
+ // through if the key doesn't change. Use the component name for now.
1454
+ // This means that two component classes with the same name isn't supported
1455
+ // in t-component. We can generate a unique id per class later if needed.
1456
+ keyArg = `(${expr}).name + ${keyArg}`;
1457
+ }
1458
+ let blockExpr = `${id}(${propString}, ${keyArg}, node, this, ${ast.isDynamic ? expr : null})`;
1459
+ if (ast.isDynamic) {
1460
+ blockExpr = `toggler(${expr}, ${blockExpr})`;
1461
+ }
1462
+ // event handling
1463
+ if (ast.on) {
1464
+ blockExpr = this.wrapWithEventCatcher(blockExpr, ast.on);
1465
+ }
1466
+ block = this.createBlock(block, "multi", ctx);
1467
+ this.insertBlock(blockExpr, block, ctx);
1468
+ return block.varName;
1469
+ }
1470
+ wrapWithEventCatcher(expr, on) {
1471
+ this.helpers.add("createCatcher");
1472
+ let name = generateId("catcher");
1473
+ let spec = {};
1474
+ let handlers = [];
1475
+ for (let ev in on) {
1476
+ let handlerId = generateId("hdlr");
1477
+ let idx = handlers.push(handlerId) - 1;
1478
+ spec[ev] = idx;
1479
+ const handler = this.generateHandlerCode(ev, on[ev]);
1480
+ this.define(handlerId, handler);
1481
+ }
1482
+ this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` });
1483
+ return `${name}(${expr}, [${handlers.join(",")}])`;
1484
+ }
1485
+ compileTCallSlot(ast, ctx) {
1486
+ this.helpers.add("callSlot");
1487
+ let { block } = ctx;
1488
+ let blockString;
1489
+ let slotName;
1490
+ let dynamic = false;
1491
+ let isMultiple = false;
1492
+ if (ast.name.match(INTERP_REGEXP)) {
1493
+ dynamic = true;
1494
+ isMultiple = true;
1495
+ slotName = interpolate(ast.name);
1496
+ }
1497
+ else {
1498
+ slotName = "'" + ast.name + "'";
1499
+ isMultiple = isMultiple || this.slotNames.has(ast.name);
1500
+ this.slotNames.add(ast.name);
1501
+ }
1502
+ const attrs = { ...ast.attrs };
1503
+ const dynProps = attrs["t-props"];
1504
+ delete attrs["t-props"];
1505
+ let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
1506
+ if (isMultiple) {
1507
+ key = this.generateComponentKey(key);
1508
+ }
1509
+ const props = ast.attrs
1510
+ ? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
1511
+ : [];
1512
+ const scope = this.getPropString(props, dynProps);
1513
+ if (ast.defaultContent) {
1514
+ const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
1515
+ blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope}, ${name}.bind(this))`;
1516
+ }
1517
+ else {
1518
+ if (dynamic) {
1519
+ let name = generateId("slot");
1520
+ this.define(name, slotName);
1521
+ blockString = `toggler(${name}, callSlot(ctx, node, ${key}, ${name}, ${dynamic}, ${scope}))`;
1522
+ }
1523
+ else {
1524
+ blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope})`;
1525
+ }
1526
+ }
1527
+ // event handling
1528
+ if (ast.on) {
1529
+ blockString = this.wrapWithEventCatcher(blockString, ast.on);
1530
+ }
1531
+ if (block) {
1532
+ this.insertAnchor(block);
1533
+ }
1534
+ block = this.createBlock(block, "multi", ctx);
1535
+ this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
1536
+ return block.varName;
1537
+ }
1538
+ compileTTranslation(ast, ctx) {
1539
+ if (ast.content) {
1540
+ return this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));
1541
+ }
1542
+ return null;
1543
+ }
1544
+ compileTTranslationContext(ast, ctx) {
1545
+ if (ast.content) {
1546
+ return this.compileAST(ast.content, Object.assign({}, ctx, { translationCtx: ast.translationCtx }));
1547
+ }
1548
+ return null;
1549
+ }
1578
1550
  }
1579
1551
 
1580
- /**
1581
- * Parses an XML string into an XML document, throwing errors on parser errors
1582
- * instead of returning an XML document containing the parseerror.
1583
- *
1584
- * @param xml the string to parse
1585
- * @returns an XML document corresponding to the content of the string
1586
- */
1587
- function parseXML(xml) {
1588
- const parser = new DOMParser();
1589
- const doc = parser.parseFromString(xml, "text/xml");
1590
- if (doc.getElementsByTagName("parsererror").length) {
1591
- let msg = "Invalid XML in template.";
1592
- const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
1593
- if (parsererrorText) {
1594
- msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
1595
- const re = /\d+/g;
1596
- const firstMatch = re.exec(parsererrorText);
1597
- if (firstMatch) {
1598
- const lineNumber = Number(firstMatch[0]);
1599
- const line = xml.split("\n")[lineNumber - 1];
1600
- const secondMatch = re.exec(parsererrorText);
1601
- if (line && secondMatch) {
1602
- const columnIndex = Number(secondMatch[0]) - 1;
1603
- if (line[columnIndex]) {
1604
- msg +=
1605
- `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
1606
- `${line}\n${"-".repeat(columnIndex - 1)}^`;
1607
- }
1608
- }
1609
- }
1610
- }
1611
- throw new OwlError(msg);
1612
- }
1613
- return doc;
1552
+ /**
1553
+ * Parses an XML string into an XML document, throwing errors on parser errors
1554
+ * instead of returning an XML document containing the parseerror.
1555
+ *
1556
+ * @param xml the string to parse
1557
+ * @returns an XML document corresponding to the content of the string
1558
+ */
1559
+ function parseXML(xml) {
1560
+ const parser = new DOMParser();
1561
+ const doc = parser.parseFromString(xml, "text/xml");
1562
+ if (doc.getElementsByTagName("parsererror").length) {
1563
+ let msg = "Invalid XML in template.";
1564
+ const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
1565
+ if (parsererrorText) {
1566
+ msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
1567
+ const re = /\d+/g;
1568
+ const firstMatch = re.exec(parsererrorText);
1569
+ if (firstMatch) {
1570
+ const lineNumber = Number(firstMatch[0]);
1571
+ const line = xml.split("\n")[lineNumber - 1];
1572
+ const secondMatch = re.exec(parsererrorText);
1573
+ if (line && secondMatch) {
1574
+ const columnIndex = Number(secondMatch[0]) - 1;
1575
+ if (line[columnIndex]) {
1576
+ msg +=
1577
+ `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
1578
+ `${line}\n${"-".repeat(columnIndex - 1)}^`;
1579
+ }
1580
+ }
1581
+ }
1582
+ }
1583
+ throw new OwlError(msg);
1584
+ }
1585
+ return doc;
1614
1586
  }
1615
1587
 
1616
- // -----------------------------------------------------------------------------
1617
- // Parser
1618
- // -----------------------------------------------------------------------------
1619
- const cache = new WeakMap();
1620
- function parse(xml, customDir) {
1621
- const ctx = {
1622
- inPreTag: false,
1623
- customDirectives: customDir,
1624
- };
1625
- if (typeof xml === "string") {
1626
- const elem = parseXML(`<t>${xml}</t>`).firstChild;
1627
- return _parse(elem, ctx);
1628
- }
1629
- let ast = cache.get(xml);
1630
- if (!ast) {
1631
- // we clone here the xml to prevent modifying it in place
1632
- ast = _parse(xml.cloneNode(true), ctx);
1633
- cache.set(xml, ast);
1634
- }
1635
- return ast;
1636
- }
1637
- function _parse(xml, ctx) {
1638
- normalizeXML(xml);
1639
- return parseNode(xml, ctx) || { type: 0 /* Text */, value: "" };
1640
- }
1641
- function parseNode(node, ctx) {
1642
- if (!(node instanceof Element)) {
1643
- return parseTextCommentNode(node, ctx);
1644
- }
1645
- return (parseTCustom(node, ctx) ||
1646
- parseTDebugLog(node, ctx) ||
1647
- parseTForEach(node, ctx) ||
1648
- parseTIf(node, ctx) ||
1649
- parseTPortal(node, ctx) ||
1650
- parseTCall(node, ctx) ||
1651
- parseTCallBlock(node) ||
1652
- parseTTranslation(node, ctx) ||
1653
- parseTTranslationContext(node, ctx) ||
1654
- parseTKey(node, ctx) ||
1655
- parseTEscNode(node, ctx) ||
1656
- parseTOutNode(node, ctx) ||
1657
- parseTSlot(node, ctx) ||
1658
- parseComponent(node, ctx) ||
1659
- parseDOMNode(node, ctx) ||
1660
- parseTSetNode(node, ctx) ||
1661
- parseTNode(node, ctx));
1662
- }
1663
- // -----------------------------------------------------------------------------
1664
- // <t /> tag
1665
- // -----------------------------------------------------------------------------
1666
- function parseTNode(node, ctx) {
1667
- if (node.tagName !== "t") {
1668
- return null;
1669
- }
1670
- return parseChildNodes(node, ctx);
1671
- }
1672
- // -----------------------------------------------------------------------------
1673
- // Text and Comment Nodes
1674
- // -----------------------------------------------------------------------------
1675
- const lineBreakRE = /[\r\n]/;
1676
- function parseTextCommentNode(node, ctx) {
1677
- if (node.nodeType === Node.TEXT_NODE) {
1678
- let value = node.textContent || "";
1679
- if (!ctx.inPreTag && lineBreakRE.test(value) && !value.trim()) {
1680
- return null;
1681
- }
1682
- return { type: 0 /* Text */, value };
1683
- }
1684
- else if (node.nodeType === Node.COMMENT_NODE) {
1685
- return { type: 1 /* Comment */, value: node.textContent || "" };
1686
- }
1687
- return null;
1688
- }
1689
- function parseTCustom(node, ctx) {
1690
- if (!ctx.customDirectives) {
1691
- return null;
1692
- }
1693
- const nodeAttrsNames = node.getAttributeNames();
1694
- for (let attr of nodeAttrsNames) {
1695
- if (attr === "t-custom" || attr === "t-custom-") {
1696
- throw new OwlError("Missing custom directive name with t-custom directive");
1697
- }
1698
- if (attr.startsWith("t-custom-")) {
1699
- const directiveName = attr.split(".")[0].slice(9);
1700
- const customDirective = ctx.customDirectives[directiveName];
1701
- if (!customDirective) {
1702
- throw new OwlError(`Custom directive "${directiveName}" is not defined`);
1703
- }
1704
- const value = node.getAttribute(attr);
1705
- const modifiers = attr.split(".").slice(1);
1706
- node.removeAttribute(attr);
1707
- try {
1708
- customDirective(node, value, modifiers);
1709
- }
1710
- catch (error) {
1711
- throw new OwlError(`Custom directive "${directiveName}" throw the following error: ${error}`);
1712
- }
1713
- return parseNode(node, ctx);
1714
- }
1715
- }
1716
- return null;
1717
- }
1718
- // -----------------------------------------------------------------------------
1719
- // debugging
1720
- // -----------------------------------------------------------------------------
1721
- function parseTDebugLog(node, ctx) {
1722
- if (node.hasAttribute("t-debug")) {
1723
- node.removeAttribute("t-debug");
1724
- const content = parseNode(node, ctx);
1725
- const ast = {
1726
- type: 12 /* TDebug */,
1727
- content,
1728
- };
1729
- if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
1730
- ast.hasNoRepresentation = true;
1731
- }
1732
- return ast;
1733
- }
1734
- if (node.hasAttribute("t-log")) {
1735
- const expr = node.getAttribute("t-log");
1736
- node.removeAttribute("t-log");
1737
- const content = parseNode(node, ctx);
1738
- const ast = {
1739
- type: 13 /* TLog */,
1740
- expr,
1741
- content,
1742
- };
1743
- if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
1744
- ast.hasNoRepresentation = true;
1745
- }
1746
- return ast;
1747
- }
1748
- return null;
1749
- }
1750
- // -----------------------------------------------------------------------------
1751
- // Regular dom node
1752
- // -----------------------------------------------------------------------------
1753
- const hasDotAtTheEnd = /\.[\w_]+\s*$/;
1754
- const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
1755
- const ROOT_SVG_TAGS = new Set(["svg", "g", "path"]);
1756
- function parseDOMNode(node, ctx) {
1757
- const { tagName } = node;
1758
- const dynamicTag = node.getAttribute("t-tag");
1759
- node.removeAttribute("t-tag");
1760
- if (tagName === "t" && !dynamicTag) {
1761
- return null;
1762
- }
1763
- if (tagName.startsWith("block-")) {
1764
- throw new OwlError(`Invalid tag name: '${tagName}'`);
1765
- }
1766
- ctx = Object.assign({}, ctx);
1767
- if (tagName === "pre") {
1768
- ctx.inPreTag = true;
1769
- }
1770
- let ns = !ctx.nameSpace && ROOT_SVG_TAGS.has(tagName) ? "http://www.w3.org/2000/svg" : null;
1771
- const ref = node.getAttribute("t-ref");
1772
- node.removeAttribute("t-ref");
1773
- const nodeAttrsNames = node.getAttributeNames();
1774
- let attrs = null;
1775
- let attrsTranslationCtx = null;
1776
- let on = null;
1777
- let model = null;
1778
- for (let attr of nodeAttrsNames) {
1779
- const value = node.getAttribute(attr);
1780
- if (attr === "t-on" || attr === "t-on-") {
1781
- throw new OwlError("Missing event name with t-on directive");
1782
- }
1783
- if (attr.startsWith("t-on-")) {
1784
- on = on || {};
1785
- on[attr.slice(5)] = value;
1786
- }
1787
- else if (attr.startsWith("t-model")) {
1788
- if (!["input", "select", "textarea"].includes(tagName)) {
1789
- throw new OwlError("The t-model directive only works with <input>, <textarea> and <select>");
1790
- }
1791
- let baseExpr, expr;
1792
- if (hasDotAtTheEnd.test(value)) {
1793
- const index = value.lastIndexOf(".");
1794
- baseExpr = value.slice(0, index);
1795
- expr = `'${value.slice(index + 1)}'`;
1796
- }
1797
- else if (hasBracketsAtTheEnd.test(value)) {
1798
- const index = value.lastIndexOf("[");
1799
- baseExpr = value.slice(0, index);
1800
- expr = value.slice(index + 1, -1);
1801
- }
1802
- else {
1803
- throw new OwlError(`Invalid t-model expression: "${value}" (it should be assignable)`);
1804
- }
1805
- const typeAttr = node.getAttribute("type");
1806
- const isInput = tagName === "input";
1807
- const isSelect = tagName === "select";
1808
- const isCheckboxInput = isInput && typeAttr === "checkbox";
1809
- const isRadioInput = isInput && typeAttr === "radio";
1810
- const hasTrimMod = attr.includes(".trim");
1811
- const hasLazyMod = hasTrimMod || attr.includes(".lazy");
1812
- const hasNumberMod = attr.includes(".number");
1813
- const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
1814
- model = {
1815
- baseExpr,
1816
- expr,
1817
- targetAttr: isCheckboxInput ? "checked" : "value",
1818
- specialInitTargetAttr: isRadioInput ? "checked" : null,
1819
- eventType,
1820
- hasDynamicChildren: false,
1821
- shouldTrim: hasTrimMod,
1822
- shouldNumberize: hasNumberMod,
1823
- };
1824
- if (isSelect) {
1825
- // don't pollute the original ctx
1826
- ctx = Object.assign({}, ctx);
1827
- ctx.tModelInfo = model;
1828
- }
1829
- }
1830
- else if (attr.startsWith("block-")) {
1831
- throw new OwlError(`Invalid attribute: '${attr}'`);
1832
- }
1833
- else if (attr === "xmlns") {
1834
- ns = value;
1835
- }
1836
- else if (attr.startsWith("t-translation-context-")) {
1837
- const attrName = attr.slice(22);
1838
- attrsTranslationCtx = attrsTranslationCtx || {};
1839
- attrsTranslationCtx[attrName] = value;
1840
- }
1841
- else if (attr !== "t-name") {
1842
- if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
1843
- throw new OwlError(`Unknown QWeb directive: '${attr}'`);
1844
- }
1845
- const tModel = ctx.tModelInfo;
1846
- if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
1847
- tModel.hasDynamicChildren = true;
1848
- }
1849
- attrs = attrs || {};
1850
- attrs[attr] = value;
1851
- }
1852
- }
1853
- if (ns) {
1854
- ctx.nameSpace = ns;
1855
- }
1856
- const children = parseChildren(node, ctx);
1857
- return {
1858
- type: 2 /* DomNode */,
1859
- tag: tagName,
1860
- dynamicTag,
1861
- attrs,
1862
- attrsTranslationCtx,
1863
- on,
1864
- ref,
1865
- content: children,
1866
- model,
1867
- ns,
1868
- };
1869
- }
1870
- // -----------------------------------------------------------------------------
1871
- // t-esc
1872
- // -----------------------------------------------------------------------------
1873
- function parseTEscNode(node, ctx) {
1874
- if (!node.hasAttribute("t-esc")) {
1875
- return null;
1876
- }
1877
- const escValue = node.getAttribute("t-esc");
1878
- node.removeAttribute("t-esc");
1879
- const tesc = {
1880
- type: 4 /* TEsc */,
1881
- expr: escValue,
1882
- defaultValue: node.textContent || "",
1883
- };
1884
- let ref = node.getAttribute("t-ref");
1885
- node.removeAttribute("t-ref");
1886
- const ast = parseNode(node, ctx);
1887
- if (!ast) {
1888
- return tesc;
1889
- }
1890
- if (ast.type === 2 /* DomNode */) {
1891
- return {
1892
- ...ast,
1893
- ref,
1894
- content: [tesc],
1895
- };
1896
- }
1897
- return tesc;
1898
- }
1899
- // -----------------------------------------------------------------------------
1900
- // t-out
1901
- // -----------------------------------------------------------------------------
1902
- function parseTOutNode(node, ctx) {
1903
- if (!node.hasAttribute("t-out") && !node.hasAttribute("t-raw")) {
1904
- return null;
1905
- }
1906
- if (node.hasAttribute("t-raw")) {
1907
- console.warn(`t-raw has been deprecated in favor of t-out. If the value to render is not wrapped by the "markup" function, it will be escaped`);
1908
- }
1909
- const expr = (node.getAttribute("t-out") || node.getAttribute("t-raw"));
1910
- node.removeAttribute("t-out");
1911
- node.removeAttribute("t-raw");
1912
- const tOut = { type: 8 /* TOut */, expr, body: null };
1913
- const ref = node.getAttribute("t-ref");
1914
- node.removeAttribute("t-ref");
1915
- const ast = parseNode(node, ctx);
1916
- if (!ast) {
1917
- return tOut;
1918
- }
1919
- if (ast.type === 2 /* DomNode */) {
1920
- tOut.body = ast.content.length ? ast.content : null;
1921
- return {
1922
- ...ast,
1923
- ref,
1924
- content: [tOut],
1925
- };
1926
- }
1927
- return tOut;
1928
- }
1929
- // -----------------------------------------------------------------------------
1930
- // t-foreach and t-key
1931
- // -----------------------------------------------------------------------------
1932
- function parseTForEach(node, ctx) {
1933
- if (!node.hasAttribute("t-foreach")) {
1934
- return null;
1935
- }
1936
- const html = node.outerHTML;
1937
- const collection = node.getAttribute("t-foreach");
1938
- node.removeAttribute("t-foreach");
1939
- const elem = node.getAttribute("t-as") || "";
1940
- node.removeAttribute("t-as");
1941
- const key = node.getAttribute("t-key");
1942
- if (!key) {
1943
- throw new OwlError(`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`);
1944
- }
1945
- node.removeAttribute("t-key");
1946
- const memo = node.getAttribute("t-memo") || "";
1947
- node.removeAttribute("t-memo");
1948
- const body = parseNode(node, ctx);
1949
- if (!body) {
1950
- return null;
1951
- }
1952
- const hasNoTCall = !html.includes("t-call");
1953
- const hasNoFirst = hasNoTCall && !html.includes(`${elem}_first`);
1954
- const hasNoLast = hasNoTCall && !html.includes(`${elem}_last`);
1955
- const hasNoIndex = hasNoTCall && !html.includes(`${elem}_index`);
1956
- const hasNoValue = hasNoTCall && !html.includes(`${elem}_value`);
1957
- return {
1958
- type: 9 /* TForEach */,
1959
- collection,
1960
- elem,
1961
- body,
1962
- memo,
1963
- key,
1964
- hasNoFirst,
1965
- hasNoLast,
1966
- hasNoIndex,
1967
- hasNoValue,
1968
- };
1969
- }
1970
- function parseTKey(node, ctx) {
1971
- if (!node.hasAttribute("t-key")) {
1972
- return null;
1973
- }
1974
- const key = node.getAttribute("t-key");
1975
- node.removeAttribute("t-key");
1976
- const content = parseNode(node, ctx);
1977
- if (!content) {
1978
- return null;
1979
- }
1980
- const ast = {
1981
- type: 10 /* TKey */,
1982
- expr: key,
1983
- content,
1984
- };
1985
- if (content.hasNoRepresentation) {
1986
- ast.hasNoRepresentation = true;
1987
- }
1988
- return ast;
1989
- }
1990
- // -----------------------------------------------------------------------------
1991
- // t-call
1992
- // -----------------------------------------------------------------------------
1993
- function parseTCall(node, ctx) {
1994
- if (!node.hasAttribute("t-call")) {
1995
- return null;
1996
- }
1997
- const subTemplate = node.getAttribute("t-call");
1998
- const context = node.getAttribute("t-call-context");
1999
- node.removeAttribute("t-call");
2000
- node.removeAttribute("t-call-context");
2001
- if (node.tagName !== "t") {
2002
- const ast = parseNode(node, ctx);
2003
- const tcall = { type: 7 /* TCall */, name: subTemplate, body: null, context };
2004
- if (ast && ast.type === 2 /* DomNode */) {
2005
- ast.content = [tcall];
2006
- return ast;
2007
- }
2008
- if (ast && ast.type === 11 /* TComponent */) {
2009
- return {
2010
- ...ast,
2011
- slots: {
2012
- default: {
2013
- content: tcall,
2014
- scope: null,
2015
- on: null,
2016
- attrs: null,
2017
- attrsTranslationCtx: null,
2018
- },
2019
- },
2020
- };
2021
- }
2022
- }
2023
- const body = parseChildren(node, ctx);
2024
- return {
2025
- type: 7 /* TCall */,
2026
- name: subTemplate,
2027
- body: body.length ? body : null,
2028
- context,
2029
- };
2030
- }
2031
- // -----------------------------------------------------------------------------
2032
- // t-call-block
2033
- // -----------------------------------------------------------------------------
2034
- function parseTCallBlock(node, ctx) {
2035
- if (!node.hasAttribute("t-call-block")) {
2036
- return null;
2037
- }
2038
- const name = node.getAttribute("t-call-block");
2039
- return {
2040
- type: 15 /* TCallBlock */,
2041
- name,
2042
- };
2043
- }
2044
- // -----------------------------------------------------------------------------
2045
- // t-if
2046
- // -----------------------------------------------------------------------------
2047
- function parseTIf(node, ctx) {
2048
- if (!node.hasAttribute("t-if")) {
2049
- return null;
2050
- }
2051
- const condition = node.getAttribute("t-if");
2052
- node.removeAttribute("t-if");
2053
- const content = parseNode(node, ctx) || { type: 0 /* Text */, value: "" };
2054
- let nextElement = node.nextElementSibling;
2055
- // t-elifs
2056
- const tElifs = [];
2057
- while (nextElement && nextElement.hasAttribute("t-elif")) {
2058
- const condition = nextElement.getAttribute("t-elif");
2059
- nextElement.removeAttribute("t-elif");
2060
- const tElif = parseNode(nextElement, ctx);
2061
- const next = nextElement.nextElementSibling;
2062
- nextElement.remove();
2063
- nextElement = next;
2064
- if (tElif) {
2065
- tElifs.push({ condition, content: tElif });
2066
- }
2067
- }
2068
- // t-else
2069
- let tElse = null;
2070
- if (nextElement && nextElement.hasAttribute("t-else")) {
2071
- nextElement.removeAttribute("t-else");
2072
- tElse = parseNode(nextElement, ctx);
2073
- nextElement.remove();
2074
- }
2075
- return {
2076
- type: 5 /* TIf */,
2077
- condition,
2078
- content,
2079
- tElif: tElifs.length ? tElifs : null,
2080
- tElse,
2081
- };
2082
- }
2083
- // -----------------------------------------------------------------------------
2084
- // t-set directive
2085
- // -----------------------------------------------------------------------------
2086
- function parseTSetNode(node, ctx) {
2087
- if (!node.hasAttribute("t-set")) {
2088
- return null;
2089
- }
2090
- const name = node.getAttribute("t-set");
2091
- const value = node.getAttribute("t-value") || null;
2092
- const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
2093
- let body = null;
2094
- if (node.textContent !== node.innerHTML) {
2095
- body = parseChildren(node, ctx);
2096
- }
2097
- return { type: 6 /* TSet */, name, value, defaultValue, body, hasNoRepresentation: true };
2098
- }
2099
- // -----------------------------------------------------------------------------
2100
- // Components
2101
- // -----------------------------------------------------------------------------
2102
- // Error messages when trying to use an unsupported directive on a component
2103
- const directiveErrorMap = new Map([
2104
- [
2105
- "t-ref",
2106
- "t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
2107
- ],
2108
- ["t-att", "t-att makes no sense on component: props are already treated as expressions"],
2109
- [
2110
- "t-attf",
2111
- "t-attf is not supported on components: use template strings for string interpolation in props",
2112
- ],
2113
- ]);
2114
- function parseComponent(node, ctx) {
2115
- let name = node.tagName;
2116
- const firstLetter = name[0];
2117
- let isDynamic = node.hasAttribute("t-component");
2118
- if (isDynamic && name !== "t") {
2119
- throw new OwlError(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
2120
- }
2121
- if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
2122
- return null;
2123
- }
2124
- if (isDynamic) {
2125
- name = node.getAttribute("t-component");
2126
- node.removeAttribute("t-component");
2127
- }
2128
- const dynamicProps = node.getAttribute("t-props");
2129
- node.removeAttribute("t-props");
2130
- const defaultSlotScope = node.getAttribute("t-slot-scope");
2131
- node.removeAttribute("t-slot-scope");
2132
- let on = null;
2133
- let props = null;
2134
- let propsTranslationCtx = null;
2135
- for (let name of node.getAttributeNames()) {
2136
- const value = node.getAttribute(name);
2137
- if (name.startsWith("t-translation-context-")) {
2138
- const attrName = name.slice(22);
2139
- propsTranslationCtx = propsTranslationCtx || {};
2140
- propsTranslationCtx[attrName] = value;
2141
- }
2142
- else if (name.startsWith("t-")) {
2143
- if (name.startsWith("t-on-")) {
2144
- on = on || {};
2145
- on[name.slice(5)] = value;
2146
- }
2147
- else {
2148
- const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
2149
- throw new OwlError(message || `unsupported directive on Component: ${name}`);
2150
- }
2151
- }
2152
- else {
2153
- props = props || {};
2154
- props[name] = value;
2155
- }
2156
- }
2157
- let slots = null;
2158
- if (node.hasChildNodes()) {
2159
- const clone = node.cloneNode(true);
2160
- // named slots
2161
- const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
2162
- for (let slotNode of slotNodes) {
2163
- if (slotNode.tagName !== "t") {
2164
- throw new OwlError(`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`);
2165
- }
2166
- const name = slotNode.getAttribute("t-set-slot");
2167
- // check if this is defined in a sub component (in which case it should
2168
- // be ignored)
2169
- let el = slotNode.parentElement;
2170
- let isInSubComponent = false;
2171
- while (el && el !== clone) {
2172
- if (el.hasAttribute("t-component") || el.tagName[0] === el.tagName[0].toUpperCase()) {
2173
- isInSubComponent = true;
2174
- break;
2175
- }
2176
- el = el.parentElement;
2177
- }
2178
- if (isInSubComponent || !el) {
2179
- continue;
2180
- }
2181
- slotNode.removeAttribute("t-set-slot");
2182
- slotNode.remove();
2183
- const slotAst = parseNode(slotNode, ctx);
2184
- let on = null;
2185
- let attrs = null;
2186
- let attrsTranslationCtx = null;
2187
- let scope = null;
2188
- for (let attributeName of slotNode.getAttributeNames()) {
2189
- const value = slotNode.getAttribute(attributeName);
2190
- if (attributeName === "t-slot-scope") {
2191
- scope = value;
2192
- continue;
2193
- }
2194
- else if (attributeName.startsWith("t-translation-context-")) {
2195
- const attrName = attributeName.slice(22);
2196
- attrsTranslationCtx = attrsTranslationCtx || {};
2197
- attrsTranslationCtx[attrName] = value;
2198
- }
2199
- else if (attributeName.startsWith("t-on-")) {
2200
- on = on || {};
2201
- on[attributeName.slice(5)] = value;
2202
- }
2203
- else {
2204
- attrs = attrs || {};
2205
- attrs[attributeName] = value;
2206
- }
2207
- }
2208
- slots = slots || {};
2209
- slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
2210
- }
2211
- // default slot
2212
- const defaultContent = parseChildNodes(clone, ctx);
2213
- slots = slots || {};
2214
- // t-set-slot="default" has priority over content
2215
- if (defaultContent && !slots.default) {
2216
- slots.default = {
2217
- content: defaultContent,
2218
- on,
2219
- attrs: null,
2220
- attrsTranslationCtx: null,
2221
- scope: defaultSlotScope,
2222
- };
2223
- }
2224
- }
2225
- return {
2226
- type: 11 /* TComponent */,
2227
- name,
2228
- isDynamic,
2229
- dynamicProps,
2230
- props,
2231
- propsTranslationCtx,
2232
- slots,
2233
- on,
2234
- };
2235
- }
2236
- // -----------------------------------------------------------------------------
2237
- // Slots
2238
- // -----------------------------------------------------------------------------
2239
- function parseTSlot(node, ctx) {
2240
- if (!node.hasAttribute("t-slot")) {
2241
- return null;
2242
- }
2243
- const name = node.getAttribute("t-slot");
2244
- node.removeAttribute("t-slot");
2245
- let attrs = null;
2246
- let attrsTranslationCtx = null;
2247
- let on = null;
2248
- for (let attributeName of node.getAttributeNames()) {
2249
- const value = node.getAttribute(attributeName);
2250
- if (attributeName.startsWith("t-on-")) {
2251
- on = on || {};
2252
- on[attributeName.slice(5)] = value;
2253
- }
2254
- else if (attributeName.startsWith("t-translation-context-")) {
2255
- const attrName = attributeName.slice(22);
2256
- attrsTranslationCtx = attrsTranslationCtx || {};
2257
- attrsTranslationCtx[attrName] = value;
2258
- }
2259
- else {
2260
- attrs = attrs || {};
2261
- attrs[attributeName] = value;
2262
- }
2263
- }
2264
- return {
2265
- type: 14 /* TSlot */,
2266
- name,
2267
- attrs,
2268
- attrsTranslationCtx,
2269
- on,
2270
- defaultContent: parseChildNodes(node, ctx),
2271
- };
2272
- }
2273
- // -----------------------------------------------------------------------------
2274
- // Translation
2275
- // -----------------------------------------------------------------------------
2276
- function wrapInTTranslationAST(r) {
2277
- const ast = { type: 16 /* TTranslation */, content: r };
2278
- if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {
2279
- ast.hasNoRepresentation = true;
2280
- }
2281
- return ast;
2282
- }
2283
- function parseTTranslation(node, ctx) {
2284
- if (node.getAttribute("t-translation") !== "off") {
2285
- return null;
2286
- }
2287
- node.removeAttribute("t-translation");
2288
- const result = parseNode(node, ctx);
2289
- if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {
2290
- const children = result.content.map(wrapInTTranslationAST);
2291
- return makeASTMulti(children);
2292
- }
2293
- return wrapInTTranslationAST(result);
2294
- }
2295
- // -----------------------------------------------------------------------------
2296
- // Translation Context
2297
- // -----------------------------------------------------------------------------
2298
- function wrapInTTranslationContextAST(r, translationCtx) {
2299
- const ast = {
2300
- type: 17 /* TTranslationContext */,
2301
- content: r,
2302
- translationCtx,
2303
- };
2304
- if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {
2305
- ast.hasNoRepresentation = true;
2306
- }
2307
- return ast;
2308
- }
2309
- function parseTTranslationContext(node, ctx) {
2310
- const translationCtx = node.getAttribute("t-translation-context");
2311
- if (!translationCtx) {
2312
- return null;
2313
- }
2314
- node.removeAttribute("t-translation-context");
2315
- const result = parseNode(node, ctx);
2316
- if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {
2317
- const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
2318
- return makeASTMulti(children);
2319
- }
2320
- return wrapInTTranslationContextAST(result, translationCtx);
2321
- }
2322
- // -----------------------------------------------------------------------------
2323
- // Portal
2324
- // -----------------------------------------------------------------------------
2325
- function parseTPortal(node, ctx) {
2326
- if (!node.hasAttribute("t-portal")) {
2327
- return null;
2328
- }
2329
- const target = node.getAttribute("t-portal");
2330
- node.removeAttribute("t-portal");
2331
- const content = parseNode(node, ctx);
2332
- if (!content) {
2333
- return {
2334
- type: 0 /* Text */,
2335
- value: "",
2336
- };
2337
- }
2338
- return {
2339
- type: 18 /* TPortal */,
2340
- target,
2341
- content,
2342
- };
2343
- }
2344
- // -----------------------------------------------------------------------------
2345
- // helpers
2346
- // -----------------------------------------------------------------------------
2347
- /**
2348
- * Parse all the child nodes of a given node and return a list of ast elements
2349
- */
2350
- function parseChildren(node, ctx) {
2351
- const children = [];
2352
- for (let child of node.childNodes) {
2353
- const childAst = parseNode(child, ctx);
2354
- if (childAst) {
2355
- if (childAst.type === 3 /* Multi */) {
2356
- children.push(...childAst.content);
2357
- }
2358
- else {
2359
- children.push(childAst);
2360
- }
2361
- }
2362
- }
2363
- return children;
2364
- }
2365
- function makeASTMulti(children) {
2366
- const ast = { type: 3 /* Multi */, content: children };
2367
- if (children.every((c) => c.hasNoRepresentation)) {
2368
- ast.hasNoRepresentation = true;
2369
- }
2370
- return ast;
2371
- }
2372
- /**
2373
- * Parse all the child nodes of a given node and return an ast if possible.
2374
- * In the case there are multiple children, they are wrapped in a astmulti.
2375
- */
2376
- function parseChildNodes(node, ctx) {
2377
- const children = parseChildren(node, ctx);
2378
- switch (children.length) {
2379
- case 0:
2380
- return null;
2381
- case 1:
2382
- return children[0];
2383
- default:
2384
- return makeASTMulti(children);
2385
- }
2386
- }
2387
- /**
2388
- * Normalizes the content of an Element so that t-if/t-elif/t-else directives
2389
- * immediately follow one another (by removing empty text nodes or comments).
2390
- * Throws an error when a conditional branching statement is malformed. This
2391
- * function modifies the Element in place.
2392
- *
2393
- * @param el the element containing the tree that should be normalized
2394
- */
2395
- function normalizeTIf(el) {
2396
- let tbranch = el.querySelectorAll("[t-elif], [t-else]");
2397
- for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
2398
- let node = tbranch[i];
2399
- let prevElem = node.previousElementSibling;
2400
- let pattr = (name) => prevElem.getAttribute(name);
2401
- let nattr = (name) => +!!node.getAttribute(name);
2402
- if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
2403
- if (pattr("t-foreach")) {
2404
- throw new OwlError("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
2405
- }
2406
- if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
2407
- return a + b;
2408
- }) > 1) {
2409
- throw new OwlError("Only one conditional branching directive is allowed per node");
2410
- }
2411
- // All text (with only spaces) and comment nodes (nodeType 8) between
2412
- // branch nodes are removed
2413
- let textNode;
2414
- while ((textNode = node.previousSibling) !== prevElem) {
2415
- if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
2416
- throw new OwlError("text is not allowed between branching directives");
2417
- }
2418
- textNode.remove();
2419
- }
2420
- }
2421
- else {
2422
- throw new OwlError("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
2423
- }
2424
- }
2425
- }
2426
- /**
2427
- * Normalizes the content of an Element so that t-esc directives on components
2428
- * are removed and instead places a <t t-esc=""> as the default slot of the
2429
- * component. Also throws if the component already has content. This function
2430
- * modifies the Element in place.
2431
- *
2432
- * @param el the element containing the tree that should be normalized
2433
- */
2434
- function normalizeTEscTOut(el) {
2435
- for (const d of ["t-esc", "t-out"]) {
2436
- const elements = [...el.querySelectorAll(`[${d}]`)].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
2437
- for (const el of elements) {
2438
- if (el.childNodes.length) {
2439
- throw new OwlError(`Cannot have ${d} on a component that already has content`);
2440
- }
2441
- const value = el.getAttribute(d);
2442
- el.removeAttribute(d);
2443
- const t = el.ownerDocument.createElement("t");
2444
- if (value != null) {
2445
- t.setAttribute(d, value);
2446
- }
2447
- el.appendChild(t);
2448
- }
2449
- }
2450
- }
2451
- /**
2452
- * Normalizes the tree inside a given element and do some preliminary validation
2453
- * on it. This function modifies the Element in place.
2454
- *
2455
- * @param el the element containing the tree that should be normalized
2456
- */
2457
- function normalizeXML(el) {
2458
- normalizeTIf(el);
2459
- normalizeTEscTOut(el);
1588
+ // -----------------------------------------------------------------------------
1589
+ // Parser
1590
+ // -----------------------------------------------------------------------------
1591
+ const cache = new WeakMap();
1592
+ function parse(xml, customDir) {
1593
+ const ctx = {
1594
+ inPreTag: false,
1595
+ customDirectives: customDir,
1596
+ };
1597
+ if (typeof xml === "string") {
1598
+ const elem = parseXML(`<t>${xml}</t>`).firstChild;
1599
+ return _parse(elem, ctx);
1600
+ }
1601
+ let ast = cache.get(xml);
1602
+ if (!ast) {
1603
+ // we clone here the xml to prevent modifying it in place
1604
+ ast = _parse(xml.cloneNode(true), ctx);
1605
+ cache.set(xml, ast);
1606
+ }
1607
+ return ast;
1608
+ }
1609
+ function _parse(xml, ctx) {
1610
+ normalizeXML(xml);
1611
+ return parseNode(xml, ctx) || { type: 0 /* ASTType.Text */, value: "" };
1612
+ }
1613
+ function parseNode(node, ctx) {
1614
+ if (!(node instanceof Element)) {
1615
+ return parseTextCommentNode(node, ctx);
1616
+ }
1617
+ return (parseTCustom(node, ctx) ||
1618
+ parseTDebugLog(node, ctx) ||
1619
+ parseTForEach(node, ctx) ||
1620
+ parseTIf(node, ctx) ||
1621
+ parseTTranslation(node, ctx) ||
1622
+ parseTTranslationContext(node, ctx) ||
1623
+ parseTCall(node, ctx) ||
1624
+ parseTCallBlock(node) ||
1625
+ parseTKey(node, ctx) ||
1626
+ parseTOutNode(node, ctx) ||
1627
+ parseTCallSlot(node, ctx) ||
1628
+ parseComponent(node, ctx) ||
1629
+ parseDOMNode(node, ctx) ||
1630
+ parseTSetNode(node, ctx) ||
1631
+ parseTNode(node, ctx));
1632
+ }
1633
+ // -----------------------------------------------------------------------------
1634
+ // <t /> tag
1635
+ // -----------------------------------------------------------------------------
1636
+ function parseTNode(node, ctx) {
1637
+ if (node.tagName !== "t") {
1638
+ return null;
1639
+ }
1640
+ return parseChildNodes(node, ctx);
1641
+ }
1642
+ // -----------------------------------------------------------------------------
1643
+ // Text and Comment Nodes
1644
+ // -----------------------------------------------------------------------------
1645
+ const lineBreakRE = /[\r\n]/;
1646
+ function parseTextCommentNode(node, ctx) {
1647
+ if (node.nodeType === Node.TEXT_NODE) {
1648
+ let value = node.textContent || "";
1649
+ if (!ctx.inPreTag && lineBreakRE.test(value) && !value.trim()) {
1650
+ return null;
1651
+ }
1652
+ return { type: 0 /* ASTType.Text */, value };
1653
+ }
1654
+ else if (node.nodeType === Node.COMMENT_NODE) {
1655
+ return { type: 1 /* ASTType.Comment */, value: node.textContent || "" };
1656
+ }
1657
+ return null;
1658
+ }
1659
+ function parseTCustom(node, ctx) {
1660
+ if (!ctx.customDirectives) {
1661
+ return null;
1662
+ }
1663
+ const nodeAttrsNames = node.getAttributeNames();
1664
+ for (let attr of nodeAttrsNames) {
1665
+ if (attr === "t-custom" || attr === "t-custom-") {
1666
+ throw new OwlError("Missing custom directive name with t-custom directive");
1667
+ }
1668
+ if (attr.startsWith("t-custom-")) {
1669
+ const directiveName = attr.split(".")[0].slice(9);
1670
+ const customDirective = ctx.customDirectives[directiveName];
1671
+ if (!customDirective) {
1672
+ throw new OwlError(`Custom directive "${directiveName}" is not defined`);
1673
+ }
1674
+ const value = node.getAttribute(attr);
1675
+ const modifiers = attr.split(".").slice(1);
1676
+ node.removeAttribute(attr);
1677
+ try {
1678
+ customDirective(node, value, modifiers);
1679
+ }
1680
+ catch (error) {
1681
+ throw new OwlError(`Custom directive "${directiveName}" throw the following error: ${error}`);
1682
+ }
1683
+ return parseNode(node, ctx);
1684
+ }
1685
+ }
1686
+ return null;
1687
+ }
1688
+ // -----------------------------------------------------------------------------
1689
+ // debugging
1690
+ // -----------------------------------------------------------------------------
1691
+ function parseTDebugLog(node, ctx) {
1692
+ if (node.hasAttribute("t-debug")) {
1693
+ node.removeAttribute("t-debug");
1694
+ const content = parseNode(node, ctx);
1695
+ const ast = {
1696
+ type: 11 /* ASTType.TDebug */,
1697
+ content,
1698
+ };
1699
+ if (content?.hasNoRepresentation) {
1700
+ ast.hasNoRepresentation = true;
1701
+ }
1702
+ return ast;
1703
+ }
1704
+ if (node.hasAttribute("t-log")) {
1705
+ const expr = node.getAttribute("t-log");
1706
+ node.removeAttribute("t-log");
1707
+ const content = parseNode(node, ctx);
1708
+ const ast = {
1709
+ type: 12 /* ASTType.TLog */,
1710
+ expr,
1711
+ content,
1712
+ };
1713
+ if (content?.hasNoRepresentation) {
1714
+ ast.hasNoRepresentation = true;
1715
+ }
1716
+ return ast;
1717
+ }
1718
+ return null;
1719
+ }
1720
+ // -----------------------------------------------------------------------------
1721
+ // Regular dom node
1722
+ // -----------------------------------------------------------------------------
1723
+ const ROOT_SVG_TAGS = new Set(["svg", "g", "path"]);
1724
+ function parseDOMNode(node, ctx) {
1725
+ const { tagName } = node;
1726
+ const dynamicTag = node.getAttribute("t-tag");
1727
+ node.removeAttribute("t-tag");
1728
+ if (tagName === "t" && !dynamicTag) {
1729
+ return null;
1730
+ }
1731
+ if (tagName.startsWith("block-")) {
1732
+ throw new OwlError(`Invalid tag name: '${tagName}'`);
1733
+ }
1734
+ ctx = Object.assign({}, ctx);
1735
+ if (tagName === "pre") {
1736
+ ctx.inPreTag = true;
1737
+ }
1738
+ let ns = !ctx.nameSpace && ROOT_SVG_TAGS.has(tagName) ? "http://www.w3.org/2000/svg" : null;
1739
+ const ref = node.getAttribute("t-ref");
1740
+ node.removeAttribute("t-ref");
1741
+ const nodeAttrsNames = node.getAttributeNames();
1742
+ let attrs = null;
1743
+ let attrsTranslationCtx = null;
1744
+ let on = null;
1745
+ let model = null;
1746
+ for (let attr of nodeAttrsNames) {
1747
+ const value = node.getAttribute(attr);
1748
+ if (attr === "t-on" || attr === "t-on-") {
1749
+ throw new OwlError("Missing event name with t-on directive");
1750
+ }
1751
+ if (attr.startsWith("t-on-")) {
1752
+ on = on || {};
1753
+ on[attr.slice(5)] = value;
1754
+ }
1755
+ else if (attr.startsWith("t-model")) {
1756
+ if (!["input", "select", "textarea"].includes(tagName)) {
1757
+ throw new OwlError("The t-model directive only works with <input>, <textarea> and <select>");
1758
+ }
1759
+ const typeAttr = node.getAttribute("type");
1760
+ const isInput = tagName === "input";
1761
+ const isSelect = tagName === "select";
1762
+ const isCheckboxInput = isInput && typeAttr === "checkbox";
1763
+ const isRadioInput = isInput && typeAttr === "radio";
1764
+ const hasTrimMod = attr.includes(".trim");
1765
+ const hasLazyMod = hasTrimMod || attr.includes(".lazy");
1766
+ const hasNumberMod = attr.includes(".number");
1767
+ const hasProxyMod = attr.includes(".proxy");
1768
+ const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
1769
+ model = {
1770
+ expr: value,
1771
+ targetAttr: isCheckboxInput ? "checked" : "value",
1772
+ specialInitTargetAttr: isRadioInput ? "checked" : null,
1773
+ eventType,
1774
+ hasDynamicChildren: false,
1775
+ shouldTrim: hasTrimMod,
1776
+ shouldNumberize: hasNumberMod,
1777
+ isProxy: hasProxyMod,
1778
+ };
1779
+ if (isSelect) {
1780
+ // don't pollute the original ctx
1781
+ ctx = Object.assign({}, ctx);
1782
+ ctx.tModelInfo = model;
1783
+ }
1784
+ }
1785
+ else if (attr.startsWith("block-")) {
1786
+ throw new OwlError(`Invalid attribute: '${attr}'`);
1787
+ }
1788
+ else if (attr === "xmlns") {
1789
+ ns = value;
1790
+ }
1791
+ else if (attr.startsWith("t-translation-context-")) {
1792
+ const attrName = attr.slice(22);
1793
+ attrsTranslationCtx = attrsTranslationCtx || {};
1794
+ attrsTranslationCtx[attrName] = value;
1795
+ }
1796
+ else if (attr !== "t-name") {
1797
+ if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
1798
+ throw new OwlError(`Unknown QWeb directive: '${attr}'`);
1799
+ }
1800
+ const tModel = ctx.tModelInfo;
1801
+ if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
1802
+ tModel.hasDynamicChildren = true;
1803
+ }
1804
+ attrs = attrs || {};
1805
+ attrs[attr] = value;
1806
+ }
1807
+ }
1808
+ if (ns) {
1809
+ ctx.nameSpace = ns;
1810
+ }
1811
+ const children = parseChildren(node, ctx);
1812
+ return {
1813
+ type: 2 /* ASTType.DomNode */,
1814
+ tag: tagName,
1815
+ dynamicTag,
1816
+ attrs,
1817
+ attrsTranslationCtx,
1818
+ on,
1819
+ ref,
1820
+ content: children,
1821
+ model,
1822
+ ns,
1823
+ };
1824
+ }
1825
+ // -----------------------------------------------------------------------------
1826
+ // t-out
1827
+ // -----------------------------------------------------------------------------
1828
+ function parseTOutNode(node, ctx) {
1829
+ if (!node.hasAttribute("t-out") && !node.hasAttribute("t-esc")) {
1830
+ return null;
1831
+ }
1832
+ if (node.hasAttribute("t-esc")) {
1833
+ console.warn(`t-esc has been deprecated in favor of t-out. If the value to render is not wrapped by the "markup" function, it will be escaped`);
1834
+ }
1835
+ const expr = (node.getAttribute("t-out") || node.getAttribute("t-esc"));
1836
+ node.removeAttribute("t-out");
1837
+ node.removeAttribute("t-esc");
1838
+ const tOut = { type: 7 /* ASTType.TOut */, expr, body: null };
1839
+ const ref = node.getAttribute("t-ref");
1840
+ node.removeAttribute("t-ref");
1841
+ const ast = parseNode(node, ctx);
1842
+ if (!ast) {
1843
+ return tOut;
1844
+ }
1845
+ if (ast.type === 2 /* ASTType.DomNode */) {
1846
+ tOut.body = ast.content.length ? ast.content : null;
1847
+ return {
1848
+ ...ast,
1849
+ ref,
1850
+ content: [tOut],
1851
+ };
1852
+ }
1853
+ return tOut;
1854
+ }
1855
+ // -----------------------------------------------------------------------------
1856
+ // t-foreach and t-key
1857
+ // -----------------------------------------------------------------------------
1858
+ function parseTForEach(node, ctx) {
1859
+ if (!node.hasAttribute("t-foreach")) {
1860
+ return null;
1861
+ }
1862
+ const html = node.outerHTML;
1863
+ const collection = node.getAttribute("t-foreach");
1864
+ node.removeAttribute("t-foreach");
1865
+ const elem = node.getAttribute("t-as") || "";
1866
+ node.removeAttribute("t-as");
1867
+ const key = node.getAttribute("t-key");
1868
+ if (!key) {
1869
+ throw new OwlError(`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`);
1870
+ }
1871
+ node.removeAttribute("t-key");
1872
+ const body = parseNode(node, ctx);
1873
+ if (!body) {
1874
+ return null;
1875
+ }
1876
+ const hasNoTCall = !html.includes("t-call");
1877
+ let noFlags = 0;
1878
+ if (hasNoTCall && !html.includes(`${elem}_first`))
1879
+ noFlags |= 1 /* ForEachNoFlag.First */;
1880
+ if (hasNoTCall && !html.includes(`${elem}_last`))
1881
+ noFlags |= 2 /* ForEachNoFlag.Last */;
1882
+ if (hasNoTCall && !html.includes(`${elem}_index`))
1883
+ noFlags |= 4 /* ForEachNoFlag.Index */;
1884
+ if (hasNoTCall && !html.includes(`${elem}_value`))
1885
+ noFlags |= 8 /* ForEachNoFlag.Value */;
1886
+ return {
1887
+ type: 8 /* ASTType.TForEach */,
1888
+ collection,
1889
+ elem,
1890
+ body,
1891
+ key,
1892
+ noFlags,
1893
+ };
1894
+ }
1895
+ function parseTKey(node, ctx) {
1896
+ if (!node.hasAttribute("t-key")) {
1897
+ return null;
1898
+ }
1899
+ const key = node.getAttribute("t-key");
1900
+ node.removeAttribute("t-key");
1901
+ const content = parseNode(node, ctx);
1902
+ if (!content) {
1903
+ return null;
1904
+ }
1905
+ const ast = {
1906
+ type: 9 /* ASTType.TKey */,
1907
+ expr: key,
1908
+ content,
1909
+ };
1910
+ if (content.hasNoRepresentation) {
1911
+ ast.hasNoRepresentation = true;
1912
+ }
1913
+ return ast;
1914
+ }
1915
+ // -----------------------------------------------------------------------------
1916
+ // t-call
1917
+ // -----------------------------------------------------------------------------
1918
+ function parseTCall(node, ctx) {
1919
+ if (!node.hasAttribute("t-call")) {
1920
+ return null;
1921
+ }
1922
+ if (node.tagName !== "t") {
1923
+ throw new OwlError(`Directive 't-call' can only be used on <t> nodes (used on a <${node.tagName}>)`);
1924
+ }
1925
+ const subTemplate = node.getAttribute("t-call");
1926
+ const context = node.getAttribute("t-call-context");
1927
+ node.removeAttribute("t-call");
1928
+ node.removeAttribute("t-call-context");
1929
+ let attrs = null;
1930
+ let attrsTranslationCtx = null;
1931
+ for (let attributeName of node.getAttributeNames()) {
1932
+ const value = node.getAttribute(attributeName);
1933
+ if (attributeName.startsWith("t-translation-context-")) {
1934
+ const attrName = attributeName.slice(22);
1935
+ attrsTranslationCtx = attrsTranslationCtx || {};
1936
+ attrsTranslationCtx[attrName] = value;
1937
+ }
1938
+ else {
1939
+ attrs = attrs || {};
1940
+ attrs[attributeName] = value;
1941
+ }
1942
+ }
1943
+ const body = parseChildNodes(node, ctx);
1944
+ return {
1945
+ type: 6 /* ASTType.TCall */,
1946
+ name: subTemplate,
1947
+ attrs,
1948
+ attrsTranslationCtx,
1949
+ body,
1950
+ context,
1951
+ };
1952
+ }
1953
+ // -----------------------------------------------------------------------------
1954
+ // t-call-block
1955
+ // -----------------------------------------------------------------------------
1956
+ function parseTCallBlock(node, ctx) {
1957
+ if (!node.hasAttribute("t-call-block")) {
1958
+ return null;
1959
+ }
1960
+ const name = node.getAttribute("t-call-block");
1961
+ return {
1962
+ type: 14 /* ASTType.TCallBlock */,
1963
+ name,
1964
+ };
1965
+ }
1966
+ // -----------------------------------------------------------------------------
1967
+ // t-if
1968
+ // -----------------------------------------------------------------------------
1969
+ function parseTIf(node, ctx) {
1970
+ if (!node.hasAttribute("t-if")) {
1971
+ return null;
1972
+ }
1973
+ const condition = node.getAttribute("t-if");
1974
+ node.removeAttribute("t-if");
1975
+ const content = parseNode(node, ctx) || { type: 0 /* ASTType.Text */, value: "" };
1976
+ let nextElement = node.nextElementSibling;
1977
+ // t-elifs
1978
+ const tElifs = [];
1979
+ while (nextElement && nextElement.hasAttribute("t-elif")) {
1980
+ const condition = nextElement.getAttribute("t-elif");
1981
+ nextElement.removeAttribute("t-elif");
1982
+ const tElif = parseNode(nextElement, ctx);
1983
+ const next = nextElement.nextElementSibling;
1984
+ nextElement.remove();
1985
+ nextElement = next;
1986
+ if (tElif) {
1987
+ tElifs.push({ condition, content: tElif });
1988
+ }
1989
+ }
1990
+ // t-else
1991
+ let tElse = null;
1992
+ if (nextElement && nextElement.hasAttribute("t-else")) {
1993
+ nextElement.removeAttribute("t-else");
1994
+ tElse = parseNode(nextElement, ctx);
1995
+ nextElement.remove();
1996
+ }
1997
+ return {
1998
+ type: 4 /* ASTType.TIf */,
1999
+ condition,
2000
+ content,
2001
+ tElif: tElifs.length ? tElifs : null,
2002
+ tElse,
2003
+ };
2004
+ }
2005
+ // -----------------------------------------------------------------------------
2006
+ // t-set directive
2007
+ // -----------------------------------------------------------------------------
2008
+ function parseTSetNode(node, ctx) {
2009
+ if (!node.hasAttribute("t-set")) {
2010
+ return null;
2011
+ }
2012
+ const name = node.getAttribute("t-set");
2013
+ const value = node.getAttribute("t-value") || null;
2014
+ const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
2015
+ let body = null;
2016
+ if (node.textContent !== node.innerHTML) {
2017
+ body = parseChildren(node, ctx);
2018
+ }
2019
+ return { type: 5 /* ASTType.TSet */, name, value, defaultValue, body, hasNoRepresentation: true };
2020
+ }
2021
+ // -----------------------------------------------------------------------------
2022
+ // Components
2023
+ // -----------------------------------------------------------------------------
2024
+ // Error messages when trying to use an unsupported directive on a component
2025
+ const directiveErrorMap = new Map([
2026
+ [
2027
+ "t-ref",
2028
+ "t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
2029
+ ],
2030
+ ["t-att", "t-att makes no sense on component: props are already treated as expressions"],
2031
+ [
2032
+ "t-attf",
2033
+ "t-attf is not supported on components: use template strings for string interpolation in props",
2034
+ ],
2035
+ ]);
2036
+ function parseComponent(node, ctx) {
2037
+ let name = node.tagName;
2038
+ const firstLetter = name[0];
2039
+ let isDynamic = node.hasAttribute("t-component");
2040
+ if (isDynamic && name !== "t") {
2041
+ throw new OwlError(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
2042
+ }
2043
+ if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
2044
+ return null;
2045
+ }
2046
+ if (isDynamic) {
2047
+ name = node.getAttribute("t-component");
2048
+ node.removeAttribute("t-component");
2049
+ }
2050
+ const dynamicProps = node.getAttribute("t-props");
2051
+ node.removeAttribute("t-props");
2052
+ const defaultSlotScope = node.getAttribute("t-slot-scope");
2053
+ node.removeAttribute("t-slot-scope");
2054
+ let on = null;
2055
+ let props = null;
2056
+ let propsTranslationCtx = null;
2057
+ for (let name of node.getAttributeNames()) {
2058
+ const value = node.getAttribute(name);
2059
+ if (name.startsWith("t-translation-context-")) {
2060
+ const attrName = name.slice(22);
2061
+ propsTranslationCtx = propsTranslationCtx || {};
2062
+ propsTranslationCtx[attrName] = value;
2063
+ }
2064
+ else if (name.startsWith("t-")) {
2065
+ if (name.startsWith("t-on-")) {
2066
+ on = on || {};
2067
+ on[name.slice(5)] = value;
2068
+ }
2069
+ else {
2070
+ const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
2071
+ throw new OwlError(message || `unsupported directive on Component: ${name}`);
2072
+ }
2073
+ }
2074
+ else {
2075
+ props = props || {};
2076
+ props[name] = value;
2077
+ }
2078
+ }
2079
+ let slots = null;
2080
+ if (node.hasChildNodes()) {
2081
+ const clone = node.cloneNode(true);
2082
+ // named slots
2083
+ const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
2084
+ for (let slotNode of slotNodes) {
2085
+ if (slotNode.tagName !== "t") {
2086
+ throw new OwlError(`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`);
2087
+ }
2088
+ const name = slotNode.getAttribute("t-set-slot");
2089
+ // check if this is defined in a sub component (in which case it should
2090
+ // be ignored)
2091
+ let el = slotNode.parentElement;
2092
+ let isInSubComponent = false;
2093
+ while (el && el !== clone) {
2094
+ if (el.hasAttribute("t-component") || el.tagName[0] === el.tagName[0].toUpperCase()) {
2095
+ isInSubComponent = true;
2096
+ break;
2097
+ }
2098
+ el = el.parentElement;
2099
+ }
2100
+ if (isInSubComponent || !el) {
2101
+ continue;
2102
+ }
2103
+ slotNode.removeAttribute("t-set-slot");
2104
+ slotNode.remove();
2105
+ const slotAst = parseNode(slotNode, ctx);
2106
+ let on = null;
2107
+ let attrs = null;
2108
+ let attrsTranslationCtx = null;
2109
+ let scope = null;
2110
+ for (let attributeName of slotNode.getAttributeNames()) {
2111
+ const value = slotNode.getAttribute(attributeName);
2112
+ if (attributeName === "t-slot-scope") {
2113
+ scope = value;
2114
+ continue;
2115
+ }
2116
+ else if (attributeName.startsWith("t-translation-context-")) {
2117
+ const attrName = attributeName.slice(22);
2118
+ attrsTranslationCtx = attrsTranslationCtx || {};
2119
+ attrsTranslationCtx[attrName] = value;
2120
+ }
2121
+ else if (attributeName.startsWith("t-on-")) {
2122
+ on = on || {};
2123
+ on[attributeName.slice(5)] = value;
2124
+ }
2125
+ else {
2126
+ attrs = attrs || {};
2127
+ attrs[attributeName] = value;
2128
+ }
2129
+ }
2130
+ slots = slots || {};
2131
+ slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
2132
+ }
2133
+ // default slot
2134
+ const defaultContent = parseChildNodes(clone, ctx);
2135
+ slots = slots || {};
2136
+ // t-set-slot="default" has priority over content
2137
+ if (defaultContent && !slots.default) {
2138
+ slots.default = {
2139
+ content: defaultContent,
2140
+ on,
2141
+ attrs: null,
2142
+ attrsTranslationCtx: null,
2143
+ scope: defaultSlotScope,
2144
+ };
2145
+ }
2146
+ }
2147
+ return {
2148
+ type: 10 /* ASTType.TComponent */,
2149
+ name,
2150
+ isDynamic,
2151
+ dynamicProps,
2152
+ props,
2153
+ propsTranslationCtx,
2154
+ slots,
2155
+ on,
2156
+ };
2157
+ }
2158
+ // -----------------------------------------------------------------------------
2159
+ // Slots
2160
+ // -----------------------------------------------------------------------------
2161
+ function parseTCallSlot(node, ctx) {
2162
+ if (!node.hasAttribute("t-call-slot") && !node.hasAttribute("t-slot")) {
2163
+ return null;
2164
+ }
2165
+ if (node.hasAttribute("t-slot")) {
2166
+ console.warn(`t-slot has been renamed t-call-slot.`);
2167
+ }
2168
+ const name = (node.getAttribute("t-call-slot") || node.getAttribute("t-slot"));
2169
+ node.removeAttribute("t-call-slot");
2170
+ node.removeAttribute("t-slot");
2171
+ let attrs = null;
2172
+ let attrsTranslationCtx = null;
2173
+ let on = null;
2174
+ for (let attributeName of node.getAttributeNames()) {
2175
+ const value = node.getAttribute(attributeName);
2176
+ if (attributeName.startsWith("t-on-")) {
2177
+ on = on || {};
2178
+ on[attributeName.slice(5)] = value;
2179
+ }
2180
+ else if (attributeName.startsWith("t-translation-context-")) {
2181
+ const attrName = attributeName.slice(22);
2182
+ attrsTranslationCtx = attrsTranslationCtx || {};
2183
+ attrsTranslationCtx[attrName] = value;
2184
+ }
2185
+ else {
2186
+ attrs = attrs || {};
2187
+ attrs[attributeName] = value;
2188
+ }
2189
+ }
2190
+ return {
2191
+ type: 13 /* ASTType.TCallSlot */,
2192
+ name,
2193
+ attrs,
2194
+ attrsTranslationCtx,
2195
+ on,
2196
+ defaultContent: parseChildNodes(node, ctx),
2197
+ };
2198
+ }
2199
+ // -----------------------------------------------------------------------------
2200
+ // Translation
2201
+ // -----------------------------------------------------------------------------
2202
+ function wrapInTTranslationAST(r) {
2203
+ const ast = { type: 15 /* ASTType.TTranslation */, content: r };
2204
+ if (r?.hasNoRepresentation) {
2205
+ ast.hasNoRepresentation = true;
2206
+ }
2207
+ return ast;
2208
+ }
2209
+ function parseTTranslation(node, ctx) {
2210
+ if (node.getAttribute("t-translation") !== "off") {
2211
+ return null;
2212
+ }
2213
+ node.removeAttribute("t-translation");
2214
+ const result = parseNode(node, ctx);
2215
+ if (result?.type === 3 /* ASTType.Multi */) {
2216
+ const children = result.content.map(wrapInTTranslationAST);
2217
+ return makeASTMulti(children);
2218
+ }
2219
+ return wrapInTTranslationAST(result);
2220
+ }
2221
+ // -----------------------------------------------------------------------------
2222
+ // Translation Context
2223
+ // -----------------------------------------------------------------------------
2224
+ function wrapInTTranslationContextAST(r, translationCtx) {
2225
+ const ast = {
2226
+ type: 16 /* ASTType.TTranslationContext */,
2227
+ content: r,
2228
+ translationCtx,
2229
+ };
2230
+ if (r?.hasNoRepresentation) {
2231
+ ast.hasNoRepresentation = true;
2232
+ }
2233
+ return ast;
2234
+ }
2235
+ function parseTTranslationContext(node, ctx) {
2236
+ const translationCtx = node.getAttribute("t-translation-context");
2237
+ if (!translationCtx) {
2238
+ return null;
2239
+ }
2240
+ node.removeAttribute("t-translation-context");
2241
+ const result = parseNode(node, ctx);
2242
+ if (result?.type === 3 /* ASTType.Multi */) {
2243
+ const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
2244
+ return makeASTMulti(children);
2245
+ }
2246
+ return wrapInTTranslationContextAST(result, translationCtx);
2247
+ }
2248
+ // -----------------------------------------------------------------------------
2249
+ // helpers
2250
+ // -----------------------------------------------------------------------------
2251
+ /**
2252
+ * Parse all the child nodes of a given node and return a list of ast elements
2253
+ */
2254
+ function parseChildren(node, ctx) {
2255
+ const children = [];
2256
+ for (let child of node.childNodes) {
2257
+ const childAst = parseNode(child, ctx);
2258
+ if (childAst) {
2259
+ if (childAst.type === 3 /* ASTType.Multi */) {
2260
+ children.push(...childAst.content);
2261
+ }
2262
+ else {
2263
+ children.push(childAst);
2264
+ }
2265
+ }
2266
+ }
2267
+ return children;
2268
+ }
2269
+ function makeASTMulti(children) {
2270
+ const ast = { type: 3 /* ASTType.Multi */, content: children };
2271
+ if (children.every((c) => c.hasNoRepresentation)) {
2272
+ ast.hasNoRepresentation = true;
2273
+ }
2274
+ return ast;
2275
+ }
2276
+ /**
2277
+ * Parse all the child nodes of a given node and return an ast if possible.
2278
+ * In the case there are multiple children, they are wrapped in a astmulti.
2279
+ */
2280
+ function parseChildNodes(node, ctx) {
2281
+ const children = parseChildren(node, ctx);
2282
+ switch (children.length) {
2283
+ case 0:
2284
+ return null;
2285
+ case 1:
2286
+ return children[0];
2287
+ default:
2288
+ return makeASTMulti(children);
2289
+ }
2290
+ }
2291
+ /**
2292
+ * Normalizes the content of an Element so that t-if/t-elif/t-else directives
2293
+ * immediately follow one another (by removing empty text nodes or comments).
2294
+ * Throws an error when a conditional branching statement is malformed. This
2295
+ * function modifies the Element in place.
2296
+ *
2297
+ * @param el the element containing the tree that should be normalized
2298
+ */
2299
+ function normalizeTIf(el) {
2300
+ let tbranch = el.querySelectorAll("[t-elif], [t-else]");
2301
+ for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
2302
+ let node = tbranch[i];
2303
+ let prevElem = node.previousElementSibling;
2304
+ let pattr = (name) => prevElem.getAttribute(name);
2305
+ let nattr = (name) => +!!node.getAttribute(name);
2306
+ if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
2307
+ if (pattr("t-foreach")) {
2308
+ throw new OwlError("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
2309
+ }
2310
+ if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
2311
+ return a + b;
2312
+ }) > 1) {
2313
+ throw new OwlError("Only one conditional branching directive is allowed per node");
2314
+ }
2315
+ // All text (with only spaces) and comment nodes (nodeType 8) between
2316
+ // branch nodes are removed
2317
+ let textNode;
2318
+ while ((textNode = node.previousSibling) !== prevElem) {
2319
+ if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
2320
+ throw new OwlError("text is not allowed between branching directives");
2321
+ }
2322
+ textNode.remove();
2323
+ }
2324
+ }
2325
+ else {
2326
+ throw new OwlError("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
2327
+ }
2328
+ }
2329
+ }
2330
+ /**
2331
+ * Normalizes the content of an Element so that t-out directives on components
2332
+ * are removed and instead places a <t t-out=""> as the default slot of the
2333
+ * component. Also throws if the component already has content. This function
2334
+ * modifies the Element in place.
2335
+ *
2336
+ * @param el the element containing the tree that should be normalized
2337
+ */
2338
+ function normalizeTOut(el) {
2339
+ const elements = [...el.querySelectorAll(`[t-out]`)].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
2340
+ for (const el of elements) {
2341
+ if (el.childNodes.length) {
2342
+ throw new OwlError(`Cannot have t-out on a component that already has content`);
2343
+ }
2344
+ const value = el.getAttribute("t-out");
2345
+ el.removeAttribute("t-out");
2346
+ const t = el.ownerDocument.createElement("t");
2347
+ if (value != null) {
2348
+ t.setAttribute("t-out", value);
2349
+ }
2350
+ el.appendChild(t);
2351
+ }
2352
+ }
2353
+ /**
2354
+ * Normalizes the tree inside a given element and do some preliminary validation
2355
+ * on it. This function modifies the Element in place.
2356
+ *
2357
+ * @param el the element containing the tree that should be normalized
2358
+ */
2359
+ function normalizeXML(el) {
2360
+ normalizeTIf(el);
2361
+ normalizeTOut(el);
2460
2362
  }
2461
2363
 
2462
- function compile(template, options = {
2463
- hasGlobalValues: false,
2464
- }) {
2465
- // parsing
2466
- const ast = parse(template, options.customDirectives);
2467
- // some work
2468
- const hasSafeContext = template instanceof Node
2469
- ? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
2470
- : !template.includes("t-set") && !template.includes("t-call");
2471
- // code generation
2472
- const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
2473
- const code = codeGenerator.generateCode();
2474
- // template function
2475
- try {
2476
- return new Function("app, bdom, helpers", code);
2477
- }
2478
- catch (originalError) {
2479
- const { name } = options;
2480
- const nameStr = name ? `template "${name}"` : "anonymous template";
2481
- const err = new OwlError(`Failed to compile ${nameStr}: ${originalError.message}\n\ngenerated code:\nfunction(app, bdom, helpers) {\n${code}\n}`);
2482
- err.cause = originalError;
2483
- throw err;
2484
- }
2364
+ function compile(template, options = {
2365
+ hasGlobalValues: false,
2366
+ }) {
2367
+ // parsing
2368
+ const ast = parse(template, options.customDirectives);
2369
+ // code generation
2370
+ const codeGenerator = new CodeGenerator(ast, options);
2371
+ const code = codeGenerator.generateCode();
2372
+ // template function
2373
+ try {
2374
+ return new Function("app, bdom, helpers", code);
2375
+ }
2376
+ catch (originalError) {
2377
+ const { name } = options;
2378
+ const nameStr = name ? `template "${name}"` : "anonymous template";
2379
+ const err = new OwlError(`Failed to compile ${nameStr}: ${originalError.message}\n\ngenerated code:\nfunction(app, bdom, helpers) {\n${code}\n}`);
2380
+ err.cause = originalError;
2381
+ throw err;
2382
+ }
2485
2383
  }
2486
2384
 
2487
- // -----------------------------------------------------------------------------
2488
- // -----------------------------------------------------------------------------
2489
- // helpers
2490
- // -----------------------------------------------------------------------------
2491
- async function getXmlFiles(paths) {
2492
- return (await Promise.all(paths.map(async (file) => {
2493
- const stats = await stat(path.join(file));
2494
- if (stats.isDirectory()) {
2495
- return await getXmlFiles((await readdir(file)).map((fileName) => path.join(file, fileName)));
2496
- }
2497
- if (file.endsWith(".xml")) {
2498
- return file;
2499
- }
2500
- return [];
2501
- }))).flat();
2502
- }
2503
- // adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
2504
- const a = "·-_,:;";
2505
- const p = new RegExp(a.split("").join("|"), "g");
2506
- function slugify(str) {
2507
- return str
2508
- .replace(/\//g, "") // remove /
2509
- .replace(/\./g, "_") // Replace . with _
2510
- .replace(p, (c) => "_") // Replace special characters
2511
- .replace(/&/g, "_and_") // Replace & with ‘and’
2512
- .replace(/[^\w\-]+/g, ""); // Remove all non-word characters
2513
- }
2514
- // -----------------------------------------------------------------------------
2515
- // main
2516
- // -----------------------------------------------------------------------------
2517
- async function compileTemplates(paths) {
2518
- const files = await getXmlFiles(paths);
2519
- process.stdout.write(`Processing ${files.length} files`);
2520
- let xmlStrings = await Promise.all(files.map((file) => readFile(file, "utf8")));
2521
- const templates = [];
2522
- const errors = [];
2523
- for (let i = 0; i < files.length; i++) {
2524
- const fileName = files[i];
2525
- const fileContent = xmlStrings[i];
2526
- process.stdout.write(`.`);
2527
- const parser = new DOMParser();
2528
- const doc = parser.parseFromString(fileContent, "text/xml");
2529
- for (const template of doc.querySelectorAll("[t-name]")) {
2530
- const name = template.getAttribute("t-name");
2531
- if (template.hasAttribute("owl")) {
2532
- template.removeAttribute("owl");
2533
- }
2534
- const fnName = slugify(name);
2535
- try {
2536
- const fn = compile(template).toString().replace("anonymous", fnName);
2537
- templates.push(`"${name}": ${fn},\n`);
2538
- }
2539
- catch (e) {
2540
- errors.push({ name, fileName, e });
2541
- }
2542
- }
2543
- }
2544
- process.stdout.write(`\n`);
2545
- for (let { name, fileName, e } of errors) {
2546
- console.warn(`Error while compiling '${name}' (in file ${fileName})`);
2547
- console.error(e);
2548
- }
2549
- console.log(`${templates.length} templates compiled`);
2550
- return `export const templates = {\n ${templates.join("\n")} \n}`;
2385
+ // -----------------------------------------------------------------------------
2386
+ // This file exports a function that allows compiling templates ahead of time.
2387
+ // It is used by the "compile_owl_template" command registered in the "bin"
2388
+ // section of owl's package.json
2389
+ // -----------------------------------------------------------------------------
2390
+ // -----------------------------------------------------------------------------
2391
+ // helpers
2392
+ // -----------------------------------------------------------------------------
2393
+ async function getXmlFiles(paths) {
2394
+ return (await Promise.all(paths.map(async (file) => {
2395
+ const stats = await stat(path.join(file));
2396
+ if (stats.isDirectory()) {
2397
+ return await getXmlFiles((await readdir(file)).map((fileName) => path.join(file, fileName)));
2398
+ }
2399
+ if (file.endsWith(".xml")) {
2400
+ return file;
2401
+ }
2402
+ return [];
2403
+ }))).flat();
2404
+ }
2405
+ // adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
2406
+ const a = "·-_,:;";
2407
+ const p = new RegExp(a.split("").join("|"), "g");
2408
+ function slugify(str) {
2409
+ return str
2410
+ .replace(/\//g, "") // remove /
2411
+ .replace(/\./g, "_") // Replace . with _
2412
+ .replace(p, (c) => "_") // Replace special characters
2413
+ .replace(/&/g, "_and_") // Replace & with ‘and’
2414
+ .replace(/[^\w\-]+/g, ""); // Remove all non-word characters
2415
+ }
2416
+ // -----------------------------------------------------------------------------
2417
+ // main
2418
+ // -----------------------------------------------------------------------------
2419
+ async function compileTemplates(paths) {
2420
+ const files = await getXmlFiles(paths);
2421
+ process.stdout.write(`Processing ${files.length} files`);
2422
+ let xmlStrings = await Promise.all(files.map((file) => readFile(file, "utf8")));
2423
+ const templates = [];
2424
+ const errors = [];
2425
+ for (let i = 0; i < files.length; i++) {
2426
+ const fileName = files[i];
2427
+ const fileContent = xmlStrings[i];
2428
+ process.stdout.write(`.`);
2429
+ const parser = new DOMParser();
2430
+ const doc = parser.parseFromString(fileContent, "text/xml");
2431
+ for (const template of doc.querySelectorAll("[t-name]")) {
2432
+ const name = template.getAttribute("t-name");
2433
+ if (template.hasAttribute("owl")) {
2434
+ template.removeAttribute("owl");
2435
+ }
2436
+ const fnName = slugify(name);
2437
+ try {
2438
+ const fn = compile(template).toString().replace("anonymous", fnName);
2439
+ templates.push(`"${name}": ${fn},\n`);
2440
+ }
2441
+ catch (e) {
2442
+ errors.push({ name, fileName, e });
2443
+ }
2444
+ }
2445
+ }
2446
+ process.stdout.write(`\n`);
2447
+ for (let { name, fileName, e } of errors) {
2448
+ console.warn(`Error while compiling '${name}' (in file ${fileName})`);
2449
+ console.error(e);
2450
+ }
2451
+ console.log(`${templates.length} templates compiled`);
2452
+ return `export const templates = {\n ${templates.join("\n")} \n}`;
2551
2453
  }
2552
2454
 
2553
2455
  export { compileTemplates };