mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[@kbn/handlebars] Split index.ts into several files (#150230)
An attempt to make the code a little bit easier to parse
This commit is contained in:
parent
dd7ce28678
commit
eafa5e7f05
8 changed files with 1081 additions and 1011 deletions
File diff suppressed because it is too large
Load diff
|
@ -3,12 +3,13 @@
|
|||
* See `packages/kbn-handlebars/LICENSE` for more information.
|
||||
*/
|
||||
|
||||
import Handlebars, {
|
||||
type DecoratorFunction,
|
||||
type DecoratorsHash,
|
||||
type ExtendedCompileOptions,
|
||||
type ExtendedRuntimeOptions,
|
||||
} from '../..';
|
||||
import Handlebars from '../..';
|
||||
import type {
|
||||
DecoratorFunction,
|
||||
DecoratorsHash,
|
||||
ExtendedCompileOptions,
|
||||
ExtendedRuntimeOptions,
|
||||
} from '../types';
|
||||
|
||||
type CompileFns = 'compile' | 'compileAST';
|
||||
const compileFns: CompileFns[] = ['compile', 'compileAST'];
|
||||
|
|
56
packages/kbn-handlebars/src/handlebars.ts
Normal file
56
packages/kbn-handlebars/src/handlebars.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Elasticsearch B.V licenses this file to you under the MIT License.
|
||||
* See `packages/kbn-handlebars/LICENSE` for more information.
|
||||
*/
|
||||
|
||||
// The handlebars module uses `export =`, so we should technically use `import Handlebars = require('handlebars')`, but Babel will not allow this:
|
||||
// https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require
|
||||
import Handlebars from 'handlebars';
|
||||
|
||||
import type { DecoratorsHash, ExtendedCompileOptions, ExtendedRuntimeOptions } from './types';
|
||||
import { ElasticHandlebarsVisitor } from './visitor';
|
||||
|
||||
const originalCreate = Handlebars.create;
|
||||
|
||||
export { Handlebars };
|
||||
|
||||
/**
|
||||
* Creates an isolated Handlebars environment.
|
||||
*
|
||||
* Each environment has its own helpers.
|
||||
* This is only necessary for use cases that demand distinct helpers.
|
||||
* Most use cases can use the root Handlebars environment directly.
|
||||
*
|
||||
* @returns A sandboxed/scoped version of the @kbn/handlebars module
|
||||
*/
|
||||
Handlebars.create = function (): typeof Handlebars {
|
||||
const SandboxedHandlebars = originalCreate.call(Handlebars) as typeof Handlebars;
|
||||
// When creating new Handlebars environments, ensure the custom compileAST function is present in the new environment as well
|
||||
SandboxedHandlebars.compileAST = Handlebars.compileAST;
|
||||
return SandboxedHandlebars;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compiles the given Handlbars template without the use of `eval`.
|
||||
*
|
||||
* @returns A render function with the same API as the return value from the regular Handlebars `compile` function.
|
||||
*/
|
||||
Handlebars.compileAST = function (
|
||||
input: string | hbs.AST.Program,
|
||||
options?: ExtendedCompileOptions
|
||||
) {
|
||||
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
|
||||
throw new Handlebars.Exception(
|
||||
`You must pass a string or Handlebars AST to Handlebars.compileAST. You passed ${input}`
|
||||
);
|
||||
}
|
||||
|
||||
// If `Handlebars.compileAST` is reassigned, `this` will be undefined.
|
||||
const helpers = (this ?? Handlebars).helpers;
|
||||
const partials = (this ?? Handlebars).partials;
|
||||
const decorators = (this ?? Handlebars).decorators as DecoratorsHash;
|
||||
|
||||
const visitor = new ElasticHandlebarsVisitor(this, input, options, helpers, partials, decorators);
|
||||
return (context: any, runtimeOptions?: ExtendedRuntimeOptions) =>
|
||||
visitor.render(context, runtimeOptions);
|
||||
};
|
8
packages/kbn-handlebars/src/symbols.ts
Normal file
8
packages/kbn-handlebars/src/symbols.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Elasticsearch B.V licenses this file to you under the MIT License.
|
||||
* See `packages/kbn-handlebars/LICENSE` for more information.
|
||||
*/
|
||||
|
||||
export const kHelper = Symbol('helper');
|
||||
export const kAmbiguous = Symbol('ambiguous');
|
||||
export const kSimple = Symbol('simple');
|
136
packages/kbn-handlebars/src/types.ts
Normal file
136
packages/kbn-handlebars/src/types.ts
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Elasticsearch B.V licenses this file to you under the MIT License.
|
||||
* See `packages/kbn-handlebars/LICENSE` for more information.
|
||||
*/
|
||||
|
||||
import { kHelper, kAmbiguous, kSimple } from './symbols';
|
||||
|
||||
/**
|
||||
* A custom version of the Handlesbars module with an extra `compileAST` function and fixed typings.
|
||||
*/
|
||||
declare module 'handlebars' {
|
||||
export function compileAST(
|
||||
input: string | hbs.AST.Program,
|
||||
options?: ExtendedCompileOptions
|
||||
): (context?: any, options?: ExtendedRuntimeOptions) => string;
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Override/Extend inherited types below that are incorrect
|
||||
// --------------------------------------------------------
|
||||
|
||||
export interface TemplateDelegate<T = any> {
|
||||
(context?: T, options?: RuntimeOptions): string; // Override to ensure `context` is optional
|
||||
blockParams?: number; // TODO: Can this really be optional?
|
||||
partials?: any; // TODO: Narrow type to something better than any?
|
||||
}
|
||||
|
||||
export interface HelperOptions {
|
||||
name: string;
|
||||
loc: { start: hbs.AST.SourceLocation['start']; end: hbs.AST.SourceLocation['end'] };
|
||||
lookupProperty: LookupProperty;
|
||||
}
|
||||
|
||||
export interface HelperDelegate {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-function-type
|
||||
(...params: any[]): any;
|
||||
}
|
||||
|
||||
export function registerPartial(spec: { [name: string]: Handlebars.Template }): void; // Ensure `spec` object values can be strings
|
||||
}
|
||||
|
||||
export type NodeType = typeof kHelper | typeof kAmbiguous | typeof kSimple;
|
||||
|
||||
type LookupProperty = <T = any>(parent: { [name: string]: any }, propertyName: string) => T;
|
||||
|
||||
export type ProcessableStatementNode =
|
||||
| hbs.AST.MustacheStatement
|
||||
| hbs.AST.PartialStatement
|
||||
| hbs.AST.SubExpression;
|
||||
export type ProcessableBlockStatementNode = hbs.AST.BlockStatement | hbs.AST.PartialBlockStatement;
|
||||
export type ProcessableNode = ProcessableStatementNode | ProcessableBlockStatementNode;
|
||||
export type ProcessableNodeWithPathParts = ProcessableNode & { path: hbs.AST.PathExpression };
|
||||
export type ProcessableNodeWithPathPartsOrLiteral = ProcessableNode & {
|
||||
path: hbs.AST.PathExpression | hbs.AST.Literal;
|
||||
};
|
||||
|
||||
export interface Helper {
|
||||
fn?: Handlebars.HelperDelegate;
|
||||
context: any[];
|
||||
params: any[];
|
||||
options: AmbiguousHelperOptions;
|
||||
}
|
||||
|
||||
export type NonBlockHelperOptions = Omit<Handlebars.HelperOptions, 'fn' | 'inverse'>;
|
||||
export type AmbiguousHelperOptions = Handlebars.HelperOptions | NonBlockHelperOptions;
|
||||
|
||||
export interface DecoratorOptions extends Omit<Handlebars.HelperOptions, 'lookupProperties'> {
|
||||
args?: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported Handlebars compile options.
|
||||
*
|
||||
* This is a subset of all the compile options supported by the upstream
|
||||
* Handlebars module.
|
||||
*/
|
||||
export type ExtendedCompileOptions = Pick<
|
||||
CompileOptions,
|
||||
| 'data'
|
||||
| 'knownHelpers'
|
||||
| 'knownHelpersOnly'
|
||||
| 'noEscape'
|
||||
| 'strict'
|
||||
| 'assumeObjects'
|
||||
| 'preventIndent'
|
||||
| 'explicitPartialContext'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Supported Handlebars runtime options
|
||||
*
|
||||
* This is a subset of all the runtime options supported by the upstream
|
||||
* Handlebars module.
|
||||
*/
|
||||
export type ExtendedRuntimeOptions = Pick<
|
||||
RuntimeOptions,
|
||||
'data' | 'helpers' | 'partials' | 'decorators' | 'blockParams'
|
||||
>;
|
||||
|
||||
/**
|
||||
* According to the [decorator docs]{@link https://github.com/handlebars-lang/handlebars.js/blob/4.x/docs/decorators-api.md},
|
||||
* a decorator will be called with a different set of arugments than what's actually happening in the upstream code.
|
||||
* So here I assume that the docs are wrong and that the upstream code is correct. In reality, `context` is the last 4
|
||||
* documented arguments rolled into one object.
|
||||
*/
|
||||
export type DecoratorFunction = (
|
||||
prog: Handlebars.TemplateDelegate,
|
||||
props: Record<string, any>,
|
||||
container: Container,
|
||||
options: any
|
||||
) => any;
|
||||
|
||||
export interface HelpersHash {
|
||||
[name: string]: Handlebars.HelperDelegate;
|
||||
}
|
||||
|
||||
export interface PartialsHash {
|
||||
[name: string]: HandlebarsTemplateDelegate;
|
||||
}
|
||||
|
||||
export interface DecoratorsHash {
|
||||
[name: string]: DecoratorFunction;
|
||||
}
|
||||
|
||||
export interface Container {
|
||||
helpers: HelpersHash;
|
||||
partials: PartialsHash;
|
||||
decorators: DecoratorsHash;
|
||||
strict: (obj: { [name: string]: any }, name: string, loc: hbs.AST.SourceLocation) => any;
|
||||
lookupProperty: LookupProperty;
|
||||
lambda: (current: any, context: any) => any;
|
||||
data: (value: any, depth: number) => any;
|
||||
hooks: {
|
||||
helperMissing?: Handlebars.HelperDelegate;
|
||||
blockHelperMissing?: Handlebars.HelperDelegate;
|
||||
};
|
||||
}
|
69
packages/kbn-handlebars/src/utils.ts
Normal file
69
packages/kbn-handlebars/src/utils.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Elasticsearch B.V licenses this file to you under the MIT License.
|
||||
* See `packages/kbn-handlebars/LICENSE` for more information.
|
||||
*/
|
||||
|
||||
// @ts-expect-error: Could not find a declaration file for module
|
||||
import { createFrame } from 'handlebars/dist/cjs/handlebars/utils';
|
||||
|
||||
import type { AmbiguousHelperOptions, DecoratorOptions } from './types';
|
||||
|
||||
export function isBlock(node: hbs.AST.Node): node is hbs.AST.BlockStatement {
|
||||
return 'program' in node || 'inverse' in node;
|
||||
}
|
||||
|
||||
export function isDecorator(
|
||||
node: hbs.AST.Node
|
||||
): node is hbs.AST.Decorator | hbs.AST.DecoratorBlock {
|
||||
return node.type === 'Decorator' || node.type === 'DecoratorBlock';
|
||||
}
|
||||
|
||||
export function toDecoratorOptions(options: AmbiguousHelperOptions) {
|
||||
// There's really no tests/documentation on this, but to match the upstream codebase we'll remove `lookupProperty` from the decorator context
|
||||
delete (options as any).lookupProperty;
|
||||
|
||||
return options as DecoratorOptions;
|
||||
}
|
||||
|
||||
export function noop() {
|
||||
return '';
|
||||
}
|
||||
|
||||
// liftet from handlebars lib/handlebars/runtime.js
|
||||
export function initData(context: any, data: any) {
|
||||
if (!data || !('root' in data)) {
|
||||
data = data ? createFrame(data) : {};
|
||||
data.root = context;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// liftet from handlebars lib/handlebars/compiler/compiler.js
|
||||
export function transformLiteralToPath(node: { path: hbs.AST.PathExpression | hbs.AST.Literal }) {
|
||||
const pathIsLiteral = 'parts' in node.path === false;
|
||||
|
||||
if (pathIsLiteral) {
|
||||
const literal = node.path;
|
||||
// @ts-expect-error: Not all `hbs.AST.Literal` sub-types has an `original` property, but that's ok, in that case we just want `undefined`
|
||||
const original = literal.original;
|
||||
// Casting to string here to make false and 0 literal values play nicely with the rest
|
||||
// of the system.
|
||||
node.path = {
|
||||
type: 'PathExpression',
|
||||
data: false,
|
||||
depth: 0,
|
||||
parts: [original + ''],
|
||||
original: original + '',
|
||||
loc: literal.loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function allowUnsafeEval() {
|
||||
try {
|
||||
new Function();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
791
packages/kbn-handlebars/src/visitor.ts
Normal file
791
packages/kbn-handlebars/src/visitor.ts
Normal file
|
@ -0,0 +1,791 @@
|
|||
/*
|
||||
* 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,
|
||||
Container,
|
||||
DecoratorFunction,
|
||||
DecoratorsHash,
|
||||
ExtendedCompileOptions,
|
||||
ExtendedRuntimeOptions,
|
||||
Helper,
|
||||
HelpersHash,
|
||||
NodeType,
|
||||
NonBlockHelperOptions,
|
||||
PartialsHash,
|
||||
ProcessableBlockStatementNode,
|
||||
ProcessableNode,
|
||||
ProcessableNodeWithPathParts,
|
||||
ProcessableNodeWithPathPartsOrLiteral,
|
||||
ProcessableStatementNode,
|
||||
} 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: ExtendedCompileOptions;
|
||||
private runtimeOptions?: ExtendedRuntimeOptions;
|
||||
private initialHelpers: HelpersHash;
|
||||
private initialPartials: PartialsHash;
|
||||
private initialDecorators: DecoratorsHash;
|
||||
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: ExtendedCompileOptions = {},
|
||||
helpers: HelpersHash,
|
||||
partials: PartialsHash,
|
||||
decorators: DecoratorsHash
|
||||
) {
|
||||
super();
|
||||
|
||||
this.env = env;
|
||||
|
||||
if (typeof input !== 'string' && input.type === 'Program') {
|
||||
this.ast = input;
|
||||
} else {
|
||||
this.template = input as string;
|
||||
}
|
||||
|
||||
this.compileOptions = Object.assign(
|
||||
{
|
||||
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
|
||||
);
|
||||
|
||||
this.initialHelpers = Object.assign({}, helpers);
|
||||
this.initialPartials = Object.assign({}, partials);
|
||||
this.initialDecorators = Object.assign({}, decorators);
|
||||
|
||||
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: ExtendedRuntimeOptions = {}): string {
|
||||
this.contexts = [context];
|
||||
this.output = [];
|
||||
this.runtimeOptions = Object.assign({}, options);
|
||||
this.container.helpers = Object.assign(this.initialHelpers, options.helpers);
|
||||
this.container.partials = Object.assign(this.initialPartials, options.partials);
|
||||
this.container.decorators = Object.assign(
|
||||
this.initialDecorators,
|
||||
options.decorators as DecoratorsHash
|
||||
);
|
||||
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: Handlebars.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: Handlebars.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: Handlebars.TemplateDelegate,
|
||||
props: Record<string, any>
|
||||
) {
|
||||
const options = this.setupDecoratorOptions(decorator);
|
||||
|
||||
const result = this.container.lookupProperty<DecoratorFunction>(
|
||||
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 & Handlebars.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?: Handlebars.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 = Object.assign({}, 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: Handlebars.Template | undefined =
|
||||
this.container.partials[name] ??
|
||||
partialBlock ??
|
||||
Handlebars.VM.resolvePartial(undefined, undefined, options);
|
||||
|
||||
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): Helper {
|
||||
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): Handlebars.HelperOptions;
|
||||
private setupParams(node: ProcessableStatementNode, name: string): NonBlockHelperOptions;
|
||||
private setupParams(node: ProcessableNode, name: string): AmbiguousHelperOptions;
|
||||
private setupParams(node: ProcessableNode, name: string): AmbiguousHelperOptions {
|
||||
const options = {
|
||||
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 Handlebars.HelperOptions).fn = node.program
|
||||
? this.processDecorators(node.program, this.generateProgramFunction(node.program))
|
||||
: noop;
|
||||
(options as Handlebars.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: Handlebars.TemplateDelegate = (
|
||||
nextContext: any,
|
||||
runtimeOptions: ExtendedRuntimeOptions = {}
|
||||
) => {
|
||||
runtimeOptions = Object.assign({}, 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];
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { encode } from '@kbn/rison';
|
||||
import Handlebars, { ExtendedCompileOptions, compileFnName } from '@kbn/handlebars';
|
||||
import Handlebars, { type ExtendedCompileOptions, compileFnName } from '@kbn/handlebars';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { emptyLabel } from '../../../../common/empty_label';
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue