UNPKG

@polight/lego

Version:

Tiny Web Components lib for future-proof HTML mentors

133 lines (117 loc) 4.91 kB
import { parseFragment } from 'parse5' import { isNativeEvent } from './native-events.js' function vnode(name, attributes = {}, children = '', indent = '') { const attrs = Object.keys(attributes).reduce((acc, name) => { acc.push(`"${name}": ${attributes[name]}`) return acc }, []) return `${indent}h("${name}", {${attrs.join(', ')}}, ${children})` } function cleanChildren(children = []) { return children.filter(c => { if(!c.nodeName.startsWith('#')) return true return c.value && c.value.trim() }) } function extractDirectives(node) { const directives = [] node.attrs = node.attrs.reduce((attrs, attr) => { let name = attr.name let value = attr.value if(name === ':for') directives.push({ name: 'for', value }) else if(name === ':if') directives.push({ name: 'if', value }) else if(name === ':else') directives.push({ name: 'else', value }) else if(name.startsWith(':')) { name = name.slice(1) attrs.push({ name, value }) } else if(name.startsWith('@')) { const onEventName = `on${name.slice(1)}` if (isNativeEvent(onEventName)) name = onEventName value = `this.${value}.bind(this)` attrs.push({ name, value }) } else attrs.push({ name, value: `\`${value}\`` }) return attrs }, []) return [node, directives] } function wrapDirectives(directives, content, indent = '') { directives.reverse().forEach(directive => { if(directive.name === 'if') return content = `((${directive.value}) ? ${content} : '')` if(directive.name === 'for') { const [_, item, items] = directive.value.match(/\s*(.*)\s+in\s+(.*)\s*/) return content = `((${items}).map((${item}) => (${content})))` } }) return indent + content } function convert(node, indentSize = 0, parentContext = {}) { const indent = ' '.repeat(indentSize) if(node.nodeName === '#text') { return node.value.trim() ? `\`${node.value}\`` : '' } if(node.nodeName === '#document-fragment') { return convertFragment(node.childNodes, indentSize) } let directives [node, directives] = extractDirectives(node) const attributes = node.attrs.reduce((attrs, attr) => Object.assign(attrs, {[attr.name]: attr.value }), {}) const children = node.childNodes ? cleanChildren(node.childNodes).map(c => convert(c, indent + 4)).join(',\n') : '' let childrenIndent if(!node.childNodes || node.childNodes.length === 0) childrenIndent = JSON.stringify('') else if(node.childNodes.length === 1) childrenIndent = children else childrenIndent = `[\n${children}\n]` return wrapDirectives(directives, vnode(node.nodeName, attributes, childrenIndent), indent) } function convertFragment(childNodes, indentSize) { const indent = ' '.repeat(indentSize) const children = cleanChildren(childNodes) let result = [] let pendingIf = null for(let i = 0; i < children.length; i++) { let child = children[i] let directives [child, directives] = extractDirectives(child) const hasIf = directives.some(d => d.name === 'if') const hasElse = directives.some(d => d.name === 'else') if(hasIf && hasElse) { throw new Error('Cannot use :if and :else on the same element') } if(hasIf) { pendingIf = { child, directives } } else if(hasElse && pendingIf) { const ifContent = convertElement(pendingIf.child, pendingIf.directives.filter(d => d.name !== 'if'), indentSize + 2) const elseContent = convertElement(child, directives.filter(d => d.name !== 'else'), indentSize + 2) const ifDirective = pendingIf.directives.find(d => d.name === 'if') const combined = `((${ifDirective.value}) ? ${ifContent} : ${elseContent})` result.push(combined) pendingIf = null } else { if(pendingIf) { result.push(convertElement(pendingIf.child, pendingIf.directives, indentSize + 2)) pendingIf = null } result.push(convertElement(child, directives, indentSize + 2)) } } if(pendingIf) { result.push(convertElement(pendingIf.child, pendingIf.directives, indentSize + 2)) } return `[${indent}\n${result.join(',\n')}]` } function convertElement(node, directives, indentSize) { const indent = ' '.repeat(indentSize) const attributes = node.attrs.reduce((attrs, attr) => Object.assign(attrs, {[attr.name]: attr.value }), {}) const children = node.childNodes ? cleanChildren(node.childNodes).map(c => convert(c, indentSize + 2)).join(',\n') : '' let childrenIndent if(!node.childNodes || node.childNodes.length === 0) childrenIndent = JSON.stringify('') else if(node.childNodes.length === 1) childrenIndent = children else childrenIndent = `[\n${children}\n]` return wrapDirectives(directives, vnode(node.nodeName, attributes, childrenIndent), indent) } function parse(html) { const document = parseFragment(html) return convert(document) } export default parse