import { getFromMap, parseTemplate, toString } from './utils.js' const parseCache = new WeakMap() const domInstancesCache = new WeakMap() const unkeyedSymbol = Symbol('un-keyed') // for debugging /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { window.parseCache = parseCache window.domInstancesCache = domInstancesCache } // Not supported in Safari <=13 const hasReplaceChildren = 'replaceChildren' in Element.prototype function replaceChildren (parentNode, newChildren) { /* istanbul ignore else */ if (hasReplaceChildren) { parentNode.replaceChildren(...newChildren) } else { // polyfill Element.prototype.replaceChildren while (parentNode.lastChild) { parentNode.removeChild(parentNode.lastChild) } for (const child of newChildren) { parentNode.appendChild(child) } } } function doChildrenNeedRerender (parentNode, newChildren) { let oldChild = parentNode.firstChild let oldChildrenCount = 0 // iterate using firstChild/nextSibling because browsers use a linked list under the hood while (oldChild) { const newChild = newChildren[oldChildrenCount] // check if the old child and new child are the same if (newChild !== oldChild) { return true } oldChild = oldChild.nextSibling oldChildrenCount++ } /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && oldChildrenCount !== parentNode.children.length) { throw new Error('parentNode.children.length is different from oldChildrenCount, it should not be') } // if new children length is different from old, we must re-render return oldChildrenCount !== newChildren.length } function patchChildren (newChildren, instanceBinding) { const { targetNode } = instanceBinding let { targetParentNode } = instanceBinding let needsRerender = false if (targetParentNode) { // already rendered once needsRerender = doChildrenNeedRerender(targetParentNode, newChildren) } else { // first render of list needsRerender = true instanceBinding.targetNode = undefined // placeholder comment not needed anymore, free memory instanceBinding.targetParentNode = targetParentNode = targetNode.parentNode } // console.log('needsRerender?', needsRerender, 'newChildren', newChildren) // avoid re-rendering list if the dom nodes are exactly the same before and after if (needsRerender) { replaceChildren(targetParentNode, newChildren) } } function patch (expressions, instanceBindings) { for (const instanceBinding of instanceBindings) { const { targetNode, currentExpression, binding: { expressionIndex, attributeName, attributeValuePre, attributeValuePost } } = instanceBinding const expression = expressions[expressionIndex] if (currentExpression === expression) { // no need to update, same as before continue } instanceBinding.currentExpression = expression if (attributeName) { // attribute replacement targetNode.setAttribute(attributeName, attributeValuePre + toString(expression) + attributeValuePost) } else { // text node / child element / children replacement let newNode if (Array.isArray(expression)) { // array of html tag templates patchChildren(expression, instanceBinding) } else if (expression instanceof Element) { // html tag template returning a DOM element newNode = expression /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && newNode === targetNode) { // it seems impossible for the framework to get into this state, may as well assert on it // worst case scenario is we lose focus if we call replaceWith on the same node throw new Error('the newNode and targetNode are the same, this should never happen') } targetNode.replaceWith(newNode) } else { // primitive - string, number, etc if (targetNode.nodeType === Node.TEXT_NODE) { // already transformed into a text node targetNode.nodeValue = toString(expression) } else { // replace comment or whatever was there before with a text node newNode = document.createTextNode(toString(expression)) targetNode.replaceWith(newNode) } } if (newNode) { instanceBinding.targetNode = newNode } } } } function parse (tokens) { let htmlString = '' let withinTag = false let withinAttribute = false let elementIndexCounter = -1 // depth-first traversal order const elementsToBindings = new Map() const elementIndexes = [] for (let i = 0, len = tokens.length; i < len; i++) { const token = tokens[i] htmlString += token if (i === len - 1) { break // no need to process characters } for (let j = 0; j < token.length; j++) { const char = token.charAt(j) switch (char) { case '<': { const nextChar = token.charAt(j + 1) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !/[/a-z]/.test(nextChar)) { // we don't need to support comments (' 2 < 3 ' throw new Error('framework currently only supports a < followed by / or a-z') } if (nextChar === '/') { // closing tag // leaving an element elementIndexes.pop() } else { // not a closing tag withinTag = true elementIndexes.push(++elementIndexCounter) } break } case '>': { withinTag = false withinAttribute = false break } case '=': { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !withinTag) { // we don't currently support '=' anywhere but inside a tag, e.g. // we don't support '