mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
365 lines
12 KiB
JavaScript
365 lines
12 KiB
JavaScript
Handlebars = {};
|
|
|
|
// XXX we probably forgot to implement the #foo case where foo is not
|
|
// a helper (and similarly the ^foo case)
|
|
|
|
// XXX there is a ton of stuff that needs testing! like,
|
|
// everything. including the '..' stuff.
|
|
|
|
Handlebars.json_ast_to_func = function (ast) {
|
|
return function (data, options) {
|
|
return Handlebars.evaluate(ast, data, options);
|
|
};
|
|
};
|
|
|
|
// block helpers take:
|
|
// (N args), options (hash args, plus 'fn' and 'inverse')
|
|
// and return text
|
|
//
|
|
// normal helpers take:
|
|
// (N args), options (hash args)
|
|
//
|
|
// partials take one argument, data
|
|
|
|
// XXX handlebars' format for arguments is not the clearest, likely
|
|
// for backwards compatibility to mustache. eg, options ===
|
|
// options.fn. take the opportunity to clean this up. treat block
|
|
// arguments (fn, inverse) as just another kind of argument, same as
|
|
// what is passed in via named arguments.
|
|
Handlebars._default_helpers = {
|
|
'with': function (data, options) {
|
|
return options.fn(data);
|
|
},
|
|
'each': function (data, options) {
|
|
var parentData = this;
|
|
if (data && data.length > 0)
|
|
return _.map(data, function(x, i) {
|
|
// infer a branch key from the data
|
|
var branch = (x._id || (typeof x === 'string' ? x : null) || i);
|
|
return Spark.labelBranch(branch, function() {
|
|
return options.fn(x);
|
|
});
|
|
}).join('');
|
|
else
|
|
return Spark.labelBranch(
|
|
'else',
|
|
function () {
|
|
return options.inverse(parentData);
|
|
});
|
|
},
|
|
'if': function (data, options) {
|
|
if (!data || (data instanceof Array && !data.length))
|
|
return options.inverse(this);
|
|
else
|
|
return options.fn(this);
|
|
},
|
|
'unless': function (data, options) {
|
|
if (!data || (data instanceof Array && !data.length))
|
|
return options.fn(this);
|
|
else
|
|
return options.inverse(this);
|
|
}
|
|
};
|
|
|
|
Handlebars.registerHelper = function (name, func) {
|
|
if (name in Handlebars._default_helpers)
|
|
throw new Error("There is already a helper '" + name + "'");
|
|
Handlebars._default_helpers[name] = func;
|
|
};
|
|
|
|
// Utility to HTML-escape a string.
|
|
Handlebars._escape = (function() {
|
|
var escape_map = {
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
"`": "`", /* IE allows backtick-delimited attributes?? */
|
|
"&": "&"
|
|
};
|
|
var escape_one = function(c) {
|
|
return escape_map[c];
|
|
};
|
|
|
|
return function (x) {
|
|
return x.replace(/[&<>"'`]/g, escape_one);
|
|
};
|
|
})();
|
|
|
|
// be able to recognize default "this", which is different in different environments
|
|
Handlebars._defaultThis = (function() { return this; })();
|
|
|
|
Handlebars.evaluate = function (ast, data, options) {
|
|
options = options || {};
|
|
var helpers = _.extend({}, Handlebars._default_helpers);
|
|
_.extend(helpers, options.helpers || {});
|
|
var partials = options.partials || {};
|
|
|
|
// re 'stack' arguments: top of stack is the current data to use for
|
|
// the template. higher levels are the data referenced by
|
|
// identifiers with one or more '..' segments. we have to keep the
|
|
// stack pure-functional style, with a tree rather than an array,
|
|
// because we want to continue to allow block helpers provided by
|
|
// the user to capture their subtemplate rendering functions and
|
|
// call them later, after we've finished running (for eg findLive.)
|
|
// maybe revisit later.
|
|
|
|
var eval_value = function (stack, id) {
|
|
if (typeof(id) !== "object")
|
|
return id;
|
|
|
|
// follow '..' in {{../../foo.bar}}
|
|
for (var i = 0; i < id[0]; i++) {
|
|
if (!stack.parent)
|
|
throw new Error("Too many '..' segments");
|
|
else
|
|
stack = stack.parent;
|
|
}
|
|
|
|
if (id.length === 1)
|
|
// no name: {{this}}, {{..}}, {{../..}}
|
|
return stack.data;
|
|
|
|
var scopedToContext = false;
|
|
if (id[1] === '') {
|
|
// an empty path segment is our AST's way of encoding
|
|
// the presence of 'this.' at the beginning of the path.
|
|
id.splice(1, 1); // remove the ''
|
|
scopedToContext = true;
|
|
}
|
|
|
|
// when calling functions (helpers/methods/getters), dataThis
|
|
// tracks what to use for `this`. For helpers, it's the
|
|
// current data context. For getters and methods on the data
|
|
// context object, and on the return value of a helper, it's
|
|
// the object where we got the getter or method.
|
|
var dataThis = stack.data;
|
|
|
|
var data;
|
|
if (id[0] === 0 && helpers.hasOwnProperty(id[1]) && ! scopedToContext) {
|
|
// first path segment is a helper
|
|
data = helpers[id[1]];
|
|
} else {
|
|
if ((! data instanceof Object) &&
|
|
(typeof (function() {})[id[1]] !== 'undefined') &&
|
|
! scopedToContext) {
|
|
// Give a helpful error message if the user tried to name
|
|
// a helper 'name', 'length', or some other built-in property
|
|
// of function objects. Unfortunately, this case is very
|
|
// hard to detect, as Template.foo.name = ... will fail silently,
|
|
// and {{name}} will be silently empty if the property doesn't
|
|
// exist (per Handlebars rules).
|
|
// However, if there is no data context at all, we jump in.
|
|
throw new Error("Can't call a helper '"+id[1]+"' because "+
|
|
"it is a built-in function property in JavaScript");
|
|
}
|
|
// first path segment is property of data context
|
|
data = (stack.data && stack.data[id[1]]);
|
|
}
|
|
|
|
// handle dots, as in {{foo.bar}}
|
|
for (var i = 2; i < id.length; i++) {
|
|
// Call functions when taking the dot, to support
|
|
// for example currentUser.name.
|
|
//
|
|
// In the case of {{foo.bar}}, we end up returning one of:
|
|
// - helpers.foo.bar
|
|
// - helpers.foo().bar
|
|
// - stack.data.foo.bar
|
|
// - stack.data.foo().bar.
|
|
//
|
|
// The caller does the final application with any
|
|
// arguments, as in {{foo.bar arg1 arg2}}, and passes
|
|
// the current data context in `this`. Therefore,
|
|
// we use the current data context (`helperThis`)
|
|
// for all function calls.
|
|
if (typeof data === 'function') {
|
|
data = data.call(dataThis);
|
|
dataThis = data;
|
|
} else if (data === undefined || data === null) {
|
|
// Handlebars fails silently and returns "" if
|
|
// we start to access properties that don't exist.
|
|
data = '';
|
|
}
|
|
|
|
data = data[id[i]];
|
|
}
|
|
|
|
// ensure `this` is bound appropriately when the caller
|
|
// invokes `data` with any arguments. For example,
|
|
// in {{foo.bar baz}}, the caller must supply `baz`,
|
|
// but we alone have `foo` (in `dataThis`).
|
|
if (typeof data === 'function')
|
|
return _.bind(data, dataThis);
|
|
|
|
return data;
|
|
};
|
|
|
|
// 'extra' will be clobbered, but not 'params'.
|
|
// if (isNested), evaluate params.slice(1) as a nested
|
|
// helper invocation if there is at least one positional
|
|
// argument. This is used for block helpers.
|
|
var invoke = function (stack, params, extra, isNested) {
|
|
extra = extra || {};
|
|
params = params.slice(0);
|
|
|
|
// remove hash (dictionary of keyword arguments) from
|
|
// the end of params, if present.
|
|
var last = params[params.length - 1];
|
|
var hash = {};
|
|
if (typeof(last) === "object" && !(last instanceof Array)) {
|
|
// evaluate hash values, which are currently invocations
|
|
// like [0, "foo"]
|
|
_.each(params.pop(), function(v,k) {
|
|
var result = eval_value(stack, v);
|
|
hash[k] = (typeof result === "function" ? result() : result);
|
|
});
|
|
}
|
|
|
|
var apply = function (values, extra) {
|
|
var args = values.slice(1);
|
|
for(var i=0; i<args.length; i++)
|
|
if (typeof args[i] === "function")
|
|
args[i] = args[i](); // `this` already bound by eval_value
|
|
if (extra)
|
|
args.push(extra);
|
|
return values[0].apply(stack.data, args);
|
|
};
|
|
|
|
var values = new Array(params.length);
|
|
for(var i=0; i<params.length; i++)
|
|
values[i] = eval_value(stack, params[i]);
|
|
|
|
if (typeof(values[0]) !== "function")
|
|
return values[0];
|
|
|
|
if (isNested && values.length > 1) {
|
|
// at least one positional argument; not no args
|
|
// or only hash args.
|
|
var oneArg = values[1];
|
|
if (typeof oneArg === "function")
|
|
// invoke the positional arguments
|
|
// (and hash arguments) as a nested helper invocation.
|
|
oneArg = apply(values.slice(1), {hash:hash});
|
|
values = [values[0], oneArg];
|
|
// keyword args don't go to the block helper, then.
|
|
extra.hash = {};
|
|
} else {
|
|
extra.hash = hash;
|
|
}
|
|
|
|
return apply(values, extra);
|
|
};
|
|
|
|
var template = function (stack, elts, basePCKey) {
|
|
var buf = [];
|
|
|
|
var toString = function (x) {
|
|
if (typeof x === "string") return x;
|
|
// May want to revisit the following one day
|
|
if (x === null) return "null";
|
|
if (x === undefined) return "";
|
|
return x.toString();
|
|
};
|
|
|
|
// wrap `fn` and `inverse` blocks in chunks having `data`, if the data
|
|
// is different from the enclosing data, so that the data is available
|
|
// at runtime for events.
|
|
var decorateBlockFn = function(fn, old_data) {
|
|
return function(data) {
|
|
// don't create spurious annotations when data is same
|
|
// as before (or when transitioning between e.g. `window` and
|
|
// `undefined`)
|
|
if ((data || Handlebars._defaultThis) ===
|
|
(old_data || Handlebars._defaultThis))
|
|
return fn(data);
|
|
else
|
|
return Spark.setDataContext(data, fn(data));
|
|
};
|
|
};
|
|
|
|
// Handle the return value of a {{helper}}.
|
|
// Takes a:
|
|
// string - escapes it
|
|
// SafeString - returns the underlying string unescaped
|
|
// other value - coerces to a string and escapes it
|
|
var maybeEscape = function(x) {
|
|
if (x instanceof Handlebars.SafeString)
|
|
return x.toString();
|
|
return Handlebars._escape(toString(x));
|
|
};
|
|
|
|
// Construct a unique key for the current position
|
|
// in the AST. Since template(...) is invoked recursively,
|
|
// the "PC" (program counter) key is hierarchical, consisting
|
|
// of one or more numbers, for example '0' or '1.3.0.1'.
|
|
var getPCKey = function(index) {
|
|
return (basePCKey ? basePCKey+'.' : '') + index;
|
|
};
|
|
|
|
_.each(elts, function (elt, index) {
|
|
if (typeof(elt) === "string")
|
|
buf.push(elt);
|
|
else if (elt[0] === '{')
|
|
// {{double stache}}
|
|
buf.push(maybeEscape(invoke(stack, elt[1])));
|
|
else if (elt[0] === '!')
|
|
// {{{triple stache}}}
|
|
buf.push(toString(invoke(stack, elt[1] || '')));
|
|
else if (elt[0] === '#') {
|
|
// {{#block helper}}
|
|
var block = decorateBlockFn(
|
|
function (data) {
|
|
return template({parent: stack, data: data}, elt[2],
|
|
getPCKey(index));
|
|
}, stack.data);
|
|
block.fn = block;
|
|
block.inverse = decorateBlockFn(
|
|
function (data) {
|
|
return template({parent: stack, data: data}, elt[3] || [],
|
|
getPCKey(index));
|
|
}, stack.data);
|
|
// construct branch label
|
|
var branch = elt[1]+"@"+getPCKey(index);
|
|
var html = Spark.labelBranch(branch, function () {
|
|
return toString(invoke(stack, elt[1], block, true));
|
|
});
|
|
buf.push(html);
|
|
} else if (elt[0] === '>') {
|
|
// {{> partial}}
|
|
var partialName = elt[1];
|
|
if (!(partialName in partials))
|
|
throw new Error("No such partial '" + partialName + "'");
|
|
// Construct a unique branch identifier based on what partial
|
|
// we're in, what partial we're calling, and our index
|
|
// into the template AST (essentially the program counter).
|
|
// If "foo" calls "bar" at index 3, it looks like: bar@foo#3.
|
|
var branch = partialName+"@"+getPCKey(index);
|
|
// call the partial
|
|
var html = Spark.labelBranch(branch, function () {
|
|
return toString(partials[partialName](stack.data));
|
|
});
|
|
buf.push(html);
|
|
} else
|
|
throw new Error("bad element in template");
|
|
});
|
|
|
|
return buf.join('');
|
|
};
|
|
|
|
// Set the prefix for PC keys, which identify call sites in the AST
|
|
// for the purpose of chunk matching.
|
|
// `options.name` will be null in the body, but otherwise have a value,
|
|
// assuming `options` was assembled in templating/deftemplate.js.
|
|
var rootPCKey = (options.name||"")+"#";
|
|
|
|
return template({data: data, parent: null}, ast, rootPCKey);
|
|
};
|
|
|
|
Handlebars.SafeString = function(string) {
|
|
this.string = string;
|
|
};
|
|
Handlebars.SafeString.prototype.toString = function() {
|
|
return this.string.toString();
|
|
};
|