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