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