/** * {模板解析, 基于 lit-html 二次开发} * @author yutent * @date 2023/03/7 16:36:59 */ import { boolMap, WC_PART, 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 TEMPLATE_CACHE = new Map() const walker = document.createTreeWalker(document, 129, null, false) function noop() {} 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')) { throw new Error('invalid html ast') } 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) let decorates = [] if (m[1] === '@' && m[2].includes('.')) { decorates = m[2].split('.') m[2] = decorates.shift() } parts.push({ type: ATTRIBUTE_PART, index: nodeIndex, name: m[2], strings: statics, decorates, 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 } } class TemplateInstance { constructor(template, parent) { this.$parts = [] this.$template = template this.$parent = parent } get parentNode() { return this.$parent.parentNode } #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, templatePart.decorates, 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, 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.startNode = startNode this.endNode = endNode this.$parent = parent this.options = options } get parentNode() { let parentNode = this.startNode.parentNode let parent = this.$parent if (parent !== void 0 && parentNode.nodeType === 11) { parentNode = parent.parentNode } return parentNode } $setValue(value) { if (isPrimitive(value)) { if (value === NOTHING || value == null || value === '') { if (this._$committedValue !== NOTHING) { this.#clear() } this._$committedValue = NOTHING } else if (value !== this._$committedValue) { 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 = TEMPLATE_CACHE.get(result.strings.join()) if (template === void 0) { template = new Template(result, this.options) TEMPLATE_CACHE.set(result.strings.join(), 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) { while (start && start !== this.endNode) { let node = start.nextSibling start.remove() start = node } } } // 常规属性 class AttributePart { constructor(element, name, strings, decorates, parent, options = {}) { this.type = ATTRIBUTE_PART this._$committedValue = NOTHING this.element = element this.name = name this.decorates = decorates 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 } $setValue(value, valueIndex) { let strings = this.strings let changed = false if (strings === void 0) { changed = !isPrimitive(value) || value !== this._$committedValue if (changed) { this._$committedValue = value } } else { let values = value value = strings[0] for (let i = 0; i < strings.length - 1; i++) { let v = values[valueIndex + i] changed || (changed = !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 (changed) { 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, immediate = false } = {}) { let fromto = MODES[type] || MODES.fade if (custom) { fromto = custom } this.element.$animate = function (out = false) { return animate.call(this, duration, fromto, out) } this.element.$animate.immediate = immediate } } // 事件属性 class EventPart extends AttributePart { #listener = null #prevent = noop #stop = noop #checkSelf = noop constructor(...args) { super(...args) this.type = EVENT_PART } $setValue(listener) { let host = this.options.host let options = {} if (this.decorates.length) { for (let it of this.decorates) { switch (it) { case 'stop': this.#stop = ev => ev.stopPropagation() break case 'prevent': this.#prevent = ev => ev.preventDefault() break case 'self': this.#checkSelf = ev => ev.target === this.element break case 'capture': case 'once': case 'passive': options[it] = true break } } } let shouldRemove = listener !== this.#listener if (this.#listener && host.$events[this.name]) { for (let it of host.$events[this.name]) { if (it.el === this.element) { shouldRemove = options.capture !== it.capture || options.once !== it.once || options.passive !== it.passive if (shouldRemove) { this.element.removeEventListener(this.name, it.listener, it.options) } break } } } if (listener && shouldRemove) { this.element.addEventListener(this.name, this, options) this.#listener = listener if (host.$events[this.name]) { host.$events[this.name].push({ el: this.element, listener: this, options }) } else { host.$events[this.name] = [ { el: this.element, listener: this, options } ] } } } handleEvent(ev) { this.#stop(ev) this.#prevent(ev) if (this.#checkSelf(ev) === false) { return } this.#listener.call(this.options.host, ev) } } class ElementPart { constructor(element, parent, options) { this.element = element this.type = ELEMENT_PART this.$parent = parent this.options = options } $setValue(value) {} } export function render(value, container, options = {}) { let part = container[WC_PART] if (part === void 0) { part = new ChildPart( container.insertBefore(createMarker(), null), null, void 0, options ) container[WC_PART] = part } part.$setValue(value) return part } export function html(strings, ...values) { return { __dom_type__: HTML_RESULT, strings, values } } export function raw(str, values = []) { let strings = values.length ? str.split('%s') : [str] strings.raw = true return html(strings, ...values) } export function svg(strings, ...values) { return { __dom_type__: SVG_RESULT, strings, values } }