diff --git a/owl.js b/owl.js index 1621e595..b2ba3273 100644 --- a/owl.js +++ b/owl.js @@ -1,158 +1,2267 @@ (function (exports) { 'use strict'; + function filterOutModifiersFromData(dataList) { + dataList = dataList.slice(); + const modifiers = []; + let elm; + while ((elm = dataList[0]) && typeof elm === "string") { + modifiers.push(dataList.shift()); + } + return { modifiers, data: dataList }; + } + const config = { + // whether or not blockdom should normalize DOM whenever a block is created. + // Normalizing dom mean removing empty text nodes (or containing only spaces) + shouldNormalizeDom: true, + // this is the main event handler. Every event handler registered with blockdom + // will go through this function, giving it the data registered in the block + // and the event + mainEventHandler: (data, ev, currentTarget) => { + if (typeof data === "function") { + data(ev); + } + else if (Array.isArray(data)) { + data = filterOutModifiersFromData(data).data; + data[0](data[1], ev); + } + return false; + }, + }; + + // ----------------------------------------------------------------------------- + // Toggler node + // ----------------------------------------------------------------------------- + class VToggler { + constructor(key, child) { + this.key = key; + this.child = child; + } + mount(parent, afterNode) { + this.parentEl = parent; + this.child.mount(parent, afterNode); + } + moveBefore(other, afterNode) { + this.child.moveBefore(other ? other.child : null, afterNode); + } + patch(other, withBeforeRemove) { + if (this === other) { + return; + } + let child1 = this.child; + let child2 = other.child; + if (this.key === other.key) { + child1.patch(child2, withBeforeRemove); + } + else { + child2.mount(this.parentEl, child1.firstNode()); + if (withBeforeRemove) { + child1.beforeRemove(); + } + child1.remove(); + this.child = child2; + this.key = other.key; + } + } + beforeRemove() { + this.child.beforeRemove(); + } + remove() { + this.child.remove(); + } + firstNode() { + return this.child.firstNode(); + } + toString() { + return this.child.toString(); + } + } + function toggler(key, child) { + return new VToggler(key, child); + } + + const { setAttribute: elemSetAttribute, removeAttribute } = Element.prototype; + const tokenList = DOMTokenList.prototype; + const tokenListAdd = tokenList.add; + const tokenListRemove = tokenList.remove; + const isArray = Array.isArray; + const { split, trim } = String.prototype; + const wordRegexp = /\s+/; /** - * We define here a simple event bus: it can - * - emit events - * - add/remove listeners. - * - * This is a useful pattern of communication in many cases. For OWL, each - * components and stores are event buses. + * We regroup here all code related to updating attributes in a very loose sense: + * attributes, properties and classs are all managed by the functions in this + * file. */ - //------------------------------------------------------------------------------ - // EventBus - //------------------------------------------------------------------------------ - class EventBus { - constructor() { - this.subscriptions = {}; + function setAttribute(key, value) { + switch (value) { + case false: + case undefined: + removeAttribute.call(this, key); + break; + case true: + elemSetAttribute.call(this, key, ""); + break; + default: + elemSetAttribute.call(this, key, value); } - /** - * Add a listener for the 'eventType' events. - * - * Note that the 'owner' of this event can be anything, but will more likely - * be a component or a class. The idea is that the callback will be called with - * the proper owner bound. - * - * Also, the owner should be kind of unique. This will be used to remove the - * listener. - */ - on(eventType, owner, callback) { - if (!callback) { - throw new Error("Missing callback"); - } - if (!this.subscriptions[eventType]) { - this.subscriptions[eventType] = []; - } - this.subscriptions[eventType].push({ - owner, - callback, - }); + } + function createAttrUpdater(attr) { + return function (value) { + setAttribute.call(this, attr, value); + }; + } + function attrsSetter(attrs) { + if (isArray(attrs)) { + setAttribute.call(this, attrs[0], attrs[1]); } - /** - * Remove a listener - */ - off(eventType, owner) { - const subs = this.subscriptions[eventType]; - if (subs) { - this.subscriptions[eventType] = subs.filter((s) => s.owner !== owner); + else { + for (let k in attrs) { + setAttribute.call(this, k, attrs[k]); } } - /** - * Emit an event of type 'eventType'. Any extra arguments will be passed to - * the listeners callback. - */ - trigger(eventType, ...args) { - const subs = this.subscriptions[eventType] || []; - for (let i = 0, iLen = subs.length; i < iLen; i++) { - const sub = subs[i]; - sub.callback.call(sub.owner, ...args); + } + function attrsUpdater(attrs, oldAttrs) { + if (isArray(attrs)) { + const name = attrs[0]; + const val = attrs[1]; + if (name === oldAttrs[0]) { + if (val === oldAttrs[1]) { + return; + } + setAttribute.call(this, name, val); + } + else { + removeAttribute.call(this, oldAttrs[0]); + setAttribute.call(this, name, val); } } - /** - * Remove all subscriptions. - */ - clear() { - this.subscriptions = {}; + else { + for (let k in oldAttrs) { + if (!(k in attrs)) { + removeAttribute.call(this, k); + } + } + for (let k in attrs) { + const val = attrs[k]; + if (val !== oldAttrs[k]) { + setAttribute.call(this, k, val); + } + } + } + } + function toClassObj(expr) { + const result = {}; + switch (typeof expr) { + case "string": + // we transform here a list of classes into an object: + // 'hey you' becomes {hey: true, you: true} + const str = trim.call(expr); + if (!str) { + return {}; + } + let words = split.call(str, wordRegexp); + for (let i = 0, l = words.length; i < l; i++) { + result[words[i]] = true; + } + return result; + case "object": + // this is already an object but we may need to split keys: + // {'a': true, 'b c': true} should become {a: true, b: true, c: true} + for (let key in expr) { + const value = expr[key]; + if (value) { + const words = split.call(key, wordRegexp); + for (let word of words) { + result[word] = value; + } + } + } + return result; + case "undefined": + return {}; + case "number": + return { [expr]: true }; + default: + return { [expr]: true }; + } + } + function setClass(val) { + val = val === "" ? {} : toClassObj(val); + // add classes + const cl = this.classList; + for (let c in val) { + tokenListAdd.call(cl, c); + } + } + function updateClass(val, oldVal) { + oldVal = oldVal === "" ? {} : toClassObj(oldVal); + val = val === "" ? {} : toClassObj(val); + const cl = this.classList; + // remove classes + for (let c in oldVal) { + if (!(c in val)) { + tokenListRemove.call(cl, c); + } + } + // add classes + for (let c in val) { + if (!(c in oldVal)) { + tokenListAdd.call(cl, c); + } + } + } + function makePropSetter(name) { + return function setProp(value) { + this[name] = value; + }; + } + function isProp(tag, key) { + switch (tag) { + case "input": + return (key === "checked" || + key === "indeterminate" || + key === "value" || + key === "readonly" || + key === "disabled"); + case "option": + return key === "selected" || key === "disabled"; + case "textarea": + return key === "value" || key === "readonly" || key === "disabled"; + case "select": + return key === "value" || key === "disabled"; + case "button": + case "optgroup": + return key === "disabled"; + } + return false; + } + + function createEventHandler(rawEvent) { + const eventName = rawEvent.split(".")[0]; + const capture = rawEvent.includes(".capture"); + if (rawEvent.includes(".synthetic")) { + return createSyntheticHandler(eventName, capture); + } + else { + return createElementHandler(eventName, capture); + } + } + // Native listener + let nextNativeEventId = 1; + function createElementHandler(evName, capture = false) { + let eventKey = `__event__${evName}_${nextNativeEventId++}`; + if (capture) { + eventKey = `${eventKey}_capture`; + } + function listener(ev) { + const currentTarget = ev.currentTarget; + if (!currentTarget || !document.contains(currentTarget)) + return; + const data = currentTarget[eventKey]; + if (!data) + return; + config.mainEventHandler(data, ev, currentTarget); + } + function setup(data) { + this[eventKey] = data; + this.addEventListener(evName, listener, { capture }); + } + function update(data) { + this[eventKey] = data; + } + return { setup, update }; + } + // Synthetic handler: a form of event delegation that allows placing only one + // listener per event type. + let nextSyntheticEventId = 1; + function createSyntheticHandler(evName, capture = false) { + let eventKey = `__event__synthetic_${evName}`; + if (capture) { + eventKey = `${eventKey}_capture`; + } + setupSyntheticEvent(evName, eventKey, capture); + const currentId = nextSyntheticEventId++; + function setup(data) { + const _data = this[eventKey] || {}; + _data[currentId] = data; + this[eventKey] = _data; + } + return { setup, update: setup }; + } + function nativeToSyntheticEvent(eventKey, event) { + let dom = event.target; + while (dom !== null) { + const _data = dom[eventKey]; + if (_data) { + for (const data of Object.values(_data)) { + const stopped = config.mainEventHandler(data, event, dom); + if (stopped) + return; + } + } + dom = dom.parentNode; + } + } + const CONFIGURED_SYNTHETIC_EVENTS = {}; + function setupSyntheticEvent(evName, eventKey, capture = false) { + if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) { + return; + } + document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event), { + capture, + }); + CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true; + } + + const getDescriptor$3 = (o, p) => Object.getOwnPropertyDescriptor(o, p); + const nodeProto$4 = Node.prototype; + const nodeInsertBefore$3 = nodeProto$4.insertBefore; + const nodeSetTextContent$1 = getDescriptor$3(nodeProto$4, "textContent").set; + const nodeRemoveChild$3 = nodeProto$4.removeChild; + // ----------------------------------------------------------------------------- + // Multi NODE + // ----------------------------------------------------------------------------- + class VMulti { + constructor(children) { + this.children = children; + } + mount(parent, afterNode) { + const children = this.children; + const l = children.length; + const anchors = new Array(l); + for (let i = 0; i < l; i++) { + let child = children[i]; + if (child) { + child.mount(parent, afterNode); + } + else { + const childAnchor = document.createTextNode(""); + anchors[i] = childAnchor; + nodeInsertBefore$3.call(parent, childAnchor, afterNode); + } + } + this.anchors = anchors; + this.parentEl = parent; + } + moveBefore(other, afterNode) { + if (other) { + const next = other.children[0]; + afterNode = (next ? next.firstNode() : other.anchors[0]) || null; + } + const children = this.children; + const parent = this.parentEl; + const anchors = this.anchors; + for (let i = 0, l = children.length; i < l; i++) { + let child = children[i]; + if (child) { + child.moveBefore(null, afterNode); + } + else { + const anchor = anchors[i]; + nodeInsertBefore$3.call(parent, anchor, afterNode); + } + } + } + patch(other, withBeforeRemove) { + if (this === other) { + return; + } + const children1 = this.children; + const children2 = other.children; + const anchors = this.anchors; + const parentEl = this.parentEl; + for (let i = 0, l = children1.length; i < l; i++) { + const vn1 = children1[i]; + const vn2 = children2[i]; + if (vn1) { + if (vn2) { + vn1.patch(vn2, withBeforeRemove); + } + else { + const afterNode = vn1.firstNode(); + const anchor = document.createTextNode(""); + anchors[i] = anchor; + nodeInsertBefore$3.call(parentEl, anchor, afterNode); + if (withBeforeRemove) { + vn1.beforeRemove(); + } + vn1.remove(); + children1[i] = undefined; + } + } + else if (vn2) { + children1[i] = vn2; + const anchor = anchors[i]; + vn2.mount(parentEl, anchor); + nodeRemoveChild$3.call(parentEl, anchor); + } + } + } + beforeRemove() { + const children = this.children; + for (let i = 0, l = children.length; i < l; i++) { + const child = children[i]; + if (child) { + child.beforeRemove(); + } + } + } + remove() { + const parentEl = this.parentEl; + if (this.isOnlyChild) { + nodeSetTextContent$1.call(parentEl, ""); + } + else { + const children = this.children; + const anchors = this.anchors; + for (let i = 0, l = children.length; i < l; i++) { + const child = children[i]; + if (child) { + child.remove(); + } + else { + nodeRemoveChild$3.call(parentEl, anchors[i]); + } + } + } + } + firstNode() { + const child = this.children[0]; + return child ? child.firstNode() : this.anchors[0]; + } + toString() { + return this.children.map((c) => (c ? c.toString() : "")).join(""); + } + } + function multi(children) { + return new VMulti(children); + } + + const getDescriptor$2 = (o, p) => Object.getOwnPropertyDescriptor(o, p); + const nodeProto$3 = Node.prototype; + const characterDataProto$1 = CharacterData.prototype; + const nodeInsertBefore$2 = nodeProto$3.insertBefore; + const characterDataSetData$1 = getDescriptor$2(characterDataProto$1, "data").set; + const nodeRemoveChild$2 = nodeProto$3.removeChild; + class VSimpleNode { + constructor(text) { + this.text = text; + } + mountNode(node, parent, afterNode) { + this.parentEl = parent; + nodeInsertBefore$2.call(parent, node, afterNode); + this.el = node; + } + moveBefore(other, afterNode) { + const target = other ? other.el : afterNode; + nodeInsertBefore$2.call(this.parentEl, this.el, target); + } + beforeRemove() { } + remove() { + nodeRemoveChild$2.call(this.parentEl, this.el); + } + firstNode() { + return this.el; + } + toString() { + return this.text; + } + } + class VText$1 extends VSimpleNode { + mount(parent, afterNode) { + this.mountNode(document.createTextNode(toText(this.text)), parent, afterNode); + } + patch(other) { + const text2 = other.text; + if (this.text !== text2) { + characterDataSetData$1.call(this.el, toText(text2)); + this.text = text2; + } + } + } + class VComment extends VSimpleNode { + mount(parent, afterNode) { + this.mountNode(document.createComment(toText(this.text)), parent, afterNode); + } + patch() { } + } + function text(str) { + return new VText$1(str); + } + function comment(str) { + return new VComment(str); + } + function toText(value) { + switch (typeof value) { + case "string": + return value; + case "number": + return String(value); + case "boolean": + return value ? "true" : "false"; + default: + return value || ""; } } + const getDescriptor$1 = (o, p) => Object.getOwnPropertyDescriptor(o, p); + const nodeProto$2 = Node.prototype; + const elementProto = Element.prototype; + const characterDataProto = CharacterData.prototype; + const characterDataSetData = getDescriptor$1(characterDataProto, "data").set; + const nodeGetFirstChild = getDescriptor$1(nodeProto$2, "firstChild").get; + const nodeGetNextSibling = getDescriptor$1(nodeProto$2, "nextSibling").get; + const NO_OP$1 = () => { }; + const cache$1 = {}; /** - * Owl Observer + * Compiling blocks is a multi-step process: * - * This code contains the logic that allows Owl to observe and react to state - * changes. + * 1. build an IntermediateTree from the HTML element. This intermediate tree + * is a binary tree structure that encode dynamic info sub nodes, and the + * path required to reach them + * 2. process the tree to build a block context, which is an object that aggregate + * all dynamic info in a list, and also, all ref indexes. + * 3. process the context to build appropriate builder/setter functions + * 4. make a dynamic block class, which will efficiently collect references and + * create/update dynamic locations/children * - * This is a Observer class that can observe any JS values. The way it works - * can be summarized thusly: - * - primitive values are not observed at all - * - Objects and arrays are observed by replacing them with a Proxy - * - each object/array metadata are tracked in a weakmap, and keep a revision - * number - * - * Note that this code is loosely inspired by Vue. + * @param str + * @returns a new block type, that can build concrete blocks */ - //------------------------------------------------------------------------------ - // Observer - //------------------------------------------------------------------------------ - class Observer { - constructor() { - this.rev = 1; - this.allowMutations = true; - this.weakMap = new WeakMap(); + function createBlock(str) { + if (str in cache$1) { + return cache$1[str]; } - notifyCB() { } - observe(value, parent) { - if (value === null || - typeof value !== "object" || - value instanceof Date || - value instanceof Promise) { - // fun fact: typeof null === 'object' - return value; + // step 0: prepare html base element + const doc = new DOMParser().parseFromString(`${str}`, "text/xml"); + const node = doc.firstChild.firstChild; + if (config.shouldNormalizeDom) { + normalizeNode(node); + } + // step 1: prepare intermediate tree + const tree = buildTree(node); + // step 2: prepare block context + const context = buildContext(tree); + // step 3: build the final block class + const template = tree.el; + const Block = buildBlock(template, context); + cache$1[str] = Block; + return Block; + } + // ----------------------------------------------------------------------------- + // Helper + // ----------------------------------------------------------------------------- + function normalizeNode(node) { + if (node.nodeType === Node.TEXT_NODE) { + if (!/\S/.test(node.textContent)) { + node.remove(); + return; } - let metadata = this.weakMap.get(value) || this._observe(value, parent); - return metadata.proxy; } - revNumber(value) { - const metadata = this.weakMap.get(value); - return metadata ? metadata.rev : 0; + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.tagName === "pre") { + return; + } } - _observe(value, parent) { - var self = this; - const proxy = new Proxy(value, { - get(target, k) { - const targetValue = target[k]; - return self.observe(targetValue, value); - }, - set(target, key, newVal) { - const value = target[key]; - if (newVal !== value) { - if (!self.allowMutations) { - throw new Error(`Observed state cannot be changed here! (key: "${key}", val: "${newVal}")`); + for (let i = node.childNodes.length - 1; i >= 0; --i) { + normalizeNode(node.childNodes.item(i)); + } + } + function buildTree(node, parent = null, domParentTree = null) { + switch (node.nodeType) { + case Node.ELEMENT_NODE: { + // HTMLElement + let currentNS = domParentTree && domParentTree.currentNS; + const tagName = node.tagName; + let el = undefined; + const info = []; + if (tagName.startsWith("block-text-")) { + const index = parseInt(tagName.slice(11), 10); + info.push({ type: "text", idx: index }); + el = document.createTextNode(""); + } + if (tagName.startsWith("block-child-")) { + if (!domParentTree.isRef) { + addRef(domParentTree); + } + const index = parseInt(tagName.slice(12), 10); + info.push({ type: "child", idx: index }); + el = document.createTextNode(""); + } + const attrs = node.attributes; + const ns = attrs.getNamedItem("block-ns"); + if (ns) { + attrs.removeNamedItem("block-ns"); + currentNS = ns.value; + } + if (!el) { + el = currentNS + ? document.createElementNS(currentNS, tagName) + : document.createElement(tagName); + } + if (el instanceof Element) { + for (let i = 0; i < attrs.length; i++) { + const attrName = attrs[i].name; + const attrValue = attrs[i].value; + if (attrName.startsWith("block-handler-")) { + const idx = parseInt(attrName.slice(14), 10); + info.push({ + type: "handler", + idx, + event: attrValue, + }); + } + else if (attrName.startsWith("block-attribute-")) { + const idx = parseInt(attrName.slice(16), 10); + info.push({ + type: "attribute", + idx, + name: attrValue, + tag: tagName, + }); + } + else if (attrName === "block-attributes") { + info.push({ + type: "attributes", + idx: parseInt(attrValue, 10), + }); + } + else if (attrName === "block-ref") { + info.push({ + type: "ref", + idx: parseInt(attrValue, 10), + }); + } + else { + el.setAttribute(attrs[i].name, attrValue); } - self._updateRevNumber(target); - target[key] = newVal; - self.notifyCB(); } - return true; - }, - deleteProperty(target, key) { - if (key in target) { - delete target[key]; - self._updateRevNumber(target); - self.notifyCB(); + } + const tree = { + parent, + firstChild: null, + nextSibling: null, + el, + info, + refN: 0, + currentNS, + }; + if (node.firstChild) { + const childNode = node.childNodes[0]; + if (node.childNodes.length === 1 && + childNode.nodeType === Node.ELEMENT_NODE && + childNode.tagName.startsWith("block-child-")) { + const tagName = childNode.tagName; + const index = parseInt(tagName.slice(12), 10); + info.push({ idx: index, type: "child", isOnlyChild: true }); } - return true; - }, - }); - const metadata = { - value, - proxy, - rev: this.rev, - parent, + else { + tree.firstChild = buildTree(node.firstChild, tree, tree); + el.appendChild(tree.firstChild.el); + let curNode = node.firstChild; + let curTree = tree.firstChild; + while ((curNode = curNode.nextSibling)) { + curTree.nextSibling = buildTree(curNode, curTree, tree); + el.appendChild(curTree.nextSibling.el); + curTree = curTree.nextSibling; + } + } + } + if (tree.info.length) { + addRef(tree); + } + return tree; + } + case Node.TEXT_NODE: + case Node.COMMENT_NODE: { + // text node or comment node + const el = node.nodeType === Node.TEXT_NODE + ? document.createTextNode(node.textContent) + : document.createComment(node.textContent); + return { + parent: parent, + firstChild: null, + nextSibling: null, + el, + info: [], + refN: 0, + currentNS: null, + }; + } + } + throw new Error("boom"); + } + function addRef(tree) { + tree.isRef = true; + do { + tree.refN++; + } while ((tree = tree.parent)); + } + function parentTree(tree) { + let parent = tree.parent; + while (parent && parent.nextSibling === tree) { + tree = parent; + parent = parent.parent; + } + return parent; + } + function buildContext(tree, ctx, fromIdx) { + if (!ctx) { + const children = new Array(tree.info.filter((v) => v.type === "child").length); + ctx = { collectors: [], locations: [], children, cbRefs: [], refN: tree.refN, refList: [] }; + fromIdx = 0; + } + if (tree.refN) { + const initialIdx = fromIdx; + const isRef = tree.isRef; + const firstChild = tree.firstChild ? tree.firstChild.refN : 0; + const nextSibling = tree.nextSibling ? tree.nextSibling.refN : 0; + //node + if (isRef) { + for (let info of tree.info) { + info.refIdx = initialIdx; + } + tree.refIdx = initialIdx; + updateCtx(ctx, tree); + fromIdx++; + } + // right + if (nextSibling) { + const idx = fromIdx + firstChild; + ctx.collectors.push({ idx, prevIdx: initialIdx, getVal: nodeGetNextSibling }); + buildContext(tree.nextSibling, ctx, idx); + } + // left + if (firstChild) { + ctx.collectors.push({ idx: fromIdx, prevIdx: initialIdx, getVal: nodeGetFirstChild }); + buildContext(tree.firstChild, ctx, fromIdx); + } + } + return ctx; + } + function updateCtx(ctx, tree) { + for (let info of tree.info) { + switch (info.type) { + case "text": + ctx.locations.push({ + idx: info.idx, + refIdx: info.refIdx, + setData: setText, + updateData: setText, + }); + break; + case "child": + if (info.isOnlyChild) { + // tree is the parentnode here + ctx.children[info.idx] = { + parentRefIdx: info.refIdx, + isOnlyChild: true, + }; + } + else { + // tree is the anchor text node + ctx.children[info.idx] = { + parentRefIdx: parentTree(tree).refIdx, + afterRefIdx: info.refIdx, + }; + } + break; + case "attribute": { + const refIdx = info.refIdx; + let updater; + let setter; + if (isProp(info.tag, info.name)) { + const setProp = makePropSetter(info.name); + setter = setProp; + updater = setProp; + } + else if (info.name === "class") { + setter = setClass; + updater = updateClass; + } + else { + setter = createAttrUpdater(info.name); + updater = setter; + } + ctx.locations.push({ + idx: info.idx, + refIdx, + setData: setter, + updateData: updater, + }); + break; + } + case "attributes": + ctx.locations.push({ + idx: info.idx, + refIdx: info.refIdx, + setData: attrsSetter, + updateData: attrsUpdater, + }); + break; + case "handler": { + const { setup, update } = createEventHandler(info.event); + ctx.locations.push({ + idx: info.idx, + refIdx: info.refIdx, + setData: setup, + updateData: update, + }); + break; + } + case "ref": + const index = ctx.cbRefs.push(info.idx) - 1; + ctx.locations.push({ + idx: info.idx, + refIdx: info.refIdx, + setData: makeRefSetter(index, ctx.refList), + updateData: NO_OP$1, + }); + } + } + } + // ----------------------------------------------------------------------------- + // building the concrete block class + // ----------------------------------------------------------------------------- + function buildBlock(template, ctx) { + let B = createBlockClass(template, ctx); + if (ctx.cbRefs.length) { + const cbRefs = ctx.cbRefs; + const refList = ctx.refList; + let cbRefsNumber = cbRefs.length; + B = class extends B { + mount(parent, afterNode) { + refList.push(new Array(cbRefsNumber)); + super.mount(parent, afterNode); + for (let cbRef of refList.pop()) { + cbRef(); + } + } + remove() { + super.remove(); + for (let cbRef of cbRefs) { + let fn = this.data[cbRef]; + fn(null); + } + } }; - this.weakMap.set(value, metadata); - this.weakMap.set(metadata.proxy, metadata); - return metadata; } - _updateRevNumber(target) { - this.rev++; - let metadata = this.weakMap.get(target); - let parent = target; - do { - metadata = this.weakMap.get(parent); - metadata.rev++; - } while ((parent = metadata.parent) && parent !== target); + if (ctx.children.length) { + B = class extends B { + constructor(data, children) { + super(data); + this.children = children; + } + }; + B.prototype.beforeRemove = VMulti.prototype.beforeRemove; + return (data, children = []) => new B(data, children); } + return (data) => new B(data); + } + function createBlockClass(template, ctx) { + const { refN, collectors, children } = ctx; + const colN = collectors.length; + ctx.locations.sort((a, b) => a.idx - b.idx); + const locations = ctx.locations.map((loc) => ({ + refIdx: loc.refIdx, + setData: loc.setData, + updateData: loc.updateData, + })); + const locN = locations.length; + const childN = children.length; + const childrenLocs = children; + const isDynamic = refN > 0; + // these values are defined here to make them faster to lookup in the class + // block scope + const nodeCloneNode = nodeProto$2.cloneNode; + const nodeInsertBefore = nodeProto$2.insertBefore; + const elementRemove = elementProto.remove; + class Block { + constructor(data) { + this.data = data; + } + beforeRemove() { } + remove() { + elementRemove.call(this.el); + } + firstNode() { + return this.el; + } + moveBefore(other, afterNode) { + const target = other ? other.el : afterNode; + nodeInsertBefore.call(this.parentEl, this.el, target); + } + toString() { + const div = document.createElement("div"); + this.mount(div, null); + return div.innerHTML; + } + mount(parent, afterNode) { + const el = nodeCloneNode.call(template, true); + nodeInsertBefore.call(parent, el, afterNode); + this.el = el; + this.parentEl = parent; + } + patch(other, withBeforeRemove) { } + } + if (isDynamic) { + Block.prototype.mount = function mount(parent, afterNode) { + const el = nodeCloneNode.call(template, true); + // collecting references + const refs = new Array(refN); + this.refs = refs; + refs[0] = el; + for (let i = 0; i < colN; i++) { + const w = collectors[i]; + refs[w.idx] = w.getVal.call(refs[w.prevIdx]); + } + // applying data to all update points + if (locN) { + const data = this.data; + for (let i = 0; i < locN; i++) { + const loc = locations[i]; + loc.setData.call(refs[loc.refIdx], data[i]); + } + } + nodeInsertBefore.call(parent, el, afterNode); + // preparing all children + if (childN) { + const children = this.children; + for (let i = 0; i < childN; i++) { + const child = children[i]; + if (child) { + const loc = childrenLocs[i]; + const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null; + child.isOnlyChild = loc.isOnlyChild; + child.mount(refs[loc.parentRefIdx], afterNode); + } + } + } + this.el = el; + this.parentEl = parent; + }; + Block.prototype.patch = function patch(other, withBeforeRemove) { + if (this === other) { + return; + } + const refs = this.refs; + // update texts/attributes/ + if (locN) { + const data1 = this.data; + const data2 = other.data; + for (let i = 0; i < locN; i++) { + const val1 = data1[i]; + const val2 = data2[i]; + if (val1 !== val2) { + const loc = locations[i]; + loc.updateData.call(refs[loc.refIdx], val2, val1); + } + } + this.data = data2; + } + // update children + if (childN) { + let children1 = this.children; + const children2 = other.children; + for (let i = 0; i < childN; i++) { + const child1 = children1[i]; + const child2 = children2[i]; + if (child1) { + if (child2) { + child1.patch(child2, withBeforeRemove); + } + else { + if (withBeforeRemove) { + child1.beforeRemove(); + } + child1.remove(); + children1[i] = undefined; + } + } + else if (child2) { + const loc = childrenLocs[i]; + const afterNode = loc.afterRefIdx ? refs[loc.afterRefIdx] : null; + child2.mount(refs[loc.parentRefIdx], afterNode); + children1[i] = child2; + } + } + } + }; + } + return Block; + } + function setText(value) { + characterDataSetData.call(this, toText(value)); + } + function makeRefSetter(index, refs) { + return function setRef(fn) { + refs[refs.length - 1][index] = () => fn(this); + }; + } + + const getDescriptor = (o, p) => Object.getOwnPropertyDescriptor(o, p); + const nodeProto$1 = Node.prototype; + const nodeInsertBefore$1 = nodeProto$1.insertBefore; + const nodeAppendChild = nodeProto$1.appendChild; + const nodeRemoveChild$1 = nodeProto$1.removeChild; + const nodeSetTextContent = getDescriptor(nodeProto$1, "textContent").set; + // ----------------------------------------------------------------------------- + // List Node + // ----------------------------------------------------------------------------- + class VList { + constructor(children) { + this.children = children; + } + mount(parent, afterNode) { + const children = this.children; + const _anchor = document.createTextNode(""); + this.anchor = _anchor; + nodeInsertBefore$1.call(parent, _anchor, afterNode); + const l = children.length; + if (l) { + const mount = children[0].mount; + for (let i = 0; i < l; i++) { + mount.call(children[i], parent, _anchor); + } + } + this.parentEl = parent; + } + moveBefore(other, afterNode) { + if (other) { + const next = other.children[0]; + afterNode = (next ? next.firstNode() : other.anchor) || null; + } + const children = this.children; + for (let i = 0, l = children.length; i < l; i++) { + children[i].moveBefore(null, afterNode); + } + this.parentEl.insertBefore(this.anchor, afterNode); + } + patch(other, withBeforeRemove) { + if (this === other) { + return; + } + const ch1 = this.children; + const ch2 = other.children; + if (ch2.length === 0 && ch1.length === 0) { + return; + } + this.children = ch2; + const proto = ch2[0] || ch1[0]; + const { mount: cMount, patch: cPatch, remove: cRemove, beforeRemove, moveBefore: cMoveBefore, firstNode: cFirstNode, } = proto; + const _anchor = this.anchor; + const isOnlyChild = this.isOnlyChild; + const parent = this.parentEl; + // fast path: no new child => only remove + if (ch2.length === 0 && isOnlyChild) { + if (withBeforeRemove) { + for (let i = 0, l = ch1.length; i < l; i++) { + beforeRemove.call(ch1[i]); + } + } + nodeSetTextContent.call(parent, ""); + nodeAppendChild.call(parent, _anchor); + return; + } + let startIdx1 = 0; + let startIdx2 = 0; + let startVn1 = ch1[0]; + let startVn2 = ch2[0]; + let endIdx1 = ch1.length - 1; + let endIdx2 = ch2.length - 1; + let endVn1 = ch1[endIdx1]; + let endVn2 = ch2[endIdx2]; + let mapping = undefined; + while (startIdx1 <= endIdx1 && startIdx2 <= endIdx2) { + // ------------------------------------------------------------------- + if (startVn1 === null) { + startVn1 = ch1[++startIdx1]; + continue; + } + // ------------------------------------------------------------------- + if (endVn1 === null) { + endVn1 = ch1[--endIdx1]; + continue; + } + // ------------------------------------------------------------------- + let startKey1 = startVn1.key; + let startKey2 = startVn2.key; + if (startKey1 === startKey2) { + cPatch.call(startVn1, startVn2, withBeforeRemove); + ch2[startIdx2] = startVn1; + startVn1 = ch1[++startIdx1]; + startVn2 = ch2[++startIdx2]; + continue; + } + // ------------------------------------------------------------------- + let endKey1 = endVn1.key; + let endKey2 = endVn2.key; + if (endKey1 === endKey2) { + cPatch.call(endVn1, endVn2, withBeforeRemove); + ch2[endIdx2] = endVn1; + endVn1 = ch1[--endIdx1]; + endVn2 = ch2[--endIdx2]; + continue; + } + // ------------------------------------------------------------------- + if (startKey1 === endKey2) { + // bnode moved right + cPatch.call(startVn1, endVn2, withBeforeRemove); + ch2[endIdx2] = startVn1; + const nextChild = ch2[endIdx2 + 1]; + cMoveBefore.call(startVn1, nextChild, _anchor); + startVn1 = ch1[++startIdx1]; + endVn2 = ch2[--endIdx2]; + continue; + } + // ------------------------------------------------------------------- + if (endKey1 === startKey2) { + // bnode moved left + cPatch.call(endVn1, startVn2, withBeforeRemove); + ch2[startIdx2] = endVn1; + const nextChild = ch1[startIdx1]; + cMoveBefore.call(endVn1, nextChild, _anchor); + endVn1 = ch1[--endIdx1]; + startVn2 = ch2[++startIdx2]; + continue; + } + // ------------------------------------------------------------------- + mapping = mapping || createMapping(ch1, startIdx1, endIdx1); + let idxInOld = mapping[startKey2]; + if (idxInOld === undefined) { + cMount.call(startVn2, parent, cFirstNode.call(startVn1) || null); + } + else { + const elmToMove = ch1[idxInOld]; + cMoveBefore.call(elmToMove, startVn1, null); + cPatch.call(elmToMove, startVn2, withBeforeRemove); + ch2[startIdx2] = elmToMove; + ch1[idxInOld] = null; + } + startVn2 = ch2[++startIdx2]; + } + // --------------------------------------------------------------------- + if (startIdx1 <= endIdx1 || startIdx2 <= endIdx2) { + if (startIdx1 > endIdx1) { + const nextChild = ch2[endIdx2 + 1]; + const anchor = nextChild ? cFirstNode.call(nextChild) || null : _anchor; + for (let i = startIdx2; i <= endIdx2; i++) { + cMount.call(ch2[i], parent, anchor); + } + } + else { + for (let i = startIdx1; i <= endIdx1; i++) { + let ch = ch1[i]; + if (ch) { + if (withBeforeRemove) { + beforeRemove.call(ch); + } + cRemove.call(ch); + } + } + } + } + } + beforeRemove() { + const children = this.children; + const l = children.length; + if (l) { + const beforeRemove = children[0].beforeRemove; + for (let i = 0; i < l; i++) { + beforeRemove.call(children[i]); + } + } + } + remove() { + const { parentEl, anchor } = this; + if (this.isOnlyChild) { + nodeSetTextContent.call(parentEl, ""); + } + else { + const children = this.children; + const l = children.length; + if (l) { + const remove = children[0].remove; + for (let i = 0; i < l; i++) { + remove.call(children[i]); + } + } + nodeRemoveChild$1.call(parentEl, anchor); + } + } + firstNode() { + const child = this.children[0]; + return child ? child.firstNode() : undefined; + } + toString() { + return this.children.map((c) => c.toString()).join(""); + } + } + function list(children) { + return new VList(children); + } + function createMapping(ch1, startIdx1, endIdx2) { + let mapping = {}; + for (let i = startIdx1; i <= endIdx2; i++) { + mapping[ch1[i].key] = i; + } + return mapping; + } + + const nodeProto = Node.prototype; + const nodeInsertBefore = nodeProto.insertBefore; + const nodeRemoveChild = nodeProto.removeChild; + class VHtml { + constructor(html) { + this.content = []; + this.html = html; + } + mount(parent, afterNode) { + this.parentEl = parent; + const template = document.createElement("template"); + template.innerHTML = this.html; + this.content = [...template.content.childNodes]; + for (let elem of this.content) { + nodeInsertBefore.call(parent, elem, afterNode); + } + if (!this.content.length) { + const textNode = document.createTextNode(""); + this.content.push(textNode); + nodeInsertBefore.call(parent, textNode, afterNode); + } + } + moveBefore(other, afterNode) { + const target = other ? other.content[0] : afterNode; + const parent = this.parentEl; + for (let elem of this.content) { + nodeInsertBefore.call(parent, elem, target); + } + } + patch(other) { + if (this === other) { + return; + } + const html2 = other.html; + if (this.html !== html2) { + const parent = this.parentEl; + // insert new html in front of current + const afterNode = this.content[0]; + const template = document.createElement("template"); + template.innerHTML = html2; + const content = [...template.content.childNodes]; + for (let elem of content) { + nodeInsertBefore.call(parent, elem, afterNode); + } + if (!content.length) { + const textNode = document.createTextNode(""); + content.push(textNode); + nodeInsertBefore.call(parent, textNode, afterNode); + } + // remove current content + this.remove(); + this.content = content; + this.html = other.html; + } + } + beforeRemove() { } + remove() { + const parent = this.parentEl; + for (let elem of this.content) { + nodeRemoveChild.call(parent, elem); + } + } + firstNode() { + return this.content[0]; + } + toString() { + return this.html; + } + } + function html(str) { + return new VHtml(str); + } + + function mount$1(vnode, fixture, afterNode = null) { + vnode.mount(fixture, afterNode); + } + function patch(vnode1, vnode2, withBeforeRemove = false) { + vnode1.patch(vnode2, withBeforeRemove); + } + function remove(vnode, withBeforeRemove = false) { + if (withBeforeRemove) { + vnode.beforeRemove(); + } + vnode.remove(); + } + + /** + * Apply default props (only top level). + * + * Note that this method does modify in place the props + */ + function applyDefaultProps(props, ComponentClass) { + const defaultProps = ComponentClass.defaultProps; + if (defaultProps) { + for (let propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + } + } + //------------------------------------------------------------------------------ + // Prop validation helper + //------------------------------------------------------------------------------ + function getPropDescription(staticProps) { + if (staticProps instanceof Array) { + return Object.fromEntries(staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true]))); + } + return staticProps || { "*": true }; + } + /** + * Validate the component props (or next props) against the (static) props + * description. This is potentially an expensive operation: it may needs to + * visit recursively the props and all the children to check if they are valid. + * This is why it is only done in 'dev' mode. + */ + function validateProps(name, props, parent) { + const ComponentClass = typeof name !== "string" + ? name + : parent.constructor.components[name]; + if (!ComponentClass) { + // this is an error, wrong component. We silently return here instead so the + // error is triggered by the usual path ('component' function) + return; + } + applyDefaultProps(props, ComponentClass); + const defaultProps = ComponentClass.defaultProps || {}; + let propsDef = getPropDescription(ComponentClass.props); + const allowAdditionalProps = "*" in propsDef; + for (let propName in propsDef) { + if (propName === "*") { + continue; + } + const propDef = propsDef[propName]; + let isMandatory = !!propDef; + if (typeof propDef === "object" && "optional" in propDef) { + isMandatory = !propDef.optional; + } + if (isMandatory && propName in defaultProps) { + throw new Error(`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name})`); + } + if (props[propName] === undefined) { + if (isMandatory) { + throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`); + } + else { + continue; + } + } + let isValid; + try { + isValid = isValidProp(props[propName], propDef); + } + catch (e) { + e.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`; + throw e; + } + if (!isValid) { + throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`); + } + } + if (!allowAdditionalProps) { + for (let propName in props) { + if (!(propName in propsDef)) { + throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`); + } + } + } + } + /** + * Check if an invidual prop value matches its (static) prop definition + */ + function isValidProp(prop, propDef) { + if (propDef === true) { + return true; + } + if (typeof propDef === "function") { + // Check if a value is constructed by some Constructor. Note that there is a + // slight abuse of language: we want to consider primitive values as well. + // + // So, even though 1 is not an instance of Number, we want to consider that + // it is valid. + if (typeof prop === "object") { + return prop instanceof propDef; + } + return typeof prop === propDef.name.toLowerCase(); + } + else if (propDef instanceof Array) { + // If this code is executed, this means that we want to check if a prop + // matches at least one of its descriptor. + let result = false; + for (let i = 0, iLen = propDef.length; i < iLen; i++) { + result = result || isValidProp(prop, propDef[i]); + } + return result; + } + // propsDef is an object + if (propDef.optional && prop === undefined) { + return true; + } + let result = propDef.type ? isValidProp(prop, propDef.type) : true; + if (propDef.validate) { + result = result && propDef.validate(prop); + } + if (propDef.type === Array && propDef.element) { + for (let i = 0, iLen = prop.length; i < iLen; i++) { + result = result && isValidProp(prop[i], propDef.element); + } + } + if (propDef.type === Object && propDef.shape) { + const shape = propDef.shape; + for (let key in shape) { + result = result && isValidProp(prop[key], shape[key]); + } + if (result) { + for (let propName in prop) { + if (!(propName in shape)) { + throw new Error(`unknown prop '${propName}'`); + } + } + } + } + return result; + } + + /** + * Creates a batched version of a callback so that all calls to it in the same + * microtick will only call the original callback once. + * + * @param callback the callback to batch + * @returns a batched version of the original callback + */ + function batched(callback) { + let called = false; + return async () => { + // This await blocks all calls to the callback here, then releases them sequentially + // in the next microtick. This line decides the granularity of the batch. + await Promise.resolve(); + if (!called) { + called = true; + callback(); + // wait for all calls in this microtick to fall through before resetting "called" + // so that only the first call to the batched function calls the original callback + await Promise.resolve(); + called = false; + } + }; + } + function validateTarget(target) { + if (!(target instanceof HTMLElement)) { + throw new Error("Cannot mount component: the target is not a valid DOM element"); + } + if (!document.body.contains(target)) { + throw new Error("Cannot mount a component on a detached dom node"); + } + } + class EventBus extends EventTarget { + trigger(name, payload) { + this.dispatchEvent(new CustomEvent(name, { detail: payload })); + } + } + function whenReady(fn) { + return new Promise(function (resolve) { + if (document.readyState !== "loading") { + resolve(true); + } + else { + document.addEventListener("DOMContentLoaded", resolve, false); + } + }).then(fn || function () { }); + } + async function loadFile(url) { + const result = await fetch(url); + if (!result.ok) { + throw new Error("Error while fetching xml templates"); + } + return await result.text(); + } + /* + * This class just transports the fact that a string is safe + * to be injected as HTML. Overriding a JS primitive is quite painful though + * so we need to redfine toString and valueOf. + */ + class Markup extends String { + } + /* + * Marks a value as safe, that is, a value that can be injected as HTML directly. + * It should be used to wrap the value passed to a t-out directive to allow a raw rendering. + */ + function markup(value) { + return new Markup(value); + } + + /** + * This file contains utility functions that will be injected in each template, + * to perform various useful tasks in the compiled code. + */ + function withDefault(value, defaultValue) { + return value === undefined || value === null || value === false ? defaultValue : value; + } + function callSlot(ctx, parent, key, name, dynamic, extra, defaultContent) { + key = key + "__slot_" + name; + const slots = (ctx.props && ctx.props.slots) || {}; + const { __render, __ctx, __scope } = slots[name] || {}; + const slotScope = Object.create(__ctx || {}); + if (__scope) { + slotScope[__scope] = extra || {}; + } + const slotBDom = __render ? __render.call(__ctx.__owl__.component, slotScope, parent, key) : null; + if (defaultContent) { + let child1 = undefined; + let child2 = undefined; + if (slotBDom) { + child1 = dynamic ? toggler(name, slotBDom) : slotBDom; + } + else { + child2 = defaultContent.call(ctx.__owl__.component, ctx, parent, key); + } + return multi([child1, child2]); + } + return slotBDom || text(""); + } + function capture(ctx) { + const component = ctx.__owl__.component; + const result = Object.create(component); + for (let k in ctx) { + result[k] = ctx[k]; + } + return result; + } + function withKey(elem, k) { + elem.key = k; + return elem; + } + function prepareList(collection) { + let keys; + let values; + if (Array.isArray(collection)) { + keys = collection; + values = collection; + } + else if (collection) { + values = Object.keys(collection); + keys = Object.values(collection); + } + else { + throw new Error("Invalid loop expression"); + } + const n = values.length; + return [keys, values, n, new Array(n)]; + } + const isBoundary = Symbol("isBoundary"); + function setContextValue(ctx, key, value) { + const ctx0 = ctx; + while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) { + const newCtx = ctx.__proto__; + if (!newCtx) { + ctx = ctx0; + break; + } + ctx = newCtx; + } + ctx[key] = value; + } + function toNumber(val) { + const n = parseFloat(val); + return isNaN(n) ? val : n; + } + function shallowEqual$1(l1, l2) { + for (let i = 0, l = l1.length; i < l; i++) { + if (l1[i] !== l2[i]) { + return false; + } + } + return true; + } + class LazyValue { + constructor(fn, ctx, node) { + this.fn = fn; + this.ctx = capture(ctx); + this.node = node; + } + evaluate() { + return this.fn(this.ctx, this.node); + } + toString() { + return this.evaluate().toString(); + } + } + /* + * Safely outputs `value` as a block depending on the nature of `value` + */ + function safeOutput(value) { + if (!value) { + return value; + } + let safeKey; + let block; + if (value instanceof Markup) { + safeKey = `string_safe`; + block = html(value); + } + else if (value instanceof LazyValue) { + safeKey = `lazy_value`; + block = value.evaluate(); + } + else if (value instanceof String || typeof value === "string") { + safeKey = "string_unsafe"; + block = text(value); + } + else { + // Assuming it is a block + safeKey = "block_safe"; + block = value; + } + return toggler(safeKey, block); + } + let boundFunctions = new WeakMap(); + function bind(ctx, fn) { + let component = ctx.__owl__.component; + let boundFnMap = boundFunctions.get(component); + if (!boundFnMap) { + boundFnMap = new WeakMap(); + boundFunctions.set(component, boundFnMap); + } + let boundFn = boundFnMap.get(fn); + if (!boundFn) { + boundFn = fn.bind(component); + boundFnMap.set(fn, boundFn); + } + return boundFn; + } + function multiRefSetter(refs, name) { + let count = 0; + return (el) => { + if (el) { + count++; + if (count > 1) { + throw new Error("Cannot have 2 elements with same ref name at the same time"); + } + } + if (count === 0 || el) { + refs[name] = el; + } + }; + } + const UTILS = { + withDefault, + zero: Symbol("zero"), + isBoundary, + callSlot, + capture, + withKey, + prepareList, + setContextValue, + multiRefSetter, + shallowEqual: shallowEqual$1, + toNumber, + validateProps, + LazyValue, + safeOutput, + bind, + }; + + const mainEventHandler = (data, ev, currentTarget) => { + const { data: _data, modifiers } = filterOutModifiersFromData(data); + data = _data; + let stopped = false; + if (modifiers.length) { + let selfMode = false; + const isSelf = ev.target === currentTarget; + for (const mod of modifiers) { + switch (mod) { + case "self": + selfMode = true; + if (isSelf) { + continue; + } + else { + return stopped; + } + case "prevent": + if ((selfMode && isSelf) || !selfMode) + ev.preventDefault(); + continue; + case "stop": + if ((selfMode && isSelf) || !selfMode) + ev.stopPropagation(); + stopped = true; + continue; + } + } + } + // If handler is empty, the array slot 0 will also be empty, and data will not have the property 0 + // We check this rather than data[0] being truthy (or typeof function) so that it crashes + // as expected when there is a handler expression that evaluates to a falsy value + if (Object.hasOwnProperty.call(data, 0)) { + const handler = data[0]; + if (typeof handler !== "function") { + throw new Error(`Invalid handler (expected a function, received: '${handler}')`); + } + let node = data[1] ? data[1].__owl__ : null; + if (node ? node.status === 1 /* MOUNTED */ : true) { + handler.call(node ? node.component : null, ev); + } + } + return stopped; + }; + + // Maps fibers to thrown errors + const fibersInError = new WeakMap(); + const nodeErrorHandlers = new WeakMap(); + function _handleError(node, error, isFirstRound = false) { + if (!node) { + return false; + } + const fiber = node.fiber; + if (fiber) { + fibersInError.set(fiber, error); + } + const errorHandlers = nodeErrorHandlers.get(node); + if (errorHandlers) { + let stopped = false; + // execute in the opposite order + for (let i = errorHandlers.length - 1; i >= 0; i--) { + try { + errorHandlers[i](error); + stopped = true; + break; + } + catch (e) { + error = e; + } + } + if (stopped) { + if (isFirstRound && fiber && fiber.node.fiber) { + fiber.root.counter--; + } + return true; + } + } + return _handleError(node.parent, error); + } + function handleError(params) { + const error = params.error; + const node = "node" in params ? params.node : params.fiber.node; + const fiber = "fiber" in params ? params.fiber : node.fiber; + // resets the fibers on components if possible. This is important so that + // new renderings can be properly included in the initial one, if any. + let current = fiber; + do { + current.node.fiber = current; + current = current.parent; + } while (current); + fibersInError.set(fiber.root, error); + const handled = _handleError(node, error, true); + if (!handled) { + console.warn(`[Owl] Unhandled error. Destroying the root component`); + try { + node.app.destroy(); + } + catch (e) { + console.error(e); + } + } + } + + function makeChildFiber(node, parent) { + let current = node.fiber; + if (current) { + let root = parent.root; + cancelFibers(root, current.children); + current.root = null; + } + return new Fiber(node, parent); + } + function makeRootFiber(node) { + let current = node.fiber; + if (current) { + let root = current.root; + root.counter -= cancelFibers(root, current.children); + current.children = []; + root.counter++; + current.bdom = null; + if (fibersInError.has(current)) { + fibersInError.delete(current); + fibersInError.delete(root); + current.appliedToDom = false; + } + return current; + } + const fiber = new RootFiber(node, null); + if (node.willPatch.length) { + fiber.willPatch.push(fiber); + } + if (node.patched.length) { + fiber.patched.push(fiber); + } + return fiber; + } + /** + * @returns number of not-yet rendered fibers cancelled + */ + function cancelFibers(root, fibers) { + let result = 0; + for (let fiber of fibers) { + fiber.node.fiber = null; + fiber.root = root; + if (!fiber.bdom) { + result++; + } + result += cancelFibers(root, fiber.children); + } + return result; + } + class Fiber { + constructor(node, parent) { + this.bdom = null; + this.children = []; + this.appliedToDom = false; + this.node = node; + this.parent = parent; + if (parent) { + const root = parent.root; + root.counter++; + this.root = root; + parent.children.push(this); + } + else { + this.root = this; + } + } + } + class RootFiber extends Fiber { + constructor() { + super(...arguments); + this.counter = 1; + // only add stuff in this if they have registered some hooks + this.willPatch = []; + this.patched = []; + this.mounted = []; + // A fiber is typically locked when it is completing and the patch has not, or is being applied. + // i.e.: render triggered in onWillUnmount or in willPatch will be delayed + this.locked = false; + } + complete() { + const node = this.node; + this.locked = true; + let current = undefined; + try { + // Step 1: calling all willPatch lifecycle hooks + for (current of this.willPatch) { + // because of the asynchronous nature of the rendering, some parts of the + // UI may have been rendered, then deleted in a followup rendering, and we + // do not want to call onWillPatch in that case. + let node = current.node; + if (node.fiber === current) { + const component = node.component; + for (let cb of node.willPatch) { + cb.call(component); + } + } + } + current = undefined; + // Step 2: patching the dom + node.patch(); + this.locked = false; + // Step 4: calling all mounted lifecycle hooks + let mountedFibers = this.mounted; + while ((current = mountedFibers.pop())) { + current = current; + if (current.appliedToDom) { + for (let cb of current.node.mounted) { + cb(); + } + } + } + // Step 5: calling all patched hooks + let patchedFibers = this.patched; + while ((current = patchedFibers.pop())) { + current = current; + if (current.appliedToDom) { + for (let cb of current.node.patched) { + cb(); + } + } + } + } + catch (e) { + this.locked = false; + handleError({ fiber: current || this, error: e }); + } + } + } + class MountFiber extends RootFiber { + constructor(node, target, options = {}) { + super(node, null); + this.target = target; + this.position = options.position || "last-child"; + } + complete() { + let current = this; + try { + const node = this.node; + node.app.constructor.validateTarget(this.target); + if (node.bdom) { + // this is a complicated situation: if we mount a fiber with an existing + // bdom, this means that this same fiber was already completed, mounted, + // but a crash occurred in some mounted hook. Then, it was handled and + // the new rendering is being applied. + node.updateDom(); + } + else { + node.bdom = this.bdom; + if (this.position === "last-child" || this.target.childNodes.length === 0) { + mount$1(node.bdom, this.target); + } + else { + const firstChild = this.target.childNodes[0]; + mount$1(node.bdom, this.target, firstChild); + } + } + // unregistering the fiber before mounted since it can do another render + // and that the current rendering is obviously completed + node.fiber = null; + node.status = 1 /* MOUNTED */; + this.appliedToDom = true; + let mountedFibers = this.mounted; + while ((current = mountedFibers.pop())) { + if (current.appliedToDom) { + for (let cb of current.node.mounted) { + cb(); + } + } + } + } + catch (e) { + handleError({ fiber: current, error: e }); + } + } + } + + let currentNode = null; + function getCurrent() { + if (!currentNode) { + throw new Error("No active component (a hook function should only be called in 'setup')"); + } + return currentNode; + } + function useComponent() { + return currentNode.component; + } + function component(name, props, key, ctx, parent) { + let node = ctx.children[key]; + let isDynamic = typeof name !== "string"; + if (node) { + if (node.status < 1 /* MOUNTED */) { + node.destroy(); + node = undefined; + } + else if (node.status === 2 /* DESTROYED */) { + node = undefined; + } + } + if (isDynamic && node && node.component.constructor !== name) { + node = undefined; + } + const parentFiber = ctx.fiber; + if (node) { + node.updateAndRender(props, parentFiber); + } + else { + // new component + let C; + if (isDynamic) { + C = name; + } + else { + C = parent.constructor.components[name]; + if (!C) { + throw new Error(`Cannot find the definition of component "${name}"`); + } + } + node = new ComponentNode(C, props, ctx.app, ctx); + ctx.children[key] = node; + const fiber = makeChildFiber(node, parentFiber); + node.initiateRender(fiber); + } + return node; + } + class ComponentNode { + constructor(C, props, app, parent) { + this.fiber = null; + this.bdom = null; + this.status = 0 /* NEW */; + this.children = Object.create(null); + this.refs = {}; + this.willStart = []; + this.willUpdateProps = []; + this.willUnmount = []; + this.mounted = []; + this.willPatch = []; + this.patched = []; + this.willDestroy = []; + currentNode = this; + this.app = app; + this.parent = parent || null; + this.level = parent ? parent.level + 1 : 0; + applyDefaultProps(props, C); + const env = (parent && parent.childEnv) || app.env; + this.childEnv = env; + this.component = new C(props, env, this); + this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this); + this.component.setup(); + currentNode = null; + } + mountComponent(target, options) { + const fiber = new MountFiber(this, target, options); + this.app.scheduler.addFiber(fiber); + this.initiateRender(fiber); + } + async initiateRender(fiber) { + this.fiber = fiber; + if (this.mounted.length) { + fiber.root.mounted.push(fiber); + } + const component = this.component; + try { + await Promise.all(this.willStart.map((f) => f.call(component))); + } + catch (e) { + handleError({ node: this, error: e }); + return; + } + if (this.status === 0 /* NEW */ && this.fiber === fiber) { + this._render(fiber); + } + } + async render() { + let current = this.fiber; + if (current && current.root.locked) { + await Promise.resolve(); + // situation may have changed after the microtask tick + current = this.fiber; + } + if (current && !current.bdom && !fibersInError.has(current)) { + return; + } + if (!this.bdom && !current) { + return; + } + const fiber = makeRootFiber(this); + this.fiber = fiber; + this.app.scheduler.addFiber(fiber); + await Promise.resolve(); + if (this.status === 2 /* DESTROYED */) { + return; + } + // We only want to actually render the component if the following two + // conditions are true: + // * this.fiber: it could be null, in which case the render has been cancelled + // * (current || !fiber.parent): if current is not null, this means that the + // render function was called when a render was already occurring. In this + // case, the pending rendering was cancelled, and the fiber needs to be + // rendered to complete the work. If current is null, we check that the + // fiber has no parent. If that is the case, the fiber was downgraded from + // a root fiber to a child fiber in the previous microtick, because it was + // embedded in a rendering coming from above, so the fiber will be rendered + // in the next microtick anyway, so we should not render it again. + if (this.fiber === fiber && (current || !fiber.parent)) { + this._render(fiber); + } + } + _render(fiber) { + try { + fiber.bdom = this.renderFn(); + fiber.root.counter--; + } + catch (e) { + handleError({ node: this, error: e }); + } + } + destroy() { + let shouldRemove = this.status === 1 /* MOUNTED */; + this._destroy(); + if (shouldRemove) { + this.bdom.remove(); + } + } + _destroy() { + const component = this.component; + if (this.status === 1 /* MOUNTED */) { + for (let cb of this.willUnmount) { + cb.call(component); + } + } + for (let child of Object.values(this.children)) { + child._destroy(); + } + for (let cb of this.willDestroy) { + cb.call(component); + } + this.status = 2 /* DESTROYED */; + } + async updateAndRender(props, parentFiber) { + // update + const fiber = makeChildFiber(this, parentFiber); + this.fiber = fiber; + const component = this.component; + applyDefaultProps(props, component.constructor); + const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props))); + await prom; + if (fiber !== this.fiber) { + return; + } + component.props = props; + this._render(fiber); + const parentRoot = parentFiber.root; + if (this.willPatch.length) { + parentRoot.willPatch.push(fiber); + } + if (this.patched.length) { + parentRoot.patched.push(fiber); + } + } + /** + * Finds a child that has dom that is not yet updated, and update it. This + * method is meant to be used only in the context of repatching the dom after + * a mounted hook failed and was handled. + */ + updateDom() { + if (!this.fiber) { + return; + } + if (this.bdom === this.fiber.bdom) { + // If the error was handled by some child component, we need to find it to + // apply its change + for (let k in this.children) { + const child = this.children[k]; + child.updateDom(); + } + } + else { + // if we get here, this is the component that handled the error and rerendered + // itself, so we can simply patch the dom + this.bdom.patch(this.fiber.bdom, false); + this.fiber.appliedToDom = true; + this.fiber = null; + } + } + // --------------------------------------------------------------------------- + // Block DOM methods + // --------------------------------------------------------------------------- + firstNode() { + const bdom = this.bdom; + return bdom ? bdom.firstNode() : undefined; + } + mount(parent, anchor) { + const bdom = this.fiber.bdom; + this.bdom = bdom; + bdom.mount(parent, anchor); + this.status = 1 /* MOUNTED */; + this.fiber.appliedToDom = true; + this.fiber = null; + } + moveBefore(other, afterNode) { + this.bdom.moveBefore(other ? other.bdom : null, afterNode); + } + patch() { + const hasChildren = Object.keys(this.children).length > 0; + this.bdom.patch(this.fiber.bdom, hasChildren); + if (hasChildren) { + this.cleanOutdatedChildren(); + } + this.fiber.appliedToDom = true; + this.fiber = null; + } + beforeRemove() { + this._destroy(); + } + remove() { + this.bdom.remove(); + } + cleanOutdatedChildren() { + const children = this.children; + for (const key in children) { + const node = children[key]; + const status = node.status; + if (status !== 1 /* MOUNTED */) { + delete children[key]; + if (status !== 2 /* DESTROYED */) { + node.destroy(); + } + } + } + } + } + + // ----------------------------------------------------------------------------- + // hooks + // ----------------------------------------------------------------------------- + function onWillStart(fn) { + const node = getCurrent(); + node.willStart.push(fn.bind(node.component)); + } + function onWillUpdateProps(fn) { + const node = getCurrent(); + node.willUpdateProps.push(fn.bind(node.component)); + } + function onMounted(fn) { + const node = getCurrent(); + node.mounted.push(fn.bind(node.component)); + } + function onWillPatch(fn) { + const node = getCurrent(); + node.willPatch.unshift(fn.bind(node.component)); + } + function onPatched(fn) { + const node = getCurrent(); + node.patched.push(fn.bind(node.component)); + } + function onWillUnmount(fn) { + const node = getCurrent(); + node.willUnmount.unshift(fn.bind(node.component)); + } + function onWillDestroy(fn) { + const node = getCurrent(); + node.willDestroy.push(fn.bind(node.component)); + } + function onWillRender(fn) { + const node = getCurrent(); + const renderFn = node.renderFn; + node.renderFn = () => { + fn.call(node.component); + return renderFn(); + }; + } + function onRendered(fn) { + const node = getCurrent(); + const renderFn = node.renderFn; + node.renderFn = () => { + const result = renderFn(); + fn.call(node.component); + return result; + }; + } + function onError(callback) { + const node = getCurrent(); + let handlers = nodeErrorHandlers.get(node); + if (!handlers) { + handlers = []; + nodeErrorHandlers.set(node, handlers); + } + handlers.push(callback.bind(node.component)); } /** @@ -358,9 +2467,8 @@ * the arrow operator, then we add the current (or some previous tokens) token to * the list of variables so it does not get replaced by a lookup in the context */ - function compileExprToArray(expr, scope) { + function compileExprToArray(expr) { const localVars = new Set(); - scope = Object.create(scope); const tokens = tokenize(expr); let i = 0; let stack = []; // to track last opening [ or { @@ -385,7 +2493,7 @@ if (groupType === "LEFT_BRACE" && isLeftSeparator(prevToken) && isRightSeparator(nextToken)) { - tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, Object.assign({}, token)); + tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token }); nextToken = tokens[i + 1]; } if (prevToken.type === "OPERATOR" && prevToken.value === ".") { @@ -399,7 +2507,7 @@ } } if (token.type === "TEMPLATE_STRING") { - token.value = token.replace((expr) => compileExpr(expr, scope)); + token.value = token.replace((expr) => compileExpr(expr)); } if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") { if (token.type === "RIGHT_PAREN") { @@ -407,25 +2515,20 @@ while (j > 0 && tokens[j].type !== "LEFT_PAREN") { if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) { tokens[j].value = tokens[j].originalValue; - scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value }; - localVars.add(tokens[j].value); + localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value }; } j--; } } else { - scope[token.value] = { id: token.value, expr: token.value }; - localVars.add(token.value); + localVars.add(token.value); //] = { id: token.value, expr: token.value }; } } if (isVar) { token.varName = token.value; - if (token.value in scope && "id" in scope[token.value]) { - token.value = scope[token.value].expr; - } - else { + if (!localVars.has(token.value)) { token.originalValue = token.value; - token.value = `scope['${token.value}']`; + token.value = `ctx['${token.value}']`; } } i++; @@ -433,1111 +2536,1853 @@ // Mark all variables that have been used locally. // This assumes the expression has only one scope (incorrect but "good enough for now") for (const token of tokens) { - if (token.type === "SYMBOL" && localVars.has(token.value)) { + if (token.type === "SYMBOL" && token.varName && localVars.has(token.value)) { + token.originalValue = token.value; + token.value = `_${token.value}`; token.isLocal = true; } } return tokens; } - function compileExpr(expr, scope) { - return compileExprToArray(expr, scope) + function compileExpr(expr) { + return compileExprToArray(expr) .map((t) => t.value) .join(""); + } + const INTERP_REGEXP = /\{\{.*?\}\}/g; + const INTERP_GROUP_REGEXP = /\{\{.*?\}\}/g; + function interpolate(s) { + let matches = s.match(INTERP_REGEXP); + if (matches && matches[0].length === s.length) { + return `(${compileExpr(s.slice(2, -2))})`; + } + let r = s.replace(INTERP_GROUP_REGEXP, (s) => "${" + compileExpr(s.slice(2, -2)) + "}"); + return "`" + r + "`"; } - const INTERP_REGEXP = /\{\{.*?\}\}/g; - //------------------------------------------------------------------------------ - // Compilation Context - //------------------------------------------------------------------------------ - class CompilationContext { + // using a non-html document so that HTML serializes as XML instead + // of HTML (as we will parse it as xml later) + const xmlDoc = document.implementation.createDocument(null, null, null); + const MODS = new Set(["stop", "capture", "prevent", "self", "synthetic"]); + // ----------------------------------------------------------------------------- + // BlockDescription + // ----------------------------------------------------------------------------- + class BlockDescription { + constructor(target, type) { + this.dynamicTagName = null; + this.isRoot = false; + this.hasDynamicChildren = false; + this.children = []; + this.data = []; + this.childNumber = 0; + this.parentVar = ""; + this.id = BlockDescription.nextBlockId++; + this.varName = "b" + this.id; + this.blockName = "block" + this.id; + this.target = target; + this.type = type; + } + static generateId(prefix) { + this.nextDataIds[prefix] = (this.nextDataIds[prefix] || 0) + 1; + return prefix + this.nextDataIds[prefix]; + } + insertData(str, prefix = "d") { + const id = BlockDescription.generateId(prefix); + this.target.addLine(`let ${id} = ${str};`); + return this.data.push(id) - 1; + } + insert(dom) { + if (this.currentDom) { + this.currentDom.appendChild(dom); + } + else { + this.dom = dom; + } + } + generateExpr(expr) { + if (this.type === "block") { + const hasChildren = this.children.length; + let params = this.data.length ? `[${this.data.join(", ")}]` : hasChildren ? "[]" : ""; + if (hasChildren) { + params += ", [" + this.children.map((c) => c.varName).join(", ") + "]"; + } + if (this.dynamicTagName) { + return `toggler(${this.dynamicTagName}, ${this.blockName}(${this.dynamicTagName})(${params}))`; + } + return `${this.blockName}(${params})`; + } + else if (this.type === "list") { + return `list(c_block${this.id})`; + } + return expr; + } + asXmlString() { + // Can't use outerHTML on text/comment nodes + // append dom to any element and use innerHTML instead + const t = xmlDoc.createElement("t"); + t.appendChild(this.dom); + return t.innerHTML; + } + } + BlockDescription.nextBlockId = 1; + BlockDescription.nextDataIds = {}; + function createContext(parentCtx, params) { + return Object.assign({ + block: null, + index: 0, + forceNewBlock: true, + translate: parentCtx.translate, + tKeyExpr: null, + nameSpace: parentCtx.nameSpace, + tModelSelectedExpr: parentCtx.tModelSelectedExpr, + }, params); + } + class CodeTarget { constructor(name) { - this.code = []; - this.variables = {}; - this.escaping = false; - this.parentNode = null; - this.parentTextNode = null; - this.rootNode = null; this.indentLevel = 0; - this.shouldDefineParent = false; - this.shouldDefineScope = false; - this.protectedScopeNumber = 0; - this.shouldDefineQWeb = false; - this.shouldDefineUtils = false; - this.shouldDefineRefs = false; - this.shouldDefineResult = true; - this.loopNumber = 0; - this.inPreTag = false; - this.allowMultipleRoots = false; - this.hasParentWidget = false; - this.hasKey0 = false; - this.keyStack = []; - this.rootContext = this; - this.templateName = name || "noname"; - this.addLine("let h = this.h;"); + this.loopLevel = 0; + this.code = []; + this.hasRoot = false; + this.hasCache = false; + this.hasRef = false; + // maps ref name to [id, expr] + this.refInfo = {}; + this.shouldProtectScope = false; + this.name = name; } - generateID() { - return CompilationContext.nextID++; - } - /** - * This method generates a "template key", which is basically a unique key - * which depends on the currently set keys, and on the iteration numbers (if - * we are in a loop). - * - * Such a key is necessary when we need to associate an id to some element - * generated by a template (for example, a component) - */ - generateTemplateKey(prefix = "") { - const id = this.generateID(); - if (this.loopNumber === 0 && !this.hasKey0) { - return `'${prefix}__${id}__'`; + addLine(line, idx) { + const prefix = new Array(this.indentLevel + 2).join(" "); + if (idx === undefined) { + this.code.push(prefix + line); } - let key = `\`${prefix}__${id}__`; - let start = this.hasKey0 ? 0 : 1; - for (let i = start; i < this.loopNumber + 1; i++) { - key += `\${key${i}}__`; + else { + this.code.splice(idx, 0, prefix + line); } - this.addLine(`let k${id} = ${key}\`;`); - return `k${id}`; } generateCode() { - if (this.shouldDefineResult) { - this.code.unshift(" let result;"); - } - if (this.shouldDefineScope) { - this.code.unshift(" let scope = Object.create(context);"); - } - if (this.shouldDefineRefs) { - this.code.unshift(" context.__owl__.refs = context.__owl__.refs || {};"); - } - if (this.shouldDefineParent) { - if (this.hasParentWidget) { - this.code.unshift(" let parent = extra.parent;"); - } - else { - this.code.unshift(" let parent = context;"); + let result = []; + result.push(`function ${this.name}(ctx, node, key = "") {`); + if (this.hasRef) { + result.push(` const refs = ctx.__owl__.refs;`); + for (let name in this.refInfo) { + const [id, expr] = this.refInfo[name]; + result.push(` const ${id} = ${expr};`); } } - if (this.shouldDefineQWeb) { - this.code.unshift(" let QWeb = this.constructor;"); + if (this.shouldProtectScope) { + result.push(` ctx = Object.create(ctx);`); + result.push(` ctx[isBoundary] = 1`); } - if (this.shouldDefineUtils) { - this.code.unshift(" let utils = this.constructor.utils;"); + if (this.hasCache) { + result.push(` let cache = ctx.cache || {};`); + result.push(` let nextCache = ctx.cache = {};`); } - return this.code; + for (let line of this.code) { + result.push(line); + } + if (!this.hasRoot) { + result.push(`return text('');`); + } + result.push(`}`); + return result.join("\n "); } - withParent(node) { - if (!this.allowMultipleRoots && - this === this.rootContext && - (this.parentNode || this.parentTextNode)) { - throw new Error("A template should not have more than one root node"); + } + const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"]; + const translationRE = /^(\s*)([\s\S]+?)(\s*)$/; + class CodeGenerator { + constructor(ast, options) { + this.blocks = []; + this.ids = {}; + this.nextBlockId = 1; + this.isDebug = false; + this.targets = []; + this.target = new CodeTarget("template"); + this.translatableAttributes = TRANSLATABLE_ATTRS; + this.staticCalls = []; + this.helpers = new Set(); + this.translateFn = options.translateFn || ((s) => s); + if (options.translatableAttributes) { + const attrs = new Set(TRANSLATABLE_ATTRS); + for (let attr of options.translatableAttributes) { + if (attr.startsWith("-")) { + attrs.delete(attr.slice(1)); + } + else { + attrs.add(attr); + } + } + this.translatableAttributes = [...attrs]; } - if (!this.rootContext.rootNode) { - this.rootContext.rootNode = node; + this.hasSafeContext = options.hasSafeContext || false; + this.dev = options.dev || false; + this.ast = ast; + this.templateName = options.name; + } + generateCode() { + const ast = this.ast; + this.isDebug = ast.type === 12 /* TDebug */; + BlockDescription.nextBlockId = 1; + BlockDescription.nextDataIds = {}; + this.compileAST(ast, { + block: null, + index: 0, + forceNewBlock: false, + isLast: true, + translate: true, + tKeyExpr: null, + }); + // define blocks and utility functions + let mainCode = [ + ` let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;`, + ]; + if (this.helpers.size) { + mainCode.push(`let { ${[...this.helpers].join(", ")} } = helpers;`); } - if (!this.parentNode && this.rootContext.shouldDefineResult) { - this.addLine(`result = vn${node};`); + if (this.templateName) { + mainCode.push(`// Template name: "${this.templateName}"`); } - return this.subContext("parentNode", node); + for (let { id, template } of this.staticCalls) { + mainCode.push(`const ${id} = getTemplate(${template});`); + } + // define all blocks + if (this.blocks.length) { + mainCode.push(``); + for (let block of this.blocks) { + if (block.dom) { + let xmlString = block.asXmlString(); + if (block.dynamicTagName) { + xmlString = xmlString.replace(/^<\w+/, `<\${tag || '${block.dom.nodeName}'}`); + xmlString = xmlString.replace(/\w+>$/, `\${tag || '${block.dom.nodeName}'}>`); + mainCode.push(`let ${block.blockName} = tag => createBlock(\`${xmlString}\`);`); + } + else { + mainCode.push(`let ${block.blockName} = createBlock(\`${xmlString}\`);`); + } + } + } + } + // define all slots/defaultcontent function + if (this.targets.length) { + for (let fn of this.targets) { + mainCode.push(""); + mainCode = mainCode.concat(fn.generateCode()); + } + } + // generate main code + mainCode.push(""); + mainCode = mainCode.concat("return " + this.target.generateCode()); + const code = mainCode.join("\n "); + if (this.isDebug) { + const msg = `[Owl Debug]\n${code}`; + console.log(msg); + } + return code; } - subContext(key, value) { - const newContext = Object.create(this); - newContext[key] = value; - return newContext; - } - indent() { - this.rootContext.indentLevel++; - } - dedent() { - this.rootContext.indentLevel--; + compileInNewTarget(prefix, ast, ctx) { + const name = this.generateId(prefix); + const initialTarget = this.target; + const target = new CodeTarget(name); + this.targets.push(target); + this.target = target; + const subCtx = createContext(ctx); + this.compileAST(ast, subCtx); + this.target = initialTarget; + return name; } addLine(line) { - const prefix = new Array(this.indentLevel + 2).join(" "); - this.code.push(prefix + line); - return this.code.length - 1; + this.target.addLine(line); } - addIf(condition) { - this.addLine(`if (${condition}) {`); - this.indent(); + generateId(prefix = "") { + this.ids[prefix] = (this.ids[prefix] || 0) + 1; + return prefix + this.ids[prefix]; } - addElse() { - this.dedent(); - this.addLine("} else {"); - this.indent(); + insertAnchor(block) { + const tag = `block-child-${block.children.length}`; + const anchor = xmlDoc.createElement(tag); + block.insert(anchor); } - closeIf() { - this.dedent(); - this.addLine("}"); + createBlock(parentBlock, type, ctx) { + const hasRoot = this.target.hasRoot; + const block = new BlockDescription(this.target, type); + if (!hasRoot && !ctx.preventRoot) { + this.target.hasRoot = true; + block.isRoot = true; + } + if (parentBlock) { + parentBlock.children.push(block); + if (parentBlock.type === "list") { + block.parentVar = `c_block${parentBlock.id}`; + } + } + return block; } - getValue(val) { - return val in this.variables ? this.getValue(this.variables[val]) : val; + insertBlock(expression, block, ctx) { + let blockExpr = block.generateExpr(expression); + const tKeyExpr = ctx.tKeyExpr; + if (block.parentVar) { + let keyArg = `key${this.target.loopLevel}`; + if (tKeyExpr) { + keyArg = `${tKeyExpr} + ${keyArg}`; + } + this.helpers.add("withKey"); + this.addLine(`${block.parentVar}[${ctx.index}] = withKey(${blockExpr}, ${keyArg});`); + return; + } + if (tKeyExpr) { + blockExpr = `toggler(${tKeyExpr}, ${blockExpr})`; + } + if (block.isRoot && !ctx.preventRoot) { + this.addLine(`return ${blockExpr};`); + } + else { + this.addLine(`let ${block.varName} = ${blockExpr};`); + } } /** - * Prepare an expression for being consumed at render time. Its main job - * is to - * - replace unknown variables by a lookup in the context - * - replace already defined variables by their internal name + * Captures variables that are used inside of an expression. This is useful + * because in compiled code, almost all variables are accessed through the ctx + * object. In the case of functions, that lookup in the context can be delayed + * which can cause issues if the value has changed since the function was + * defined. + * + * @param expr the expression to capture + * @param forceCapture whether the expression should capture its scope even if + * it doesn't contain a function. Useful when the expression will be used as + * a function body. + * @returns a new expression that uses the captured values */ - formatExpression(expr) { - this.rootContext.shouldDefineScope = true; - return compileExpr(expr, this.variables); - } - captureExpression(expr) { - this.rootContext.shouldDefineScope = true; - const argId = this.generateID(); - const tokens = compileExprToArray(expr, this.variables); - const done = new Set(); + captureExpression(expr, forceCapture = false) { + if (!forceCapture && !expr.includes("=>")) { + return compileExpr(expr); + } + const tokens = compileExprToArray(expr); + const mapping = new Map(); return tokens - .map((tok, i) => { - // "this" in captured expressions should be the current component - if (tok.value === "this") { - if (!done.has("this")) { - done.add("this"); - this.addLine(`const this_${argId} = utils.getComponent(context);`); + .map((tok) => { + if (tok.varName && !tok.isLocal) { + if (!mapping.has(tok.varName)) { + const varId = this.generateId("v"); + mapping.set(tok.varName, varId); + this.addLine(`const ${varId} = ${tok.value};`); } - tok.value = `this_${argId}`; - } - // Variables that should be looked up in the scope. isLocal is for arrow - // function arguments that should stay untouched (eg "ev => ev" should - // not become "const ev_1 = scope['ev']; ev_1 => ev_1") - if (tok.varName && - !tok.isLocal && - // HACK: for backwards compatibility, we don't capture bare methods - // this allows them to be called with the rendering context/scope - // as their this value. - (!tokens[i + 1] || tokens[i + 1].type !== "LEFT_PAREN")) { - if (!done.has(tok.varName)) { - done.add(tok.varName); - this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`); - } - tok.value = `${tok.varName}_${argId}`; + tok.value = mapping.get(tok.varName); } return tok.value; }) .join(""); } + compileAST(ast, ctx) { + switch (ast.type) { + case 1 /* Comment */: + this.compileComment(ast, ctx); + break; + case 0 /* Text */: + this.compileText(ast, ctx); + break; + case 2 /* DomNode */: + this.compileTDomNode(ast, ctx); + break; + case 4 /* TEsc */: + this.compileTEsc(ast, ctx); + break; + case 8 /* TOut */: + this.compileTOut(ast, ctx); + break; + case 5 /* TIf */: + this.compileTIf(ast, ctx); + break; + case 9 /* TForEach */: + this.compileTForeach(ast, ctx); + break; + case 10 /* TKey */: + this.compileTKey(ast, ctx); + break; + case 3 /* Multi */: + this.compileMulti(ast, ctx); + break; + case 7 /* TCall */: + this.compileTCall(ast, ctx); + break; + case 15 /* TCallBlock */: + this.compileTCallBlock(ast, ctx); + break; + case 6 /* TSet */: + this.compileTSet(ast, ctx); + break; + case 11 /* TComponent */: + this.compileComponent(ast, ctx); + break; + case 12 /* TDebug */: + this.compileDebug(ast, ctx); + break; + case 13 /* TLog */: + this.compileLog(ast, ctx); + break; + case 14 /* TSlot */: + this.compileTSlot(ast, ctx); + break; + case 16 /* TTranslation */: + this.compileTTranslation(ast, ctx); + break; + case 17 /* TPortal */: + this.compileTPortal(ast, ctx); + } + } + compileDebug(ast, ctx) { + this.addLine(`debugger;`); + if (ast.content) { + this.compileAST(ast.content, ctx); + } + } + compileLog(ast, ctx) { + this.addLine(`console.log(${compileExpr(ast.expr)});`); + if (ast.content) { + this.compileAST(ast.content, ctx); + } + } + compileComment(ast, ctx) { + let { block, forceNewBlock } = ctx; + const isNewBlock = !block || forceNewBlock; + if (isNewBlock) { + block = this.createBlock(block, "comment", ctx); + this.insertBlock(`comment(\`${ast.value}\`)`, block, { + ...ctx, + forceNewBlock: forceNewBlock && !block, + }); + } + else { + const text = xmlDoc.createComment(ast.value); + block.insert(text); + } + } + compileText(ast, ctx) { + let { block, forceNewBlock } = ctx; + let value = ast.value; + if (value && ctx.translate !== false) { + const match = translationRE.exec(value); + value = match[1] + this.translateFn(match[2]) + match[3]; + } + if (!block || forceNewBlock) { + block = this.createBlock(block, "text", ctx); + this.insertBlock(`text(\`${value}\`)`, block, { + ...ctx, + forceNewBlock: forceNewBlock && !block, + }); + } + else { + const createFn = ast.type === 0 /* Text */ ? xmlDoc.createTextNode : xmlDoc.createComment; + block.insert(createFn.call(xmlDoc, value)); + } + } + generateHandlerCode(rawEvent, handler) { + const modifiers = rawEvent + .split(".") + .slice(1) + .map((m) => { + if (!MODS.has(m)) { + throw new Error(`Unknown event modifier: '${m}'`); + } + return `"${m}"`; + }); + let modifiersCode = ""; + if (modifiers.length) { + modifiersCode = `${modifiers.join(",")}, `; + } + return `[${modifiersCode}${this.captureExpression(handler)}, ctx]`; + } + compileTDomNode(ast, ctx) { + let { block, forceNewBlock } = ctx; + const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns; + let codeIdx = this.target.code.length; + if (isNewBlock) { + if ((ast.dynamicTag || ctx.tKeyExpr || ast.ns) && ctx.block) { + this.insertAnchor(ctx.block); + } + block = this.createBlock(block, "block", ctx); + this.blocks.push(block); + if (ast.dynamicTag) { + const tagExpr = this.generateId("tag"); + this.addLine(`let ${tagExpr} = ${compileExpr(ast.dynamicTag)};`); + block.dynamicTagName = tagExpr; + } + } + // attributes + const attrs = {}; + const nameSpace = ast.ns || ctx.nameSpace; + if (nameSpace && isNewBlock) { + // specific namespace uri + attrs["block-ns"] = nameSpace; + } + for (let key in ast.attrs) { + let expr, attrName; + if (key.startsWith("t-attf")) { + expr = interpolate(ast.attrs[key]); + const idx = block.insertData(expr, "attr"); + attrName = key.slice(7); + attrs["block-attribute-" + idx] = attrName; + } + else if (key.startsWith("t-att")) { + expr = compileExpr(ast.attrs[key]); + const idx = block.insertData(expr, "attr"); + if (key === "t-att") { + attrs[`block-attributes`] = String(idx); + } + else { + attrName = key.slice(6); + attrs[`block-attribute-${idx}`] = attrName; + } + } + else if (this.translatableAttributes.includes(key)) { + attrs[key] = this.translateFn(ast.attrs[key]); + } + else { + expr = `"${ast.attrs[key]}"`; + attrName = key; + attrs[key] = ast.attrs[key]; + } + if (attrName === "value" && ctx.tModelSelectedExpr) { + let selectedId = block.insertData(`${ctx.tModelSelectedExpr} === ${expr}`, "attr"); + attrs[`block-attribute-${selectedId}`] = "selected"; + } + } + // event handlers + for (let ev in ast.on) { + const name = this.generateHandlerCode(ev, ast.on[ev]); + const idx = block.insertData(name, "hdlr"); + attrs[`block-handler-${idx}`] = ev; + } + // t-ref + if (ast.ref) { + this.target.hasRef = true; + const isDynamic = INTERP_REGEXP.test(ast.ref); + if (isDynamic) { + const str = ast.ref.replace(INTERP_REGEXP, (expr) => "${" + this.captureExpression(expr.slice(2, -2), true) + "}"); + const idx = block.insertData(`(el) => refs[\`${str}\`] = el`, "ref"); + attrs["block-ref"] = String(idx); + } + else { + let name = ast.ref; + if (name in this.target.refInfo) { + // ref has already been defined + this.helpers.add("multiRefSetter"); + const info = this.target.refInfo[name]; + const index = block.data.push(info[0]) - 1; + attrs["block-ref"] = String(index); + info[1] = `multiRefSetter(refs, \`${name}\`)`; + } + else { + let id = this.generateId("ref"); + this.target.refInfo[name] = [id, `(el) => refs[\`${name}\`] = el`]; + const index = block.data.push(id) - 1; + attrs["block-ref"] = String(index); + } + } + } + // t-model + let tModelSelectedExpr; + if (ast.model) { + const { hasDynamicChildren, baseExpr, expr, eventType, shouldNumberize, shouldTrim, targetAttr, specialInitTargetAttr, } = ast.model; + const baseExpression = compileExpr(baseExpr); + const bExprId = this.generateId("bExpr"); + this.addLine(`const ${bExprId} = ${baseExpression};`); + const expression = compileExpr(expr); + const exprId = this.generateId("expr"); + this.addLine(`const ${exprId} = ${expression};`); + const fullExpression = `${bExprId}[${exprId}]`; + let idx; + if (specialInitTargetAttr) { + idx = block.insertData(`${fullExpression} === '${attrs[targetAttr]}'`, "attr"); + attrs[`block-attribute-${idx}`] = specialInitTargetAttr; + } + else if (hasDynamicChildren) { + const bValueId = this.generateId("bValue"); + tModelSelectedExpr = `${bValueId}`; + this.addLine(`let ${tModelSelectedExpr} = ${fullExpression}`); + } + else { + idx = block.insertData(`${fullExpression}`, "attr"); + attrs[`block-attribute-${idx}`] = targetAttr; + } + this.helpers.add("toNumber"); + let valueCode = `ev.target.${targetAttr}`; + valueCode = shouldTrim ? `${valueCode}.trim()` : valueCode; + valueCode = shouldNumberize ? `toNumber(${valueCode})` : valueCode; + const handler = `[(ev) => { ${fullExpression} = ${valueCode}; }]`; + idx = block.insertData(handler, "hdlr"); + attrs[`block-handler-${idx}`] = eventType; + } + const dom = xmlDoc.createElement(ast.tag); + for (const [attr, val] of Object.entries(attrs)) { + if (!(attr === "class" && val === "")) { + dom.setAttribute(attr, val); + } + } + block.insert(dom); + if (ast.content.length) { + const initialDom = block.currentDom; + block.currentDom = dom; + const children = ast.content; + for (let i = 0; i < children.length; i++) { + const child = ast.content[i]; + const subCtx = createContext(ctx, { + block, + index: block.childNumber, + forceNewBlock: false, + isLast: ctx.isLast && i === children.length - 1, + tKeyExpr: ctx.tKeyExpr, + nameSpace, + tModelSelectedExpr, + }); + this.compileAST(child, subCtx); + } + block.currentDom = initialDom; + } + if (isNewBlock) { + this.insertBlock(`${block.blockName}(ddd)`, block, ctx); + // may need to rewrite code! + if (block.children.length && block.hasDynamicChildren) { + const code = this.target.code; + const children = block.children.slice(); + let current = children.shift(); + for (let i = codeIdx; i < code.length; i++) { + if (code[i].trimStart().startsWith(`let ${current.varName} `)) { + code[i] = code[i].replace(`let ${current.varName}`, current.varName); + current = children.shift(); + if (!current) + break; + } + } + this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx); + } + } + } + compileTEsc(ast, ctx) { + let { block, forceNewBlock } = ctx; + let expr; + if (ast.expr === "0") { + this.helpers.add("zero"); + expr = `ctx[zero]`; + } + else { + expr = compileExpr(ast.expr); + if (ast.defaultValue) { + this.helpers.add("withDefault"); + expr = `withDefault(${expr}, \`${ast.defaultValue}\`)`; + } + } + if (!block || forceNewBlock) { + block = this.createBlock(block, "text", ctx); + this.insertBlock(`text(${expr})`, block, { ...ctx, forceNewBlock: forceNewBlock && !block }); + } + else { + const idx = block.insertData(expr, "txt"); + const text = xmlDoc.createElement(`block-text-${idx}`); + block.insert(text); + } + } + compileTOut(ast, ctx) { + let { block } = ctx; + if (block) { + this.insertAnchor(block); + } + block = this.createBlock(block, "html", ctx); + this.helpers.add(ast.expr === "0" ? "zero" : "safeOutput"); + let expr = ast.expr === "0" ? "ctx[zero]" : `safeOutput(${compileExpr(ast.expr)})`; + if (ast.body) { + const nextId = BlockDescription.nextBlockId; + const subCtx = createContext(ctx); + this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx); + this.helpers.add("withDefault"); + expr = `withDefault(${expr}, b${nextId})`; + } + this.insertBlock(`${expr}`, block, ctx); + } + compileTIf(ast, ctx, nextNode) { + let { block, forceNewBlock, index } = ctx; + let currentIndex = index; + const codeIdx = this.target.code.length; + const isNewBlock = !block || (block.type !== "multi" && forceNewBlock); + if (block) { + block.hasDynamicChildren = true; + } + if (!block || (block.type !== "multi" && forceNewBlock)) { + block = this.createBlock(block, "multi", ctx); + } + this.addLine(`if (${compileExpr(ast.condition)}) {`); + this.target.indentLevel++; + this.insertAnchor(block); + const subCtx = createContext(ctx, { block, index: currentIndex }); + this.compileAST(ast.content, subCtx); + this.target.indentLevel--; + if (ast.tElif) { + for (let clause of ast.tElif) { + this.addLine(`} else if (${compileExpr(clause.condition)}) {`); + this.target.indentLevel++; + this.insertAnchor(block); + const subCtx = createContext(ctx, { block, index: currentIndex }); + this.compileAST(clause.content, subCtx); + this.target.indentLevel--; + } + } + if (ast.tElse) { + this.addLine(`} else {`); + this.target.indentLevel++; + this.insertAnchor(block); + const subCtx = createContext(ctx, { block, index: currentIndex }); + this.compileAST(ast.tElse, subCtx); + this.target.indentLevel--; + } + this.addLine("}"); + if (isNewBlock) { + // note: this part is duplicated from end of compiledomnode: + if (block.children.length) { + const code = this.target.code; + const children = block.children.slice(); + let current = children.shift(); + for (let i = codeIdx; i < code.length; i++) { + if (code[i].trimStart().startsWith(`let ${current.varName} `)) { + code[i] = code[i].replace(`let ${current.varName}`, current.varName); + current = children.shift(); + if (!current) + break; + } + } + this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx); + } + // note: this part is duplicated from end of compilemulti: + const args = block.children.map((c) => c.varName).join(", "); + this.insertBlock(`multi([${args}])`, block, ctx); + } + } + compileTForeach(ast, ctx) { + let { block } = ctx; + if (block) { + this.insertAnchor(block); + } + block = this.createBlock(block, "list", ctx); + this.target.loopLevel++; + const loopVar = `i${this.target.loopLevel}`; + this.addLine(`ctx = Object.create(ctx);`); + const vals = `v_block${block.id}`; + const keys = `k_block${block.id}`; + const l = `l_block${block.id}`; + const c = `c_block${block.id}`; + this.helpers.add("prepareList"); + this.addLine(`const [${keys}, ${vals}, ${l}, ${c}] = prepareList(${compileExpr(ast.collection)});`); + // Throw errors on duplicate keys in dev mode + if (this.dev) { + this.addLine(`const keys${block.id} = new Set();`); + } + this.addLine(`for (let ${loopVar} = 0; ${loopVar} < ${l}; ${loopVar}++) {`); + this.target.indentLevel++; + this.addLine(`ctx[\`${ast.elem}\`] = ${vals}[${loopVar}];`); + if (!ast.hasNoFirst) { + this.addLine(`ctx[\`${ast.elem}_first\`] = ${loopVar} === 0;`); + } + if (!ast.hasNoLast) { + this.addLine(`ctx[\`${ast.elem}_last\`] = ${loopVar} === ${vals}.length - 1;`); + } + if (!ast.hasNoIndex) { + this.addLine(`ctx[\`${ast.elem}_index\`] = ${loopVar};`); + } + if (!ast.hasNoValue) { + this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`); + } + this.addLine(`let key${this.target.loopLevel} = ${ast.key ? compileExpr(ast.key) : loopVar};`); + if (this.dev) { + // Throw error on duplicate keys in dev mode + this.addLine(`if (keys${block.id}.has(key${this.target.loopLevel})) { throw new Error(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`); + this.addLine(`keys${block.id}.add(key${this.target.loopLevel});`); + } + let id; + if (ast.memo) { + this.target.hasCache = true; + id = this.generateId(); + this.addLine(`let memo${id} = ${compileExpr(ast.memo)}`); + this.addLine(`let vnode${id} = cache[key${this.target.loopLevel}];`); + this.addLine(`if (vnode${id}) {`); + this.target.indentLevel++; + this.addLine(`if (shallowEqual(vnode${id}.memo, memo${id})) {`); + this.target.indentLevel++; + this.addLine(`${c}[${loopVar}] = vnode${id};`); + this.addLine(`nextCache[key${this.target.loopLevel}] = vnode${id};`); + this.addLine(`continue;`); + this.target.indentLevel--; + this.addLine("}"); + this.target.indentLevel--; + this.addLine("}"); + } + const subCtx = createContext(ctx, { block, index: loopVar }); + this.compileAST(ast.body, subCtx); + if (ast.memo) { + this.addLine(`nextCache[key${this.target.loopLevel}] = Object.assign(${c}[${loopVar}], {memo: memo${id}});`); + } + this.target.indentLevel--; + this.target.loopLevel--; + this.addLine(`}`); + if (!ctx.isLast) { + this.addLine(`ctx = ctx.__proto__;`); + } + this.insertBlock("l", block, ctx); + } + compileTKey(ast, ctx) { + const tKeyExpr = this.generateId("tKey_"); + this.addLine(`const ${tKeyExpr} = ${compileExpr(ast.expr)};`); + ctx = createContext(ctx, { + tKeyExpr, + block: ctx.block, + index: ctx.index, + }); + this.compileAST(ast.content, ctx); + } + compileMulti(ast, ctx) { + let { block, forceNewBlock } = ctx; + const isNewBlock = !block || forceNewBlock; + let codeIdx = this.target.code.length; + if (isNewBlock) { + const n = ast.content.filter((c) => c.type !== 6 /* TSet */).length; + if (n <= 1) { + for (let child of ast.content) { + this.compileAST(child, ctx); + } + return; + } + block = this.createBlock(block, "multi", ctx); + } + let index = 0; + for (let i = 0, l = ast.content.length; i < l; i++) { + const child = ast.content[i]; + const isTSet = child.type === 6 /* TSet */; + const subCtx = createContext(ctx, { + block, + index, + forceNewBlock: !isTSet, + preventRoot: ctx.preventRoot, + isLast: ctx.isLast && i === l - 1, + }); + this.compileAST(child, subCtx); + if (!isTSet) { + index++; + } + } + if (isNewBlock) { + if (block.hasDynamicChildren) { + if (block.children.length) { + const code = this.target.code; + const children = block.children.slice(); + let current = children.shift(); + for (let i = codeIdx; i < code.length; i++) { + if (code[i].trimStart().startsWith(`let ${current.varName} `)) { + code[i] = code[i].replace(`let ${current.varName}`, current.varName); + current = children.shift(); + if (!current) + break; + } + } + this.target.addLine(`let ${block.children.map((c) => c.varName)};`, codeIdx); + } + } + const args = block.children.map((c) => c.varName).join(", "); + this.insertBlock(`multi([${args}])`, block, ctx); + } + } + compileTCall(ast, ctx) { + let { block, forceNewBlock } = ctx; + if (ast.body) { + this.addLine(`ctx = Object.create(ctx);`); + this.addLine(`ctx[isBoundary] = 1;`); + this.helpers.add("isBoundary"); + const nextId = BlockDescription.nextBlockId; + const subCtx = createContext(ctx, { preventRoot: true }); + this.compileAST({ type: 3 /* Multi */, content: ast.body }, subCtx); + if (nextId !== BlockDescription.nextBlockId) { + this.helpers.add("zero"); + this.addLine(`ctx[zero] = b${nextId};`); + } + } + const isDynamic = INTERP_REGEXP.test(ast.name); + const subTemplate = isDynamic ? interpolate(ast.name) : "`" + ast.name + "`"; + if (block) { + if (!forceNewBlock) { + this.insertAnchor(block); + } + } + const key = `key + \`${this.generateComponentKey()}\``; + if (isDynamic) { + const templateVar = this.generateId("template"); + this.addLine(`const ${templateVar} = ${subTemplate};`); + block = this.createBlock(block, "multi", ctx); + this.helpers.add("call"); + this.insertBlock(`call(this, ${templateVar}, ctx, node, ${key})`, block, { + ...ctx, + forceNewBlock: !block, + }); + } + else { + const id = this.generateId(`callTemplate_`); + this.helpers.add("getTemplate"); + this.staticCalls.push({ id, template: subTemplate }); + block = this.createBlock(block, "multi", ctx); + this.insertBlock(`${id}.call(this, ctx, node, ${key})`, block, { + ...ctx, + forceNewBlock: !block, + }); + } + if (ast.body && !ctx.isLast) { + this.addLine(`ctx = ctx.__proto__;`); + } + } + compileTCallBlock(ast, ctx) { + let { block, forceNewBlock } = ctx; + if (block) { + if (!forceNewBlock) { + this.insertAnchor(block); + } + } + block = this.createBlock(block, "multi", ctx); + this.insertBlock(compileExpr(ast.name), block, { ...ctx, forceNewBlock: !block }); + } + compileTSet(ast, ctx) { + this.target.shouldProtectScope = true; + this.helpers.add("isBoundary").add("withDefault"); + const expr = ast.value ? compileExpr(ast.value || "") : "null"; + if (ast.body) { + this.helpers.add("LazyValue"); + const bodyAst = { type: 3 /* Multi */, content: ast.body }; + const name = this.compileInNewTarget("value", bodyAst, ctx); + let value = `new LazyValue(${name}, ctx, node)`; + value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value; + this.addLine(`ctx[\`${ast.name}\`] = ${value};`); + } + else { + let value; + if (ast.defaultValue) { + if (ast.value) { + value = `withDefault(${expr}, \`${ast.defaultValue}\`)`; + } + else { + value = `\`${ast.defaultValue}\``; + } + } + else { + value = expr; + } + this.helpers.add("setContextValue"); + this.addLine(`setContextValue(ctx, "${ast.name}", ${value});`); + } + } + generateComponentKey() { + const parts = [this.generateId("__")]; + for (let i = 0; i < this.target.loopLevel; i++) { + parts.push(`\${key${i + 1}}`); + } + return parts.join("__"); + } /** - * Perform string interpolation on the given string. Note that if the whole - * string is an expression, it simply returns it (formatted and enclosed in - * parentheses). - * For instance: - * 'Hello {{x}}!' -> `Hello ${x}` - * '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b') + * Formats a prop name and value into a string suitable to be inserted in the + * generated code. For example: + * + * Name Value Result + * --------------------------------------------------------- + * "number" "state" "number: ctx['state']" + * "something" "" "something: undefined" + * "some-prop" "state" "'some-prop': ctx['state']" + * "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])" */ - interpolate(s) { - let matches = s.match(INTERP_REGEXP); - if (matches && matches[0].length === s.length) { - return `(${this.formatExpression(s.slice(2, -2))})`; - } - let r = s.replace(/\{\{.*?\}\}/g, (s) => "${" + this.formatExpression(s.slice(2, -2)) + "}"); - return "`" + r + "`"; - } - startProtectScope(codeBlock) { - const protectID = this.generateID(); - this.rootContext.protectedScopeNumber++; - this.rootContext.shouldDefineScope = true; - const scopeExpr = `Object.create(scope);`; - this.addLine(`let _origScope${protectID} = scope;`); - this.addLine(`scope = ${scopeExpr}`); - if (!codeBlock) { - this.addLine(`scope.__access_mode__ = 'ro';`); - } - return protectID; - } - stopProtectScope(protectID) { - this.rootContext.protectedScopeNumber--; - this.addLine(`scope = _origScope${protectID};`); - } - } - CompilationContext.nextID = 1; - - //------------------------------------------------------------------------------ - // module/props.ts - //------------------------------------------------------------------------------ - function updateProps(oldVnode, vnode) { - var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; - if (!oldProps && !props) - return; - if (oldProps === props) - return; - oldProps = oldProps || {}; - props = props || {}; - for (key in oldProps) { - if (!props[key]) { - delete elm[key]; - } - } - for (key in props) { - cur = props[key]; - old = oldProps[key]; - if (old !== cur && (key !== "value" || elm[key] !== cur)) { - elm[key] = cur; - } - } - } - const propsModule = { - create: updateProps, - update: updateProps, - }; - //------------------------------------------------------------------------------ - // module/eventlisteners.ts - //------------------------------------------------------------------------------ - function invokeHandler(handler, vnode, event) { - if (typeof handler === "function") { - // call function handler - handler.call(vnode, event, vnode); - } - else if (typeof handler === "object") { - // call handler with arguments - if (typeof handler[0] === "function") { - // special case for single argument for performance - if (handler.length === 2) { - handler[0].call(vnode, handler[1], event, vnode); + formatProp(name, value) { + value = this.captureExpression(value); + if (name.includes(".")) { + let [_name, suffix] = name.split("."); + if (suffix === "bind") { + this.helpers.add("bind"); + name = _name; + value = `bind(ctx, ${value || undefined})`; } else { - var args = handler.slice(1); - args.push(event); - args.push(vnode); - handler[0].apply(vnode, args); + throw new Error("Invalid prop suffix"); } } - else { - // call multiple handlers - for (let i = 0, iLen = handler.length; i < iLen; i++) { - invokeHandler(handler[i], vnode, event); + name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`; + return `${name}: ${value || undefined}`; + } + formatPropObject(obj) { + const params = []; + for (const [n, v] of Object.entries(obj)) { + params.push(this.formatProp(n, v)); + } + return params.join(", "); + } + compileComponent(ast, ctx) { + let { block } = ctx; + // props + const hasSlotsProp = "slots" in ast.props; + const props = []; + const propExpr = this.formatPropObject(ast.props); + if (propExpr) { + props.push(propExpr); + } + // slots + const hasSlot = !!Object.keys(ast.slots).length; + let slotDef = ""; + if (hasSlot) { + let ctxStr = "ctx"; + if (this.target.loopLevel || !this.hasSafeContext) { + ctxStr = this.generateId("ctx"); + this.helpers.add("capture"); + this.addLine(`const ${ctxStr} = capture(ctx);`); } - } - } - } - function handleEvent(event, vnode) { - var name = event.type, on = vnode.data.on; - // call event handler(s) if exists - if (on) { - if (on[name]) { - invokeHandler(on[name], vnode, event); - } - else if (on["!" + name]) { - invokeHandler(on["!" + name], vnode, event); - } - } - } - function createListener() { - return function handler(event) { - handleEvent(event, handler.vnode); - }; - } - function updateEventListeners(oldVnode, vnode) { - var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = (vnode && vnode.elm), name; - // optimization for reused immutable handlers - if (oldOn === on) { - return; - } - // remove existing listeners which no longer used - if (oldOn && oldListener) { - // if element changed or deleted we remove all existing listeners unconditionally - if (!on) { - for (name in oldOn) { - // remove listener if element was changed or existing listeners removed - const capture = name.charAt(0) === "!"; - name = capture ? name.slice(1) : name; - oldElm.removeEventListener(name, oldListener, capture); - } - } - else { - for (name in oldOn) { - // remove listener if existing listener removed - if (!on[name]) { - const capture = name.charAt(0) === "!"; - name = capture ? name.slice(1) : name; - oldElm.removeEventListener(name, oldListener, capture); + let slotStr = []; + for (let slotName in ast.slots) { + const slotAst = ast.slots[slotName].content; + const name = this.compileInNewTarget("slot", slotAst, ctx); + const params = [`__render: ${name}, __ctx: ${ctxStr}`]; + const scope = ast.slots[slotName].scope; + if (scope) { + params.push(`__scope: "${scope}"`); } - } - } - } - // add new listeners which has not already attached - if (on) { - // reuse existing listener or create new - var listener = (vnode.listener = oldVnode.listener || createListener()); - // update vnode for listener - listener.vnode = vnode; - // if element changed or added we add all needed listeners unconditionally - if (!oldOn) { - for (name in on) { - // add listener if element was changed or new listeners added - const capture = name.charAt(0) === "!"; - name = capture ? name.slice(1) : name; - elm.addEventListener(name, listener, capture); - } - } - else { - for (name in on) { - // add listener if new listener added - if (!oldOn[name]) { - const capture = name.charAt(0) === "!"; - name = capture ? name.slice(1) : name; - elm.addEventListener(name, listener, capture); + if (ast.slots[slotName].attrs) { + params.push(this.formatPropObject(ast.slots[slotName].attrs)); } + const slotInfo = `{${params.join(", ")}}`; + slotStr.push(`'${slotName}': ${slotInfo}`); } + slotDef = `{${slotStr.join(", ")}}`; } - } - } - const eventListenersModule = { - create: updateEventListeners, - update: updateEventListeners, - destroy: updateEventListeners, - }; - //------------------------------------------------------------------------------ - // attributes.ts - //------------------------------------------------------------------------------ - const xlinkNS = "http://www.w3.org/1999/xlink"; - const xmlNS = "http://www.w3.org/XML/1998/namespace"; - const colonChar = 58; - const xChar = 120; - function updateAttrs(oldVnode, vnode) { - var key, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs; - if (!oldAttrs && !attrs) - return; - if (oldAttrs === attrs) - return; - oldAttrs = oldAttrs || {}; - attrs = attrs || {}; - // update modified attributes, add new attributes - for (key in attrs) { - const cur = attrs[key]; - const old = oldAttrs[key]; - if (old !== cur) { - if (cur === true) { - elm.setAttribute(key, ""); - } - else if (cur === false) { - elm.removeAttribute(key); + if (slotDef && !(ast.dynamicProps || hasSlotsProp)) { + props.push(`slots: ${slotDef}`); + } + const propStr = `{${props.join(",")}}`; + let propString = propStr; + if (ast.dynamicProps) { + if (!props.length) { + propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)})`; } else { - if (key.charCodeAt(0) !== xChar) { - elm.setAttribute(key, cur); - } - else if (key.charCodeAt(3) === colonChar) { - // Assume xml namespace - elm.setAttributeNS(xmlNS, key, cur); - } - else if (key.charCodeAt(5) === colonChar) { - // Assume xlink namespace - elm.setAttributeNS(xlinkNS, key, cur); - } - else { - elm.setAttribute(key, cur); - } + propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`; } } - } - // remove removed attributes - // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) - // the other option is to remove all attributes with value == undefined - for (key in oldAttrs) { - if (!(key in attrs)) { - elm.removeAttribute(key); + let propVar; + if ((slotDef && (ast.dynamicProps || hasSlotsProp)) || this.dev) { + propVar = this.generateId("props"); + this.addLine(`const ${propVar} = ${propString};`); + propString = propVar; } - } - } - const attrsModule = { - create: updateAttrs, - update: updateAttrs, - }; - //------------------------------------------------------------------------------ - // class.ts - //------------------------------------------------------------------------------ - function updateClass(oldVnode, vnode) { - var cur, name, elm, oldClass = oldVnode.data.class, klass = vnode.data.class; - if (!oldClass && !klass) - return; - if (oldClass === klass) - return; - oldClass = oldClass || {}; - klass = klass || {}; - elm = vnode.elm; - for (name in oldClass) { - if (name && !klass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) { - // was `true` and now not provided - elm.classList.remove(name); + if (slotDef && (ast.dynamicProps || hasSlotsProp)) { + this.addLine(`${propVar}.slots = Object.assign(${slotDef}, ${propVar}.slots)`); } - } - for (name in klass) { - cur = klass[name]; - if (cur !== oldClass[name]) { - elm.classList[cur ? "add" : "remove"](name); - } - } - } - const classModule = { create: updateClass, update: updateClass }; - - /** - * Owl VDOM - * - * This file contains an implementation of a virtual DOM, which is a system that - * can generate in-memory representations of a DOM tree, compare them, and - * eventually change a concrete DOM tree to match its representation, in an - * hopefully efficient way. - * - * Note that this code is a fork of Snabbdom, slightly tweaked/optimized for our - * needs (see https://github.com/snabbdom/snabbdom). - * - * The main exported values are: - * - interface VNode - * - h function (a helper function to generate a vnode) - * - patch function (to apply a vnode to an actual DOM node) - */ - function vnode(sel, data, children, text, elm) { - let key = data === undefined ? undefined : data.key; - return { sel, data, children, text, elm, key }; - } - //------------------------------------------------------------------------------ - // snabbdom.ts - //------------------------------------------------------------------------------ - function isUndef(s) { - return s === undefined; - } - function isDef(s) { - return s !== undefined; - } - const emptyNode = vnode("", {}, [], undefined, undefined); - function sameVnode(vnode1, vnode2) { - return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; - } - function isVnode(vnode) { - return vnode.sel !== undefined; - } - function createKeyToOldIdx(children, beginIdx, endIdx) { - let i, map = {}, key, ch; - for (i = beginIdx; i <= endIdx; ++i) { - ch = children[i]; - if (ch != null) { - key = ch.key; - if (key !== undefined) - map[key] = i; - } - } - return map; - } - const hooks$1 = ["create", "update", "remove", "destroy", "pre", "post"]; - function init(modules, domApi) { - let i, j, cbs = {}; - const api = domApi !== undefined ? domApi : htmlDomApi; - for (i = 0; i < hooks$1.length; ++i) { - cbs[hooks$1[i]] = []; - for (j = 0; j < modules.length; ++j) { - const hook = modules[j][hooks$1[i]]; - if (hook !== undefined) { - cbs[hooks$1[i]].push(hook); - } - } - } - function emptyNodeAt(elm) { - const id = elm.id ? "#" + elm.id : ""; - const c = elm.className ? "." + elm.className.split(" ").join(".") : ""; - return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); - } - function createRmCb(childElm, listeners) { - return function rmCb() { - if (--listeners === 0) { - const parent = api.parentNode(childElm); - api.removeChild(parent, childElm); - } - }; - } - function createElm(vnode, insertedVnodeQueue) { - let i, iLen, data = vnode.data; - if (data !== undefined) { - if (isDef((i = data.hook)) && isDef((i = i.init))) { - i(vnode); - data = vnode.data; - } - } - let children = vnode.children, sel = vnode.sel; - if (sel === "!") { - if (isUndef(vnode.text)) { - vnode.text = ""; - } - vnode.elm = api.createComment(vnode.text); - } - else if (sel !== undefined) { - const elm = vnode.elm || - (vnode.elm = - isDef(data) && isDef((i = data.ns)) - ? api.createElementNS(i, sel) - : api.createElement(sel)); - for (i = 0, iLen = cbs.create.length; i < iLen; ++i) - cbs.create[i](emptyNode, vnode); - if (array(children)) { - for (i = 0, iLen = children.length; i < iLen; ++i) { - const ch = children[i]; - if (ch != null) { - api.appendChild(elm, createElm(ch, insertedVnodeQueue)); - } - } - } - else if (primitive(vnode.text)) { - api.appendChild(elm, api.createTextNode(vnode.text)); - } - i = vnode.data.hook; // Reuse variable - if (isDef(i)) { - if (i.create) - i.create(emptyNode, vnode); - if (i.insert) - insertedVnodeQueue.push(vnode); - } + // cmap key + const key = this.generateComponentKey(); + let expr; + if (ast.isDynamic) { + expr = this.generateId("Comp"); + this.addLine(`let ${expr} = ${compileExpr(ast.name)};`); } else { - vnode.elm = api.createTextNode(vnode.text); + expr = `\`${ast.name}\``; } - return vnode.elm; - } - function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { - for (; startIdx <= endIdx; ++startIdx) { - const ch = vnodes[startIdx]; - if (ch != null) { - api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); - } + if (this.dev) { + this.addLine(`helpers.validateProps(${expr}, ${propVar}, ctx);`); } - } - function invokeDestroyHook(vnode) { - let i, iLen, j, jLen, data = vnode.data; - if (data !== undefined) { - if (isDef((i = data.hook)) && isDef((i = i.destroy))) - i(vnode); - for (i = 0, iLen = cbs.destroy.length; i < iLen; ++i) - cbs.destroy[i](vnode); - if (vnode.children !== undefined) { - for (j = 0, jLen = vnode.children.length; j < jLen; ++j) { - i = vnode.children[j]; - if (i != null && typeof i !== "string") { - invokeDestroyHook(i); - } - } - } + if (block && (ctx.forceNewBlock === false || ctx.tKeyExpr)) { + // todo: check the forcenewblock condition + this.insertAnchor(block); } - } - function removeVnodes(parentElm, vnodes, startIdx, endIdx) { - for (; startIdx <= endIdx; ++startIdx) { - let i, iLen, listeners, rm, ch = vnodes[startIdx]; - if (ch != null) { - if (isDef(ch.sel)) { - invokeDestroyHook(ch); - listeners = cbs.remove.length + 1; - rm = createRmCb(ch.elm, listeners); - for (i = 0, iLen = cbs.remove.length; i < iLen; ++i) - cbs.remove[i](ch, rm); - if (isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove))) { - i(ch, rm); - } - else { - rm(); - } - } - else { - // Text node - api.removeChild(parentElm, ch.elm); - } - } + let keyArg = `key + \`${key}\``; + if (ctx.tKeyExpr) { + keyArg = `${ctx.tKeyExpr} + ${keyArg}`; } + const blockArgs = `${expr}, ${propString}, ${keyArg}, node, ctx`; + let blockExpr = `component(${blockArgs})`; + if (ast.isDynamic) { + blockExpr = `toggler(${expr}, ${blockExpr})`; + } + block = this.createBlock(block, "multi", ctx); + this.insertBlock(blockExpr, block, ctx); } - function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { - let oldStartIdx = 0, newStartIdx = 0; - let oldEndIdx = oldCh.length - 1; - let oldStartVnode = oldCh[0]; - let oldEndVnode = oldCh[oldEndIdx]; - let newEndIdx = newCh.length - 1; - let newStartVnode = newCh[0]; - let newEndVnode = newCh[newEndIdx]; - let oldKeyToIdx; - let idxInOld; - let elmToMove; - let before; - while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { - if (oldStartVnode == null) { - oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left - } - else if (oldEndVnode == null) { - oldEndVnode = oldCh[--oldEndIdx]; - } - else if (newStartVnode == null) { - newStartVnode = newCh[++newStartIdx]; - } - else if (newEndVnode == null) { - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldStartVnode, newStartVnode)) { - patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); - oldStartVnode = oldCh[++oldStartIdx]; - newStartVnode = newCh[++newStartIdx]; - } - else if (sameVnode(oldEndVnode, newEndVnode)) { - patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); - oldEndVnode = oldCh[--oldEndIdx]; - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldStartVnode, newEndVnode)) { - // Vnode moved right - patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); - oldStartVnode = oldCh[++oldStartIdx]; - newEndVnode = newCh[--newEndIdx]; - } - else if (sameVnode(oldEndVnode, newStartVnode)) { - // Vnode moved left - patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); - api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); - oldEndVnode = oldCh[--oldEndIdx]; - newStartVnode = newCh[++newStartIdx]; + compileTSlot(ast, ctx) { + this.helpers.add("callSlot"); + let { block } = ctx; + let blockString; + let slotName; + let dynamic = false; + if (ast.name.match(INTERP_REGEXP)) { + dynamic = true; + slotName = interpolate(ast.name); + } + else { + slotName = "'" + ast.name + "'"; + } + const scope = ast.attrs ? `{${this.formatPropObject(ast.attrs)}}` : null; + if (ast.defaultContent) { + const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx); + blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`; + } + else { + if (dynamic) { + let name = this.generateId("slot"); + this.addLine(`const ${name} = ${slotName};`); + blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}), ${dynamic}, ${scope})`; } else { - if (oldKeyToIdx === undefined) { - oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); - } - idxInOld = oldKeyToIdx[newStartVnode.key]; - if (isUndef(idxInOld)) { - // New element - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - newStartVnode = newCh[++newStartIdx]; - } - else { - elmToMove = oldCh[idxInOld]; - if (elmToMove.sel !== newStartVnode.sel) { - api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); - } - else { - patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); - oldCh[idxInOld] = undefined; - api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); - } - newStartVnode = newCh[++newStartIdx]; - } + blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`; } } - if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { - if (oldStartIdx > oldEndIdx) { - before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; - addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); - } - else { - removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); - } + if (block) { + this.insertAnchor(block); + } + block = this.createBlock(block, "multi", ctx); + this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false }); + } + compileTTranslation(ast, ctx) { + if (ast.content) { + this.compileAST(ast.content, Object.assign({}, ctx, { translate: false })); } } - function patchVnode(oldVnode, vnode, insertedVnodeQueue) { - let i, iLen, hook; - if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) { - i(oldVnode, vnode); + compileTPortal(ast, ctx) { + this.helpers.add("Portal"); + let { block } = ctx; + const name = this.compileInNewTarget("slot", ast.content, ctx); + const key = this.generateComponentKey(); + let ctxStr = "ctx"; + if (this.target.loopLevel || !this.hasSafeContext) { + ctxStr = this.generateId("ctx"); + this.helpers.add("capture"); + this.addLine(`const ${ctxStr} = capture(ctx);`); } - const elm = (vnode.elm = oldVnode.elm); - let oldCh = oldVnode.children; - let ch = vnode.children; - if (oldVnode === vnode) - return; - if (vnode.data !== undefined) { - for (i = 0, iLen = cbs.update.length; i < iLen; ++i) - cbs.update[i](oldVnode, vnode); - i = vnode.data.hook; - if (isDef(i) && isDef((i = i.update))) - i(oldVnode, vnode); - } - if (isUndef(vnode.text)) { - if (isDef(oldCh) && isDef(ch)) { - if (oldCh !== ch) - updateChildren(elm, oldCh, ch, insertedVnodeQueue); - } - else if (isDef(ch)) { - if (isDef(oldVnode.text)) - api.setTextContent(elm, ""); - addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); - } - else if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - else if (isDef(oldVnode.text)) { - api.setTextContent(elm, ""); - } - } - else if (oldVnode.text !== vnode.text) { - if (isDef(oldCh)) { - removeVnodes(elm, oldCh, 0, oldCh.length - 1); - } - api.setTextContent(elm, vnode.text); - } - if (isDef(hook) && isDef((i = hook.postpatch))) { - i(oldVnode, vnode); + const blockString = `component(Portal, {target: ${ast.target},slots: {'default': {__render: ${name}, __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx)`; + if (block) { + this.insertAnchor(block); } + block = this.createBlock(block, "multi", ctx); + this.insertBlock(blockString, block, { ...ctx, forceNewBlock: false }); } - return function patch(oldVnode, vnode) { - let i, iLen, elm, parent; - const insertedVnodeQueue = []; - for (i = 0, iLen = cbs.pre.length; i < iLen; ++i) - cbs.pre[i](); - if (!isVnode(oldVnode)) { - oldVnode = emptyNodeAt(oldVnode); - } - if (sameVnode(oldVnode, vnode)) { - patchVnode(oldVnode, vnode, insertedVnodeQueue); - } - else { - elm = oldVnode.elm; - parent = api.parentNode(elm); - createElm(vnode, insertedVnodeQueue); - if (parent !== null) { - api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); - removeVnodes(parent, [oldVnode], 0, 0); - } - } - for (i = 0, iLen = insertedVnodeQueue.length; i < iLen; ++i) { - insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); - } - for (i = 0, iLen = cbs.post.length; i < iLen; ++i) - cbs.post[i](); - return vnode; - }; - } - //------------------------------------------------------------------------------ - // is.ts - //------------------------------------------------------------------------------ - const array = Array.isArray; - function primitive(s) { - return typeof s === "string" || typeof s === "number"; - } - function createElement(tagName) { - return document.createElement(tagName); - } - function createElementNS(namespaceURI, qualifiedName) { - return document.createElementNS(namespaceURI, qualifiedName); - } - function createTextNode(text) { - return document.createTextNode(text); - } - function createComment(text) { - return document.createComment(text); - } - function insertBefore(parentNode, newNode, referenceNode) { - parentNode.insertBefore(newNode, referenceNode); - } - function removeChild(node, child) { - node.removeChild(child); - } - function appendChild(node, child) { - node.appendChild(child); - } - function parentNode(node) { - return node.parentNode; - } - function nextSibling(node) { - return node.nextSibling; - } - function tagName(elm) { - return elm.tagName; - } - function setTextContent(node, text) { - node.textContent = text; - } - const htmlDomApi = { - createElement, - createElementNS, - createTextNode, - createComment, - insertBefore, - removeChild, - appendChild, - parentNode, - nextSibling, - tagName, - setTextContent, - }; - function addNS(data, children, sel) { - if (sel === "dummy") { - // we do not need to add the namespace on dummy elements, they come from a - // subcomponent, which will handle the namespace itself - return; - } - data.ns = "http://www.w3.org/2000/svg"; - if (sel !== "foreignObject" && children !== undefined) { - for (let i = 0, iLen = children.length; i < iLen; ++i) { - const child = children[i]; - let childData = child.data; - if (childData !== undefined) { - addNS(childData, child.children, child.sel); - } - } - } - } - function h(sel, b, c) { - var data = {}, children, text, i, iLen; - if (c !== undefined) { - data = b; - if (array(c)) { - children = c; - } - else if (primitive(c)) { - text = c; - } - else if (c && c.sel) { - children = [c]; - } - } - else if (b !== undefined) { - if (array(b)) { - children = b; - } - else if (primitive(b)) { - text = b; - } - else if (b && b.sel) { - children = [b]; - } - else { - data = b; - } - } - if (children !== undefined) { - for (i = 0, iLen = children.length; i < iLen; ++i) { - if (primitive(children[i])) - children[i] = vnode(undefined, undefined, undefined, children[i], undefined); - } - } - return vnode(sel, data, children, text, undefined); } - const patch = init([eventListenersModule, attrsModule, propsModule, classModule]); - - let localStorage = null; - const browser = { - setTimeout: window.setTimeout.bind(window), - clearTimeout: window.clearTimeout.bind(window), - setInterval: window.setInterval.bind(window), - clearInterval: window.clearInterval.bind(window), - requestAnimationFrame: window.requestAnimationFrame.bind(window), - random: Math.random, - Date: window.Date, - fetch: (window.fetch || (() => { })).bind(window), - get localStorage() { - return localStorage || window.localStorage; - }, - set localStorage(newLocalStorage) { - localStorage = newLocalStorage; - }, - }; - - /** - * Owl Utils - * - * We have here a small collection of utility functions: - * - * - whenReady - * - loadJS - * - loadFile - * - escape - * - debounce - */ - function whenReady(fn) { - return new Promise(function (resolve) { - if (document.readyState !== "loading") { - resolve(); - } - else { - document.addEventListener("DOMContentLoaded", resolve, false); - } - }).then(fn || function () { }); - } - const loadedScripts = {}; - function loadJS(url) { - if (url in loadedScripts) { - return loadedScripts[url]; + // ----------------------------------------------------------------------------- + // AST Type definition + // ----------------------------------------------------------------------------- + // ----------------------------------------------------------------------------- + // Parser + // ----------------------------------------------------------------------------- + const cache = new WeakMap(); + function parse(xml) { + if (typeof xml === "string") { + const elem = parseXML$1(`${xml}`).firstChild; + return _parse(elem); } - const promise = new Promise(function (resolve, reject) { - const script = document.createElement("script"); - script.type = "text/javascript"; - script.src = url; - script.onload = function () { - resolve(); - }; - script.onerror = function () { - reject(`Error loading file '${url}'`); - }; - const head = document.head || document.getElementsByTagName("head")[0]; - head.appendChild(script); - }); - loadedScripts[url] = promise; - return promise; - } - async function loadFile(url) { - const result = await browser.fetch(url); - if (!result.ok) { - throw new Error("Error while fetching xml templates"); + let ast = cache.get(xml); + if (!ast) { + // we clone here the xml to prevent modifying it in place + ast = _parse(xml.cloneNode(true)); + cache.set(xml, ast); } - return await result.text(); + return ast; } - function escape(str) { - if (str === undefined) { - return ""; - } - if (typeof str === "number") { - return String(str); - } - const p = document.createElement("p"); - p.textContent = str; - return p.innerHTML; + function _parse(xml) { + normalizeXML(xml); + const ctx = { inPreTag: false, inSVG: false }; + return parseNode(xml, ctx) || { type: 0 /* Text */, value: "" }; } - /** - * Returns a function, that, as long as it continues to be invoked, will not - * be triggered. The function will be called after it stops being called for - * N milliseconds. If `immediate` is passed, trigger the function on the - * leading edge, instead of the trailing. - * - * Inspired by https://davidwalsh.name/javascript-debounce-function - */ - function debounce(func, wait, immediate) { - let timeout; - return function () { - const context = this; - const args = arguments; - function later() { - timeout = null; - if (!immediate) { - func.apply(context, args); - } - } - const callNow = immediate && !timeout; - browser.clearTimeout(timeout); - timeout = browser.setTimeout(later, wait); - if (callNow) { - func.apply(context, args); - } - }; - } - function shallowEqual(p1, p2) { - for (let k in p1) { - if (p1[k] !== p2[k]) { - return false; - } + function parseNode(node, ctx) { + if (!(node instanceof Element)) { + return parseTextCommentNode(node, ctx); } - return true; - } - - var _utils = /*#__PURE__*/Object.freeze({ - __proto__: null, - whenReady: whenReady, - loadJS: loadJS, - loadFile: loadFile, - escape: escape, - debounce: debounce, - shallowEqual: shallowEqual - }); - - //------------------------------------------------------------------------------ - // Const/global stuff/helpers - //------------------------------------------------------------------------------ - const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"]; + return (parseTDebugLog(node, ctx) || + parseTForEach(node, ctx) || + parseTIf(node, ctx) || + parseTPortal(node, ctx) || + parseTCall(node, ctx) || + parseTCallBlock(node) || + parseTEscNode(node, ctx) || + parseTKey(node, ctx) || + parseTTranslation(node, ctx) || + parseTSlot(node, ctx) || + parseTOutNode(node, ctx) || + parseComponent(node, ctx) || + parseDOMNode(node, ctx) || + parseTSetNode(node, ctx) || + parseTNode(node, ctx)); + } + // ----------------------------------------------------------------------------- + // tag + // ----------------------------------------------------------------------------- + function parseTNode(node, ctx) { + if (node.tagName !== "t") { + return null; + } + return parseChildNodes(node, ctx); + } + // ----------------------------------------------------------------------------- + // Text and Comment Nodes + // ----------------------------------------------------------------------------- const lineBreakRE = /[\r\n]/; const whitespaceRE = /\s+/g; - const translationRE = /^(\s*)([\s\S]+?)(\s*)$/; - const NODE_HOOKS_PARAMS = { - create: "(_, n)", - insert: "vn", - remove: "(vn, rm)", - destroy: "()", - }; - function isComponent(obj) { - return obj && obj.hasOwnProperty("__owl__"); - } - class VDomArray extends Array { - toString() { - return vDomToString(this); + function parseTextCommentNode(node, ctx) { + if (node.nodeType === Node.TEXT_NODE) { + let value = node.textContent || ""; + if (!ctx.inPreTag) { + if (lineBreakRE.test(value) && !value.trim()) { + return null; + } + value = value.replace(whitespaceRE, " "); + } + return { type: 0 /* Text */, value }; } + else if (node.nodeType === Node.COMMENT_NODE) { + return { type: 1 /* Comment */, value: node.textContent || "" }; + } + return null; } - function vDomToString(vdom) { - return vdom - .map((vnode) => { - if (vnode.sel) { - const node = document.createElement(vnode.sel); - const result = patch(node, vnode); - return result.elm.outerHTML; + // ----------------------------------------------------------------------------- + // debugging + // ----------------------------------------------------------------------------- + function parseTDebugLog(node, ctx) { + if (node.hasAttribute("t-debug")) { + node.removeAttribute("t-debug"); + return { + type: 12 /* TDebug */, + content: parseNode(node, ctx), + }; + } + if (node.hasAttribute("t-log")) { + const expr = node.getAttribute("t-log"); + node.removeAttribute("t-log"); + return { + type: 13 /* TLog */, + expr, + content: parseNode(node, ctx), + }; + } + return null; + } + // ----------------------------------------------------------------------------- + // Regular dom node + // ----------------------------------------------------------------------------- + const hasDotAtTheEnd = /\.[\w_]+\s*$/; + const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/; + const ROOT_SVG_TAGS = new Set(["svg", "g", "path"]); + function parseDOMNode(node, ctx) { + const { tagName } = node; + const dynamicTag = node.getAttribute("t-tag"); + node.removeAttribute("t-tag"); + if (tagName === "t" && !dynamicTag) { + return null; + } + ctx = Object.assign({}, ctx); + if (tagName === "pre") { + ctx.inPreTag = true; + } + const shouldAddSVGNS = ROOT_SVG_TAGS.has(tagName) && !ctx.inSVG; + ctx.inSVG = ctx.inSVG || shouldAddSVGNS; + const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null; + const ref = node.getAttribute("t-ref"); + node.removeAttribute("t-ref"); + const nodeAttrsNames = node.getAttributeNames(); + const attrs = {}; + const on = {}; + let model = null; + for (let attr of nodeAttrsNames) { + const value = node.getAttribute(attr); + if (attr.startsWith("t-on")) { + if (attr === "t-on") { + throw new Error("Missing event name with t-on directive"); + } + on[attr.slice(5)] = value; + } + else if (attr.startsWith("t-model")) { + if (!["input", "select", "textarea"].includes(tagName)) { + throw new Error("The t-model directive only works with ,