[@kbn/handlebars] Refactor types (#150520)

This commit is contained in:
Thomas Watson 2023-02-14 13:37:41 +01:00 committed by GitHub
parent 50b83014a3
commit 77ed48a75a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 304 additions and 216 deletions

View file

@ -11,6 +11,7 @@
*/ */
import Handlebars from '.'; import Handlebars from '.';
import type { HelperOptions, TemplateDelegate } from './src/types';
import { expectTemplate, forEachCompileFunctionName } from './src/__jest__/test_bench'; import { expectTemplate, forEachCompileFunctionName } from './src/__jest__/test_bench';
it('Handlebars.create', () => { it('Handlebars.create', () => {
@ -419,7 +420,7 @@ describe('blocks', () => {
.withInput({ me: 'my' }) .withInput({ me: 'my' })
.withDecorator( .withDecorator(
'decorator', 'decorator',
(fn): Handlebars.TemplateDelegate => (fn): TemplateDelegate =>
(context, options) => { (context, options) => {
expect(context).toMatchInlineSnapshot(` expect(context).toMatchInlineSnapshot(`
Object { Object {
@ -446,7 +447,7 @@ describe('blocks', () => {
.withInput({ arr: ['my'] }) .withInput({ arr: ['my'] })
.withDecorator( .withDecorator(
'decorator', 'decorator',
(fn): Handlebars.TemplateDelegate => (fn): TemplateDelegate =>
(context, options) => { (context, options) => {
expect(context).toMatchInlineSnapshot(`"my"`); expect(context).toMatchInlineSnapshot(`"my"`);
expect(options).toMatchInlineSnapshot(` expect(options).toMatchInlineSnapshot(`
@ -483,12 +484,12 @@ describe('blocks', () => {
it('decorator nested inside of custom helper', () => { it('decorator nested inside of custom helper', () => {
expectTemplate('{{#helper}}{{*decorator}}world{{/helper}}') expectTemplate('{{#helper}}{{*decorator}}world{{/helper}}')
.withHelper('helper', function (options: Handlebars.HelperOptions) { .withHelper('helper', function (options: HelperOptions) {
return options.fn('my', { foo: 'bar' } as any); return options.fn('my', { foo: 'bar' } as any);
}) })
.withDecorator( .withDecorator(
'decorator', 'decorator',
(fn): Handlebars.TemplateDelegate => (fn): TemplateDelegate =>
(context, options) => { (context, options) => {
expect(context).toMatchInlineSnapshot(`"my"`); expect(context).toMatchInlineSnapshot(`"my"`);
expect(options).toMatchInlineSnapshot(` expect(options).toMatchInlineSnapshot(`
@ -519,7 +520,7 @@ describe('blocks', () => {
}) })
.withDecorator('decorator', (fn) => { .withDecorator('decorator', (fn) => {
const decoratorCallOrder = ++decoratorCall; const decoratorCallOrder = ++decoratorCall;
const ret: Handlebars.TemplateDelegate = () => { const ret: TemplateDelegate = () => {
const progCallOrder = ++progCall; const progCallOrder = ++progCall;
return `(decorator: ${decoratorCallOrder}, prog: ${progCallOrder}, fn: "${fn()}")`; return `(decorator: ${decoratorCallOrder}, prog: ${progCallOrder}, fn: "${fn()}")`;
}; };

View file

@ -24,7 +24,10 @@ export default Handlebars;
export const compileFnName: 'compile' | 'compileAST' = allowUnsafeEval() ? 'compile' : 'compileAST'; export const compileFnName: 'compile' | 'compileAST' = allowUnsafeEval() ? 'compile' : 'compileAST';
export type { export type {
DecoratorFunction, CompileOptions,
ExtendedCompileOptions, RuntimeOptions,
ExtendedRuntimeOptions, HelperDelegate,
TemplateDelegate,
DecoratorDelegate,
HelperOptions,
} from './src/types'; } from './src/types';

View file

@ -3,13 +3,13 @@
* See `packages/kbn-handlebars/LICENSE` for more information. * See `packages/kbn-handlebars/LICENSE` for more information.
*/ */
import Handlebars from '../..'; import Handlebars, {
import type { type CompileOptions,
DecoratorFunction, type DecoratorDelegate,
DecoratorsHash, type HelperDelegate,
ExtendedCompileOptions, type RuntimeOptions,
ExtendedRuntimeOptions, } from '../..';
} from '../types'; import type { DecoratorsHash, HelpersHash, PartialsHash, Template } from '../types';
type CompileFns = 'compile' | 'compileAST'; type CompileFns = 'compile' | 'compileAST';
const compileFns: CompileFns[] = ['compile', 'compileAST']; const compileFns: CompileFns[] = ['compile', 'compileAST'];
@ -40,10 +40,10 @@ export function forEachCompileFunctionName(
class HandlebarsTestBench { class HandlebarsTestBench {
private template: string; private template: string;
private options: TestOptions; private options: TestOptions;
private compileOptions?: ExtendedCompileOptions; private compileOptions?: CompileOptions;
private runtimeOptions?: ExtendedRuntimeOptions; private runtimeOptions?: RuntimeOptions;
private helpers: { [name: string]: Handlebars.HelperDelegate | undefined } = {}; private helpers: HelpersHash = {};
private partials: { [name: string]: Handlebars.Template } = {}; private partials: PartialsHash = {};
private decorators: DecoratorsHash = {}; private decorators: DecoratorsHash = {};
private input: any = {}; private input: any = {};
@ -52,12 +52,12 @@ class HandlebarsTestBench {
this.options = options; this.options = options;
} }
withCompileOptions(compileOptions?: ExtendedCompileOptions) { withCompileOptions(compileOptions?: CompileOptions) {
this.compileOptions = compileOptions; this.compileOptions = compileOptions;
return this; return this;
} }
withRuntimeOptions(runtimeOptions?: ExtendedRuntimeOptions) { withRuntimeOptions(runtimeOptions?: RuntimeOptions) {
this.runtimeOptions = runtimeOptions; this.runtimeOptions = runtimeOptions;
return this; return this;
} }
@ -67,36 +67,36 @@ class HandlebarsTestBench {
return this; return this;
} }
withHelper<F extends Handlebars.HelperDelegate>(name: string, helper?: F) { withHelper<F extends HelperDelegate>(name: string, helper: F) {
this.helpers[name] = helper; this.helpers[name] = helper;
return this; return this;
} }
withHelpers<F extends Handlebars.HelperDelegate>(helperFunctions: { [name: string]: F }) { withHelpers<F extends HelperDelegate>(helperFunctions: Record<string, F>) {
for (const [name, helper] of Object.entries(helperFunctions)) { for (const [name, helper] of Object.entries(helperFunctions)) {
this.withHelper(name, helper); this.withHelper(name, helper);
} }
return this; return this;
} }
withPartial(name: string | number, partial: Handlebars.Template) { withPartial(name: string | number, partial: Template) {
this.partials[name] = partial; this.partials[name] = partial;
return this; return this;
} }
withPartials(partials: { [name: string]: Handlebars.Template }) { withPartials(partials: Record<string, Template>) {
for (const [name, partial] of Object.entries(partials)) { for (const [name, partial] of Object.entries(partials)) {
this.withPartial(name, partial); this.withPartial(name, partial);
} }
return this; return this;
} }
withDecorator(name: string, decoratorFunction: DecoratorFunction) { withDecorator(name: string, decoratorFunction: DecoratorDelegate) {
this.decorators[name] = decoratorFunction; this.decorators[name] = decoratorFunction;
return this; return this;
} }
withDecorators(decoratorFunctions: { [key: string]: DecoratorFunction }) { withDecorators(decoratorFunctions: Record<string, DecoratorDelegate>) {
for (const [name, decoratorFunction] of Object.entries(decoratorFunctions)) { for (const [name, decoratorFunction] of Object.entries(decoratorFunctions)) {
this.withDecorator(name, decoratorFunction); this.withDecorator(name, decoratorFunction);
} }
@ -154,9 +154,9 @@ class HandlebarsTestBench {
private compileAndExecuteEval() { private compileAndExecuteEval() {
const renderEval = this.compileEval(); const renderEval = this.compileEval();
const runtimeOptions: ExtendedRuntimeOptions = { const runtimeOptions: RuntimeOptions = {
helpers: this.helpers as Record<string, Function>, helpers: this.helpers,
partials: this.partials as Record<string, HandlebarsTemplateDelegate>, partials: this.partials,
decorators: this.decorators, decorators: this.decorators,
...this.runtimeOptions, ...this.runtimeOptions,
}; };
@ -169,9 +169,9 @@ class HandlebarsTestBench {
private compileAndExecuteAST() { private compileAndExecuteAST() {
const renderAST = this.compileAST(); const renderAST = this.compileAST();
const runtimeOptions: ExtendedRuntimeOptions = { const runtimeOptions: RuntimeOptions = {
helpers: this.helpers as Record<string, Function>, helpers: this.helpers,
partials: this.partials as Record<string, HandlebarsTemplateDelegate>, partials: this.partials,
decorators: this.decorators, decorators: this.decorators,
...this.runtimeOptions, ...this.runtimeOptions,
}; };

View file

@ -7,7 +7,7 @@
// https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require // https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require
import Handlebars from 'handlebars'; import Handlebars from 'handlebars';
import type { ExtendedCompileOptions, ExtendedRuntimeOptions } from './types'; import type { CompileOptions, RuntimeOptions, TemplateDelegate } from './types';
import { ElasticHandlebarsVisitor } from './visitor'; import { ElasticHandlebarsVisitor } from './visitor';
const originalCreate = Handlebars.create; const originalCreate = Handlebars.create;
@ -30,15 +30,10 @@ Handlebars.create = function (): typeof Handlebars {
return SandboxedHandlebars; 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 ( Handlebars.compileAST = function (
input: string | hbs.AST.Program, input: string | hbs.AST.Program,
options?: ExtendedCompileOptions options?: CompileOptions
) { ): TemplateDelegate {
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) { if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
throw new Handlebars.Exception( throw new Handlebars.Exception(
`You must pass a string or Handlebars AST to Handlebars.compileAST. You passed ${input}` `You must pass a string or Handlebars AST to Handlebars.compileAST. You passed ${input}`
@ -48,6 +43,5 @@ Handlebars.compileAST = function (
// If `Handlebars.compileAST` is reassigned, `this` will be undefined. // If `Handlebars.compileAST` is reassigned, `this` will be undefined.
const visitor = new ElasticHandlebarsVisitor(this ?? Handlebars, input, options); const visitor = new ElasticHandlebarsVisitor(this ?? Handlebars, input, options);
return (context: any, runtimeOptions?: ExtendedRuntimeOptions) => return (context: any, runtimeOptions?: RuntimeOptions) => visitor.render(context, runtimeOptions);
visitor.render(context, runtimeOptions);
}; };

View file

@ -5,7 +5,7 @@
* See `packages/kbn-handlebars/LICENSE` for more information. * See `packages/kbn-handlebars/LICENSE` for more information.
*/ */
import Handlebars from '../..'; import Handlebars, { type HelperOptions } from '../..';
import { expectTemplate } from '../__jest__/test_bench'; import { expectTemplate } from '../__jest__/test_bench';
describe('blocks', () => { describe('blocks', () => {
@ -200,7 +200,7 @@ describe('blocks', () => {
describe('decorators', () => { describe('decorators', () => {
it('should apply mustache decorators', () => { it('should apply mustache decorators', () => {
expectTemplate('{{#helper}}{{*decorator}}{{/helper}}') expectTemplate('{{#helper}}{{*decorator}}{{/helper}}')
.withHelper('helper', function (options: Handlebars.HelperOptions) { .withHelper('helper', function (options: HelperOptions) {
return (options.fn as any).run; return (options.fn as any).run;
}) })
.withDecorator('decorator', function (fn) { .withDecorator('decorator', function (fn) {
@ -212,7 +212,7 @@ describe('blocks', () => {
it('should apply allow undefined return', () => { it('should apply allow undefined return', () => {
expectTemplate('{{#helper}}{{*decorator}}suc{{/helper}}') expectTemplate('{{#helper}}{{*decorator}}suc{{/helper}}')
.withHelper('helper', function (options: Handlebars.HelperOptions) { .withHelper('helper', function (options: HelperOptions) {
return options.fn() + (options.fn as any).run; return options.fn() + (options.fn as any).run;
}) })
.withDecorator('decorator', function (fn) { .withDecorator('decorator', function (fn) {
@ -223,7 +223,7 @@ describe('blocks', () => {
it('should apply block decorators', () => { it('should apply block decorators', () => {
expectTemplate('{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}') expectTemplate('{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}')
.withHelper('helper', function (options: Handlebars.HelperOptions) { .withHelper('helper', function (options: HelperOptions) {
return (options.fn as any).run; return (options.fn as any).run;
}) })
.withDecorator('decorator', function (fn, props, container, options) { .withDecorator('decorator', function (fn, props, container, options) {
@ -237,7 +237,7 @@ describe('blocks', () => {
expectTemplate( expectTemplate(
'{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}' '{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}'
) )
.withHelper('helper', function (options: Handlebars.HelperOptions) { .withHelper('helper', function (options: HelperOptions) {
return (options.fn as any).run; return (options.fn as any).run;
}) })
.withDecorators({ .withDecorators({
@ -256,7 +256,7 @@ describe('blocks', () => {
expectTemplate( expectTemplate(
'{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}' '{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}'
) )
.withHelper('helper', function (options: Handlebars.HelperOptions) { .withHelper('helper', function (options: HelperOptions) {
return (options.fn as any).run; return (options.fn as any).run;
}) })
.withDecorator('decorator', function (fn, props, container, options) { .withDecorator('decorator', function (fn, props, container, options) {
@ -268,7 +268,7 @@ describe('blocks', () => {
it('should access parent variables', () => { it('should access parent variables', () => {
expectTemplate('{{#helper}}{{*decorator foo}}{{/helper}}') expectTemplate('{{#helper}}{{*decorator foo}}{{/helper}}')
.withHelper('helper', function (options: Handlebars.HelperOptions) { .withHelper('helper', function (options: HelperOptions) {
return (options.fn as any).run; return (options.fn as any).run;
}) })
.withDecorator('decorator', function (fn, props, container, options) { .withDecorator('decorator', function (fn, props, container, options) {

View file

@ -5,7 +5,7 @@
* See `packages/kbn-handlebars/LICENSE` for more information. * See `packages/kbn-handlebars/LICENSE` for more information.
*/ */
import Handlebars from '../..'; import Handlebars, { type HelperOptions } from '../..';
import { expectTemplate } from '../__jest__/test_bench'; import { expectTemplate } from '../__jest__/test_bench';
describe('data', () => { describe('data', () => {
@ -30,7 +30,7 @@ describe('data', () => {
global.kbnHandlebarsEnv = Handlebars.create(); global.kbnHandlebarsEnv = Handlebars.create();
const helpers = Handlebars.createFrame(kbnHandlebarsEnv!.helpers); const helpers = Handlebars.createFrame(kbnHandlebarsEnv!.helpers);
helpers.let = function (options: Handlebars.HelperOptions) { helpers.let = function (options: HelperOptions) {
const frame = Handlebars.createFrame(options.data); const frame = Handlebars.createFrame(options.data);
for (const prop in options.hash) { for (const prop in options.hash) {
@ -138,7 +138,7 @@ describe('data', () => {
expectTemplate('{{>myPartial}}') expectTemplate('{{>myPartial}}')
.withCompileOptions({ data: true }) .withCompileOptions({ data: true })
.withPartial('myPartial', '{{hello}}') .withPartial('myPartial', '{{hello}}')
.withHelper('hello', function (this: any, options: Handlebars.HelperOptions) { .withHelper('hello', function (this: any, options: HelperOptions) {
return options.data.adjective + ' ' + this.noun; return options.data.adjective + ' ' + this.noun;
}) })
.withInput({ noun: 'cat' }) .withInput({ noun: 'cat' })

View file

@ -5,7 +5,7 @@
* See `packages/kbn-handlebars/LICENSE` for more information. * See `packages/kbn-handlebars/LICENSE` for more information.
*/ */
import Handlebars from '../..'; import Handlebars, { type HelperOptions } from '../..';
import { expectTemplate } from '../__jest__/test_bench'; import { expectTemplate } from '../__jest__/test_bench';
beforeEach(() => { beforeEach(() => {
@ -32,7 +32,7 @@ describe('helpers', () => {
it('helper for raw block gets raw content', () => { it('helper for raw block gets raw content', () => {
expectTemplate('{{{{raw}}}} {{test}} {{{{/raw}}}}') expectTemplate('{{{{raw}}}} {{test}} {{{{/raw}}}}')
.withInput({ test: 'hello' }) .withInput({ test: 'hello' })
.withHelper('raw', function (options: Handlebars.HelperOptions) { .withHelper('raw', function (options: HelperOptions) {
return options.fn(); return options.fn();
}) })
.toCompileTo(' {{test}} '); .toCompileTo(' {{test}} ');
@ -41,7 +41,7 @@ describe('helpers', () => {
it('helper for raw block gets parameters', () => { it('helper for raw block gets parameters', () => {
expectTemplate('{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}') expectTemplate('{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}')
.withInput({ test: 'hello' }) .withInput({ test: 'hello' })
.withHelper('raw', function (a, b, c, options: Handlebars.HelperOptions) { .withHelper('raw', function (a, b, c, options: HelperOptions) {
const ret = options.fn() + a + b + c; const ret = options.fn() + a + b + c;
return ret; return ret;
}) })
@ -51,7 +51,7 @@ describe('helpers', () => {
describe('raw block parsing (with identity helper-function)', () => { describe('raw block parsing (with identity helper-function)', () => {
function runWithIdentityHelper(template: string, expected: string) { function runWithIdentityHelper(template: string, expected: string) {
expectTemplate(template) expectTemplate(template)
.withHelper('identity', function (options: Handlebars.HelperOptions) { .withHelper('identity', function (options: HelperOptions) {
return options.fn(); return options.fn();
}) })
.toCompileTo(expected); .toCompileTo(expected);
@ -95,7 +95,7 @@ describe('helpers', () => {
it('helper block with identical context', () => { it('helper block with identical context', () => {
expectTemplate('{{#goodbyes}}{{name}}{{/goodbyes}}') expectTemplate('{{#goodbyes}}{{name}}{{/goodbyes}}')
.withInput({ name: 'Alan' }) .withInput({ name: 'Alan' })
.withHelper('goodbyes', function (this: any, options: Handlebars.HelperOptions) { .withHelper('goodbyes', function (this: any, options: HelperOptions) {
let out = ''; let out = '';
const byes = ['Goodbye', 'goodbye', 'GOODBYE']; const byes = ['Goodbye', 'goodbye', 'GOODBYE'];
for (let i = 0, j = byes.length; i < j; i++) { for (let i = 0, j = byes.length; i < j; i++) {
@ -109,7 +109,7 @@ describe('helpers', () => {
it('helper block with complex lookup expression', () => { it('helper block with complex lookup expression', () => {
expectTemplate('{{#goodbyes}}{{../name}}{{/goodbyes}}') expectTemplate('{{#goodbyes}}{{../name}}{{/goodbyes}}')
.withInput({ name: 'Alan' }) .withInput({ name: 'Alan' })
.withHelper('goodbyes', function (options: Handlebars.HelperOptions) { .withHelper('goodbyes', function (options: HelperOptions) {
let out = ''; let out = '';
const byes = ['Goodbye', 'goodbye', 'GOODBYE']; const byes = ['Goodbye', 'goodbye', 'GOODBYE'];
for (let i = 0, j = byes.length; i < j; i++) { for (let i = 0, j = byes.length; i < j; i++) {
@ -126,7 +126,7 @@ describe('helpers', () => {
prefix: '/root', prefix: '/root',
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], goodbyes: [{ text: 'Goodbye', url: 'goodbye' }],
}) })
.withHelper('link', function (this: any, prefix, options: Handlebars.HelperOptions) { .withHelper('link', function (this: any, prefix, options: HelperOptions) {
return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>'; return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>';
}) })
.toCompileTo('<a href="/root/goodbye">Goodbye</a>'); .toCompileTo('<a href="/root/goodbye">Goodbye</a>');
@ -138,7 +138,7 @@ describe('helpers', () => {
prefix: '/root', prefix: '/root',
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], goodbyes: [{ text: 'Goodbye', url: 'goodbye' }],
}) })
.withHelper('link', function (this: any, prefix, options: Handlebars.HelperOptions) { .withHelper('link', function (this: any, prefix, options: HelperOptions) {
return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>'; return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>';
}) })
.toCompileTo('<a href="/root/goodbye">Goodbye</a>'); .toCompileTo('<a href="/root/goodbye">Goodbye</a>');
@ -161,7 +161,7 @@ describe('helpers', () => {
it('block helper', () => { it('block helper', () => {
expectTemplate('{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!') expectTemplate('{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!')
.withInput({ world: 'world' }) .withInput({ world: 'world' })
.withHelper('goodbyes', function (options: Handlebars.HelperOptions) { .withHelper('goodbyes', function (options: HelperOptions) {
return options.fn({ text: 'GOODBYE' }); return options.fn({ text: 'GOODBYE' });
}) })
.toCompileTo('GOODBYE! cruel world!'); .toCompileTo('GOODBYE! cruel world!');
@ -170,14 +170,14 @@ describe('helpers', () => {
it('block helper staying in the same context', () => { it('block helper staying in the same context', () => {
expectTemplate('{{#form}}<p>{{name}}</p>{{/form}}') expectTemplate('{{#form}}<p>{{name}}</p>{{/form}}')
.withInput({ name: 'Yehuda' }) .withInput({ name: 'Yehuda' })
.withHelper('form', function (this: any, options: Handlebars.HelperOptions) { .withHelper('form', function (this: any, options: HelperOptions) {
return '<form>' + options.fn(this) + '</form>'; return '<form>' + options.fn(this) + '</form>';
}) })
.toCompileTo('<form><p>Yehuda</p></form>'); .toCompileTo('<form><p>Yehuda</p></form>');
}); });
it('block helper should have context in this', () => { it('block helper should have context in this', () => {
function link(this: any, options: Handlebars.HelperOptions) { function link(this: any, options: HelperOptions) {
return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>'; return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
} }
@ -201,7 +201,7 @@ describe('helpers', () => {
it('block helper passing a new context', () => { it('block helper passing a new context', () => {
expectTemplate('{{#form yehuda}}<p>{{name}}</p>{{/form}}') expectTemplate('{{#form yehuda}}<p>{{name}}</p>{{/form}}')
.withInput({ yehuda: { name: 'Yehuda' } }) .withInput({ yehuda: { name: 'Yehuda' } })
.withHelper('form', function (context, options: Handlebars.HelperOptions) { .withHelper('form', function (context, options: HelperOptions) {
return '<form>' + options.fn(context) + '</form>'; return '<form>' + options.fn(context) + '</form>';
}) })
.toCompileTo('<form><p>Yehuda</p></form>'); .toCompileTo('<form><p>Yehuda</p></form>');
@ -210,7 +210,7 @@ describe('helpers', () => {
it('block helper passing a complex path context', () => { it('block helper passing a complex path context', () => {
expectTemplate('{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}') expectTemplate('{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}')
.withInput({ yehuda: { name: 'Yehuda', cat: { name: 'Harold' } } }) .withInput({ yehuda: { name: 'Yehuda', cat: { name: 'Harold' } } })
.withHelper('form', function (context, options: Handlebars.HelperOptions) { .withHelper('form', function (context, options: HelperOptions) {
return '<form>' + options.fn(context) + '</form>'; return '<form>' + options.fn(context) + '</form>';
}) })
.toCompileTo('<form><p>Harold</p></form>'); .toCompileTo('<form><p>Harold</p></form>');
@ -221,10 +221,10 @@ describe('helpers', () => {
.withInput({ .withInput({
yehuda: { name: 'Yehuda' }, yehuda: { name: 'Yehuda' },
}) })
.withHelper('link', function (this: any, options: Handlebars.HelperOptions) { .withHelper('link', function (this: any, options: HelperOptions) {
return '<a href="' + this.name + '">' + options.fn(this) + '</a>'; return '<a href="' + this.name + '">' + options.fn(this) + '</a>';
}) })
.withHelper('form', function (context, options: Handlebars.HelperOptions) { .withHelper('form', function (context, options: HelperOptions) {
return '<form>' + options.fn(context) + '</form>'; return '<form>' + options.fn(context) + '</form>';
}) })
.toCompileTo('<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>'); .toCompileTo('<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>');
@ -232,7 +232,7 @@ describe('helpers', () => {
it('block helper inverted sections', () => { it('block helper inverted sections', () => {
const string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}"; const string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}";
function list(this: any, context: any, options: Handlebars.HelperOptions) { function list(this: any, context: any, options: HelperOptions) {
if (context.length > 0) { if (context.length > 0) {
let out = '<ul>'; let out = '<ul>';
for (let i = 0, j = context.length; i < j; i++) { for (let i = 0, j = context.length; i < j; i++) {
@ -477,7 +477,7 @@ describe('helpers', () => {
it('block multi-params work', () => { it('block multi-params work', () => {
expectTemplate('Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}') expectTemplate('Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}')
.withInput({ cruel: 'cruel', world: 'world' }) .withInput({ cruel: 'cruel', world: 'world' })
.withHelper('goodbye', function (cruel, world, options: Handlebars.HelperOptions) { .withHelper('goodbye', function (cruel, world, options: HelperOptions) {
return options.fn({ greeting: 'Goodbye', adj: cruel, noun: world }); return options.fn({ greeting: 'Goodbye', adj: cruel, noun: world });
}) })
.toCompileTo('Message: Goodbye cruel world'); .toCompileTo('Message: Goodbye cruel world');
@ -487,7 +487,7 @@ describe('helpers', () => {
describe('hash', () => { describe('hash', () => {
it('helpers can take an optional hash', () => { it('helpers can take an optional hash', () => {
expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" times=12}}') expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" times=12}}')
.withHelper('goodbye', function (options: Handlebars.HelperOptions) { .withHelper('goodbye', function (options: HelperOptions) {
return ( return (
'GOODBYE ' + 'GOODBYE ' +
options.hash.cruel + options.hash.cruel +
@ -502,7 +502,7 @@ describe('helpers', () => {
}); });
it('helpers can take an optional hash with booleans', () => { it('helpers can take an optional hash with booleans', () => {
function goodbye(options: Handlebars.HelperOptions) { function goodbye(options: HelperOptions) {
if (options.hash.print === true) { if (options.hash.print === true) {
return 'GOODBYE ' + options.hash.cruel + ' ' + options.hash.world; return 'GOODBYE ' + options.hash.cruel + ' ' + options.hash.world;
} else if (options.hash.print === false) { } else if (options.hash.print === false) {
@ -523,7 +523,7 @@ describe('helpers', () => {
it('block helpers can take an optional hash', () => { it('block helpers can take an optional hash', () => {
expectTemplate('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}') expectTemplate('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}')
.withHelper('goodbye', function (this: any, options: Handlebars.HelperOptions) { .withHelper('goodbye', function (this: any, options: HelperOptions) {
return ( return (
'GOODBYE ' + 'GOODBYE ' +
options.hash.cruel + options.hash.cruel +
@ -539,7 +539,7 @@ describe('helpers', () => {
it('block helpers can take an optional hash with single quoted stings', () => { it('block helpers can take an optional hash with single quoted stings', () => {
expectTemplate('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}') expectTemplate('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}')
.withHelper('goodbye', function (this: any, options: Handlebars.HelperOptions) { .withHelper('goodbye', function (this: any, options: HelperOptions) {
return ( return (
'GOODBYE ' + 'GOODBYE ' +
options.hash.cruel + options.hash.cruel +
@ -554,7 +554,7 @@ describe('helpers', () => {
}); });
it('block helpers can take an optional hash with booleans', () => { it('block helpers can take an optional hash with booleans', () => {
function goodbye(this: any, options: Handlebars.HelperOptions) { function goodbye(this: any, options: HelperOptions) {
if (options.hash.print === true) { if (options.hash.print === true) {
return 'GOODBYE ' + options.hash.cruel + ' ' + options.fn(this); return 'GOODBYE ' + options.hash.cruel + ' ' + options.fn(this);
} else if (options.hash.print === false) { } else if (options.hash.print === false) {
@ -582,7 +582,7 @@ describe('helpers', () => {
it('if a context is not found, custom helperMissing is used', () => { it('if a context is not found, custom helperMissing is used', () => {
expectTemplate('{{hello}} {{link_to world}}') expectTemplate('{{hello}} {{link_to world}}')
.withInput({ hello: 'Hello', world: 'world' }) .withInput({ hello: 'Hello', world: 'world' })
.withHelper('helperMissing', function (mesg, options: Handlebars.HelperOptions) { .withHelper('helperMissing', function (mesg, options: HelperOptions) {
if (options.name === 'link_to') { if (options.name === 'link_to') {
return new Handlebars.SafeString('<a>' + mesg + '</a>'); return new Handlebars.SafeString('<a>' + mesg + '</a>');
} }
@ -593,7 +593,7 @@ describe('helpers', () => {
it('if a value is not found, custom helperMissing is used', () => { it('if a value is not found, custom helperMissing is used', () => {
expectTemplate('{{hello}} {{link_to}}') expectTemplate('{{hello}} {{link_to}}')
.withInput({ hello: 'Hello', world: 'world' }) .withInput({ hello: 'Hello', world: 'world' })
.withHelper('helperMissing', function (options: Handlebars.HelperOptions) { .withHelper('helperMissing', function (options: HelperOptions) {
if (options.name === 'link_to') { if (options.name === 'link_to') {
return new Handlebars.SafeString('<a>winning</a>'); return new Handlebars.SafeString('<a>winning</a>');
} }
@ -788,7 +788,7 @@ describe('helpers', () => {
it('helpers take precedence over same-named context properties$', () => { it('helpers take precedence over same-named context properties$', () => {
expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}}') expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}}')
.withHelper('goodbye', function (this: any, options: Handlebars.HelperOptions) { .withHelper('goodbye', function (this: any, options: HelperOptions) {
return this.goodbye.toUpperCase() + options.fn(this); return this.goodbye.toUpperCase() + options.fn(this);
}) })
.withHelper('cruel', function (world) { .withHelper('cruel', function (world) {
@ -818,7 +818,7 @@ describe('helpers', () => {
it('Scoped names take precedence over block helpers', () => { it('Scoped names take precedence over block helpers', () => {
expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}') expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}')
.withHelper('goodbye', function (this: any, options: Handlebars.HelperOptions) { .withHelper('goodbye', function (this: any, options: HelperOptions) {
return this.goodbye.toUpperCase() + options.fn(this); return this.goodbye.toUpperCase() + options.fn(this);
}) })
.withHelper('cruel', function (world) { .withHelper('cruel', function (world) {
@ -836,7 +836,7 @@ describe('helpers', () => {
it('should take presedence over context values', () => { it('should take presedence over context values', () => {
expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}') expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}')
.withInput({ value: 'foo' }) .withInput({ value: 'foo' })
.withHelper('goodbyes', function (options: Handlebars.HelperOptions) { .withHelper('goodbyes', function (options: HelperOptions) {
expect(options.fn.blockParams).toEqual(1); expect(options.fn.blockParams).toEqual(1);
return options.fn({ value: 'bar' }, { blockParams: [1, 2] }); return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
}) })
@ -848,7 +848,7 @@ describe('helpers', () => {
.withHelper('value', function () { .withHelper('value', function () {
return 'foo'; return 'foo';
}) })
.withHelper('goodbyes', function (options: Handlebars.HelperOptions) { .withHelper('goodbyes', function (options: HelperOptions) {
expect(options.fn.blockParams).toEqual(1); expect(options.fn.blockParams).toEqual(1);
return options.fn({}, { blockParams: [1, 2] }); return options.fn({}, { blockParams: [1, 2] });
}) })
@ -861,7 +861,7 @@ describe('helpers', () => {
.withHelper('value', function () { .withHelper('value', function () {
return 'foo'; return 'foo';
}) })
.withHelper('goodbyes', function (this: any, options: Handlebars.HelperOptions) { .withHelper('goodbyes', function (this: any, options: HelperOptions) {
expect(options.fn.blockParams).toEqual(1); expect(options.fn.blockParams).toEqual(1);
return options.fn(this, { blockParams: [1, 2] }); return options.fn(this, { blockParams: [1, 2] });
}) })
@ -879,7 +879,7 @@ describe('helpers', () => {
} }
) )
.withInput({ value: 'foo' }) .withInput({ value: 'foo' })
.withHelper('goodbyes', function (options: Handlebars.HelperOptions) { .withHelper('goodbyes', function (options: HelperOptions) {
return options.fn( return options.fn(
{ value: 'bar' }, { value: 'bar' },
{ {
@ -893,7 +893,7 @@ describe('helpers', () => {
it('should allow block params on chained helpers', () => { it('should allow block params on chained helpers', () => {
expectTemplate('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}') expectTemplate('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}')
.withInput({ value: 'foo' }) .withInput({ value: 'foo' })
.withHelper('goodbyes', function (options: Handlebars.HelperOptions) { .withHelper('goodbyes', function (options: HelperOptions) {
expect(options.fn.blockParams).toEqual(1); expect(options.fn.blockParams).toEqual(1);
return options.fn({ value: 'bar' }, { blockParams: [1, 2] }); return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
}) })
@ -942,12 +942,9 @@ describe('helpers', () => {
describe('the lookupProperty-option', () => { describe('the lookupProperty-option', () => {
it('should be passed to custom helpers', () => { it('should be passed to custom helpers', () => {
expectTemplate('{{testHelper}}') expectTemplate('{{testHelper}}')
.withHelper( .withHelper('testHelper', function testHelper(this: any, options: HelperOptions) {
'testHelper', return options.lookupProperty(this, 'testProperty');
function testHelper(this: any, options: Handlebars.HelperOptions) { })
return options.lookupProperty(this, 'testProperty');
}
)
.withInput({ testProperty: 'abc' }) .withInput({ testProperty: 'abc' })
.toCompileTo('abc'); .toCompileTo('abc');
}); });

View file

@ -163,8 +163,7 @@ describe('partials', () => {
global.kbnHandlebarsEnv = Handlebars.create(); global.kbnHandlebarsEnv = Handlebars.create();
expect(() => { expect(() => {
const undef: unknown = undefined; kbnHandlebarsEnv!.registerPartial('undefined_test', undefined as any);
kbnHandlebarsEnv!.registerPartial('undefined_test', undef as Handlebars.Template);
}).toThrow('Attempting to register a partial called "undefined_test" as undefined'); }).toThrow('Attempting to register a partial called "undefined_test" as undefined');
global.kbnHandlebarsEnv = null; global.kbnHandlebarsEnv = null;
@ -294,7 +293,6 @@ describe('partials', () => {
{}, {},
{ {
partials: { partials: {
// @ts-expect-error
dude: 'fail', dude: 'fail',
}, },
} }

View file

@ -5,7 +5,7 @@
* See `packages/kbn-handlebars/LICENSE` for more information. * See `packages/kbn-handlebars/LICENSE` for more information.
*/ */
import Handlebars from '../..'; import Handlebars, { type HelperOptions } from '../..';
import { expectTemplate, forEachCompileFunctionName } from '../__jest__/test_bench'; import { expectTemplate, forEachCompileFunctionName } from '../__jest__/test_bench';
describe('Regressions', () => { describe('Regressions', () => {
@ -99,10 +99,10 @@ describe('Regressions', () => {
'{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}';
const helpers = { const helpers = {
blk(block: Handlebars.HelperOptions) { blk(block: HelperOptions) {
return block.fn(''); return block.fn('');
}, },
inverse(block: Handlebars.HelperOptions) { inverse(block: HelperOptions) {
return block.inverse(''); return block.inverse('');
}, },
}; };
@ -204,7 +204,7 @@ describe('Regressions', () => {
it('GH-1054: Should handle simple safe string responses', () => { it('GH-1054: Should handle simple safe string responses', () => {
expectTemplate('{{#wrap}}{{>partial}}{{/wrap}}') expectTemplate('{{#wrap}}{{>partial}}{{/wrap}}')
.withHelpers({ .withHelpers({
wrap(options: Handlebars.HelperOptions) { wrap(options: HelperOptions) {
return new Handlebars.SafeString(options.fn()); return new Handlebars.SafeString(options.fn());
}, },
}) })
@ -279,7 +279,7 @@ describe('Regressions', () => {
) )
.withInput({ array: [1], name: 'John' }) .withInput({ array: [1], name: 'John' })
.withHelpers({ .withHelpers({
myif(conditional, options: Handlebars.HelperOptions) { myif(conditional, options: HelperOptions) {
if (conditional) { if (conditional) {
return options.fn(this); return options.fn(this);
} else { } else {
@ -325,7 +325,7 @@ describe('Regressions', () => {
expectTemplate('{{helpa length="foo"}}') expectTemplate('{{helpa length="foo"}}')
.withInput({ array: [1], name: 'John' }) .withInput({ array: [1], name: 'John' })
.withHelpers({ .withHelpers({
helpa(options: Handlebars.HelperOptions) { helpa(options: HelperOptions) {
return options.hash.length; return options.hash.length;
}, },
}) })
@ -371,7 +371,7 @@ describe('Regressions', () => {
describe("GH-1639: TypeError: Cannot read property 'apply' of undefined\" when handlebars version > 4.6.0 (undocumented, deprecated usage)", () => { describe("GH-1639: TypeError: Cannot read property 'apply' of undefined\" when handlebars version > 4.6.0 (undocumented, deprecated usage)", () => {
it('should treat undefined helpers like non-existing helpers', () => { it('should treat undefined helpers like non-existing helpers', () => {
expectTemplate('{{foo}}') expectTemplate('{{foo}}')
.withHelper('foo', undefined) .withHelper('foo', undefined as any)
.withInput({ foo: 'bar' }) .withInput({ foo: 'bar' })
.toCompileTo('bar'); .toCompileTo('bar');
}); });

View file

@ -5,7 +5,7 @@
* See `packages/kbn-handlebars/LICENSE` for more information. * See `packages/kbn-handlebars/LICENSE` for more information.
*/ */
import Handlebars from '../..'; import Handlebars, { type HelperOptions } from '../..';
import { expectTemplate } from '../__jest__/test_bench'; import { expectTemplate } from '../__jest__/test_bench';
describe('subexpressions', () => { describe('subexpressions', () => {
@ -102,9 +102,9 @@ describe('subexpressions', () => {
}); });
it('provides each nested helper invocation its own options hash', () => { it('provides each nested helper invocation its own options hash', () => {
let lastOptions: Handlebars.HelperOptions; let lastOptions: HelperOptions;
const helpers = { const helpers = {
equal(x: any, y: any, options: Handlebars.HelperOptions) { equal(x: any, y: any, options: HelperOptions) {
if (!options || options === lastOptions) { if (!options || options === lastOptions) {
throw new Error('options hash was reused'); throw new Error('options hash was reused');
} }

View file

@ -5,66 +5,67 @@
import { kHelper, kAmbiguous, kSimple } from './symbols'; import { kHelper, kAmbiguous, kSimple } from './symbols';
// Unexported `CompileOptions` lifted from node_modules/handlebars/types/index.d.ts
// While it could also be extracted using `NonNullable<Parameters<typeof Handlebars.compile>[1]>`, this isn't possible since we declare the handlebars module below
interface HandlebarsCompileOptions {
data?: boolean;
compat?: boolean;
knownHelpers?: KnownHelpers;
knownHelpersOnly?: boolean;
noEscape?: boolean;
strict?: boolean;
assumeObjects?: boolean;
preventIndent?: boolean;
ignoreStandalone?: boolean;
explicitPartialContext?: boolean;
}
/** /**
* A custom version of the Handlesbars module with an extra `compileAST` function and fixed typings. * A custom version of the Handlebars module with an extra `compileAST` function and fixed typings.
*/ */
declare module 'handlebars' { declare module 'handlebars' {
/**
* Compiles the given Handlebars template without the use of `eval`.
*
* @returns A render function with the same API as the return value from the regular Handlebars `compile` function.
*/
export function compileAST( export function compileAST(
input: string | hbs.AST.Program, input: string | hbs.AST.Program,
options?: ExtendedCompileOptions options?: CompileOptions
): (context?: any, options?: ExtendedRuntimeOptions) => string; ): TemplateDelegateFixed;
// -------------------------------------------------------- // --------------------------------------------------------
// Override/Extend inherited types below that are incorrect // Override/Extend inherited funcions and interfaces below that are incorrect.
//
// Any exported `const` or `type` types can't be overwritten, so we'll just
// have to live with those and cast them to the correct types in our code.
// Some of these fixed types, we'll instead export outside the scope of this
// 'handlebars' module so consumers of @kbn/handlebars at least have a way to
// access the correct types.
// -------------------------------------------------------- // --------------------------------------------------------
export interface TemplateDelegate<T = any> { /**
(context?: T, options?: RuntimeOptions): string; // Override to ensure `context` is optional * A {@link https://handlebarsjs.com/api-reference/helpers.html helper-function} type.
blockParams?: number; // TODO: Can this really be optional? *
partials?: any; // TODO: Narrow type to something better than any? * When registering a helper function, it should be of this type.
} */
export interface HelperDelegate extends HelperDelegateFixed {} // eslint-disable-line @typescript-eslint/no-empty-interface
export interface HelperOptions { /**
name: string; * A template-function type.
loc: { start: hbs.AST.SourceLocation['start']; end: hbs.AST.SourceLocation['end'] }; *
lookupProperty: LookupProperty; * This type is primarily used for the return value of by calls to
} * {@link https://handlebarsjs.com/api-reference/compilation.html#handlebars-compile-template-options Handlebars.compile},
* Handlebars.compileAST and {@link https://handlebarsjs.com/api-reference/compilation.html#handlebars-precompile-template-options Handlebars.template}.
*/
export interface TemplateDelegate<T = any> extends TemplateDelegateFixed<T> {} // eslint-disable-line @typescript-eslint/no-empty-interface
export interface HelperDelegate { /**
// eslint-disable-next-line @typescript-eslint/prefer-function-type * Register one or more {@link https://handlebarsjs.com/api-reference/runtime.html#handlebars-registerpartial-name-partial partials}.
(...params: any[]): any; *
} * @param spec A key/value object where each key is the name of a partial (a string) and each value is the partial (either a string or a partial function).
*/
export function registerPartial(spec: { [name: string]: Handlebars.Template }): void; // Ensure `spec` object values can be strings export function registerPartial(spec: Record<string, TemplateFixed>): 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[];
} }
/** /**
@ -73,8 +74,8 @@ export interface DecoratorOptions extends Omit<Handlebars.HelperOptions, 'lookup
* This is a subset of all the compile options supported by the upstream * This is a subset of all the compile options supported by the upstream
* Handlebars module. * Handlebars module.
*/ */
export type ExtendedCompileOptions = Pick< export type CompileOptions = Pick<
CompileOptions, HandlebarsCompileOptions,
| 'data' | 'data'
| 'knownHelpers' | 'knownHelpers'
| 'knownHelpersOnly' | 'knownHelpersOnly'
@ -91,46 +92,134 @@ export type ExtendedCompileOptions = Pick<
* This is a subset of all the runtime options supported by the upstream * This is a subset of all the runtime options supported by the upstream
* Handlebars module. * Handlebars module.
*/ */
export type ExtendedRuntimeOptions = Pick< export interface RuntimeOptions extends Pick<Handlebars.RuntimeOptions, 'data' | 'blockParams'> {
RuntimeOptions, // The upstream `helpers` property is too loose and allows all functions.
'data' | 'helpers' | 'partials' | 'decorators' | 'blockParams' helpers?: HelpersHash;
>; // The upstream `partials` property is incorrectly typed and doesn't allow
// partials to be strings.
partials?: PartialsHash;
// The upstream `decorators` property is too loose and allows all functions.
decorators?: DecoratorsHash;
}
/** /**
* According to the [decorator docs]{@link https://github.com/handlebars-lang/handlebars.js/blob/4.x/docs/decorators-api.md}, * The last argument being passed to a helper function is a an {@link https://handlebarsjs.com/api-reference/helpers.html#the-options-parameter options object}.
* 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 = ( export interface HelperOptions extends Omit<Handlebars.HelperOptions, 'fn' | 'inverse'> {
prog: Handlebars.TemplateDelegate, name: string;
fn: TemplateDelegateFixed;
inverse: TemplateDelegateFixed;
loc: { start: hbs.AST.SourceLocation['start']; end: hbs.AST.SourceLocation['end'] };
lookupProperty: LookupProperty;
}
// Use the post-fix `Fixed` to allow us to acces it inside the 'handlebars' module declared above
/**
* A {@link https://handlebarsjs.com/api-reference/helpers.html helper-function} type.
*
* When registering a helper function, it should be of this type.
*/
interface HelperDelegateFixed {
// eslint-disable-next-line @typescript-eslint/prefer-function-type
(...params: any[]): any;
}
export type { HelperDelegateFixed as HelperDelegate };
// Use the post-fix `Fixed` to allow us to acces it inside the 'handlebars' module declared above
/**
* A template-function type.
*
* This type is primarily used for the return value of by calls to
* {@link https://handlebarsjs.com/api-reference/compilation.html#handlebars-compile-template-options Handlebars.compile},
* Handlebars.compileAST and {@link https://handlebarsjs.com/api-reference/compilation.html#handlebars-precompile-template-options Handlebars.template}.
*/
interface TemplateDelegateFixed<T = any> {
(context?: T, options?: RuntimeOptions): string; // Override to ensure `context` is optional
blockParams?: number; // TODO: Can this really be optional?
partials?: PartialsHash;
}
export type { TemplateDelegateFixed as TemplateDelegate };
// According to the decorator docs
// (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.
/**
* A {@link https://github.com/handlebars-lang/handlebars.js/blob/master/docs/decorators-api.md decorator-function} type.
*
* When registering a decorator function, it should be of this type.
*/
export type DecoratorDelegate = (
prog: TemplateDelegateFixed,
props: Record<string, any>, props: Record<string, any>,
container: Container, container: Container,
options: any options: any
) => any; ) => any;
export interface HelpersHash { // -----------------------------------------------------------------------------
[name: string]: Handlebars.HelperDelegate; // INTERNAL TYPES
// -----------------------------------------------------------------------------
export type NodeType = typeof kHelper | typeof kAmbiguous | typeof kSimple;
type LookupProperty = <T = any>(parent: Record<string, any>, propertyName: string) => T;
export type NonBlockHelperOptions = Omit<HelperOptions, 'fn' | 'inverse'>;
export type AmbiguousHelperOptions = HelperOptions | NonBlockHelperOptions;
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 type HelpersHash = Record<string, HelperDelegateFixed>;
export type PartialsHash = Record<string, TemplateFixed>;
export type DecoratorsHash = Record<string, DecoratorDelegate>;
// Use the post-fix `Fixed` to allow us to acces it inside the 'handlebars' module declared above
type TemplateFixed = TemplateDelegateFixed | string;
export type { TemplateFixed as Template };
export interface DecoratorOptions extends Omit<HelperOptions, 'lookupProperties'> {
args?: any[];
} }
export interface PartialsHash { export interface VisitorHelper {
[name: string]: HandlebarsTemplateDelegate; fn?: HelperDelegateFixed;
context: any[];
params: any[];
options: AmbiguousHelperOptions;
} }
export interface DecoratorsHash { export interface ResolvePartialOptions
[name: string]: DecoratorFunction; extends Omit<Handlebars.ResolvePartialOptions, 'helpers' | 'partials' | 'decorators'> {
// The upstream `helpers` property is too loose and allows all functions.
helpers?: HelpersHash;
// The upstream `partials` property is incorrectly typed and doesn't allow
// partials to be strings.
partials?: PartialsHash;
// The upstream `decorators` property is too loose and allows all functions.
decorators?: DecoratorsHash;
} }
export interface Container { export interface Container {
helpers: HelpersHash; helpers: HelpersHash;
partials: PartialsHash; partials: PartialsHash;
decorators: DecoratorsHash; decorators: DecoratorsHash;
strict: (obj: { [name: string]: any }, name: string, loc: hbs.AST.SourceLocation) => any; strict: (obj: Record<string, any>, name: string, loc: hbs.AST.SourceLocation) => any;
lookupProperty: LookupProperty; lookupProperty: LookupProperty;
lambda: (current: any, context: any) => any; lambda: (current: any, context: any) => any;
data: (value: any, depth: number) => any; data: (value: any, depth: number) => any;
hooks: { hooks: {
helperMissing?: Handlebars.HelperDelegate; helperMissing?: HelperDelegateFixed;
blockHelperMissing?: Handlebars.HelperDelegate; blockHelperMissing?: HelperDelegateFixed;
}; };
} }

View file

@ -18,13 +18,11 @@ import { moveHelperToHooks } from 'handlebars/dist/cjs/handlebars/helpers';
import type { import type {
AmbiguousHelperOptions, AmbiguousHelperOptions,
CompileOptions,
Container, Container,
DecoratorFunction, DecoratorDelegate,
DecoratorsHash, DecoratorsHash,
ExtendedCompileOptions, HelperOptions,
ExtendedRuntimeOptions,
Helper,
HelpersHash,
NodeType, NodeType,
NonBlockHelperOptions, NonBlockHelperOptions,
ProcessableBlockStatementNode, ProcessableBlockStatementNode,
@ -32,6 +30,11 @@ import type {
ProcessableNodeWithPathParts, ProcessableNodeWithPathParts,
ProcessableNodeWithPathPartsOrLiteral, ProcessableNodeWithPathPartsOrLiteral,
ProcessableStatementNode, ProcessableStatementNode,
ResolvePartialOptions,
RuntimeOptions,
Template,
TemplateDelegate,
VisitorHelper,
} from './types'; } from './types';
import { kAmbiguous, kHelper, kSimple } from './symbols'; import { kAmbiguous, kHelper, kSimple } from './symbols';
import { import {
@ -48,8 +51,8 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
private contexts: any[] = []; private contexts: any[] = [];
private output: any[] = []; private output: any[] = [];
private template?: string; private template?: string;
private compileOptions: ExtendedCompileOptions; private compileOptions: CompileOptions;
private runtimeOptions?: ExtendedRuntimeOptions; private runtimeOptions?: RuntimeOptions;
private blockParamNames: any[][] = []; private blockParamNames: any[][] = [];
private blockParamValues: any[][] = []; private blockParamValues: any[][] = [];
private ast?: hbs.AST.Program; private ast?: hbs.AST.Program;
@ -61,7 +64,7 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
constructor( constructor(
env: typeof Handlebars, env: typeof Handlebars,
input: string | hbs.AST.Program, input: string | hbs.AST.Program,
options: ExtendedCompileOptions = {} options: CompileOptions = {}
) { ) {
super(); super();
@ -136,18 +139,15 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
}; };
} }
render(context: any, options: ExtendedRuntimeOptions = {}): string { render(context: any, options: RuntimeOptions = {}): string {
this.contexts = [context]; this.contexts = [context];
this.output = []; this.output = [];
this.runtimeOptions = { ...options }; this.runtimeOptions = { ...options };
this.container.helpers = { this.container.helpers = { ...this.env.helpers, ...options.helpers };
...this.env.helpers,
...(options.helpers as HelpersHash),
};
this.container.partials = { ...this.env.partials, ...options.partials }; this.container.partials = { ...this.env.partials, ...options.partials };
this.container.decorators = { this.container.decorators = {
...(this.env.decorators as DecoratorsHash), ...(this.env.decorators as DecoratorsHash),
...(options.decorators as DecoratorsHash), ...options.decorators,
}; };
this.container.hooks = {}; this.container.hooks = {};
this.processedRootDecorators = false; this.processedRootDecorators = false;
@ -170,7 +170,7 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
// Generate a "program" function based on the root `Program` in the AST and // 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 // call it. This will start the processing of all the child nodes in the
// AST. // AST.
const defaultMain: Handlebars.TemplateDelegate = (_context) => { const defaultMain: TemplateDelegate = (_context) => {
const prog = this.generateProgramFunction(this.ast!); const prog = this.generateProgramFunction(this.ast!);
return prog(_context, this.runtimeOptions); return prog(_context, this.runtimeOptions);
}; };
@ -296,7 +296,7 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
* So we have to look into the program AST body and see if it contains any decorators that we have to process * 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. * before we can finish processing of the wrapping program.
*/ */
private processDecorators(program: hbs.AST.Program, prog: Handlebars.TemplateDelegate) { private processDecorators(program: hbs.AST.Program, prog: TemplateDelegate) {
if (!this.processedDecoratorsForProgram.has(program)) { if (!this.processedDecoratorsForProgram.has(program)) {
this.processedDecoratorsForProgram.add(program); this.processedDecoratorsForProgram.add(program);
const props = {}; const props = {};
@ -312,12 +312,12 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
private processDecorator( private processDecorator(
decorator: hbs.AST.DecoratorBlock | hbs.AST.Decorator, decorator: hbs.AST.DecoratorBlock | hbs.AST.Decorator,
prog: Handlebars.TemplateDelegate, prog: TemplateDelegate,
props: Record<string, any> props: Record<string, any>
) { ) {
const options = this.setupDecoratorOptions(decorator); const options = this.setupDecoratorOptions(decorator);
const result = this.container.lookupProperty<DecoratorFunction>( const result = this.container.lookupProperty<DecoratorDelegate>(
this.container.decorators, this.container.decorators,
options.name options.name
)(prog, props, this.container, options); )(prog, props, this.container, options);
@ -499,10 +499,7 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
? this.resolveNodes(partial.name).join('') ? this.resolveNodes(partial.name).join('')
: (partial.name as hbs.AST.PathExpression).original; : (partial.name as hbs.AST.PathExpression).original;
const options: AmbiguousHelperOptions & Handlebars.ResolvePartialOptions = this.setupParams( const options: AmbiguousHelperOptions & ResolvePartialOptions = this.setupParams(partial, name);
partial,
name
);
options.helpers = this.container.helpers; options.helpers = this.container.helpers;
options.partials = this.container.partials; options.partials = this.container.partials;
options.decorators = this.container.decorators; options.decorators = this.container.decorators;
@ -516,7 +513,7 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
// Wrapper function to get access to currentPartialBlock from the closure // Wrapper function to get access to currentPartialBlock from the closure
partialBlock = options.data['partial-block'] = function partialBlockWrapper( partialBlock = options.data['partial-block'] = function partialBlockWrapper(
context: any, context: any,
wrapperOptions: { data?: Handlebars.HelperOptions['data'] } = {} wrapperOptions: { data?: HelperOptions['data'] } = {}
) { ) {
// Restore the partial-block from the closure for the execution of the block // Restore the partial-block from the closure for the execution of the block
// i.e. the part inside the block of the partial call. // i.e. the part inside the block of the partial call.
@ -542,10 +539,17 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
context = Object.assign({}, context, options.hash); context = Object.assign({}, context, options.hash);
} }
const partialTemplate: Handlebars.Template | undefined = const partialTemplate: Template | undefined =
this.container.partials[name] ?? this.container.partials[name] ??
partialBlock ?? partialBlock ??
Handlebars.VM.resolvePartial(undefined, undefined, options); // 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) { if (partialTemplate === undefined) {
throw new Handlebars.Exception(`The partial ${name} could not be found`); throw new Handlebars.Exception(`The partial ${name} could not be found`);
@ -619,7 +623,7 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
} }
} }
private setupHelper(node: ProcessableNode, helperName: string): Helper { private setupHelper(node: ProcessableNode, helperName: string): VisitorHelper {
return { return {
fn: this.container.lookupProperty(this.container.helpers, helperName), fn: this.container.lookupProperty(this.container.helpers, helperName),
context: this.context, context: this.context,
@ -649,11 +653,11 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
return options; return options;
} }
private setupParams(node: ProcessableBlockStatementNode, name: string): Handlebars.HelperOptions; private setupParams(node: ProcessableBlockStatementNode, name: string): HelperOptions;
private setupParams(node: ProcessableStatementNode, name: string): NonBlockHelperOptions; private setupParams(node: ProcessableStatementNode, name: string): NonBlockHelperOptions;
private setupParams(node: ProcessableNode, name: string): AmbiguousHelperOptions; private setupParams(node: ProcessableNode, name: string): AmbiguousHelperOptions;
private setupParams(node: ProcessableNode, name: string): AmbiguousHelperOptions { private setupParams(node: ProcessableNode, name: string) {
const options = { const options: AmbiguousHelperOptions = {
name, name,
hash: this.getHash(node), hash: this.getHash(node),
data: this.runtimeOptions!.data, data: this.runtimeOptions!.data,
@ -662,10 +666,10 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
}; };
if (isBlock(node)) { if (isBlock(node)) {
(options as Handlebars.HelperOptions).fn = node.program (options as HelperOptions).fn = node.program
? this.processDecorators(node.program, this.generateProgramFunction(node.program)) ? this.processDecorators(node.program, this.generateProgramFunction(node.program))
: noop; : noop;
(options as Handlebars.HelperOptions).inverse = node.inverse (options as HelperOptions).inverse = node.inverse
? this.processDecorators(node.inverse, this.generateProgramFunction(node.inverse)) ? this.processDecorators(node.inverse, this.generateProgramFunction(node.inverse))
: noop; : noop;
} }
@ -676,10 +680,7 @@ export class ElasticHandlebarsVisitor extends Handlebars.Visitor {
private generateProgramFunction(program: hbs.AST.Program) { private generateProgramFunction(program: hbs.AST.Program) {
if (!program) return noop; if (!program) return noop;
const prog: Handlebars.TemplateDelegate = ( const prog: TemplateDelegate = (nextContext: any, runtimeOptions: RuntimeOptions = {}) => {
nextContext: any,
runtimeOptions: ExtendedRuntimeOptions = {}
) => {
runtimeOptions = { ...runtimeOptions }; runtimeOptions = { ...runtimeOptions };
// inherit data in blockParams from parent program // inherit data in blockParams from parent program

View file

@ -6,7 +6,7 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import Handlebars from '@kbn/handlebars'; import Handlebars, { type HelperOptions, type HelperDelegate } from '@kbn/handlebars';
import { encode } from '@kbn/rison'; import { encode } from '@kbn/rison';
import dateMath from '@kbn/datemath'; import dateMath from '@kbn/datemath';
import moment, { Moment } from 'moment'; import moment, { Moment } from 'moment';
@ -18,9 +18,9 @@ const handlebars = Handlebars.create();
function createSerializationHelper( function createSerializationHelper(
fnName: string, fnName: string,
serializeFn: (value: unknown) => string serializeFn: (value: unknown) => string
): Handlebars.HelperDelegate { ): HelperDelegate {
return (...args) => { return (...args) => {
const { hash } = args.slice(-1)[0] as Handlebars.HelperOptions; const { hash } = args.slice(-1)[0] as HelperOptions;
const hasHash = Object.keys(hash).length > 0; const hasHash = Object.keys(hash).length > 0;
const hasValues = args.length > 1; const hasValues = args.length > 1;
if (hasHash && hasValues) { if (hasHash && hasValues) {
@ -49,7 +49,7 @@ handlebars.registerHelper(
handlebars.registerHelper('date', (...args) => { handlebars.registerHelper('date', (...args) => {
const values = args.slice(0, -1) as [string | Date, string | undefined]; const values = args.slice(0, -1) as [string | Date, string | undefined];
const { hash } = args.slice(-1)[0] as Handlebars.HelperOptions; const { hash } = args.slice(-1)[0] as HelperOptions;
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let [date, format] = values; let [date, format] = values;
if (typeof date === 'undefined') throw new Error(`[date]: unknown variable`); if (typeof date === 'undefined') throw new Error(`[date]: unknown variable`);

View file

@ -7,7 +7,12 @@
*/ */
import { encode } from '@kbn/rison'; import { encode } from '@kbn/rison';
import Handlebars, { type ExtendedCompileOptions, compileFnName } from '@kbn/handlebars'; import Handlebars, {
type CompileOptions,
type HelperOptions,
type HelperDelegate,
compileFnName,
} from '@kbn/handlebars';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { emptyLabel } from '../../../../common/empty_label'; import { emptyLabel } from '../../../../common/empty_label';
@ -16,9 +21,9 @@ const handlebars = Handlebars.create();
function createSerializationHelper( function createSerializationHelper(
fnName: string, fnName: string,
serializeFn: (value: unknown) => string serializeFn: (value: unknown) => string
): Handlebars.HelperDelegate { ): HelperDelegate {
return (...args) => { return (...args) => {
const { hash } = args.slice(-1)[0] as Handlebars.HelperOptions; const { hash } = args.slice(-1)[0] as HelperOptions;
const hasHash = Object.keys(hash).length > 0; const hasHash = Object.keys(hash).length > 0;
const hasValues = args.length > 1; const hasValues = args.length > 1;
if (hasHash && hasValues) { if (hasHash && hasValues) {
@ -53,7 +58,7 @@ export function replaceVars(
str: string, str: string,
args: Record<string, unknown> = {}, args: Record<string, unknown> = {},
vars: Record<string, unknown> = {}, vars: Record<string, unknown> = {},
compileOptions: Partial<ExtendedCompileOptions> = {} compileOptions: Partial<CompileOptions> = {}
) { ) {
try { try {
/** we need add '[]' for emptyLabel because this value contains special characters. /** we need add '[]' for emptyLabel because this value contains special characters.