import { boolMap, WC_PART, NO_CHANGE, NOTHING } from './constants.js' import { animate, MODES } from './anim.js' const boundAttributeSuffix = '$wc$' const marker = `wc$${String(Math.random()).slice(9)}$` const markerMatch = '?' + marker const nodeMarker = `<${markerMatch}>` const createMarker = (v = '') => document.createComment(v) const isPrimitive = value => value === null || (typeof value != 'object' && typeof value != 'function') const isArray = Array.isArray const isIterable = value => isArray(value) || typeof (value === null || value === void 0 ? false : value[Symbol.iterator]) === 'function' const SPACE_CHAR = `[ \n\f\r]` const ATTR_VALUE_CHAR = `[^ \n\f\r"'\`<>=]` const NAME_CHAR = `[^\\s"'>=/]` const textEndRegex = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g const COMMENT_START = 1 const TAG_NAME = 2 const DYNAMIC_TAG_NAME = 3 const commentEndRegex = /-->/g const comment2EndRegex = />/g const tagEndRegex = new RegExp( `>|${SPACE_CHAR}(?:(${NAME_CHAR}+)(${SPACE_CHAR}*=${SPACE_CHAR}*(?:${ATTR_VALUE_CHAR}|("|')|))|$)`, 'g' ) const ENTIRE_MATCH = 0 const ATTRIBUTE_NAME = 1 const SPACES_AND_EQUALS = 2 const QUOTE_CHAR = 3 const singleQuoteAttrEndRegex = /'/g const doubleQuoteAttrEndRegex = /"/g const rawTextElement = /^(?:script|style|textarea|title)$/i const HTML_RESULT = 1 const SVG_RESULT = 2 const ATTRIBUTE_PART = 1 const CHILD_PART = 2 const EVENT_PART = 5 const ELEMENT_PART = 6 const COMMENT_PART = 7 const templateCache = new WeakMap() const walker = document.createTreeWalker(document, 129, null, false) function getTemplateHtml(strings, type) { let len = strings.length - 1 let attrNames = [] let html2 = type === SVG_RESULT ? '' : '' let rawTextEndRegex let regex = textEndRegex for (let i = 0; i < len; i++) { let s = strings[i] let attrNameEndIndex = -1 let attrName let lastIndex = 0 let match while (lastIndex < s.length) { regex.lastIndex = lastIndex match = regex.exec(s) if (match === null) { break } lastIndex = regex.lastIndex if (regex === textEndRegex) { if (match[COMMENT_START] === '!--') { regex = commentEndRegex } else if (match[COMMENT_START] !== void 0) { regex = comment2EndRegex } else if (match[TAG_NAME] !== void 0) { if (rawTextElement.test(match[TAG_NAME])) { rawTextEndRegex = new RegExp(`') { regex = rawTextEndRegex !== null && rawTextEndRegex !== void 0 ? rawTextEndRegex : textEndRegex attrNameEndIndex = -1 } else if (match[ATTRIBUTE_NAME] === void 0) { attrNameEndIndex = -2 } else { attrNameEndIndex = regex.lastIndex - match[SPACES_AND_EQUALS].length attrName = match[ATTRIBUTE_NAME] regex = match[QUOTE_CHAR] === void 0 ? tagEndRegex : match[QUOTE_CHAR] === '"' ? doubleQuoteAttrEndRegex : singleQuoteAttrEndRegex } } else if ( regex === doubleQuoteAttrEndRegex || regex === singleQuoteAttrEndRegex ) { regex = tagEndRegex } else if (regex === commentEndRegex || regex === comment2EndRegex) { regex = textEndRegex } else { regex = tagEndRegex rawTextEndRegex = void 0 } } let end = regex === tagEndRegex && strings[i + 1].startsWith('/>') ? ' ' : '' html2 += regex === textEndRegex ? s + nodeMarker : attrNameEndIndex >= 0 ? (attrNames.push(attrName), s.slice(0, attrNameEndIndex) + boundAttributeSuffix + s.slice(attrNameEndIndex)) + marker + end : s + marker + (attrNameEndIndex === -2 ? (attrNames.push(void 0), i) : end) } let htmlResult = html2 + (strings[len] || '') + (type === SVG_RESULT ? '' : '') if (!Array.isArray(strings) || !strings.hasOwnProperty('raw')) { let message = 'invalid template strings array' throw new Error(message) } return [htmlResult, attrNames] } class Template { constructor({ strings, ['__dom_type__']: type }, options) { this.parts = [] let node let nodeIndex = 0 let attrNameIndex = 0 let partCount = strings.length - 1 let parts = this.parts let [html2, attrNames] = getTemplateHtml(strings, type) this.el = Template.createElement(html2) walker.currentNode = this.el.content if (type === SVG_RESULT) { let content = this.el.content let svgElement = content.firstChild svgElement.remove() content.append(...svgElement.childNodes) } while ((node = walker.nextNode()) !== null && parts.length < partCount) { if (node.nodeType === 1) { if (node.hasAttributes()) { let attrsToRemove = [] for (let name of node.getAttributeNames()) { if ( name.endsWith(boundAttributeSuffix) || name.startsWith(marker) ) { let realName = attrNames[attrNameIndex++] attrsToRemove.push(name) if (realName !== void 0) { let value = node.getAttribute( realName.toLowerCase() + boundAttributeSuffix ) let statics = value.split(marker) let m = /([#:@])?(.*)/.exec(realName) parts.push({ type: ATTRIBUTE_PART, index: nodeIndex, name: m[2], strings: statics, ctor: m[1] === ':' ? PropertyPart : m[1] === '#' && m[2] === 'animation' ? AnimPart : m[1] === '@' ? EventPart : AttributePart }) } else { parts.push({ type: ELEMENT_PART, index: nodeIndex }) } } } for (let name of attrsToRemove) { node.removeAttribute(name) } } if (rawTextElement.test(node.tagName)) { let strings2 = node.textContent.split(marker) let lastIndex = strings2.length - 1 if (lastIndex > 0) { node.textContent = '' for (let i = 0; i < lastIndex; i++) { node.append(strings2[i], createMarker()) walker.nextNode() parts.push({ type: CHILD_PART, index: ++nodeIndex }) } node.append(strings2[lastIndex], createMarker()) } } } else if (node.nodeType === 8) { let data = node.data if (data === markerMatch) { parts.push({ type: CHILD_PART, index: nodeIndex }) } else { let i = -1 while ((i = node.data.indexOf(marker, i + 1)) !== -1) { parts.push({ type: COMMENT_PART, index: nodeIndex }) i += marker.length - 1 } } } nodeIndex++ } } static createElement(html2) { let el = document.createElement('template') el.innerHTML = html2 return el } } function resolveDirective(part, value, parent = part, attributeIndex) { if (value === NO_CHANGE) { return value } let currentDirective = attributeIndex !== void 0 ? parent.__directives?.[attributeIndex] : parent.__directive let nextDirectiveConstructor = isPrimitive(value) ? void 0 : value['_$litDirective$'] if (currentDirective?.constructor !== nextDirectiveConstructor) { currentDirective._$notifyDirectiveConnectionChanged?.call( currentDirective, false ) if (nextDirectiveConstructor === void 0) { currentDirective = void 0 } else { currentDirective = new nextDirectiveConstructor(part) currentDirective._$initialize(part, parent, attributeIndex) } if (attributeIndex !== void 0) { if (!parent.__directives) { parent.__directives = [] } parent.__directives[attributeIndex] = currentDirective } else { parent.__directive = currentDirective } } if (currentDirective !== void 0) { value = resolveDirective( part, currentDirective._$resolve(part, value.values), currentDirective, attributeIndex ) } return value } class TemplateInstance { constructor(template, parent) { this._parts = [] this._$disconnectableChildren = void 0 this._$template = template this._$parent = parent } get parentNode() { return this._$parent.parentNode } get _$isConnected() { return this._$parent._$isConnected } #checkRef(node, walker, options) { do { if (node && node.nodeType === 1 && node.getAttribute('ref')) { options.host.$refs[node.getAttribute('ref')] = node node.removeAttribute('ref') } } while ((node = walker.nextNode()) !== null) } _clone(options) { let { el: { content }, parts } = this._$template let fragment = document.importNode(content, true) let nodeIndex = 0 let partIndex = 0 let templatePart = parts[0] let node = null walker.currentNode = fragment node = walker.nextNode() // 没有动态绑定时, 查一遍是否有ref属性 if (templatePart === void 0) { this.#checkRef(node, walker, options) } else { while (templatePart !== void 0) { if (node.nodeType === 1 && node.getAttribute('ref')) { if (options.host.$refs) { options.host.$refs[node.getAttribute('ref')] = node node.removeAttribute('ref') } } if (nodeIndex === templatePart.index) { let part if (templatePart.type === CHILD_PART) { part = new ChildPart(node, node.nextSibling, this, options) } else if (templatePart.type === ATTRIBUTE_PART) { part = new templatePart.ctor( node, templatePart.name, templatePart.strings, this, options ) } else if (templatePart.type === ELEMENT_PART) { part = new ElementPart(node, this, options) } this._parts.push(part) templatePart = parts[++partIndex] } if ( nodeIndex !== (templatePart === null || templatePart === void 0 ? void 0 : templatePart.index) ) { node = walker.nextNode() nodeIndex++ } } // 再检查剩下没有动态绑定的节点 this.#checkRef(node, walker, options) } return fragment } _update(values) { let i = 0 for (let part of this._parts) { if (part !== void 0) { if (part.strings !== void 0) { part._$setValue(values, part, i) i += part.strings.length - 2 } else { part._$setValue(values[i]) } } i++ } } } class ChildPart { constructor(startNode, endNode, parent, options) { this.type = CHILD_PART this._$committedValue = NOTHING this._$disconnectableChildren = void 0 this._$startNode = startNode this._$endNode = endNode this._$parent = parent this.options = options this.__isConnected = options.isConnected || true } get _$isConnected() { return this._$parent?._$isConnected || this.__isConnected } get parentNode() { let parentNode = this._$startNode.parentNode let parent = this._$parent if (parent !== void 0 && parentNode.nodeType === 11) { parentNode = parent.parentNode } return parentNode } get startNode() { return this._$startNode } get endNode() { return this._$endNode } _$setValue(value, directiveParent = this) { value = resolveDirective(this, value, directiveParent) if (isPrimitive(value)) { if (value === NOTHING || value == null || value === '') { if (this._$committedValue !== NOTHING) { this.#clear() } this._$committedValue = NOTHING } else if (value !== this._$committedValue && value !== NO_CHANGE) { this._commitText(value) } } else if (value['__dom_type__'] !== void 0) { this._commitTemplateResult(value) } else if (value.nodeType !== void 0) { this._commitNode(value) } else if (isIterable(value)) { this._commitIterable(value) } else { this._commitText(value) } } _insert(node, target = this._$endNode) { return this._$startNode.parentNode.insertBefore(node, target) } _commitNode(value) { if (this._$committedValue !== value) { this.#clear() this._$committedValue = this._insert(value) } } _commitText(value) { if ( this._$committedValue !== NOTHING && isPrimitive(this._$committedValue) ) { let node = this._$startNode.nextSibling node.data = value } else { this._commitNode(document.createTextNode(value)) } this._$committedValue = value } _commitTemplateResult(result) { let { values, ['__dom_type__']: type } = result let template = typeof type === 'number' ? this.#getTemplate(result) : (type.el === void 0 && (type.el = Template.createElement(type.h)), type) if (this._$committedValue?._$template === template) { this._$committedValue._update(values) } else { let instance = new TemplateInstance(template, this) let fragment = instance._clone(this.options) instance._update(values) this._commitNode(fragment) this._$committedValue = instance } } #getTemplate(result) { let template = templateCache.get(result.strings) if (template === void 0) { template = new Template(result, this.options) templateCache.set(result.strings, template) } return template } _commitIterable(value) { if (!isArray(this._$committedValue)) { this._$committedValue = [] this.#clear() } let itemParts = this._$committedValue let partIndex = 0 let itemPart for (let item of value) { if (partIndex === itemParts.length) { itemParts.push( (itemPart = new ChildPart( this._insert(createMarker()), this._insert(createMarker()), this, this.options )) ) } else { itemPart = itemParts[partIndex] } itemPart._$setValue(item) partIndex++ } if (partIndex < itemParts.length) { this.#clear(itemPart && itemPart._$endNode.nextSibling, partIndex) itemParts.length = partIndex } } #clear(start = this._$startNode.nextSibling, from) { this._$notifyConnectionChanged?.call(this, false, true, from) while (start && start !== this._$endNode) { let node = start.nextSibling start.remove() start = node } } setConnected(isConnected) { if (this._$parent === void 0) { this.__isConnected = isConnected this._$notifyConnectionChanged?.call(this, isConnected) } } } // 常规属性 class AttributePart { constructor(element, name, strings, parent, options = {}) { this.type = ATTRIBUTE_PART this._$committedValue = NOTHING this._$disconnectableChildren = void 0 this.element = element this.name = name this._$parent = parent this.options = options if (strings.length > 2 || strings[0] !== '' || strings[1] !== '') { this._$committedValue = new Array(strings.length - 1).fill(new String()) this.strings = strings } else { this._$committedValue = NOTHING } } get tagName() { return this.element.tagName } get _$isConnected() { return this._$parent._$isConnected } _$setValue(value, directiveParent = this, valueIndex, noCommit) { let strings = this.strings let change = false if (strings === void 0) { value = resolveDirective(this, value, directiveParent, 0) change = !isPrimitive(value) || (value !== this._$committedValue && value !== NO_CHANGE) if (change) { this._$committedValue = value } } else { let values = value value = strings[0] for (let i = 0; i < strings.length - 1; i++) { let v = resolveDirective( this, values[valueIndex + i], directiveParent, i ) if (v === NO_CHANGE) { v = this._$committedValue[i] } change || (change = !isPrimitive(v) || v !== this._$committedValue[i]) if (v === NOTHING) { value = NOTHING } else if (value !== NOTHING) { value += (v !== null && v !== void 0 ? v : '') + strings[i + 1] } this._$committedValue[i] = v } } if (change && !noCommit) { this.commitValue(value) } } commitValue(value) { let isBoolAttr = boolMap[this.name] // ref属性不渲染到节点上 if (this.name === 'ref') { this.options.host.$refs[value] = this.element return } if (isBoolAttr) { this.element[isBoolAttr] = !(value === false || value === null) if (this.element[isBoolAttr]) { this.element.setAttribute(this.name, '') } else { this.element.removeAttribute(this.name) } } else { if (value === null || value === void 0) { this.element.removeAttribute(this.name) } else { this.element.setAttribute(this.name, value) } } } } // 赋值属性 class PropertyPart extends AttributePart { constructor(...args) { super(...args) } commitValue(value) { this.element[this.name] = value } } // 动画属性 class AnimPart extends AttributePart { constructor(...args) { super(...args) } commitValue({ type = 'fade', duration, custom } = {}) { let fromto = MODES[type] if (custom) { fromto = custom } this.element.$anim = { el: this.element, start(out = false) { animate.call(this.el, duration, fromto, out) } } } } // 事件属性 class EventPart extends AttributePart { constructor(...args) { super(...args) this.type = EVENT_PART } _$setValue(newListener, directiveParent = this) { newListener = resolveDirective(this, newListener, directiveParent, 0) || NOTHING if (newListener === NO_CHANGE) { return } let host = this.options.host let oldListener = this._$committedValue let shouldRemoveListener = (newListener === NOTHING && oldListener !== NOTHING) || newListener.capture !== oldListener.capture || newListener.once !== oldListener.once || newListener.passive !== oldListener.passive let shouldAddListener = newListener !== NOTHING && (oldListener === NOTHING || shouldRemoveListener) if (shouldRemoveListener) { this.element.removeEventListener(this.name, this, oldListener) } if (shouldAddListener) { this.element.addEventListener(this.name, this, newListener) } this._$committedValue = newListener if (host) { if (host.$events[this.name]) { host.$events[this.name].push({ el: this.element, listener: this }) } else { host.$events[this.name] = [ { el: this.element, listener: this } ] } } } handleEvent(event) { if (typeof this._$committedValue === 'function') { this._$committedValue.call(this.options.host || this.element, event) } else { this._$committedValue.handleEvent(event) } } } class ElementPart { constructor(element, parent, options) { this.element = element this.type = ELEMENT_PART this._$disconnectableChildren = void 0 this._$parent = parent this.options = options } get _$isConnected() { return this._$parent._$isConnected } _$setValue(value) { resolveDirective(this, value) } } export function render(value, container, options = {}) { let part = container[WC_PART] if (part === void 0) { container[WC_PART] = part = new ChildPart( container.insertBefore(createMarker(), null), null, void 0, options ) } part._$setValue(value) return part } export const html = (strings, ...values) => { return { __dom_type__: HTML_RESULT, strings, values } } export const svg = (strings, ...values) => { return { __dom_type__: SVG_RESULT, strings, values } }