UNPKG

khufu

Version:

A template language for incremental-dom or DSL for javascript views

267 lines (247 loc) 9.52 kB
import * as babel from 'babel-core' import * as T from "babel-types" import {compile_body, join_key} from "./compile_view.js" import * as expression from "./compile_expression.js" import {push_to_body} from "./babel-util" import {parse_tree_error} from "./compiler" import {set_var, get_var} from './vars' function optimize_plus(a, b) { if(a[0] == 'string' && b[0] == 'string') { return ['string', a[1] + b[1]] } return ['binop', '+', a, b] } function sort_attributes(attributes, elname, opt) { let stat = [] let dyn = [] let cls_stat = [] let cls_dyn = [] for(let [name, value] of attributes) { let is_static = false; /// TODO(tailhook) maybe employ more deep analysis? switch(value && value[0]) { case undefined: case 'string': case 'number': is_static = !!opt.static_attrs; break; default: break; } if(name == 'class') { (is_static ? cls_stat : cls_dyn).push(value) } else { (is_static ? stat : dyn).push([name, value]) } } if(opt.additional_class && (cls_stat.length || cls_dyn.length || opt.always_add_class.has(elname))) { cls_stat.unshift(['string', opt.additional_class]) } if(cls_stat.length && !cls_dyn.length) { if(opt.static_attrs) { stat.push(['class', ['string', cls_stat.map(x => x[1]).join(' ')]]) } else { dyn.push(['class', ['string', cls_stat.map(x => x[1]).join(' ')]]) } } else if(cls_stat.length && cls_dyn.length) { dyn.push(['class', cls_dyn.reduce((x, y) => optimize_plus(optimize_plus(x, ['string', ' ']), y), ['string', cls_stat.map(x => x[1]).join(' ')])]) } else if(cls_dyn.length) { dyn.push(['class', cls_dyn.slice(1).reduce((x, y) => optimize_plus(optimize_plus(x, ['string', ' ']), y), cls_dyn[0])]) } return [stat, dyn] } function insert_static(elname, attributes, path, opt) { let topscope = path.scope; while(topscope.parent) { topscope = topscope.parent; } let array = []; for(var [aname, value] of attributes) { array.push(T.stringLiteral(aname)) if(value == undefined) { array.push(T.stringLiteral(aname)) } else { array.push(expression.compile(value, path, opt)) } } let ident = topscope.generateUidIdentifier( elname.toUpperCase() + '_ATTRS'); topscope.push({ id: ident, init: T.arrayExpression(array), kind: 'let' }) return ident; } function insert_stores(elname, stores, genattrs, path, opt) { let local_stores = new Map() let stores_id = path.scope.generateUidIdentifier(elname + '_stores'); genattrs.push(['__stores', T.objectExpression( stores.map(([_store, name, value, middlewares]) => { local_stores.set(name, T.memberExpression(stores_id, T.identifier(name))); return T.objectProperty( T.identifier(name), T.functionExpression(null, [], T.blockStatement([ // TODO(tailhook) pass path to inner function T.returnStatement(T.arrayExpression([ expression.compile(value, path, opt), T.arrayExpression(middlewares.map(m => expression.compile(m, path, opt)))])) ]))) }))]) return [stores_id, local_stores] } function insert_links(links, genattrs, local_stores, path, opt) { let map = new Map() for(let [_link, names, action, target] of links) { for(let name of names) { if(map.has(name)) { map.get(name).push([action, target]) } else { map.set(name, [[action, target]]) } } } /// First visiting single-handler events for(let [_link, names, action, target] of links) { let single_refcnt_names = names.filter(x => map.get(x).length == 1); if(single_refcnt_names.length == 0) continue; let fid = path.scope.generateUidIdentifier( 'ln_' + single_refcnt_names.join('_')) let fun = T.functionDeclaration(fid, [T.identifier('event')], T.blockStatement([])) let fpath = push_to_body(path, fun).get('body'); console.assert(target[0] == 'store'); let store = get_var(path, target[1]); if(!store) { store = local_stores.get(target[1]); if(!store) { throw parse_tree_error("Unknown store: " + target[1], target); } } set_var(fpath, 'this', T.identifier('this')) set_var(fpath, 'event', T.identifier('event')) push_to_body(fpath, T.callExpression( T.memberExpression(store, T.identifier('dispatch')), [expression.compile(action, fpath, opt)])); for(let name of single_refcnt_names) { genattrs.push(['on' + name, fid]); } } /// For now multiple handler events we create for each event separately for(let [name, handlers] of map.entries()) { if(handlers.length == 1) continue; let fid = path.scope.generateUidIdentifier('ln_' + name) let fun = T.functionDeclaration(fid, [T.identifier('event')], T.blockStatement([])) let fpath = push_to_body(path, fun).get('body'); for(let [action, target] of handlers) { console.assert(target[0] == 'store'); let store = get_var(path, target[1]); if(!store) { throw parse_tree_error("Unknown store: " + target[1], handlers); } set_var(fpath, 'this', T.identifier('this')) set_var(fpath, 'event', T.identifier('event')) push_to_body(fpath, T.callExpression( T.memberExpression(store, T.identifier('dispatch')), [expression.compile(action, fpath, opt)])); } genattrs.push(['on' + name, fid]); } } export function compile(element, path, opt, key) { let [_element, name, classes, attributes, children] = element; let links = children.filter(([x]) => x == 'link') let stores = children.filter(([x]) => x == 'store') let body = children.filter(([x]) => x != 'link' && x != 'store'); if(classes.length) { for(let [cls, cond] of classes) { if(cond) { attributes.push(['class', ['if', cond, ['string', cls], ['string', '']]]); } else { attributes.push(['class', ['string', cls]]); } } } let [stat, dyn] = sort_attributes(attributes, name, opt) attributes = dyn; let attrib_expr; if(stat.length) { attrib_expr = insert_static(name, stat, path, opt) } let genattrs = [] let local_stores = new Map() let stores_id; if(stores.length) { [stores_id, local_stores] = insert_stores(name, stores, genattrs, path, opt) } if(links.length) { insert_links(links, genattrs, local_stores, path, opt) } let attribs = [ T.stringLiteral(name), // We need to add a tag name to the element, because incremental-dom // throws an exception when tag name is changed. Changing tag // name is is only possible with hot-reload, though. // // TODO(tailhook) should we do it only if hot-reload is enabled? join_key(key, T.stringLiteral('-'+name)), ]; if(attributes.length || genattrs.length) { attribs.push(attrib_expr || T.nullLiteral()) for(var [aname, value] of attributes) { attribs.push(T.stringLiteral(aname)) if(value) { attribs.push(expression.compile(value, path, opt)) } else { attribs.push(T.stringLiteral(aname)) } } for(var [aname, value] of genattrs) { attribs.push(T.stringLiteral(aname)) attribs.push(value) } } else if(attrib_expr) { attribs.push(attrib_expr) } if(body.length == 0 && stores.length == 0) { let node = T.callExpression(T.identifier('elementVoid'), attribs) path.node.body.push(T.expressionStatement(node)) } else { let el = T.callExpression(T.identifier('elementOpen'), attribs); if(stores_id) { el = T.variableDeclaration('let', [ T.variableDeclarator(stores_id, T.memberExpression(el, T.identifier('__stores')))]) } else { el = T.expressionStatement(el) } push_to_body(path, el) let blockpath = push_to_body(path, T.blockStatement([])); if(stores_id) { for(let [name, expr] of local_stores) { set_var(blockpath, name, expr); } } compile_body(body, blockpath, opt) /// Optimize the scope without variables if(Object.keys(blockpath.scope.bindings).length == 0) { blockpath.replaceWithMultiple( blockpath.get('body').map(x => x.node)) } path.node.body.push(T.expressionStatement( T.callExpression(T.identifier('elementClose'), [T.stringLiteral(name)]))) } }