mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.6`: - [[@kbn/handlebars] Add support for partials (#150151)](https://github.com/elastic/kibana/pull/150151) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Thomas Watson","email":"watson@elastic.co"},"sourceCommit":{"committedDate":"2023-02-02T19:59:09Z","message":"[@kbn/handlebars] Add support for partials (#150151)\n\nAdd support for [partials](https://handlebarsjs.com/guide/partials.html)\r\nto our own implementation of the [handlebars](https://handlebarsjs.com)\r\ntemplate engine.\r\n\r\nCloses #139068","sha":"2b82cb7fa24e019a3717b60abbe0f814b5ddcd5a","branchLabelMapping":{"^v8.7.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","backport:prev-minor","v8.7.0"],"number":150151,"url":"https://github.com/elastic/kibana/pull/150151","mergeCommit":{"message":"[@kbn/handlebars] Add support for partials (#150151)\n\nAdd support for [partials](https://handlebarsjs.com/guide/partials.html)\r\nto our own implementation of the [handlebars](https://handlebarsjs.com)\r\ntemplate engine.\r\n\r\nCloses #139068","sha":"2b82cb7fa24e019a3717b60abbe0f814b5ddcd5a"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.7.0","labelRegex":"^v8.7.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/150151","number":150151,"mergeCommit":{"message":"[@kbn/handlebars] Add support for partials (#150151)\n\nAdd support for [partials](https://handlebarsjs.com/guide/partials.html)\r\nto our own implementation of the [handlebars](https://handlebarsjs.com)\r\ntemplate engine.\r\n\r\nCloses #139068","sha":"2b82cb7fa24e019a3717b60abbe0f814b5ddcd5a"}}]}] BACKPORT--> Co-authored-by: Thomas Watson <watson@elastic.co>
This commit is contained in:
parent
6c767922d8
commit
0659916073
9 changed files with 866 additions and 9 deletions
|
@ -28,9 +28,9 @@ Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Co
|
|||
`csp.disableUnsafeEval`::
|
||||
experimental[] Set this to `true` to remove the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#unsafe_eval_expressions[`unsafe-eval`] source expression from the `script-src` directive. *Default: `false`*
|
||||
+
|
||||
By enabling `csp.disableUnsafeEval`, Kibana will use a custom version of the Handlebars template library which doesn't support https://handlebarsjs.com/guide/partials.html#inline-partials[inline partials].
|
||||
By enabling `csp.disableUnsafeEval`, Kibana will use a custom version of the Handlebars template library.
|
||||
Handlebars is used in various locations in the Kibana frontend where custom templates can be supplied by the user when for instance setting up a visualisation.
|
||||
If you experience any issues rendering Handlebars templates after turning on `csp.disableUnsafeEval`, or if you rely on inline partials, please revert this setting to `false` and https://github.com/elastic/kibana/issues/new/choose[open an issue] in the Kibana GitHub repository.
|
||||
If you experience any issues rendering Handlebars templates after turning on `csp.disableUnsafeEval`, please revert this setting to `false` and https://github.com/elastic/kibana/issues/new/choose[open an issue] in the Kibana GitHub repository.
|
||||
|
||||
`csp.worker_src`::
|
||||
Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/worker-src[Content Security Policy `worker-src` directive].
|
||||
|
|
|
@ -11,15 +11,16 @@ A custom version of the handlebars package which, to improve security, does not
|
|||
- `noEscape`
|
||||
- `strict`
|
||||
- `assumeObjects`
|
||||
- `preventIndent`
|
||||
- `explicitPartialContext`
|
||||
|
||||
- Only the following runtime options are supported:
|
||||
- `data`
|
||||
- `helpers`
|
||||
- `partials`
|
||||
- `decorators` (not documented in the official Handlebars [runtime options documentation](https://handlebarsjs.com/api-reference/runtime-options.html))
|
||||
- `blockParams` (not documented in the official Handlebars [runtime options documentation](https://handlebarsjs.com/api-reference/runtime-options.html))
|
||||
|
||||
The [Inline partials](https://handlebarsjs.com/guide/partials.html#inline-partials) handlebars template feature is currently not supported by `@kbn/handlebars`.
|
||||
|
||||
## Implementation differences
|
||||
|
||||
The standard `handlebars` implementation:
|
||||
|
|
|
@ -432,6 +432,7 @@ describe('blocks', () => {
|
|||
"decorator": [Function],
|
||||
},
|
||||
"helpers": Object {},
|
||||
"partials": Object {},
|
||||
}
|
||||
`);
|
||||
return `hello ${context.me} ${fn()}!`;
|
||||
|
|
|
@ -35,6 +35,7 @@ declare module 'handlebars' {
|
|||
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 {
|
||||
|
@ -47,6 +48,8 @@ declare module 'handlebars' {
|
|||
// 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
|
||||
}
|
||||
|
||||
const kHelper = Symbol('helper');
|
||||
|
@ -56,7 +59,10 @@ type NodeType = typeof kHelper | typeof kAmbiguous | typeof kSimple;
|
|||
|
||||
type LookupProperty = <T = any>(parent: { [name: string]: any }, propertyName: string) => T;
|
||||
|
||||
type ProcessableStatementNode = hbs.AST.MustacheStatement | hbs.AST.SubExpression;
|
||||
type ProcessableStatementNode =
|
||||
| hbs.AST.MustacheStatement
|
||||
| hbs.AST.PartialStatement
|
||||
| hbs.AST.SubExpression;
|
||||
type ProcessableBlockStatementNode = hbs.AST.BlockStatement | hbs.AST.PartialBlockStatement;
|
||||
type ProcessableNode = ProcessableStatementNode | ProcessableBlockStatementNode;
|
||||
type ProcessableNodeWithPathParts = ProcessableNode & { path: hbs.AST.PathExpression };
|
||||
|
@ -96,7 +102,14 @@ export const compileFnName: 'compile' | 'compileAST' = allowUnsafeEval() ? 'comp
|
|||
*/
|
||||
export type ExtendedCompileOptions = Pick<
|
||||
CompileOptions,
|
||||
'data' | 'knownHelpers' | 'knownHelpersOnly' | 'noEscape' | 'strict' | 'assumeObjects'
|
||||
| 'data'
|
||||
| 'knownHelpers'
|
||||
| 'knownHelpersOnly'
|
||||
| 'noEscape'
|
||||
| 'strict'
|
||||
| 'assumeObjects'
|
||||
| 'preventIndent'
|
||||
| 'explicitPartialContext'
|
||||
>;
|
||||
|
||||
/**
|
||||
|
@ -107,7 +120,7 @@ export type ExtendedCompileOptions = Pick<
|
|||
*/
|
||||
export type ExtendedRuntimeOptions = Pick<
|
||||
RuntimeOptions,
|
||||
'data' | 'helpers' | 'decorators' | 'blockParams'
|
||||
'data' | 'helpers' | 'partials' | 'decorators' | 'blockParams'
|
||||
>;
|
||||
|
||||
/**
|
||||
|
@ -127,6 +140,10 @@ export interface HelpersHash {
|
|||
[name: string]: Handlebars.HelperDelegate;
|
||||
}
|
||||
|
||||
export interface PartialsHash {
|
||||
[name: string]: HandlebarsTemplateDelegate;
|
||||
}
|
||||
|
||||
export interface DecoratorsHash {
|
||||
[name: string]: DecoratorFunction;
|
||||
}
|
||||
|
@ -173,15 +190,17 @@ Handlebars.compileAST = function (
|
|||
|
||||
// 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(input, options, helpers, decorators);
|
||||
const visitor = new ElasticHandlebarsVisitor(this, input, options, helpers, partials, decorators);
|
||||
return (context: any, runtimeOptions?: ExtendedRuntimeOptions) =>
|
||||
visitor.render(context, runtimeOptions);
|
||||
};
|
||||
|
||||
interface Container {
|
||||
helpers: HelpersHash;
|
||||
partials: PartialsHash;
|
||||
decorators: DecoratorsHash;
|
||||
strict: (obj: { [name: string]: any }, name: string, loc: hbs.AST.SourceLocation) => any;
|
||||
lookupProperty: LookupProperty;
|
||||
|
@ -194,12 +213,14 @@ interface Container {
|
|||
}
|
||||
|
||||
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[][] = [];
|
||||
|
@ -210,13 +231,17 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor {
|
|||
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 {
|
||||
|
@ -246,12 +271,14 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor {
|
|||
);
|
||||
|
||||
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)) {
|
||||
|
@ -299,6 +326,7 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor {
|
|||
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
|
||||
|
@ -379,6 +407,14 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor {
|
|||
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.
|
||||
|
@ -631,6 +667,95 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor {
|
|||
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 as hbs.AST.PartialStatement).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);
|
||||
|
@ -747,6 +872,9 @@ class ElasticHandlebarsVisitor extends Handlebars.Visitor {
|
|||
);
|
||||
}
|
||||
|
||||
// inherit partials from parent program
|
||||
runtimeOptions.partials = runtimeOptions.partials || this.runtimeOptions!.partials;
|
||||
|
||||
// stash parent program data
|
||||
const tmpRuntimeOptions = this.runtimeOptions;
|
||||
this.runtimeOptions = runtimeOptions;
|
||||
|
|
|
@ -42,6 +42,7 @@ class HandlebarsTestBench {
|
|||
private compileOptions?: ExtendedCompileOptions;
|
||||
private runtimeOptions?: ExtendedRuntimeOptions;
|
||||
private helpers: { [name: string]: Handlebars.HelperDelegate | undefined } = {};
|
||||
private partials: { [name: string]: Handlebars.Template } = {};
|
||||
private decorators: DecoratorsHash = {};
|
||||
private input: any = {};
|
||||
|
||||
|
@ -82,6 +83,18 @@ class HandlebarsTestBench {
|
|||
return this;
|
||||
}
|
||||
|
||||
withPartial(name: string | number, partial: Handlebars.Template) {
|
||||
this.partials[name] = partial;
|
||||
return this;
|
||||
}
|
||||
|
||||
withPartials(partials: { [name: string]: Handlebars.Template }) {
|
||||
for (const [name, partial] of Object.entries(partials)) {
|
||||
this.withPartial(name, partial);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
withDecorator(name: string, decoratorFunction: DecoratorFunction) {
|
||||
this.decorators[name] = decoratorFunction;
|
||||
return this;
|
||||
|
@ -148,6 +161,7 @@ class HandlebarsTestBench {
|
|||
const runtimeOptions: ExtendedRuntimeOptions = Object.assign(
|
||||
{
|
||||
helpers: this.helpers,
|
||||
partials: this.partials,
|
||||
decorators: this.decorators,
|
||||
},
|
||||
this.runtimeOptions
|
||||
|
@ -164,6 +178,7 @@ class HandlebarsTestBench {
|
|||
const runtimeOptions: ExtendedRuntimeOptions = Object.assign(
|
||||
{
|
||||
helpers: this.helpers,
|
||||
partials: this.partials,
|
||||
decorators: this.decorators,
|
||||
},
|
||||
this.runtimeOptions
|
||||
|
|
|
@ -134,6 +134,18 @@ describe('data', () => {
|
|||
.toCompileTo('2hello world1');
|
||||
});
|
||||
|
||||
it('passing in data to a compiled function that expects data - works with helpers in partials', () => {
|
||||
expectTemplate('{{>myPartial}}')
|
||||
.withCompileOptions({ data: true })
|
||||
.withPartial('myPartial', '{{hello}}')
|
||||
.withHelper('hello', function (this: any, options: Handlebars.HelperOptions) {
|
||||
return options.data.adjective + ' ' + this.noun;
|
||||
})
|
||||
.withInput({ noun: 'cat' })
|
||||
.withRuntimeOptions({ data: { adjective: 'happy' } })
|
||||
.toCompileTo('happy cat');
|
||||
});
|
||||
|
||||
it('passing in data to a compiled function that expects data - works with helpers and parameters', () => {
|
||||
expectTemplate('{{hello world}}')
|
||||
.withCompileOptions({ data: true })
|
||||
|
|
593
packages/kbn-handlebars/src/spec/index.partials.test.ts
Normal file
593
packages/kbn-handlebars/src/spec/index.partials.test.ts
Normal file
|
@ -0,0 +1,593 @@
|
|||
/*
|
||||
* This file is forked from the handlebars project (https://github.com/handlebars-lang/handlebars.js),
|
||||
* and may include modifications made by Elasticsearch B.V.
|
||||
* Elasticsearch B.V. licenses this file to you under the MIT License.
|
||||
* See `packages/kbn-handlebars/LICENSE` for more information.
|
||||
*/
|
||||
|
||||
import Handlebars from '../..';
|
||||
import { expectTemplate, forEachCompileFunctionName } from '../__jest__/test_bench';
|
||||
|
||||
describe('partials', () => {
|
||||
it('basic partials', () => {
|
||||
const string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
|
||||
const partial = '{{name}} ({{url}}) ';
|
||||
const hash = {
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
};
|
||||
|
||||
expectTemplate(string)
|
||||
.withInput(hash)
|
||||
.withPartials({ dude: partial })
|
||||
.toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
|
||||
|
||||
expectTemplate(string)
|
||||
.withInput(hash)
|
||||
.withPartials({ dude: partial })
|
||||
.withRuntimeOptions({ data: false })
|
||||
.withCompileOptions({ data: false })
|
||||
.toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
|
||||
});
|
||||
|
||||
it('dynamic partials', () => {
|
||||
const string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}';
|
||||
const partial = '{{name}} ({{url}}) ';
|
||||
const hash = {
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
};
|
||||
const helpers = {
|
||||
partial: () => 'dude',
|
||||
};
|
||||
|
||||
expectTemplate(string)
|
||||
.withInput(hash)
|
||||
.withHelpers(helpers)
|
||||
.withPartials({ dude: partial })
|
||||
.toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
|
||||
|
||||
expectTemplate(string)
|
||||
.withInput(hash)
|
||||
.withHelpers(helpers)
|
||||
.withPartials({ dude: partial })
|
||||
.withRuntimeOptions({ data: false })
|
||||
.withCompileOptions({ data: false })
|
||||
.toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
|
||||
});
|
||||
|
||||
it('failing dynamic partials', () => {
|
||||
expectTemplate('Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}')
|
||||
.withInput({
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
})
|
||||
.withHelper('partial', () => 'missing')
|
||||
.withPartial('dude', '{{name}} ({{url}}) ')
|
||||
.toThrow('The partial missing could not be found'); // TODO: Is there a way we can test that the error is of type `Handlebars.Exception`?
|
||||
});
|
||||
|
||||
it('partials with context', () => {
|
||||
expectTemplate('Dudes: {{>dude dudes}}')
|
||||
.withInput({
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
})
|
||||
.withPartial('dude', '{{#this}}{{name}} ({{url}}) {{/this}}')
|
||||
.toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
|
||||
});
|
||||
|
||||
it('partials with no context', () => {
|
||||
const partial = '{{name}} ({{url}}) ';
|
||||
const hash = {
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
};
|
||||
|
||||
expectTemplate('Dudes: {{#dudes}}{{>dude}}{{/dudes}}')
|
||||
.withInput(hash)
|
||||
.withPartial('dude', partial)
|
||||
.withCompileOptions({ explicitPartialContext: true })
|
||||
.toCompileTo('Dudes: () () ');
|
||||
|
||||
expectTemplate('Dudes: {{#dudes}}{{>dude name="foo"}}{{/dudes}}')
|
||||
.withInput(hash)
|
||||
.withPartial('dude', partial)
|
||||
.withCompileOptions({ explicitPartialContext: true })
|
||||
.toCompileTo('Dudes: foo () foo () ');
|
||||
});
|
||||
|
||||
it('partials with string context', () => {
|
||||
expectTemplate('Dudes: {{>dude "dudes"}}')
|
||||
.withPartial('dude', '{{.}}')
|
||||
.toCompileTo('Dudes: dudes');
|
||||
});
|
||||
|
||||
it('partials with undefined context', () => {
|
||||
expectTemplate('Dudes: {{>dude dudes}}')
|
||||
.withPartial('dude', '{{foo}} Empty')
|
||||
.toCompileTo('Dudes: Empty');
|
||||
});
|
||||
|
||||
it('partials with duplicate parameters', () => {
|
||||
expectTemplate('Dudes: {{>dude dudes foo bar=baz}}').toThrow(
|
||||
'Unsupported number of partial arguments: 2 - 1:7'
|
||||
);
|
||||
});
|
||||
|
||||
it('partials with parameters', () => {
|
||||
expectTemplate('Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}')
|
||||
.withInput({
|
||||
foo: 'bar',
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
})
|
||||
.withPartial('dude', '{{others.foo}}{{name}} ({{url}}) ')
|
||||
.toCompileTo('Dudes: barYehuda (http://yehuda) barAlan (http://alan) ');
|
||||
});
|
||||
|
||||
it('partial in a partial', () => {
|
||||
expectTemplate('Dudes: {{#dudes}}{{>dude}}{{/dudes}}')
|
||||
.withInput({
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
})
|
||||
.withPartials({
|
||||
dude: '{{name}} {{> url}} ',
|
||||
url: '<a href="{{url}}">{{url}}</a>',
|
||||
})
|
||||
.toCompileTo(
|
||||
'Dudes: Yehuda <a href="http://yehuda">http://yehuda</a> Alan <a href="http://alan">http://alan</a> '
|
||||
);
|
||||
});
|
||||
|
||||
it('rendering undefined partial throws an exception', () => {
|
||||
expectTemplate('{{> whatever}}').toThrow('The partial whatever could not be found');
|
||||
});
|
||||
|
||||
it('registering undefined partial throws an exception', () => {
|
||||
global.kbnHandlebarsEnv = Handlebars.create();
|
||||
|
||||
expect(() => {
|
||||
const undef: unknown = undefined;
|
||||
kbnHandlebarsEnv!.registerPartial('undefined_test', undef as Handlebars.Template);
|
||||
}).toThrow('Attempting to register a partial called "undefined_test" as undefined');
|
||||
|
||||
global.kbnHandlebarsEnv = null;
|
||||
});
|
||||
|
||||
it('rendering template partial in vm mode throws an exception', () => {
|
||||
expectTemplate('{{> whatever}}').toThrow('The partial whatever could not be found');
|
||||
});
|
||||
|
||||
it('rendering function partial in vm mode', () => {
|
||||
function partial(context: any) {
|
||||
return context.name + ' (' + context.url + ') ';
|
||||
}
|
||||
expectTemplate('Dudes: {{#dudes}}{{> dude}}{{/dudes}}')
|
||||
.withInput({
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
})
|
||||
.withPartial('dude', partial)
|
||||
.toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
|
||||
});
|
||||
|
||||
it('GH-14: a partial preceding a selector', () => {
|
||||
expectTemplate('Dudes: {{>dude}} {{anotherDude}}')
|
||||
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
|
||||
.withPartial('dude', '{{name}}')
|
||||
.toCompileTo('Dudes: Jeepers Creepers');
|
||||
});
|
||||
|
||||
it('Partials with slash paths', () => {
|
||||
expectTemplate('Dudes: {{> shared/dude}}')
|
||||
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
|
||||
.withPartial('shared/dude', '{{name}}')
|
||||
.toCompileTo('Dudes: Jeepers');
|
||||
});
|
||||
|
||||
it('Partials with slash and point paths', () => {
|
||||
expectTemplate('Dudes: {{> shared/dude.thing}}')
|
||||
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
|
||||
.withPartial('shared/dude.thing', '{{name}}')
|
||||
.toCompileTo('Dudes: Jeepers');
|
||||
});
|
||||
|
||||
it('Global Partials', () => {
|
||||
global.kbnHandlebarsEnv = Handlebars.create();
|
||||
|
||||
kbnHandlebarsEnv!.registerPartial('globalTest', '{{anotherDude}}');
|
||||
|
||||
expectTemplate('Dudes: {{> shared/dude}} {{> globalTest}}')
|
||||
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
|
||||
.withPartial('shared/dude', '{{name}}')
|
||||
.toCompileTo('Dudes: Jeepers Creepers');
|
||||
|
||||
kbnHandlebarsEnv!.unregisterPartial('globalTest');
|
||||
expect(kbnHandlebarsEnv!.partials.globalTest).toBeUndefined();
|
||||
|
||||
global.kbnHandlebarsEnv = null;
|
||||
});
|
||||
|
||||
it('Multiple partial registration', () => {
|
||||
global.kbnHandlebarsEnv = Handlebars.create();
|
||||
|
||||
kbnHandlebarsEnv!.registerPartial({
|
||||
'shared/dude': '{{name}}',
|
||||
globalTest: '{{anotherDude}}',
|
||||
});
|
||||
|
||||
expectTemplate('Dudes: {{> shared/dude}} {{> globalTest}}')
|
||||
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
|
||||
.withPartial('notused', 'notused') // trick the test bench into running with partials enabled
|
||||
.toCompileTo('Dudes: Jeepers Creepers');
|
||||
|
||||
global.kbnHandlebarsEnv = null;
|
||||
});
|
||||
|
||||
it('Partials with integer path', () => {
|
||||
expectTemplate('Dudes: {{> 404}}')
|
||||
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
|
||||
.withPartial(404, '{{name}}')
|
||||
.toCompileTo('Dudes: Jeepers');
|
||||
});
|
||||
|
||||
it('Partials with complex path', () => {
|
||||
expectTemplate('Dudes: {{> 404/asdf?.bar}}')
|
||||
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
|
||||
.withPartial('404/asdf?.bar', '{{name}}')
|
||||
.toCompileTo('Dudes: Jeepers');
|
||||
});
|
||||
|
||||
it('Partials with escaped', () => {
|
||||
expectTemplate('Dudes: {{> [+404/asdf?.bar]}}')
|
||||
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
|
||||
.withPartial('+404/asdf?.bar', '{{name}}')
|
||||
.toCompileTo('Dudes: Jeepers');
|
||||
});
|
||||
|
||||
it('Partials with string', () => {
|
||||
expectTemplate("Dudes: {{> '+404/asdf?.bar'}}")
|
||||
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
|
||||
.withPartial('+404/asdf?.bar', '{{name}}')
|
||||
.toCompileTo('Dudes: Jeepers');
|
||||
});
|
||||
|
||||
it('should handle empty partial', () => {
|
||||
expectTemplate('Dudes: {{#dudes}}{{> dude}}{{/dudes}}')
|
||||
.withInput({
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
})
|
||||
.withPartial('dude', '')
|
||||
.toCompileTo('Dudes: ');
|
||||
});
|
||||
|
||||
// Skipping test as this only makes sense when there's no `compile` function (i.e. runtime-only mode).
|
||||
// We do not support that mode with `@kbn/handlebars`, so there's no need to test it
|
||||
it.skip('throw on missing partial', () => {
|
||||
const handlebars = Handlebars.create();
|
||||
(handlebars.compile as any) = undefined;
|
||||
const template = handlebars.precompile('{{> dude}}');
|
||||
const render = handlebars.template(eval('(' + template + ')')); // eslint-disable-line no-eval
|
||||
expect(() => {
|
||||
render(
|
||||
{},
|
||||
{
|
||||
partials: {
|
||||
// @ts-expect-error
|
||||
dude: 'fail',
|
||||
},
|
||||
}
|
||||
);
|
||||
}).toThrow(/The partial dude could not be compiled/);
|
||||
});
|
||||
|
||||
describe('partial blocks', () => {
|
||||
it('should render partial block as default', () => {
|
||||
expectTemplate('{{#> dude}}success{{/dude}}').toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should execute default block with proper context', () => {
|
||||
expectTemplate('{{#> dude context}}{{value}}{{/dude}}')
|
||||
.withInput({ context: { value: 'success' } })
|
||||
.toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should propagate block parameters to default block', () => {
|
||||
expectTemplate('{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}')
|
||||
.withInput({ context: { value: 'success' } })
|
||||
.toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should not use partial block if partial exists', () => {
|
||||
expectTemplate('{{#> dude}}fail{{/dude}}')
|
||||
.withPartials({ dude: 'success' })
|
||||
.toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should render block from partial', () => {
|
||||
expectTemplate('{{#> dude}}success{{/dude}}')
|
||||
.withPartials({ dude: '{{> @partial-block }}' })
|
||||
.toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should be able to render the partial-block twice', () => {
|
||||
expectTemplate('{{#> dude}}success{{/dude}}')
|
||||
.withPartials({ dude: '{{> @partial-block }} {{> @partial-block }}' })
|
||||
.toCompileTo('success success');
|
||||
});
|
||||
|
||||
it('should render block from partial with context', () => {
|
||||
expectTemplate('{{#> dude}}{{value}}{{/dude}}')
|
||||
.withInput({ context: { value: 'success' } })
|
||||
.withPartials({
|
||||
dude: '{{#with context}}{{> @partial-block }}{{/with}}',
|
||||
})
|
||||
.toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should be able to access the @data frame from a partial-block', () => {
|
||||
expectTemplate('{{#> dude}}in-block: {{@root/value}}{{/dude}}')
|
||||
.withInput({ value: 'success' })
|
||||
.withPartials({
|
||||
dude: '<code>before-block: {{@root/value}} {{> @partial-block }}</code>',
|
||||
})
|
||||
.toCompileTo('<code>before-block: success in-block: success</code>');
|
||||
});
|
||||
|
||||
it('should allow the #each-helper to be used along with partial-blocks', () => {
|
||||
expectTemplate('<template>{{#> list value}}value = {{.}}{{/list}}</template>')
|
||||
.withInput({
|
||||
value: ['a', 'b', 'c'],
|
||||
})
|
||||
.withPartials({
|
||||
list: '<list>{{#each .}}<item>{{> @partial-block}}</item>{{/each}}</list>',
|
||||
})
|
||||
.toCompileTo(
|
||||
'<template><list><item>value = a</item><item>value = b</item><item>value = c</item></list></template>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render block from partial with context (twice)', () => {
|
||||
expectTemplate('{{#> dude}}{{value}}{{/dude}}')
|
||||
.withInput({ context: { value: 'success' } })
|
||||
.withPartials({
|
||||
dude: '{{#with context}}{{> @partial-block }} {{> @partial-block }}{{/with}}',
|
||||
})
|
||||
.toCompileTo('success success');
|
||||
});
|
||||
|
||||
it('should render block from partial with context [2]', () => {
|
||||
expectTemplate('{{#> dude}}{{../context/value}}{{/dude}}')
|
||||
.withInput({ context: { value: 'success' } })
|
||||
.withPartials({
|
||||
dude: '{{#with context}}{{> @partial-block }}{{/with}}',
|
||||
})
|
||||
.toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should render block from partial with block params', () => {
|
||||
expectTemplate('{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}')
|
||||
.withInput({ context: { value: 'success' } })
|
||||
.withPartials({ dude: '{{> @partial-block }}' })
|
||||
.toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should render nested partial blocks', () => {
|
||||
expectTemplate('<template>{{#> outer}}{{value}}{{/outer}}</template>')
|
||||
.withInput({ value: 'success' })
|
||||
.withPartials({
|
||||
outer:
|
||||
'<outer>{{#> nested}}<outer-block>{{> @partial-block}}</outer-block>{{/nested}}</outer>',
|
||||
nested: '<nested>{{> @partial-block}}</nested>',
|
||||
})
|
||||
.toCompileTo(
|
||||
'<template><outer><nested><outer-block>success</outer-block></nested></outer></template>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render nested partial blocks at different nesting levels', () => {
|
||||
expectTemplate('<template>{{#> outer}}{{value}}{{/outer}}</template>')
|
||||
.withInput({ value: 'success' })
|
||||
.withPartials({
|
||||
outer:
|
||||
'<outer>{{#> nested}}<outer-block>{{> @partial-block}}</outer-block>{{/nested}}{{> @partial-block}}</outer>',
|
||||
nested: '<nested>{{> @partial-block}}</nested>',
|
||||
})
|
||||
.toCompileTo(
|
||||
'<template><outer><nested><outer-block>success</outer-block></nested>success</outer></template>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render nested partial blocks at different nesting levels (twice)', () => {
|
||||
expectTemplate('<template>{{#> outer}}{{value}}{{/outer}}</template>')
|
||||
.withInput({ value: 'success' })
|
||||
.withPartials({
|
||||
outer:
|
||||
'<outer>{{#> nested}}<outer-block>{{> @partial-block}} {{> @partial-block}}</outer-block>{{/nested}}{{> @partial-block}}+{{> @partial-block}}</outer>',
|
||||
nested: '<nested>{{> @partial-block}}</nested>',
|
||||
})
|
||||
.toCompileTo(
|
||||
'<template><outer><nested><outer-block>success success</outer-block></nested>success+success</outer></template>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render nested partial blocks (twice at each level)', () => {
|
||||
expectTemplate('<template>{{#> outer}}{{value}}{{/outer}}</template>')
|
||||
.withInput({ value: 'success' })
|
||||
.withPartials({
|
||||
outer:
|
||||
'<outer>{{#> nested}}<outer-block>{{> @partial-block}} {{> @partial-block}}</outer-block>{{/nested}}</outer>',
|
||||
nested: '<nested>{{> @partial-block}}{{> @partial-block}}</nested>',
|
||||
})
|
||||
.toCompileTo(
|
||||
'<template><outer>' +
|
||||
'<nested><outer-block>success success</outer-block><outer-block>success success</outer-block></nested>' +
|
||||
'</outer></template>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inline partials', () => {
|
||||
it('should define inline partials for template', () => {
|
||||
expectTemplate('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}').toCompileTo(
|
||||
'success'
|
||||
);
|
||||
});
|
||||
|
||||
it('should overwrite multiple partials in the same template', () => {
|
||||
expectTemplate(
|
||||
'{{#*inline "myPartial"}}fail{{/inline}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}'
|
||||
).toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should define inline partials for block', () => {
|
||||
expectTemplate(
|
||||
'{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}'
|
||||
).toCompileTo('success');
|
||||
|
||||
expectTemplate(
|
||||
'{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{/with}}{{> myPartial}}'
|
||||
).toThrow(/myPartial could not/);
|
||||
});
|
||||
|
||||
it('should override global partials', () => {
|
||||
expectTemplate('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}')
|
||||
.withPartials({
|
||||
myPartial: () => 'fail',
|
||||
})
|
||||
.toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should override template partials', () => {
|
||||
expectTemplate(
|
||||
'{{#*inline "myPartial"}}fail{{/inline}}{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}'
|
||||
).toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should override partials down the entire stack', () => {
|
||||
expectTemplate(
|
||||
'{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{#with .}}{{#with .}}{{> myPartial}}{{/with}}{{/with}}{{/with}}'
|
||||
).toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should define inline partials for partial call', () => {
|
||||
expectTemplate('{{#*inline "myPartial"}}success{{/inline}}{{> dude}}')
|
||||
.withPartials({ dude: '{{> myPartial }}' })
|
||||
.toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should define inline partials in partial block call', () => {
|
||||
expectTemplate('{{#> dude}}{{#*inline "myPartial"}}success{{/inline}}{{/dude}}')
|
||||
.withPartials({ dude: '{{> myPartial }}' })
|
||||
.toCompileTo('success');
|
||||
});
|
||||
|
||||
it('should render nested inline partials', () => {
|
||||
expectTemplate(
|
||||
'{{#*inline "outer"}}{{#>inner}}<outer-block>{{>@partial-block}}</outer-block>{{/inner}}{{/inline}}' +
|
||||
'{{#*inline "inner"}}<inner>{{>@partial-block}}</inner>{{/inline}}' +
|
||||
'{{#>outer}}{{value}}{{/outer}}'
|
||||
)
|
||||
.withInput({ value: 'success' })
|
||||
.toCompileTo('<inner><outer-block>success</outer-block></inner>');
|
||||
});
|
||||
|
||||
it('should render nested inline partials with partial-blocks on different nesting levels', () => {
|
||||
expectTemplate(
|
||||
'{{#*inline "outer"}}{{#>inner}}<outer-block>{{>@partial-block}}</outer-block>{{/inner}}{{>@partial-block}}{{/inline}}' +
|
||||
'{{#*inline "inner"}}<inner>{{>@partial-block}}</inner>{{/inline}}' +
|
||||
'{{#>outer}}{{value}}{{/outer}}'
|
||||
)
|
||||
.withInput({ value: 'success' })
|
||||
.toCompileTo('<inner><outer-block>success</outer-block></inner>success');
|
||||
});
|
||||
|
||||
it('should render nested inline partials (twice at each level)', () => {
|
||||
expectTemplate(
|
||||
'{{#*inline "outer"}}{{#>inner}}<outer-block>{{>@partial-block}} {{>@partial-block}}</outer-block>{{/inner}}{{/inline}}' +
|
||||
'{{#*inline "inner"}}<inner>{{>@partial-block}}{{>@partial-block}}</inner>{{/inline}}' +
|
||||
'{{#>outer}}{{value}}{{/outer}}'
|
||||
)
|
||||
.withInput({ value: 'success' })
|
||||
.toCompileTo(
|
||||
'<inner><outer-block>success success</outer-block><outer-block>success success</outer-block></inner>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
forEachCompileFunctionName((compileName) => {
|
||||
it(`should pass compiler flags for ${compileName} function`, () => {
|
||||
const env = Handlebars.create();
|
||||
env.registerPartial('partial', '{{foo}}');
|
||||
const compile = env[compileName].bind(env);
|
||||
const template = compile('{{foo}} {{> partial}}', { noEscape: true });
|
||||
expect(template({ foo: '<' })).toEqual('< <');
|
||||
});
|
||||
});
|
||||
|
||||
describe('standalone partials', () => {
|
||||
it('indented partials', () => {
|
||||
expectTemplate('Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}')
|
||||
.withInput({
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
})
|
||||
.withPartial('dude', '{{name}}\n')
|
||||
.toCompileTo('Dudes:\n Yehuda\n Alan\n');
|
||||
});
|
||||
|
||||
it('nested indented partials', () => {
|
||||
expectTemplate('Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}')
|
||||
.withInput({
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
})
|
||||
.withPartials({
|
||||
dude: '{{name}}\n {{> url}}',
|
||||
url: '{{url}}!\n',
|
||||
})
|
||||
.toCompileTo('Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n');
|
||||
});
|
||||
|
||||
it('prevent nested indented partials', () => {
|
||||
expectTemplate('Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}')
|
||||
.withInput({
|
||||
dudes: [
|
||||
{ name: 'Yehuda', url: 'http://yehuda' },
|
||||
{ name: 'Alan', url: 'http://alan' },
|
||||
],
|
||||
})
|
||||
.withPartials({
|
||||
dude: '{{name}}\n {{> url}}',
|
||||
url: '{{url}}!\n',
|
||||
})
|
||||
.withCompileOptions({ preventIndent: true })
|
||||
.toCompileTo('Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import Handlebars from '../..';
|
||||
import { expectTemplate } from '../__jest__/test_bench';
|
||||
import { expectTemplate, forEachCompileFunctionName } from '../__jest__/test_bench';
|
||||
|
||||
describe('Regressions', () => {
|
||||
it('GH-94: Cannot read property of undefined', () => {
|
||||
|
@ -201,6 +201,19 @@ describe('Regressions', () => {
|
|||
.toCompileTo('Key: \nKey: name\nKey: value\n');
|
||||
});
|
||||
|
||||
it('GH-1054: Should handle simple safe string responses', () => {
|
||||
expectTemplate('{{#wrap}}{{>partial}}{{/wrap}}')
|
||||
.withHelpers({
|
||||
wrap(options: Handlebars.HelperOptions) {
|
||||
return new Handlebars.SafeString(options.fn());
|
||||
},
|
||||
})
|
||||
.withPartials({
|
||||
partial: '{{#wrap}}<partial>{{/wrap}}',
|
||||
})
|
||||
.toCompileTo('<partial>');
|
||||
});
|
||||
|
||||
it('GH-1065: Sparse arrays', () => {
|
||||
const array = [];
|
||||
array[1] = 'foo';
|
||||
|
@ -229,6 +242,34 @@ describe('Regressions', () => {
|
|||
.toCompileTo('notfoundbat');
|
||||
});
|
||||
|
||||
it('should support multiple levels of inline partials', () => {
|
||||
expectTemplate('{{#> layout}}{{#*inline "subcontent"}}subcontent{{/inline}}{{/layout}}')
|
||||
.withPartials({
|
||||
doctype: 'doctype{{> content}}',
|
||||
layout: '{{#> doctype}}{{#*inline "content"}}layout{{> subcontent}}{{/inline}}{{/doctype}}',
|
||||
})
|
||||
.toCompileTo('doctypelayoutsubcontent');
|
||||
});
|
||||
|
||||
it('GH-1089: should support failover content in multiple levels of inline partials', () => {
|
||||
expectTemplate('{{#> layout}}{{/layout}}')
|
||||
.withPartials({
|
||||
doctype: 'doctype{{> content}}',
|
||||
layout:
|
||||
'{{#> doctype}}{{#*inline "content"}}layout{{#> subcontent}}subcontent{{/subcontent}}{{/inline}}{{/doctype}}',
|
||||
})
|
||||
.toCompileTo('doctypelayoutsubcontent');
|
||||
});
|
||||
|
||||
it('GH-1099: should support greater than 3 nested levels of inline partials', () => {
|
||||
expectTemplate('{{#> layout}}Outer{{/layout}}')
|
||||
.withPartials({
|
||||
layout: '{{#> inner}}Inner{{/inner}}{{> @partial-block }}',
|
||||
inner: '',
|
||||
})
|
||||
.toCompileTo('Outer');
|
||||
});
|
||||
|
||||
it('GH-1135 : Context handling within each iteration', () => {
|
||||
expectTemplate(
|
||||
'{{#each array}}\n' +
|
||||
|
@ -249,6 +290,19 @@ describe('Regressions', () => {
|
|||
.toCompileTo(' 1. IF: John--\n' + ' 2. MYIF: John==\n');
|
||||
});
|
||||
|
||||
it('GH-1186: Support block params for existing programs', () => {
|
||||
expectTemplate(
|
||||
'{{#*inline "test"}}{{> @partial-block }}{{/inline}}' +
|
||||
'{{#>test }}{{#each listOne as |item|}}{{ item }}{{/each}}{{/test}}' +
|
||||
'{{#>test }}{{#each listTwo as |item|}}{{ item }}{{/each}}{{/test}}'
|
||||
)
|
||||
.withInput({
|
||||
listOne: ['a'],
|
||||
listTwo: ['b'],
|
||||
})
|
||||
.toCompileTo('ab');
|
||||
});
|
||||
|
||||
it('GH-1319: "unless" breaks when "each" value equals "null"', () => {
|
||||
expectTemplate('{{#each list}}{{#unless ./prop}}parent={{../value}} {{/unless}}{{/each}}')
|
||||
.withInput({
|
||||
|
@ -258,6 +312,15 @@ describe('Regressions', () => {
|
|||
.toCompileTo('parent=parent parent=parent ');
|
||||
});
|
||||
|
||||
it('GH-1341: 4.0.7 release breaks {{#if @partial-block}} usage', () => {
|
||||
expectTemplate('template {{>partial}} template')
|
||||
.withPartials({
|
||||
partialWithBlock: '{{#if @partial-block}} block {{> @partial-block}} block {{/if}}',
|
||||
partial: '{{#> partialWithBlock}} partial {{/partialWithBlock}}',
|
||||
})
|
||||
.toCompileTo('template block partial block template');
|
||||
});
|
||||
|
||||
it('should allow hash with protected array names', () => {
|
||||
expectTemplate('{{helpa length="foo"}}')
|
||||
.withInput({ array: [1], name: 'John' })
|
||||
|
@ -269,6 +332,42 @@ describe('Regressions', () => {
|
|||
.toCompileTo('foo');
|
||||
});
|
||||
|
||||
describe('GH-1598: Performance degradation for partials since v4.3.0', () => {
|
||||
let newHandlebarsInstance: typeof Handlebars;
|
||||
let spy: jest.SpyInstance;
|
||||
beforeEach(() => {
|
||||
newHandlebarsInstance = Handlebars.create();
|
||||
});
|
||||
afterEach(() => {
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
forEachCompileFunctionName((compileName) => {
|
||||
it(`should only compile global partials once when calling #${compileName}`, () => {
|
||||
const compile = newHandlebarsInstance[compileName].bind(newHandlebarsInstance);
|
||||
let calls;
|
||||
switch (compileName) {
|
||||
case 'compile':
|
||||
spy = jest.spyOn(newHandlebarsInstance, 'template');
|
||||
calls = 3;
|
||||
break;
|
||||
case 'compileAST':
|
||||
spy = jest.spyOn(newHandlebarsInstance, 'compileAST');
|
||||
calls = 2;
|
||||
break;
|
||||
}
|
||||
newHandlebarsInstance.registerPartial({
|
||||
dude: 'I am a partial',
|
||||
});
|
||||
const string = 'Dudes: {{> dude}} {{> dude}}';
|
||||
compile(string)(); // This should compile template + partial once
|
||||
compile(string)(); // This should only compile template
|
||||
expect(spy).toHaveBeenCalledTimes(calls);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
expectTemplate('{{foo}}')
|
||||
|
|
|
@ -72,6 +72,14 @@ describe('whitespace control', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should strip whitespace around partials', () => {
|
||||
expectTemplate('foo {{~> dude~}} ').withPartials({ dude: 'bar' }).toCompileTo('foobar');
|
||||
expectTemplate('foo {{> dude~}} ').withPartials({ dude: 'bar' }).toCompileTo('foo bar');
|
||||
expectTemplate('foo {{> dude}} ').withPartials({ dude: 'bar' }).toCompileTo('foo bar ');
|
||||
expectTemplate('foo\n {{~> dude}} ').withPartials({ dude: 'bar' }).toCompileTo('foobar');
|
||||
expectTemplate('foo\n {{> dude}} ').withPartials({ dude: 'bar' }).toCompileTo('foo\n bar');
|
||||
});
|
||||
|
||||
it('should only strip whitespace once', () => {
|
||||
expectTemplate(' {{~foo~}} {{foo}} {{foo}} ')
|
||||
.withInput({ foo: 'bar' })
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue