279e038ba7
Creating a new depth value seems to confuse users as they don’t expect things like `if` to require multiple `..` to break out of. With the change, we avoid pushing a context to the depth list if it’s already on the top of the stack, effectively removing cases where `.` and `..` are the same object and multiple `..` references are required. This is a breaking change and all templates that utilize `..` will have to check their usage and confirm that this does not break desired behavior. Helper authors now need to take care to return the same context value whenever it is conceptually the same and to avoid behaviors that may execute children under the current context in some situations and under different contexts under other situations. Fixes #1028
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;
|
|
}
|