@odoo/owl 2.5.0 → 2.5.3
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.
- package/dist/compile_templates.mjs +2414 -0
- package/dist/owl-devtools.zip +0 -0
- package/dist/owl.cjs.js +23 -7
- package/dist/owl.es.js +23 -7
- package/dist/owl.iife.js +23 -7
- package/dist/owl.iife.min.js +1 -1
- package/dist/types/common/types.d.ts +1 -1
- package/dist/types/compiler/standalone/index.d.ts +2 -0
- package/dist/types/compiler/standalone/setup_jsdom.d.ts +1 -0
- package/dist/types/owl.d.ts +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +9 -2
- package/tools/compile_owl_templates.mjs +31 -0
|
@@ -0,0 +1,2414 @@
|
|
|
1
|
+
import { readFile, stat, readdir } from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import jsdom from 'jsdom';
|
|
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;
|
|
15
|
+
global.Node = window.Node;
|
|
16
|
+
|
|
17
|
+
// Custom error class that wraps error that happen in the owl lifecycle
|
|
18
|
+
class OwlError extends Error {
|
|
19
|
+
}
|
|
20
|
+
|
|
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);
|
|
328
|
+
}
|
|
329
|
+
|
|
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
|
+
tKeyExpr: null,
|
|
432
|
+
nameSpace: parentCtx.nameSpace,
|
|
433
|
+
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
|
434
|
+
}, params);
|
|
435
|
+
}
|
|
436
|
+
class CodeTarget {
|
|
437
|
+
constructor(name, on) {
|
|
438
|
+
this.indentLevel = 0;
|
|
439
|
+
this.loopLevel = 0;
|
|
440
|
+
this.code = [];
|
|
441
|
+
this.hasRoot = false;
|
|
442
|
+
this.hasCache = false;
|
|
443
|
+
this.shouldProtectScope = false;
|
|
444
|
+
this.hasRefWrapper = false;
|
|
445
|
+
this.name = name;
|
|
446
|
+
this.on = on || null;
|
|
447
|
+
}
|
|
448
|
+
addLine(line, idx) {
|
|
449
|
+
const prefix = new Array(this.indentLevel + 2).join(" ");
|
|
450
|
+
if (idx === undefined) {
|
|
451
|
+
this.code.push(prefix + line);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
this.code.splice(idx, 0, prefix + line);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
generateCode() {
|
|
458
|
+
let result = [];
|
|
459
|
+
result.push(`function ${this.name}(ctx, node, key = "") {`);
|
|
460
|
+
if (this.shouldProtectScope) {
|
|
461
|
+
result.push(` ctx = Object.create(ctx);`);
|
|
462
|
+
result.push(` ctx[isBoundary] = 1`);
|
|
463
|
+
}
|
|
464
|
+
if (this.hasRefWrapper) {
|
|
465
|
+
result.push(` let refWrapper = makeRefWrapper(this.__owl__);`);
|
|
466
|
+
}
|
|
467
|
+
if (this.hasCache) {
|
|
468
|
+
result.push(` let cache = ctx.cache || {};`);
|
|
469
|
+
result.push(` let nextCache = ctx.cache = {};`);
|
|
470
|
+
}
|
|
471
|
+
for (let line of this.code) {
|
|
472
|
+
result.push(line);
|
|
473
|
+
}
|
|
474
|
+
if (!this.hasRoot) {
|
|
475
|
+
result.push(`return text('');`);
|
|
476
|
+
}
|
|
477
|
+
result.push(`}`);
|
|
478
|
+
return result.join("\n ");
|
|
479
|
+
}
|
|
480
|
+
currentKey(ctx) {
|
|
481
|
+
let key = this.loopLevel ? `key${this.loopLevel}` : "key";
|
|
482
|
+
if (ctx.tKeyExpr) {
|
|
483
|
+
key = `${ctx.tKeyExpr} + ${key}`;
|
|
484
|
+
}
|
|
485
|
+
return key;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
|
489
|
+
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
|
490
|
+
class CodeGenerator {
|
|
491
|
+
constructor(ast, options) {
|
|
492
|
+
this.blocks = [];
|
|
493
|
+
this.nextBlockId = 1;
|
|
494
|
+
this.isDebug = false;
|
|
495
|
+
this.targets = [];
|
|
496
|
+
this.target = new CodeTarget("template");
|
|
497
|
+
this.translatableAttributes = TRANSLATABLE_ATTRS;
|
|
498
|
+
this.staticDefs = [];
|
|
499
|
+
this.slotNames = new Set();
|
|
500
|
+
this.helpers = new Set();
|
|
501
|
+
this.translateFn = options.translateFn || ((s) => s);
|
|
502
|
+
if (options.translatableAttributes) {
|
|
503
|
+
const attrs = new Set(TRANSLATABLE_ATTRS);
|
|
504
|
+
for (let attr of options.translatableAttributes) {
|
|
505
|
+
if (attr.startsWith("-")) {
|
|
506
|
+
attrs.delete(attr.slice(1));
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
attrs.add(attr);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
this.translatableAttributes = [...attrs];
|
|
513
|
+
}
|
|
514
|
+
this.hasSafeContext = options.hasSafeContext || false;
|
|
515
|
+
this.dev = options.dev || false;
|
|
516
|
+
this.ast = ast;
|
|
517
|
+
this.templateName = options.name;
|
|
518
|
+
if (options.hasGlobalValues) {
|
|
519
|
+
this.helpers.add("__globals__");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
generateCode() {
|
|
523
|
+
const ast = this.ast;
|
|
524
|
+
this.isDebug = ast.type === 12 /* TDebug */;
|
|
525
|
+
BlockDescription.nextBlockId = 1;
|
|
526
|
+
nextDataIds = {};
|
|
527
|
+
this.compileAST(ast, {
|
|
528
|
+
block: null,
|
|
529
|
+
index: 0,
|
|
530
|
+
forceNewBlock: false,
|
|
531
|
+
isLast: true,
|
|
532
|
+
translate: true,
|
|
533
|
+
tKeyExpr: null,
|
|
534
|
+
});
|
|
535
|
+
// define blocks and utility functions
|
|
536
|
+
let mainCode = [` let { text, createBlock, list, multi, html, toggler, comment } = bdom;`];
|
|
537
|
+
if (this.helpers.size) {
|
|
538
|
+
mainCode.push(`let { ${[...this.helpers].join(", ")} } = helpers;`);
|
|
539
|
+
}
|
|
540
|
+
if (this.templateName) {
|
|
541
|
+
mainCode.push(`// Template name: "${this.templateName}"`);
|
|
542
|
+
}
|
|
543
|
+
for (let { id, expr } of this.staticDefs) {
|
|
544
|
+
mainCode.push(`const ${id} = ${expr};`);
|
|
545
|
+
}
|
|
546
|
+
// define all blocks
|
|
547
|
+
if (this.blocks.length) {
|
|
548
|
+
mainCode.push(``);
|
|
549
|
+
for (let block of this.blocks) {
|
|
550
|
+
if (block.dom) {
|
|
551
|
+
let xmlString = toStringExpression(block.asXmlString());
|
|
552
|
+
if (block.dynamicTagName) {
|
|
553
|
+
xmlString = xmlString.replace(/^`<\w+/, `\`<\${tag || '${block.dom.nodeName}'}`);
|
|
554
|
+
xmlString = xmlString.replace(/\w+>`$/, `\${tag || '${block.dom.nodeName}'}>\``);
|
|
555
|
+
mainCode.push(`let ${block.blockName} = tag => createBlock(${xmlString});`);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
mainCode.push(`let ${block.blockName} = createBlock(${xmlString});`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// define all slots/defaultcontent function
|
|
564
|
+
if (this.targets.length) {
|
|
565
|
+
for (let fn of this.targets) {
|
|
566
|
+
mainCode.push("");
|
|
567
|
+
mainCode = mainCode.concat(fn.generateCode());
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
// generate main code
|
|
571
|
+
mainCode.push("");
|
|
572
|
+
mainCode = mainCode.concat("return " + this.target.generateCode());
|
|
573
|
+
const code = mainCode.join("\n ");
|
|
574
|
+
if (this.isDebug) {
|
|
575
|
+
const msg = `[Owl Debug]\n${code}`;
|
|
576
|
+
console.log(msg);
|
|
577
|
+
}
|
|
578
|
+
return code;
|
|
579
|
+
}
|
|
580
|
+
compileInNewTarget(prefix, ast, ctx, on) {
|
|
581
|
+
const name = generateId(prefix);
|
|
582
|
+
const initialTarget = this.target;
|
|
583
|
+
const target = new CodeTarget(name, on);
|
|
584
|
+
this.targets.push(target);
|
|
585
|
+
this.target = target;
|
|
586
|
+
this.compileAST(ast, createContext(ctx));
|
|
587
|
+
this.target = initialTarget;
|
|
588
|
+
return name;
|
|
589
|
+
}
|
|
590
|
+
addLine(line, idx) {
|
|
591
|
+
this.target.addLine(line, idx);
|
|
592
|
+
}
|
|
593
|
+
define(varName, expr) {
|
|
594
|
+
this.addLine(`const ${varName} = ${expr};`);
|
|
595
|
+
}
|
|
596
|
+
insertAnchor(block, index = block.children.length) {
|
|
597
|
+
const tag = `block-child-${index}`;
|
|
598
|
+
const anchor = xmlDoc.createElement(tag);
|
|
599
|
+
block.insert(anchor);
|
|
600
|
+
}
|
|
601
|
+
createBlock(parentBlock, type, ctx) {
|
|
602
|
+
const hasRoot = this.target.hasRoot;
|
|
603
|
+
const block = new BlockDescription(this.target, type);
|
|
604
|
+
if (!hasRoot) {
|
|
605
|
+
this.target.hasRoot = true;
|
|
606
|
+
block.isRoot = true;
|
|
607
|
+
}
|
|
608
|
+
if (parentBlock) {
|
|
609
|
+
parentBlock.children.push(block);
|
|
610
|
+
if (parentBlock.type === "list") {
|
|
611
|
+
block.parentVar = `c_block${parentBlock.id}`;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return block;
|
|
615
|
+
}
|
|
616
|
+
insertBlock(expression, block, ctx) {
|
|
617
|
+
let blockExpr = block.generateExpr(expression);
|
|
618
|
+
if (block.parentVar) {
|
|
619
|
+
let key = this.target.currentKey(ctx);
|
|
620
|
+
this.helpers.add("withKey");
|
|
621
|
+
this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${key});`);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (ctx.tKeyExpr) {
|
|
625
|
+
blockExpr = `toggler(${ctx.tKeyExpr}, ${blockExpr})`;
|
|
626
|
+
}
|
|
627
|
+
if (block.isRoot) {
|
|
628
|
+
if (this.target.on) {
|
|
629
|
+
blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
|
|
630
|
+
}
|
|
631
|
+
this.addLine(`return ${blockExpr};`);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
this.define(block.varName, blockExpr);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Captures variables that are used inside of an expression. This is useful
|
|
639
|
+
* because in compiled code, almost all variables are accessed through the ctx
|
|
640
|
+
* object. In the case of functions, that lookup in the context can be delayed
|
|
641
|
+
* which can cause issues if the value has changed since the function was
|
|
642
|
+
* defined.
|
|
643
|
+
*
|
|
644
|
+
* @param expr the expression to capture
|
|
645
|
+
* @param forceCapture whether the expression should capture its scope even if
|
|
646
|
+
* it doesn't contain a function. Useful when the expression will be used as
|
|
647
|
+
* a function body.
|
|
648
|
+
* @returns a new expression that uses the captured values
|
|
649
|
+
*/
|
|
650
|
+
captureExpression(expr, forceCapture = false) {
|
|
651
|
+
if (!forceCapture && !expr.includes("=>")) {
|
|
652
|
+
return compileExpr(expr);
|
|
653
|
+
}
|
|
654
|
+
const tokens = compileExprToArray(expr);
|
|
655
|
+
const mapping = new Map();
|
|
656
|
+
return tokens
|
|
657
|
+
.map((tok) => {
|
|
658
|
+
if (tok.varName && !tok.isLocal) {
|
|
659
|
+
if (!mapping.has(tok.varName)) {
|
|
660
|
+
const varId = generateId("v");
|
|
661
|
+
mapping.set(tok.varName, varId);
|
|
662
|
+
this.define(varId, tok.value);
|
|
663
|
+
}
|
|
664
|
+
tok.value = mapping.get(tok.varName);
|
|
665
|
+
}
|
|
666
|
+
return tok.value;
|
|
667
|
+
})
|
|
668
|
+
.join("");
|
|
669
|
+
}
|
|
670
|
+
translate(str) {
|
|
671
|
+
const match = translationRE.exec(str);
|
|
672
|
+
return match[1] + this.translateFn(match[2]) + match[3];
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* @returns the newly created block name, if any
|
|
676
|
+
*/
|
|
677
|
+
compileAST(ast, ctx) {
|
|
678
|
+
switch (ast.type) {
|
|
679
|
+
case 1 /* Comment */:
|
|
680
|
+
return this.compileComment(ast, ctx);
|
|
681
|
+
case 0 /* Text */:
|
|
682
|
+
return this.compileText(ast, ctx);
|
|
683
|
+
case 2 /* DomNode */:
|
|
684
|
+
return this.compileTDomNode(ast, ctx);
|
|
685
|
+
case 4 /* TEsc */:
|
|
686
|
+
return this.compileTEsc(ast, ctx);
|
|
687
|
+
case 8 /* TOut */:
|
|
688
|
+
return this.compileTOut(ast, ctx);
|
|
689
|
+
case 5 /* TIf */:
|
|
690
|
+
return this.compileTIf(ast, ctx);
|
|
691
|
+
case 9 /* TForEach */:
|
|
692
|
+
return this.compileTForeach(ast, ctx);
|
|
693
|
+
case 10 /* TKey */:
|
|
694
|
+
return this.compileTKey(ast, ctx);
|
|
695
|
+
case 3 /* Multi */:
|
|
696
|
+
return this.compileMulti(ast, ctx);
|
|
697
|
+
case 7 /* TCall */:
|
|
698
|
+
return this.compileTCall(ast, ctx);
|
|
699
|
+
case 15 /* TCallBlock */:
|
|
700
|
+
return this.compileTCallBlock(ast, ctx);
|
|
701
|
+
case 6 /* TSet */:
|
|
702
|
+
return this.compileTSet(ast, ctx);
|
|
703
|
+
case 11 /* TComponent */:
|
|
704
|
+
return this.compileComponent(ast, ctx);
|
|
705
|
+
case 12 /* TDebug */:
|
|
706
|
+
return this.compileDebug(ast, ctx);
|
|
707
|
+
case 13 /* TLog */:
|
|
708
|
+
return this.compileLog(ast, ctx);
|
|
709
|
+
case 14 /* TSlot */:
|
|
710
|
+
return this.compileTSlot(ast, ctx);
|
|
711
|
+
case 16 /* TTranslation */:
|
|
712
|
+
return this.compileTTranslation(ast, ctx);
|
|
713
|
+
case 17 /* TPortal */:
|
|
714
|
+
return this.compileTPortal(ast, ctx);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
compileDebug(ast, ctx) {
|
|
718
|
+
this.addLine(`debugger;`);
|
|
719
|
+
if (ast.content) {
|
|
720
|
+
return this.compileAST(ast.content, ctx);
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
compileLog(ast, ctx) {
|
|
725
|
+
this.addLine(`console.log(${compileExpr(ast.expr)});`);
|
|
726
|
+
if (ast.content) {
|
|
727
|
+
return this.compileAST(ast.content, ctx);
|
|
728
|
+
}
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
compileComment(ast, ctx) {
|
|
732
|
+
let { block, forceNewBlock } = ctx;
|
|
733
|
+
const isNewBlock = !block || forceNewBlock;
|
|
734
|
+
if (isNewBlock) {
|
|
735
|
+
block = this.createBlock(block, "comment", ctx);
|
|
736
|
+
this.insertBlock(`comment(${toStringExpression(ast.value)})`, block, {
|
|
737
|
+
...ctx,
|
|
738
|
+
forceNewBlock: forceNewBlock && !block,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
const text = xmlDoc.createComment(ast.value);
|
|
743
|
+
block.insert(text);
|
|
744
|
+
}
|
|
745
|
+
return block.varName;
|
|
746
|
+
}
|
|
747
|
+
compileText(ast, ctx) {
|
|
748
|
+
let { block, forceNewBlock } = ctx;
|
|
749
|
+
let value = ast.value;
|
|
750
|
+
if (value && ctx.translate !== false) {
|
|
751
|
+
value = this.translate(value);
|
|
752
|
+
}
|
|
753
|
+
if (!ctx.inPreTag) {
|
|
754
|
+
value = value.replace(whitespaceRE, " ");
|
|
755
|
+
}
|
|
756
|
+
if (!block || forceNewBlock) {
|
|
757
|
+
block = this.createBlock(block, "text", ctx);
|
|
758
|
+
this.insertBlock(`text(${toStringExpression(value)})`, block, {
|
|
759
|
+
...ctx,
|
|
760
|
+
forceNewBlock: forceNewBlock && !block,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
const createFn = ast.type === 0 /* Text */ ? xmlDoc.createTextNode : xmlDoc.createComment;
|
|
765
|
+
block.insert(createFn.call(xmlDoc, value));
|
|
766
|
+
}
|
|
767
|
+
return block.varName;
|
|
768
|
+
}
|
|
769
|
+
generateHandlerCode(rawEvent, handler) {
|
|
770
|
+
const modifiers = rawEvent
|
|
771
|
+
.split(".")
|
|
772
|
+
.slice(1)
|
|
773
|
+
.map((m) => {
|
|
774
|
+
if (!MODS.has(m)) {
|
|
775
|
+
throw new OwlError(`Unknown event modifier: '${m}'`);
|
|
776
|
+
}
|
|
777
|
+
return `"${m}"`;
|
|
778
|
+
});
|
|
779
|
+
let modifiersCode = "";
|
|
780
|
+
if (modifiers.length) {
|
|
781
|
+
modifiersCode = `${modifiers.join(",")}, `;
|
|
782
|
+
}
|
|
783
|
+
return `[${modifiersCode}${this.captureExpression(handler)}, ctx]`;
|
|
784
|
+
}
|
|
785
|
+
compileTDomNode(ast, ctx) {
|
|
786
|
+
let { block, forceNewBlock } = ctx;
|
|
787
|
+
const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;
|
|
788
|
+
let codeIdx = this.target.code.length;
|
|
789
|
+
if (isNewBlock) {
|
|
790
|
+
if ((ast.dynamicTag || ctx.tKeyExpr || ast.ns) && ctx.block) {
|
|
791
|
+
this.insertAnchor(ctx.block);
|
|
792
|
+
}
|
|
793
|
+
block = this.createBlock(block, "block", ctx);
|
|
794
|
+
this.blocks.push(block);
|
|
795
|
+
if (ast.dynamicTag) {
|
|
796
|
+
const tagExpr = generateId("tag");
|
|
797
|
+
this.define(tagExpr, compileExpr(ast.dynamicTag));
|
|
798
|
+
block.dynamicTagName = tagExpr;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// attributes
|
|
802
|
+
const attrs = {};
|
|
803
|
+
for (let key in ast.attrs) {
|
|
804
|
+
let expr, attrName;
|
|
805
|
+
if (key.startsWith("t-attf")) {
|
|
806
|
+
expr = interpolate(ast.attrs[key]);
|
|
807
|
+
const idx = block.insertData(expr, "attr");
|
|
808
|
+
attrName = key.slice(7);
|
|
809
|
+
attrs["block-attribute-" + idx] = attrName;
|
|
810
|
+
}
|
|
811
|
+
else if (key.startsWith("t-att")) {
|
|
812
|
+
attrName = key === "t-att" ? null : key.slice(6);
|
|
813
|
+
expr = compileExpr(ast.attrs[key]);
|
|
814
|
+
if (attrName && isProp(ast.tag, attrName)) {
|
|
815
|
+
if (attrName === "readonly") {
|
|
816
|
+
// the property has a different name than the attribute
|
|
817
|
+
attrName = "readOnly";
|
|
818
|
+
}
|
|
819
|
+
// we force a new string or new boolean to bypass the equality check in blockdom when patching same value
|
|
820
|
+
if (attrName === "value") {
|
|
821
|
+
// When the expression is falsy (except 0), fall back to an empty string
|
|
822
|
+
expr = `new String((${expr}) === 0 ? 0 : ((${expr}) || ""))`;
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
expr = `new Boolean(${expr})`;
|
|
826
|
+
}
|
|
827
|
+
const idx = block.insertData(expr, "prop");
|
|
828
|
+
attrs[`block-property-${idx}`] = attrName;
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
const idx = block.insertData(expr, "attr");
|
|
832
|
+
if (key === "t-att") {
|
|
833
|
+
attrs[`block-attributes`] = String(idx);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
attrs[`block-attribute-${idx}`] = attrName;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
else if (this.translatableAttributes.includes(key)) {
|
|
841
|
+
attrs[key] = this.translateFn(ast.attrs[key]);
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
expr = `"${ast.attrs[key]}"`;
|
|
845
|
+
attrName = key;
|
|
846
|
+
attrs[key] = ast.attrs[key];
|
|
847
|
+
}
|
|
848
|
+
if (attrName === "value" && ctx.tModelSelectedExpr) {
|
|
849
|
+
let selectedId = block.insertData(`${ctx.tModelSelectedExpr} === ${expr}`, "attr");
|
|
850
|
+
attrs[`block-attribute-${selectedId}`] = "selected";
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
// t-model
|
|
854
|
+
let tModelSelectedExpr;
|
|
855
|
+
if (ast.model) {
|
|
856
|
+
const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;
|
|
857
|
+
const baseExpression = compileExpr(baseExpr);
|
|
858
|
+
const bExprId = generateId("bExpr");
|
|
859
|
+
this.define(bExprId, baseExpression);
|
|
860
|
+
const expression = compileExpr(expr);
|
|
861
|
+
const exprId = generateId("expr");
|
|
862
|
+
this.define(exprId, expression);
|
|
863
|
+
const fullExpression = `${bExprId}[${exprId}]`;
|
|
864
|
+
let idx;
|
|
865
|
+
if (specialInitTargetAttr) {
|
|
866
|
+
let targetExpr = targetAttr in attrs && `'${attrs[targetAttr]}'`;
|
|
867
|
+
if (!targetExpr && ast.attrs) {
|
|
868
|
+
// look at the dynamic attribute counterpart
|
|
869
|
+
const dynamicTgExpr = ast.attrs[`t-att-${targetAttr}`];
|
|
870
|
+
if (dynamicTgExpr) {
|
|
871
|
+
targetExpr = compileExpr(dynamicTgExpr);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
idx = block.insertData(`${fullExpression} === ${targetExpr}`, "prop");
|
|
875
|
+
attrs[`block-property-${idx}`] = specialInitTargetAttr;
|
|
876
|
+
}
|
|
877
|
+
else if (hasDynamicChildren) {
|
|
878
|
+
const bValueId = generateId("bValue");
|
|
879
|
+
tModelSelectedExpr = `${bValueId}`;
|
|
880
|
+
this.define(tModelSelectedExpr, fullExpression);
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
idx = block.insertData(`${fullExpression}`, "prop");
|
|
884
|
+
attrs[`block-property-${idx}`] = targetAttr;
|
|
885
|
+
}
|
|
886
|
+
this.helpers.add("toNumber");
|
|
887
|
+
let valueCode = `ev.target.${targetAttr}`;
|
|
888
|
+
valueCode = shouldTrim ? `${valueCode}.trim()` : valueCode;
|
|
889
|
+
valueCode = shouldNumberize ? `toNumber(${valueCode})` : valueCode;
|
|
890
|
+
const handler = `[(ev) => { ${fullExpression} = ${valueCode}; }]`;
|
|
891
|
+
idx = block.insertData(handler, "hdlr");
|
|
892
|
+
attrs[`block-handler-${idx}`] = eventType;
|
|
893
|
+
}
|
|
894
|
+
// event handlers
|
|
895
|
+
for (let ev in ast.on) {
|
|
896
|
+
const name = this.generateHandlerCode(ev, ast.on[ev]);
|
|
897
|
+
const idx = block.insertData(name, "hdlr");
|
|
898
|
+
attrs[`block-handler-${idx}`] = ev;
|
|
899
|
+
}
|
|
900
|
+
// t-ref
|
|
901
|
+
if (ast.ref) {
|
|
902
|
+
if (this.dev) {
|
|
903
|
+
this.helpers.add("makeRefWrapper");
|
|
904
|
+
this.target.hasRefWrapper = true;
|
|
905
|
+
}
|
|
906
|
+
const isDynamic = INTERP_REGEXP.test(ast.ref);
|
|
907
|
+
let name = `\`${ast.ref}\``;
|
|
908
|
+
if (isDynamic) {
|
|
909
|
+
name = replaceDynamicParts(ast.ref, (expr) => this.captureExpression(expr, true));
|
|
910
|
+
}
|
|
911
|
+
let setRefStr = `(el) => this.__owl__.setRef((${name}), el)`;
|
|
912
|
+
if (this.dev) {
|
|
913
|
+
setRefStr = `refWrapper(${name}, ${setRefStr})`;
|
|
914
|
+
}
|
|
915
|
+
const idx = block.insertData(setRefStr, "ref");
|
|
916
|
+
attrs["block-ref"] = String(idx);
|
|
917
|
+
}
|
|
918
|
+
const nameSpace = ast.ns || ctx.nameSpace;
|
|
919
|
+
const dom = nameSpace
|
|
920
|
+
? xmlDoc.createElementNS(nameSpace, ast.tag)
|
|
921
|
+
: xmlDoc.createElement(ast.tag);
|
|
922
|
+
for (const [attr, val] of Object.entries(attrs)) {
|
|
923
|
+
if (!(attr === "class" && val === "")) {
|
|
924
|
+
dom.setAttribute(attr, val);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
block.insert(dom);
|
|
928
|
+
if (ast.content.length) {
|
|
929
|
+
const initialDom = block.currentDom;
|
|
930
|
+
block.currentDom = dom;
|
|
931
|
+
const children = ast.content;
|
|
932
|
+
for (let i = 0; i < children.length; i++) {
|
|
933
|
+
const child = ast.content[i];
|
|
934
|
+
const subCtx = createContext(ctx, {
|
|
935
|
+
block,
|
|
936
|
+
index: block.childNumber,
|
|
937
|
+
forceNewBlock: false,
|
|
938
|
+
isLast: ctx.isLast && i === children.length - 1,
|
|
939
|
+
tKeyExpr: ctx.tKeyExpr,
|
|
940
|
+
nameSpace,
|
|
941
|
+
tModelSelectedExpr,
|
|
942
|
+
inPreTag: ctx.inPreTag || ast.tag === "pre",
|
|
943
|
+
});
|
|
944
|
+
this.compileAST(child, subCtx);
|
|
945
|
+
}
|
|
946
|
+
block.currentDom = initialDom;
|
|
947
|
+
}
|
|
948
|
+
if (isNewBlock) {
|
|
949
|
+
this.insertBlock(`${block.blockName}(ddd)`, block, ctx);
|
|
950
|
+
// may need to rewrite code!
|
|
951
|
+
if (block.children.length && block.hasDynamicChildren) {
|
|
952
|
+
const code = this.target.code;
|
|
953
|
+
const children = block.children.slice();
|
|
954
|
+
let current = children.shift();
|
|
955
|
+
for (let i = codeIdx; i < code.length; i++) {
|
|
956
|
+
if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
|
|
957
|
+
code[i] = code[i].replace(`const ${current.varName}`, current.varName);
|
|
958
|
+
current = children.shift();
|
|
959
|
+
if (!current)
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return block.varName;
|
|
967
|
+
}
|
|
968
|
+
compileTEsc(ast, ctx) {
|
|
969
|
+
let { block, forceNewBlock } = ctx;
|
|
970
|
+
let expr;
|
|
971
|
+
if (ast.expr === "0") {
|
|
972
|
+
this.helpers.add("zero");
|
|
973
|
+
expr = `ctx[zero]`;
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
expr = compileExpr(ast.expr);
|
|
977
|
+
if (ast.defaultValue) {
|
|
978
|
+
this.helpers.add("withDefault");
|
|
979
|
+
// FIXME: defaultValue is not translated
|
|
980
|
+
expr = `withDefault(${expr}, ${toStringExpression(ast.defaultValue)})`;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (!block || forceNewBlock) {
|
|
984
|
+
block = this.createBlock(block, "text", ctx);
|
|
985
|
+
this.insertBlock(`text(${expr})`, block, { ...ctx, forceNewBlock: forceNewBlock && !block });
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
const idx = block.insertData(expr, "txt");
|
|
989
|
+
const text = xmlDoc.createElement(`block-text-${idx}`);
|
|
990
|
+
block.insert(text);
|
|
991
|
+
}
|
|
992
|
+
return block.varName;
|
|
993
|
+
}
|
|
994
|
+
compileTOut(ast, ctx) {
|
|
995
|
+
let { block } = ctx;
|
|
996
|
+
if (block) {
|
|
997
|
+
this.insertAnchor(block);
|
|
998
|
+
}
|
|
999
|
+
block = this.createBlock(block, "html", ctx);
|
|
1000
|
+
let blockStr;
|
|
1001
|
+
if (ast.expr === "0") {
|
|
1002
|
+
this.helpers.add("zero");
|
|
1003
|
+
blockStr = `ctx[zero]`;
|
|
1004
|
+
}
|
|
1005
|
+
else if (ast.body) {
|
|
1006
|
+
let bodyValue = null;
|
|
1007
|
+
bodyValue = BlockDescription.nextBlockId;
|
|
1008
|
+
const subCtx = createContext(ctx);
|
|
1009
|
+
this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx);
|
|
1010
|
+
this.helpers.add("safeOutput");
|
|
1011
|
+
blockStr = `safeOutput(${compileExpr(ast.expr)}, b${bodyValue})`;
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
this.helpers.add("safeOutput");
|
|
1015
|
+
blockStr = `safeOutput(${compileExpr(ast.expr)})`;
|
|
1016
|
+
}
|
|
1017
|
+
this.insertBlock(blockStr, block, ctx);
|
|
1018
|
+
return block.varName;
|
|
1019
|
+
}
|
|
1020
|
+
compileTIfBranch(content, block, ctx) {
|
|
1021
|
+
this.target.indentLevel++;
|
|
1022
|
+
let childN = block.children.length;
|
|
1023
|
+
this.compileAST(content, createContext(ctx, { block, index: ctx.index }));
|
|
1024
|
+
if (block.children.length > childN) {
|
|
1025
|
+
// we have some content => need to insert an anchor at correct index
|
|
1026
|
+
this.insertAnchor(block, childN);
|
|
1027
|
+
}
|
|
1028
|
+
this.target.indentLevel--;
|
|
1029
|
+
}
|
|
1030
|
+
compileTIf(ast, ctx, nextNode) {
|
|
1031
|
+
let { block, forceNewBlock } = ctx;
|
|
1032
|
+
const codeIdx = this.target.code.length;
|
|
1033
|
+
const isNewBlock = !block || (block.type !== "multi" && forceNewBlock);
|
|
1034
|
+
if (block) {
|
|
1035
|
+
block.hasDynamicChildren = true;
|
|
1036
|
+
}
|
|
1037
|
+
if (!block || (block.type !== "multi" && forceNewBlock)) {
|
|
1038
|
+
block = this.createBlock(block, "multi", ctx);
|
|
1039
|
+
}
|
|
1040
|
+
this.addLine(`if (${compileExpr(ast.condition)}) {`);
|
|
1041
|
+
this.compileTIfBranch(ast.content, block, ctx);
|
|
1042
|
+
if (ast.tElif) {
|
|
1043
|
+
for (let clause of ast.tElif) {
|
|
1044
|
+
this.addLine(`} else if (${compileExpr(clause.condition)}) {`);
|
|
1045
|
+
this.compileTIfBranch(clause.content, block, ctx);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (ast.tElse) {
|
|
1049
|
+
this.addLine(`} else {`);
|
|
1050
|
+
this.compileTIfBranch(ast.tElse, block, ctx);
|
|
1051
|
+
}
|
|
1052
|
+
this.addLine("}");
|
|
1053
|
+
if (isNewBlock) {
|
|
1054
|
+
// note: this part is duplicated from end of compiledomnode:
|
|
1055
|
+
if (block.children.length) {
|
|
1056
|
+
const code = this.target.code;
|
|
1057
|
+
const children = block.children.slice();
|
|
1058
|
+
let current = children.shift();
|
|
1059
|
+
for (let i = codeIdx; i < code.length; i++) {
|
|
1060
|
+
if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
|
|
1061
|
+
code[i] = code[i].replace(`const ${current.varName}`, current.varName);
|
|
1062
|
+
current = children.shift();
|
|
1063
|
+
if (!current)
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
|
1068
|
+
}
|
|
1069
|
+
// note: this part is duplicated from end of compilemulti:
|
|
1070
|
+
const args = block.children.map((c) => c.varName).join(", ");
|
|
1071
|
+
this.insertBlock(`multi([${args}])`, block, ctx);
|
|
1072
|
+
}
|
|
1073
|
+
return block.varName;
|
|
1074
|
+
}
|
|
1075
|
+
compileTForeach(ast, ctx) {
|
|
1076
|
+
let { block } = ctx;
|
|
1077
|
+
if (block) {
|
|
1078
|
+
this.insertAnchor(block);
|
|
1079
|
+
}
|
|
1080
|
+
block = this.createBlock(block, "list", ctx);
|
|
1081
|
+
this.target.loopLevel++;
|
|
1082
|
+
const loopVar = `i${this.target.loopLevel}`;
|
|
1083
|
+
this.addLine(`ctx = Object.create(ctx);`);
|
|
1084
|
+
const vals = `v_block${block.id}`;
|
|
1085
|
+
const keys = `k_block${block.id}`;
|
|
1086
|
+
const l = `l_block${block.id}`;
|
|
1087
|
+
const c = `c_block${block.id}`;
|
|
1088
|
+
this.helpers.add("prepareList");
|
|
1089
|
+
this.define(`[${keys}, ${vals}, ${l}, ${c}]`, `prepareList(${compileExpr(ast.collection)});`);
|
|
1090
|
+
// Throw errors on duplicate keys in dev mode
|
|
1091
|
+
if (this.dev) {
|
|
1092
|
+
this.define(`keys${block.id}`, `new Set()`);
|
|
1093
|
+
}
|
|
1094
|
+
this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`);
|
|
1095
|
+
this.target.indentLevel++;
|
|
1096
|
+
this.addLine(`ctx[\`${ast.elem}\`] = ${keys}[${loopVar}];`);
|
|
1097
|
+
if (!ast.hasNoFirst) {
|
|
1098
|
+
this.addLine(`ctx[\`${ast.elem}_first\`] = ${loopVar} === 0;`);
|
|
1099
|
+
}
|
|
1100
|
+
if (!ast.hasNoLast) {
|
|
1101
|
+
this.addLine(`ctx[\`${ast.elem}_last\`] = ${loopVar} === ${keys}.length - 1;`);
|
|
1102
|
+
}
|
|
1103
|
+
if (!ast.hasNoIndex) {
|
|
1104
|
+
this.addLine(`ctx[\`${ast.elem}_index\`] = ${loopVar};`);
|
|
1105
|
+
}
|
|
1106
|
+
if (!ast.hasNoValue) {
|
|
1107
|
+
this.addLine(`ctx[\`${ast.elem}_value\`] = ${vals}[${loopVar}];`);
|
|
1108
|
+
}
|
|
1109
|
+
this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
|
|
1110
|
+
if (this.dev) {
|
|
1111
|
+
// Throw error on duplicate keys in dev mode
|
|
1112
|
+
this.helpers.add("OwlError");
|
|
1113
|
+
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}}\`)}`);
|
|
1114
|
+
this.addLine(`keys${block.id}.add(String(key${this.target.loopLevel}));`);
|
|
1115
|
+
}
|
|
1116
|
+
let id;
|
|
1117
|
+
if (ast.memo) {
|
|
1118
|
+
this.target.hasCache = true;
|
|
1119
|
+
id = generateId();
|
|
1120
|
+
this.define(`memo${id}`, compileExpr(ast.memo));
|
|
1121
|
+
this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);
|
|
1122
|
+
this.addLine(`if (vnode${id}) {`);
|
|
1123
|
+
this.target.indentLevel++;
|
|
1124
|
+
this.addLine(`if (shallowEqual(vnode${id}.memo, memo${id})) {`);
|
|
1125
|
+
this.target.indentLevel++;
|
|
1126
|
+
this.addLine(`${c}[${loopVar}] = vnode${id};`);
|
|
1127
|
+
this.addLine(`nextCache[key${this.target.loopLevel}] = vnode${id};`);
|
|
1128
|
+
this.addLine(`continue;`);
|
|
1129
|
+
this.target.indentLevel--;
|
|
1130
|
+
this.addLine("}");
|
|
1131
|
+
this.target.indentLevel--;
|
|
1132
|
+
this.addLine("}");
|
|
1133
|
+
}
|
|
1134
|
+
const subCtx = createContext(ctx, { block, index: loopVar });
|
|
1135
|
+
this.compileAST(ast.body, subCtx);
|
|
1136
|
+
if (ast.memo) {
|
|
1137
|
+
this.addLine(`nextCache[key${this.target.loopLevel}] = Object.assign(${c}[${loopVar}], {memo: memo${id}});`);
|
|
1138
|
+
}
|
|
1139
|
+
this.target.indentLevel--;
|
|
1140
|
+
this.target.loopLevel--;
|
|
1141
|
+
this.addLine(`}`);
|
|
1142
|
+
if (!ctx.isLast) {
|
|
1143
|
+
this.addLine(`ctx = ctx.__proto__;`);
|
|
1144
|
+
}
|
|
1145
|
+
this.insertBlock("l", block, ctx);
|
|
1146
|
+
return block.varName;
|
|
1147
|
+
}
|
|
1148
|
+
compileTKey(ast, ctx) {
|
|
1149
|
+
const tKeyExpr = generateId("tKey_");
|
|
1150
|
+
this.define(tKeyExpr, compileExpr(ast.expr));
|
|
1151
|
+
ctx = createContext(ctx, {
|
|
1152
|
+
tKeyExpr,
|
|
1153
|
+
block: ctx.block,
|
|
1154
|
+
index: ctx.index,
|
|
1155
|
+
});
|
|
1156
|
+
return this.compileAST(ast.content, ctx);
|
|
1157
|
+
}
|
|
1158
|
+
compileMulti(ast, ctx) {
|
|
1159
|
+
let { block, forceNewBlock } = ctx;
|
|
1160
|
+
const isNewBlock = !block || forceNewBlock;
|
|
1161
|
+
let codeIdx = this.target.code.length;
|
|
1162
|
+
if (isNewBlock) {
|
|
1163
|
+
const n = ast.content.filter((c) => c.type !== 6 /* TSet */).length;
|
|
1164
|
+
let result = null;
|
|
1165
|
+
if (n <= 1) {
|
|
1166
|
+
for (let child of ast.content) {
|
|
1167
|
+
const blockName = this.compileAST(child, ctx);
|
|
1168
|
+
result = result || blockName;
|
|
1169
|
+
}
|
|
1170
|
+
return result;
|
|
1171
|
+
}
|
|
1172
|
+
block = this.createBlock(block, "multi", ctx);
|
|
1173
|
+
}
|
|
1174
|
+
let index = 0;
|
|
1175
|
+
for (let i = 0, l = ast.content.length; i < l; i++) {
|
|
1176
|
+
const child = ast.content[i];
|
|
1177
|
+
const isTSet = child.type === 6 /* TSet */;
|
|
1178
|
+
const subCtx = createContext(ctx, {
|
|
1179
|
+
block,
|
|
1180
|
+
index,
|
|
1181
|
+
forceNewBlock: !isTSet,
|
|
1182
|
+
isLast: ctx.isLast && i === l - 1,
|
|
1183
|
+
});
|
|
1184
|
+
this.compileAST(child, subCtx);
|
|
1185
|
+
if (!isTSet) {
|
|
1186
|
+
index++;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (isNewBlock) {
|
|
1190
|
+
if (block.hasDynamicChildren && block.children.length) {
|
|
1191
|
+
const code = this.target.code;
|
|
1192
|
+
const children = block.children.slice();
|
|
1193
|
+
let current = children.shift();
|
|
1194
|
+
for (let i = codeIdx; i < code.length; i++) {
|
|
1195
|
+
if (code[i].trimStart().startsWith(`const ${current.varName} `)) {
|
|
1196
|
+
code[i] = code[i].replace(`const ${current.varName}`, current.varName);
|
|
1197
|
+
current = children.shift();
|
|
1198
|
+
if (!current)
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
this.addLine(`let ${block.children.map((c) => c.varName).join(", ")};`, codeIdx);
|
|
1203
|
+
}
|
|
1204
|
+
const args = block.children.map((c) => c.varName).join(", ");
|
|
1205
|
+
this.insertBlock(`multi([${args}])`, block, ctx);
|
|
1206
|
+
}
|
|
1207
|
+
return block.varName;
|
|
1208
|
+
}
|
|
1209
|
+
compileTCall(ast, ctx) {
|
|
1210
|
+
let { block, forceNewBlock } = ctx;
|
|
1211
|
+
let ctxVar = ctx.ctxVar || "ctx";
|
|
1212
|
+
if (ast.context) {
|
|
1213
|
+
ctxVar = generateId("ctx");
|
|
1214
|
+
this.addLine(`let ${ctxVar} = ${compileExpr(ast.context)};`);
|
|
1215
|
+
}
|
|
1216
|
+
const isDynamic = INTERP_REGEXP.test(ast.name);
|
|
1217
|
+
const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`";
|
|
1218
|
+
if (block && !forceNewBlock) {
|
|
1219
|
+
this.insertAnchor(block);
|
|
1220
|
+
}
|
|
1221
|
+
block = this.createBlock(block, "multi", ctx);
|
|
1222
|
+
if (ast.body) {
|
|
1223
|
+
this.addLine(`${ctxVar} = Object.create(${ctxVar});`);
|
|
1224
|
+
this.addLine(`${ctxVar}[isBoundary] = 1;`);
|
|
1225
|
+
this.helpers.add("isBoundary");
|
|
1226
|
+
const subCtx = createContext(ctx, { ctxVar });
|
|
1227
|
+
const bl = this.compileMulti({ type: 3 /* Multi */, content: ast.body }, subCtx);
|
|
1228
|
+
if (bl) {
|
|
1229
|
+
this.helpers.add("zero");
|
|
1230
|
+
this.addLine(`${ctxVar}[zero] = ${bl};`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const key = this.generateComponentKey();
|
|
1234
|
+
if (isDynamic) {
|
|
1235
|
+
const templateVar = generateId("template");
|
|
1236
|
+
if (!this.staticDefs.find((d) => d.id === "call")) {
|
|
1237
|
+
this.staticDefs.push({ id: "call", expr: `app.callTemplate.bind(app)` });
|
|
1238
|
+
}
|
|
1239
|
+
this.define(templateVar, subTemplate);
|
|
1240
|
+
this.insertBlock(`call(this, ${templateVar}, ${ctxVar}, node, ${key})`, block, {
|
|
1241
|
+
...ctx,
|
|
1242
|
+
forceNewBlock: !block,
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
const id = generateId(`callTemplate_`);
|
|
1247
|
+
this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
|
|
1248
|
+
this.insertBlock(`${id}.call(this, ${ctxVar}, node, ${key})`, block, {
|
|
1249
|
+
...ctx,
|
|
1250
|
+
forceNewBlock: !block,
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
if (ast.body && !ctx.isLast) {
|
|
1254
|
+
this.addLine(`${ctxVar} = ${ctxVar}.__proto__;`);
|
|
1255
|
+
}
|
|
1256
|
+
return block.varName;
|
|
1257
|
+
}
|
|
1258
|
+
compileTCallBlock(ast, ctx) {
|
|
1259
|
+
let { block, forceNewBlock } = ctx;
|
|
1260
|
+
if (block) {
|
|
1261
|
+
if (!forceNewBlock) {
|
|
1262
|
+
this.insertAnchor(block);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
block = this.createBlock(block, "multi", ctx);
|
|
1266
|
+
this.insertBlock(compileExpr(ast.name), block, { ...ctx, forceNewBlock: !block });
|
|
1267
|
+
return block.varName;
|
|
1268
|
+
}
|
|
1269
|
+
compileTSet(ast, ctx) {
|
|
1270
|
+
this.target.shouldProtectScope = true;
|
|
1271
|
+
this.helpers.add("isBoundary").add("withDefault");
|
|
1272
|
+
const expr = ast.value ? compileExpr(ast.value || "") : "null";
|
|
1273
|
+
if (ast.body) {
|
|
1274
|
+
this.helpers.add("LazyValue");
|
|
1275
|
+
const bodyAst = { type: 3 /* Multi */, content: ast.body };
|
|
1276
|
+
const name = this.compileInNewTarget("value", bodyAst, ctx);
|
|
1277
|
+
let key = this.target.currentKey(ctx);
|
|
1278
|
+
let value = `new LazyValue(${name}, ctx, this, node, ${key})`;
|
|
1279
|
+
value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value;
|
|
1280
|
+
this.addLine(`ctx[\`${ast.name}\`] = ${value};`);
|
|
1281
|
+
}
|
|
1282
|
+
else {
|
|
1283
|
+
let value;
|
|
1284
|
+
if (ast.defaultValue) {
|
|
1285
|
+
const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue);
|
|
1286
|
+
if (ast.value) {
|
|
1287
|
+
value = `withDefault(${expr}, ${defaultValue})`;
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
value = defaultValue;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
else {
|
|
1294
|
+
value = expr;
|
|
1295
|
+
}
|
|
1296
|
+
this.helpers.add("setContextValue");
|
|
1297
|
+
this.addLine(`setContextValue(${ctx.ctxVar || "ctx"}, "${ast.name}", ${value});`);
|
|
1298
|
+
}
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
generateComponentKey(currentKey = "key") {
|
|
1302
|
+
const parts = [generateId("__")];
|
|
1303
|
+
for (let i = 0; i < this.target.loopLevel; i++) {
|
|
1304
|
+
parts.push(`\${key${i + 1}}`);
|
|
1305
|
+
}
|
|
1306
|
+
return `${currentKey} + \`${parts.join("__")}\``;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Formats a prop name and value into a string suitable to be inserted in the
|
|
1310
|
+
* generated code. For example:
|
|
1311
|
+
*
|
|
1312
|
+
* Name Value Result
|
|
1313
|
+
* ---------------------------------------------------------
|
|
1314
|
+
* "number" "state" "number: ctx['state']"
|
|
1315
|
+
* "something" "" "something: undefined"
|
|
1316
|
+
* "some-prop" "state" "'some-prop': ctx['state']"
|
|
1317
|
+
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
|
1318
|
+
*/
|
|
1319
|
+
formatProp(name, value) {
|
|
1320
|
+
if (name.endsWith(".translate")) {
|
|
1321
|
+
value = toStringExpression(this.translateFn(value));
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
value = this.captureExpression(value);
|
|
1325
|
+
}
|
|
1326
|
+
if (name.includes(".")) {
|
|
1327
|
+
let [_name, suffix] = name.split(".");
|
|
1328
|
+
name = _name;
|
|
1329
|
+
switch (suffix) {
|
|
1330
|
+
case "bind":
|
|
1331
|
+
value = `(${value}).bind(this)`;
|
|
1332
|
+
break;
|
|
1333
|
+
case "alike":
|
|
1334
|
+
case "translate":
|
|
1335
|
+
break;
|
|
1336
|
+
default:
|
|
1337
|
+
throw new OwlError(`Invalid prop suffix: ${suffix}`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
|
1341
|
+
return `${name}: ${value || undefined}`;
|
|
1342
|
+
}
|
|
1343
|
+
formatPropObject(obj) {
|
|
1344
|
+
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
|
|
1345
|
+
}
|
|
1346
|
+
getPropString(props, dynProps) {
|
|
1347
|
+
let propString = `{${props.join(",")}}`;
|
|
1348
|
+
if (dynProps) {
|
|
1349
|
+
propString = `Object.assign({}, ${compileExpr(dynProps)}${props.length ? ", " + propString : ""})`;
|
|
1350
|
+
}
|
|
1351
|
+
return propString;
|
|
1352
|
+
}
|
|
1353
|
+
compileComponent(ast, ctx) {
|
|
1354
|
+
let { block } = ctx;
|
|
1355
|
+
// props
|
|
1356
|
+
const hasSlotsProp = "slots" in (ast.props || {});
|
|
1357
|
+
const props = ast.props ? this.formatPropObject(ast.props) : [];
|
|
1358
|
+
// slots
|
|
1359
|
+
let slotDef = "";
|
|
1360
|
+
if (ast.slots) {
|
|
1361
|
+
let ctxStr = "ctx";
|
|
1362
|
+
if (this.target.loopLevel || !this.hasSafeContext) {
|
|
1363
|
+
ctxStr = generateId("ctx");
|
|
1364
|
+
this.helpers.add("capture");
|
|
1365
|
+
this.define(ctxStr, `capture(ctx)`);
|
|
1366
|
+
}
|
|
1367
|
+
let slotStr = [];
|
|
1368
|
+
for (let slotName in ast.slots) {
|
|
1369
|
+
const slotAst = ast.slots[slotName];
|
|
1370
|
+
const params = [];
|
|
1371
|
+
if (slotAst.content) {
|
|
1372
|
+
const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on);
|
|
1373
|
+
params.push(`__render: ${name}.bind(this), __ctx: ${ctxStr}`);
|
|
1374
|
+
}
|
|
1375
|
+
const scope = ast.slots[slotName].scope;
|
|
1376
|
+
if (scope) {
|
|
1377
|
+
params.push(`__scope: "${scope}"`);
|
|
1378
|
+
}
|
|
1379
|
+
if (ast.slots[slotName].attrs) {
|
|
1380
|
+
params.push(...this.formatPropObject(ast.slots[slotName].attrs));
|
|
1381
|
+
}
|
|
1382
|
+
const slotInfo = `{${params.join(", ")}}`;
|
|
1383
|
+
slotStr.push(`'${slotName}': ${slotInfo}`);
|
|
1384
|
+
}
|
|
1385
|
+
slotDef = `{${slotStr.join(", ")}}`;
|
|
1386
|
+
}
|
|
1387
|
+
if (slotDef && !(ast.dynamicProps || hasSlotsProp)) {
|
|
1388
|
+
this.helpers.add("markRaw");
|
|
1389
|
+
props.push(`slots: markRaw(${slotDef})`);
|
|
1390
|
+
}
|
|
1391
|
+
let propString = this.getPropString(props, ast.dynamicProps);
|
|
1392
|
+
let propVar;
|
|
1393
|
+
if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
|
|
1394
|
+
propVar = generateId("props");
|
|
1395
|
+
this.define(propVar, propString);
|
|
1396
|
+
propString = propVar;
|
|
1397
|
+
}
|
|
1398
|
+
if (slotDef && (ast.dynamicProps || hasSlotsProp)) {
|
|
1399
|
+
this.helpers.add("markRaw");
|
|
1400
|
+
this.addLine(`${propVar}.slots = markRaw(Object.assign(${slotDef}, ${propVar}.slots))`);
|
|
1401
|
+
}
|
|
1402
|
+
// cmap key
|
|
1403
|
+
let expr;
|
|
1404
|
+
if (ast.isDynamic) {
|
|
1405
|
+
expr = generateId("Comp");
|
|
1406
|
+
this.define(expr, compileExpr(ast.name));
|
|
1407
|
+
}
|
|
1408
|
+
else {
|
|
1409
|
+
expr = `\`${ast.name}\``;
|
|
1410
|
+
}
|
|
1411
|
+
if (this.dev) {
|
|
1412
|
+
this.addLine(`helpers.validateProps(${expr}, ${propVar}, this);`);
|
|
1413
|
+
}
|
|
1414
|
+
if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) {
|
|
1415
|
+
// todo: check the forcenewblock condition
|
|
1416
|
+
this.insertAnchor(block);
|
|
1417
|
+
}
|
|
1418
|
+
let keyArg = this.generateComponentKey();
|
|
1419
|
+
if (ctx.tKeyExpr) {
|
|
1420
|
+
keyArg = `${ctx.tKeyExpr} + ${keyArg}`;
|
|
1421
|
+
}
|
|
1422
|
+
let id = generateId("comp");
|
|
1423
|
+
const propList = [];
|
|
1424
|
+
for (let p in ast.props || {}) {
|
|
1425
|
+
let [name, suffix] = p.split(".");
|
|
1426
|
+
if (!suffix) {
|
|
1427
|
+
propList.push(`"${name}"`);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
this.staticDefs.push({
|
|
1431
|
+
id,
|
|
1432
|
+
expr: `app.createComponent(${ast.isDynamic ? null : expr}, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, [${propList}])`,
|
|
1433
|
+
});
|
|
1434
|
+
if (ast.isDynamic) {
|
|
1435
|
+
// If the component class changes, this can cause delayed renders to go
|
|
1436
|
+
// through if the key doesn't change. Use the component name for now.
|
|
1437
|
+
// This means that two component classes with the same name isn't supported
|
|
1438
|
+
// in t-component. We can generate a unique id per class later if needed.
|
|
1439
|
+
keyArg = `(${expr}).name + ${keyArg}`;
|
|
1440
|
+
}
|
|
1441
|
+
let blockExpr = `${id}(${propString}, ${keyArg}, node, this, ${ast.isDynamic ? expr : null})`;
|
|
1442
|
+
if (ast.isDynamic) {
|
|
1443
|
+
blockExpr = `toggler(${expr}, ${blockExpr})`;
|
|
1444
|
+
}
|
|
1445
|
+
// event handling
|
|
1446
|
+
if (ast.on) {
|
|
1447
|
+
blockExpr = this.wrapWithEventCatcher(blockExpr, ast.on);
|
|
1448
|
+
}
|
|
1449
|
+
block = this.createBlock(block, "multi", ctx);
|
|
1450
|
+
this.insertBlock(blockExpr, block, ctx);
|
|
1451
|
+
return block.varName;
|
|
1452
|
+
}
|
|
1453
|
+
wrapWithEventCatcher(expr, on) {
|
|
1454
|
+
this.helpers.add("createCatcher");
|
|
1455
|
+
let name = generateId("catcher");
|
|
1456
|
+
let spec = {};
|
|
1457
|
+
let handlers = [];
|
|
1458
|
+
for (let ev in on) {
|
|
1459
|
+
let handlerId = generateId("hdlr");
|
|
1460
|
+
let idx = handlers.push(handlerId) - 1;
|
|
1461
|
+
spec[ev] = idx;
|
|
1462
|
+
const handler = this.generateHandlerCode(ev, on[ev]);
|
|
1463
|
+
this.define(handlerId, handler);
|
|
1464
|
+
}
|
|
1465
|
+
this.staticDefs.push({ id: name, expr: `createCatcher(${JSON.stringify(spec)})` });
|
|
1466
|
+
return `${name}(${expr}, [${handlers.join(",")}])`;
|
|
1467
|
+
}
|
|
1468
|
+
compileTSlot(ast, ctx) {
|
|
1469
|
+
this.helpers.add("callSlot");
|
|
1470
|
+
let { block } = ctx;
|
|
1471
|
+
let blockString;
|
|
1472
|
+
let slotName;
|
|
1473
|
+
let dynamic = false;
|
|
1474
|
+
let isMultiple = false;
|
|
1475
|
+
if (ast.name.match(INTERP_REGEXP)) {
|
|
1476
|
+
dynamic = true;
|
|
1477
|
+
isMultiple = true;
|
|
1478
|
+
slotName = interpolate(ast.name);
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
1481
|
+
slotName = "'" + ast.name + "'";
|
|
1482
|
+
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
|
1483
|
+
this.slotNames.add(ast.name);
|
|
1484
|
+
}
|
|
1485
|
+
const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
|
|
1486
|
+
if (ast.attrs) {
|
|
1487
|
+
delete ast.attrs["t-props"];
|
|
1488
|
+
}
|
|
1489
|
+
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
|
1490
|
+
if (isMultiple) {
|
|
1491
|
+
key = this.generateComponentKey(key);
|
|
1492
|
+
}
|
|
1493
|
+
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
|
1494
|
+
const scope = this.getPropString(props, dynProps);
|
|
1495
|
+
if (ast.defaultContent) {
|
|
1496
|
+
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
|
1497
|
+
blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope}, ${name}.bind(this))`;
|
|
1498
|
+
}
|
|
1499
|
+
else {
|
|
1500
|
+
if (dynamic) {
|
|
1501
|
+
let name = generateId("slot");
|
|
1502
|
+
this.define(name, slotName);
|
|
1503
|
+
blockString = `toggler(${name}, callSlot(ctx, node, ${key}, ${name}, ${dynamic}, ${scope}))`;
|
|
1504
|
+
}
|
|
1505
|
+
else {
|
|
1506
|
+
blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope})`;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
// event handling
|
|
1510
|
+
if (ast.on) {
|
|
1511
|
+
blockString = this.wrapWithEventCatcher(blockString, ast.on);
|
|
1512
|
+
}
|
|
1513
|
+
if (block) {
|
|
1514
|
+
this.insertAnchor(block);
|
|
1515
|
+
}
|
|
1516
|
+
block = this.createBlock(block, "multi", ctx);
|
|
1517
|
+
this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
|
|
1518
|
+
return block.varName;
|
|
1519
|
+
}
|
|
1520
|
+
compileTTranslation(ast, ctx) {
|
|
1521
|
+
if (ast.content) {
|
|
1522
|
+
return this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));
|
|
1523
|
+
}
|
|
1524
|
+
return null;
|
|
1525
|
+
}
|
|
1526
|
+
compileTPortal(ast, ctx) {
|
|
1527
|
+
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
|
1528
|
+
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
|
1529
|
+
}
|
|
1530
|
+
let { block } = ctx;
|
|
1531
|
+
const name = this.compileInNewTarget("slot", ast.content, ctx);
|
|
1532
|
+
let ctxStr = "ctx";
|
|
1533
|
+
if (this.target.loopLevel || !this.hasSafeContext) {
|
|
1534
|
+
ctxStr = generateId("ctx");
|
|
1535
|
+
this.helpers.add("capture");
|
|
1536
|
+
this.define(ctxStr, `capture(ctx)`);
|
|
1537
|
+
}
|
|
1538
|
+
let id = generateId("comp");
|
|
1539
|
+
this.staticDefs.push({
|
|
1540
|
+
id,
|
|
1541
|
+
expr: `app.createComponent(null, false, true, false, false)`,
|
|
1542
|
+
});
|
|
1543
|
+
const target = compileExpr(ast.target);
|
|
1544
|
+
const key = this.generateComponentKey();
|
|
1545
|
+
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, ${key}, node, ctx, Portal)`;
|
|
1546
|
+
if (block) {
|
|
1547
|
+
this.insertAnchor(block);
|
|
1548
|
+
}
|
|
1549
|
+
block = this.createBlock(block, "multi", ctx);
|
|
1550
|
+
this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
|
|
1551
|
+
return block.varName;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* Parses an XML string into an XML document, throwing errors on parser errors
|
|
1557
|
+
* instead of returning an XML document containing the parseerror.
|
|
1558
|
+
*
|
|
1559
|
+
* @param xml the string to parse
|
|
1560
|
+
* @returns an XML document corresponding to the content of the string
|
|
1561
|
+
*/
|
|
1562
|
+
function parseXML(xml) {
|
|
1563
|
+
const parser = new DOMParser();
|
|
1564
|
+
const doc = parser.parseFromString(xml, "text/xml");
|
|
1565
|
+
if (doc.getElementsByTagName("parsererror").length) {
|
|
1566
|
+
let msg = "Invalid XML in template.";
|
|
1567
|
+
const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
|
|
1568
|
+
if (parsererrorText) {
|
|
1569
|
+
msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
|
|
1570
|
+
const re = /\d+/g;
|
|
1571
|
+
const firstMatch = re.exec(parsererrorText);
|
|
1572
|
+
if (firstMatch) {
|
|
1573
|
+
const lineNumber = Number(firstMatch[0]);
|
|
1574
|
+
const line = xml.split("\n")[lineNumber - 1];
|
|
1575
|
+
const secondMatch = re.exec(parsererrorText);
|
|
1576
|
+
if (line && secondMatch) {
|
|
1577
|
+
const columnIndex = Number(secondMatch[0]) - 1;
|
|
1578
|
+
if (line[columnIndex]) {
|
|
1579
|
+
msg +=
|
|
1580
|
+
`\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
|
|
1581
|
+
`${line}\n${"-".repeat(columnIndex - 1)}^`;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
throw new OwlError(msg);
|
|
1587
|
+
}
|
|
1588
|
+
return doc;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// -----------------------------------------------------------------------------
|
|
1592
|
+
// Parser
|
|
1593
|
+
// -----------------------------------------------------------------------------
|
|
1594
|
+
const cache = new WeakMap();
|
|
1595
|
+
function parse(xml, customDir) {
|
|
1596
|
+
const ctx = {
|
|
1597
|
+
inPreTag: false,
|
|
1598
|
+
customDirectives: customDir,
|
|
1599
|
+
};
|
|
1600
|
+
if (typeof xml === "string") {
|
|
1601
|
+
const elem = parseXML(`<t>${xml}</t>`).firstChild;
|
|
1602
|
+
return _parse(elem, ctx);
|
|
1603
|
+
}
|
|
1604
|
+
let ast = cache.get(xml);
|
|
1605
|
+
if (!ast) {
|
|
1606
|
+
// we clone here the xml to prevent modifying it in place
|
|
1607
|
+
ast = _parse(xml.cloneNode(true), ctx);
|
|
1608
|
+
cache.set(xml, ast);
|
|
1609
|
+
}
|
|
1610
|
+
return ast;
|
|
1611
|
+
}
|
|
1612
|
+
function _parse(xml, ctx) {
|
|
1613
|
+
normalizeXML(xml);
|
|
1614
|
+
return parseNode(xml, ctx) || { type: 0 /* Text */, value: "" };
|
|
1615
|
+
}
|
|
1616
|
+
function parseNode(node, ctx) {
|
|
1617
|
+
if (!(node instanceof Element)) {
|
|
1618
|
+
return parseTextCommentNode(node, ctx);
|
|
1619
|
+
}
|
|
1620
|
+
return (parseTCustom(node, ctx) ||
|
|
1621
|
+
parseTDebugLog(node, ctx) ||
|
|
1622
|
+
parseTForEach(node, ctx) ||
|
|
1623
|
+
parseTIf(node, ctx) ||
|
|
1624
|
+
parseTPortal(node, ctx) ||
|
|
1625
|
+
parseTCall(node, ctx) ||
|
|
1626
|
+
parseTCallBlock(node) ||
|
|
1627
|
+
parseTEscNode(node, ctx) ||
|
|
1628
|
+
parseTOutNode(node, ctx) ||
|
|
1629
|
+
parseTKey(node, ctx) ||
|
|
1630
|
+
parseTTranslation(node, ctx) ||
|
|
1631
|
+
parseTSlot(node, ctx) ||
|
|
1632
|
+
parseComponent(node, ctx) ||
|
|
1633
|
+
parseDOMNode(node, ctx) ||
|
|
1634
|
+
parseTSetNode(node, ctx) ||
|
|
1635
|
+
parseTNode(node, ctx));
|
|
1636
|
+
}
|
|
1637
|
+
// -----------------------------------------------------------------------------
|
|
1638
|
+
// <t /> tag
|
|
1639
|
+
// -----------------------------------------------------------------------------
|
|
1640
|
+
function parseTNode(node, ctx) {
|
|
1641
|
+
if (node.tagName !== "t") {
|
|
1642
|
+
return null;
|
|
1643
|
+
}
|
|
1644
|
+
return parseChildNodes(node, ctx);
|
|
1645
|
+
}
|
|
1646
|
+
// -----------------------------------------------------------------------------
|
|
1647
|
+
// Text and Comment Nodes
|
|
1648
|
+
// -----------------------------------------------------------------------------
|
|
1649
|
+
const lineBreakRE = /[\r\n]/;
|
|
1650
|
+
function parseTextCommentNode(node, ctx) {
|
|
1651
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1652
|
+
let value = node.textContent || "";
|
|
1653
|
+
if (!ctx.inPreTag && lineBreakRE.test(value) && !value.trim()) {
|
|
1654
|
+
return null;
|
|
1655
|
+
}
|
|
1656
|
+
return { type: 0 /* Text */, value };
|
|
1657
|
+
}
|
|
1658
|
+
else if (node.nodeType === Node.COMMENT_NODE) {
|
|
1659
|
+
return { type: 1 /* Comment */, value: node.textContent || "" };
|
|
1660
|
+
}
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
function parseTCustom(node, ctx) {
|
|
1664
|
+
if (!ctx.customDirectives) {
|
|
1665
|
+
return null;
|
|
1666
|
+
}
|
|
1667
|
+
const nodeAttrsNames = node.getAttributeNames();
|
|
1668
|
+
for (let attr of nodeAttrsNames) {
|
|
1669
|
+
if (attr === "t-custom" || attr === "t-custom-") {
|
|
1670
|
+
throw new OwlError("Missing custom directive name with t-custom directive");
|
|
1671
|
+
}
|
|
1672
|
+
if (attr.startsWith("t-custom-")) {
|
|
1673
|
+
const directiveName = attr.split(".")[0].slice(9);
|
|
1674
|
+
const customDirective = ctx.customDirectives[directiveName];
|
|
1675
|
+
if (!customDirective) {
|
|
1676
|
+
throw new OwlError(`Custom directive "${directiveName}" is not defined`);
|
|
1677
|
+
}
|
|
1678
|
+
const value = node.getAttribute(attr);
|
|
1679
|
+
const modifiers = attr.split(".").slice(1);
|
|
1680
|
+
node.removeAttribute(attr);
|
|
1681
|
+
try {
|
|
1682
|
+
customDirective(node, value, modifiers);
|
|
1683
|
+
}
|
|
1684
|
+
catch (error) {
|
|
1685
|
+
throw new OwlError(`Custom directive "${directiveName}" throw the following error: ${error}`);
|
|
1686
|
+
}
|
|
1687
|
+
return parseNode(node, ctx);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
// -----------------------------------------------------------------------------
|
|
1693
|
+
// debugging
|
|
1694
|
+
// -----------------------------------------------------------------------------
|
|
1695
|
+
function parseTDebugLog(node, ctx) {
|
|
1696
|
+
if (node.hasAttribute("t-debug")) {
|
|
1697
|
+
node.removeAttribute("t-debug");
|
|
1698
|
+
return {
|
|
1699
|
+
type: 12 /* TDebug */,
|
|
1700
|
+
content: parseNode(node, ctx),
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
if (node.hasAttribute("t-log")) {
|
|
1704
|
+
const expr = node.getAttribute("t-log");
|
|
1705
|
+
node.removeAttribute("t-log");
|
|
1706
|
+
return {
|
|
1707
|
+
type: 13 /* TLog */,
|
|
1708
|
+
expr,
|
|
1709
|
+
content: parseNode(node, ctx),
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
return null;
|
|
1713
|
+
}
|
|
1714
|
+
// -----------------------------------------------------------------------------
|
|
1715
|
+
// Regular dom node
|
|
1716
|
+
// -----------------------------------------------------------------------------
|
|
1717
|
+
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
|
|
1718
|
+
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
|
|
1719
|
+
const ROOT_SVG_TAGS = new Set(["svg", "g", "path"]);
|
|
1720
|
+
function parseDOMNode(node, ctx) {
|
|
1721
|
+
const { tagName } = node;
|
|
1722
|
+
const dynamicTag = node.getAttribute("t-tag");
|
|
1723
|
+
node.removeAttribute("t-tag");
|
|
1724
|
+
if (tagName === "t" && !dynamicTag) {
|
|
1725
|
+
return null;
|
|
1726
|
+
}
|
|
1727
|
+
if (tagName.startsWith("block-")) {
|
|
1728
|
+
throw new OwlError(`Invalid tag name: '${tagName}'`);
|
|
1729
|
+
}
|
|
1730
|
+
ctx = Object.assign({}, ctx);
|
|
1731
|
+
if (tagName === "pre") {
|
|
1732
|
+
ctx.inPreTag = true;
|
|
1733
|
+
}
|
|
1734
|
+
let ns = !ctx.nameSpace && ROOT_SVG_TAGS.has(tagName) ? "http://www.w3.org/2000/svg" : null;
|
|
1735
|
+
const ref = node.getAttribute("t-ref");
|
|
1736
|
+
node.removeAttribute("t-ref");
|
|
1737
|
+
const nodeAttrsNames = node.getAttributeNames();
|
|
1738
|
+
let attrs = null;
|
|
1739
|
+
let on = null;
|
|
1740
|
+
let model = null;
|
|
1741
|
+
for (let attr of nodeAttrsNames) {
|
|
1742
|
+
const value = node.getAttribute(attr);
|
|
1743
|
+
if (attr === "t-on" || attr === "t-on-") {
|
|
1744
|
+
throw new OwlError("Missing event name with t-on directive");
|
|
1745
|
+
}
|
|
1746
|
+
if (attr.startsWith("t-on-")) {
|
|
1747
|
+
on = on || {};
|
|
1748
|
+
on[attr.slice(5)] = value;
|
|
1749
|
+
}
|
|
1750
|
+
else if (attr.startsWith("t-model")) {
|
|
1751
|
+
if (!["input", "select", "textarea"].includes(tagName)) {
|
|
1752
|
+
throw new OwlError("The t-model directive only works with <input>, <textarea> and <select>");
|
|
1753
|
+
}
|
|
1754
|
+
let baseExpr, expr;
|
|
1755
|
+
if (hasDotAtTheEnd.test(value)) {
|
|
1756
|
+
const index = value.lastIndexOf(".");
|
|
1757
|
+
baseExpr = value.slice(0, index);
|
|
1758
|
+
expr = `'${value.slice(index + 1)}'`;
|
|
1759
|
+
}
|
|
1760
|
+
else if (hasBracketsAtTheEnd.test(value)) {
|
|
1761
|
+
const index = value.lastIndexOf("[");
|
|
1762
|
+
baseExpr = value.slice(0, index);
|
|
1763
|
+
expr = value.slice(index + 1, -1);
|
|
1764
|
+
}
|
|
1765
|
+
else {
|
|
1766
|
+
throw new OwlError(`Invalid t-model expression: "${value}" (it should be assignable)`);
|
|
1767
|
+
}
|
|
1768
|
+
const typeAttr = node.getAttribute("type");
|
|
1769
|
+
const isInput = tagName === "input";
|
|
1770
|
+
const isSelect = tagName === "select";
|
|
1771
|
+
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
|
1772
|
+
const isRadioInput = isInput && typeAttr === "radio";
|
|
1773
|
+
const hasTrimMod = attr.includes(".trim");
|
|
1774
|
+
const hasLazyMod = hasTrimMod || attr.includes(".lazy");
|
|
1775
|
+
const hasNumberMod = attr.includes(".number");
|
|
1776
|
+
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
|
1777
|
+
model = {
|
|
1778
|
+
baseExpr,
|
|
1779
|
+
expr,
|
|
1780
|
+
targetAttr: isCheckboxInput ? "checked" : "value",
|
|
1781
|
+
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
|
1782
|
+
eventType,
|
|
1783
|
+
hasDynamicChildren: false,
|
|
1784
|
+
shouldTrim: hasTrimMod,
|
|
1785
|
+
shouldNumberize: hasNumberMod,
|
|
1786
|
+
};
|
|
1787
|
+
if (isSelect) {
|
|
1788
|
+
// don't pollute the original ctx
|
|
1789
|
+
ctx = Object.assign({}, ctx);
|
|
1790
|
+
ctx.tModelInfo = model;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
else if (attr.startsWith("block-")) {
|
|
1794
|
+
throw new OwlError(`Invalid attribute: '${attr}'`);
|
|
1795
|
+
}
|
|
1796
|
+
else if (attr === "xmlns") {
|
|
1797
|
+
ns = value;
|
|
1798
|
+
}
|
|
1799
|
+
else if (attr !== "t-name") {
|
|
1800
|
+
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
|
1801
|
+
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
|
1802
|
+
}
|
|
1803
|
+
const tModel = ctx.tModelInfo;
|
|
1804
|
+
if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
|
|
1805
|
+
tModel.hasDynamicChildren = true;
|
|
1806
|
+
}
|
|
1807
|
+
attrs = attrs || {};
|
|
1808
|
+
attrs[attr] = value;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (ns) {
|
|
1812
|
+
ctx.nameSpace = ns;
|
|
1813
|
+
}
|
|
1814
|
+
const children = parseChildren(node, ctx);
|
|
1815
|
+
return {
|
|
1816
|
+
type: 2 /* DomNode */,
|
|
1817
|
+
tag: tagName,
|
|
1818
|
+
dynamicTag,
|
|
1819
|
+
attrs,
|
|
1820
|
+
on,
|
|
1821
|
+
ref,
|
|
1822
|
+
content: children,
|
|
1823
|
+
model,
|
|
1824
|
+
ns,
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
// -----------------------------------------------------------------------------
|
|
1828
|
+
// t-esc
|
|
1829
|
+
// -----------------------------------------------------------------------------
|
|
1830
|
+
function parseTEscNode(node, ctx) {
|
|
1831
|
+
if (!node.hasAttribute("t-esc")) {
|
|
1832
|
+
return null;
|
|
1833
|
+
}
|
|
1834
|
+
const escValue = node.getAttribute("t-esc");
|
|
1835
|
+
node.removeAttribute("t-esc");
|
|
1836
|
+
const tesc = {
|
|
1837
|
+
type: 4 /* TEsc */,
|
|
1838
|
+
expr: escValue,
|
|
1839
|
+
defaultValue: node.textContent || "",
|
|
1840
|
+
};
|
|
1841
|
+
let ref = node.getAttribute("t-ref");
|
|
1842
|
+
node.removeAttribute("t-ref");
|
|
1843
|
+
const ast = parseNode(node, ctx);
|
|
1844
|
+
if (!ast) {
|
|
1845
|
+
return tesc;
|
|
1846
|
+
}
|
|
1847
|
+
if (ast.type === 2 /* DomNode */) {
|
|
1848
|
+
return {
|
|
1849
|
+
...ast,
|
|
1850
|
+
ref,
|
|
1851
|
+
content: [tesc],
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
return tesc;
|
|
1855
|
+
}
|
|
1856
|
+
// -----------------------------------------------------------------------------
|
|
1857
|
+
// t-out
|
|
1858
|
+
// -----------------------------------------------------------------------------
|
|
1859
|
+
function parseTOutNode(node, ctx) {
|
|
1860
|
+
if (!node.hasAttribute("t-out") && !node.hasAttribute("t-raw")) {
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
if (node.hasAttribute("t-raw")) {
|
|
1864
|
+
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`);
|
|
1865
|
+
}
|
|
1866
|
+
const expr = (node.getAttribute("t-out") || node.getAttribute("t-raw"));
|
|
1867
|
+
node.removeAttribute("t-out");
|
|
1868
|
+
node.removeAttribute("t-raw");
|
|
1869
|
+
const tOut = { type: 8 /* TOut */, expr, body: null };
|
|
1870
|
+
const ref = node.getAttribute("t-ref");
|
|
1871
|
+
node.removeAttribute("t-ref");
|
|
1872
|
+
const ast = parseNode(node, ctx);
|
|
1873
|
+
if (!ast) {
|
|
1874
|
+
return tOut;
|
|
1875
|
+
}
|
|
1876
|
+
if (ast.type === 2 /* DomNode */) {
|
|
1877
|
+
tOut.body = ast.content.length ? ast.content : null;
|
|
1878
|
+
return {
|
|
1879
|
+
...ast,
|
|
1880
|
+
ref,
|
|
1881
|
+
content: [tOut],
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
return tOut;
|
|
1885
|
+
}
|
|
1886
|
+
// -----------------------------------------------------------------------------
|
|
1887
|
+
// t-foreach and t-key
|
|
1888
|
+
// -----------------------------------------------------------------------------
|
|
1889
|
+
function parseTForEach(node, ctx) {
|
|
1890
|
+
if (!node.hasAttribute("t-foreach")) {
|
|
1891
|
+
return null;
|
|
1892
|
+
}
|
|
1893
|
+
const html = node.outerHTML;
|
|
1894
|
+
const collection = node.getAttribute("t-foreach");
|
|
1895
|
+
node.removeAttribute("t-foreach");
|
|
1896
|
+
const elem = node.getAttribute("t-as") || "";
|
|
1897
|
+
node.removeAttribute("t-as");
|
|
1898
|
+
const key = node.getAttribute("t-key");
|
|
1899
|
+
if (!key) {
|
|
1900
|
+
throw new OwlError(`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`);
|
|
1901
|
+
}
|
|
1902
|
+
node.removeAttribute("t-key");
|
|
1903
|
+
const memo = node.getAttribute("t-memo") || "";
|
|
1904
|
+
node.removeAttribute("t-memo");
|
|
1905
|
+
const body = parseNode(node, ctx);
|
|
1906
|
+
if (!body) {
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
const hasNoTCall = !html.includes("t-call");
|
|
1910
|
+
const hasNoFirst = hasNoTCall && !html.includes(`${elem}_first`);
|
|
1911
|
+
const hasNoLast = hasNoTCall && !html.includes(`${elem}_last`);
|
|
1912
|
+
const hasNoIndex = hasNoTCall && !html.includes(`${elem}_index`);
|
|
1913
|
+
const hasNoValue = hasNoTCall && !html.includes(`${elem}_value`);
|
|
1914
|
+
return {
|
|
1915
|
+
type: 9 /* TForEach */,
|
|
1916
|
+
collection,
|
|
1917
|
+
elem,
|
|
1918
|
+
body,
|
|
1919
|
+
memo,
|
|
1920
|
+
key,
|
|
1921
|
+
hasNoFirst,
|
|
1922
|
+
hasNoLast,
|
|
1923
|
+
hasNoIndex,
|
|
1924
|
+
hasNoValue,
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
function parseTKey(node, ctx) {
|
|
1928
|
+
if (!node.hasAttribute("t-key")) {
|
|
1929
|
+
return null;
|
|
1930
|
+
}
|
|
1931
|
+
const key = node.getAttribute("t-key");
|
|
1932
|
+
node.removeAttribute("t-key");
|
|
1933
|
+
const body = parseNode(node, ctx);
|
|
1934
|
+
if (!body) {
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
return { type: 10 /* TKey */, expr: key, content: body };
|
|
1938
|
+
}
|
|
1939
|
+
// -----------------------------------------------------------------------------
|
|
1940
|
+
// t-call
|
|
1941
|
+
// -----------------------------------------------------------------------------
|
|
1942
|
+
function parseTCall(node, ctx) {
|
|
1943
|
+
if (!node.hasAttribute("t-call")) {
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
const subTemplate = node.getAttribute("t-call");
|
|
1947
|
+
const context = node.getAttribute("t-call-context");
|
|
1948
|
+
node.removeAttribute("t-call");
|
|
1949
|
+
node.removeAttribute("t-call-context");
|
|
1950
|
+
if (node.tagName !== "t") {
|
|
1951
|
+
const ast = parseNode(node, ctx);
|
|
1952
|
+
const tcall = { type: 7 /* TCall */, name: subTemplate, body: null, context };
|
|
1953
|
+
if (ast && ast.type === 2 /* DomNode */) {
|
|
1954
|
+
ast.content = [tcall];
|
|
1955
|
+
return ast;
|
|
1956
|
+
}
|
|
1957
|
+
if (ast && ast.type === 11 /* TComponent */) {
|
|
1958
|
+
return {
|
|
1959
|
+
...ast,
|
|
1960
|
+
slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
const body = parseChildren(node, ctx);
|
|
1965
|
+
return {
|
|
1966
|
+
type: 7 /* TCall */,
|
|
1967
|
+
name: subTemplate,
|
|
1968
|
+
body: body.length ? body : null,
|
|
1969
|
+
context,
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
// -----------------------------------------------------------------------------
|
|
1973
|
+
// t-call-block
|
|
1974
|
+
// -----------------------------------------------------------------------------
|
|
1975
|
+
function parseTCallBlock(node, ctx) {
|
|
1976
|
+
if (!node.hasAttribute("t-call-block")) {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
const name = node.getAttribute("t-call-block");
|
|
1980
|
+
return {
|
|
1981
|
+
type: 15 /* TCallBlock */,
|
|
1982
|
+
name,
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
// -----------------------------------------------------------------------------
|
|
1986
|
+
// t-if
|
|
1987
|
+
// -----------------------------------------------------------------------------
|
|
1988
|
+
function parseTIf(node, ctx) {
|
|
1989
|
+
if (!node.hasAttribute("t-if")) {
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
const condition = node.getAttribute("t-if");
|
|
1993
|
+
node.removeAttribute("t-if");
|
|
1994
|
+
const content = parseNode(node, ctx) || { type: 0 /* Text */, value: "" };
|
|
1995
|
+
let nextElement = node.nextElementSibling;
|
|
1996
|
+
// t-elifs
|
|
1997
|
+
const tElifs = [];
|
|
1998
|
+
while (nextElement && nextElement.hasAttribute("t-elif")) {
|
|
1999
|
+
const condition = nextElement.getAttribute("t-elif");
|
|
2000
|
+
nextElement.removeAttribute("t-elif");
|
|
2001
|
+
const tElif = parseNode(nextElement, ctx);
|
|
2002
|
+
const next = nextElement.nextElementSibling;
|
|
2003
|
+
nextElement.remove();
|
|
2004
|
+
nextElement = next;
|
|
2005
|
+
if (tElif) {
|
|
2006
|
+
tElifs.push({ condition, content: tElif });
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
// t-else
|
|
2010
|
+
let tElse = null;
|
|
2011
|
+
if (nextElement && nextElement.hasAttribute("t-else")) {
|
|
2012
|
+
nextElement.removeAttribute("t-else");
|
|
2013
|
+
tElse = parseNode(nextElement, ctx);
|
|
2014
|
+
nextElement.remove();
|
|
2015
|
+
}
|
|
2016
|
+
return {
|
|
2017
|
+
type: 5 /* TIf */,
|
|
2018
|
+
condition,
|
|
2019
|
+
content,
|
|
2020
|
+
tElif: tElifs.length ? tElifs : null,
|
|
2021
|
+
tElse,
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
// -----------------------------------------------------------------------------
|
|
2025
|
+
// t-set directive
|
|
2026
|
+
// -----------------------------------------------------------------------------
|
|
2027
|
+
function parseTSetNode(node, ctx) {
|
|
2028
|
+
if (!node.hasAttribute("t-set")) {
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
const name = node.getAttribute("t-set");
|
|
2032
|
+
const value = node.getAttribute("t-value") || null;
|
|
2033
|
+
const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
|
|
2034
|
+
let body = null;
|
|
2035
|
+
if (node.textContent !== node.innerHTML) {
|
|
2036
|
+
body = parseChildren(node, ctx);
|
|
2037
|
+
}
|
|
2038
|
+
return { type: 6 /* TSet */, name, value, defaultValue, body };
|
|
2039
|
+
}
|
|
2040
|
+
// -----------------------------------------------------------------------------
|
|
2041
|
+
// Components
|
|
2042
|
+
// -----------------------------------------------------------------------------
|
|
2043
|
+
// Error messages when trying to use an unsupported directive on a component
|
|
2044
|
+
const directiveErrorMap = new Map([
|
|
2045
|
+
[
|
|
2046
|
+
"t-ref",
|
|
2047
|
+
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
|
|
2048
|
+
],
|
|
2049
|
+
["t-att", "t-att makes no sense on component: props are already treated as expressions"],
|
|
2050
|
+
[
|
|
2051
|
+
"t-attf",
|
|
2052
|
+
"t-attf is not supported on components: use template strings for string interpolation in props",
|
|
2053
|
+
],
|
|
2054
|
+
]);
|
|
2055
|
+
function parseComponent(node, ctx) {
|
|
2056
|
+
let name = node.tagName;
|
|
2057
|
+
const firstLetter = name[0];
|
|
2058
|
+
let isDynamic = node.hasAttribute("t-component");
|
|
2059
|
+
if (isDynamic && name !== "t") {
|
|
2060
|
+
throw new OwlError(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
|
|
2061
|
+
}
|
|
2062
|
+
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
if (isDynamic) {
|
|
2066
|
+
name = node.getAttribute("t-component");
|
|
2067
|
+
node.removeAttribute("t-component");
|
|
2068
|
+
}
|
|
2069
|
+
const dynamicProps = node.getAttribute("t-props");
|
|
2070
|
+
node.removeAttribute("t-props");
|
|
2071
|
+
const defaultSlotScope = node.getAttribute("t-slot-scope");
|
|
2072
|
+
node.removeAttribute("t-slot-scope");
|
|
2073
|
+
let on = null;
|
|
2074
|
+
let props = null;
|
|
2075
|
+
for (let name of node.getAttributeNames()) {
|
|
2076
|
+
const value = node.getAttribute(name);
|
|
2077
|
+
if (name.startsWith("t-")) {
|
|
2078
|
+
if (name.startsWith("t-on-")) {
|
|
2079
|
+
on = on || {};
|
|
2080
|
+
on[name.slice(5)] = value;
|
|
2081
|
+
}
|
|
2082
|
+
else {
|
|
2083
|
+
const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
|
|
2084
|
+
throw new OwlError(message || `unsupported directive on Component: ${name}`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
else {
|
|
2088
|
+
props = props || {};
|
|
2089
|
+
props[name] = value;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
let slots = null;
|
|
2093
|
+
if (node.hasChildNodes()) {
|
|
2094
|
+
const clone = node.cloneNode(true);
|
|
2095
|
+
// named slots
|
|
2096
|
+
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
|
|
2097
|
+
for (let slotNode of slotNodes) {
|
|
2098
|
+
if (slotNode.tagName !== "t") {
|
|
2099
|
+
throw new OwlError(`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`);
|
|
2100
|
+
}
|
|
2101
|
+
const name = slotNode.getAttribute("t-set-slot");
|
|
2102
|
+
// check if this is defined in a sub component (in which case it should
|
|
2103
|
+
// be ignored)
|
|
2104
|
+
let el = slotNode.parentElement;
|
|
2105
|
+
let isInSubComponent = false;
|
|
2106
|
+
while (el && el !== clone) {
|
|
2107
|
+
if (el.hasAttribute("t-component") || el.tagName[0] === el.tagName[0].toUpperCase()) {
|
|
2108
|
+
isInSubComponent = true;
|
|
2109
|
+
break;
|
|
2110
|
+
}
|
|
2111
|
+
el = el.parentElement;
|
|
2112
|
+
}
|
|
2113
|
+
if (isInSubComponent || !el) {
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
slotNode.removeAttribute("t-set-slot");
|
|
2117
|
+
slotNode.remove();
|
|
2118
|
+
const slotAst = parseNode(slotNode, ctx);
|
|
2119
|
+
let on = null;
|
|
2120
|
+
let attrs = null;
|
|
2121
|
+
let scope = null;
|
|
2122
|
+
for (let attributeName of slotNode.getAttributeNames()) {
|
|
2123
|
+
const value = slotNode.getAttribute(attributeName);
|
|
2124
|
+
if (attributeName === "t-slot-scope") {
|
|
2125
|
+
scope = value;
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
else if (attributeName.startsWith("t-on-")) {
|
|
2129
|
+
on = on || {};
|
|
2130
|
+
on[attributeName.slice(5)] = value;
|
|
2131
|
+
}
|
|
2132
|
+
else {
|
|
2133
|
+
attrs = attrs || {};
|
|
2134
|
+
attrs[attributeName] = value;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
slots = slots || {};
|
|
2138
|
+
slots[name] = { content: slotAst, on, attrs, scope };
|
|
2139
|
+
}
|
|
2140
|
+
// default slot
|
|
2141
|
+
const defaultContent = parseChildNodes(clone, ctx);
|
|
2142
|
+
slots = slots || {};
|
|
2143
|
+
// t-set-slot="default" has priority over content
|
|
2144
|
+
if (defaultContent && !slots.default) {
|
|
2145
|
+
slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };
|
|
2149
|
+
}
|
|
2150
|
+
// -----------------------------------------------------------------------------
|
|
2151
|
+
// Slots
|
|
2152
|
+
// -----------------------------------------------------------------------------
|
|
2153
|
+
function parseTSlot(node, ctx) {
|
|
2154
|
+
if (!node.hasAttribute("t-slot")) {
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
const name = node.getAttribute("t-slot");
|
|
2158
|
+
node.removeAttribute("t-slot");
|
|
2159
|
+
let attrs = null;
|
|
2160
|
+
let on = null;
|
|
2161
|
+
for (let attributeName of node.getAttributeNames()) {
|
|
2162
|
+
const value = node.getAttribute(attributeName);
|
|
2163
|
+
if (attributeName.startsWith("t-on-")) {
|
|
2164
|
+
on = on || {};
|
|
2165
|
+
on[attributeName.slice(5)] = value;
|
|
2166
|
+
}
|
|
2167
|
+
else {
|
|
2168
|
+
attrs = attrs || {};
|
|
2169
|
+
attrs[attributeName] = value;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return {
|
|
2173
|
+
type: 14 /* TSlot */,
|
|
2174
|
+
name,
|
|
2175
|
+
attrs,
|
|
2176
|
+
on,
|
|
2177
|
+
defaultContent: parseChildNodes(node, ctx),
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
function parseTTranslation(node, ctx) {
|
|
2181
|
+
if (node.getAttribute("t-translation") !== "off") {
|
|
2182
|
+
return null;
|
|
2183
|
+
}
|
|
2184
|
+
node.removeAttribute("t-translation");
|
|
2185
|
+
return {
|
|
2186
|
+
type: 16 /* TTranslation */,
|
|
2187
|
+
content: parseNode(node, ctx),
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
// -----------------------------------------------------------------------------
|
|
2191
|
+
// Portal
|
|
2192
|
+
// -----------------------------------------------------------------------------
|
|
2193
|
+
function parseTPortal(node, ctx) {
|
|
2194
|
+
if (!node.hasAttribute("t-portal")) {
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
const target = node.getAttribute("t-portal");
|
|
2198
|
+
node.removeAttribute("t-portal");
|
|
2199
|
+
const content = parseNode(node, ctx);
|
|
2200
|
+
if (!content) {
|
|
2201
|
+
return {
|
|
2202
|
+
type: 0 /* Text */,
|
|
2203
|
+
value: "",
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
return {
|
|
2207
|
+
type: 17 /* TPortal */,
|
|
2208
|
+
target,
|
|
2209
|
+
content,
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
// -----------------------------------------------------------------------------
|
|
2213
|
+
// helpers
|
|
2214
|
+
// -----------------------------------------------------------------------------
|
|
2215
|
+
/**
|
|
2216
|
+
* Parse all the child nodes of a given node and return a list of ast elements
|
|
2217
|
+
*/
|
|
2218
|
+
function parseChildren(node, ctx) {
|
|
2219
|
+
const children = [];
|
|
2220
|
+
for (let child of node.childNodes) {
|
|
2221
|
+
const childAst = parseNode(child, ctx);
|
|
2222
|
+
if (childAst) {
|
|
2223
|
+
if (childAst.type === 3 /* Multi */) {
|
|
2224
|
+
children.push(...childAst.content);
|
|
2225
|
+
}
|
|
2226
|
+
else {
|
|
2227
|
+
children.push(childAst);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
return children;
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Parse all the child nodes of a given node and return an ast if possible.
|
|
2235
|
+
* In the case there are multiple children, they are wrapped in a astmulti.
|
|
2236
|
+
*/
|
|
2237
|
+
function parseChildNodes(node, ctx) {
|
|
2238
|
+
const children = parseChildren(node, ctx);
|
|
2239
|
+
switch (children.length) {
|
|
2240
|
+
case 0:
|
|
2241
|
+
return null;
|
|
2242
|
+
case 1:
|
|
2243
|
+
return children[0];
|
|
2244
|
+
default:
|
|
2245
|
+
return { type: 3 /* Multi */, content: children };
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Normalizes the content of an Element so that t-if/t-elif/t-else directives
|
|
2250
|
+
* immediately follow one another (by removing empty text nodes or comments).
|
|
2251
|
+
* Throws an error when a conditional branching statement is malformed. This
|
|
2252
|
+
* function modifies the Element in place.
|
|
2253
|
+
*
|
|
2254
|
+
* @param el the element containing the tree that should be normalized
|
|
2255
|
+
*/
|
|
2256
|
+
function normalizeTIf(el) {
|
|
2257
|
+
let tbranch = el.querySelectorAll("[t-elif], [t-else]");
|
|
2258
|
+
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
|
|
2259
|
+
let node = tbranch[i];
|
|
2260
|
+
let prevElem = node.previousElementSibling;
|
|
2261
|
+
let pattr = (name) => prevElem.getAttribute(name);
|
|
2262
|
+
let nattr = (name) => +!!node.getAttribute(name);
|
|
2263
|
+
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
|
|
2264
|
+
if (pattr("t-foreach")) {
|
|
2265
|
+
throw new OwlError("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
|
|
2266
|
+
}
|
|
2267
|
+
if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
|
|
2268
|
+
return a + b;
|
|
2269
|
+
}) > 1) {
|
|
2270
|
+
throw new OwlError("Only one conditional branching directive is allowed per node");
|
|
2271
|
+
}
|
|
2272
|
+
// All text (with only spaces) and comment nodes (nodeType 8) between
|
|
2273
|
+
// branch nodes are removed
|
|
2274
|
+
let textNode;
|
|
2275
|
+
while ((textNode = node.previousSibling) !== prevElem) {
|
|
2276
|
+
if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
|
|
2277
|
+
throw new OwlError("text is not allowed between branching directives");
|
|
2278
|
+
}
|
|
2279
|
+
textNode.remove();
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
else {
|
|
2283
|
+
throw new OwlError("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Normalizes the content of an Element so that t-esc directives on components
|
|
2289
|
+
* are removed and instead places a <t t-esc=""> as the default slot of the
|
|
2290
|
+
* component. Also throws if the component already has content. This function
|
|
2291
|
+
* modifies the Element in place.
|
|
2292
|
+
*
|
|
2293
|
+
* @param el the element containing the tree that should be normalized
|
|
2294
|
+
*/
|
|
2295
|
+
function normalizeTEscTOut(el) {
|
|
2296
|
+
for (const d of ["t-esc", "t-out"]) {
|
|
2297
|
+
const elements = [...el.querySelectorAll(`[${d}]`)].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
|
|
2298
|
+
for (const el of elements) {
|
|
2299
|
+
if (el.childNodes.length) {
|
|
2300
|
+
throw new OwlError(`Cannot have ${d} on a component that already has content`);
|
|
2301
|
+
}
|
|
2302
|
+
const value = el.getAttribute(d);
|
|
2303
|
+
el.removeAttribute(d);
|
|
2304
|
+
const t = el.ownerDocument.createElement("t");
|
|
2305
|
+
if (value != null) {
|
|
2306
|
+
t.setAttribute(d, value);
|
|
2307
|
+
}
|
|
2308
|
+
el.appendChild(t);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Normalizes the tree inside a given element and do some preliminary validation
|
|
2314
|
+
* on it. This function modifies the Element in place.
|
|
2315
|
+
*
|
|
2316
|
+
* @param el the element containing the tree that should be normalized
|
|
2317
|
+
*/
|
|
2318
|
+
function normalizeXML(el) {
|
|
2319
|
+
normalizeTIf(el);
|
|
2320
|
+
normalizeTEscTOut(el);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
function compile(template, options = {
|
|
2324
|
+
hasGlobalValues: false,
|
|
2325
|
+
}) {
|
|
2326
|
+
// parsing
|
|
2327
|
+
const ast = parse(template, options.customDirectives);
|
|
2328
|
+
// some work
|
|
2329
|
+
const hasSafeContext = template instanceof Node
|
|
2330
|
+
? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
|
|
2331
|
+
: !template.includes("t-set") && !template.includes("t-call");
|
|
2332
|
+
// code generation
|
|
2333
|
+
const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
|
|
2334
|
+
const code = codeGenerator.generateCode();
|
|
2335
|
+
// template function
|
|
2336
|
+
try {
|
|
2337
|
+
return new Function("app, bdom, helpers", code);
|
|
2338
|
+
}
|
|
2339
|
+
catch (originalError) {
|
|
2340
|
+
const { name } = options;
|
|
2341
|
+
const nameStr = name ? `template "${name}"` : "anonymous template";
|
|
2342
|
+
const err = new OwlError(`Failed to compile ${nameStr}: ${originalError.message}\n\ngenerated code:\nfunction(app, bdom, helpers) {\n${code}\n}`);
|
|
2343
|
+
err.cause = originalError;
|
|
2344
|
+
throw err;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// -----------------------------------------------------------------------------
|
|
2349
|
+
// -----------------------------------------------------------------------------
|
|
2350
|
+
// helpers
|
|
2351
|
+
// -----------------------------------------------------------------------------
|
|
2352
|
+
async function getXmlFiles(paths) {
|
|
2353
|
+
return (await Promise.all(paths.map(async (file) => {
|
|
2354
|
+
const stats = await stat(path.join(file));
|
|
2355
|
+
if (stats.isDirectory()) {
|
|
2356
|
+
return await getXmlFiles((await readdir(file)).map((fileName) => path.join(file, fileName)));
|
|
2357
|
+
}
|
|
2358
|
+
if (file.endsWith(".xml")) {
|
|
2359
|
+
return file;
|
|
2360
|
+
}
|
|
2361
|
+
return [];
|
|
2362
|
+
}))).flat();
|
|
2363
|
+
}
|
|
2364
|
+
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
|
2365
|
+
const a = "·-_,:;";
|
|
2366
|
+
const p = new RegExp(a.split("").join("|"), "g");
|
|
2367
|
+
function slugify(str) {
|
|
2368
|
+
return str
|
|
2369
|
+
.replace(/\//g, "") // remove /
|
|
2370
|
+
.replace(/\./g, "_") // Replace . with _
|
|
2371
|
+
.replace(p, (c) => "_") // Replace special characters
|
|
2372
|
+
.replace(/&/g, "_and_") // Replace & with ‘and’
|
|
2373
|
+
.replace(/[^\w\-]+/g, ""); // Remove all non-word characters
|
|
2374
|
+
}
|
|
2375
|
+
// -----------------------------------------------------------------------------
|
|
2376
|
+
// main
|
|
2377
|
+
// -----------------------------------------------------------------------------
|
|
2378
|
+
async function compileTemplates(paths) {
|
|
2379
|
+
const files = await getXmlFiles(paths);
|
|
2380
|
+
process.stdout.write(`Processing ${files.length} files`);
|
|
2381
|
+
let xmlStrings = await Promise.all(files.map((file) => readFile(file, "utf8")));
|
|
2382
|
+
const templates = [];
|
|
2383
|
+
const errors = [];
|
|
2384
|
+
for (let i = 0; i < files.length; i++) {
|
|
2385
|
+
const fileName = files[i];
|
|
2386
|
+
const fileContent = xmlStrings[i];
|
|
2387
|
+
process.stdout.write(`.`);
|
|
2388
|
+
const parser = new DOMParser();
|
|
2389
|
+
const doc = parser.parseFromString(fileContent, "text/xml");
|
|
2390
|
+
for (const template of doc.querySelectorAll("[t-name]")) {
|
|
2391
|
+
const name = template.getAttribute("t-name");
|
|
2392
|
+
if (template.hasAttribute("owl")) {
|
|
2393
|
+
template.removeAttribute("owl");
|
|
2394
|
+
}
|
|
2395
|
+
const fnName = slugify(name);
|
|
2396
|
+
try {
|
|
2397
|
+
const fn = compile(template).toString().replace("anonymous", fnName);
|
|
2398
|
+
templates.push(`"${name}": ${fn},\n`);
|
|
2399
|
+
}
|
|
2400
|
+
catch (e) {
|
|
2401
|
+
errors.push({ name, fileName, e });
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
process.stdout.write(`\n`);
|
|
2406
|
+
for (let { name, fileName, e } of errors) {
|
|
2407
|
+
console.warn(`Error while compiling '${name}' (in file ${fileName})`);
|
|
2408
|
+
console.error(e);
|
|
2409
|
+
}
|
|
2410
|
+
console.log(`${templates.length} templates compiled`);
|
|
2411
|
+
return `export const templates = {\n ${templates.join("\n")} \n}`;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
export { compileTemplates };
|