8e868ab225
Certain optimizations for simple templates could result in objects returned by helpers returned rather than their string representation, resulting in some odd edge cases. This ensures that strings are always returned from the API for consistency. Fixes #1054.
228 lines
7.0 KiB
JavaScript
228 lines
7.0 KiB
JavaScript
import * as Utils from './utils';
|
|
import Exception from './exception';
|
|
import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from './base';
|
|
|
|
export function checkRevision(compilerInfo) {
|
|
const compilerRevision = compilerInfo && compilerInfo[0] || 1,
|
|
currentRevision = COMPILER_REVISION;
|
|
|
|
if (compilerRevision !== currentRevision) {
|
|
if (compilerRevision < currentRevision) {
|
|
const runtimeVersions = REVISION_CHANGES[currentRevision],
|
|
compilerVersions = REVISION_CHANGES[compilerRevision];
|
|
throw new Exception('Template was precompiled with an older version of Handlebars than the current runtime. ' +
|
|
'Please update your precompiler to a newer version (' + runtimeVersions + ') or downgrade your runtime to an older version (' + compilerVersions + ').');
|
|
} else {
|
|
// Use the embedded version info since the runtime doesn't know about this revision yet
|
|
throw new Exception('Template was precompiled with a newer version of Handlebars than the current runtime. ' +
|
|
'Please update your runtime to a newer version (' + compilerInfo[1] + ').');
|
|
}
|
|
}
|
|
}
|
|
|
|
export function template(templateSpec, env) {
|
|
/* istanbul ignore next */
|
|
if (!env) {
|
|
throw new Exception('No environment passed to template');
|
|
}
|
|
if (!templateSpec || !templateSpec.main) {
|
|
throw new Exception('Unknown template object: ' + typeof templateSpec);
|
|
}
|
|
|
|
// Note: Using env.VM references rather than local var references throughout this section to allow
|
|
// for external users to override these as psuedo-supported APIs.
|
|
env.VM.checkRevision(templateSpec.compiler);
|
|
|
|
function invokePartialWrapper(partial, context, options) {
|
|
if (options.hash) {
|
|
context = Utils.extend({}, context, options.hash);
|
|
if (options.ids) {
|
|
options.ids[0] = true;
|
|
}
|
|
}
|
|
|
|
partial = env.VM.resolvePartial.call(this, partial, context, options);
|
|
let result = env.VM.invokePartial.call(this, partial, context, options);
|
|
|
|
if (result == null && env.compile) {
|
|
options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env);
|
|
result = options.partials[options.name](context, options);
|
|
}
|
|
if (result != null) {
|
|
if (options.indent) {
|
|
let lines = result.split('\n');
|
|
for (let i = 0, l = lines.length; i < l; i++) {
|
|
if (!lines[i] && i + 1 === l) {
|
|
break;
|
|
}
|
|
|
|
lines[i] = options.indent + lines[i];
|
|
}
|
|
result = lines.join('\n');
|
|
}
|
|
return result;
|
|
} else {
|
|
throw new Exception('The partial ' + options.name + ' could not be compiled when running in runtime-only mode');
|
|
}
|
|
}
|
|
|
|
// Just add water
|
|
let container = {
|
|
strict: function(obj, name) {
|
|
if (!(name in obj)) {
|
|
throw new Exception('"' + name + '" not defined in ' + obj);
|
|
}
|
|
return obj[name];
|
|
},
|
|
lookup: function(depths, name) {
|
|
const len = depths.length;
|
|
for (let i = 0; i < len; i++) {
|
|
if (depths[i] && depths[i][name] != null) {
|
|
return depths[i][name];
|
|
}
|
|
}
|
|
},
|
|
lambda: function(current, context) {
|
|
return typeof current === 'function' ? current.call(context) : current;
|
|
},
|
|
|
|
escapeExpression: Utils.escapeExpression,
|
|
invokePartial: invokePartialWrapper,
|
|
|
|
fn: function(i) {
|
|
return templateSpec[i];
|
|
},
|
|
|
|
programs: [],
|
|
program: function(i, data, declaredBlockParams, blockParams, depths) {
|
|
let programWrapper = this.programs[i],
|
|
fn = this.fn(i);
|
|
if (data || depths || blockParams || declaredBlockParams) {
|
|
programWrapper = wrapProgram(this, i, fn, data, declaredBlockParams, blockParams, depths);
|
|
} else if (!programWrapper) {
|
|
programWrapper = this.programs[i] = wrapProgram(this, i, fn);
|
|
}
|
|
return programWrapper;
|
|
},
|
|
|
|
data: function(value, depth) {
|
|
while (value && depth--) {
|
|
value = value._parent;
|
|
}
|
|
return value;
|
|
},
|
|
merge: function(param, common) {
|
|
let obj = param || common;
|
|
|
|
if (param && common && (param !== common)) {
|
|
obj = Utils.extend({}, common, param);
|
|
}
|
|
|
|
return obj;
|
|
},
|
|
|
|
noop: env.VM.noop,
|
|
compilerInfo: templateSpec.compiler
|
|
};
|
|
|
|
function ret(context, options = {}) {
|
|
let data = options.data;
|
|
|
|
ret._setup(options);
|
|
if (!options.partial && templateSpec.useData) {
|
|
data = initData(context, data);
|
|
}
|
|
let depths,
|
|
blockParams = templateSpec.useBlockParams ? [] : undefined;
|
|
if (templateSpec.useDepths) {
|
|
if (options.depths) {
|
|
depths = context !== options.depths[0] ? [context].concat(depths) : options.depths;
|
|
} else {
|
|
depths = [context];
|
|
}
|
|
}
|
|
|
|
return '' + templateSpec.main.call(container, context, container.helpers, container.partials, data, blockParams, depths);
|
|
}
|
|
ret.isTop = true;
|
|
|
|
ret._setup = function(options) {
|
|
if (!options.partial) {
|
|
container.helpers = container.merge(options.helpers, env.helpers);
|
|
|
|
if (templateSpec.usePartial) {
|
|
container.partials = container.merge(options.partials, env.partials);
|
|
}
|
|
} else {
|
|
container.helpers = options.helpers;
|
|
container.partials = options.partials;
|
|
}
|
|
};
|
|
|
|
ret._child = function(i, data, blockParams, depths) {
|
|
if (templateSpec.useBlockParams && !blockParams) {
|
|
throw new Exception('must pass block params');
|
|
}
|
|
if (templateSpec.useDepths && !depths) {
|
|
throw new Exception('must pass parent depths');
|
|
}
|
|
|
|
return wrapProgram(container, i, templateSpec[i], data, 0, blockParams, depths);
|
|
};
|
|
return ret;
|
|
}
|
|
|
|
export function wrapProgram(container, i, fn, data, declaredBlockParams, blockParams, depths) {
|
|
function prog(context, options = {}) {
|
|
let currentDepths = depths;
|
|
if (depths && context !== depths[0]) {
|
|
currentDepths = [context].concat(depths);
|
|
}
|
|
|
|
return fn.call(container,
|
|
context,
|
|
container.helpers, container.partials,
|
|
options.data || data,
|
|
blockParams && [options.blockParams].concat(blockParams),
|
|
currentDepths);
|
|
}
|
|
prog.program = i;
|
|
prog.depth = depths ? depths.length : 0;
|
|
prog.blockParams = declaredBlockParams || 0;
|
|
return prog;
|
|
}
|
|
|
|
export function resolvePartial(partial, context, options) {
|
|
if (!partial) {
|
|
partial = options.partials[options.name];
|
|
} else if (!partial.call && !options.name) {
|
|
// This is a dynamic partial that returned a string
|
|
options.name = partial;
|
|
partial = options.partials[partial];
|
|
}
|
|
return partial;
|
|
}
|
|
|
|
export function invokePartial(partial, context, options) {
|
|
options.partial = true;
|
|
if (options.ids) {
|
|
options.data.contextPath = options.ids[0] || options.data.contextPath;
|
|
}
|
|
|
|
if (partial === undefined) {
|
|
throw new Exception('The partial ' + options.name + ' could not be found');
|
|
} else if (partial instanceof Function) {
|
|
return partial(context, options);
|
|
}
|
|
}
|
|
|
|
export function noop() { return ''; }
|
|
|
|
function initData(context, data) {
|
|
if (!data || !('root' in data)) {
|
|
data = data ? createFrame(data) : {};
|
|
data.root = context;
|
|
}
|
|
return data;
|
|
}
|