Last active
December 17, 2015 08:29
-
-
Save urban/5580457 to your computer and use it in GitHub Desktop.
Hogan.js with inheritance
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* Copyright 2011 Twitter, Inc. | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
var Hogan = {}; | |
(function (Hogan, useArrayBuffer) { | |
Hogan.Template = function (codeObj, text, compiler, options) { | |
codeObj = codeObj || {}; | |
this.r = codeObj.code || this.r; | |
this.c = compiler; | |
this.options = options || {}; | |
this.text = text || ''; | |
this.partials = codeObj.partials || {}; | |
this.subs = codeObj.subs || {}; | |
this.ib(); | |
} | |
Hogan.Template.prototype = { | |
// render: replaced by generated code. | |
r: function (context, partials, indent) { return ''; }, | |
// variable escaping | |
v: hoganEscape, | |
// triple stache | |
t: coerceToString, | |
render: function render(context, partials, indent) { | |
return this.ri([context], partials || {}, indent); | |
}, | |
// render internal -- a hook for overrides that catches partials too | |
ri: function (context, partials, indent) { | |
return this.r(context, partials, indent); | |
}, | |
// ensurePartial | |
ep: function(symbol, partials) { | |
var partial = this.partials[symbol]; | |
// check to see that if we've instantiated this partial before | |
var template = partials[partial.name]; | |
if (partial.instance && partial.base == template) { | |
return partial.instance; | |
} | |
if (typeof template == 'string') { | |
if (!this.c) { | |
throw new Error("No compiler available."); | |
} | |
template = this.c.compile(template, this.options); | |
} | |
if (!template) { | |
return null; | |
} | |
// We use this to check whether the partials dictionary has changed | |
this.partials[symbol].base = template; | |
if (partial.subs) { | |
// Make sure we consider parent template now | |
if (this.activeSub === undefined) { | |
// Store parent template text in partials.stackText to perform substitutions in child templates correctly | |
partials.stackText = this.text; | |
} | |
template = createSpecializedPartial(template, partial.subs, partial.partials, partials.stackText || this.text); | |
} | |
this.partials[symbol].instance = template; | |
return template; | |
}, | |
// tries to find a partial in the current scope and render it | |
rp: function(symbol, context, partials, indent) { | |
var partial = this.ep(symbol, partials); | |
if (!partial) { | |
return ''; | |
} | |
return partial.ri(context, partials, indent); | |
}, | |
// render a section | |
rs: function(context, partials, section) { | |
var tail = context[context.length - 1]; | |
if (!isArray(tail)) { | |
section(context, partials, this); | |
return; | |
} | |
for (var i = 0; i < tail.length; i++) { | |
context.push(tail[i]); | |
section(context, partials, this); | |
context.pop(); | |
} | |
}, | |
// maybe start a section | |
s: function(val, ctx, partials, inverted, start, end, tags) { | |
var pass; | |
if (isArray(val) && val.length === 0) { | |
return false; | |
} | |
if (typeof val == 'function') { | |
val = this.ms(val, ctx, partials, inverted, start, end, tags); | |
} | |
pass = !!val; | |
if (!inverted && pass && ctx) { | |
ctx.push((typeof val == 'object') ? val : ctx[ctx.length - 1]); | |
} | |
return pass; | |
}, | |
// find values with dotted names | |
d: function(key, ctx, partials, returnFound) { | |
var found, | |
names = key.split('.'), | |
val = this.f(names[0], ctx, partials, returnFound), | |
doModelGet = this.options.modelGet, | |
cx = null; | |
if (key === '.' && isArray(ctx[ctx.length - 2])) { | |
val = ctx[ctx.length - 1]; | |
} else { | |
for (var i = 1; i < names.length; i++) { | |
found = findInScope(names[i], val, doModelGet); | |
if (found != null) { | |
cx = val; | |
val = found; | |
} else { | |
val = ''; | |
} | |
} | |
} | |
if (returnFound && !val) { | |
return false; | |
} | |
if (!returnFound && typeof val == 'function') { | |
ctx.push(cx); | |
val = this.mv(val, ctx, partials); | |
ctx.pop(); | |
} | |
return val; | |
}, | |
// find values with normal names | |
f: function(key, ctx, partials, returnFound) { | |
var val = false, | |
v = null, | |
found = false, | |
doModelGet = this.options.modelGet; | |
for (var i = ctx.length - 1; i >= 0; i--) { | |
v = ctx[i]; | |
val = findInScope(key, v, doModelGet); | |
if (val != null) { | |
found = true; | |
break; | |
} | |
} | |
if (!found) { | |
return (returnFound) ? false : ""; | |
} | |
if (!returnFound && typeof val == 'function') { | |
val = this.mv(val, ctx, partials); | |
} | |
return val; | |
}, | |
// higher order templates | |
ls: function(func, cx, partials, text, tags) { | |
var oldTags = this.options.delimiters; | |
this.options.delimiters = tags; | |
this.b(this.ct(coerceToString(func.call(cx, text)), cx, partials)); | |
this.options.delimiters = oldTags; | |
return false; | |
}, | |
// compile text | |
ct: function(text, cx, partials) { | |
if (this.options.disableLambda) { | |
throw new Error('Lambda features disabled.'); | |
} | |
return this.c.compile(text, this.options).render(cx, partials); | |
}, | |
// template result buffering | |
b: (useArrayBuffer) ? function(s) { this.buf.push(s); } : | |
function(s) { this.buf += s; }, | |
fl: (useArrayBuffer) ? function() { var r = this.buf.join(''); this.buf = []; return r; } : | |
function() { var r = this.buf; this.buf = ''; return r; }, | |
// init the buffer | |
ib: function () { | |
this.buf = (useArrayBuffer) ? [] : ''; | |
}, | |
// method replace section | |
ms: function(func, ctx, partials, inverted, start, end, tags) { | |
var textSource, | |
cx = ctx[ctx.length - 1], | |
result = func.call(cx); | |
if (typeof result == 'function') { | |
if (inverted) { | |
return true; | |
} else { | |
textSource = (this.activeSub && this.subsText[this.activeSub]) ? this.subsText[this.activeSub] : this.text; | |
return this.ls(result, cx, partials, textSource.substring(start, end), tags); | |
} | |
} | |
return result; | |
}, | |
// method replace variable | |
mv: function(func, ctx, partials) { | |
var cx = ctx[ctx.length - 1]; | |
var result = func.call(cx); | |
if (typeof result == 'function') { | |
return this.ct(coerceToString(result.call(cx)), cx, partials); | |
} | |
return result; | |
}, | |
sub: function(name, context, partials, indent) { | |
var f = this.subs[name]; | |
if (f) { | |
this.activeSub = name; | |
f(context, partials, this, indent); | |
this.activeSub = false; | |
} | |
} | |
}; | |
//Find a key in an object | |
function findInScope(key, scope, doModelGet) { | |
var val, checkVal; | |
if (scope && typeof scope == 'object') { | |
if (scope[key] != null) { | |
val = scope[key]; | |
// try lookup with get for backbone or similar model data | |
} else if (doModelGet && scope.get && typeof scope.get == 'function') { | |
val = scope.get(key); | |
} | |
} | |
return val; | |
} | |
function createSpecializedPartial(instance, subs, partials, childText) { | |
function PartialTemplate() {}; | |
PartialTemplate.prototype = instance; | |
function Substitutions() {}; | |
Substitutions.prototype = instance.subs; | |
var key; | |
var partial = new PartialTemplate(); | |
partial.subs = new Substitutions(); | |
partial.subsText = {}; //hehe. substext. | |
partial.ib(); | |
for (key in subs) { | |
partial.subs[key] = subs[key]; | |
partial.subsText[key] = childText; | |
} | |
for (key in partials) { | |
partial.partials[key] = partials[key]; | |
} | |
return partial; | |
} | |
var rAmp = /&/g, | |
rLt = /</g, | |
rGt = />/g, | |
rApos = /\'/g, | |
rQuot = /\"/g, | |
hChars = /[&<>\"\']/; | |
function coerceToString(val) { | |
return String((val === null || val === undefined) ? '' : val); | |
} | |
function hoganEscape(str) { | |
str = coerceToString(str); | |
return hChars.test(str) ? | |
str | |
.replace(rAmp, '&') | |
.replace(rLt, '<') | |
.replace(rGt, '>') | |
.replace(rApos, ''') | |
.replace(rQuot, '"') : | |
str; | |
} | |
var isArray = Array.isArray || function(a) { | |
return Object.prototype.toString.call(a) === '[object Array]'; | |
}; | |
})(typeof exports !== 'undefined' ? exports : Hogan); | |
(function (Hogan) { | |
// Setup regex assignments | |
// remove whitespace according to Mustache spec | |
var rIsWhitespace = /\S/, | |
rQuot = /\"/g, | |
rNewline = /\n/g, | |
rCr = /\r/g, | |
rSlash = /\\/g; | |
Hogan.tags = { | |
'#': 1, '^': 2, '<': 3, '$': 4, | |
'/': 5, '!': 6, '>': 7, '=': 8, '_v': 9, | |
'{': 10, '&': 11, '_t': 12 | |
}; | |
Hogan.scan = function scan(text, delimiters) { | |
var len = text.length, | |
IN_TEXT = 0, | |
IN_TAG_TYPE = 1, | |
IN_TAG = 2, | |
state = IN_TEXT, | |
tagType = null, | |
tag = null, | |
buf = '', | |
tokens = [], | |
seenTag = false, | |
i = 0, | |
lineStart = 0, | |
otag = '{{', | |
ctag = '}}'; | |
function addBuf() { | |
if (buf.length > 0) { | |
tokens.push({tag: '_t', text: new String(buf)}); | |
buf = ''; | |
} | |
} | |
function lineIsWhitespace() { | |
var isAllWhitespace = true; | |
for (var j = lineStart; j < tokens.length; j++) { | |
isAllWhitespace = | |
(Hogan.tags[tokens[j].tag] < Hogan.tags['_v']) || | |
(tokens[j].tag == '_t' && tokens[j].text.match(rIsWhitespace) === null); | |
if (!isAllWhitespace) { | |
return false; | |
} | |
} | |
return isAllWhitespace; | |
} | |
function filterLine(haveSeenTag, noNewLine) { | |
addBuf(); | |
if (haveSeenTag && lineIsWhitespace()) { | |
for (var j = lineStart, next; j < tokens.length; j++) { | |
if (tokens[j].text) { | |
if ((next = tokens[j+1]) && next.tag == '>') { | |
// set indent to token value | |
next.indent = tokens[j].text.toString() | |
} | |
tokens.splice(j, 1); | |
} | |
} | |
} else if (!noNewLine) { | |
tokens.push({tag:'\n'}); | |
} | |
seenTag = false; | |
lineStart = tokens.length; | |
} | |
function changeDelimiters(text, index) { | |
var close = '=' + ctag, | |
closeIndex = text.indexOf(close, index), | |
delimiters = trim( | |
text.substring(text.indexOf('=', index) + 1, closeIndex) | |
).split(' '); | |
otag = delimiters[0]; | |
ctag = delimiters[delimiters.length - 1]; | |
return closeIndex + close.length - 1; | |
} | |
if (delimiters) { | |
delimiters = delimiters.split(' '); | |
otag = delimiters[0]; | |
ctag = delimiters[1]; | |
} | |
for (i = 0; i < len; i++) { | |
if (state == IN_TEXT) { | |
if (tagChange(otag, text, i)) { | |
--i; | |
addBuf(); | |
state = IN_TAG_TYPE; | |
} else { | |
if (text.charAt(i) == '\n') { | |
filterLine(seenTag); | |
} else { | |
buf += text.charAt(i); | |
} | |
} | |
} else if (state == IN_TAG_TYPE) { | |
i += otag.length - 1; | |
tag = Hogan.tags[text.charAt(i + 1)]; | |
tagType = tag ? text.charAt(i + 1) : '_v'; | |
if (tagType == '=') { | |
i = changeDelimiters(text, i); | |
state = IN_TEXT; | |
} else { | |
if (tag) { | |
i++; | |
} | |
state = IN_TAG; | |
} | |
seenTag = i; | |
} else { | |
if (tagChange(ctag, text, i)) { | |
tokens.push({tag: tagType, n: trim(buf), otag: otag, ctag: ctag, | |
i: (tagType == '/') ? seenTag - otag.length : i + ctag.length}); | |
buf = ''; | |
i += ctag.length - 1; | |
state = IN_TEXT; | |
if (tagType == '{') { | |
if (ctag == '}}') { | |
i++; | |
} else { | |
cleanTripleStache(tokens[tokens.length - 1]); | |
} | |
} | |
} else { | |
buf += text.charAt(i); | |
} | |
} | |
} | |
filterLine(seenTag, true); | |
return tokens; | |
} | |
function cleanTripleStache(token) { | |
if (token.n.substr(token.n.length - 1) === '}') { | |
token.n = token.n.substring(0, token.n.length - 1); | |
} | |
} | |
function trim(s) { | |
if (s.trim) { | |
return s.trim(); | |
} | |
return s.replace(/^\s*|\s*$/g, ''); | |
} | |
function tagChange(tag, text, index) { | |
if (text.charAt(index) != tag.charAt(0)) { | |
return false; | |
} | |
for (var i = 1, l = tag.length; i < l; i++) { | |
if (text.charAt(index + i) != tag.charAt(i)) { | |
return false; | |
} | |
} | |
return true; | |
} | |
// the tags allowed inside super templates | |
var allowedInSuper = {'_t': true, '\n': true, '$': true, '/': true}; | |
function buildTree(tokens, kind, stack, customTags) { | |
var instructions = [], | |
opener = null, | |
tail = null, | |
token = null; | |
tail = stack[stack.length - 1]; | |
while (tokens.length > 0) { | |
token = tokens.shift(); | |
if (tail && tail.tag == '<' && !(token.tag in allowedInSuper)) { | |
throw new Error('Illegal content in < super tag.'); | |
} | |
if (Hogan.tags[token.tag] <= Hogan.tags['$'] || isOpener(token, customTags)) { | |
stack.push(token); | |
token.nodes = buildTree(tokens, token.tag, stack, customTags); | |
} else if (token.tag == '/') { | |
if (stack.length === 0) { | |
throw new Error('Closing tag without opener: /' + token.n); | |
} | |
opener = stack.pop(); | |
if (token.n != opener.n && !isCloser(token.n, opener.n, customTags)) { | |
throw new Error('Nesting error: ' + opener.n + ' vs. ' + token.n); | |
} | |
opener.end = token.i; | |
return instructions; | |
} else if (token.tag == '\n') { | |
token.last = (tokens.length == 0) || (tokens[0].tag == '\n'); | |
} | |
instructions.push(token); | |
} | |
if (stack.length > 0) { | |
throw new Error('missing closing tag: ' + stack.pop().n); | |
} | |
return instructions; | |
} | |
function isOpener(token, tags) { | |
for (var i = 0, l = tags.length; i < l; i++) { | |
if (tags[i].o == token.n) { | |
token.tag = '#'; | |
return true; | |
} | |
} | |
} | |
function isCloser(close, open, tags) { | |
for (var i = 0, l = tags.length; i < l; i++) { | |
if (tags[i].c == close && tags[i].o == open) { | |
return true; | |
} | |
} | |
} | |
function stringifySubstitutions(obj) { | |
var items = []; | |
for (var key in obj) { | |
items.push('"' + esc(key) + '": function(c,p,t,i) {' + obj[key] + '}'); | |
} | |
return "{ " + items.join(",") + " }"; | |
} | |
function stringifyPartials(codeObj) { | |
var partials = []; | |
for (var key in codeObj.partials) { | |
partials.push('"' + esc(key) + '":{name:"' + esc(codeObj.partials[key].name) + '", ' + stringifyPartials(codeObj.partials[key]) + "}"); | |
} | |
return "partials: {" + partials.join(",") + "}, subs: " + stringifySubstitutions(codeObj.subs); | |
} | |
Hogan.stringify = function(codeObj, text, options) { | |
return "{code: function (c,p,i) { " + Hogan.wrapMain(codeObj.code) + " }," + stringifyPartials(codeObj) + "}"; | |
} | |
var serialNo = 0; | |
Hogan.generate = function(tree, text, options) { | |
serialNo = 0; | |
var context = { code: '', subs: {}, partials: {} }; | |
Hogan.walk(tree, context); | |
if (options.asString) { | |
return this.stringify(context, text, options); | |
} | |
return this.makeTemplate(context, text, options); | |
} | |
Hogan.wrapMain = function(code) { | |
return 'var t=this;t.b(i=i||"");' + code + 'return t.fl();'; | |
} | |
Hogan.template = Hogan.Template; | |
Hogan.makeTemplate = function(codeObj, text, options) { | |
var template = this.makePartials(codeObj); | |
template.code = new Function('c', 'p', 'i', this.wrapMain(codeObj.code)); | |
return new this.template(template, text, this, options); | |
} | |
Hogan.makePartials = function(codeObj) { | |
var key, template = {subs: {}, partials: codeObj.partials, name: codeObj.name}; | |
for (key in template.partials) { | |
template.partials[key] = this.makePartials(template.partials[key]); | |
} | |
for (key in codeObj.subs) { | |
template.subs[key] = new Function('c', 'p', 't', 'i', codeObj.subs[key]); | |
} | |
return template; | |
} | |
function esc(s) { | |
return s.replace(rSlash, '\\\\') | |
.replace(rQuot, '\\\"') | |
.replace(rNewline, '\\n') | |
.replace(rCr, '\\r'); | |
} | |
function chooseMethod(s) { | |
return (~s.indexOf('.')) ? 'd' : 'f'; | |
} | |
function createPartial(node, context) { | |
var prefix = "<" + (context.prefix || ""); | |
var sym = prefix + node.n + serialNo++; | |
context.partials[sym] = {name: node.n, partials: {}}; | |
context.code += 't.b(t.rp("' + esc(sym) + '",c,p,"' + (node.indent || '') + '"));'; | |
return sym; | |
} | |
Hogan.codegen = { | |
'#': function(node, context) { | |
context.code += 'if(t.s(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,1),' + | |
'c,p,0,' + node.i + ',' + node.end + ',"' + node.otag + " " + node.ctag + '")){' + | |
't.rs(c,p,' + 'function(c,p,t){'; | |
Hogan.walk(node.nodes, context); | |
context.code += '});c.pop();}'; | |
}, | |
'^': function(node, context) { | |
context.code += 'if(!t.s(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,1),c,p,1,0,0,"")){'; | |
Hogan.walk(node.nodes, context); | |
context.code += '};'; | |
}, | |
'>': createPartial, | |
'<': function(node, context) { | |
var ctx = {partials: {}, code: '', subs: {}, inPartial: true}; | |
Hogan.walk(node.nodes, ctx); | |
var template = context.partials[createPartial(node, context)]; | |
template.subs = ctx.subs; | |
template.partials = ctx.partials; | |
}, | |
'$': function(node, context) { | |
var ctx = {subs: {}, code: '', partials: context.partials, prefix: node.n}; | |
Hogan.walk(node.nodes, ctx); | |
context.subs[node.n] = ctx.code; | |
if (!context.inPartial) { | |
context.code += 't.sub("' + esc(node.n) + '",c,p,i);'; | |
} | |
}, | |
'\n': function(node, context) { | |
context.code += write('"\\n"' + (node.last ? '' : ' + i')); | |
}, | |
'_v': function(node, context) { | |
context.code += 't.b(t.v(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,0)));'; | |
}, | |
'_t': function(node, context) { | |
context.code += write('"' + esc(node.text) + '"'); | |
}, | |
'{': tripleStache, | |
'&': tripleStache | |
} | |
function tripleStache(node, context) { | |
context.code += 't.b(t.t(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,0)));'; | |
} | |
function write(s) { | |
return 't.b(' + s + ');'; | |
} | |
Hogan.walk = function(nodelist, context) { | |
var func; | |
for (var i = 0, l = nodelist.length; i < l; i++) { | |
func = Hogan.codegen[nodelist[i].tag]; | |
func && func(nodelist[i], context); | |
} | |
return context; | |
} | |
Hogan.parse = function(tokens, text, options) { | |
options = options || {}; | |
return buildTree(tokens, '', [], options.sectionTags || []); | |
} | |
Hogan.cache = {}; | |
Hogan.cacheKey = function(text, options) { | |
return [text, !!options.asString, !!options.disableLambda, options.delimiters, !!options.modelGet].join('||'); | |
} | |
Hogan.compile = function(text, options) { | |
options = options || {}; | |
var key = Hogan.cacheKey(text, options); | |
var template = this.cache[key]; | |
if (template) { | |
return template; | |
} | |
template = this.generate(this.parse(this.scan(text, options.delimiters), text, options), text, options); | |
return this.cache[key] = template; | |
} | |
})(typeof exports !== 'undefined' ? exports : Hogan); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Hogan.js | |
description: Custom build of Hogan.js | |
authors: | |
- Urban Faubion | |
resources: | |
normalize_css: no |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment