/* * Template code * * A template is just a javascript structure. An element is represented as: * * [tag_name, {attr_name:attr_value}, child1, child2] * * the children can either be strings (which act like text nodes), other templates or * functions (see below) * * A text node is represented as * * ["{text}", value] * * String values have a simple substitution syntax; ${foo} represents a variable foo. * * It is possible to embed logic in templates by using a function in a place where a * node would usually go. The function must either return part of a template or null. * * In cases where a set of nodes are required as output rather than a single node * with children it is possible to just use a list * [node1, node2, node3] * * Usage: * * render(template, substitutions) - take a template and an object mapping * variable names to parameters and return either a DOM node or a list of DOM nodes * * substitute(template, substitutions) - take a template and variable mapping object, * make the variable substitutions and return the substituted template * */ function is_single_node(template) { return typeof template[0] === "string"; } function substitute(template, substitutions) { if (typeof template === "function") { var replacement = template(substitutions); if (replacement) { var rv = substitute(replacement, substitutions); return rv; } else { return null; } } else if (is_single_node(template)) { return substitute_single(template, substitutions); } else { return filter(map(template, function(x) { return substitute(x, substitutions); }), function(x) {return x !== null;}); } } expose(substitute, "template.substitute"); function substitute_single(template, substitutions) { var substitution_re = /\${([^ }]*)}/g; function do_substitution(input) { var components = input.split(substitution_re); var rv = []; for (var i=0; i<components.length; i+=2) { rv.push(components[i]); if (components[i+1]) { rv.push(substitutions[components[i+1]]); } } return rv; } var rv = []; rv.push(do_substitution(String(template[0])).join("")); if (template[0] === "{text}") { substitute_children(template.slice(1), rv); } else { substitute_attrs(template[1], rv); substitute_children(template.slice(2), rv); } function substitute_attrs(attrs, rv) { rv[1] = {}; for (name in template[1]) { if (attrs.hasOwnProperty(name)) { var new_name = do_substitution(name).join(""); var new_value = do_substitution(attrs[name]).join(""); rv[1][new_name] = new_value; }; } } function substitute_children(children, rv) { for (var i=0; i<children.length; i++) { if (children[i] instanceof Object) { var replacement = substitute(children[i], substitutions); if (replacement !== null) { if (is_single_node(replacement)) { rv.push(replacement); } else { extend(rv, replacement); } } } else { extend(rv, do_substitution(String(children[i]))); } } return rv; } return rv; } function make_dom_single(template) { if (template[0] === "{text}") { var element = document.createTextNode(""); for (var i=1; i<template.length; i++) { element.data += template[i]; } } else { var element = document.createElement(template[0]); for (name in template[1]) { if (template[1].hasOwnProperty(name)) { element.setAttribute(name, template[1][name]); } } for (var i=2; i<template.length; i++) { if (template[i] instanceof Object) { var sub_element = make_dom(template[i]); element.appendChild(sub_element); } else { var text_node = document.createTextNode(template[i]); element.appendChild(text_node); } } } return element; } function make_dom(template, substitutions) { if (is_single_node(template)) { return make_dom_single(template); } else { return map(template, function(x) { return make_dom_single(x); }); } } function render(template, substitutions) { return make_dom(substitute(template, substitutions)); } expose(render, "template.render"); function expose(object, name) { var components = name.split("."); var target = window; for (var i=0; i<components.length - 1; i++) { if (!(components[i] in target)) { target[components[i]] = {}; } target = target[components[i]]; } target[components[components.length - 1]] = object; } function extend(array, items) { Array.prototype.push.apply(array, items); }