/*\ title: js/Renderer.js Renderer objects encapsulate a tree of nodes that are capable of rendering and selectively updating an HTML representation of themselves. The following node types are defined: * ''MacroNode'' - represents an invocation of a macro * ''ElementNode'' - represents a single HTML element * ''TextNode'' - represents an HTML text node * ''EntityNode'' - represents an HTML entity node * ''RawNode'' - represents a chunk of unparsed HTML text These node types are implemented with prototypal inheritance from a base Node class. One unusual convenience in the implementation is that the node constructors can be called without an explicit `new` keyword: the constructors check for `this` not being an instance of themselves, and recursively invoke themselves with `new` when required. Convenience functions are provided to wrap up the construction of some common interface elements: * ''ErrorNode'' * ''LabelNode'' * ''SplitLabelNode'' \*/ (function(){ /*jshint node: true, browser: true */ "use strict"; var utils = require("./Utils.js"), ArgParser = require("./ArgParser.js").ArgParser, Dependencies = require("./Dependencies.js").Dependencies, esprima = require("esprima"); /* Intialise the renderer object nodes: an array of Node objects dependencies: an optional Dependencies object store: a reference to the WikiStore object to use for rendering these nodes */ var Renderer = function(nodes,dependencies,store) { this.nodes = nodes; this.dependencies = dependencies; this.store = store; }; /* The base class for all types of renderer nodes */ var Node = function(children) { if(this instanceof Node) { this.children = children; } else { return new Node(children); } }; Node.prototype.clone = function() { // By default we don't actually clone nodes, we just re-use them (we do clone macros and elements) return this; }; Node.prototype.broadcastEvent = function(event) { return true; }; Node.prototype.execute = Node.prototype.render = Node.prototype.renderInDom = Node.prototype.refresh = Node.prototype.refreshInDom = function() { // All these methods are no-ops by default }; /* Construct a renderer node representing a macro invocation macroName: name of the macro srcParams: a hashmap of parameters (each can be a string, or a fn(tiddler,store,utils) for evaluated parameters) children: optional array of child nodes store: reference to the WikiStore associated with this macro dependencies: optional Dependencies object representing the dependencies of this macro Note that the dependencies will be evaluated if not provided. */ var MacroNode = function(macroName,srcParams,children,store,dependencies) { if(this instanceof MacroNode) { // Save the details this.macroName = macroName; this.macro = store.macros[macroName]; this.children = children; this.store = store; this.srcParams = typeof srcParams === "string" ? this.parseMacroParamString(srcParams) : srcParams; // Evaluate the dependencies if required this.dependencies = dependencies ? dependencies : this.evaluateDependencies(); } else { return new MacroNode(macroName,srcParams,children,store,dependencies); } }; MacroNode.prototype = new Node(); MacroNode.prototype.constructor = MacroNode; /* Evaluate the dependencies of this macro invocation. If the macro provides an `evaluateDependencies` method then it is invoked to evaluate the dependencies. Otherwise it generates the dependencies based on the macro parameters provided */ MacroNode.prototype.evaluateDependencies = function() { if(this.srcParams && this.macro) { if(this.macro.evaluateDependencies) { // Call the evaluateDependencies method if the macro provides it return this.macro.evaluateDependencies.call(this); } else { // Figure out the dependencies from the metadata and parameters var dependencies = new Dependencies(); if(this.macro.dependentAll) { dependencies.dependentAll = true; } if(this.macro.dependentOnContextTiddler) { dependencies.dependentOnContextTiddler = true; } for(var m in this.macro.params) { var paramInfo = this.macro.params[m]; if(m in this.srcParams && paramInfo.type === "tiddler") { if(typeof this.srcParams[m] === "function") { dependencies.dependentAll = true; } else { dependencies.addDependency(this.srcParams[m],!paramInfo.skinny); } } } return dependencies; } } }; MacroNode.prototype.parseMacroParamString = function(paramString) { /*jslint evil: true */ var params = {}, args = new ArgParser(paramString,{defaultName: "anon", cascadeDefaults: this.macro.cascadeDefaults}), self = this, insertParam = function(name,arg) { if(arg.evaluated) { params[name] = eval(esprima.generate( // (function(tiddler,store,utils) {return {paramOne: 1};}) { "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "FunctionExpression", "id": null, "params": [ { "type": "Identifier", "name": "tiddler" }, { "type": "Identifier", "name": "store" }, { "type": "Identifier", "name": "utils" } ], "body": { "type": "BlockStatement", "body": [ { "type": "ReturnStatement", "argument": esprima.parse("(" + arg.string + ")").body[0].expression } ] } } } ] } )); } else { params[name] = arg.string; } }; for(var m in this.macro.params) { var param = this.macro.params[m], arg; if("byPos" in param && args.byPos[param.byPos] && (args.byPos[param.byPos].n === "anon" || args.byPos[param.byPos].n === m)) { arg = args.byPos[param.byPos].v; insertParam(m,arg); } else { arg = args.getValueByName(m); if(!arg && param.byName === "default") { arg = args.getValueByName("anon"); } if(arg) { insertParam(m,arg); } } } return params; }; MacroNode.prototype.hasParameter = function(name) { return this.params.hasOwnProperty(name); }; MacroNode.prototype.clone = function() { return new MacroNode(this.macroName,this.srcParams,this.cloneChildren(),this.store,this.dependencies); }; MacroNode.prototype.cloneChildren = function() { var childClones; if(this.children) { childClones = []; for(var t=0; t"); } if(this.children) { for(var t=0; t"); } } return output.join(""); }; ElementNode.prototype.renderInDom = function(parentDomNode,insertBefore) { var element = document.createElement(this.type); if(this.attributes) { for(var a in this.attributes) { var v = this.attributes[a]; if(v !== undefined) { if(v instanceof Array) { // Ahem, could there be arrays other than className? element.className = v.join(" "); } else if (typeof v === "object") { // ...or objects other than style? for(var p in v) { element.style[p] = v[p]; } } else { element.setAttribute(a,v); } } } } if(insertBefore) { parentDomNode.insertBefore(element,insertBefore); } else { parentDomNode.appendChild(element); } this.domNode = element; if(this.children) { for(var t=0; t