2
* Copyright 2011 Twitter, Inc.
3
* Licensed under the Apache License, Version 2.0 (the "License");
4
* you may not use this file except in compliance with the License.
5
* You may obtain a copy of the License at
7
* http://www.apache.org/licenses/LICENSE-2.0
9
* Unless required by applicable law or agreed to in writing, software
10
* distributed under the License is distributed on an "AS IS" BASIS,
11
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
* See the License for the specific language governing permissions and
13
* limitations under the License.
21
Hogan.Template = function constructor(renderFunc, text, compiler) {
26
this.text = text || '';
29
Hogan.Template.prototype = {
30
// render: replaced by generated code.
31
r: function (context, partials, indent) { return ''; },
36
render: function render(context, partials, indent) {
37
return this.ri([context], partials || {}, indent);
40
// render internal -- a hook for overrides that catches partials too
41
ri: function (context, partials, indent) {
42
return this.r(context, partials, indent);
45
// tries to find a partial in the curent scope and render it
46
rp: function(name, context, partials, indent) {
47
var partial = partials[name];
53
if (this.c && typeof partial == 'string') {
54
partial = this.c.compile(partial);
57
return partial.ri(context, partials, indent);
61
rs: function(context, partials, section) {
63
tail = context[context.length - 1];
66
return buf = section(context, partials);
69
for (var i = 0; i < tail.length; i++) {
70
context.push(tail[i]);
71
buf += section(context, partials);
78
// maybe start a section
79
s: function(val, ctx, partials, inverted, start, end, tags) {
82
if (isArray(val) && val.length === 0) {
86
if (typeof val == 'function') {
87
val = this.ls(val, ctx, partials, inverted, start, end, tags);
90
pass = (val === '') || !!val;
92
if (!inverted && pass && ctx) {
93
ctx.push((typeof val == 'object') ? val : ctx[ctx.length - 1]);
99
// find values with dotted names
100
d: function(key, ctx, partials, returnFound) {
101
var names = key.split('.'),
102
val = this.f(names[0], ctx, partials, returnFound),
105
if (key === '.' && isArray(ctx[ctx.length - 2])) {
106
return ctx[ctx.length - 1];
109
for (var i = 1; i < names.length; i++) {
110
if (val && typeof val == 'object' && names[i] in val) {
118
if (returnFound && !val) {
122
if (!returnFound && typeof val == 'function') {
124
val = this.lv(val, ctx, partials);
131
// find values with normal names
132
f: function(key, ctx, partials, returnFound) {
137
for (var i = ctx.length - 1; i >= 0; i--) {
139
if (v && typeof v == 'object' && key in v) {
147
return (returnFound) ? false : "";
150
if (!returnFound && typeof val == 'function') {
151
val = this.lv(val, ctx, partials);
157
// higher order templates
158
ho: function(val, cx, partials, text, tags) {
159
var compiler = this.c;
160
var t = val.call(cx, text, function(t) {
161
return compiler.compile(t, {delimiters: tags}).render(cx, partials);
163
var s = compiler.compile(t.toString(), {delimiters: tags}).render(cx, partials);
168
// higher order template result buffer
171
// lambda replace section
172
ls: function(val, ctx, partials, inverted, start, end, tags) {
173
var cx = ctx[ctx.length - 1],
176
if (!inverted && this.c && val.length > 0) {
177
return this.ho(val, cx, partials, this.text.substring(start, end), tags);
182
if (typeof t == 'function') {
186
return this.ho(t, cx, partials, this.text.substring(start, end), tags);
193
// lambda replace variable
194
lv: function(val, ctx, partials) {
195
var cx = ctx[ctx.length - 1];
196
var result = val.call(cx);
197
if (typeof result == 'function') {
198
result = result.call(cx);
200
result = result.toString();
202
if (this.c && ~result.indexOf("{{")) {
203
return this.c.compile(result).render(cx, partials);
218
function hoganEscape(str) {
219
str = String((str === null || str === undefined) ? '' : str);
220
return hChars.test(str) ?
222
.replace(rAmp,'&')
225
.replace(rApos,''')
226
.replace(rQuot, '"') :
230
var isArray = Array.isArray || function(a) {
231
return Object.prototype.toString.call(a) === '[object Array]';
234
})(typeof exports !== 'undefined' ? exports : Hogan);
240
// Setup regex assignments
241
// remove whitespace according to Mustache spec
242
var rIsWhitespace = /\S/,
248
'#': 1, '^': 2, '/': 3, '!': 4, '>': 5,
249
'<': 6, '=': 7, '_v': 8, '{': 9, '&': 10
252
Hogan.scan = function scan(text, delimiters) {
253
var len = text.length,
269
if (buf.length > 0) {
270
tokens.push(new String(buf));
275
function lineIsWhitespace() {
276
var isAllWhitespace = true;
277
for (var j = lineStart; j < tokens.length; j++) {
279
(tokens[j].tag && tagTypes[tokens[j].tag] < tagTypes['_v']) ||
280
(!tokens[j].tag && tokens[j].match(rIsWhitespace) === null);
281
if (!isAllWhitespace) {
286
return isAllWhitespace;
289
function filterLine(haveSeenTag, noNewLine) {
292
if (haveSeenTag && lineIsWhitespace()) {
293
for (var j = lineStart, next; j < tokens.length; j++) {
294
if (!tokens[j].tag) {
295
if ((next = tokens[j+1]) && next.tag == '>') {
296
// set indent to token value
297
next.indent = tokens[j].toString()
302
} else if (!noNewLine) {
303
tokens.push({tag:'\n'});
307
lineStart = tokens.length;
310
function changeDelimiters(text, index) {
311
var close = '=' + ctag,
312
closeIndex = text.indexOf(close, index),
314
text.substring(text.indexOf('=', index) + 1, closeIndex)
317
otag = delimiters[0];
318
ctag = delimiters[1];
320
return closeIndex + close.length - 1;
324
delimiters = delimiters.split(' ');
325
otag = delimiters[0];
326
ctag = delimiters[1];
329
for (i = 0; i < len; i++) {
330
if (state == IN_TEXT) {
331
if (tagChange(otag, text, i)) {
336
if (text.charAt(i) == '\n') {
339
buf += text.charAt(i);
342
} else if (state == IN_TAG_TYPE) {
343
i += otag.length - 1;
344
tag = tagTypes[text.charAt(i + 1)];
345
tagType = tag ? text.charAt(i + 1) : '_v';
346
if (tagType == '=') {
347
i = changeDelimiters(text, i);
357
if (tagChange(ctag, text, i)) {
358
tokens.push({tag: tagType, n: trim(buf), otag: otag, ctag: ctag,
359
i: (tagType == '/') ? seenTag - ctag.length : i + otag.length});
361
i += ctag.length - 1;
363
if (tagType == '{') {
367
cleanTripleStache(tokens[tokens.length - 1]);
371
buf += text.charAt(i);
376
filterLine(seenTag, true);
381
function cleanTripleStache(token) {
382
if (token.n.substr(token.n.length - 1) === '}') {
383
token.n = token.n.substring(0, token.n.length - 1);
392
return s.replace(/^\s*|\s*$/g, '');
395
function tagChange(tag, text, index) {
396
if (text.charAt(index) != tag.charAt(0)) {
400
for (var i = 1, l = tag.length; i < l; i++) {
401
if (text.charAt(index + i) != tag.charAt(i)) {
409
function buildTree(tokens, kind, stack, customTags) {
410
var instructions = [],
414
while (tokens.length > 0) {
415
token = tokens.shift();
416
if (token.tag == '#' || token.tag == '^' || isOpener(token, customTags)) {
418
token.nodes = buildTree(tokens, token.tag, stack, customTags);
419
instructions.push(token);
420
} else if (token.tag == '/') {
421
if (stack.length === 0) {
422
throw new Error('Closing tag without opener: /' + token.n);
424
opener = stack.pop();
425
if (token.n != opener.n && !isCloser(token.n, opener.n, customTags)) {
426
throw new Error('Nesting error: ' + opener.n + ' vs. ' + token.n);
428
opener.end = token.i;
431
instructions.push(token);
435
if (stack.length > 0) {
436
throw new Error('missing closing tag: ' + stack.pop().n);
442
function isOpener(token, tags) {
443
for (var i = 0, l = tags.length; i < l; i++) {
444
if (tags[i].o == token.n) {
451
function isCloser(close, open, tags) {
452
for (var i = 0, l = tags.length; i < l; i++) {
453
if (tags[i].c == close && tags[i].o == open) {
459
function writeCode(tree) {
460
return 'i = i || "";var b = i + "";var _ = this;' + walk(tree) + 'return b;';
463
Hogan.generate = function (code, text, options) {
464
if (options.asString) {
465
return 'function(c,p,i){' + code + ';}';
468
return new Hogan.Template(new Function('c', 'p', 'i', code), text, Hogan);
472
return s.replace(rSlash, '\\\\')
473
.replace(rQuot, '\\\"')
474
.replace(rNewline, '\\n')
475
.replace(rCr, '\\r');
478
function chooseMethod(s) {
479
return (~s.indexOf('.')) ? 'd' : 'f';
482
function walk(tree) {
484
for (var i = 0, l = tree.length; i < l; i++) {
485
var tag = tree[i].tag;
487
code += section(tree[i].nodes, tree[i].n, chooseMethod(tree[i].n),
488
tree[i].i, tree[i].end, tree[i].otag + " " + tree[i].ctag);
489
} else if (tag == '^') {
490
code += invertedSection(tree[i].nodes, tree[i].n,
491
chooseMethod(tree[i].n));
492
} else if (tag == '<' || tag == '>') {
493
code += partial(tree[i]);
494
} else if (tag == '{' || tag == '&') {
495
code += tripleStache(tree[i].n, chooseMethod(tree[i].n));
496
} else if (tag == '\n') {
497
code += text('"\\n"' + (tree.length-1 == i ? '' : ' + i'));
498
} else if (tag == '_v') {
499
code += variable(tree[i].n, chooseMethod(tree[i].n));
500
} else if (tag === undefined) {
501
code += text('"' + esc(tree[i]) + '"');
507
function section(nodes, id, method, start, end, tags) {
508
return 'if(_.s(_.' + method + '("' + esc(id) + '",c,p,1),' +
509
'c,p,0,' + start + ',' + end + ', "' + tags + '")){' +
511
'function(c,p){ var b = "";' +
513
'return b;});c.pop();}' +
514
'else{b += _.b; _.b = ""};';
517
function invertedSection(nodes, id, method) {
518
return 'if (!_.s(_.' + method + '("' + esc(id) + '",c,p,1),c,p,1,0,0,"")){' +
523
function partial(tok) {
524
return 'b += _.rp("' + esc(tok.n) + '",c,p,"' + (tok.indent || '') + '");';
527
function tripleStache(id, method) {
528
return 'b += (_.' + method + '("' + esc(id) + '",c,p,0));';
531
function variable(id, method) {
532
return 'b += (_.v(_.' + method + '("' + esc(id) + '",c,p,0)));';
536
return 'b += ' + id + ';';
539
Hogan.parse = function(tokens, options) {
540
options = options || {};
541
return buildTree(tokens, '', [], options.sectionTags || []);
546
Hogan.compile = function(text, options) {
549
// asString: false (default)
551
// sectionTags: [{o: '_foo', c: 'foo'}]
552
// An array of object with o and c fields that indicate names for custom
553
// section tags. The example above allows parsing of {{_foo}}{{/foo}}.
555
// delimiters: A string that overrides the default delimiters.
558
options = options || {};
560
var key = text + '||' + !!options.asString;
562
var t = this.cache[key];
568
t = this.generate(writeCode(this.parse(this.scan(text, options.delimiters), options)), text, options);
569
return this.cache[key] = t;
571
})(typeof exports !== 'undefined' ? exports : Hogan);
574
if (typeof define === 'function' && define.amd) {