UNPKG

lettuce

Version:

Lettuce JS, Mini Mobile Framework for Romantic with DSL.

320 lines (275 loc) 9.18 kB
Handlebars = { compile: function(string) { var compiler = new Handlebars.Compiler(string); var result = compiler.compile(); return new Function("context", "fallback", "fallback = fallback || {}; var stack = [];" + result); }, isFunction: function(fn) { return toString.call(fn) === "[object Function]"; }, escapeText: function(string) { string = string.replace("'", "\\'"); if (string.slice(-1) == "\\") { string = string + "\\"; } return string; }, escapeExpression: function(string) { // don't escape SafeStrings, since they're already safe if (string instanceof Handlebars.SafeString) { return string.toString(); } else if (string === null) { string = ""; } return string.replace(/&(?!\w+;)|["\\<>]/g, function(str) { switch(str) { case "&": return "&amp;"; break; case '"': return "\""; case "\\": return "\\\\"; break; case "<": return "&lt;"; break; case ">": return "&gt;"; break; default: return str; } }); }, buildContext: function(context, stack) { var contextWrapper = function(stack) { this.__stack__ = stack; this.__get__ = function(path) { var context = this; var parsedPath = Handlebars.parsePath(path); var depth = parsedPath[0]; var parts = parsedPath[1]; if (depth > 0) { context = this.__stack__[stack.length - depth]; } for (var i = 0; i < parts.length; i++) { context = context[parts[i]]; } return context; }; }; contextWrapper.prototype = context; return new contextWrapper(stack); }, // Returns a two element array containing the numbers of contexts to back up the stack and // the properties to dig into on the current context // // For example, if the path is "../../alan/name", the result will be [2, ["alan", "name"]]. parsePath: function(path) { if (path == null) { return [0, []]; } var parts = path.split("/"); var readDepth = false; var depth = 0; var dig = []; for (var i = 0, j = parts.length; i < j; i++) { switch(parts[i]) { case "..": if (readDepth) { throw new Handlebars.Exception("Cannot jump out of context after moving into a context."); } else { depth += 1; } break; case ".": // do nothing - using .'s is pretty dumb, but it's also basically free for us to support case "this": // if we do nothing you'll end up sticking in the same context break; default: readDepth = true; dig.push(parts[i]); } } return [depth, dig]; } } Handlebars.Compiler = function(string) { this.string = string; this.pointer = -1; this.mustache = false; this.text = ""; this.fn = "var out = ''; var lookup; "; this.newlines = ""; this.comment = false; this.escaped = true; this.partial = false; }; Handlebars.Exception = function(message) { this.message = message; }; // Build out our basic SafeString type Handlebars.SafeString = function(string) { this.string = string; } Handlebars.SafeString.prototype.toString = function() { return this.string.toString(); } Handlebars.helperMissing = function(object, fn) { var ret = ""; if(object === true) { return fn(this); } else if(object === false) { return ""; } else if(toString.call(object) === "[object Array]") { for(var i=0, j=object.length; i<j; i++) { ret = ret + fn(object[i]); } return ret; } }; Handlebars.Compiler.prototype = { getChar: function(n) { var ret = this.peek(n); this.pointer = this.pointer + (n || 1); return ret; }, peek: function(n) { n = n || 1; var start = this.pointer + 1; return this.string.slice(start, start + n); }, compile: function(endCondition) { var chr; while(chr = this.getChar()) { if(chr === "{" && this.peek() === "{" && !this.mustache) { this.getChar(); this.parseMustache(); } else { if(chr === "\n") { this.newlines = this.newlines + "\n"; chr = "\\n"; } this.text = this.text + chr; } if(endCondition && endCondition(this)) { break }; } this.addText(); return this.fn + "return out;"; }, addText: function() { if(this.text) { this.fn = this.fn + "out = out + \"" + Handlebars.escapeText(this.text) + "\"; "; this.fn = this.fn + this.newlines; this.newlines = ""; this.text = ""; } }, addExpression: function(mustache) { var expr = this.lookupFor(mustache); var escapeCall = "("; if (this.escaped) { escapeCall = "Handlebars.escapeExpression("; } this.fn += "if (Handlebars.isFunction(" + expr + ")) out = out + " + escapeCall + expr + ".call(Handlebars.buildContext(context, stack))); "; this.fn += "else if(typeof " + expr + "!== 'undefined') out = out + " + escapeCall + expr + ");"; }, lookupFor: function(param) { var parsed = Handlebars.parsePath(param); var depth = parsed[0]; var parts = parsed[1]; var paramExpr = ""; for(var i = 0; i < parts.length; i++) { paramExpr += "['" + parts[i] + "']"; } if (depth > 0) { return "( stack[stack.length - " + depth + "]" + paramExpr + ")"; } else { return "( context" + paramExpr + " || fallback " + paramExpr + " )"; } }, addBlock: function(mustache, param, parts) { // set up the stack before the new compiler starts this.fn += "stack.push(context);"; var compiler = new Handlebars.Compiler(this.string.slice(this.pointer + 1, -1)); // sub-compile with a custom EOF instruction var result = compiler.compile(function(compiler) { if(compiler.peek(mustache.length + 5) === "{{/" + mustache + "}}") { compiler.getChar(mustache.length + 5); return true; } }); // move the pointer forward the amount of characters handled by the sub-compiler this.pointer += compiler.pointer + 1; // each function made internally needs a unique IDs. These are locals, so they // don't need to be globally unique, just per compiler var fnId = "fn" + this.pointer.toString(); this.fn += "var proxy = Handlebars.buildContext(" + param + ", stack);"; this.fn += "var " + fnId + " = function(context) {" + result + "}; "; this.fn += "lookup = " + this.lookupFor(mustache) + "; "; this.fn += "if(Handlebars.isFunction(lookup)) out = out + lookup.call(proxy, " + fnId + "); "; this.fn += "else if(typeof lookup !== 'undefined') out = out + Handlebars.helperMissing.call(proxy, lookup, " + fnId + "); "; this.fn += "stack.pop();"; this.openBlock = false; }, addPartial: function(mustache, param) { // either used a cached copy of the partial or compile a new one this.fn += "if (typeof fallback['partials'] === 'undefined' || typeof fallback['partials']['" + mustache + "'] === 'undefined') throw new Handlebars.Exception('Attempted to render undefined partial: " + mustache + "');"; this.fn += "if (!Handlebars.isFunction(fallback['partials']['" + mustache + "'])) fallback['partials']['" + mustache + "'] = Handlebars.compile(fallback['partials']['" + mustache + "']);"; this.fn += "out = out + fallback['partials']['" + mustache + "'](" + param + ", fallback);"; }, parseMustache: function() { var chr, part, mustache, param; var next = this.peek(); if(next === "!") { this.comment = true; this.getChar(); } else if(next === "#") { this.openBlock = true; this.getChar(); } else if (next === ">") { this.partial = true; this.getChar(); } else if(next === "{" || next === "&") { this.escaped = false; this.getChar(); } this.addText(); this.mustache = " "; while(chr = this.getChar()) { if(this.mustache && chr === "}" && this.peek() === "}") { var parts = this.mustache.trim().split(/\s+/); mustache = parts[0]; param = this.lookupFor(parts[1]); this.mustache = false; // finish reading off the close of the handlebars this.getChar(); // {{{expression}} is techically valid, but if we started with {{{ we'll try to read // }}} off of the close of the handlebars if (!this.escaped && this.peek() === "}") { this.getChar(); } if(this.comment) { this.comment = false; return; } else if (this.partial) { this.addPartial(mustache, param) return; } else if(this.openBlock) { this.addBlock(mustache, param, parts) return; } else { return this.addExpression(mustache); } this.escaped = true; } else if(this.comment) { ; } else { this.mustache = this.mustache + chr; } } } }