aboutsummaryrefslogtreecommitdiff
path: root/dev/lib/handlebars/src/visitor.ts
diff options
context:
space:
mode:
authorDarius Jahandarie <djahandarie@gmail.com>2023-11-09 13:30:31 +0000
committerGitHub <noreply@github.com>2023-11-09 13:30:31 +0000
commit5c5a167b4792af379cdacf633513cebf20728cd2 (patch)
tree5b6be3620a557d0b9177047003f6d742d9d2a32d /dev/lib/handlebars/src/visitor.ts
parentb64f51c3b13a46af4dd7f1e43048ac19c781ca7b (diff)
parent0f4d36938fd0d844f548aa5a7f7e7842df8dfb41 (diff)
Merge pull request #307 from themoeway/modernize
Modernize codebase
Diffstat (limited to 'dev/lib/handlebars/src/visitor.ts')
-rw-r--r--dev/lib/handlebars/src/visitor.ts778
1 files changed, 778 insertions, 0 deletions
diff --git a/dev/lib/handlebars/src/visitor.ts b/dev/lib/handlebars/src/visitor.ts
new file mode 100644
index 00000000..1842c8e5
--- /dev/null
+++ b/dev/lib/handlebars/src/visitor.ts
@@ -0,0 +1,778 @@
+/*
+ * Elasticsearch B.V licenses this file to you under the MIT License.
+ * See `packages/kbn-handlebars/LICENSE` for more information.
+ */
+
+import Handlebars from 'handlebars';
+import {
+ createProtoAccessControl,
+ resultIsAllowed,
+ // @ts-expect-error: Could not find a declaration file for module
+} from 'handlebars/dist/cjs/handlebars/internal/proto-access';
+// @ts-expect-error: Could not find a declaration file for module
+import AST from 'handlebars/dist/cjs/handlebars/compiler/ast';
+// @ts-expect-error: Could not find a declaration file for module
+import { indexOf, createFrame } from 'handlebars/dist/cjs/handlebars/utils';
+// @ts-expect-error: Could not find a declaration file for module
+import { moveHelperToHooks } from 'handlebars/dist/cjs/handlebars/helpers';
+
+import type {
+ AmbiguousHelperOptions,
+ CompileOptions,
+ Container,
+ DecoratorDelegate,
+ DecoratorsHash,
+ HelperOptions,
+ NodeType,
+ NonBlockHelperOptions,
+ ProcessableBlockStatementNode,
+ ProcessableNode,
+ ProcessableNodeWithPathParts,
+ ProcessableNodeWithPathPartsOrLiteral,
+ ProcessableStatementNode,
+ ResolvePartialOptions,
+ RuntimeOptions,
+ Template,
+ TemplateDelegate,
+ VisitorHelper,
+} from './types';
+import { kAmbiguous, kHelper, kSimple } from './symbols';
+import {
+ initData,
+ isBlock,
+ isDecorator,
+ noop,
+ toDecoratorOptions,
+ transformLiteralToPath,
+} from './utils';
+
+export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
+ private env: typeof Handlebars;
+ private contexts: any[] = [];
+ private output: any[] = [];
+ private template?: string;
+ private compileOptions: CompileOptions;
+ private runtimeOptions?: RuntimeOptions;
+ private blockParamNames: any[][] = [];
+ private blockParamValues: any[][] = [];
+ private ast?: hbs.AST.Program;
+ private container: Container;
+ private defaultHelperOptions: Pick<NonBlockHelperOptions, 'lookupProperty'>;
+ private processedRootDecorators = false; // Root decorators should not have access to input arguments. This flag helps us detect them.
+ private processedDecoratorsForProgram = new Set(); // It's important that a given program node only has its decorators run once, we use this Map to keep track of them
+
+ constructor(
+ env: typeof Handlebars,
+ input: string | hbs.AST.Program,
+ options: CompileOptions = {}
+ ) {
+ super();
+
+ this.env = env;
+
+ if (typeof input !== 'string' && input.type === 'Program') {
+ this.ast = input;
+ } else {
+ this.template = input as string;
+ }
+
+ this.compileOptions = { data: true, ...options };
+ this.compileOptions.knownHelpers = Object.assign(
+ Object.create(null),
+ {
+ helperMissing: true,
+ blockHelperMissing: true,
+ each: true,
+ if: true,
+ unless: true,
+ with: true,
+ log: true,
+ lookup: true,
+ },
+ this.compileOptions.knownHelpers
+ );
+
+ const protoAccessControl = createProtoAccessControl({});
+
+ const container: Container = (this.container = {
+ helpers: {},
+ partials: {},
+ decorators: {},
+ strict(obj, name, loc) {
+ if (!obj || !(name in obj)) {
+ throw new Handlebars.Exception('"' + name + '" not defined in ' + obj, {
+ loc,
+ } as hbs.AST.Node);
+ }
+ return container.lookupProperty(obj, name);
+ },
+ // this function is lifted from the handlebars source and slightly modified (lib/handlebars/runtime.js)
+ lookupProperty(parent, propertyName) {
+ const result = parent[propertyName];
+ if (result == null) {
+ return result;
+ }
+ if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
+ return result;
+ }
+
+ if (resultIsAllowed(result, protoAccessControl, propertyName)) {
+ return result;
+ }
+ return undefined;
+ },
+ // this function is lifted from the handlebars source and slightly modified (lib/handlebars/runtime.js)
+ lambda(current, context) {
+ return typeof current === 'function' ? current.call(context) : current;
+ },
+ data(value: any, depth: number) {
+ while (value && depth--) {
+ value = value._parent;
+ }
+ return value;
+ },
+ hooks: {},
+ });
+
+ this.defaultHelperOptions = {
+ lookupProperty: container.lookupProperty,
+ };
+ }
+
+ render(context: any, options: RuntimeOptions = {}): string {
+ this.contexts = [context];
+ this.output = [];
+ this.runtimeOptions = { ...options };
+ this.container.helpers = { ...this.env.helpers, ...options.helpers };
+ this.container.partials = { ...this.env.partials, ...options.partials };
+ this.container.decorators = {
+ ...(this.env.decorators as DecoratorsHash),
+ ...options.decorators,
+ };
+ this.container.hooks = {};
+ this.processedRootDecorators = false;
+ this.processedDecoratorsForProgram.clear();
+
+ if (this.compileOptions.data) {
+ this.runtimeOptions.data = initData(context, this.runtimeOptions.data);
+ }
+
+ const keepHelperInHelpers = false;
+ moveHelperToHooks(this.container, 'helperMissing', keepHelperInHelpers);
+ moveHelperToHooks(this.container, 'blockHelperMissing', keepHelperInHelpers);
+
+ if (!this.ast) {
+ this.ast = Handlebars.parse(this.template!);
+ }
+
+ // The `defaultMain` function contains the default behavior:
+ //
+ // Generate a "program" function based on the root `Program` in the AST and
+ // call it. This will start the processing of all the child nodes in the
+ // AST.
+ const defaultMain: TemplateDelegate = (_context) => {
+ const prog = this.generateProgramFunction(this.ast!);
+ return prog(_context, this.runtimeOptions);
+ };
+
+ // Run any decorators that might exist on the root:
+ //
+ // The `defaultMain` function is passed in, and if there are no root
+ // decorators, or if the decorators chooses to do so, the same function is
+ // returned from `processDecorators` and the default behavior is retained.
+ //
+ // Alternatively any of the root decorators might call the `defaultMain`
+ // function themselves, process its return value, and return a completely
+ // different `main` function.
+ const main = this.processDecorators(this.ast, defaultMain);
+ this.processedRootDecorators = true;
+
+ // Call the `main` function and add the result to the final output.
+ const result = main(this.context, options);
+
+ if (main === defaultMain) {
+ this.output.push(result);
+ return this.output.join('');
+ } else {
+ // We normally expect the return value of `main` to be a string. However,
+ // if a decorator is used to override the `defaultMain` function, the
+ // return value can be any type. To match the upstream handlebars project
+ // behavior, we want the result of rendering the template to be the
+ // literal value returned by the decorator.
+ //
+ // Since the output array in this case always will be empty, we just
+ // return that single value instead of attempting to join all the array
+ // elements as strings.
+ return result;
+ }
+ }
+
+ // ********************************************** //
+ // *** Visitor AST Traversal Functions *** //
+ // ********************************************** //
+
+ Program(program: hbs.AST.Program) {
+ this.blockParamNames.unshift(program.blockParams);
+ super.Program(program);
+ this.blockParamNames.shift();
+ }
+
+ MustacheStatement(mustache: hbs.AST.MustacheStatement) {
+ this.processStatementOrExpression(mustache);
+ }
+
+ BlockStatement(block: hbs.AST.BlockStatement) {
+ this.processStatementOrExpression(block);
+ }
+
+ PartialStatement(partial: hbs.AST.PartialStatement) {
+ this.invokePartial(partial);
+ }
+
+ PartialBlockStatement(partial: hbs.AST.PartialBlockStatement) {
+ this.invokePartial(partial);
+ }
+
+ // This space is intentionally left blank: We want to override the Visitor
+ // class implementation of this method, but since we handle decorators
+ // separately before traversing the nodes, we just want to make this a no-op.
+ DecoratorBlock(decorator: hbs.AST.DecoratorBlock) {}
+
+ // This space is intentionally left blank: We want to override the Visitor
+ // class implementation of this method, but since we handle decorators
+ // separately before traversing the nodes, we just want to make this a no-op.
+ Decorator(decorator: hbs.AST.Decorator) {}
+
+ SubExpression(sexpr: hbs.AST.SubExpression) {
+ this.processStatementOrExpression(sexpr);
+ }
+
+ PathExpression(path: hbs.AST.PathExpression) {
+ const blockParamId =
+ !path.depth && !AST.helpers.scopedId(path) && this.blockParamIndex(path.parts[0]);
+
+ let result;
+ if (blockParamId) {
+ result = this.lookupBlockParam(blockParamId, path);
+ } else if (path.data) {
+ result = this.lookupData(this.runtimeOptions!.data, path);
+ } else {
+ result = this.resolvePath(this.contexts[path.depth], path);
+ }
+
+ this.output.push(result);
+ }
+
+ ContentStatement(content: hbs.AST.ContentStatement) {
+ this.output.push(content.value);
+ }
+
+ StringLiteral(string: hbs.AST.StringLiteral) {
+ this.output.push(string.value);
+ }
+
+ NumberLiteral(number: hbs.AST.NumberLiteral) {
+ this.output.push(number.value);
+ }
+
+ BooleanLiteral(bool: hbs.AST.BooleanLiteral) {
+ this.output.push(bool.value);
+ }
+
+ UndefinedLiteral() {
+ this.output.push(undefined);
+ }
+
+ NullLiteral() {
+ this.output.push(null);
+ }
+
+ // ********************************************** //
+ // *** Visitor AST Helper Functions *** //
+ // ********************************************** //
+
+ /**
+ * Special code for decorators, since they have to be executed ahead of time (before the wrapping program).
+ * So we have to look into the program AST body and see if it contains any decorators that we have to process
+ * before we can finish processing of the wrapping program.
+ */
+ private processDecorators(program: hbs.AST.Program, prog: TemplateDelegate) {
+ if (!this.processedDecoratorsForProgram.has(program)) {
+ this.processedDecoratorsForProgram.add(program);
+ const props = {};
+ for (const node of program.body) {
+ if (isDecorator(node)) {
+ prog = this.processDecorator(node, prog, props);
+ }
+ }
+ }
+
+ return prog;
+ }
+
+ private processDecorator(
+ decorator: hbs.AST.DecoratorBlock | hbs.AST.Decorator,
+ prog: TemplateDelegate,
+ props: Record<string, any>
+ ) {
+ const options = this.setupDecoratorOptions(decorator);
+
+ const result = this.container.lookupProperty<DecoratorDelegate>(
+ this.container.decorators,
+ options.name
+ )(prog, props, this.container, options);
+
+ return Object.assign(result || prog, props);
+ }
+
+ private processStatementOrExpression(node: ProcessableNodeWithPathPartsOrLiteral) {
+ // Calling `transformLiteralToPath` has side-effects!
+ // It converts a node from type `ProcessableNodeWithPathPartsOrLiteral` to `ProcessableNodeWithPathParts`
+ transformLiteralToPath(node);
+
+ switch (this.classifyNode(node as ProcessableNodeWithPathParts)) {
+ case kSimple:
+ this.processSimpleNode(node as ProcessableNodeWithPathParts);
+ break;
+ case kHelper:
+ this.processHelperNode(node as ProcessableNodeWithPathParts);
+ break;
+ case kAmbiguous:
+ this.processAmbiguousNode(node as ProcessableNodeWithPathParts);
+ break;
+ }
+ }
+
+ // Liftet from lib/handlebars/compiler/compiler.js (original name: classifySexpr)
+ private classifyNode(node: { path: hbs.AST.PathExpression }): NodeType {
+ const isSimple = AST.helpers.simpleId(node.path);
+ const isBlockParam = isSimple && !!this.blockParamIndex(node.path.parts[0]);
+
+ // a mustache is an eligible helper if:
+ // * its id is simple (a single part, not `this` or `..`)
+ let isHelper = !isBlockParam && AST.helpers.helperExpression(node);
+
+ // if a mustache is an eligible helper but not a definite
+ // helper, it is ambiguous, and will be resolved in a later
+ // pass or at runtime.
+ let isEligible = !isBlockParam && (isHelper || isSimple);
+
+ // if ambiguous, we can possibly resolve the ambiguity now
+ // An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
+ if (isEligible && !isHelper) {
+ const name = node.path.parts[0];
+ const options = this.compileOptions;
+ if (options.knownHelpers && options.knownHelpers[name]) {
+ isHelper = true;
+ } else if (options.knownHelpersOnly) {
+ isEligible = false;
+ }
+ }
+
+ if (isHelper) {
+ return kHelper;
+ } else if (isEligible) {
+ return kAmbiguous;
+ } else {
+ return kSimple;
+ }
+ }
+
+ // Liftet from lib/handlebars/compiler/compiler.js
+ private blockParamIndex(name: string): [number, any] | undefined {
+ for (let depth = 0, len = this.blockParamNames.length; depth < len; depth++) {
+ const blockParams = this.blockParamNames[depth];
+ const param = blockParams && indexOf(blockParams, name);
+ if (blockParams && param >= 0) {
+ return [depth, param];
+ }
+ }
+ }
+
+ // Looks up the value of `parts` on the given block param and pushes
+ // it onto the stack.
+ private lookupBlockParam(blockParamId: [number, any], path: hbs.AST.PathExpression) {
+ const value = this.blockParamValues[blockParamId[0]][blockParamId[1]];
+ return this.resolvePath(value, path, 1);
+ }
+
+ // Push the data lookup operator
+ private lookupData(data: any, path: hbs.AST.PathExpression) {
+ if (path.depth) {
+ data = this.container.data(data, path.depth);
+ }
+
+ return this.resolvePath(data, path);
+ }
+
+ private processSimpleNode(node: ProcessableNodeWithPathParts) {
+ const path = node.path;
+ // @ts-expect-error strict is not a valid property on PathExpression, but we used in the same way it's also used in the original handlebars
+ path.strict = true;
+ const result = this.resolveNodes(path)[0];
+ const lambdaResult = this.container.lambda(result, this.context);
+
+ if (isBlock(node)) {
+ this.blockValue(node, lambdaResult);
+ } else {
+ this.output.push(lambdaResult);
+ }
+ }
+
+ // The purpose of this opcode is to take a block of the form
+ // `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and
+ // replace it on the stack with the result of properly
+ // invoking blockHelperMissing.
+ private blockValue(node: hbs.AST.BlockStatement, value: any) {
+ const name = node.path.original;
+ const options = this.setupParams(node, name);
+
+ const result = this.container.hooks.blockHelperMissing!.call(this.context, value, options);
+
+ this.output.push(result);
+ }
+
+ private processHelperNode(node: ProcessableNodeWithPathParts) {
+ const path = node.path;
+ const name = path.parts[0];
+
+ if (this.compileOptions.knownHelpers && this.compileOptions.knownHelpers[name]) {
+ this.invokeKnownHelper(node);
+ } else if (this.compileOptions.knownHelpersOnly) {
+ throw new Handlebars.Exception(
+ 'You specified knownHelpersOnly, but used the unknown helper ' + name,
+ node
+ );
+ } else {
+ this.invokeHelper(node);
+ }
+ }
+
+ // This operation is used when the helper is known to exist,
+ // so a `helperMissing` fallback is not required.
+ private invokeKnownHelper(node: ProcessableNodeWithPathParts) {
+ const name = node.path.parts[0];
+ const helper = this.setupHelper(node, name);
+ // TypeScript: `helper.fn` might be `undefined` at this point, but to match the upstream behavior we call it without any guards
+ const result = helper.fn!.call(helper.context, ...helper.params, helper.options);
+ this.output.push(result);
+ }
+
+ // Pops off the helper's parameters, invokes the helper,
+ // and pushes the helper's return value onto the stack.
+ //
+ // If the helper is not found, `helperMissing` is called.
+ private invokeHelper(node: ProcessableNodeWithPathParts) {
+ const path = node.path;
+ const name = path.original;
+ const isSimple = AST.helpers.simpleId(path);
+ const helper = this.setupHelper(node, name);
+
+ const loc = isSimple && helper.fn ? node.loc : path.loc;
+ helper.fn = (isSimple && helper.fn) || this.resolveNodes(path)[0];
+
+ if (!helper.fn) {
+ if (this.compileOptions.strict) {
+ helper.fn = this.container.strict(helper.context, name, loc);
+ } else {
+ helper.fn = this.container.hooks.helperMissing;
+ }
+ }
+
+ // TypeScript: `helper.fn` might be `undefined` at this point, but to match the upstream behavior we call it without any guards
+ const result = helper.fn!.call(helper.context, ...helper.params, helper.options);
+
+ this.output.push(result);
+ }
+
+ private invokePartial(partial: hbs.AST.PartialStatement | hbs.AST.PartialBlockStatement) {
+ const { params } = partial;
+ if (params.length > 1) {
+ throw new Handlebars.Exception(
+ `Unsupported number of partial arguments: ${params.length}`,
+ partial
+ );
+ }
+
+ const isDynamic = partial.name.type === 'SubExpression';
+ const name = isDynamic
+ ? this.resolveNodes(partial.name).join('')
+ : (partial.name as hbs.AST.PathExpression).original;
+
+ const options: AmbiguousHelperOptions & ResolvePartialOptions = this.setupParams(partial, name);
+ options.helpers = this.container.helpers;
+ options.partials = this.container.partials;
+ options.decorators = this.container.decorators;
+
+ let partialBlock;
+ if ('fn' in options && options.fn !== noop) {
+ const { fn } = options;
+ const currentPartialBlock = options.data?.['partial-block'];
+ options.data = createFrame(options.data);
+
+ // Wrapper function to get access to currentPartialBlock from the closure
+ partialBlock = options.data['partial-block'] = function partialBlockWrapper(
+ context: any,
+ wrapperOptions: { data?: HelperOptions['data'] } = {}
+ ) {
+ // Restore the partial-block from the closure for the execution of the block
+ // i.e. the part inside the block of the partial call.
+ wrapperOptions.data = createFrame(wrapperOptions.data);
+ wrapperOptions.data['partial-block'] = currentPartialBlock;
+ return fn(context, wrapperOptions);
+ };
+
+ if (fn.partials) {
+ options.partials = { ...options.partials, ...fn.partials };
+ }
+ }
+
+ let context = {};
+ if (params.length === 0 && !this.compileOptions.explicitPartialContext) {
+ context = this.context;
+ } else if (params.length === 1) {
+ context = this.resolveNodes(params[0])[0];
+ }
+
+ if (Object.keys(options.hash).length > 0) {
+ // TODO: context can be an array, but maybe never when we have a hash???
+ context = Object.assign({}, context, options.hash);
+ }
+
+ const partialTemplate: Template | undefined =
+ this.container.partials[name] ??
+ partialBlock ??
+ // TypeScript note: We extend ResolvePartialOptions in our types.ts file
+ // to fix an error in the upstream type. When calling back into the
+ // upstream code, we just cast back to the non-extended type
+ Handlebars.VM.resolvePartial(
+ undefined,
+ undefined,
+ options as Handlebars.ResolvePartialOptions
+ );
+
+ if (partialTemplate === undefined) {
+ throw new Handlebars.Exception(`The partial ${name} could not be found`);
+ }
+
+ let render;
+ if (typeof partialTemplate === 'string') {
+ render = this.env.compileAST(partialTemplate, this.compileOptions);
+ if (name in this.container.partials) {
+ this.container.partials[name] = render;
+ }
+ } else {
+ render = partialTemplate;
+ }
+
+ let result = render(context, options);
+
+ if ('indent' in partial) {
+ result =
+ partial.indent +
+ (this.compileOptions.preventIndent
+ ? result
+ : result.replace(/\n(?!$)/g, `\n${partial.indent}`)); // indent each line, ignoring any trailing linebreak
+ }
+
+ this.output.push(result);
+ }
+
+ private processAmbiguousNode(node: ProcessableNodeWithPathParts) {
+ const name = node.path.parts[0];
+ const helper = this.setupHelper(node, name);
+ let { fn: helperFn } = helper;
+
+ const loc = helperFn ? node.loc : node.path.loc;
+ helperFn = helperFn ?? this.resolveNodes(node.path)[0];
+
+ if (helperFn === undefined) {
+ if (this.compileOptions.strict) {
+ helperFn = this.container.strict(helper.context, name, loc);
+ } else {
+ helperFn =
+ helper.context != null
+ ? this.container.lookupProperty(helper.context, name)
+ : helper.context;
+ if (helperFn == null) helperFn = this.container.hooks.helperMissing;
+ }
+ }
+
+ const helperResult =
+ typeof helperFn === 'function'
+ ? helperFn.call(helper.context, ...helper.params, helper.options)
+ : helperFn;
+
+ if (isBlock(node)) {
+ const result = helper.fn
+ ? helperResult
+ : this.container.hooks.blockHelperMissing!.call(this.context, helperResult, helper.options);
+ if (result != null) {
+ this.output.push(result);
+ }
+ } else {
+ if (
+ (node as hbs.AST.MustacheStatement).escaped === false ||
+ this.compileOptions.noEscape === true ||
+ typeof helperResult !== 'string'
+ ) {
+ this.output.push(helperResult);
+ } else {
+ this.output.push(Handlebars.escapeExpression(helperResult));
+ }
+ }
+ }
+
+ private setupHelper(node: ProcessableNode, helperName: string): VisitorHelper {
+ return {
+ fn: this.container.lookupProperty(this.container.helpers, helperName),
+ context: this.context,
+ params: this.resolveNodes(node.params),
+ options: this.setupParams(node, helperName),
+ };
+ }
+
+ private setupDecoratorOptions(decorator: hbs.AST.Decorator | hbs.AST.DecoratorBlock) {
+ // TypeScript: The types indicate that `decorator.path` technically can be an `hbs.AST.Literal`. However, the upstream codebase always treats it as an `hbs.AST.PathExpression`, so we do too.
+ const name = (decorator.path as hbs.AST.PathExpression).original;
+ const options = toDecoratorOptions(this.setupParams(decorator, name));
+
+ if (decorator.params.length > 0) {
+ if (!this.processedRootDecorators) {
+ // When processing the root decorators, temporarily remove the root context so it's not accessible to the decorator
+ const context = this.contexts.shift();
+ options.args = this.resolveNodes(decorator.params);
+ this.contexts.unshift(context);
+ } else {
+ options.args = this.resolveNodes(decorator.params);
+ }
+ } else {
+ options.args = [];
+ }
+
+ return options;
+ }
+
+ private setupParams(node: ProcessableBlockStatementNode, name: string): HelperOptions;
+ private setupParams(node: ProcessableStatementNode, name: string): NonBlockHelperOptions;
+ private setupParams(node: ProcessableNode, name: string): AmbiguousHelperOptions;
+ private setupParams(node: ProcessableNode, name: string) {
+ const options: AmbiguousHelperOptions = {
+ name,
+ hash: this.getHash(node),
+ data: this.runtimeOptions!.data,
+ loc: { start: node.loc.start, end: node.loc.end },
+ ...this.defaultHelperOptions,
+ };
+
+ if (isBlock(node)) {
+ (options as HelperOptions).fn = node.program
+ ? this.processDecorators(node.program, this.generateProgramFunction(node.program))
+ : noop;
+ (options as HelperOptions).inverse = node.inverse
+ ? this.processDecorators(node.inverse, this.generateProgramFunction(node.inverse))
+ : noop;
+ }
+
+ return options;
+ }
+
+ private generateProgramFunction(program: hbs.AST.Program) {
+ if (!program) return noop;
+
+ const prog: TemplateDelegate = (nextContext: any, runtimeOptions: RuntimeOptions = {}) => {
+ runtimeOptions = { ...runtimeOptions };
+
+ // inherit data in blockParams from parent program
+ runtimeOptions.data = runtimeOptions.data || this.runtimeOptions!.data;
+ if (runtimeOptions.blockParams) {
+ runtimeOptions.blockParams = runtimeOptions.blockParams.concat(
+ this.runtimeOptions!.blockParams
+ );
+ }
+
+ // inherit partials from parent program
+ runtimeOptions.partials = runtimeOptions.partials || this.runtimeOptions!.partials;
+
+ // stash parent program data
+ const tmpRuntimeOptions = this.runtimeOptions;
+ this.runtimeOptions = runtimeOptions;
+ const shiftContext = nextContext !== this.context;
+ if (shiftContext) this.contexts.unshift(nextContext);
+ this.blockParamValues.unshift(runtimeOptions.blockParams || []);
+
+ // execute child program
+ const result = this.resolveNodes(program).join('');
+
+ // unstash parent program data
+ this.blockParamValues.shift();
+ if (shiftContext) this.contexts.shift();
+ this.runtimeOptions = tmpRuntimeOptions;
+
+ // return result of child program
+ return result;
+ };
+
+ prog.blockParams = program.blockParams?.length ?? 0;
+ return prog;
+ }
+
+ private getHash(statement: { hash?: hbs.AST.Hash }) {
+ const result: { [key: string]: any } = {};
+ if (!statement.hash) return result;
+ for (const { key, value } of statement.hash.pairs) {
+ result[key] = this.resolveNodes(value)[0];
+ }
+ return result;
+ }
+
+ private resolvePath(obj: any, path: hbs.AST.PathExpression, index = 0) {
+ if (this.compileOptions.strict || this.compileOptions.assumeObjects) {
+ return this.strictLookup(obj, path);
+ }
+
+ for (; index < path.parts.length; index++) {
+ if (obj == null) return;
+ obj = this.container.lookupProperty(obj, path.parts[index]);
+ }
+
+ return obj;
+ }
+
+ private strictLookup(obj: any, path: hbs.AST.PathExpression) {
+ // @ts-expect-error strict is not a valid property on PathExpression, but we used in the same way it's also used in the original handlebars
+ const requireTerminal = this.compileOptions.strict && path.strict;
+ const len = path.parts.length - (requireTerminal ? 1 : 0);
+
+ for (let i = 0; i < len; i++) {
+ obj = this.container.lookupProperty(obj, path.parts[i]);
+ }
+
+ if (requireTerminal) {
+ return this.container.strict(obj, path.parts[len], path.loc);
+ } else {
+ return obj;
+ }
+ }
+
+ private resolveNodes(nodes: hbs.AST.Node | hbs.AST.Node[]): any[] {
+ const currentOutput = this.output;
+ this.output = [];
+
+ if (Array.isArray(nodes)) {
+ this.acceptArray(nodes);
+ } else {
+ this.accept(nodes);
+ }
+
+ const result = this.output;
+
+ this.output = currentOutput;
+
+ return result;
+ }
+
+ private get context() {
+ return this.contexts[0];
+ }
+}