@odoo/owl 2.0.0-beta-5 → 2.0.0-beta-8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +1 -0
  2. package/dist/owl.cjs.js +1984 -1893
  3. package/dist/owl.es.js +1984 -1894
  4. package/dist/owl.iife.js +1984 -1893
  5. package/dist/owl.iife.min.js +1 -1
  6. package/dist/types/compiler/code_generator.d.ts +0 -8
  7. package/dist/types/compiler/index.d.ts +3 -2
  8. package/dist/types/index.d.ts +1 -27
  9. package/dist/types/{app → runtime}/app.d.ts +7 -5
  10. package/dist/types/{blockdom → runtime/blockdom}/attributes.d.ts +0 -0
  11. package/dist/types/{blockdom → runtime/blockdom}/block_compiler.d.ts +0 -0
  12. package/dist/types/{blockdom → runtime/blockdom}/config.d.ts +0 -0
  13. package/dist/types/{blockdom → runtime/blockdom}/event_catcher.d.ts +0 -0
  14. package/dist/types/{blockdom → runtime/blockdom}/events.d.ts +0 -0
  15. package/dist/types/{blockdom → runtime/blockdom}/html.d.ts +0 -0
  16. package/dist/types/{blockdom → runtime/blockdom}/index.d.ts +0 -0
  17. package/dist/types/{blockdom → runtime/blockdom}/list.d.ts +0 -0
  18. package/dist/types/{blockdom → runtime/blockdom}/multi.d.ts +0 -0
  19. package/dist/types/{blockdom → runtime/blockdom}/text.d.ts +0 -0
  20. package/dist/types/{blockdom → runtime/blockdom}/toggler.d.ts +0 -0
  21. package/dist/types/{component → runtime}/component.d.ts +5 -1
  22. package/dist/types/{component → runtime}/component_node.d.ts +10 -7
  23. package/dist/types/{component → runtime}/error_handling.d.ts +0 -0
  24. package/dist/types/{component/handler.d.ts → runtime/event_handling.d.ts} +0 -0
  25. package/dist/types/{component → runtime}/fibers.d.ts +1 -1
  26. package/dist/types/{hooks.d.ts → runtime/hooks.d.ts} +1 -1
  27. package/dist/types/runtime/index.d.ts +29 -0
  28. package/dist/types/{component → runtime}/lifecycle_hooks.d.ts +0 -0
  29. package/dist/types/{portal.d.ts → runtime/portal.d.ts} +5 -1
  30. package/dist/types/{reactivity.d.ts → runtime/reactivity.d.ts} +0 -0
  31. package/dist/types/{component → runtime}/scheduler.d.ts +0 -0
  32. package/dist/types/{component → runtime}/status.d.ts +0 -0
  33. package/dist/types/{app → runtime}/template_helpers.d.ts +11 -2
  34. package/dist/types/runtime/template_set.d.ts +32 -0
  35. package/dist/types/{utils.d.ts → runtime/utils.d.ts} +0 -7
  36. package/dist/types/runtime/validation.d.ts +30 -0
  37. package/package.json +5 -2
  38. package/dist/owl.cjs.min.js +0 -1
  39. package/dist/owl.es.min.js +0 -1
  40. package/dist/types/app/template_set.d.ts +0 -27
  41. package/dist/types/component/props_validation.d.ts +0 -14
package/dist/owl.cjs.js CHANGED
@@ -1372,51 +1372,6 @@ function remove(vnode, withBeforeRemove = false) {
1372
1372
  vnode.remove();
1373
1373
  }
1374
1374
 
1375
- const mainEventHandler = (data, ev, currentTarget) => {
1376
- const { data: _data, modifiers } = filterOutModifiersFromData(data);
1377
- data = _data;
1378
- let stopped = false;
1379
- if (modifiers.length) {
1380
- let selfMode = false;
1381
- const isSelf = ev.target === currentTarget;
1382
- for (const mod of modifiers) {
1383
- switch (mod) {
1384
- case "self":
1385
- selfMode = true;
1386
- if (isSelf) {
1387
- continue;
1388
- }
1389
- else {
1390
- return stopped;
1391
- }
1392
- case "prevent":
1393
- if ((selfMode && isSelf) || !selfMode)
1394
- ev.preventDefault();
1395
- continue;
1396
- case "stop":
1397
- if ((selfMode && isSelf) || !selfMode)
1398
- ev.stopPropagation();
1399
- stopped = true;
1400
- continue;
1401
- }
1402
- }
1403
- }
1404
- // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
1405
- // We check this rather than data[0] being truthy (or typeof function) so that it crashes
1406
- // as expected when there is a handler expression that evaluates to a falsy value
1407
- if (Object.hasOwnProperty.call(data, 0)) {
1408
- const handler = data[0];
1409
- if (typeof handler !== "function") {
1410
- throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
1411
- }
1412
- let node = data[1] ? data[1].__owl__ : null;
1413
- if (node ? node.status === 1 /* MOUNTED */ : true) {
1414
- handler.call(node ? node.component : null, ev);
1415
- }
1416
- }
1417
- return stopped;
1418
- };
1419
-
1420
1375
  // Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
1421
1376
  const TARGET = Symbol("Target");
1422
1377
  // Escape hatch to prevent reactivity system to turn something into a reactive
@@ -1600,7 +1555,7 @@ function reactive(target, callback = () => { }) {
1600
1555
  return reactive(originalTarget, callback);
1601
1556
  }
1602
1557
  if (!reactiveCache.has(target)) {
1603
- reactiveCache.set(target, new Map());
1558
+ reactiveCache.set(target, new WeakMap());
1604
1559
  }
1605
1560
  const reactivesForTarget = reactiveCache.get(target);
1606
1561
  if (!reactivesForTarget.has(callback)) {
@@ -1900,18 +1855,20 @@ class Markup extends String {
1900
1855
  */
1901
1856
  function markup(value) {
1902
1857
  return new Markup(value);
1858
+ }
1859
+
1860
+ class Component {
1861
+ constructor(props, env, node) {
1862
+ this.props = props;
1863
+ this.env = env;
1864
+ this.__owl__ = node;
1865
+ }
1866
+ setup() { }
1867
+ render(deep = false) {
1868
+ this.__owl__.render(deep === true);
1869
+ }
1903
1870
  }
1904
- // -----------------------------------------------------------------------------
1905
- // xml tag helper
1906
- // -----------------------------------------------------------------------------
1907
- const globalTemplates = {};
1908
- function xml(...args) {
1909
- const name = `__template__${xml.nextId++}`;
1910
- const value = String.raw(...args);
1911
- globalTemplates[name] = value;
1912
- return name;
1913
- }
1914
- xml.nextId = 1;
1871
+ Component.template = "";
1915
1872
 
1916
1873
  // Maps fibers to thrown errors
1917
1874
  const fibersInError = new WeakMap();
@@ -2005,6 +1962,9 @@ function makeRootFiber(node) {
2005
1962
  }
2006
1963
  return fiber;
2007
1964
  }
1965
+ function throwOnRender() {
1966
+ throw new Error("Attempted to render cancelled fiber");
1967
+ }
2008
1968
  /**
2009
1969
  * @returns number of not-yet rendered fibers cancelled
2010
1970
  */
@@ -2012,6 +1972,7 @@ function cancelFibers(fibers) {
2012
1972
  let result = 0;
2013
1973
  for (let fiber of fibers) {
2014
1974
  let node = fiber.node;
1975
+ fiber.render = throwOnRender;
2015
1976
  if (node.status === 0 /* NEW */) {
2016
1977
  node.destroy();
2017
1978
  }
@@ -2205,145 +2166,6 @@ class MountFiber extends RootFiber {
2205
2166
  }
2206
2167
  }
2207
2168
 
2208
- /**
2209
- * Apply default props (only top level).
2210
- *
2211
- * Note that this method does modify in place the props
2212
- */
2213
- function applyDefaultProps(props, ComponentClass) {
2214
- const defaultProps = ComponentClass.defaultProps;
2215
- if (defaultProps) {
2216
- for (let propName in defaultProps) {
2217
- if (props[propName] === undefined) {
2218
- props[propName] = defaultProps[propName];
2219
- }
2220
- }
2221
- }
2222
- }
2223
- //------------------------------------------------------------------------------
2224
- // Prop validation helper
2225
- //------------------------------------------------------------------------------
2226
- function getPropDescription(staticProps) {
2227
- if (staticProps instanceof Array) {
2228
- return Object.fromEntries(staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true])));
2229
- }
2230
- return staticProps || { "*": true };
2231
- }
2232
- /**
2233
- * Validate the component props (or next props) against the (static) props
2234
- * description. This is potentially an expensive operation: it may needs to
2235
- * visit recursively the props and all the children to check if they are valid.
2236
- * This is why it is only done in 'dev' mode.
2237
- */
2238
- function validateProps(name, props, parent) {
2239
- const ComponentClass = typeof name !== "string"
2240
- ? name
2241
- : parent.constructor.components[name];
2242
- if (!ComponentClass) {
2243
- // this is an error, wrong component. We silently return here instead so the
2244
- // error is triggered by the usual path ('component' function)
2245
- return;
2246
- }
2247
- applyDefaultProps(props, ComponentClass);
2248
- const defaultProps = ComponentClass.defaultProps || {};
2249
- let propsDef = getPropDescription(ComponentClass.props);
2250
- const allowAdditionalProps = "*" in propsDef;
2251
- for (let propName in propsDef) {
2252
- if (propName === "*") {
2253
- continue;
2254
- }
2255
- const propDef = propsDef[propName];
2256
- let isMandatory = !!propDef;
2257
- if (typeof propDef === "object" && "optional" in propDef) {
2258
- isMandatory = !propDef.optional;
2259
- }
2260
- if (isMandatory && propName in defaultProps) {
2261
- throw new Error(`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`);
2262
- }
2263
- if (props[propName] === undefined) {
2264
- if (isMandatory) {
2265
- throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
2266
- }
2267
- else {
2268
- continue;
2269
- }
2270
- }
2271
- let isValid;
2272
- try {
2273
- isValid = isValidProp(props[propName], propDef);
2274
- }
2275
- catch (e) {
2276
- e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
2277
- throw e;
2278
- }
2279
- if (!isValid) {
2280
- throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
2281
- }
2282
- }
2283
- if (!allowAdditionalProps) {
2284
- for (let propName in props) {
2285
- if (!(propName in propsDef)) {
2286
- throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
2287
- }
2288
- }
2289
- }
2290
- }
2291
- /**
2292
- * Check if an invidual prop value matches its (static) prop definition
2293
- */
2294
- function isValidProp(prop, propDef) {
2295
- if (propDef === true) {
2296
- return true;
2297
- }
2298
- if (typeof propDef === "function") {
2299
- // Check if a value is constructed by some Constructor. Note that there is a
2300
- // slight abuse of language: we want to consider primitive values as well.
2301
- //
2302
- // So, even though 1 is not an instance of Number, we want to consider that
2303
- // it is valid.
2304
- if (typeof prop === "object") {
2305
- return prop instanceof propDef;
2306
- }
2307
- return typeof prop === propDef.name.toLowerCase();
2308
- }
2309
- else if (propDef instanceof Array) {
2310
- // If this code is executed, this means that we want to check if a prop
2311
- // matches at least one of its descriptor.
2312
- let result = false;
2313
- for (let i = 0, iLen = propDef.length; i < iLen; i++) {
2314
- result = result || isValidProp(prop, propDef[i]);
2315
- }
2316
- return result;
2317
- }
2318
- // propsDef is an object
2319
- if (propDef.optional && prop === undefined) {
2320
- return true;
2321
- }
2322
- let result = propDef.type ? isValidProp(prop, propDef.type) : true;
2323
- if (propDef.validate) {
2324
- result = result && propDef.validate(prop);
2325
- }
2326
- if (propDef.type === Array && propDef.element) {
2327
- for (let i = 0, iLen = prop.length; i < iLen; i++) {
2328
- result = result && isValidProp(prop[i], propDef.element);
2329
- }
2330
- }
2331
- if (propDef.type === Object && propDef.shape) {
2332
- const shape = propDef.shape;
2333
- for (let key in shape) {
2334
- result = result && isValidProp(prop[key], shape[key]);
2335
- }
2336
- if (result) {
2337
- for (let propName in prop) {
2338
- if (!(propName in shape)) {
2339
- throw new Error(`unknown prop '${propName}'`);
2340
- }
2341
- }
2342
- }
2343
- }
2344
- return result;
2345
- }
2346
-
2347
2169
  let currentNode = null;
2348
2170
  function getCurrent() {
2349
2171
  if (!currentNode) {
@@ -2354,6 +2176,18 @@ function getCurrent() {
2354
2176
  function useComponent() {
2355
2177
  return currentNode.component;
2356
2178
  }
2179
+ /**
2180
+ * Apply default props (only top level).
2181
+ *
2182
+ * Note that this method does modify in place the props
2183
+ */
2184
+ function applyDefaultProps(props, defaultProps) {
2185
+ for (let propName in defaultProps) {
2186
+ if (props[propName] === undefined) {
2187
+ props[propName] = defaultProps[propName];
2188
+ }
2189
+ }
2190
+ }
2357
2191
  // -----------------------------------------------------------------------------
2358
2192
  // Integration with reactivity system (useState)
2359
2193
  // -----------------------------------------------------------------------------
@@ -2372,7 +2206,7 @@ function useState(state) {
2372
2206
  const node = getCurrent();
2373
2207
  let render = batchedRenderFunctions.get(node);
2374
2208
  if (!render) {
2375
- render = batched(node.render.bind(node));
2209
+ render = batched(node.render.bind(node, false));
2376
2210
  batchedRenderFunctions.set(node, render);
2377
2211
  // manual implementation of onWillDestroy to break cyclic dependency
2378
2212
  node.willDestroy.push(clearReactivesForCallback.bind(null, render));
@@ -2381,7 +2215,9 @@ function useState(state) {
2381
2215
  }
2382
2216
  function arePropsDifferent(props1, props2) {
2383
2217
  for (let k in props1) {
2384
- if (props1[k] !== props2[k]) {
2218
+ const prop1 = props1[k] && typeof props1[k] === "object" ? toRaw(props1[k]) : props1[k];
2219
+ const prop2 = props2[k] && typeof props2[k] === "object" ? toRaw(props2[k]) : props2[k];
2220
+ if (prop1 !== prop2) {
2385
2221
  return true;
2386
2222
  }
2387
2223
  }
@@ -2403,7 +2239,7 @@ function component(name, props, key, ctx, parent) {
2403
2239
  node.forceNextRender = false;
2404
2240
  }
2405
2241
  else {
2406
- const currentProps = node.component.props[TARGET];
2242
+ const currentProps = node.component.props;
2407
2243
  shouldRender = parentFiber.deep || arePropsDifferent(currentProps, props);
2408
2244
  }
2409
2245
  if (shouldRender) {
@@ -2421,6 +2257,9 @@ function component(name, props, key, ctx, parent) {
2421
2257
  if (!C) {
2422
2258
  throw new Error(`Cannot find the definition of component "${name}"`);
2423
2259
  }
2260
+ else if (!(C.prototype instanceof Component)) {
2261
+ throw new Error(`"${name}" is not a Component. It must inherit from the Component class`);
2262
+ }
2424
2263
  }
2425
2264
  node = new ComponentNode(C, props, ctx.app, ctx, key);
2426
2265
  ctx.children[key] = node;
@@ -2449,10 +2288,18 @@ class ComponentNode {
2449
2288
  this.parent = parent;
2450
2289
  this.parentKey = parentKey;
2451
2290
  this.level = parent ? parent.level + 1 : 0;
2452
- applyDefaultProps(props, C);
2291
+ const defaultProps = C.defaultProps;
2292
+ if (defaultProps) {
2293
+ applyDefaultProps(props, defaultProps);
2294
+ }
2453
2295
  const env = (parent && parent.childEnv) || app.env;
2454
2296
  this.childEnv = env;
2455
- props = useState(props);
2297
+ for (const key in props) {
2298
+ const prop = props[key];
2299
+ if (prop && typeof prop === "object" && prop[TARGET]) {
2300
+ props[key] = useState(prop);
2301
+ }
2302
+ }
2456
2303
  this.component = new C(props, env, this);
2457
2304
  this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
2458
2305
  this.component.setup();
@@ -2480,7 +2327,7 @@ class ComponentNode {
2480
2327
  fiber.render();
2481
2328
  }
2482
2329
  }
2483
- async render(deep = false) {
2330
+ async render(deep) {
2484
2331
  let current = this.fiber;
2485
2332
  if (current && (current.root.locked || current.bdom === true)) {
2486
2333
  await Promise.resolve();
@@ -2558,9 +2405,17 @@ class ComponentNode {
2558
2405
  const fiber = makeChildFiber(this, parentFiber);
2559
2406
  this.fiber = fiber;
2560
2407
  const component = this.component;
2561
- applyDefaultProps(props, component.constructor);
2408
+ const defaultProps = component.constructor.defaultProps;
2409
+ if (defaultProps) {
2410
+ applyDefaultProps(props, defaultProps);
2411
+ }
2562
2412
  currentNode = this;
2563
- props = useState(props);
2413
+ for (const key in props) {
2414
+ const prop = props[key];
2415
+ if (prop && typeof prop === "object" && prop[TARGET]) {
2416
+ props[key] = useState(prop);
2417
+ }
2418
+ }
2564
2419
  currentNode = null;
2565
2420
  const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
2566
2421
  await prom;
@@ -2654,663 +2509,1234 @@ class ComponentNode {
2654
2509
  }
2655
2510
  }
2656
2511
 
2657
- // -----------------------------------------------------------------------------
2658
- // Scheduler
2659
- // -----------------------------------------------------------------------------
2660
- class Scheduler {
2661
- constructor() {
2662
- this.tasks = new Set();
2663
- this.frame = 0;
2664
- this.delayedRenders = [];
2665
- this.requestAnimationFrame = Scheduler.requestAnimationFrame;
2666
- }
2667
- addFiber(fiber) {
2668
- this.tasks.add(fiber.root);
2669
- }
2670
- /**
2671
- * Process all current tasks. This only applies to the fibers that are ready.
2672
- * Other tasks are left unchanged.
2673
- */
2674
- flush() {
2675
- if (this.delayedRenders.length) {
2676
- let renders = this.delayedRenders;
2677
- this.delayedRenders = [];
2678
- for (let f of renders) {
2679
- if (f.root && f.node.status !== 2 /* DESTROYED */) {
2680
- f.render();
2512
+ const TIMEOUT = Symbol("timeout");
2513
+ function wrapError(fn, hookName) {
2514
+ const error = new Error(`The following error occurred in ${hookName}: `);
2515
+ const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`);
2516
+ const node = getCurrent();
2517
+ return (...args) => {
2518
+ try {
2519
+ const result = fn(...args);
2520
+ if (result instanceof Promise) {
2521
+ if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
2522
+ const fiber = node.fiber;
2523
+ Promise.race([
2524
+ result,
2525
+ new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
2526
+ ]).then((res) => {
2527
+ if (res === TIMEOUT && node.fiber === fiber) {
2528
+ console.warn(timeoutError);
2529
+ }
2530
+ });
2681
2531
  }
2682
- }
2683
- }
2684
- if (this.frame === 0) {
2685
- this.frame = this.requestAnimationFrame(() => {
2686
- this.frame = 0;
2687
- this.tasks.forEach((fiber) => this.processFiber(fiber));
2688
- for (let task of this.tasks) {
2689
- if (task.node.status === 2 /* DESTROYED */) {
2690
- this.tasks.delete(task);
2532
+ return result.catch((cause) => {
2533
+ error.cause = cause;
2534
+ if (cause instanceof Error) {
2535
+ error.message += `"${cause.message}"`;
2691
2536
  }
2692
- }
2693
- });
2694
- }
2695
- }
2696
- processFiber(fiber) {
2697
- if (fiber.root !== fiber) {
2698
- this.tasks.delete(fiber);
2699
- return;
2700
- }
2701
- const hasError = fibersInError.has(fiber);
2702
- if (hasError && fiber.counter !== 0) {
2703
- this.tasks.delete(fiber);
2704
- return;
2705
- }
2706
- if (fiber.node.status === 2 /* DESTROYED */) {
2707
- this.tasks.delete(fiber);
2708
- return;
2537
+ throw error;
2538
+ });
2539
+ }
2540
+ return result;
2709
2541
  }
2710
- if (fiber.counter === 0) {
2711
- if (!hasError) {
2712
- fiber.complete();
2542
+ catch (cause) {
2543
+ if (cause instanceof Error) {
2544
+ error.message += `"${cause.message}"`;
2713
2545
  }
2714
- this.tasks.delete(fiber);
2546
+ throw error;
2715
2547
  }
2716
- }
2548
+ };
2717
2549
  }
2718
- // capture the value of requestAnimationFrame as soon as possible, to avoid
2719
- // interactions with other code, such as test frameworks that override them
2720
- Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
2721
-
2722
- /**
2723
- * Owl QWeb Expression Parser
2724
- *
2725
- * Owl needs in various contexts to be able to understand the structure of a
2726
- * string representing a javascript expression. The usual goal is to be able
2727
- * to rewrite some variables. For example, if a template has
2728
- *
2729
- * ```xml
2730
- * <t t-if="computeSomething({val: state.val})">...</t>
2731
- * ```
2732
- *
2733
- * this needs to be translated in something like this:
2734
- *
2735
- * ```js
2736
- * if (context["computeSomething"]({val: context["state"].val})) { ... }
2737
- * ```
2738
- *
2739
- * This file contains the implementation of an extremely naive tokenizer/parser
2740
- * and evaluator for javascript expressions. The supported grammar is basically
2741
- * only expressive enough to understand the shape of objects, of arrays, and
2742
- * various operators.
2743
- */
2744
- //------------------------------------------------------------------------------
2745
- // Misc types, constants and helpers
2746
- //------------------------------------------------------------------------------
2747
- const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(",");
2748
- const WORD_REPLACEMENT = Object.assign(Object.create(null), {
2749
- and: "&&",
2750
- or: "||",
2751
- gt: ">",
2752
- gte: ">=",
2753
- lt: "<",
2754
- lte: "<=",
2755
- });
2756
- const STATIC_TOKEN_MAP = Object.assign(Object.create(null), {
2757
- "{": "LEFT_BRACE",
2758
- "}": "RIGHT_BRACE",
2759
- "[": "LEFT_BRACKET",
2760
- "]": "RIGHT_BRACKET",
2761
- ":": "COLON",
2762
- ",": "COMMA",
2763
- "(": "LEFT_PAREN",
2764
- ")": "RIGHT_PAREN",
2765
- });
2766
- // note that the space after typeof is relevant. It makes sure that the formatted
2767
- // expression has a space after typeof
2768
- const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
2769
- let tokenizeString = function (expr) {
2770
- let s = expr[0];
2771
- let start = s;
2772
- if (s !== "'" && s !== '"' && s !== "`") {
2773
- return false;
2550
+ // -----------------------------------------------------------------------------
2551
+ // hooks
2552
+ // -----------------------------------------------------------------------------
2553
+ function onWillStart(fn) {
2554
+ const node = getCurrent();
2555
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2556
+ node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
2557
+ }
2558
+ function onWillUpdateProps(fn) {
2559
+ const node = getCurrent();
2560
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2561
+ node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
2562
+ }
2563
+ function onMounted(fn) {
2564
+ const node = getCurrent();
2565
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2566
+ node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
2567
+ }
2568
+ function onWillPatch(fn) {
2569
+ const node = getCurrent();
2570
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2571
+ node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
2572
+ }
2573
+ function onPatched(fn) {
2574
+ const node = getCurrent();
2575
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2576
+ node.patched.push(decorate(fn.bind(node.component), "onPatched"));
2577
+ }
2578
+ function onWillUnmount(fn) {
2579
+ const node = getCurrent();
2580
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2581
+ node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
2582
+ }
2583
+ function onWillDestroy(fn) {
2584
+ const node = getCurrent();
2585
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2586
+ node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
2587
+ }
2588
+ function onWillRender(fn) {
2589
+ const node = getCurrent();
2590
+ const renderFn = node.renderFn;
2591
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2592
+ fn = decorate(fn.bind(node.component), "onWillRender");
2593
+ node.renderFn = () => {
2594
+ fn();
2595
+ return renderFn();
2596
+ };
2597
+ }
2598
+ function onRendered(fn) {
2599
+ const node = getCurrent();
2600
+ const renderFn = node.renderFn;
2601
+ const decorate = node.app.dev ? wrapError : (fn) => fn;
2602
+ fn = decorate(fn.bind(node.component), "onRendered");
2603
+ node.renderFn = () => {
2604
+ const result = renderFn();
2605
+ fn();
2606
+ return result;
2607
+ };
2608
+ }
2609
+ function onError(callback) {
2610
+ const node = getCurrent();
2611
+ let handlers = nodeErrorHandlers.get(node);
2612
+ if (!handlers) {
2613
+ handlers = [];
2614
+ nodeErrorHandlers.set(node, handlers);
2774
2615
  }
2775
- let i = 1;
2776
- let cur;
2777
- while (expr[i] && expr[i] !== start) {
2778
- cur = expr[i];
2779
- s += cur;
2780
- if (cur === "\\") {
2781
- i++;
2782
- cur = expr[i];
2783
- if (!cur) {
2784
- throw new Error("Invalid expression");
2616
+ handlers.push(callback.bind(node.component));
2617
+ }
2618
+
2619
+ const VText = text("").constructor;
2620
+ class VPortal extends VText {
2621
+ constructor(selector, realBDom) {
2622
+ super("");
2623
+ this.target = null;
2624
+ this.selector = selector;
2625
+ this.realBDom = realBDom;
2626
+ }
2627
+ mount(parent, anchor) {
2628
+ super.mount(parent, anchor);
2629
+ this.target = document.querySelector(this.selector);
2630
+ if (!this.target) {
2631
+ let el = this.el;
2632
+ while (el && el.parentElement instanceof HTMLElement) {
2633
+ el = el.parentElement;
2634
+ }
2635
+ this.target = el && el.querySelector(this.selector);
2636
+ if (!this.target) {
2637
+ throw new Error("invalid portal target");
2785
2638
  }
2786
- s += cur;
2787
2639
  }
2788
- i++;
2640
+ this.realBDom.mount(this.target, null);
2789
2641
  }
2790
- if (expr[i] !== start) {
2791
- throw new Error("Invalid expression");
2642
+ beforeRemove() {
2643
+ this.realBDom.beforeRemove();
2792
2644
  }
2793
- s += start;
2794
- if (start === "`") {
2795
- return {
2796
- type: "TEMPLATE_STRING",
2797
- value: s,
2798
- replace(replacer) {
2799
- return s.replace(/\$\{(.*?)\}/g, (match, group) => {
2800
- return "${" + replacer(group) + "}";
2801
- });
2802
- },
2803
- };
2645
+ remove() {
2646
+ if (this.realBDom) {
2647
+ super.remove();
2648
+ this.realBDom.remove();
2649
+ this.realBDom = null;
2650
+ }
2804
2651
  }
2805
- return { type: "VALUE", value: s };
2806
- };
2807
- let tokenizeNumber = function (expr) {
2808
- let s = expr[0];
2809
- if (s && s.match(/[0-9]/)) {
2810
- let i = 1;
2811
- while (expr[i] && expr[i].match(/[0-9]|\./)) {
2812
- s += expr[i];
2813
- i++;
2652
+ patch(other) {
2653
+ super.patch(other);
2654
+ if (this.realBDom) {
2655
+ this.realBDom.patch(other.realBDom, true);
2656
+ }
2657
+ else {
2658
+ this.realBDom = other.realBDom;
2659
+ this.realBDom.mount(this.target, null);
2814
2660
  }
2815
- return { type: "VALUE", value: s };
2816
2661
  }
2817
- else {
2818
- return false;
2662
+ }
2663
+ /**
2664
+ * <t t-slot="default"/>
2665
+ */
2666
+ function portalTemplate(app, bdom, helpers) {
2667
+ let { callSlot } = helpers;
2668
+ return function template(ctx, node, key = "") {
2669
+ return callSlot(ctx, node, key, "default", false, null);
2670
+ };
2671
+ }
2672
+ class Portal extends Component {
2673
+ setup() {
2674
+ const node = this.__owl__;
2675
+ const renderFn = node.renderFn;
2676
+ node.renderFn = () => new VPortal(this.props.target, renderFn());
2677
+ onWillUnmount(() => {
2678
+ if (node.bdom) {
2679
+ node.bdom.remove();
2680
+ }
2681
+ });
2819
2682
  }
2820
- };
2821
- let tokenizeSymbol = function (expr) {
2822
- let s = expr[0];
2823
- if (s && s.match(/[a-zA-Z_\$]/)) {
2824
- let i = 1;
2825
- while (expr[i] && expr[i].match(/\w/)) {
2826
- s += expr[i];
2827
- i++;
2683
+ }
2684
+ Portal.template = "__portal__";
2685
+ Portal.props = {
2686
+ target: {
2687
+ type: String,
2688
+ },
2689
+ slots: true,
2690
+ };
2691
+
2692
+ // -----------------------------------------------------------------------------
2693
+ // helpers
2694
+ // -----------------------------------------------------------------------------
2695
+ const isUnionType = (t) => Array.isArray(t);
2696
+ const isBaseType = (t) => typeof t !== "object";
2697
+ function isOptional(t) {
2698
+ return typeof t === "object" && "optional" in t ? t.optional || false : false;
2699
+ }
2700
+ function describeType(type) {
2701
+ return type === "*" || type === true ? "value" : type.name.toLowerCase();
2702
+ }
2703
+ function describe(info) {
2704
+ if (isBaseType(info)) {
2705
+ return describeType(info);
2706
+ }
2707
+ else if (isUnionType(info)) {
2708
+ return info.map(describe).join(" or ");
2709
+ }
2710
+ if ("element" in info) {
2711
+ return `list of ${describe({ type: info.element, optional: false })}s`;
2712
+ }
2713
+ if ("shape" in info) {
2714
+ return `object`;
2715
+ }
2716
+ return describe(info.type || "*");
2717
+ }
2718
+ function toSchema(spec) {
2719
+ return Object.fromEntries(spec.map((e) => e.endsWith("?") ? [e.slice(0, -1), { optional: true }] : [e, { type: "*", optional: false }]));
2720
+ }
2721
+ /**
2722
+ * Main validate function
2723
+ */
2724
+ function validate(obj, spec) {
2725
+ let errors = validateSchema(obj, spec);
2726
+ if (errors.length) {
2727
+ throw new Error("Invalid object: " + errors.join(", "));
2728
+ }
2729
+ }
2730
+ /**
2731
+ * Helper validate function, to get the list of errors. useful if one want to
2732
+ * manipulate the errors without parsing an error object
2733
+ */
2734
+ function validateSchema(obj, schema) {
2735
+ if (Array.isArray(schema)) {
2736
+ schema = toSchema(schema);
2737
+ }
2738
+ let errors = [];
2739
+ // check if each value in obj has correct shape
2740
+ for (let key in obj) {
2741
+ if (key in schema) {
2742
+ let result = validateType(key, obj[key], schema[key]);
2743
+ if (result) {
2744
+ errors.push(result);
2745
+ }
2828
2746
  }
2829
- if (s in WORD_REPLACEMENT) {
2830
- return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
2747
+ else if (!("*" in schema)) {
2748
+ errors.push(`unknown key '${key}'`);
2831
2749
  }
2832
- return { type: "SYMBOL", value: s };
2833
2750
  }
2834
- else {
2835
- return false;
2751
+ // check that all specified keys are defined in obj
2752
+ for (let key in schema) {
2753
+ const spec = schema[key];
2754
+ if (key !== "*" && !isOptional(spec) && !(key in obj)) {
2755
+ const isObj = typeof spec === "object" && !Array.isArray(spec);
2756
+ const isAny = spec === "*" || (isObj && "type" in spec ? spec.type === "*" : isObj);
2757
+ let detail = isAny ? "" : ` (should be a ${describe(spec)})`;
2758
+ errors.push(`'${key}' is missing${detail}`);
2759
+ }
2836
2760
  }
2837
- };
2838
- const tokenizeStatic = function (expr) {
2839
- const char = expr[0];
2840
- if (char && char in STATIC_TOKEN_MAP) {
2841
- return { type: STATIC_TOKEN_MAP[char], value: char };
2761
+ return errors;
2762
+ }
2763
+ function validateBaseType(key, value, type) {
2764
+ if (typeof type === "function") {
2765
+ if (typeof value === "object") {
2766
+ if (!(value instanceof type)) {
2767
+ return `'${key}' is not a ${describeType(type)}`;
2768
+ }
2769
+ }
2770
+ else if (typeof value !== type.name.toLowerCase()) {
2771
+ return `'${key}' is not a ${describeType(type)}`;
2772
+ }
2842
2773
  }
2843
- return false;
2844
- };
2845
- const tokenizeOperator = function (expr) {
2846
- for (let op of OPERATORS) {
2847
- if (expr.startsWith(op)) {
2848
- return { type: "OPERATOR", value: op };
2774
+ return null;
2775
+ }
2776
+ function validateArrayType(key, value, descr) {
2777
+ if (!Array.isArray(value)) {
2778
+ return `'${key}' is not a list of ${describe(descr)}s`;
2779
+ }
2780
+ for (let i = 0; i < value.length; i++) {
2781
+ const error = validateType(`${key}[${i}]`, value[i], descr);
2782
+ if (error) {
2783
+ return error;
2849
2784
  }
2850
2785
  }
2851
- return false;
2852
- };
2853
- const TOKENIZERS = [
2854
- tokenizeString,
2855
- tokenizeNumber,
2856
- tokenizeOperator,
2857
- tokenizeSymbol,
2858
- tokenizeStatic,
2859
- ];
2860
- /**
2861
- * Convert a javascript expression (as a string) into a list of tokens. For
2862
- * example: `tokenize("1 + b")` will return:
2863
- * ```js
2864
- * [
2865
- * {type: "VALUE", value: "1"},
2866
- * {type: "OPERATOR", value: "+"},
2867
- * {type: "SYMBOL", value: "b"}
2868
- * ]
2869
- * ```
2870
- */
2871
- function tokenize(expr) {
2872
- const result = [];
2873
- let token = true;
2874
- let error;
2875
- let current = expr;
2876
- try {
2877
- while (token) {
2878
- current = current.trim();
2879
- if (current) {
2880
- for (let tokenizer of TOKENIZERS) {
2881
- token = tokenizer(current);
2882
- if (token) {
2883
- result.push(token);
2884
- current = current.slice(token.size || token.value.length);
2885
- break;
2886
- }
2887
- }
2888
- }
2889
- else {
2890
- token = false;
2786
+ return null;
2787
+ }
2788
+ function validateType(key, value, descr) {
2789
+ if (value === undefined) {
2790
+ return isOptional(descr) ? null : `'${key}' is undefined (should be a ${describe(descr)})`;
2791
+ }
2792
+ else if (isBaseType(descr)) {
2793
+ return validateBaseType(key, value, descr);
2794
+ }
2795
+ else if (isUnionType(descr)) {
2796
+ let validDescr = descr.find((p) => !validateType(key, value, p));
2797
+ return validDescr ? null : `'${key}' is not a ${describe(descr)}`;
2798
+ }
2799
+ let result = null;
2800
+ if ("element" in descr) {
2801
+ result = validateArrayType(key, value, descr.element);
2802
+ }
2803
+ else if ("shape" in descr && !result) {
2804
+ if (typeof value !== "object" || Array.isArray(value)) {
2805
+ result = `'${key}' is not an object`;
2806
+ }
2807
+ else {
2808
+ const errors = validateSchema(value, descr.shape);
2809
+ if (errors.length) {
2810
+ result = `'${key}' has not the correct shape (${errors.join(", ")})`;
2891
2811
  }
2892
2812
  }
2893
2813
  }
2894
- catch (e) {
2895
- error = e; // Silence all errors and throw a generic error below
2814
+ if ("type" in descr && !result) {
2815
+ result = validateType(key, value, descr.type);
2896
2816
  }
2897
- if (current.length || error) {
2898
- throw new Error(`Tokenizer error: could not tokenize \`${expr}\``);
2817
+ if ("validate" in descr && !result) {
2818
+ result = !descr.validate(value) ? `'${key}' is not valid` : null;
2899
2819
  }
2900
2820
  return result;
2901
- }
2902
- //------------------------------------------------------------------------------
2903
- // Expression "evaluator"
2904
- //------------------------------------------------------------------------------
2905
- const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
2906
- const isRightSeparator = (token) => token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
2821
+ }
2822
+
2907
2823
  /**
2908
- * This is the main function exported by this file. This is the code that will
2909
- * process an expression (given as a string) and returns another expression with
2910
- * proper lookups in the context.
2911
- *
2912
- * Usually, this kind of code would be very simple to do if we had an AST (so,
2913
- * if we had a javascript parser), since then, we would only need to find the
2914
- * variables and replace them. However, a parser is more complicated, and there
2915
- * are no standard builtin parser API.
2916
- *
2917
- * Since this method is applied to simple javasript expressions, and the work to
2918
- * be done is actually quite simple, we actually can get away with not using a
2919
- * parser, which helps with the code size.
2920
- *
2921
- * Here is the heuristic used by this method to determine if a token is a
2922
- * variable:
2923
- * - by default, all symbols are considered a variable
2924
- * - unless the previous token is a dot (in that case, this is a property: `a.b`)
2925
- * - or if the previous token is a left brace or a comma, and the next token is
2926
- * a colon (in that case, this is an object key: `{a: b}`)
2927
- *
2928
- * Some specific code is also required to support arrow functions. If we detect
2929
- * the arrow operator, then we add the current (or some previous tokens) token to
2930
- * the list of variables so it does not get replaced by a lookup in the context
2824
+ * This file contains utility functions that will be injected in each template,
2825
+ * to perform various useful tasks in the compiled code.
2931
2826
  */
2932
- function compileExprToArray(expr) {
2933
- const localVars = new Set();
2934
- const tokens = tokenize(expr);
2935
- let i = 0;
2936
- let stack = []; // to track last opening [ or {
2937
- while (i < tokens.length) {
2938
- let token = tokens[i];
2939
- let prevToken = tokens[i - 1];
2940
- let nextToken = tokens[i + 1];
2941
- let groupType = stack[stack.length - 1];
2942
- switch (token.type) {
2943
- case "LEFT_BRACE":
2944
- case "LEFT_BRACKET":
2945
- stack.push(token.type);
2946
- break;
2947
- case "RIGHT_BRACE":
2948
- case "RIGHT_BRACKET":
2949
- stack.pop();
2950
- }
2951
- let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
2952
- if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
2953
- if (prevToken) {
2954
- // normalize missing tokens: {a} should be equivalent to {a:a}
2955
- if (groupType === "LEFT_BRACE" &&
2956
- isLeftSeparator(prevToken) &&
2957
- isRightSeparator(nextToken)) {
2958
- tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
2959
- nextToken = tokens[i + 1];
2960
- }
2961
- if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
2962
- isVar = false;
2963
- }
2964
- else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
2965
- if (nextToken && nextToken.type === "COLON") {
2966
- isVar = false;
2967
- }
2968
- }
2969
- }
2970
- }
2971
- if (token.type === "TEMPLATE_STRING") {
2972
- token.value = token.replace((expr) => compileExpr(expr));
2973
- }
2974
- if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
2975
- if (token.type === "RIGHT_PAREN") {
2976
- let j = i - 1;
2977
- while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
2978
- if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
2979
- tokens[j].value = tokens[j].originalValue;
2980
- localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };
2981
- }
2982
- j--;
2983
- }
2984
- }
2985
- else {
2986
- localVars.add(token.value); //] = { id: token.value, expr: token.value };
2987
- }
2827
+ function withDefault(value, defaultValue) {
2828
+ return value === undefined || value === null || value === false ? defaultValue : value;
2829
+ }
2830
+ function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
2831
+ key = key + "__slot_" + name;
2832
+ const slots = ctx.props.slots || {};
2833
+ const { __render, __ctx, __scope } = slots[name] || {};
2834
+ const slotScope = Object.create(__ctx || {});
2835
+ if (__scope) {
2836
+ slotScope[__scope] = extra;
2837
+ }
2838
+ const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
2839
+ if (defaultContent) {
2840
+ let child1 = undefined;
2841
+ let child2 = undefined;
2842
+ if (slotBDom) {
2843
+ child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
2988
2844
  }
2989
- if (isVar) {
2990
- token.varName = token.value;
2991
- if (!localVars.has(token.value)) {
2992
- token.originalValue = token.value;
2993
- token.value = `ctx['${token.value}']`;
2994
- }
2845
+ else {
2846
+ child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
2995
2847
  }
2996
- i++;
2848
+ return multi([child1, child2]);
2997
2849
  }
2998
- // Mark all variables that have been used locally.
2999
- // This assumes the expression has only one scope (incorrect but "good enough for now")
3000
- for (const token of tokens) {
3001
- if (token.type === "SYMBOL" && token.varName && localVars.has(token.value)) {
3002
- token.originalValue = token.value;
3003
- token.value = `_${token.value}`;
3004
- token.isLocal = true;
3005
- }
2850
+ return slotBDom || text("");
2851
+ }
2852
+ function capture(ctx) {
2853
+ const component = ctx.__owl__.component;
2854
+ const result = Object.create(component);
2855
+ for (let k in ctx) {
2856
+ result[k] = ctx[k];
3006
2857
  }
3007
- return tokens;
2858
+ return result;
3008
2859
  }
3009
- function compileExpr(expr) {
3010
- return compileExprToArray(expr)
3011
- .map((t) => t.value)
3012
- .join("");
2860
+ function withKey(elem, k) {
2861
+ elem.key = k;
2862
+ return elem;
3013
2863
  }
3014
- const INTERP_REGEXP = /\{\{.*?\}\}/g;
3015
- const INTERP_GROUP_REGEXP = /\{\{.*?\}\}/g;
3016
- function interpolate(s) {
3017
- let matches = s.match(INTERP_REGEXP);
3018
- if (matches && matches[0].length === s.length) {
3019
- return `(${compileExpr(s.slice(2, -2))})`;
3020
- }
3021
- let r = s.replace(INTERP_GROUP_REGEXP, (s) => "${" + compileExpr(s.slice(2, -2)) + "}");
3022
- return "`" + r + "`";
3023
- }
3024
-
3025
- // using a non-html document so that <inner/outer>HTML serializes as XML instead
3026
- // of HTML (as we will parse it as xml later)
3027
- const xmlDoc = document.implementation.createDocument(null, null, null);
3028
- const MODS = new Set(["stop", "capture", "prevent", "self", "synthetic"]);
3029
- // -----------------------------------------------------------------------------
3030
- // BlockDescription
3031
- // -----------------------------------------------------------------------------
3032
- class BlockDescription {
3033
- constructor(target, type) {
3034
- this.dynamicTagName = null;
3035
- this.isRoot = false;
3036
- this.hasDynamicChildren = false;
3037
- this.children = [];
3038
- this.data = [];
3039
- this.childNumber = 0;
3040
- this.parentVar = "";
3041
- this.id = BlockDescription.nextBlockId++;
3042
- this.varName = "b" + this.id;
3043
- this.blockName = "block" + this.id;
3044
- this.target = target;
3045
- this.type = type;
2864
+ function prepareList(collection) {
2865
+ let keys;
2866
+ let values;
2867
+ if (Array.isArray(collection)) {
2868
+ keys = collection;
2869
+ values = collection;
3046
2870
  }
3047
- static generateId(prefix) {
3048
- this.nextDataIds[prefix] = (this.nextDataIds[prefix] || 0) + 1;
3049
- return prefix + this.nextDataIds[prefix];
2871
+ else if (collection) {
2872
+ values = Object.keys(collection);
2873
+ keys = Object.values(collection);
3050
2874
  }
3051
- insertData(str, prefix = "d") {
3052
- const id = BlockDescription.generateId(prefix);
3053
- this.target.addLine(`let ${id} = ${str};`);
3054
- return this.data.push(id) - 1;
2875
+ else {
2876
+ throw new Error("Invalid loop expression");
3055
2877
  }
3056
- insert(dom) {
3057
- if (this.currentDom) {
3058
- this.currentDom.appendChild(dom);
3059
- }
3060
- else {
3061
- this.dom = dom;
2878
+ const n = values.length;
2879
+ return [keys, values, n, new Array(n)];
2880
+ }
2881
+ const isBoundary = Symbol("isBoundary");
2882
+ function setContextValue(ctx, key, value) {
2883
+ const ctx0 = ctx;
2884
+ while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
2885
+ const newCtx = ctx.__proto__;
2886
+ if (!newCtx) {
2887
+ ctx = ctx0;
2888
+ break;
3062
2889
  }
2890
+ ctx = newCtx;
3063
2891
  }
3064
- generateExpr(expr) {
3065
- if (this.type === "block") {
3066
- const hasChildren = this.children.length;
3067
- let params = this.data.length ? `[${this.data.join(", ")}]` : hasChildren ? "[]" : "";
3068
- if (hasChildren) {
3069
- params += ", [" + this.children.map((c) => c.varName).join(", ") + "]";
3070
- }
3071
- if (this.dynamicTagName) {
3072
- return `toggler(${this.dynamicTagName}, ${this.blockName}(${this.dynamicTagName})(${params}))`;
3073
- }
3074
- return `${this.blockName}(${params})`;
3075
- }
3076
- else if (this.type === "list") {
3077
- return `list(c_block${this.id})`;
2892
+ ctx[key] = value;
2893
+ }
2894
+ function toNumber(val) {
2895
+ const n = parseFloat(val);
2896
+ return isNaN(n) ? val : n;
2897
+ }
2898
+ function shallowEqual(l1, l2) {
2899
+ for (let i = 0, l = l1.length; i < l; i++) {
2900
+ if (l1[i] !== l2[i]) {
2901
+ return false;
3078
2902
  }
3079
- return expr;
3080
2903
  }
3081
- asXmlString() {
3082
- // Can't use outerHTML on text/comment nodes
3083
- // append dom to any element and use innerHTML instead
3084
- const t = xmlDoc.createElement("t");
3085
- t.appendChild(this.dom);
3086
- return t.innerHTML;
2904
+ return true;
2905
+ }
2906
+ class LazyValue {
2907
+ constructor(fn, ctx, node) {
2908
+ this.fn = fn;
2909
+ this.ctx = capture(ctx);
2910
+ this.node = node;
2911
+ }
2912
+ evaluate() {
2913
+ return this.fn(this.ctx, this.node);
2914
+ }
2915
+ toString() {
2916
+ return this.evaluate().toString();
3087
2917
  }
3088
2918
  }
3089
- BlockDescription.nextBlockId = 1;
3090
- BlockDescription.nextDataIds = {};
3091
- function createContext(parentCtx, params) {
3092
- return Object.assign({
3093
- block: null,
3094
- index: 0,
3095
- forceNewBlock: true,
3096
- translate: parentCtx.translate,
3097
- tKeyExpr: null,
3098
- nameSpace: parentCtx.nameSpace,
3099
- tModelSelectedExpr: parentCtx.tModelSelectedExpr,
3100
- }, params);
2919
+ /*
2920
+ * Safely outputs `value` as a block depending on the nature of `value`
2921
+ */
2922
+ function safeOutput(value) {
2923
+ if (!value) {
2924
+ return value;
2925
+ }
2926
+ let safeKey;
2927
+ let block;
2928
+ if (value instanceof Markup) {
2929
+ safeKey = `string_safe`;
2930
+ block = html(value);
2931
+ }
2932
+ else if (value instanceof LazyValue) {
2933
+ safeKey = `lazy_value`;
2934
+ block = value.evaluate();
2935
+ }
2936
+ else if (value instanceof String || typeof value === "string") {
2937
+ safeKey = "string_unsafe";
2938
+ block = text(value);
2939
+ }
2940
+ else {
2941
+ // Assuming it is a block
2942
+ safeKey = "block_safe";
2943
+ block = value;
2944
+ }
2945
+ return toggler(safeKey, block);
3101
2946
  }
3102
- class CodeTarget {
3103
- constructor(name, on) {
3104
- this.indentLevel = 0;
3105
- this.loopLevel = 0;
3106
- this.code = [];
3107
- this.hasRoot = false;
3108
- this.hasCache = false;
3109
- this.hasRef = false;
3110
- // maps ref name to [id, expr]
3111
- this.refInfo = {};
3112
- this.shouldProtectScope = false;
3113
- this.name = name;
3114
- this.on = on || null;
2947
+ let boundFunctions = new WeakMap();
2948
+ function bind(ctx, fn) {
2949
+ let component = ctx.__owl__.component;
2950
+ let boundFnMap = boundFunctions.get(component);
2951
+ if (!boundFnMap) {
2952
+ boundFnMap = new WeakMap();
2953
+ boundFunctions.set(component, boundFnMap);
3115
2954
  }
3116
- addLine(line, idx) {
3117
- const prefix = new Array(this.indentLevel + 2).join(" ");
3118
- if (idx === undefined) {
3119
- this.code.push(prefix + line);
3120
- }
3121
- else {
3122
- this.code.splice(idx, 0, prefix + line);
3123
- }
2955
+ let boundFn = boundFnMap.get(fn);
2956
+ if (!boundFn) {
2957
+ boundFn = fn.bind(component);
2958
+ boundFnMap.set(fn, boundFn);
3124
2959
  }
3125
- generateCode() {
3126
- let result = [];
3127
- result.push(`function ${this.name}(ctx, node, key = "") {`);
3128
- if (this.hasRef) {
3129
- result.push(` const refs = ctx.__owl__.refs;`);
3130
- for (let name in this.refInfo) {
3131
- const [id, expr] = this.refInfo[name];
3132
- result.push(` const ${id} = ${expr};`);
2960
+ return boundFn;
2961
+ }
2962
+ function multiRefSetter(refs, name) {
2963
+ let count = 0;
2964
+ return (el) => {
2965
+ if (el) {
2966
+ count++;
2967
+ if (count > 1) {
2968
+ throw new Error("Cannot have 2 elements with same ref name at the same time");
3133
2969
  }
3134
2970
  }
3135
- if (this.shouldProtectScope) {
3136
- result.push(` ctx = Object.create(ctx);`);
3137
- result.push(` ctx[isBoundary] = 1`);
3138
- }
3139
- if (this.hasCache) {
3140
- result.push(` let cache = ctx.cache || {};`);
3141
- result.push(` let nextCache = ctx.cache = {};`);
2971
+ if (count === 0 || el) {
2972
+ refs[name] = el;
3142
2973
  }
3143
- for (let line of this.code) {
3144
- result.push(line);
2974
+ };
2975
+ }
2976
+ /**
2977
+ * Validate the component props (or next props) against the (static) props
2978
+ * description. This is potentially an expensive operation: it may needs to
2979
+ * visit recursively the props and all the children to check if they are valid.
2980
+ * This is why it is only done in 'dev' mode.
2981
+ */
2982
+ function validateProps(name, props, parent) {
2983
+ const ComponentClass = typeof name !== "string"
2984
+ ? name
2985
+ : parent.constructor.components[name];
2986
+ if (!ComponentClass) {
2987
+ // this is an error, wrong component. We silently return here instead so the
2988
+ // error is triggered by the usual path ('component' function)
2989
+ return;
2990
+ }
2991
+ const schema = ComponentClass.props;
2992
+ if (!schema) {
2993
+ if (parent.__owl__.app.warnIfNoStaticProps) {
2994
+ console.warn(`Component '${ComponentClass.name}' does not have a static props description`);
3145
2995
  }
3146
- if (!this.hasRoot) {
3147
- result.push(`return text('');`);
2996
+ return;
2997
+ }
2998
+ const defaultProps = ComponentClass.defaultProps;
2999
+ if (defaultProps) {
3000
+ let isMandatory = (name) => Array.isArray(schema)
3001
+ ? schema.includes(name)
3002
+ : name in schema && !("*" in schema) && !isOptional(schema[name]);
3003
+ for (let p in defaultProps) {
3004
+ if (isMandatory(p)) {
3005
+ throw new Error(`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`);
3006
+ }
3148
3007
  }
3149
- result.push(`}`);
3150
- return result.join("\n ");
3008
+ }
3009
+ const errors = validateSchema(props, schema);
3010
+ if (errors.length) {
3011
+ throw new Error(`Invalid props for component '${ComponentClass.name}': ` + errors.join(", "));
3151
3012
  }
3152
3013
  }
3153
- const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
3154
- const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
3155
- class CodeGenerator {
3156
- constructor(ast, options) {
3157
- this.blocks = [];
3158
- this.ids = {};
3159
- this.nextBlockId = 1;
3160
- this.isDebug = false;
3161
- this.targets = [];
3162
- this.target = new CodeTarget("template");
3163
- this.translatableAttributes = TRANSLATABLE_ATTRS;
3164
- this.staticDefs = [];
3165
- this.helpers = new Set();
3166
- this.translateFn = options.translateFn || ((s) => s);
3167
- if (options.translatableAttributes) {
3168
- const attrs = new Set(TRANSLATABLE_ATTRS);
3169
- for (let attr of options.translatableAttributes) {
3170
- if (attr.startsWith("-")) {
3171
- attrs.delete(attr.slice(1));
3172
- }
3173
- else {
3174
- attrs.add(attr);
3014
+ const helpers = {
3015
+ withDefault,
3016
+ zero: Symbol("zero"),
3017
+ isBoundary,
3018
+ callSlot,
3019
+ capture,
3020
+ withKey,
3021
+ prepareList,
3022
+ setContextValue,
3023
+ multiRefSetter,
3024
+ shallowEqual,
3025
+ toNumber,
3026
+ validateProps,
3027
+ LazyValue,
3028
+ safeOutput,
3029
+ bind,
3030
+ createCatcher,
3031
+ markRaw,
3032
+ };
3033
+
3034
+ const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
3035
+ function parseXML$1(xml) {
3036
+ const parser = new DOMParser();
3037
+ const doc = parser.parseFromString(xml, "text/xml");
3038
+ if (doc.getElementsByTagName("parsererror").length) {
3039
+ let msg = "Invalid XML in template.";
3040
+ const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
3041
+ if (parsererrorText) {
3042
+ msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
3043
+ const re = /\d+/g;
3044
+ const firstMatch = re.exec(parsererrorText);
3045
+ if (firstMatch) {
3046
+ const lineNumber = Number(firstMatch[0]);
3047
+ const line = xml.split("\n")[lineNumber - 1];
3048
+ const secondMatch = re.exec(parsererrorText);
3049
+ if (line && secondMatch) {
3050
+ const columnIndex = Number(secondMatch[0]) - 1;
3051
+ if (line[columnIndex]) {
3052
+ msg +=
3053
+ `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
3054
+ `${line}\n${"-".repeat(columnIndex - 1)}^`;
3055
+ }
3175
3056
  }
3176
3057
  }
3177
- this.translatableAttributes = [...attrs];
3178
3058
  }
3179
- this.hasSafeContext = options.hasSafeContext || false;
3180
- this.dev = options.dev || false;
3181
- this.ast = ast;
3182
- this.templateName = options.name;
3059
+ throw new Error(msg);
3183
3060
  }
3184
- generateCode() {
3185
- const ast = this.ast;
3186
- this.isDebug = ast.type === 12 /* TDebug */;
3187
- BlockDescription.nextBlockId = 1;
3188
- BlockDescription.nextDataIds = {};
3189
- this.compileAST(ast, {
3190
- block: null,
3191
- index: 0,
3192
- forceNewBlock: false,
3193
- isLast: true,
3194
- translate: true,
3195
- tKeyExpr: null,
3196
- });
3197
- // define blocks and utility functions
3198
- let mainCode = [
3199
- ` let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;`,
3200
- ];
3201
- if (this.helpers.size) {
3202
- mainCode.push(`let { ${[...this.helpers].join(", ")} } = helpers;`);
3203
- }
3204
- if (this.templateName) {
3205
- mainCode.push(`// Template name: "${this.templateName}"`);
3206
- }
3207
- for (let { id, expr } of this.staticDefs) {
3208
- mainCode.push(`const ${id} = ${expr};`);
3209
- }
3210
- // define all blocks
3211
- if (this.blocks.length) {
3212
- mainCode.push(``);
3213
- for (let block of this.blocks) {
3214
- if (block.dom) {
3215
- let xmlString = block.asXmlString();
3216
- if (block.dynamicTagName) {
3217
- xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
3218
- xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
3219
- mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
3220
- }
3221
- else {
3222
- mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
3223
- }
3224
- }
3225
- }
3061
+ return doc;
3062
+ }
3063
+ class TemplateSet {
3064
+ constructor(config = {}) {
3065
+ this.rawTemplates = Object.create(globalTemplates);
3066
+ this.templates = {};
3067
+ this.Portal = Portal;
3068
+ this.dev = config.dev || false;
3069
+ this.translateFn = config.translateFn;
3070
+ this.translatableAttributes = config.translatableAttributes;
3071
+ if (config.templates) {
3072
+ this.addTemplates(config.templates);
3226
3073
  }
3227
- // define all slots/defaultcontent function
3228
- if (this.targets.length) {
3229
- for (let fn of this.targets) {
3230
- mainCode.push("");
3231
- mainCode = mainCode.concat(fn.generateCode());
3074
+ }
3075
+ static registerTemplate(name, fn) {
3076
+ globalTemplates[name] = fn;
3077
+ }
3078
+ addTemplate(name, template) {
3079
+ if (name in this.rawTemplates) {
3080
+ const rawTemplate = this.rawTemplates[name];
3081
+ const currentAsString = typeof rawTemplate === "string"
3082
+ ? rawTemplate
3083
+ : rawTemplate instanceof Element
3084
+ ? rawTemplate.outerHTML
3085
+ : rawTemplate.toString();
3086
+ const newAsString = typeof template === "string" ? template : template.outerHTML;
3087
+ if (currentAsString === newAsString) {
3088
+ return;
3232
3089
  }
3090
+ throw new Error(`Template ${name} already defined with different content`);
3233
3091
  }
3234
- // generate main code
3235
- mainCode.push("");
3236
- mainCode = mainCode.concat("return " + this.target.generateCode());
3237
- const code = mainCode.join("\n ");
3238
- if (this.isDebug) {
3239
- const msg = `[Owl Debug]\n${code}`;
3240
- console.log(msg);
3241
- }
3242
- return code;
3092
+ this.rawTemplates[name] = template;
3243
3093
  }
3244
- compileInNewTarget(prefix, ast, ctx, on) {
3245
- const name = this.generateId(prefix);
3246
- const initialTarget = this.target;
3247
- const target = new CodeTarget(name, on);
3248
- this.targets.push(target);
3249
- this.target = target;
3250
- this.compileAST(ast, createContext(ctx));
3251
- this.target = initialTarget;
3252
- return name;
3094
+ addTemplates(xml) {
3095
+ if (!xml) {
3096
+ // empty string
3097
+ return;
3098
+ }
3099
+ xml = xml instanceof Document ? xml : parseXML$1(xml);
3100
+ for (const template of xml.querySelectorAll("[t-name]")) {
3101
+ const name = template.getAttribute("t-name");
3102
+ this.addTemplate(name, template);
3103
+ }
3253
3104
  }
3254
- addLine(line, idx) {
3255
- this.target.addLine(line, idx);
3105
+ getTemplate(name) {
3106
+ if (!(name in this.templates)) {
3107
+ const rawTemplate = this.rawTemplates[name];
3108
+ if (rawTemplate === undefined) {
3109
+ let extraInfo = "";
3110
+ try {
3111
+ const componentName = getCurrent().component.constructor.name;
3112
+ extraInfo = ` (for component "${componentName}")`;
3113
+ }
3114
+ catch { }
3115
+ throw new Error(`Missing template: "${name}"${extraInfo}`);
3116
+ }
3117
+ const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
3118
+ const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
3119
+ // first add a function to lazily get the template, in case there is a
3120
+ // recursive call to the template name
3121
+ const templates = this.templates;
3122
+ this.templates[name] = function (context, parent) {
3123
+ return templates[name].call(this, context, parent);
3124
+ };
3125
+ const template = templateFn(this, bdom, helpers);
3126
+ this.templates[name] = template;
3127
+ }
3128
+ return this.templates[name];
3256
3129
  }
3257
- define(varName, expr) {
3258
- this.addLine(`const ${varName} = ${expr};`);
3130
+ _compileTemplate(name, template) {
3131
+ throw new Error(`Unable to compile a template. Please use owl full build instead`);
3259
3132
  }
3260
- generateId(prefix = "") {
3261
- this.ids[prefix] = (this.ids[prefix] || 0) + 1;
3262
- return prefix + this.ids[prefix];
3133
+ callTemplate(owner, subTemplate, ctx, parent, key) {
3134
+ const template = this.getTemplate(subTemplate);
3135
+ return toggler(subTemplate, template.call(owner, ctx, parent, key));
3263
3136
  }
3264
- insertAnchor(block) {
3265
- const tag = `block-child-${block.children.length}`;
3266
- const anchor = xmlDoc.createElement(tag);
3267
- block.insert(anchor);
3137
+ }
3138
+ // -----------------------------------------------------------------------------
3139
+ // xml tag helper
3140
+ // -----------------------------------------------------------------------------
3141
+ const globalTemplates = {};
3142
+ function xml(...args) {
3143
+ const name = `__template__${xml.nextId++}`;
3144
+ const value = String.raw(...args);
3145
+ globalTemplates[name] = value;
3146
+ return name;
3147
+ }
3148
+ xml.nextId = 1;
3149
+ TemplateSet.registerTemplate("__portal__", portalTemplate);
3150
+
3151
+ /**
3152
+ * Owl QWeb Expression Parser
3153
+ *
3154
+ * Owl needs in various contexts to be able to understand the structure of a
3155
+ * string representing a javascript expression. The usual goal is to be able
3156
+ * to rewrite some variables. For example, if a template has
3157
+ *
3158
+ * ```xml
3159
+ * <t t-if="computeSomething({val: state.val})">...</t>
3160
+ * ```
3161
+ *
3162
+ * this needs to be translated in something like this:
3163
+ *
3164
+ * ```js
3165
+ * if (context["computeSomething"]({val: context["state"].val})) { ... }
3166
+ * ```
3167
+ *
3168
+ * This file contains the implementation of an extremely naive tokenizer/parser
3169
+ * and evaluator for javascript expressions. The supported grammar is basically
3170
+ * only expressive enough to understand the shape of objects, of arrays, and
3171
+ * various operators.
3172
+ */
3173
+ //------------------------------------------------------------------------------
3174
+ // Misc types, constants and helpers
3175
+ //------------------------------------------------------------------------------
3176
+ const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(",");
3177
+ const WORD_REPLACEMENT = Object.assign(Object.create(null), {
3178
+ and: "&&",
3179
+ or: "||",
3180
+ gt: ">",
3181
+ gte: ">=",
3182
+ lt: "<",
3183
+ lte: "<=",
3184
+ });
3185
+ const STATIC_TOKEN_MAP = Object.assign(Object.create(null), {
3186
+ "{": "LEFT_BRACE",
3187
+ "}": "RIGHT_BRACE",
3188
+ "[": "LEFT_BRACKET",
3189
+ "]": "RIGHT_BRACKET",
3190
+ ":": "COLON",
3191
+ ",": "COMMA",
3192
+ "(": "LEFT_PAREN",
3193
+ ")": "RIGHT_PAREN",
3194
+ });
3195
+ // note that the space after typeof is relevant. It makes sure that the formatted
3196
+ // expression has a space after typeof. Currently we don't support delete and void
3197
+ const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ,new ".split(",");
3198
+ let tokenizeString = function (expr) {
3199
+ let s = expr[0];
3200
+ let start = s;
3201
+ if (s !== "'" && s !== '"' && s !== "`") {
3202
+ return false;
3268
3203
  }
3269
- createBlock(parentBlock, type, ctx) {
3270
- const hasRoot = this.target.hasRoot;
3271
- const block = new BlockDescription(this.target, type);
3272
- if (!hasRoot && !ctx.preventRoot) {
3273
- this.target.hasRoot = true;
3274
- block.isRoot = true;
3275
- }
3276
- if (parentBlock) {
3277
- parentBlock.children.push(block);
3278
- if (parentBlock.type === "list") {
3279
- block.parentVar = `c_block${parentBlock.id}`;
3204
+ let i = 1;
3205
+ let cur;
3206
+ while (expr[i] && expr[i] !== start) {
3207
+ cur = expr[i];
3208
+ s += cur;
3209
+ if (cur === "\\") {
3210
+ i++;
3211
+ cur = expr[i];
3212
+ if (!cur) {
3213
+ throw new Error("Invalid expression");
3280
3214
  }
3215
+ s += cur;
3281
3216
  }
3282
- return block;
3217
+ i++;
3283
3218
  }
3284
- insertBlock(expression, block, ctx) {
3285
- let blockExpr = block.generateExpr(expression);
3286
- const tKeyExpr = ctx.tKeyExpr;
3287
- if (block.parentVar) {
3288
- let keyArg = `key${this.target.loopLevel}`;
3289
- if (tKeyExpr) {
3290
- keyArg = `${tKeyExpr} + ${keyArg}`;
3291
- }
3292
- this.helpers.add("withKey");
3293
- this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${keyArg});`);
3294
- return;
3219
+ if (expr[i] !== start) {
3220
+ throw new Error("Invalid expression");
3221
+ }
3222
+ s += start;
3223
+ if (start === "`") {
3224
+ return {
3225
+ type: "TEMPLATE_STRING",
3226
+ value: s,
3227
+ replace(replacer) {
3228
+ return s.replace(/\$\{(.*?)\}/g, (match, group) => {
3229
+ return "${" + replacer(group) + "}";
3230
+ });
3231
+ },
3232
+ };
3233
+ }
3234
+ return { type: "VALUE", value: s };
3235
+ };
3236
+ let tokenizeNumber = function (expr) {
3237
+ let s = expr[0];
3238
+ if (s && s.match(/[0-9]/)) {
3239
+ let i = 1;
3240
+ while (expr[i] && expr[i].match(/[0-9]|\./)) {
3241
+ s += expr[i];
3242
+ i++;
3295
3243
  }
3296
- if (tKeyExpr) {
3297
- blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`;
3244
+ return { type: "VALUE", value: s };
3245
+ }
3246
+ else {
3247
+ return false;
3248
+ }
3249
+ };
3250
+ let tokenizeSymbol = function (expr) {
3251
+ let s = expr[0];
3252
+ if (s && s.match(/[a-zA-Z_\$]/)) {
3253
+ let i = 1;
3254
+ while (expr[i] && expr[i].match(/\w/)) {
3255
+ s += expr[i];
3256
+ i++;
3298
3257
  }
3299
- if (block.isRoot && !ctx.preventRoot) {
3300
- if (this.target.on) {
3301
- blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
3302
- }
3303
- this.addLine(`return ${blockExpr};`);
3258
+ if (s in WORD_REPLACEMENT) {
3259
+ return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
3304
3260
  }
3305
- else {
3306
- this.define(block.varName, blockExpr);
3261
+ return { type: "SYMBOL", value: s };
3262
+ }
3263
+ else {
3264
+ return false;
3265
+ }
3266
+ };
3267
+ const tokenizeStatic = function (expr) {
3268
+ const char = expr[0];
3269
+ if (char && char in STATIC_TOKEN_MAP) {
3270
+ return { type: STATIC_TOKEN_MAP[char], value: char };
3271
+ }
3272
+ return false;
3273
+ };
3274
+ const tokenizeOperator = function (expr) {
3275
+ for (let op of OPERATORS) {
3276
+ if (expr.startsWith(op)) {
3277
+ return { type: "OPERATOR", value: op };
3307
3278
  }
3308
3279
  }
3309
- /**
3310
- * Captures variables that are used inside of an expression. This is useful
3311
- * because in compiled code, almost all variables are accessed through the ctx
3312
- * object. In the case of functions, that lookup in the context can be delayed
3313
- * which can cause issues if the value has changed since the function was
3280
+ return false;
3281
+ };
3282
+ const TOKENIZERS = [
3283
+ tokenizeString,
3284
+ tokenizeNumber,
3285
+ tokenizeOperator,
3286
+ tokenizeSymbol,
3287
+ tokenizeStatic,
3288
+ ];
3289
+ /**
3290
+ * Convert a javascript expression (as a string) into a list of tokens. For
3291
+ * example: `tokenize("1 + b")` will return:
3292
+ * ```js
3293
+ * [
3294
+ * {type: "VALUE", value: "1"},
3295
+ * {type: "OPERATOR", value: "+"},
3296
+ * {type: "SYMBOL", value: "b"}
3297
+ * ]
3298
+ * ```
3299
+ */
3300
+ function tokenize(expr) {
3301
+ const result = [];
3302
+ let token = true;
3303
+ let error;
3304
+ let current = expr;
3305
+ try {
3306
+ while (token) {
3307
+ current = current.trim();
3308
+ if (current) {
3309
+ for (let tokenizer of TOKENIZERS) {
3310
+ token = tokenizer(current);
3311
+ if (token) {
3312
+ result.push(token);
3313
+ current = current.slice(token.size || token.value.length);
3314
+ break;
3315
+ }
3316
+ }
3317
+ }
3318
+ else {
3319
+ token = false;
3320
+ }
3321
+ }
3322
+ }
3323
+ catch (e) {
3324
+ error = e; // Silence all errors and throw a generic error below
3325
+ }
3326
+ if (current.length || error) {
3327
+ throw new Error(`Tokenizer error: could not tokenize \`${expr}\``);
3328
+ }
3329
+ return result;
3330
+ }
3331
+ //------------------------------------------------------------------------------
3332
+ // Expression "evaluator"
3333
+ //------------------------------------------------------------------------------
3334
+ const isLeftSeparator = (token) => token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
3335
+ const isRightSeparator = (token) => token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
3336
+ /**
3337
+ * This is the main function exported by this file. This is the code that will
3338
+ * process an expression (given as a string) and returns another expression with
3339
+ * proper lookups in the context.
3340
+ *
3341
+ * Usually, this kind of code would be very simple to do if we had an AST (so,
3342
+ * if we had a javascript parser), since then, we would only need to find the
3343
+ * variables and replace them. However, a parser is more complicated, and there
3344
+ * are no standard builtin parser API.
3345
+ *
3346
+ * Since this method is applied to simple javasript expressions, and the work to
3347
+ * be done is actually quite simple, we actually can get away with not using a
3348
+ * parser, which helps with the code size.
3349
+ *
3350
+ * Here is the heuristic used by this method to determine if a token is a
3351
+ * variable:
3352
+ * - by default, all symbols are considered a variable
3353
+ * - unless the previous token is a dot (in that case, this is a property: `a.b`)
3354
+ * - or if the previous token is a left brace or a comma, and the next token is
3355
+ * a colon (in that case, this is an object key: `{a: b}`)
3356
+ *
3357
+ * Some specific code is also required to support arrow functions. If we detect
3358
+ * the arrow operator, then we add the current (or some previous tokens) token to
3359
+ * the list of variables so it does not get replaced by a lookup in the context
3360
+ */
3361
+ function compileExprToArray(expr) {
3362
+ const localVars = new Set();
3363
+ const tokens = tokenize(expr);
3364
+ let i = 0;
3365
+ let stack = []; // to track last opening [ or {
3366
+ while (i < tokens.length) {
3367
+ let token = tokens[i];
3368
+ let prevToken = tokens[i - 1];
3369
+ let nextToken = tokens[i + 1];
3370
+ let groupType = stack[stack.length - 1];
3371
+ switch (token.type) {
3372
+ case "LEFT_BRACE":
3373
+ case "LEFT_BRACKET":
3374
+ stack.push(token.type);
3375
+ break;
3376
+ case "RIGHT_BRACE":
3377
+ case "RIGHT_BRACKET":
3378
+ stack.pop();
3379
+ }
3380
+ let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
3381
+ if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
3382
+ if (prevToken) {
3383
+ // normalize missing tokens: {a} should be equivalent to {a:a}
3384
+ if (groupType === "LEFT_BRACE" &&
3385
+ isLeftSeparator(prevToken) &&
3386
+ isRightSeparator(nextToken)) {
3387
+ tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
3388
+ nextToken = tokens[i + 1];
3389
+ }
3390
+ if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
3391
+ isVar = false;
3392
+ }
3393
+ else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
3394
+ if (nextToken && nextToken.type === "COLON") {
3395
+ isVar = false;
3396
+ }
3397
+ }
3398
+ }
3399
+ }
3400
+ if (token.type === "TEMPLATE_STRING") {
3401
+ token.value = token.replace((expr) => compileExpr(expr));
3402
+ }
3403
+ if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
3404
+ if (token.type === "RIGHT_PAREN") {
3405
+ let j = i - 1;
3406
+ while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
3407
+ if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
3408
+ tokens[j].value = tokens[j].originalValue;
3409
+ localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };
3410
+ }
3411
+ j--;
3412
+ }
3413
+ }
3414
+ else {
3415
+ localVars.add(token.value); //] = { id: token.value, expr: token.value };
3416
+ }
3417
+ }
3418
+ if (isVar) {
3419
+ token.varName = token.value;
3420
+ if (!localVars.has(token.value)) {
3421
+ token.originalValue = token.value;
3422
+ token.value = `ctx['${token.value}']`;
3423
+ }
3424
+ }
3425
+ i++;
3426
+ }
3427
+ // Mark all variables that have been used locally.
3428
+ // This assumes the expression has only one scope (incorrect but "good enough for now")
3429
+ for (const token of tokens) {
3430
+ if (token.type === "SYMBOL" && token.varName && localVars.has(token.value)) {
3431
+ token.originalValue = token.value;
3432
+ token.value = `_${token.value}`;
3433
+ token.isLocal = true;
3434
+ }
3435
+ }
3436
+ return tokens;
3437
+ }
3438
+ // Leading spaces are trimmed during tokenization, so they need to be added back for some values
3439
+ const paddedValues = new Map([["in ", " in "]]);
3440
+ function compileExpr(expr) {
3441
+ return compileExprToArray(expr)
3442
+ .map((t) => paddedValues.get(t.value) || t.value)
3443
+ .join("");
3444
+ }
3445
+ const INTERP_REGEXP = /\{\{.*?\}\}/g;
3446
+ const INTERP_GROUP_REGEXP = /\{\{.*?\}\}/g;
3447
+ function interpolate(s) {
3448
+ let matches = s.match(INTERP_REGEXP);
3449
+ if (matches && matches[0].length === s.length) {
3450
+ return `(${compileExpr(s.slice(2, -2))})`;
3451
+ }
3452
+ let r = s.replace(INTERP_GROUP_REGEXP, (s) => "${" + compileExpr(s.slice(2, -2)) + "}");
3453
+ return "`" + r + "`";
3454
+ }
3455
+
3456
+ // using a non-html document so that <inner/outer>HTML serializes as XML instead
3457
+ // of HTML (as we will parse it as xml later)
3458
+ const xmlDoc = document.implementation.createDocument(null, null, null);
3459
+ const MODS = new Set(["stop", "capture", "prevent", "self", "synthetic"]);
3460
+ let nextDataIds = {};
3461
+ function generateId(prefix = "") {
3462
+ nextDataIds[prefix] = (nextDataIds[prefix] || 0) + 1;
3463
+ return prefix + nextDataIds[prefix];
3464
+ }
3465
+ // -----------------------------------------------------------------------------
3466
+ // BlockDescription
3467
+ // -----------------------------------------------------------------------------
3468
+ class BlockDescription {
3469
+ constructor(target, type) {
3470
+ this.dynamicTagName = null;
3471
+ this.isRoot = false;
3472
+ this.hasDynamicChildren = false;
3473
+ this.children = [];
3474
+ this.data = [];
3475
+ this.childNumber = 0;
3476
+ this.parentVar = "";
3477
+ this.id = BlockDescription.nextBlockId++;
3478
+ this.varName = "b" + this.id;
3479
+ this.blockName = "block" + this.id;
3480
+ this.target = target;
3481
+ this.type = type;
3482
+ }
3483
+ insertData(str, prefix = "d") {
3484
+ const id = generateId(prefix);
3485
+ this.target.addLine(`let ${id} = ${str};`);
3486
+ return this.data.push(id) - 1;
3487
+ }
3488
+ insert(dom) {
3489
+ if (this.currentDom) {
3490
+ this.currentDom.appendChild(dom);
3491
+ }
3492
+ else {
3493
+ this.dom = dom;
3494
+ }
3495
+ }
3496
+ generateExpr(expr) {
3497
+ if (this.type === "block") {
3498
+ const hasChildren = this.children.length;
3499
+ let params = this.data.length ? `[${this.data.join(", ")}]` : hasChildren ? "[]" : "";
3500
+ if (hasChildren) {
3501
+ params += ", [" + this.children.map((c) => c.varName).join(", ") + "]";
3502
+ }
3503
+ if (this.dynamicTagName) {
3504
+ return `toggler(${this.dynamicTagName}, ${this.blockName}(${this.dynamicTagName})(${params}))`;
3505
+ }
3506
+ return `${this.blockName}(${params})`;
3507
+ }
3508
+ else if (this.type === "list") {
3509
+ return `list(c_block${this.id})`;
3510
+ }
3511
+ return expr;
3512
+ }
3513
+ asXmlString() {
3514
+ // Can't use outerHTML on text/comment nodes
3515
+ // append dom to any element and use innerHTML instead
3516
+ const t = xmlDoc.createElement("t");
3517
+ t.appendChild(this.dom);
3518
+ return t.innerHTML;
3519
+ }
3520
+ }
3521
+ BlockDescription.nextBlockId = 1;
3522
+ function createContext(parentCtx, params) {
3523
+ return Object.assign({
3524
+ block: null,
3525
+ index: 0,
3526
+ forceNewBlock: true,
3527
+ translate: parentCtx.translate,
3528
+ tKeyExpr: null,
3529
+ nameSpace: parentCtx.nameSpace,
3530
+ tModelSelectedExpr: parentCtx.tModelSelectedExpr,
3531
+ }, params);
3532
+ }
3533
+ class CodeTarget {
3534
+ constructor(name, on) {
3535
+ this.indentLevel = 0;
3536
+ this.loopLevel = 0;
3537
+ this.code = [];
3538
+ this.hasRoot = false;
3539
+ this.hasCache = false;
3540
+ this.hasRef = false;
3541
+ // maps ref name to [id, expr]
3542
+ this.refInfo = {};
3543
+ this.shouldProtectScope = false;
3544
+ this.name = name;
3545
+ this.on = on || null;
3546
+ }
3547
+ addLine(line, idx) {
3548
+ const prefix = new Array(this.indentLevel + 2).join(" ");
3549
+ if (idx === undefined) {
3550
+ this.code.push(prefix + line);
3551
+ }
3552
+ else {
3553
+ this.code.splice(idx, 0, prefix + line);
3554
+ }
3555
+ }
3556
+ generateCode() {
3557
+ let result = [];
3558
+ result.push(`function ${this.name}(ctx, node, key = "") {`);
3559
+ if (this.hasRef) {
3560
+ result.push(` const refs = ctx.__owl__.refs;`);
3561
+ for (let name in this.refInfo) {
3562
+ const [id, expr] = this.refInfo[name];
3563
+ result.push(` const ${id} = ${expr};`);
3564
+ }
3565
+ }
3566
+ if (this.shouldProtectScope) {
3567
+ result.push(` ctx = Object.create(ctx);`);
3568
+ result.push(` ctx[isBoundary] = 1`);
3569
+ }
3570
+ if (this.hasCache) {
3571
+ result.push(` let cache = ctx.cache || {};`);
3572
+ result.push(` let nextCache = ctx.cache = {};`);
3573
+ }
3574
+ for (let line of this.code) {
3575
+ result.push(line);
3576
+ }
3577
+ if (!this.hasRoot) {
3578
+ result.push(`return text('');`);
3579
+ }
3580
+ result.push(`}`);
3581
+ return result.join("\n ");
3582
+ }
3583
+ }
3584
+ const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
3585
+ const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
3586
+ class CodeGenerator {
3587
+ constructor(ast, options) {
3588
+ this.blocks = [];
3589
+ this.nextBlockId = 1;
3590
+ this.isDebug = false;
3591
+ this.targets = [];
3592
+ this.target = new CodeTarget("template");
3593
+ this.translatableAttributes = TRANSLATABLE_ATTRS;
3594
+ this.staticDefs = [];
3595
+ this.helpers = new Set();
3596
+ this.translateFn = options.translateFn || ((s) => s);
3597
+ if (options.translatableAttributes) {
3598
+ const attrs = new Set(TRANSLATABLE_ATTRS);
3599
+ for (let attr of options.translatableAttributes) {
3600
+ if (attr.startsWith("-")) {
3601
+ attrs.delete(attr.slice(1));
3602
+ }
3603
+ else {
3604
+ attrs.add(attr);
3605
+ }
3606
+ }
3607
+ this.translatableAttributes = [...attrs];
3608
+ }
3609
+ this.hasSafeContext = options.hasSafeContext || false;
3610
+ this.dev = options.dev || false;
3611
+ this.ast = ast;
3612
+ this.templateName = options.name;
3613
+ }
3614
+ generateCode() {
3615
+ const ast = this.ast;
3616
+ this.isDebug = ast.type === 12 /* TDebug */;
3617
+ BlockDescription.nextBlockId = 1;
3618
+ nextDataIds = {};
3619
+ this.compileAST(ast, {
3620
+ block: null,
3621
+ index: 0,
3622
+ forceNewBlock: false,
3623
+ isLast: true,
3624
+ translate: true,
3625
+ tKeyExpr: null,
3626
+ });
3627
+ // define blocks and utility functions
3628
+ let mainCode = [
3629
+ ` let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;`,
3630
+ ];
3631
+ if (this.helpers.size) {
3632
+ mainCode.push(`let { ${[...this.helpers].join(", ")} } = helpers;`);
3633
+ }
3634
+ if (this.templateName) {
3635
+ mainCode.push(`// Template name: "${this.templateName}"`);
3636
+ }
3637
+ for (let { id, expr } of this.staticDefs) {
3638
+ mainCode.push(`const ${id} = ${expr};`);
3639
+ }
3640
+ // define all blocks
3641
+ if (this.blocks.length) {
3642
+ mainCode.push(``);
3643
+ for (let block of this.blocks) {
3644
+ if (block.dom) {
3645
+ let xmlString = block.asXmlString();
3646
+ if (block.dynamicTagName) {
3647
+ xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`);
3648
+ xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`);
3649
+ mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`);
3650
+ }
3651
+ else {
3652
+ mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`);
3653
+ }
3654
+ }
3655
+ }
3656
+ }
3657
+ // define all slots/defaultcontent function
3658
+ if (this.targets.length) {
3659
+ for (let fn of this.targets) {
3660
+ mainCode.push("");
3661
+ mainCode = mainCode.concat(fn.generateCode());
3662
+ }
3663
+ }
3664
+ // generate main code
3665
+ mainCode.push("");
3666
+ mainCode = mainCode.concat("return " + this.target.generateCode());
3667
+ const code = mainCode.join("\n ");
3668
+ if (this.isDebug) {
3669
+ const msg = `[Owl Debug]\n${code}`;
3670
+ console.log(msg);
3671
+ }
3672
+ return code;
3673
+ }
3674
+ compileInNewTarget(prefix, ast, ctx, on) {
3675
+ const name = generateId(prefix);
3676
+ const initialTarget = this.target;
3677
+ const target = new CodeTarget(name, on);
3678
+ this.targets.push(target);
3679
+ this.target = target;
3680
+ this.compileAST(ast, createContext(ctx));
3681
+ this.target = initialTarget;
3682
+ return name;
3683
+ }
3684
+ addLine(line, idx) {
3685
+ this.target.addLine(line, idx);
3686
+ }
3687
+ define(varName, expr) {
3688
+ this.addLine(`const ${varName} = ${expr};`);
3689
+ }
3690
+ insertAnchor(block) {
3691
+ const tag = `block-child-${block.children.length}`;
3692
+ const anchor = xmlDoc.createElement(tag);
3693
+ block.insert(anchor);
3694
+ }
3695
+ createBlock(parentBlock, type, ctx) {
3696
+ const hasRoot = this.target.hasRoot;
3697
+ const block = new BlockDescription(this.target, type);
3698
+ if (!hasRoot && !ctx.preventRoot) {
3699
+ this.target.hasRoot = true;
3700
+ block.isRoot = true;
3701
+ }
3702
+ if (parentBlock) {
3703
+ parentBlock.children.push(block);
3704
+ if (parentBlock.type === "list") {
3705
+ block.parentVar = `c_block${parentBlock.id}`;
3706
+ }
3707
+ }
3708
+ return block;
3709
+ }
3710
+ insertBlock(expression, block, ctx) {
3711
+ let blockExpr = block.generateExpr(expression);
3712
+ const tKeyExpr = ctx.tKeyExpr;
3713
+ if (block.parentVar) {
3714
+ let keyArg = `key${this.target.loopLevel}`;
3715
+ if (tKeyExpr) {
3716
+ keyArg = `${tKeyExpr} + ${keyArg}`;
3717
+ }
3718
+ this.helpers.add("withKey");
3719
+ this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${keyArg});`);
3720
+ return;
3721
+ }
3722
+ if (tKeyExpr) {
3723
+ blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`;
3724
+ }
3725
+ if (block.isRoot && !ctx.preventRoot) {
3726
+ if (this.target.on) {
3727
+ blockExpr = this.wrapWithEventCatcher(blockExpr, this.target.on);
3728
+ }
3729
+ this.addLine(`return ${blockExpr};`);
3730
+ }
3731
+ else {
3732
+ this.define(block.varName, blockExpr);
3733
+ }
3734
+ }
3735
+ /**
3736
+ * Captures variables that are used inside of an expression. This is useful
3737
+ * because in compiled code, almost all variables are accessed through the ctx
3738
+ * object. In the case of functions, that lookup in the context can be delayed
3739
+ * which can cause issues if the value has changed since the function was
3314
3740
  * defined.
3315
3741
  *
3316
3742
  * @param expr the expression to capture
@@ -3329,7 +3755,7 @@ class CodeGenerator {
3329
3755
  .map((tok) => {
3330
3756
  if (tok.varName && !tok.isLocal) {
3331
3757
  if (!mapping.has(tok.varName)) {
3332
- const varId = this.generateId("v");
3758
+ const varId = generateId("v");
3333
3759
  mapping.set(tok.varName, varId);
3334
3760
  this.define(varId, tok.value);
3335
3761
  }
@@ -3469,7 +3895,7 @@ class CodeGenerator {
3469
3895
  block = this.createBlock(block, "block", ctx);
3470
3896
  this.blocks.push(block);
3471
3897
  if (ast.dynamicTag) {
3472
- const tagExpr = this.generateId("tag");
3898
+ const tagExpr = generateId("tag");
3473
3899
  this.define(tagExpr, compileExpr(ast.dynamicTag));
3474
3900
  block.dynamicTagName = tagExpr;
3475
3901
  }
@@ -3539,7 +3965,7 @@ class CodeGenerator {
3539
3965
  info[1] = `multiRefSetter(refs, \`${name}\`)`;
3540
3966
  }
3541
3967
  else {
3542
- let id = this.generateId("ref");
3968
+ let id = generateId("ref");
3543
3969
  this.target.refInfo[name] = [id, `(el) => refs[\`${name}\`] = el`];
3544
3970
  const index = block.data.push(id) - 1;
3545
3971
  attrs["block-ref"] = String(index);
@@ -3551,10 +3977,10 @@ class CodeGenerator {
3551
3977
  if (ast.model) {
3552
3978
  const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model;
3553
3979
  const baseExpression = compileExpr(baseExpr);
3554
- const bExprId = this.generateId("bExpr");
3980
+ const bExprId = generateId("bExpr");
3555
3981
  this.define(bExprId, baseExpression);
3556
3982
  const expression = compileExpr(expr);
3557
- const exprId = this.generateId("expr");
3983
+ const exprId = generateId("expr");
3558
3984
  this.define(exprId, expression);
3559
3985
  const fullExpression = `${bExprId}[${exprId}]`;
3560
3986
  let idx;
@@ -3563,7 +3989,7 @@ class CodeGenerator {
3563
3989
  attrs[`block-attribute-${idx}`] = specialInitTargetAttr;
3564
3990
  }
3565
3991
  else if (hasDynamicChildren) {
3566
- const bValueId = this.generateId("bValue");
3992
+ const bValueId = generateId("bValue");
3567
3993
  tModelSelectedExpr = `${bValueId}`;
3568
3994
  this.define(tModelSelectedExpr, fullExpression);
3569
3995
  }
@@ -3765,7 +4191,7 @@ class CodeGenerator {
3765
4191
  let id;
3766
4192
  if (ast.memo) {
3767
4193
  this.target.hasCache = true;
3768
- id = this.generateId();
4194
+ id = generateId();
3769
4195
  this.define(`memo${id}`, compileExpr(ast.memo));
3770
4196
  this.define(`vnode${id}`, `cache[key${this.target.loopLevel}];`);
3771
4197
  this.addLine(`if (vnode${id}) {`);
@@ -3794,7 +4220,7 @@ class CodeGenerator {
3794
4220
  this.insertBlock("l", block, ctx);
3795
4221
  }
3796
4222
  compileTKey(ast, ctx) {
3797
- const tKeyExpr = this.generateId("tKey_");
4223
+ const tKeyExpr = generateId("tKey_");
3798
4224
  this.define(tKeyExpr, compileExpr(ast.expr));
3799
4225
  ctx = createContext(ctx, {
3800
4226
  tKeyExpr,
@@ -3877,19 +4303,20 @@ class CodeGenerator {
3877
4303
  }
3878
4304
  const key = `key + \`${this.generateComponentKey()}\``;
3879
4305
  if (isDynamic) {
3880
- const templateVar = this.generateId("template");
4306
+ const templateVar = generateId("template");
4307
+ if (!this.staticDefs.find((d) => d.id === "call")) {
4308
+ this.staticDefs.push({ id: "call", expr: `app.callTemplate.bind(app)` });
4309
+ }
3881
4310
  this.define(templateVar, subTemplate);
3882
4311
  block = this.createBlock(block, "multi", ctx);
3883
- this.helpers.add("call");
3884
4312
  this.insertBlock(`call(this, ${templateVar}, ctx, node, ${key})`, block, {
3885
4313
  ...ctx,
3886
4314
  forceNewBlock: !block,
3887
4315
  });
3888
4316
  }
3889
4317
  else {
3890
- const id = this.generateId(`callTemplate_`);
3891
- this.helpers.add("getTemplate");
3892
- this.staticDefs.push({ id, expr: `getTemplate(${subTemplate})` });
4318
+ const id = generateId(`callTemplate_`);
4319
+ this.staticDefs.push({ id, expr: `app.getTemplate(${subTemplate})` });
3893
4320
  block = this.createBlock(block, "multi", ctx);
3894
4321
  this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block, {
3895
4322
  ...ctx,
@@ -3940,7 +4367,7 @@ class CodeGenerator {
3940
4367
  }
3941
4368
  }
3942
4369
  generateComponentKey() {
3943
- const parts = [this.generateId("__")];
4370
+ const parts = [generateId("__")];
3944
4371
  for (let i = 0; i < this.target.loopLevel; i++) {
3945
4372
  parts.push(`\${key${i + 1}}`);
3946
4373
  }
@@ -3994,7 +4421,7 @@ class CodeGenerator {
3994
4421
  if (ast.slots) {
3995
4422
  let ctxStr = "ctx";
3996
4423
  if (this.target.loopLevel || !this.hasSafeContext) {
3997
- ctxStr = this.generateId("ctx");
4424
+ ctxStr = generateId("ctx");
3998
4425
  this.helpers.add("capture");
3999
4426
  this.define(ctxStr, `capture(ctx)`);
4000
4427
  }
@@ -4029,7 +4456,7 @@ class CodeGenerator {
4029
4456
  }
4030
4457
  let propVar;
4031
4458
  if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) {
4032
- propVar = this.generateId("props");
4459
+ propVar = generateId("props");
4033
4460
  this.define(propVar, propString);
4034
4461
  propString = propVar;
4035
4462
  }
@@ -4041,7 +4468,7 @@ class CodeGenerator {
4041
4468
  const key = this.generateComponentKey();
4042
4469
  let expr;
4043
4470
  if (ast.isDynamic) {
4044
- expr = this.generateId("Comp");
4471
+ expr = generateId("Comp");
4045
4472
  this.define(expr, compileExpr(ast.name));
4046
4473
  }
4047
4474
  else {
@@ -4072,11 +4499,11 @@ class CodeGenerator {
4072
4499
  }
4073
4500
  wrapWithEventCatcher(expr, on) {
4074
4501
  this.helpers.add("createCatcher");
4075
- let name = this.generateId("catcher");
4502
+ let name = generateId("catcher");
4076
4503
  let spec = {};
4077
4504
  let handlers = [];
4078
4505
  for (let ev in on) {
4079
- let handlerId = this.generateId("hdlr");
4506
+ let handlerId = generateId("hdlr");
4080
4507
  let idx = handlers.push(handlerId) - 1;
4081
4508
  spec[ev] = idx;
4082
4509
  const handler = this.generateHandlerCode(ev, on[ev]);
@@ -4105,7 +4532,7 @@ class CodeGenerator {
4105
4532
  }
4106
4533
  else {
4107
4534
  if (dynamic) {
4108
- let name = this.generateId("slot");
4535
+ let name = generateId("slot");
4109
4536
  this.define(name, slotName);
4110
4537
  blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}, ${dynamic}, ${scope}))`;
4111
4538
  }
@@ -4121,1140 +4548,743 @@ class CodeGenerator {
4121
4548
  this.insertAnchor(block);
4122
4549
  }
4123
4550
  block = this.createBlock(block, "multi", ctx);
4124
- this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
4125
- }
4126
- compileTTranslation(ast, ctx) {
4127
- if (ast.content) {
4128
- this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));
4129
- }
4130
- }
4131
- compileTPortal(ast, ctx) {
4132
- this.helpers.add("Portal");
4133
- let { block } = ctx;
4134
- const name = this.compileInNewTarget("slot", ast.content, ctx);
4135
- const key = this.generateComponentKey();
4136
- let ctxStr = "ctx";
4137
- if (this.target.loopLevel || !this.hasSafeContext) {
4138
- ctxStr = this.generateId("ctx");
4139
- this.helpers.add("capture");
4140
- this.define(ctxStr, `capture(ctx);`);
4141
- }
4142
- const blockString = `component(Portal, {target: ${ast.target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx)`;
4143
- if (block) {
4144
- this.insertAnchor(block);
4145
- }
4146
- block = this.createBlock(block, "multi", ctx);
4147
- this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
4148
- }
4149
- }
4150
-
4151
- // -----------------------------------------------------------------------------
4152
- // AST Type definition
4153
- // -----------------------------------------------------------------------------
4154
- // -----------------------------------------------------------------------------
4155
- // Parser
4156
- // -----------------------------------------------------------------------------
4157
- const cache = new WeakMap();
4158
- function parse(xml) {
4159
- if (typeof xml === "string") {
4160
- const elem = parseXML$1(`<t>${xml}</t>`).firstChild;
4161
- return _parse(elem);
4162
- }
4163
- let ast = cache.get(xml);
4164
- if (!ast) {
4165
- // we clone here the xml to prevent modifying it in place
4166
- ast = _parse(xml.cloneNode(true));
4167
- cache.set(xml, ast);
4168
- }
4169
- return ast;
4170
- }
4171
- function _parse(xml) {
4172
- normalizeXML(xml);
4173
- const ctx = { inPreTag: false, inSVG: false };
4174
- return parseNode(xml, ctx) || { type: 0 /* Text */, value: "" };
4175
- }
4176
- function parseNode(node, ctx) {
4177
- if (!(node instanceof Element)) {
4178
- return parseTextCommentNode(node, ctx);
4179
- }
4180
- return (parseTDebugLog(node, ctx) ||
4181
- parseTForEach(node, ctx) ||
4182
- parseTIf(node, ctx) ||
4183
- parseTPortal(node, ctx) ||
4184
- parseTCall(node, ctx) ||
4185
- parseTCallBlock(node) ||
4186
- parseTEscNode(node, ctx) ||
4187
- parseTKey(node, ctx) ||
4188
- parseTTranslation(node, ctx) ||
4189
- parseTSlot(node, ctx) ||
4190
- parseTOutNode(node, ctx) ||
4191
- parseComponent(node, ctx) ||
4192
- parseDOMNode(node, ctx) ||
4193
- parseTSetNode(node, ctx) ||
4194
- parseTNode(node, ctx));
4195
- }
4196
- // -----------------------------------------------------------------------------
4197
- // <t /> tag
4198
- // -----------------------------------------------------------------------------
4199
- function parseTNode(node, ctx) {
4200
- if (node.tagName !== "t") {
4201
- return null;
4202
- }
4203
- return parseChildNodes(node, ctx);
4204
- }
4205
- // -----------------------------------------------------------------------------
4206
- // Text and Comment Nodes
4207
- // -----------------------------------------------------------------------------
4208
- const lineBreakRE = /[\r\n]/;
4209
- const whitespaceRE = /\s+/g;
4210
- function parseTextCommentNode(node, ctx) {
4211
- if (node.nodeType === Node.TEXT_NODE) {
4212
- let value = node.textContent || "";
4213
- if (!ctx.inPreTag) {
4214
- if (lineBreakRE.test(value) && !value.trim()) {
4215
- return null;
4216
- }
4217
- value = value.replace(whitespaceRE, " ");
4218
- }
4219
- return { type: 0 /* Text */, value };
4220
- }
4221
- else if (node.nodeType === Node.COMMENT_NODE) {
4222
- return { type: 1 /* Comment */, value: node.textContent || "" };
4223
- }
4224
- return null;
4225
- }
4226
- // -----------------------------------------------------------------------------
4227
- // debugging
4228
- // -----------------------------------------------------------------------------
4229
- function parseTDebugLog(node, ctx) {
4230
- if (node.hasAttribute("t-debug")) {
4231
- node.removeAttribute("t-debug");
4232
- return {
4233
- type: 12 /* TDebug */,
4234
- content: parseNode(node, ctx),
4235
- };
4236
- }
4237
- if (node.hasAttribute("t-log")) {
4238
- const expr = node.getAttribute("t-log");
4239
- node.removeAttribute("t-log");
4240
- return {
4241
- type: 13 /* TLog */,
4242
- expr,
4243
- content: parseNode(node, ctx),
4244
- };
4245
- }
4246
- return null;
4247
- }
4248
- // -----------------------------------------------------------------------------
4249
- // Regular dom node
4250
- // -----------------------------------------------------------------------------
4251
- const hasDotAtTheEnd = /\.[\w_]+\s*$/;
4252
- const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
4253
- const ROOT_SVG_TAGS = new Set(["svg", "g", "path"]);
4254
- function parseDOMNode(node, ctx) {
4255
- const { tagName } = node;
4256
- const dynamicTag = node.getAttribute("t-tag");
4257
- node.removeAttribute("t-tag");
4258
- if (tagName === "t" && !dynamicTag) {
4259
- return null;
4260
- }
4261
- if (tagName.startsWith("block-")) {
4262
- throw new Error(`Invalid tag name: '${tagName}'`);
4263
- }
4264
- ctx = Object.assign({}, ctx);
4265
- if (tagName === "pre") {
4266
- ctx.inPreTag = true;
4267
- }
4268
- const shouldAddSVGNS = ROOT_SVG_TAGS.has(tagName) && !ctx.inSVG;
4269
- ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
4270
- const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
4271
- const ref = node.getAttribute("t-ref");
4272
- node.removeAttribute("t-ref");
4273
- const nodeAttrsNames = node.getAttributeNames();
4274
- let attrs = null;
4275
- let on = null;
4276
- let model = null;
4277
- for (let attr of nodeAttrsNames) {
4278
- const value = node.getAttribute(attr);
4279
- if (attr.startsWith("t-on")) {
4280
- if (attr === "t-on") {
4281
- throw new Error("Missing event name with t-on directive");
4282
- }
4283
- on = on || {};
4284
- on[attr.slice(5)] = value;
4285
- }
4286
- else if (attr.startsWith("t-model")) {
4287
- if (!["input", "select", "textarea"].includes(tagName)) {
4288
- throw new Error("The t-model directive only works with <input>, <textarea> and <select>");
4289
- }
4290
- let baseExpr, expr;
4291
- if (hasDotAtTheEnd.test(value)) {
4292
- const index = value.lastIndexOf(".");
4293
- baseExpr = value.slice(0, index);
4294
- expr = `'${value.slice(index + 1)}'`;
4295
- }
4296
- else if (hasBracketsAtTheEnd.test(value)) {
4297
- const index = value.lastIndexOf("[");
4298
- baseExpr = value.slice(0, index);
4299
- expr = value.slice(index + 1, -1);
4300
- }
4301
- else {
4302
- throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
4303
- }
4304
- const typeAttr = node.getAttribute("type");
4305
- const isInput = tagName === "input";
4306
- const isSelect = tagName === "select";
4307
- const isTextarea = tagName === "textarea";
4308
- const isCheckboxInput = isInput && typeAttr === "checkbox";
4309
- const isRadioInput = isInput && typeAttr === "radio";
4310
- const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
4311
- const hasLazyMod = attr.includes(".lazy");
4312
- const hasNumberMod = attr.includes(".number");
4313
- const hasTrimMod = attr.includes(".trim");
4314
- const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
4315
- model = {
4316
- baseExpr,
4317
- expr,
4318
- targetAttr: isCheckboxInput ? "checked" : "value",
4319
- specialInitTargetAttr: isRadioInput ? "checked" : null,
4320
- eventType,
4321
- hasDynamicChildren: false,
4322
- shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
4323
- shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
4324
- };
4325
- if (isSelect) {
4326
- // don't pollute the original ctx
4327
- ctx = Object.assign({}, ctx);
4328
- ctx.tModelInfo = model;
4329
- }
4330
- }
4331
- else if (attr.startsWith("block-")) {
4332
- throw new Error(`Invalid attribute: '${attr}'`);
4333
- }
4334
- else if (attr !== "t-name") {
4335
- if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
4336
- throw new Error(`Unknown QWeb directive: '${attr}'`);
4337
- }
4338
- const tModel = ctx.tModelInfo;
4339
- if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
4340
- tModel.hasDynamicChildren = true;
4341
- }
4342
- attrs = attrs || {};
4343
- attrs[attr] = value;
4344
- }
4345
- }
4346
- const children = parseChildren(node, ctx);
4347
- return {
4348
- type: 2 /* DomNode */,
4349
- tag: tagName,
4350
- dynamicTag,
4351
- attrs,
4352
- on,
4353
- ref,
4354
- content: children,
4355
- model,
4356
- ns,
4357
- };
4358
- }
4359
- // -----------------------------------------------------------------------------
4360
- // t-esc
4361
- // -----------------------------------------------------------------------------
4362
- function parseTEscNode(node, ctx) {
4363
- if (!node.hasAttribute("t-esc")) {
4364
- return null;
4365
- }
4366
- const escValue = node.getAttribute("t-esc");
4367
- node.removeAttribute("t-esc");
4368
- const tesc = {
4369
- type: 4 /* TEsc */,
4370
- expr: escValue,
4371
- defaultValue: node.textContent || "",
4372
- };
4373
- let ref = node.getAttribute("t-ref");
4374
- node.removeAttribute("t-ref");
4375
- const ast = parseNode(node, ctx);
4376
- if (!ast) {
4377
- return tesc;
4551
+ this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
4378
4552
  }
4379
- if (ast.type === 2 /* DomNode */) {
4380
- return {
4381
- ...ast,
4382
- ref,
4383
- content: [tesc],
4384
- };
4553
+ compileTTranslation(ast, ctx) {
4554
+ if (ast.content) {
4555
+ this.compileAST(ast.content, Object.assign({}, ctx, { translate: false }));
4556
+ }
4385
4557
  }
4386
- if (ast.type === 11 /* TComponent */) {
4387
- throw new Error("t-esc is not supported on Component nodes");
4558
+ compileTPortal(ast, ctx) {
4559
+ if (!this.staticDefs.find((d) => d.id === "Portal")) {
4560
+ this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
4561
+ }
4562
+ let { block } = ctx;
4563
+ const name = this.compileInNewTarget("slot", ast.content, ctx);
4564
+ const key = this.generateComponentKey();
4565
+ let ctxStr = "ctx";
4566
+ if (this.target.loopLevel || !this.hasSafeContext) {
4567
+ ctxStr = generateId("ctx");
4568
+ this.helpers.add("capture");
4569
+ this.define(ctxStr, `capture(ctx);`);
4570
+ }
4571
+ const target = compileExpr(ast.target);
4572
+ const blockString = `component(Portal, {target: ${target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx)`;
4573
+ if (block) {
4574
+ this.insertAnchor(block);
4575
+ }
4576
+ block = this.createBlock(block, "multi", ctx);
4577
+ this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false });
4388
4578
  }
4389
- return tesc;
4390
- }
4579
+ }
4580
+
4391
4581
  // -----------------------------------------------------------------------------
4392
- // t-out
4582
+ // AST Type definition
4393
4583
  // -----------------------------------------------------------------------------
4394
- function parseTOutNode(node, ctx) {
4395
- if (!node.hasAttribute("t-out") && !node.hasAttribute("t-raw")) {
4396
- return null;
4397
- }
4398
- if (node.hasAttribute("t-raw")) {
4399
- 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`);
4400
- }
4401
- const expr = (node.getAttribute("t-out") || node.getAttribute("t-raw"));
4402
- node.removeAttribute("t-out");
4403
- node.removeAttribute("t-raw");
4404
- const tOut = { type: 8 /* TOut */, expr, body: null };
4405
- const ref = node.getAttribute("t-ref");
4406
- node.removeAttribute("t-ref");
4407
- const ast = parseNode(node, ctx);
4408
- if (!ast) {
4409
- return tOut;
4410
- }
4411
- if (ast.type === 2 /* DomNode */) {
4412
- tOut.body = ast.content.length ? ast.content : null;
4413
- return {
4414
- ...ast,
4415
- ref,
4416
- content: [tOut],
4417
- };
4418
- }
4419
- return tOut;
4420
- }
4421
4584
  // -----------------------------------------------------------------------------
4422
- // t-foreach and t-key
4585
+ // Parser
4423
4586
  // -----------------------------------------------------------------------------
4424
- function parseTForEach(node, ctx) {
4425
- if (!node.hasAttribute("t-foreach")) {
4426
- return null;
4427
- }
4428
- const html = node.outerHTML;
4429
- const collection = node.getAttribute("t-foreach");
4430
- node.removeAttribute("t-foreach");
4431
- const elem = node.getAttribute("t-as") || "";
4432
- node.removeAttribute("t-as");
4433
- const key = node.getAttribute("t-key");
4434
- if (!key) {
4435
- throw new Error(`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`);
4587
+ const cache = new WeakMap();
4588
+ function parse(xml) {
4589
+ if (typeof xml === "string") {
4590
+ const elem = parseXML(`<t>${xml}</t>`).firstChild;
4591
+ return _parse(elem);
4436
4592
  }
4437
- node.removeAttribute("t-key");
4438
- const memo = node.getAttribute("t-memo") || "";
4439
- node.removeAttribute("t-memo");
4440
- const body = parseNode(node, ctx);
4441
- if (!body) {
4442
- return null;
4593
+ let ast = cache.get(xml);
4594
+ if (!ast) {
4595
+ // we clone here the xml to prevent modifying it in place
4596
+ ast = _parse(xml.cloneNode(true));
4597
+ cache.set(xml, ast);
4443
4598
  }
4444
- const hasNoTCall = !html.includes("t-call");
4445
- const hasNoFirst = hasNoTCall && !html.includes(`${elem}_first`);
4446
- const hasNoLast = hasNoTCall && !html.includes(`${elem}_last`);
4447
- const hasNoIndex = hasNoTCall && !html.includes(`${elem}_index`);
4448
- const hasNoValue = hasNoTCall && !html.includes(`${elem}_value`);
4449
- return {
4450
- type: 9 /* TForEach */,
4451
- collection,
4452
- elem,
4453
- body,
4454
- memo,
4455
- key,
4456
- hasNoFirst,
4457
- hasNoLast,
4458
- hasNoIndex,
4459
- hasNoValue,
4460
- };
4599
+ return ast;
4461
4600
  }
4462
- function parseTKey(node, ctx) {
4463
- if (!node.hasAttribute("t-key")) {
4464
- return null;
4465
- }
4466
- const key = node.getAttribute("t-key");
4467
- node.removeAttribute("t-key");
4468
- const body = parseNode(node, ctx);
4469
- if (!body) {
4470
- return null;
4471
- }
4472
- return { type: 10 /* TKey */, expr: key, content: body };
4601
+ function _parse(xml) {
4602
+ normalizeXML(xml);
4603
+ const ctx = { inPreTag: false, inSVG: false };
4604
+ return parseNode(xml, ctx) || { type: 0 /* Text */, value: "" };
4473
4605
  }
4474
- // -----------------------------------------------------------------------------
4475
- // t-call
4476
- // -----------------------------------------------------------------------------
4477
- function parseTCall(node, ctx) {
4478
- if (!node.hasAttribute("t-call")) {
4479
- return null;
4480
- }
4481
- const subTemplate = node.getAttribute("t-call");
4482
- node.removeAttribute("t-call");
4483
- if (node.tagName !== "t") {
4484
- const ast = parseNode(node, ctx);
4485
- const tcall = { type: 7 /* TCall */, name: subTemplate, body: null };
4486
- if (ast && ast.type === 2 /* DomNode */) {
4487
- ast.content = [tcall];
4488
- return ast;
4489
- }
4490
- if (ast && ast.type === 11 /* TComponent */) {
4491
- return {
4492
- ...ast,
4493
- slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
4494
- };
4495
- }
4606
+ function parseNode(node, ctx) {
4607
+ if (!(node instanceof Element)) {
4608
+ return parseTextCommentNode(node, ctx);
4496
4609
  }
4497
- const body = parseChildren(node, ctx);
4498
- return {
4499
- type: 7 /* TCall */,
4500
- name: subTemplate,
4501
- body: body.length ? body : null,
4502
- };
4610
+ return (parseTDebugLog(node, ctx) ||
4611
+ parseTForEach(node, ctx) ||
4612
+ parseTIf(node, ctx) ||
4613
+ parseTPortal(node, ctx) ||
4614
+ parseTCall(node, ctx) ||
4615
+ parseTCallBlock(node) ||
4616
+ parseTEscNode(node, ctx) ||
4617
+ parseTKey(node, ctx) ||
4618
+ parseTTranslation(node, ctx) ||
4619
+ parseTSlot(node, ctx) ||
4620
+ parseTOutNode(node, ctx) ||
4621
+ parseComponent(node, ctx) ||
4622
+ parseDOMNode(node, ctx) ||
4623
+ parseTSetNode(node, ctx) ||
4624
+ parseTNode(node, ctx));
4503
4625
  }
4504
4626
  // -----------------------------------------------------------------------------
4505
- // t-call-block
4627
+ // <t /> tag
4506
4628
  // -----------------------------------------------------------------------------
4507
- function parseTCallBlock(node, ctx) {
4508
- if (!node.hasAttribute("t-call-block")) {
4629
+ function parseTNode(node, ctx) {
4630
+ if (node.tagName !== "t") {
4509
4631
  return null;
4510
4632
  }
4511
- const name = node.getAttribute("t-call-block");
4512
- return {
4513
- type: 15 /* TCallBlock */,
4514
- name,
4515
- };
4633
+ return parseChildNodes(node, ctx);
4516
4634
  }
4517
4635
  // -----------------------------------------------------------------------------
4518
- // t-if
4636
+ // Text and Comment Nodes
4519
4637
  // -----------------------------------------------------------------------------
4520
- function parseTIf(node, ctx) {
4521
- if (!node.hasAttribute("t-if")) {
4522
- return null;
4523
- }
4524
- const condition = node.getAttribute("t-if");
4525
- node.removeAttribute("t-if");
4526
- const content = parseNode(node, ctx) || { type: 0 /* Text */, value: "" };
4527
- let nextElement = node.nextElementSibling;
4528
- // t-elifs
4529
- const tElifs = [];
4530
- while (nextElement && nextElement.hasAttribute("t-elif")) {
4531
- const condition = nextElement.getAttribute("t-elif");
4532
- nextElement.removeAttribute("t-elif");
4533
- const tElif = parseNode(nextElement, ctx);
4534
- const next = nextElement.nextElementSibling;
4535
- nextElement.remove();
4536
- nextElement = next;
4537
- if (tElif) {
4538
- tElifs.push({ condition, content: tElif });
4638
+ const lineBreakRE = /[\r\n]/;
4639
+ const whitespaceRE = /\s+/g;
4640
+ function parseTextCommentNode(node, ctx) {
4641
+ if (node.nodeType === Node.TEXT_NODE) {
4642
+ let value = node.textContent || "";
4643
+ if (!ctx.inPreTag) {
4644
+ if (lineBreakRE.test(value) && !value.trim()) {
4645
+ return null;
4646
+ }
4647
+ value = value.replace(whitespaceRE, " ");
4539
4648
  }
4649
+ return { type: 0 /* Text */, value };
4540
4650
  }
4541
- // t-else
4542
- let tElse = null;
4543
- if (nextElement && nextElement.hasAttribute("t-else")) {
4544
- nextElement.removeAttribute("t-else");
4545
- tElse = parseNode(nextElement, ctx);
4546
- nextElement.remove();
4651
+ else if (node.nodeType === Node.COMMENT_NODE) {
4652
+ return { type: 1 /* Comment */, value: node.textContent || "" };
4547
4653
  }
4548
- return {
4549
- type: 5 /* TIf */,
4550
- condition,
4551
- content,
4552
- tElif: tElifs.length ? tElifs : null,
4553
- tElse,
4554
- };
4654
+ return null;
4555
4655
  }
4556
4656
  // -----------------------------------------------------------------------------
4557
- // t-set directive
4657
+ // debugging
4558
4658
  // -----------------------------------------------------------------------------
4559
- function parseTSetNode(node, ctx) {
4560
- if (!node.hasAttribute("t-set")) {
4561
- return null;
4659
+ function parseTDebugLog(node, ctx) {
4660
+ if (node.hasAttribute("t-debug")) {
4661
+ node.removeAttribute("t-debug");
4662
+ return {
4663
+ type: 12 /* TDebug */,
4664
+ content: parseNode(node, ctx),
4665
+ };
4562
4666
  }
4563
- const name = node.getAttribute("t-set");
4564
- const value = node.getAttribute("t-value") || null;
4565
- const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
4566
- let body = null;
4567
- if (node.textContent !== node.innerHTML) {
4568
- body = parseChildren(node, ctx);
4667
+ if (node.hasAttribute("t-log")) {
4668
+ const expr = node.getAttribute("t-log");
4669
+ node.removeAttribute("t-log");
4670
+ return {
4671
+ type: 13 /* TLog */,
4672
+ expr,
4673
+ content: parseNode(node, ctx),
4674
+ };
4569
4675
  }
4570
- return { type: 6 /* TSet */, name, value, defaultValue, body };
4676
+ return null;
4571
4677
  }
4572
4678
  // -----------------------------------------------------------------------------
4573
- // Components
4679
+ // Regular dom node
4574
4680
  // -----------------------------------------------------------------------------
4575
- // Error messages when trying to use an unsupported directive on a component
4576
- const directiveErrorMap = new Map([
4577
- [
4578
- "t-ref",
4579
- "t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
4580
- ],
4581
- ["t-att", "t-att makes no sense on component: props are already treated as expressions"],
4582
- [
4583
- "t-attf",
4584
- "t-attf is not supported on components: use template strings for string interpolation in props",
4585
- ],
4586
- ]);
4587
- function parseComponent(node, ctx) {
4588
- let name = node.tagName;
4589
- const firstLetter = name[0];
4590
- let isDynamic = node.hasAttribute("t-component");
4591
- if (isDynamic && name !== "t") {
4592
- throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
4593
- }
4594
- if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
4681
+ const hasDotAtTheEnd = /\.[\w_]+\s*$/;
4682
+ const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
4683
+ const ROOT_SVG_TAGS = new Set(["svg", "g", "path"]);
4684
+ function parseDOMNode(node, ctx) {
4685
+ const { tagName } = node;
4686
+ const dynamicTag = node.getAttribute("t-tag");
4687
+ node.removeAttribute("t-tag");
4688
+ if (tagName === "t" && !dynamicTag) {
4595
4689
  return null;
4596
4690
  }
4597
- if (isDynamic) {
4598
- name = node.getAttribute("t-component");
4599
- node.removeAttribute("t-component");
4691
+ if (tagName.startsWith("block-")) {
4692
+ throw new Error(`Invalid tag name: '${tagName}'`);
4600
4693
  }
4601
- const dynamicProps = node.getAttribute("t-props");
4602
- node.removeAttribute("t-props");
4603
- const defaultSlotScope = node.getAttribute("t-slot-scope");
4604
- node.removeAttribute("t-slot-scope");
4694
+ ctx = Object.assign({}, ctx);
4695
+ if (tagName === "pre") {
4696
+ ctx.inPreTag = true;
4697
+ }
4698
+ const shouldAddSVGNS = ROOT_SVG_TAGS.has(tagName) && !ctx.inSVG;
4699
+ ctx.inSVG = ctx.inSVG || shouldAddSVGNS;
4700
+ const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null;
4701
+ const ref = node.getAttribute("t-ref");
4702
+ node.removeAttribute("t-ref");
4703
+ const nodeAttrsNames = node.getAttributeNames();
4704
+ let attrs = null;
4605
4705
  let on = null;
4606
- let props = null;
4607
- for (let name of node.getAttributeNames()) {
4608
- const value = node.getAttribute(name);
4609
- if (name.startsWith("t-")) {
4610
- if (name.startsWith("t-on-")) {
4611
- on = on || {};
4612
- on[name.slice(5)] = value;
4613
- }
4614
- else {
4615
- const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
4616
- throw new Error(message || `unsupported directive on Component: ${name}`);
4706
+ let model = null;
4707
+ for (let attr of nodeAttrsNames) {
4708
+ const value = node.getAttribute(attr);
4709
+ if (attr.startsWith("t-on")) {
4710
+ if (attr === "t-on") {
4711
+ throw new Error("Missing event name with t-on directive");
4617
4712
  }
4713
+ on = on || {};
4714
+ on[attr.slice(5)] = value;
4618
4715
  }
4619
- else {
4620
- props = props || {};
4621
- props[name] = value;
4622
- }
4623
- }
4624
- let slots = null;
4625
- if (node.hasChildNodes()) {
4626
- const clone = node.cloneNode(true);
4627
- // named slots
4628
- const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
4629
- for (let slotNode of slotNodes) {
4630
- if (slotNode.tagName !== "t") {
4631
- throw new Error(`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`);
4716
+ else if (attr.startsWith("t-model")) {
4717
+ if (!["input", "select", "textarea"].includes(tagName)) {
4718
+ throw new Error("The t-model directive only works with <input>, <textarea> and <select>");
4632
4719
  }
4633
- const name = slotNode.getAttribute("t-set-slot");
4634
- // check if this is defined in a sub component (in which case it should
4635
- // be ignored)
4636
- let el = slotNode.parentElement;
4637
- let isInSubComponent = false;
4638
- while (el !== clone) {
4639
- if (el.hasAttribute("t-component") || el.tagName[0] === el.tagName[0].toUpperCase()) {
4640
- isInSubComponent = true;
4641
- break;
4642
- }
4643
- el = el.parentElement;
4720
+ let baseExpr, expr;
4721
+ if (hasDotAtTheEnd.test(value)) {
4722
+ const index = value.lastIndexOf(".");
4723
+ baseExpr = value.slice(0, index);
4724
+ expr = `'${value.slice(index + 1)}'`;
4644
4725
  }
4645
- if (isInSubComponent) {
4646
- continue;
4726
+ else if (hasBracketsAtTheEnd.test(value)) {
4727
+ const index = value.lastIndexOf("[");
4728
+ baseExpr = value.slice(0, index);
4729
+ expr = value.slice(index + 1, -1);
4647
4730
  }
4648
- slotNode.removeAttribute("t-set-slot");
4649
- slotNode.remove();
4650
- const slotAst = parseNode(slotNode, ctx);
4651
- let on = null;
4652
- let attrs = null;
4653
- let scope = null;
4654
- for (let attributeName of slotNode.getAttributeNames()) {
4655
- const value = slotNode.getAttribute(attributeName);
4656
- if (attributeName === "t-slot-scope") {
4657
- scope = value;
4658
- continue;
4659
- }
4660
- else if (attributeName.startsWith("t-on-")) {
4661
- on = on || {};
4662
- on[attributeName.slice(5)] = value;
4663
- }
4664
- else {
4665
- attrs = attrs || {};
4666
- attrs[attributeName] = value;
4667
- }
4731
+ else {
4732
+ throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
4733
+ }
4734
+ const typeAttr = node.getAttribute("type");
4735
+ const isInput = tagName === "input";
4736
+ const isSelect = tagName === "select";
4737
+ const isTextarea = tagName === "textarea";
4738
+ const isCheckboxInput = isInput && typeAttr === "checkbox";
4739
+ const isRadioInput = isInput && typeAttr === "radio";
4740
+ const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
4741
+ const hasLazyMod = attr.includes(".lazy");
4742
+ const hasNumberMod = attr.includes(".number");
4743
+ const hasTrimMod = attr.includes(".trim");
4744
+ const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
4745
+ model = {
4746
+ baseExpr,
4747
+ expr,
4748
+ targetAttr: isCheckboxInput ? "checked" : "value",
4749
+ specialInitTargetAttr: isRadioInput ? "checked" : null,
4750
+ eventType,
4751
+ hasDynamicChildren: false,
4752
+ shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
4753
+ shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
4754
+ };
4755
+ if (isSelect) {
4756
+ // don't pollute the original ctx
4757
+ ctx = Object.assign({}, ctx);
4758
+ ctx.tModelInfo = model;
4668
4759
  }
4669
- slots = slots || {};
4670
- slots[name] = { content: slotAst, on, attrs, scope };
4671
- }
4672
- // default slot
4673
- const defaultContent = parseChildNodes(clone, ctx);
4674
- if (defaultContent) {
4675
- slots = slots || {};
4676
- slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
4677
4760
  }
4678
- }
4679
- return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };
4680
- }
4681
- // -----------------------------------------------------------------------------
4682
- // Slots
4683
- // -----------------------------------------------------------------------------
4684
- function parseTSlot(node, ctx) {
4685
- if (!node.hasAttribute("t-slot")) {
4686
- return null;
4687
- }
4688
- const name = node.getAttribute("t-slot");
4689
- node.removeAttribute("t-slot");
4690
- let attrs = null;
4691
- let on = null;
4692
- for (let attributeName of node.getAttributeNames()) {
4693
- const value = node.getAttribute(attributeName);
4694
- if (attributeName.startsWith("t-on-")) {
4695
- on = on || {};
4696
- on[attributeName.slice(5)] = value;
4761
+ else if (attr.startsWith("block-")) {
4762
+ throw new Error(`Invalid attribute: '${attr}'`);
4697
4763
  }
4698
- else {
4764
+ else if (attr !== "t-name") {
4765
+ if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
4766
+ throw new Error(`Unknown QWeb directive: '${attr}'`);
4767
+ }
4768
+ const tModel = ctx.tModelInfo;
4769
+ if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
4770
+ tModel.hasDynamicChildren = true;
4771
+ }
4699
4772
  attrs = attrs || {};
4700
- attrs[attributeName] = value;
4773
+ attrs[attr] = value;
4701
4774
  }
4702
4775
  }
4776
+ const children = parseChildren(node, ctx);
4703
4777
  return {
4704
- type: 14 /* TSlot */,
4705
- name,
4778
+ type: 2 /* DomNode */,
4779
+ tag: tagName,
4780
+ dynamicTag,
4706
4781
  attrs,
4707
4782
  on,
4708
- defaultContent: parseChildNodes(node, ctx),
4783
+ ref,
4784
+ content: children,
4785
+ model,
4786
+ ns,
4709
4787
  };
4710
4788
  }
4711
- function parseTTranslation(node, ctx) {
4712
- if (node.getAttribute("t-translation") !== "off") {
4789
+ // -----------------------------------------------------------------------------
4790
+ // t-esc
4791
+ // -----------------------------------------------------------------------------
4792
+ function parseTEscNode(node, ctx) {
4793
+ if (!node.hasAttribute("t-esc")) {
4713
4794
  return null;
4714
4795
  }
4715
- node.removeAttribute("t-translation");
4716
- return {
4717
- type: 16 /* TTranslation */,
4718
- content: parseNode(node, ctx),
4796
+ const escValue = node.getAttribute("t-esc");
4797
+ node.removeAttribute("t-esc");
4798
+ const tesc = {
4799
+ type: 4 /* TEsc */,
4800
+ expr: escValue,
4801
+ defaultValue: node.textContent || "",
4719
4802
  };
4803
+ let ref = node.getAttribute("t-ref");
4804
+ node.removeAttribute("t-ref");
4805
+ const ast = parseNode(node, ctx);
4806
+ if (!ast) {
4807
+ return tesc;
4808
+ }
4809
+ if (ast.type === 2 /* DomNode */) {
4810
+ return {
4811
+ ...ast,
4812
+ ref,
4813
+ content: [tesc],
4814
+ };
4815
+ }
4816
+ if (ast.type === 11 /* TComponent */) {
4817
+ throw new Error("t-esc is not supported on Component nodes");
4818
+ }
4819
+ return tesc;
4720
4820
  }
4721
4821
  // -----------------------------------------------------------------------------
4722
- // Portal
4822
+ // t-out
4723
4823
  // -----------------------------------------------------------------------------
4724
- function parseTPortal(node, ctx) {
4725
- if (!node.hasAttribute("t-portal")) {
4824
+ function parseTOutNode(node, ctx) {
4825
+ if (!node.hasAttribute("t-out") && !node.hasAttribute("t-raw")) {
4726
4826
  return null;
4727
4827
  }
4728
- const target = node.getAttribute("t-portal");
4729
- node.removeAttribute("t-portal");
4730
- const content = parseNode(node, ctx);
4731
- if (!content) {
4828
+ if (node.hasAttribute("t-raw")) {
4829
+ 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`);
4830
+ }
4831
+ const expr = (node.getAttribute("t-out") || node.getAttribute("t-raw"));
4832
+ node.removeAttribute("t-out");
4833
+ node.removeAttribute("t-raw");
4834
+ const tOut = { type: 8 /* TOut */, expr, body: null };
4835
+ const ref = node.getAttribute("t-ref");
4836
+ node.removeAttribute("t-ref");
4837
+ const ast = parseNode(node, ctx);
4838
+ if (!ast) {
4839
+ return tOut;
4840
+ }
4841
+ if (ast.type === 2 /* DomNode */) {
4842
+ tOut.body = ast.content.length ? ast.content : null;
4732
4843
  return {
4733
- type: 0 /* Text */,
4734
- value: "",
4844
+ ...ast,
4845
+ ref,
4846
+ content: [tOut],
4735
4847
  };
4736
4848
  }
4737
- return {
4738
- type: 17 /* TPortal */,
4739
- target,
4740
- content,
4741
- };
4849
+ return tOut;
4742
4850
  }
4743
4851
  // -----------------------------------------------------------------------------
4744
- // helpers
4852
+ // t-foreach and t-key
4745
4853
  // -----------------------------------------------------------------------------
4746
- /**
4747
- * Parse all the child nodes of a given node and return a list of ast elements
4748
- */
4749
- function parseChildren(node, ctx) {
4750
- const children = [];
4751
- for (let child of node.childNodes) {
4752
- const childAst = parseNode(child, ctx);
4753
- if (childAst) {
4754
- if (childAst.type === 3 /* Multi */) {
4755
- children.push(...childAst.content);
4756
- }
4757
- else {
4758
- children.push(childAst);
4759
- }
4760
- }
4854
+ function parseTForEach(node, ctx) {
4855
+ if (!node.hasAttribute("t-foreach")) {
4856
+ return null;
4761
4857
  }
4762
- return children;
4763
- }
4764
- /**
4765
- * Parse all the child nodes of a given node and return an ast if possible.
4766
- * In the case there are multiple children, they are wrapped in a astmulti.
4767
- */
4768
- function parseChildNodes(node, ctx) {
4769
- const children = parseChildren(node, ctx);
4770
- switch (children.length) {
4771
- case 0:
4772
- return null;
4773
- case 1:
4774
- return children[0];
4775
- default:
4776
- return { type: 3 /* Multi */, content: children };
4858
+ const html = node.outerHTML;
4859
+ const collection = node.getAttribute("t-foreach");
4860
+ node.removeAttribute("t-foreach");
4861
+ const elem = node.getAttribute("t-as") || "";
4862
+ node.removeAttribute("t-as");
4863
+ const key = node.getAttribute("t-key");
4864
+ if (!key) {
4865
+ throw new Error(`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`);
4777
4866
  }
4778
- }
4779
- /**
4780
- * Normalizes the content of an Element so that t-if/t-elif/t-else directives
4781
- * immediately follow one another (by removing empty text nodes or comments).
4782
- * Throws an error when a conditional branching statement is malformed. This
4783
- * function modifies the Element in place.
4784
- *
4785
- * @param el the element containing the tree that should be normalized
4786
- */
4787
- function normalizeTIf(el) {
4788
- let tbranch = el.querySelectorAll("[t-elif], [t-else]");
4789
- for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
4790
- let node = tbranch[i];
4791
- let prevElem = node.previousElementSibling;
4792
- let pattr = (name) => prevElem.getAttribute(name);
4793
- let nattr = (name) => +!!node.getAttribute(name);
4794
- if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
4795
- if (pattr("t-foreach")) {
4796
- throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
4797
- }
4798
- if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
4799
- return a + b;
4800
- }) > 1) {
4801
- throw new Error("Only one conditional branching directive is allowed per node");
4802
- }
4803
- // All text (with only spaces) and comment nodes (nodeType 8) between
4804
- // branch nodes are removed
4805
- let textNode;
4806
- while ((textNode = node.previousSibling) !== prevElem) {
4807
- if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
4808
- throw new Error("text is not allowed between branching directives");
4809
- }
4810
- textNode.remove();
4811
- }
4812
- }
4813
- else {
4814
- throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
4815
- }
4867
+ node.removeAttribute("t-key");
4868
+ const memo = node.getAttribute("t-memo") || "";
4869
+ node.removeAttribute("t-memo");
4870
+ const body = parseNode(node, ctx);
4871
+ if (!body) {
4872
+ return null;
4816
4873
  }
4874
+ const hasNoTCall = !html.includes("t-call");
4875
+ const hasNoFirst = hasNoTCall && !html.includes(`${elem}_first`);
4876
+ const hasNoLast = hasNoTCall && !html.includes(`${elem}_last`);
4877
+ const hasNoIndex = hasNoTCall && !html.includes(`${elem}_index`);
4878
+ const hasNoValue = hasNoTCall && !html.includes(`${elem}_value`);
4879
+ return {
4880
+ type: 9 /* TForEach */,
4881
+ collection,
4882
+ elem,
4883
+ body,
4884
+ memo,
4885
+ key,
4886
+ hasNoFirst,
4887
+ hasNoLast,
4888
+ hasNoIndex,
4889
+ hasNoValue,
4890
+ };
4817
4891
  }
4818
- /**
4819
- * Normalizes the content of an Element so that t-esc directives on components
4820
- * are removed and instead places a <t t-esc=""> as the default slot of the
4821
- * component. Also throws if the component already has content. This function
4822
- * modifies the Element in place.
4823
- *
4824
- * @param el the element containing the tree that should be normalized
4825
- */
4826
- function normalizeTEsc(el) {
4827
- const elements = [...el.querySelectorAll("[t-esc]")].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
4828
- for (const el of elements) {
4829
- if (el.childNodes.length) {
4830
- throw new Error("Cannot have t-esc on a component that already has content");
4831
- }
4832
- const value = el.getAttribute("t-esc");
4833
- el.removeAttribute("t-esc");
4834
- const t = el.ownerDocument.createElement("t");
4835
- if (value != null) {
4836
- t.setAttribute("t-esc", value);
4837
- }
4838
- el.appendChild(t);
4892
+ function parseTKey(node, ctx) {
4893
+ if (!node.hasAttribute("t-key")) {
4894
+ return null;
4839
4895
  }
4896
+ const key = node.getAttribute("t-key");
4897
+ node.removeAttribute("t-key");
4898
+ const body = parseNode(node, ctx);
4899
+ if (!body) {
4900
+ return null;
4901
+ }
4902
+ return { type: 10 /* TKey */, expr: key, content: body };
4840
4903
  }
4841
- /**
4842
- * Normalizes the tree inside a given element and do some preliminary validation
4843
- * on it. This function modifies the Element in place.
4844
- *
4845
- * @param el the element containing the tree that should be normalized
4846
- */
4847
- function normalizeXML(el) {
4848
- normalizeTIf(el);
4849
- normalizeTEsc(el);
4850
- }
4851
- /**
4852
- * Parses an XML string into an XML document, throwing errors on parser errors
4853
- * instead of returning an XML document containing the parseerror.
4854
- *
4855
- * @param xml the string to parse
4856
- * @returns an XML document corresponding to the content of the string
4857
- */
4858
- function parseXML$1(xml) {
4859
- const parser = new DOMParser();
4860
- const doc = parser.parseFromString(xml, "text/xml");
4861
- if (doc.getElementsByTagName("parsererror").length) {
4862
- let msg = "Invalid XML in template.";
4863
- const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent;
4864
- if (parsererrorText) {
4865
- msg += "\nThe parser has produced the following error message:\n" + parsererrorText;
4866
- const re = /\d+/g;
4867
- const firstMatch = re.exec(parsererrorText);
4868
- if (firstMatch) {
4869
- const lineNumber = Number(firstMatch[0]);
4870
- const line = xml.split("\n")[lineNumber - 1];
4871
- const secondMatch = re.exec(parsererrorText);
4872
- if (line && secondMatch) {
4873
- const columnIndex = Number(secondMatch[0]) - 1;
4874
- if (line[columnIndex]) {
4875
- msg +=
4876
- `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` +
4877
- `${line}\n${"-".repeat(columnIndex - 1)}^`;
4878
- }
4879
- }
4880
- }
4881
- }
4882
- throw new Error(msg);
4904
+ // -----------------------------------------------------------------------------
4905
+ // t-call
4906
+ // -----------------------------------------------------------------------------
4907
+ function parseTCall(node, ctx) {
4908
+ if (!node.hasAttribute("t-call")) {
4909
+ return null;
4883
4910
  }
4884
- return doc;
4885
- }
4886
-
4887
- function compile(template, options = {}) {
4888
- // parsing
4889
- const ast = parse(template);
4890
- // some work
4891
- const hasSafeContext = template instanceof Node
4892
- ? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
4893
- : !template.includes("t-set") && !template.includes("t-call");
4894
- // code generation
4895
- const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
4896
- const code = codeGenerator.generateCode();
4897
- // template function
4898
- return new Function("bdom, helpers", code);
4899
- }
4900
-
4901
- const TIMEOUT = Symbol("timeout");
4902
- function wrapError(fn, hookName) {
4903
- const error = new Error(`The following error occurred in ${hookName}: `);
4904
- const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`);
4905
- const node = getCurrent();
4906
- return (...args) => {
4907
- try {
4908
- const result = fn(...args);
4909
- if (result instanceof Promise) {
4910
- if (hookName === "onWillStart" || hookName === "onWillUpdateProps") {
4911
- const fiber = node.fiber;
4912
- Promise.race([
4913
- result,
4914
- new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), 3000)),
4915
- ]).then((res) => {
4916
- if (res === TIMEOUT && node.fiber === fiber) {
4917
- console.warn(timeoutError);
4918
- }
4919
- });
4920
- }
4921
- return result.catch((cause) => {
4922
- error.cause = cause;
4923
- if (cause instanceof Error) {
4924
- error.message += `"${cause.message}"`;
4925
- }
4926
- throw error;
4927
- });
4928
- }
4929
- return result;
4911
+ const subTemplate = node.getAttribute("t-call");
4912
+ node.removeAttribute("t-call");
4913
+ if (node.tagName !== "t") {
4914
+ const ast = parseNode(node, ctx);
4915
+ const tcall = { type: 7 /* TCall */, name: subTemplate, body: null };
4916
+ if (ast && ast.type === 2 /* DomNode */) {
4917
+ ast.content = [tcall];
4918
+ return ast;
4930
4919
  }
4931
- catch (cause) {
4932
- if (cause instanceof Error) {
4933
- error.message += `"${cause.message}"`;
4934
- }
4935
- throw error;
4920
+ if (ast && ast.type === 11 /* TComponent */) {
4921
+ return {
4922
+ ...ast,
4923
+ slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
4924
+ };
4936
4925
  }
4926
+ }
4927
+ const body = parseChildren(node, ctx);
4928
+ return {
4929
+ type: 7 /* TCall */,
4930
+ name: subTemplate,
4931
+ body: body.length ? body : null,
4937
4932
  };
4938
4933
  }
4939
4934
  // -----------------------------------------------------------------------------
4940
- // hooks
4935
+ // t-call-block
4941
4936
  // -----------------------------------------------------------------------------
4942
- function onWillStart(fn) {
4943
- const node = getCurrent();
4944
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4945
- node.willStart.push(decorate(fn.bind(node.component), "onWillStart"));
4946
- }
4947
- function onWillUpdateProps(fn) {
4948
- const node = getCurrent();
4949
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4950
- node.willUpdateProps.push(decorate(fn.bind(node.component), "onWillUpdateProps"));
4951
- }
4952
- function onMounted(fn) {
4953
- const node = getCurrent();
4954
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4955
- node.mounted.push(decorate(fn.bind(node.component), "onMounted"));
4956
- }
4957
- function onWillPatch(fn) {
4958
- const node = getCurrent();
4959
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4960
- node.willPatch.unshift(decorate(fn.bind(node.component), "onWillPatch"));
4961
- }
4962
- function onPatched(fn) {
4963
- const node = getCurrent();
4964
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4965
- node.patched.push(decorate(fn.bind(node.component), "onPatched"));
4966
- }
4967
- function onWillUnmount(fn) {
4968
- const node = getCurrent();
4969
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4970
- node.willUnmount.unshift(decorate(fn.bind(node.component), "onWillUnmount"));
4971
- }
4972
- function onWillDestroy(fn) {
4973
- const node = getCurrent();
4974
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4975
- node.willDestroy.push(decorate(fn.bind(node.component), "onWillDestroy"));
4976
- }
4977
- function onWillRender(fn) {
4978
- const node = getCurrent();
4979
- const renderFn = node.renderFn;
4980
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4981
- fn = decorate(fn.bind(node.component), "onWillRender");
4982
- node.renderFn = () => {
4983
- fn();
4984
- return renderFn();
4937
+ function parseTCallBlock(node, ctx) {
4938
+ if (!node.hasAttribute("t-call-block")) {
4939
+ return null;
4940
+ }
4941
+ const name = node.getAttribute("t-call-block");
4942
+ return {
4943
+ type: 15 /* TCallBlock */,
4944
+ name,
4985
4945
  };
4986
4946
  }
4987
- function onRendered(fn) {
4988
- const node = getCurrent();
4989
- const renderFn = node.renderFn;
4990
- const decorate = node.app.dev ? wrapError : (fn) => fn;
4991
- fn = decorate(fn.bind(node.component), "onRendered");
4992
- node.renderFn = () => {
4993
- const result = renderFn();
4994
- fn();
4995
- return result;
4947
+ // -----------------------------------------------------------------------------
4948
+ // t-if
4949
+ // -----------------------------------------------------------------------------
4950
+ function parseTIf(node, ctx) {
4951
+ if (!node.hasAttribute("t-if")) {
4952
+ return null;
4953
+ }
4954
+ const condition = node.getAttribute("t-if");
4955
+ node.removeAttribute("t-if");
4956
+ const content = parseNode(node, ctx) || { type: 0 /* Text */, value: "" };
4957
+ let nextElement = node.nextElementSibling;
4958
+ // t-elifs
4959
+ const tElifs = [];
4960
+ while (nextElement && nextElement.hasAttribute("t-elif")) {
4961
+ const condition = nextElement.getAttribute("t-elif");
4962
+ nextElement.removeAttribute("t-elif");
4963
+ const tElif = parseNode(nextElement, ctx);
4964
+ const next = nextElement.nextElementSibling;
4965
+ nextElement.remove();
4966
+ nextElement = next;
4967
+ if (tElif) {
4968
+ tElifs.push({ condition, content: tElif });
4969
+ }
4970
+ }
4971
+ // t-else
4972
+ let tElse = null;
4973
+ if (nextElement && nextElement.hasAttribute("t-else")) {
4974
+ nextElement.removeAttribute("t-else");
4975
+ tElse = parseNode(nextElement, ctx);
4976
+ nextElement.remove();
4977
+ }
4978
+ return {
4979
+ type: 5 /* TIf */,
4980
+ condition,
4981
+ content,
4982
+ tElif: tElifs.length ? tElifs : null,
4983
+ tElse,
4996
4984
  };
4997
4985
  }
4998
- function onError(callback) {
4999
- const node = getCurrent();
5000
- let handlers = nodeErrorHandlers.get(node);
5001
- if (!handlers) {
5002
- handlers = [];
5003
- nodeErrorHandlers.set(node, handlers);
5004
- }
5005
- handlers.push(callback.bind(node.component));
5006
- }
5007
-
5008
- class Component {
5009
- constructor(props, env, node) {
5010
- this.props = props;
5011
- this.env = env;
5012
- this.__owl__ = node;
4986
+ // -----------------------------------------------------------------------------
4987
+ // t-set directive
4988
+ // -----------------------------------------------------------------------------
4989
+ function parseTSetNode(node, ctx) {
4990
+ if (!node.hasAttribute("t-set")) {
4991
+ return null;
5013
4992
  }
5014
- setup() { }
5015
- render(deep = false) {
5016
- this.__owl__.render(deep);
4993
+ const name = node.getAttribute("t-set");
4994
+ const value = node.getAttribute("t-value") || null;
4995
+ const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
4996
+ let body = null;
4997
+ if (node.textContent !== node.innerHTML) {
4998
+ body = parseChildren(node, ctx);
5017
4999
  }
5000
+ return { type: 6 /* TSet */, name, value, defaultValue, body };
5018
5001
  }
5019
- Component.template = "";
5020
-
5021
- const VText = text("").constructor;
5022
- class VPortal extends VText {
5023
- constructor(selector, realBDom) {
5024
- super("");
5025
- this.target = null;
5026
- this.selector = selector;
5027
- this.realBDom = realBDom;
5002
+ // -----------------------------------------------------------------------------
5003
+ // Components
5004
+ // -----------------------------------------------------------------------------
5005
+ // Error messages when trying to use an unsupported directive on a component
5006
+ const directiveErrorMap = new Map([
5007
+ [
5008
+ "t-ref",
5009
+ "t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop.",
5010
+ ],
5011
+ ["t-att", "t-att makes no sense on component: props are already treated as expressions"],
5012
+ [
5013
+ "t-attf",
5014
+ "t-attf is not supported on components: use template strings for string interpolation in props",
5015
+ ],
5016
+ ]);
5017
+ function parseComponent(node, ctx) {
5018
+ let name = node.tagName;
5019
+ const firstLetter = name[0];
5020
+ let isDynamic = node.hasAttribute("t-component");
5021
+ if (isDynamic && name !== "t") {
5022
+ throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
5028
5023
  }
5029
- mount(parent, anchor) {
5030
- super.mount(parent, anchor);
5031
- this.target = document.querySelector(this.selector);
5032
- if (!this.target) {
5033
- let el = this.el;
5034
- while (el && el.parentElement instanceof HTMLElement) {
5035
- el = el.parentElement;
5024
+ if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
5025
+ return null;
5026
+ }
5027
+ if (isDynamic) {
5028
+ name = node.getAttribute("t-component");
5029
+ node.removeAttribute("t-component");
5030
+ }
5031
+ const dynamicProps = node.getAttribute("t-props");
5032
+ node.removeAttribute("t-props");
5033
+ const defaultSlotScope = node.getAttribute("t-slot-scope");
5034
+ node.removeAttribute("t-slot-scope");
5035
+ let on = null;
5036
+ let props = null;
5037
+ for (let name of node.getAttributeNames()) {
5038
+ const value = node.getAttribute(name);
5039
+ if (name.startsWith("t-")) {
5040
+ if (name.startsWith("t-on-")) {
5041
+ on = on || {};
5042
+ on[name.slice(5)] = value;
5036
5043
  }
5037
- this.target = el && el.querySelector(this.selector);
5038
- if (!this.target) {
5039
- throw new Error("invalid portal target");
5044
+ else {
5045
+ const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
5046
+ throw new Error(message || `unsupported directive on Component: ${name}`);
5040
5047
  }
5041
5048
  }
5042
- this.realBDom.mount(this.target, null);
5043
- }
5044
- beforeRemove() {
5045
- this.realBDom.beforeRemove();
5046
- }
5047
- remove() {
5048
- if (this.realBDom) {
5049
- super.remove();
5050
- this.realBDom.remove();
5051
- this.realBDom = null;
5049
+ else {
5050
+ props = props || {};
5051
+ props[name] = value;
5052
5052
  }
5053
5053
  }
5054
- patch(other) {
5055
- super.patch(other);
5056
- if (this.realBDom) {
5057
- this.realBDom.patch(other.realBDom, true);
5054
+ let slots = null;
5055
+ if (node.hasChildNodes()) {
5056
+ const clone = node.cloneNode(true);
5057
+ // named slots
5058
+ const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
5059
+ for (let slotNode of slotNodes) {
5060
+ if (slotNode.tagName !== "t") {
5061
+ throw new Error(`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`);
5062
+ }
5063
+ const name = slotNode.getAttribute("t-set-slot");
5064
+ // check if this is defined in a sub component (in which case it should
5065
+ // be ignored)
5066
+ let el = slotNode.parentElement;
5067
+ let isInSubComponent = false;
5068
+ while (el !== clone) {
5069
+ if (el.hasAttribute("t-component") || el.tagName[0] === el.tagName[0].toUpperCase()) {
5070
+ isInSubComponent = true;
5071
+ break;
5072
+ }
5073
+ el = el.parentElement;
5074
+ }
5075
+ if (isInSubComponent) {
5076
+ continue;
5077
+ }
5078
+ slotNode.removeAttribute("t-set-slot");
5079
+ slotNode.remove();
5080
+ const slotAst = parseNode(slotNode, ctx);
5081
+ let on = null;
5082
+ let attrs = null;
5083
+ let scope = null;
5084
+ for (let attributeName of slotNode.getAttributeNames()) {
5085
+ const value = slotNode.getAttribute(attributeName);
5086
+ if (attributeName === "t-slot-scope") {
5087
+ scope = value;
5088
+ continue;
5089
+ }
5090
+ else if (attributeName.startsWith("t-on-")) {
5091
+ on = on || {};
5092
+ on[attributeName.slice(5)] = value;
5093
+ }
5094
+ else {
5095
+ attrs = attrs || {};
5096
+ attrs[attributeName] = value;
5097
+ }
5098
+ }
5099
+ slots = slots || {};
5100
+ slots[name] = { content: slotAst, on, attrs, scope };
5058
5101
  }
5059
- else {
5060
- this.realBDom = other.realBDom;
5061
- this.realBDom.mount(this.target, null);
5102
+ // default slot
5103
+ const defaultContent = parseChildNodes(clone, ctx);
5104
+ if (defaultContent) {
5105
+ slots = slots || {};
5106
+ slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
5062
5107
  }
5063
5108
  }
5109
+ return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };
5064
5110
  }
5065
- class Portal extends Component {
5066
- setup() {
5067
- const node = this.__owl__;
5068
- const renderFn = node.renderFn;
5069
- node.renderFn = () => new VPortal(this.props.target, renderFn());
5070
- onWillUnmount(() => {
5071
- if (node.bdom) {
5072
- node.bdom.remove();
5073
- }
5074
- });
5075
- }
5076
- }
5077
- Portal.template = xml `<t t-slot="default"/>`;
5078
- Portal.props = {
5079
- target: {
5080
- type: String,
5081
- },
5082
- slots: true,
5083
- };
5084
-
5085
- /**
5086
- * This file contains utility functions that will be injected in each template,
5087
- * to perform various useful tasks in the compiled code.
5088
- */
5089
- function withDefault(value, defaultValue) {
5090
- return value === undefined || value === null || value === false ? defaultValue : value;
5091
- }
5092
- function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) {
5093
- key = key + "__slot_" + name;
5094
- const slots = ctx.props[TARGET].slots || {};
5095
- const { __render, __ctx, __scope } = slots[name] || {};
5096
- const slotScope = Object.create(__ctx || {});
5097
- if (__scope) {
5098
- slotScope[__scope] = extra;
5111
+ // -----------------------------------------------------------------------------
5112
+ // Slots
5113
+ // -----------------------------------------------------------------------------
5114
+ function parseTSlot(node, ctx) {
5115
+ if (!node.hasAttribute("t-slot")) {
5116
+ return null;
5099
5117
  }
5100
- const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null;
5101
- if (defaultContent) {
5102
- let child1 = undefined;
5103
- let child2 = undefined;
5104
- if (slotBDom) {
5105
- child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
5118
+ const name = node.getAttribute("t-slot");
5119
+ node.removeAttribute("t-slot");
5120
+ let attrs = null;
5121
+ let on = null;
5122
+ for (let attributeName of node.getAttributeNames()) {
5123
+ const value = node.getAttribute(attributeName);
5124
+ if (attributeName.startsWith("t-on-")) {
5125
+ on = on || {};
5126
+ on[attributeName.slice(5)] = value;
5106
5127
  }
5107
5128
  else {
5108
- child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key);
5129
+ attrs = attrs || {};
5130
+ attrs[attributeName] = value;
5109
5131
  }
5110
- return multi([child1, child2]);
5111
5132
  }
5112
- return slotBDom || text("");
5133
+ return {
5134
+ type: 14 /* TSlot */,
5135
+ name,
5136
+ attrs,
5137
+ on,
5138
+ defaultContent: parseChildNodes(node, ctx),
5139
+ };
5113
5140
  }
5114
- function capture(ctx) {
5115
- const component = ctx.__owl__.component;
5116
- const result = Object.create(component);
5117
- for (let k in ctx) {
5118
- result[k] = ctx[k];
5141
+ function parseTTranslation(node, ctx) {
5142
+ if (node.getAttribute("t-translation") !== "off") {
5143
+ return null;
5119
5144
  }
5120
- return result;
5121
- }
5122
- function withKey(elem, k) {
5123
- elem.key = k;
5124
- return elem;
5145
+ node.removeAttribute("t-translation");
5146
+ return {
5147
+ type: 16 /* TTranslation */,
5148
+ content: parseNode(node, ctx),
5149
+ };
5125
5150
  }
5126
- function prepareList(collection) {
5127
- let keys;
5128
- let values;
5129
- if (Array.isArray(collection)) {
5130
- keys = collection;
5131
- values = collection;
5132
- }
5133
- else if (collection) {
5134
- values = Object.keys(collection);
5135
- keys = Object.values(collection);
5136
- }
5137
- else {
5138
- throw new Error("Invalid loop expression");
5151
+ // -----------------------------------------------------------------------------
5152
+ // Portal
5153
+ // -----------------------------------------------------------------------------
5154
+ function parseTPortal(node, ctx) {
5155
+ if (!node.hasAttribute("t-portal")) {
5156
+ return null;
5139
5157
  }
5140
- const n = values.length;
5141
- return [keys, values, n, new Array(n)];
5142
- }
5143
- const isBoundary = Symbol("isBoundary");
5144
- function setContextValue(ctx, key, value) {
5145
- const ctx0 = ctx;
5146
- while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
5147
- const newCtx = ctx.__proto__;
5148
- if (!newCtx) {
5149
- ctx = ctx0;
5150
- break;
5151
- }
5152
- ctx = newCtx;
5158
+ const target = node.getAttribute("t-portal");
5159
+ node.removeAttribute("t-portal");
5160
+ const content = parseNode(node, ctx);
5161
+ if (!content) {
5162
+ return {
5163
+ type: 0 /* Text */,
5164
+ value: "",
5165
+ };
5153
5166
  }
5154
- ctx[key] = value;
5155
- }
5156
- function toNumber(val) {
5157
- const n = parseFloat(val);
5158
- return isNaN(n) ? val : n;
5167
+ return {
5168
+ type: 17 /* TPortal */,
5169
+ target,
5170
+ content,
5171
+ };
5159
5172
  }
5160
- function shallowEqual(l1, l2) {
5161
- for (let i = 0, l = l1.length; i < l; i++) {
5162
- if (l1[i] !== l2[i]) {
5163
- return false;
5173
+ // -----------------------------------------------------------------------------
5174
+ // helpers
5175
+ // -----------------------------------------------------------------------------
5176
+ /**
5177
+ * Parse all the child nodes of a given node and return a list of ast elements
5178
+ */
5179
+ function parseChildren(node, ctx) {
5180
+ const children = [];
5181
+ for (let child of node.childNodes) {
5182
+ const childAst = parseNode(child, ctx);
5183
+ if (childAst) {
5184
+ if (childAst.type === 3 /* Multi */) {
5185
+ children.push(...childAst.content);
5186
+ }
5187
+ else {
5188
+ children.push(childAst);
5189
+ }
5164
5190
  }
5165
5191
  }
5166
- return true;
5167
- }
5168
- class LazyValue {
5169
- constructor(fn, ctx, node) {
5170
- this.fn = fn;
5171
- this.ctx = capture(ctx);
5172
- this.node = node;
5173
- }
5174
- evaluate() {
5175
- return this.fn(this.ctx, this.node);
5176
- }
5177
- toString() {
5178
- return this.evaluate().toString();
5179
- }
5192
+ return children;
5180
5193
  }
5181
- /*
5182
- * Safely outputs `value` as a block depending on the nature of `value`
5194
+ /**
5195
+ * Parse all the child nodes of a given node and return an ast if possible.
5196
+ * In the case there are multiple children, they are wrapped in a astmulti.
5183
5197
  */
5184
- function safeOutput(value) {
5185
- if (!value) {
5186
- return value;
5187
- }
5188
- let safeKey;
5189
- let block;
5190
- if (value instanceof Markup) {
5191
- safeKey = `string_safe`;
5192
- block = html(value);
5193
- }
5194
- else if (value instanceof LazyValue) {
5195
- safeKey = `lazy_value`;
5196
- block = value.evaluate();
5197
- }
5198
- else if (value instanceof String || typeof value === "string") {
5199
- safeKey = "string_unsafe";
5200
- block = text(value);
5201
- }
5202
- else {
5203
- // Assuming it is a block
5204
- safeKey = "block_safe";
5205
- block = value;
5198
+ function parseChildNodes(node, ctx) {
5199
+ const children = parseChildren(node, ctx);
5200
+ switch (children.length) {
5201
+ case 0:
5202
+ return null;
5203
+ case 1:
5204
+ return children[0];
5205
+ default:
5206
+ return { type: 3 /* Multi */, content: children };
5206
5207
  }
5207
- return toggler(safeKey, block);
5208
5208
  }
5209
- let boundFunctions = new WeakMap();
5210
- function bind(ctx, fn) {
5211
- let component = ctx.__owl__.component;
5212
- let boundFnMap = boundFunctions.get(component);
5213
- if (!boundFnMap) {
5214
- boundFnMap = new WeakMap();
5215
- boundFunctions.set(component, boundFnMap);
5216
- }
5217
- let boundFn = boundFnMap.get(fn);
5218
- if (!boundFn) {
5219
- boundFn = fn.bind(component);
5220
- boundFnMap.set(fn, boundFn);
5209
+ /**
5210
+ * Normalizes the content of an Element so that t-if/t-elif/t-else directives
5211
+ * immediately follow one another (by removing empty text nodes or comments).
5212
+ * Throws an error when a conditional branching statement is malformed. This
5213
+ * function modifies the Element in place.
5214
+ *
5215
+ * @param el the element containing the tree that should be normalized
5216
+ */
5217
+ function normalizeTIf(el) {
5218
+ let tbranch = el.querySelectorAll("[t-elif], [t-else]");
5219
+ for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
5220
+ let node = tbranch[i];
5221
+ let prevElem = node.previousElementSibling;
5222
+ let pattr = (name) => prevElem.getAttribute(name);
5223
+ let nattr = (name) => +!!node.getAttribute(name);
5224
+ if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
5225
+ if (pattr("t-foreach")) {
5226
+ throw new Error("t-if cannot stay at the same level as t-foreach when using t-elif or t-else");
5227
+ }
5228
+ if (["t-if", "t-elif", "t-else"].map(nattr).reduce(function (a, b) {
5229
+ return a + b;
5230
+ }) > 1) {
5231
+ throw new Error("Only one conditional branching directive is allowed per node");
5232
+ }
5233
+ // All text (with only spaces) and comment nodes (nodeType 8) between
5234
+ // branch nodes are removed
5235
+ let textNode;
5236
+ while ((textNode = node.previousSibling) !== prevElem) {
5237
+ if (textNode.nodeValue.trim().length && textNode.nodeType !== 8) {
5238
+ throw new Error("text is not allowed between branching directives");
5239
+ }
5240
+ textNode.remove();
5241
+ }
5242
+ }
5243
+ else {
5244
+ throw new Error("t-elif and t-else directives must be preceded by a t-if or t-elif directive");
5245
+ }
5221
5246
  }
5222
- return boundFn;
5223
5247
  }
5224
- function multiRefSetter(refs, name) {
5225
- let count = 0;
5226
- return (el) => {
5227
- if (el) {
5228
- count++;
5229
- if (count > 1) {
5230
- throw new Error("Cannot have 2 elements with same ref name at the same time");
5231
- }
5248
+ /**
5249
+ * Normalizes the content of an Element so that t-esc directives on components
5250
+ * are removed and instead places a <t t-esc=""> as the default slot of the
5251
+ * component. Also throws if the component already has content. This function
5252
+ * modifies the Element in place.
5253
+ *
5254
+ * @param el the element containing the tree that should be normalized
5255
+ */
5256
+ function normalizeTEsc(el) {
5257
+ const elements = [...el.querySelectorAll("[t-esc]")].filter((el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component"));
5258
+ for (const el of elements) {
5259
+ if (el.childNodes.length) {
5260
+ throw new Error("Cannot have t-esc on a component that already has content");
5232
5261
  }
5233
- if (count === 0 || el) {
5234
- refs[name] = el;
5262
+ const value = el.getAttribute("t-esc");
5263
+ el.removeAttribute("t-esc");
5264
+ const t = el.ownerDocument.createElement("t");
5265
+ if (value != null) {
5266
+ t.setAttribute("t-esc", value);
5235
5267
  }
5236
- };
5268
+ el.appendChild(t);
5269
+ }
5237
5270
  }
5238
- const helpers = {
5239
- withDefault,
5240
- zero: Symbol("zero"),
5241
- isBoundary,
5242
- callSlot,
5243
- capture,
5244
- withKey,
5245
- prepareList,
5246
- setContextValue,
5247
- multiRefSetter,
5248
- shallowEqual,
5249
- toNumber,
5250
- validateProps,
5251
- LazyValue,
5252
- safeOutput,
5253
- bind,
5254
- createCatcher,
5255
- };
5256
-
5257
- const bdom = { text, createBlock, list, multi, html, toggler, component, comment };
5271
+ /**
5272
+ * Normalizes the tree inside a given element and do some preliminary validation
5273
+ * on it. This function modifies the Element in place.
5274
+ *
5275
+ * @param el the element containing the tree that should be normalized
5276
+ */
5277
+ function normalizeXML(el) {
5278
+ normalizeTIf(el);
5279
+ normalizeTEsc(el);
5280
+ }
5281
+ /**
5282
+ * Parses an XML string into an XML document, throwing errors on parser errors
5283
+ * instead of returning an XML document containing the parseerror.
5284
+ *
5285
+ * @param xml the string to parse
5286
+ * @returns an XML document corresponding to the content of the string
5287
+ */
5258
5288
  function parseXML(xml) {
5259
5289
  const parser = new DOMParser();
5260
5290
  const doc = parser.parseFromString(xml, "text/xml");
@@ -5282,84 +5312,131 @@ function parseXML(xml) {
5282
5312
  throw new Error(msg);
5283
5313
  }
5284
5314
  return doc;
5285
- }
5286
- /**
5287
- * Returns the helpers object that will be injected in each template closure
5288
- * function
5289
- */
5290
- function makeHelpers(getTemplate) {
5291
- return Object.assign({}, helpers, {
5292
- Portal,
5293
- markRaw,
5294
- getTemplate,
5295
- call: (owner, subTemplate, ctx, parent, key) => {
5296
- const template = getTemplate(subTemplate);
5297
- return toggler(subTemplate, template.call(owner, ctx, parent, key));
5298
- },
5299
- });
5300
- }
5301
- class TemplateSet {
5302
- constructor(config = {}) {
5303
- this.rawTemplates = Object.create(globalTemplates);
5304
- this.templates = {};
5305
- this.dev = config.dev || false;
5306
- this.translateFn = config.translateFn;
5307
- this.translatableAttributes = config.translatableAttributes;
5308
- if (config.templates) {
5309
- this.addTemplates(config.templates);
5310
- }
5311
- this.helpers = makeHelpers(this.getTemplate.bind(this));
5312
- }
5313
- addTemplate(name, template, options = {}) {
5314
- if (name in this.rawTemplates && !options.allowDuplicate) {
5315
- throw new Error(`Template ${name} already defined`);
5315
+ }
5316
+
5317
+ function compile(template, options = {}) {
5318
+ // parsing
5319
+ const ast = parse(template);
5320
+ // some work
5321
+ const hasSafeContext = template instanceof Node
5322
+ ? !(template instanceof Element) || template.querySelector("[t-set], [t-call]") === null
5323
+ : !template.includes("t-set") && !template.includes("t-call");
5324
+ // code generation
5325
+ const codeGenerator = new CodeGenerator(ast, { ...options, hasSafeContext });
5326
+ const code = codeGenerator.generateCode();
5327
+ // template function
5328
+ return new Function("app, bdom, helpers", code);
5329
+ }
5330
+
5331
+ const mainEventHandler = (data, ev, currentTarget) => {
5332
+ const { data: _data, modifiers } = filterOutModifiersFromData(data);
5333
+ data = _data;
5334
+ let stopped = false;
5335
+ if (modifiers.length) {
5336
+ let selfMode = false;
5337
+ const isSelf = ev.target === currentTarget;
5338
+ for (const mod of modifiers) {
5339
+ switch (mod) {
5340
+ case "self":
5341
+ selfMode = true;
5342
+ if (isSelf) {
5343
+ continue;
5344
+ }
5345
+ else {
5346
+ return stopped;
5347
+ }
5348
+ case "prevent":
5349
+ if ((selfMode && isSelf) || !selfMode)
5350
+ ev.preventDefault();
5351
+ continue;
5352
+ case "stop":
5353
+ if ((selfMode && isSelf) || !selfMode)
5354
+ ev.stopPropagation();
5355
+ stopped = true;
5356
+ continue;
5357
+ }
5316
5358
  }
5317
- this.rawTemplates[name] = template;
5318
5359
  }
5319
- addTemplates(xml, options = {}) {
5320
- if (!xml) {
5321
- // empty string
5322
- return;
5360
+ // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0
5361
+ // We check this rather than data[0] being truthy (or typeof function) so that it crashes
5362
+ // as expected when there is a handler expression that evaluates to a falsy value
5363
+ if (Object.hasOwnProperty.call(data, 0)) {
5364
+ const handler = data[0];
5365
+ if (typeof handler !== "function") {
5366
+ throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
5323
5367
  }
5324
- xml = xml instanceof Document ? xml : parseXML(xml);
5325
- for (const template of xml.querySelectorAll("[t-name]")) {
5326
- const name = template.getAttribute("t-name");
5327
- this.addTemplate(name, template, options);
5368
+ let node = data[1] ? data[1].__owl__ : null;
5369
+ if (node ? node.status === 1 /* MOUNTED */ : true) {
5370
+ handler.call(node ? node.component : null, ev);
5328
5371
  }
5329
5372
  }
5330
- getTemplate(name) {
5331
- if (!(name in this.templates)) {
5332
- const rawTemplate = this.rawTemplates[name];
5333
- if (rawTemplate === undefined) {
5334
- let extraInfo = "";
5335
- try {
5336
- const componentName = getCurrent().component.constructor.name;
5337
- extraInfo = ` (for component "${componentName}")`;
5373
+ return stopped;
5374
+ };
5375
+
5376
+ // -----------------------------------------------------------------------------
5377
+ // Scheduler
5378
+ // -----------------------------------------------------------------------------
5379
+ class Scheduler {
5380
+ constructor() {
5381
+ this.tasks = new Set();
5382
+ this.frame = 0;
5383
+ this.delayedRenders = [];
5384
+ this.requestAnimationFrame = Scheduler.requestAnimationFrame;
5385
+ }
5386
+ addFiber(fiber) {
5387
+ this.tasks.add(fiber.root);
5388
+ }
5389
+ /**
5390
+ * Process all current tasks. This only applies to the fibers that are ready.
5391
+ * Other tasks are left unchanged.
5392
+ */
5393
+ flush() {
5394
+ if (this.delayedRenders.length) {
5395
+ let renders = this.delayedRenders;
5396
+ this.delayedRenders = [];
5397
+ for (let f of renders) {
5398
+ if (f.root && f.node.status !== 2 /* DESTROYED */ && f.node.fiber === f) {
5399
+ f.render();
5338
5400
  }
5339
- catch { }
5340
- throw new Error(`Missing template: "${name}"${extraInfo}`);
5341
5401
  }
5342
- const templateFn = this._compileTemplate(name, rawTemplate);
5343
- // first add a function to lazily get the template, in case there is a
5344
- // recursive call to the template name
5345
- const templates = this.templates;
5346
- this.templates[name] = function (context, parent) {
5347
- return templates[name].call(this, context, parent);
5348
- };
5349
- const template = templateFn(bdom, this.helpers);
5350
- this.templates[name] = template;
5351
5402
  }
5352
- return this.templates[name];
5403
+ if (this.frame === 0) {
5404
+ this.frame = this.requestAnimationFrame(() => {
5405
+ this.frame = 0;
5406
+ this.tasks.forEach((fiber) => this.processFiber(fiber));
5407
+ for (let task of this.tasks) {
5408
+ if (task.node.status === 2 /* DESTROYED */) {
5409
+ this.tasks.delete(task);
5410
+ }
5411
+ }
5412
+ });
5413
+ }
5353
5414
  }
5354
- _compileTemplate(name, template) {
5355
- return compile(template, {
5356
- name,
5357
- dev: this.dev,
5358
- translateFn: this.translateFn,
5359
- translatableAttributes: this.translatableAttributes,
5360
- });
5415
+ processFiber(fiber) {
5416
+ if (fiber.root !== fiber) {
5417
+ this.tasks.delete(fiber);
5418
+ return;
5419
+ }
5420
+ const hasError = fibersInError.has(fiber);
5421
+ if (hasError && fiber.counter !== 0) {
5422
+ this.tasks.delete(fiber);
5423
+ return;
5424
+ }
5425
+ if (fiber.node.status === 2 /* DESTROYED */) {
5426
+ this.tasks.delete(fiber);
5427
+ return;
5428
+ }
5429
+ if (fiber.counter === 0) {
5430
+ if (!hasError) {
5431
+ fiber.complete();
5432
+ }
5433
+ this.tasks.delete(fiber);
5434
+ }
5361
5435
  }
5362
- }
5436
+ }
5437
+ // capture the value of requestAnimationFrame as soon as possible, to avoid
5438
+ // interactions with other code, such as test frameworks that override them
5439
+ Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
5363
5440
 
5364
5441
  let hasBeenLogged = false;
5365
5442
  const DEV_MSG = () => {
@@ -5378,6 +5455,7 @@ class App extends TemplateSet {
5378
5455
  if (config.test) {
5379
5456
  this.dev = true;
5380
5457
  }
5458
+ this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
5381
5459
  if (this.dev && !config.test && !hasBeenLogged) {
5382
5460
  console.info(DEV_MSG());
5383
5461
  hasBeenLogged = true;
@@ -5389,6 +5467,9 @@ class App extends TemplateSet {
5389
5467
  }
5390
5468
  mount(target, options) {
5391
5469
  App.validateTarget(target);
5470
+ if (this.dev) {
5471
+ validateProps(this.Root, this.props, { __owl__: { app: this } });
5472
+ }
5392
5473
  const node = this.makeNode(this.Root, this.props);
5393
5474
  const prom = this.mountNode(node, target, options);
5394
5475
  this.root = node;
@@ -5567,6 +5648,15 @@ const blockDom = {
5567
5648
  };
5568
5649
  const __info__ = {};
5569
5650
 
5651
+ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, template) {
5652
+ return compile(template, {
5653
+ name,
5654
+ dev: this.dev,
5655
+ translateFn: this.translateFn,
5656
+ translatableAttributes: this.translatableAttributes,
5657
+ });
5658
+ };
5659
+
5570
5660
  exports.App = App;
5571
5661
  exports.Component = Component;
5572
5662
  exports.EventBus = EventBus;
@@ -5597,11 +5687,12 @@ exports.useExternalListener = useExternalListener;
5597
5687
  exports.useRef = useRef;
5598
5688
  exports.useState = useState;
5599
5689
  exports.useSubEnv = useSubEnv;
5690
+ exports.validate = validate;
5600
5691
  exports.whenReady = whenReady;
5601
5692
  exports.xml = xml;
5602
5693
 
5603
5694
 
5604
- __info__.version = '2.0.0-beta-5';
5605
- __info__.date = '2022-04-07T13:36:37.300Z';
5606
- __info__.hash = '1179e84';
5695
+ __info__.version = '2.0.0-beta-8';
5696
+ __info__.date = '2022-05-31T12:25:19.319Z';
5697
+ __info__.hash = 'b56a9c2';
5607
5698
  __info__.url = 'https://github.com/odoo/owl';