UNPKG

hyperviews

Version:

View template language based targeting hyperscript

309 lines (257 loc) 6.97 kB
const standard = require('standard') const htmlparser = require('htmlparser2') const delim = ['{', '}'] const startDelim = delim[0] const specialEls = ['elseif', 'else'] const specialAttrs = ['if', 'each'] let root, buffer, curr, defaultFnName, defaultFnArgs function strify (str) { str = str ? str.replace('\n', '\\\n') : '' return '"' + str + '"' } function interpolate (text) { const parts = text.split(/({.*?})/) text = parts.filter(p => p).map((part, index) => { if (part.startsWith('{')) { return `(${part.slice(1, -1)})` } else { return strify(part) } }).join(' + ') return text } function getIterator (target) { return `(${target} || [])` } function getAttrs (target) { const attributes = [] for (const name in target.attribs) { if (specialAttrs.indexOf(name) === -1) { const value = target.attribs[name] let val = '' if (name === 'style' || name.startsWith('on')) { val = value } else if (value.indexOf(startDelim) > -1) { val = interpolate(value) } else { val = strify(value) } attributes.push({ name: name, value: val }) } } const attribs = attributes.length ? `{ ${attributes.map(a => `'${a.name}': ${a.value}`).join(', ')} }` : null return attribs } function getBranches (node, nodeOutput) { const branches = [] if (node.name === 'if') { // Element based `if` let n = node do { branches.push({ condition: n.attribs['condition'], rtn: n.childrenToString(true) }) n = n.children.find(c => c.name === 'elseif' || c.name === 'else') } while (n) } else { // Attribute based `if` branches.push({ condition: node.attribs['if'], rtn: nodeOutput }) } return branches } class Node { constructor (parent, name, attribs) { this.parent = parent this.name = name this.attribs = attribs this.children = [] } get isSpecial () { return specialEls.indexOf(this.name) > -1 } childrenToString (filterSpecial) { const children = (filterSpecial ? this.children.filter(c => !c.isSpecial) : this.children).map(c => c.toString()) let childstr = '' if (children.length) { childstr = children.length === 1 ? children[0] : '[\n' + children.join(',\n') + '\n]' } return childstr } toString () { // Attributes const attribs = getAttrs(this) // Children const childstr = this.childrenToString() let node if (this.name === 'script') { node = this.children.toString() } else if (this.name === 'function') { const name = this.attribs.name || defaultFnName const argstr = this.attribs.args ? buildArgs(this.attribs.args) : defaultFnArgs node = ` ${wrapFn(name, argstr, this.children.toString().trimLeft())} ` } else { const isComponent = /^[A-Z]/.test(this.name) const name = isComponent ? this.name : `"${this.name}"` const args = [name] if (attribs || childstr) { args.push(attribs || 'null') if (childstr) { args.push(childstr) } } node = `h(${args.join(', ')})` } if (this.name === 'if') { const branches = getBranches(this, node) let str = '' branches.forEach((branch, index) => { if (branch.condition) { str += `${index === 0 ? 'if' : ' else if '} (${branch.condition}) { return ${branch.rtn} }` } else { str += ` else { return ${branch.rtn} }` } }) return `(function () { ${str} }).call(this)` } else if ('if' in this.attribs) { return `${this.attribs['if']} ? ${node} : undefined` } else if ('each' in this.attribs) { const eachAttr = this.attribs['each'] const eachParts = eachAttr.split(' in ') const key = eachParts[0] const target = eachParts[1] return `${getIterator(target)}.map(function ($value, $index, $target) {\nvar ${key} = $value\nreturn ${node}\n}, this)` } else { return node } } } class Root extends Node { toString () { return this.children.map(c => c.toString()).join('\n').trim() } } const handler = { onopentag: function (name, attribs) { const newCurr = new Node(curr, name, attribs) curr.children.push(newCurr) buffer.push(newCurr) curr = newCurr }, ontext: function (text) { if (!text || !(text = text.trim())) { return } let value if (curr.name === 'script') { value = text } else if (text.indexOf(startDelim) > -1) { value = interpolate(text) } else { value = strify(text) } curr.children.push(value) }, onclosetag: function (name) { buffer.pop() curr = buffer[buffer.length - 1] } } function buildArgs (args) { return args.split(' ').filter(item => { return item.trim() }).join(', ') } function wrapFn (name, args, value) { return `function ${name} (${args}) { return ${value} }` } module.exports = function (tmpl, mode = 'raw', name = 'view', args = 'props state') { root = new Root() buffer = [root] curr = root defaultFnName = name defaultFnArgs = buildArgs(args) const parser = new htmlparser.Parser(handler, { decodeEntities: false, lowerCaseAttributeNames: false, lowerCaseTags: false, recognizeSelfClosing: true }) parser.write(tmpl) parser.end() const js = root.toString() let result = '' try { if (mode === 'raw') { result = js } else { let wrap = false let useMode = false const children = root.children if (children.length === 1) { const onlyChild = children[0] // Only wrap the output if there's a single root // child and that child is not a <function> tag if (onlyChild.name !== 'function') { wrap = true } // Only mode the output if there's a single // root child and that child is not a <script> tag if (onlyChild.name !== 'script') { useMode = true } } let value = js if (wrap) { value = wrapFn(defaultFnName, defaultFnArgs, js) } if (useMode) { switch (mode) { case 'esm': result = `export default ${value}` break case 'cjs': result = `module.exports = ${value}` break case 'browser': result = `window.${name} = ${value}` break default: result = `${mode ? `var ${mode} =` : ''} ${value}` } } else { result = value } } const lintResult = standard.lintTextSync(result, { fix: true }) return lintResult.results[0].output } catch (err) { throw new Error(`Error ${err.message}\n${result}\nraw:\n${js}`) } }