diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2024-02-05 06:11:08 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-05 11:11:08 +0000 |
commit | 71c3aff53173cc83a96d7d2715b7918bdbc2d8a5 (patch) | |
tree | d13cc4a90ef26bb6476daca1edc1999208f4fadc /dev/lib | |
parent | 0e7531bc5b443461d7e76e20877464ccf48a3ef5 (diff) |
kbn-handlebars dependency update (#613)
* Update kbn-handlebars dependency
* Move handlebars dependency to dev
* Update package
* Update readme
* Update readme
* Ignore legal file
Diffstat (limited to 'dev/lib')
33 files changed, 1 insertions, 6818 deletions
diff --git a/dev/lib/handlebars.js b/dev/lib/handlebars.js index c99a3b13..82fa4552 100644 --- a/dev/lib/handlebars.js +++ b/dev/lib/handlebars.js @@ -14,5 +14,5 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -export {Handlebars} from './handlebars/src/handlebars.js'; +export {Handlebars} from 'yomitan-handlebars/src/handlebars.js'; diff --git a/dev/lib/handlebars/LICENSE b/dev/lib/handlebars/LICENSE deleted file mode 100644 index 5d971a17..00000000 --- a/dev/lib/handlebars/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -The MIT License (MIT) - -Copyright (c) Elasticsearch BV -Copyright (c) Copyright (C) 2011-2019 by Yehuda Katz - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at the following locations: - - https://github.com/handlebars-lang/handlebars.js - - https://github.com/elastic/kibana/tree/main/packages/kbn-handlebars diff --git a/dev/lib/handlebars/README.md b/dev/lib/handlebars/README.md deleted file mode 100644 index 1ec14ef4..00000000 --- a/dev/lib/handlebars/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# @kbn/handlebars - -A custom version of the handlebars package which, to improve security, does not use `eval` or `new Function`. This means that templates can't be compiled into JavaScript functions in advance and hence, rendering the templates is a lot slower. - -## Limitations - -- Only the following compile options are supported: - - - `data` - - `knownHelpers` - - `knownHelpersOnly` - - `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)) - -## Implementation differences - -The standard `handlebars` implementation: - -1. When given a template string, e.g. `Hello {{x}}`, return a "render" function which takes an "input" object, e.g. `{ x: 'World' }`. -1. The first time the "render" function is called the following happens: - 1. Turn the template string into an Abstract Syntax Tree (AST). - 1. Convert the AST into a hyper optimized JavaScript function which takes the input object as an argument. - 1. Call the generate JavaScript function with the given "input" object to produce and return the final output string (`Hello World`). -1. Subsequent calls to the "render" function will re-use the already generated JavaScript function. - -The custom `@kbn/handlebars` implementation: - -1. When given a template string, e.g. `Hello {{x}}`, return a "render" function which takes an "input" object, e.g. `{ x: 'World' }`. -1. The first time the "render" function is called the following happens: - 1. Turn the template string into an Abstract Syntax Tree (AST). - 1. Process the AST with the given "input" object to produce and return the final output string (`Hello World`). -1. Subsequent calls to the "render" function will re-use the already generated AST. - -_Note: Not parsing of the template string until the first call to the "render" function is deliberate as it mimics the original `handlebars` implementation. This means that any errors that occur due to an invalid template string will not be thrown until the first call to the "render" function._ - -## Technical details - -The `handlebars` library exposes the API for both [generating the AST](https://github.com/handlebars-lang/handlebars.js/blob/master/docs/compiler-api.md#ast) and walking it by implementing the [Visitor API](https://github.com/handlebars-lang/handlebars.js/blob/master/docs/compiler-api.md#ast-visitor). We can leverage that to our advantage and create our own "render" function, which internally calls this API to generate the AST and then the API to walk the AST. - -The `@kbn/handlebars` implementation of the `Visitor` class implements all the necessary methods called by the parent `Visitor` code when instructed to walk the AST. They all start with an upppercase letter, e.g. `MustacheStatement` or `SubExpression`. We call this class `ElasticHandlebarsVisitor`. - -To parse the template string to an AST representation, we call `Handlebars.parse(templateString)`, which returns an AST object. - -The AST object contains a bunch of nodes, one for each element of the template string, all arranged in a tree-like structure. The root of the AST object is a node of type `Program`. This is a special node, which we do not need to worry about, but each of its direct children has a type named like the method which will be called when the walking algorithm reaches that node, e.g. `ContentStatement` or `BlockStatement`. These are the methods that our `Visitor` implementation implements. - -To instruct our `ElasticHandlebarsVisitor` class to start walking the AST object, we call the `accept()` method inherited from the parent `Visitor` class with the main AST object. The `Visitor` will walk each node in turn that is directly attached to the root `Program` node. For each node it traverses, it will call the matching method in our `ElasticHandlebarsVisitor` class. - -To instruct the `Visitor` code to traverse any child nodes of a given node, our implementation needs to manually call `accept(childNode)`, `acceptArray(arrayOfChildNodes)`, `acceptKey(node, childKeyName)`, or `acceptRequired(node, childKeyName)` from within any of the "node" methods, otherwise the child nodes are ignored. - -### State - -We keep state internally in the `ElasticHandlebarsVisitor` object using the following private properties: - -- `contexts`: An array (stack) of `context` objects. In a simple template this array will always only contain a single element: The main `context` object. In more complicated scenarios, new `context` objects will be pushed and popped to and from the `contexts` stack as needed. -- `output`: An array containing the "rendered" output of each node (normally just one element per node). In the most simple template, this is simply joined together into a the final output string after the AST has been traversed. In more complicated templates, we use this array temporarily to collect parameters to give to helper functions (see the `getParams` function). - -## Testing - -The tests for `@kbn/handlebars` are integrated into the regular test suite of Kibana and are all jest tests. To run them all, simply do: - -```sh -node scripts/jest packages/kbn-handlebars -``` - -By default, each test will run both the original `handlebars` code and the modified `@kbn/handlebars` code to compare if the output of the two are identical. To isolate a test run to just one or the other, you can use the following environment variables: - -- `EVAL=1` - Set to only run the original `handlebars` implementation that uses `eval`. -- `AST=1` - Set to only run the modified `@kbn/handlebars` implementation that doesn't use `eval`. - -## Development - -Some of the tests have been copied from the upstream `handlebars` project and modified to fit our use-case, test-suite, and coding conventions. They are all located under the `packages/kbn-handlebars/src/spec` directory. To check if any of the copied files have received updates upstream that we might want to include in our copies, you can run the following script: - -```sh -./packages/kbn-handlebars/scripts/check_for_upstream_updates.sh -``` - -_Note: This will look for changes in the `4.x` branch of the `handlebars.js` repo only. Changes in the `master` branch are ignored._ - -Once all updates have been manually merged with our versions of the files, run the following script to "lock" us into the new updates: - -```sh -./packages/kbn-handlebars/scripts/update_upstream_git_hash.sh -``` - -This will update file `packages/kbn-handlebars/src/spec/.upstream_git_hash`. Make sure to commit changes to this file as well. - -## Debugging - -### Print AST - -To output the generated AST object structure in a somewhat readable form, use the following script: - -```sh -./packages/kbn-handlebars/scripts/print_ast.js -``` - -Example: - -```sh -./packages/kbn-handlebars/scripts/print_ast.js '{{value}}' -``` - -Output: - -```js -{ - type: 'Program', - body: [ - { - type: 'MustacheStatement', - path: { - type: 'PathExpression', - data: false, - depth: 0, - parts: [ 'value' ], - original: 'value' - }, - params: [], - hash: undefined, - escaped: true - } - ] -} -``` - -By default certain properties will be hidden in the output. -For more control over the output, check out the options by running the script without any arguments. - -### Print generated code - -It's possible to see the generated JavaScript code that `handlebars` create for a given template using the following command line tool: - -```sh -./node_modules/handlebars/print-script <template> [options] -``` - -Options: - -- `-v`: Enable verbose mode. - -Example: - -```sh -./node_modules/handlebars/print-script '{{value}}' -v -``` - -You can pretty print just the generated code using this command: - -```sh -./node_modules/handlebars/print-script '{{value}}' | \ - node -e 'process.stdin.on(`data`, c => console.log(`(${eval(`(${c})`).code})`))' | \ - npx prettier --write --stdin-filepath template.js | \ - npx cli-highlight -l javascript -``` diff --git a/dev/lib/handlebars/__snapshots__/index.test.ts.snap b/dev/lib/handlebars/__snapshots__/index.test.ts.snap deleted file mode 100644 index b9a8c27e..00000000 --- a/dev/lib/handlebars/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,91 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Handlebars.create 1`] = ` -HandlebarsEnvironment { - "AST": Object { - "helpers": Object { - "helperExpression": [Function], - "scopedId": [Function], - "simpleId": [Function], - }, - }, - "COMPILER_REVISION": 8, - "Compiler": [Function], - "Exception": [Function], - "HandlebarsEnvironment": [Function], - "JavaScriptCompiler": [Function], - "LAST_COMPATIBLE_COMPILER_REVISION": 7, - "Parser": Object { - "yy": Object {}, - }, - "REVISION_CHANGES": Object { - "1": "<= 1.0.rc.2", - "2": "== 1.0.0-rc.3", - "3": "== 1.0.0-rc.4", - "4": "== 1.x.x", - "5": "== 2.0.0-alpha.x", - "6": ">= 2.0.0-beta.1", - "7": ">= 4.0.0 <4.3.0", - "8": ">= 4.3.0", - }, - "SafeString": [Function], - "Utils": Object { - "__esModule": true, - "appendContextPath": [Function], - "blockParams": [Function], - "createFrame": [Function], - "escapeExpression": [Function], - "extend": [Function], - "indexOf": [Function], - "isArray": [Function], - "isEmpty": [Function], - "isFunction": [Function], - "toString": [Function], - }, - "VERSION": "4.7.7", - "VM": Object { - "__esModule": true, - "checkRevision": [Function], - "invokePartial": [Function], - "noop": [Function], - "resolvePartial": [Function], - "template": [Function], - "wrapProgram": [Function], - }, - "__esModule": true, - "compile": [Function], - "compileAST": [Function], - "createFrame": [Function], - "decorators": Object { - "inline": [Function], - }, - "escapeExpression": [Function], - "helpers": Object { - "blockHelperMissing": [Function], - "each": [Function], - "helperMissing": [Function], - "if": [Function], - "log": [Function], - "lookup": [Function], - "unless": [Function], - "with": [Function], - }, - "log": [Function], - "logger": Object { - "level": "info", - "log": [Function], - "lookupLevel": [Function], - "methodMap": Array [ - "debug", - "info", - "warn", - "error", - ], - }, - "parse": [Function], - "parseWithoutProcessing": [Function], - "partials": Object {}, - "precompile": [Function], - "template": [Function], -} -`; diff --git a/dev/lib/handlebars/index.test.ts b/dev/lib/handlebars/index.test.ts deleted file mode 100644 index ed607db1..00000000 --- a/dev/lib/handlebars/index.test.ts +++ /dev/null @@ -1,567 +0,0 @@ -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/kbn-handlebars/LICENSE` for more information. - */ - -/** - * ABOUT THIS FILE: - * - * This file is for tests not copied from the upstream handlebars project, but - * tests that we feel are needed in order to fully cover our use-cases. - */ - -import Handlebars from '.'; -import type { HelperOptions, TemplateDelegate } from './src/types'; -import { expectTemplate, forEachCompileFunctionName } from './src/__jest__/test_bench'; - -it('Handlebars.create', () => { - expect(Handlebars.create()).toMatchSnapshot(); -}); - -describe('Handlebars.compileAST', () => { - describe('compiler options', () => { - it('noEscape', () => { - expectTemplate('{{value}}').withInput({ value: '<foo>' }).toCompileTo('<foo>'); - - expectTemplate('{{value}}') - .withCompileOptions({ noEscape: false }) - .withInput({ value: '<foo>' }) - .toCompileTo('<foo>'); - - expectTemplate('{{value}}') - .withCompileOptions({ noEscape: true }) - .withInput({ value: '<foo>' }) - .toCompileTo('<foo>'); - }); - }); - - it('invalid template', () => { - expectTemplate('{{value').withInput({ value: 42 }).toThrow(`Parse error on line 1: -{{value ---^ -Expecting 'ID', 'STRING', 'NUMBER', 'BOOLEAN', 'UNDEFINED', 'NULL', 'DATA', got 'INVALID'`); - }); - - if (!process.env.EVAL) { - it('reassign', () => { - const fn = Handlebars.compileAST; - expect(fn('{{value}}')({ value: 42 })).toEqual('42'); - }); - } -}); - -// Extra "helpers" tests -describe('helpers', () => { - it('Only provide options.fn/inverse to block helpers', () => { - function toHaveProperties(...args: any[]) { - toHaveProperties.calls++; - const options = args[args.length - 1]; - expect(options).toHaveProperty('fn'); - expect(options).toHaveProperty('inverse'); - return 42; - } - toHaveProperties.calls = 0; - - function toNotHaveProperties(...args: any[]) { - toNotHaveProperties.calls++; - const options = args[args.length - 1]; - expect(options).not.toHaveProperty('fn'); - expect(options).not.toHaveProperty('inverse'); - return 42; - } - toNotHaveProperties.calls = 0; - - const nonBlockTemplates = ['{{foo}}', '{{foo 1 2}}']; - const blockTemplates = ['{{#foo}}42{{/foo}}', '{{#foo 1 2}}42{{/foo}}']; - - for (const template of nonBlockTemplates) { - expectTemplate(template) - .withInput({ - foo: toNotHaveProperties, - }) - .toCompileTo('42'); - - expectTemplate(template).withHelper('foo', toNotHaveProperties).toCompileTo('42'); - } - - for (const template of blockTemplates) { - expectTemplate(template) - .withInput({ - foo: toHaveProperties, - }) - .toCompileTo('42'); - - expectTemplate(template).withHelper('foo', toHaveProperties).toCompileTo('42'); - } - - const factor = process.env.AST || process.env.EVAL ? 1 : 2; - expect(toNotHaveProperties.calls).toEqual(nonBlockTemplates.length * 2 * factor); - expect(toHaveProperties.calls).toEqual(blockTemplates.length * 2 * factor); - }); - - it('should pass expected "this" to helper functions (without input)', () => { - expectTemplate('{{hello "world" 12 true false}}') - .withHelper('hello', function (this: any, ...args: any[]) { - expect(this).toMatchInlineSnapshot(`Object {}`); - }) - .toCompileTo(''); - }); - - it('should pass expected "this" to helper functions (with input)', () => { - expectTemplate('{{hello "world" 12 true false}}') - .withHelper('hello', function (this: any, ...args: any[]) { - expect(this).toMatchInlineSnapshot(` - Object { - "people": Array [ - Object { - "id": 1, - "name": "Alan", - }, - Object { - "id": 2, - "name": "Yehuda", - }, - ], - } - `); - }) - .withInput({ - people: [ - { name: 'Alan', id: 1 }, - { name: 'Yehuda', id: 2 }, - ], - }) - .toCompileTo(''); - }); - - it('should pass expected "this" and arguments to helper functions (non-block helper)', () => { - expectTemplate('{{hello "world" 12 true false}}') - .withHelper('hello', function (this: any, ...args: any[]) { - expect(args).toMatchInlineSnapshot(` - Array [ - "world", - 12, - true, - false, - Object { - "data": Object { - "root": Object { - "people": Array [ - Object { - "id": 1, - "name": "Alan", - }, - Object { - "id": 2, - "name": "Yehuda", - }, - ], - }, - }, - "hash": Object {}, - "loc": Object { - "end": Object { - "column": 31, - "line": 1, - }, - "start": Object { - "column": 0, - "line": 1, - }, - }, - "lookupProperty": [Function], - "name": "hello", - }, - ] - `); - }) - .withInput({ - people: [ - { name: 'Alan', id: 1 }, - { name: 'Yehuda', id: 2 }, - ], - }) - .toCompileTo(''); - }); - - it('should pass expected "this" and arguments to helper functions (block helper)', () => { - expectTemplate('{{#hello "world" 12 true false}}{{/hello}}') - .withHelper('hello', function (this: any, ...args: any[]) { - expect(args).toMatchInlineSnapshot(` - Array [ - "world", - 12, - true, - false, - Object { - "data": Object { - "root": Object { - "people": Array [ - Object { - "id": 1, - "name": "Alan", - }, - Object { - "id": 2, - "name": "Yehuda", - }, - ], - }, - }, - "fn": [Function], - "hash": Object {}, - "inverse": [Function], - "loc": Object { - "end": Object { - "column": 42, - "line": 1, - }, - "start": Object { - "column": 0, - "line": 1, - }, - }, - "lookupProperty": [Function], - "name": "hello", - }, - ] - `); - }) - .withInput({ - people: [ - { name: 'Alan', id: 1 }, - { name: 'Yehuda', id: 2 }, - ], - }) - .toCompileTo(''); - }); -}); - -// Extra "blocks" tests -describe('blocks', () => { - describe('decorators', () => { - it('should only call decorator once', () => { - let calls = 0; - const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; - expectTemplate('{{#helper}}{{*decorator}}{{/helper}}') - .withHelper('helper', () => {}) - .withDecorator('decorator', () => { - calls++; - }) - .toCompileTo(''); - expect(calls).toEqual(callsExpected); - }); - - forEachCompileFunctionName((compileName) => { - it(`should call decorator again if render function is called again for #${compileName}`, () => { - global.kbnHandlebarsEnv = Handlebars.create(); - - kbnHandlebarsEnv!.registerDecorator('decorator', () => { - calls++; - }); - - const compile = kbnHandlebarsEnv![compileName].bind(kbnHandlebarsEnv); - const render = compile('{{*decorator}}'); - - let calls = 0; - expect(render()).toEqual(''); - expect(calls).toEqual(1); - - calls = 0; - expect(render()).toEqual(''); - expect(calls).toEqual(1); - - global.kbnHandlebarsEnv = null; - }); - }); - - it('should pass expected options to nested decorator', () => { - expectTemplate('{{#helper}}{{*decorator foo}}{{/helper}}') - .withHelper('helper', () => {}) - .withDecorator('decorator', function (fn, props, container, options) { - expect(options).toMatchInlineSnapshot(` - Object { - "args": Array [ - "bar", - ], - "data": Object { - "root": Object { - "foo": "bar", - }, - }, - "hash": Object {}, - "loc": Object { - "end": Object { - "column": 29, - "line": 1, - }, - "start": Object { - "column": 11, - "line": 1, - }, - }, - "name": "decorator", - } - `); - }) - .withInput({ foo: 'bar' }) - .toCompileTo(''); - }); - - it('should pass expected options to root decorator with no args', () => { - expectTemplate('{{*decorator}}') - .withDecorator('decorator', function (fn, props, container, options) { - expect(options).toMatchInlineSnapshot(` - Object { - "args": Array [], - "data": Object { - "root": Object { - "foo": "bar", - }, - }, - "hash": Object {}, - "loc": Object { - "end": Object { - "column": 14, - "line": 1, - }, - "start": Object { - "column": 0, - "line": 1, - }, - }, - "name": "decorator", - } - `); - }) - .withInput({ foo: 'bar' }) - .toCompileTo(''); - }); - - it('should pass expected options to root decorator with one arg', () => { - expectTemplate('{{*decorator foo}}') - .withDecorator('decorator', function (fn, props, container, options) { - expect(options).toMatchInlineSnapshot(` - Object { - "args": Array [ - undefined, - ], - "data": Object { - "root": Object { - "foo": "bar", - }, - }, - "hash": Object {}, - "loc": Object { - "end": Object { - "column": 18, - "line": 1, - }, - "start": Object { - "column": 0, - "line": 1, - }, - }, - "name": "decorator", - } - `); - }) - .withInput({ foo: 'bar' }) - .toCompileTo(''); - }); - - describe('return values', () => { - for (const [desc, template, result] of [ - ['non-block', '{{*decorator}}cont{{*decorator}}ent', 'content'], - ['block', '{{#*decorator}}con{{/decorator}}tent', 'tent'], - ]) { - describe(desc, () => { - const falsy = [undefined, null, false, 0, '']; - const truthy = [true, 42, 'foo', {}]; - - // Falsy return values from decorators are simply ignored and the - // execution falls back to default behavior which is to render the - // other parts of the template. - for (const value of falsy) { - it(`falsy value (type ${typeof value}): ${JSON.stringify(value)}`, () => { - expectTemplate(template) - .withDecorator('decorator', () => value) - .toCompileTo(result); - }); - } - - // Truthy return values from decorators are expected to be functions - // and the program will attempt to call them. We expect an error to - // be thrown in this case. - for (const value of truthy) { - it(`non-falsy value (type ${typeof value}): ${JSON.stringify(value)}`, () => { - expectTemplate(template) - .withDecorator('decorator', () => value) - .toThrow('is not a function'); - }); - } - - // If the decorator return value is a custom function, its return - // value will be the final content of the template. - for (const value of [...falsy, ...truthy]) { - it(`function returning ${typeof value}: ${JSON.stringify(value)}`, () => { - expectTemplate(template) - .withDecorator('decorator', () => () => value) - .toCompileTo(value as string); - }); - } - }); - } - }); - - describe('custom return function should be called with expected arguments and its return value should be rendered in the template', () => { - it('root decorator', () => { - expectTemplate('{{*decorator}}world') - .withInput({ me: 'my' }) - .withDecorator( - 'decorator', - (fn): TemplateDelegate => - (context, options) => { - expect(context).toMatchInlineSnapshot(` - Object { - "me": "my", - } - `); - expect(options).toMatchInlineSnapshot(` - Object { - "decorators": Object { - "decorator": [Function], - }, - "helpers": Object {}, - "partials": Object {}, - } - `); - return `hello ${context.me} ${fn()}!`; - } - ) - .toCompileTo('hello my world!'); - }); - - it('decorator nested inside of array-helper', () => { - expectTemplate('{{#arr}}{{*decorator}}world{{/arr}}') - .withInput({ arr: ['my'] }) - .withDecorator( - 'decorator', - (fn): TemplateDelegate => - (context, options) => { - expect(context).toMatchInlineSnapshot(`"my"`); - expect(options).toMatchInlineSnapshot(` - Object { - "blockParams": Array [ - "my", - 0, - ], - "data": Object { - "_parent": Object { - "root": Object { - "arr": Array [ - "my", - ], - }, - }, - "first": true, - "index": 0, - "key": 0, - "last": true, - "root": Object { - "arr": Array [ - "my", - ], - }, - }, - } - `); - return `hello ${context} ${fn()}!`; - } - ) - .toCompileTo('hello my world!'); - }); - - it('decorator nested inside of custom helper', () => { - expectTemplate('{{#helper}}{{*decorator}}world{{/helper}}') - .withHelper('helper', function (options: HelperOptions) { - return options.fn('my', { foo: 'bar' } as any); - }) - .withDecorator( - 'decorator', - (fn): TemplateDelegate => - (context, options) => { - expect(context).toMatchInlineSnapshot(`"my"`); - expect(options).toMatchInlineSnapshot(` - Object { - "foo": "bar", - } - `); - return `hello ${context} ${fn()}!`; - } - ) - .toCompileTo('hello my world!'); - }); - }); - - it('should call multiple decorators in the same program body in the expected order and get the expected output', () => { - let decoratorCall = 0; - let progCall = 0; - expectTemplate('{{*decorator}}con{{*decorator}}tent', { - beforeRender() { - // ensure the counters are reset between EVAL/AST render calls - decoratorCall = 0; - progCall = 0; - }, - }) - .withInput({ - decoratorCall: 0, - progCall: 0, - }) - .withDecorator('decorator', (fn) => { - const decoratorCallOrder = ++decoratorCall; - const ret: TemplateDelegate = () => { - const progCallOrder = ++progCall; - return `(decorator: ${decoratorCallOrder}, prog: ${progCallOrder}, fn: "${fn()}")`; - }; - return ret; - }) - .toCompileTo('(decorator: 2, prog: 1, fn: "(decorator: 1, prog: 2, fn: "content")")'); - }); - - describe('registration', () => { - beforeEach(() => { - global.kbnHandlebarsEnv = Handlebars.create(); - }); - - afterEach(() => { - global.kbnHandlebarsEnv = null; - }); - - it('should be able to call decorators registered using the `registerDecorator` function', () => { - let calls = 0; - const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; - - kbnHandlebarsEnv!.registerDecorator('decorator', () => { - calls++; - }); - - expectTemplate('{{*decorator}}').toCompileTo(''); - expect(calls).toEqual(callsExpected); - }); - - it('should not be able to call decorators unregistered using the `unregisterDecorator` function', () => { - let calls = 0; - - kbnHandlebarsEnv!.registerDecorator('decorator', () => { - calls++; - }); - - kbnHandlebarsEnv!.unregisterDecorator('decorator'); - - expectTemplate('{{*decorator}}').toThrow('lookupProperty(...) is not a function'); - expect(calls).toEqual(0); - }); - }); - }); -}); diff --git a/dev/lib/handlebars/index.ts b/dev/lib/handlebars/index.ts deleted file mode 100644 index 16030445..00000000 --- a/dev/lib/handlebars/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/kbn-handlebars/LICENSE` for more information. - */ - -import { Handlebars } from './src/handlebars'; -import { allowUnsafeEval } from './src/utils'; - -// The handlebars module uses `export =`, so it can't be re-exported using `export *`. -// However, because of Babel, we're not allowed to use `export =` ourselves. -// So we have to resort to using `exports default` even though eslint doesn't like it. -// -// eslint-disable-next-line import/no-default-export -globalThis.Handlebars = Handlebars; - -/** - * If the `unsafe-eval` CSP is set, this string constant will be `compile`, - * otherwise `compileAST`. - * - * This can be used to call the more optimized `compile` function in - * environments that support it, or fall back to `compileAST` on environments - * that don't. - */ -globalThis.handlebarsCompileFnName = allowUnsafeEval() ? 'compile' : 'compileAST'; - -export type { - CompileOptions, - RuntimeOptions, - HelperDelegate, - TemplateDelegate, - DecoratorDelegate, - HelperOptions, -} from './src/types'; diff --git a/dev/lib/handlebars/jest.config.js b/dev/lib/handlebars/jest.config.js deleted file mode 100644 index feb9f905..00000000 --- a/dev/lib/handlebars/jest.config.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/kbn-handlebars/LICENSE` for more information. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['<rootDir>/packages/kbn-handlebars'], -}; diff --git a/dev/lib/handlebars/kibana.jsonc b/dev/lib/handlebars/kibana.jsonc deleted file mode 100644 index 59b3c28d..00000000 --- a/dev/lib/handlebars/kibana.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "shared-common", - "id": "@kbn/handlebars", - "owner": "@elastic/kibana-security" -} diff --git a/dev/lib/handlebars/package.json b/dev/lib/handlebars/package.json deleted file mode 100644 index 46ca823a..00000000 --- a/dev/lib/handlebars/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "@kbn/handlebars", - "version": "1.0.0", - "private": true, - "license": "MIT" -}
\ No newline at end of file diff --git a/dev/lib/handlebars/scripts/check_for_upstream_updates.sh b/dev/lib/handlebars/scripts/check_for_upstream_updates.sh deleted file mode 100755 index 73f7376a..00000000 --- a/dev/lib/handlebars/scripts/check_for_upstream_updates.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -set -e - -TMP=.tmp-handlebars -HASH_FILE=packages/kbn-handlebars/src/spec/.upstream_git_hash - -function cleanup { - rm -fr $TMP -} - -trap cleanup EXIT - -rm -fr $TMP -mkdir $TMP - -echo "Cloning handlebars repo..." -git clone -q --depth 1 https://github.com/handlebars-lang/handlebars.js.git -b 4.x $TMP - -echo "Looking for updates..." -hash=$(git -C $TMP rev-parse HEAD) -expected_hash=$(cat $HASH_FILE) - -if [ "$hash" = "$expected_hash" ]; then - echo "You're all up to date :)" -else - echo - echo "New changes has been committed to the '4.x' branch in the upstream git repository" - echo - echo "To resolve this issue, do the following:" - echo - echo " 1. Investigate the commits in the '4.x' branch of the upstream git repository." - echo " If files inside the 'spec' folder has been updated, sync those updates with" - echo " our local versions of these files (located in" - echo " 'packages/kbn-handlebars/src/spec')." - echo - echo " https://github.com/handlebars-lang/handlebars.js/compare/$hash...4.x" - echo - echo " 2. Execute the following script and commit the updated '$HASH_FILE'" - echo " file including any changes you made to our own spec files." - echo - echo " ./packages/kbn-handlebars/scripts/update_upstream_git_hash.sh" - echo - exit 1 -fi diff --git a/dev/lib/handlebars/scripts/print_ast.js b/dev/lib/handlebars/scripts/print_ast.js deleted file mode 100755 index b97fb5a6..00000000 --- a/dev/lib/handlebars/scripts/print_ast.js +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env node -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/kbn-handlebars/LICENSE` for more information. - */ -'use strict'; // eslint-disable-line strict - -const { relative } = require('path'); -const { inspect } = require('util'); - -const { parse } = require('handlebars'); -const argv = require('minimist')(process.argv.slice(2)); - -const DEFAULT_FILTER = 'loc,strip,openStrip,inverseStrip,closeStrip'; - -const filter = argv['show-all'] ? [''] : (argv.filter || DEFAULT_FILTER).split(','); -const hideEmpty = argv['hide-empty'] || false; -const template = argv._[0]; - -if (template === undefined) { - const script = relative(process.cwd(), process.argv[1]); - console.log(`Usage: ${script} [options] <template>`); - console.log(); - console.log('Options:'); - console.log(' --filter=... A comma separated list of keys to filter from the output.'); - console.log(` Default: ${DEFAULT_FILTER}`); - console.log(' --hide-empty Do not display empty properties.'); - console.log(' --show-all Do not filter out any properties. Equivalent to --filter="".'); - console.log(); - console.log('Example:'); - console.log(` ${script} --hide-empty -- 'hello {{name}}'`); - console.log(); - process.exit(1); -} - -console.log(inspect(reduce(parse(template, filter)), { colors: true, depth: null })); - -function reduce(ast) { - if (Array.isArray(ast)) { - for (let i = 0; i < ast.length; i++) { - ast[i] = reduce(ast[i]); - } - } else { - for (const k of filter) { - delete ast[k]; - } - - if (hideEmpty) { - for (const [k, v] of Object.entries(ast)) { - if (v === undefined || v === null || (Array.isArray(v) && v.length === 0)) { - delete ast[k]; - } - } - } - - for (const [k, v] of Object.entries(ast)) { - if (typeof v === 'object' && v !== null) { - ast[k] = reduce(v); - } - } - } - - return ast; -} diff --git a/dev/lib/handlebars/scripts/update_upstream_git_hash.sh b/dev/lib/handlebars/scripts/update_upstream_git_hash.sh deleted file mode 100755 index 52cc39e0..00000000 --- a/dev/lib/handlebars/scripts/update_upstream_git_hash.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -e - -TMP=.tmp-handlebars -HASH_FILE=packages/kbn-handlebars/src/spec/.upstream_git_hash - -function cleanup { - rm -fr $TMP -} - -trap cleanup EXIT - -rm -fr $TMP -mkdir $TMP - -echo "Cloning handlebars repo..." -git clone -q --depth 1 https://github.com/handlebars-lang/handlebars.js.git -b 4.x $TMP - -echo "Updating hash file..." -git -C $TMP rev-parse HEAD | tr -d '\n' > $HASH_FILE -git add $HASH_FILE - -echo "Done! - Don't forget to commit any changes to $HASH_FILE" diff --git a/dev/lib/handlebars/src/__jest__/test_bench.ts b/dev/lib/handlebars/src/__jest__/test_bench.ts deleted file mode 100644 index d17f7f12..00000000 --- a/dev/lib/handlebars/src/__jest__/test_bench.ts +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/kbn-handlebars/LICENSE` for more information. - */ - -import Handlebars, { - type CompileOptions, - type DecoratorDelegate, - type HelperDelegate, - type RuntimeOptions, -} from '../..'; -import type { DecoratorsHash, HelpersHash, PartialsHash, Template } from '../types'; - -type CompileFns = 'compile' | 'compileAST'; -const compileFns: CompileFns[] = ['compile', 'compileAST']; -if (process.env.AST) compileFns.splice(0, 1); -else if (process.env.EVAL) compileFns.splice(1, 1); - -declare global { - var kbnHandlebarsEnv: typeof Handlebars | null; // eslint-disable-line no-var -} - -global.kbnHandlebarsEnv = null; - -interface TestOptions { - beforeEach?: Function; - beforeRender?: Function; -} - -export function expectTemplate(template: string, options?: TestOptions) { - return new HandlebarsTestBench(template, options); -} - -export function forEachCompileFunctionName( - cb: (compileName: CompileFns, index: number, array: CompileFns[]) => void -) { - compileFns.forEach(cb); -} - -class HandlebarsTestBench { - private template: string; - private options: TestOptions; - private compileOptions?: CompileOptions; - private runtimeOptions?: RuntimeOptions; - private helpers: HelpersHash = {}; - private partials: PartialsHash = {}; - private decorators: DecoratorsHash = {}; - private input: any = {}; - - constructor(template: string, options: TestOptions = {}) { - this.template = template; - this.options = options; - } - - withCompileOptions(compileOptions?: CompileOptions) { - this.compileOptions = compileOptions; - return this; - } - - withRuntimeOptions(runtimeOptions?: RuntimeOptions) { - this.runtimeOptions = runtimeOptions; - return this; - } - - withInput(input: any) { - this.input = input; - return this; - } - - withHelper<F extends HelperDelegate>(name: string, helper: F) { - this.helpers[name] = helper; - return this; - } - - withHelpers<F extends HelperDelegate>(helperFunctions: Record<string, F>) { - for (const [name, helper] of Object.entries(helperFunctions)) { - this.withHelper(name, helper); - } - return this; - } - - withPartial(name: string | number, partial: Template) { - this.partials[name] = partial; - return this; - } - - withPartials(partials: Record<string, Template>) { - for (const [name, partial] of Object.entries(partials)) { - this.withPartial(name, partial); - } - return this; - } - - withDecorator(name: string, decoratorFunction: DecoratorDelegate) { - this.decorators[name] = decoratorFunction; - return this; - } - - withDecorators(decoratorFunctions: Record<string, DecoratorDelegate>) { - for (const [name, decoratorFunction] of Object.entries(decoratorFunctions)) { - this.withDecorator(name, decoratorFunction); - } - return this; - } - - toCompileTo(outputExpected: string) { - const { outputEval, outputAST } = this.compileAndExecute(); - if (process.env.EVAL) { - expect(outputEval).toEqual(outputExpected); - } else if (process.env.AST) { - expect(outputAST).toEqual(outputExpected); - } else { - expect(outputAST).toEqual(outputExpected); - expect(outputAST).toEqual(outputEval); - } - } - - toThrow(error?: string | RegExp | jest.Constructable | Error | undefined) { - if (process.env.EVAL) { - expect(() => { - this.compileAndExecuteEval(); - }).toThrowError(error); - } else if (process.env.AST) { - expect(() => { - this.compileAndExecuteAST(); - }).toThrowError(error); - } else { - expect(() => { - this.compileAndExecuteEval(); - }).toThrowError(error); - expect(() => { - this.compileAndExecuteAST(); - }).toThrowError(error); - } - } - - private compileAndExecute() { - if (process.env.EVAL) { - return { - outputEval: this.compileAndExecuteEval(), - }; - } else if (process.env.AST) { - return { - outputAST: this.compileAndExecuteAST(), - }; - } else { - return { - outputEval: this.compileAndExecuteEval(), - outputAST: this.compileAndExecuteAST(), - }; - } - } - - private compileAndExecuteEval() { - const renderEval = this.compileEval(); - - const runtimeOptions: RuntimeOptions = { - helpers: this.helpers, - partials: this.partials, - decorators: this.decorators, - ...this.runtimeOptions, - }; - - this.execBeforeRender(); - - return renderEval(this.input, runtimeOptions); - } - - private compileAndExecuteAST() { - const renderAST = this.compileAST(); - - const runtimeOptions: RuntimeOptions = { - helpers: this.helpers, - partials: this.partials, - decorators: this.decorators, - ...this.runtimeOptions, - }; - - this.execBeforeRender(); - - return renderAST(this.input, runtimeOptions); - } - - private compileEval(handlebarsEnv = getHandlebarsEnv()) { - this.execBeforeEach(); - return handlebarsEnv.compile(this.template, this.compileOptions); - } - - private compileAST(handlebarsEnv = getHandlebarsEnv()) { - this.execBeforeEach(); - return handlebarsEnv.compileAST(this.template, this.compileOptions); - } - - private execBeforeRender() { - this.options.beforeRender?.(); - } - - private execBeforeEach() { - if (this.options.beforeEach) { - this.options.beforeEach(); - } - } -} - -function getHandlebarsEnv() { - return kbnHandlebarsEnv || Handlebars.create(); -} diff --git a/dev/lib/handlebars/src/handlebars.ts b/dev/lib/handlebars/src/handlebars.ts deleted file mode 100644 index 358d1b73..00000000 --- a/dev/lib/handlebars/src/handlebars.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/kbn-handlebars/LICENSE` for more information. - */ - -// The handlebars module uses `export =`, so we should technically use `import Handlebars = require('handlebars')`, but Babel will not allow this: -// https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -import Handlebars = require('handlebars'); - -import type { CompileOptions, RuntimeOptions, TemplateDelegate } from './types'; -import { ElasticHandlebarsVisitor } from './visitor'; - -const originalCreate = Handlebars.create; - -export { Handlebars }; - -/** - * Creates an isolated Handlebars environment. - * - * Each environment has its own helpers. - * This is only necessary for use cases that demand distinct helpers. - * Most use cases can use the root Handlebars environment directly. - * - * @returns A sandboxed/scoped version of the @kbn/handlebars module - */ -Handlebars.create = function (): typeof Handlebars { - const SandboxedHandlebars = originalCreate.call(Handlebars) as typeof Handlebars; - // When creating new Handlebars environments, ensure the custom compileAST function is present in the new environment as well - SandboxedHandlebars.compileAST = Handlebars.compileAST; - return SandboxedHandlebars; -}; - -Handlebars.compileAST = function ( - input: string | hbs.AST.Program, - options?: CompileOptions -): TemplateDelegate { - if (input == null || (typeof input !== 'string' && input.type !== 'Program')) { - throw new Handlebars.Exception( - `You must pass a string or Handlebars AST to Handlebars.compileAST. You passed ${input}` - ); - } - - // If `Handlebars.compileAST` is reassigned, `this` will be undefined. - const visitor = new ElasticHandlebarsVisitor(this ?? Handlebars, input, options); - - return (context: any, runtimeOptions?: RuntimeOptions) => visitor.render(context, runtimeOptions); -}; diff --git a/dev/lib/handlebars/src/spec/.upstream_git_hash b/dev/lib/handlebars/src/spec/.upstream_git_hash deleted file mode 100644 index 5a6b1831..00000000 --- a/dev/lib/handlebars/src/spec/.upstream_git_hash +++ /dev/null @@ -1 +0,0 @@ -c65c6cce3f626e4896a9d59250f0908be695adae
\ No newline at end of file diff --git a/dev/lib/handlebars/src/spec/index.basic.test.ts b/dev/lib/handlebars/src/spec/index.basic.test.ts deleted file mode 100644 index 6acf3ae9..00000000 --- a/dev/lib/handlebars/src/spec/index.basic.test.ts +++ /dev/null @@ -1,481 +0,0 @@ -/* - * 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 } from '../__jest__/test_bench'; - -describe('basic context', () => { - it('most basic', () => { - expectTemplate('{{foo}}').withInput({ foo: 'foo' }).toCompileTo('foo'); - }); - - it('escaping', () => { - expectTemplate('\\{{foo}}').withInput({ foo: 'food' }).toCompileTo('{{foo}}'); - expectTemplate('content \\{{foo}}').withInput({ foo: 'food' }).toCompileTo('content {{foo}}'); - expectTemplate('\\\\{{foo}}').withInput({ foo: 'food' }).toCompileTo('\\food'); - expectTemplate('content \\\\{{foo}}').withInput({ foo: 'food' }).toCompileTo('content \\food'); - expectTemplate('\\\\ {{foo}}').withInput({ foo: 'food' }).toCompileTo('\\\\ food'); - }); - - it('compiling with a basic context', () => { - expectTemplate('Goodbye\n{{cruel}}\n{{world}}!') - .withInput({ - cruel: 'cruel', - world: 'world', - }) - .toCompileTo('Goodbye\ncruel\nworld!'); - }); - - it('compiling with a string context', () => { - expectTemplate('{{.}}{{length}}').withInput('bye').toCompileTo('bye3'); - }); - - it('compiling with an undefined context', () => { - expectTemplate('Goodbye\n{{cruel}}\n{{world.bar}}!') - .withInput(undefined) - .toCompileTo('Goodbye\n\n!'); - - expectTemplate('{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}') - .withInput(undefined) - .toCompileTo('Goodbye'); - }); - - it('comments', () => { - expectTemplate('{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!') - .withInput({ - cruel: 'cruel', - world: 'world', - }) - .toCompileTo('Goodbye\ncruel\nworld!'); - - expectTemplate(' {{~! comment ~}} blah').toCompileTo('blah'); - expectTemplate(' {{~!-- long-comment --~}} blah').toCompileTo('blah'); - expectTemplate(' {{! comment ~}} blah').toCompileTo(' blah'); - expectTemplate(' {{!-- long-comment --~}} blah').toCompileTo(' blah'); - expectTemplate(' {{~! comment}} blah').toCompileTo(' blah'); - expectTemplate(' {{~!-- long-comment --}} blah').toCompileTo(' blah'); - }); - - it('boolean', () => { - const string = '{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!'; - expectTemplate(string) - .withInput({ - goodbye: true, - world: 'world', - }) - .toCompileTo('GOODBYE cruel world!'); - - expectTemplate(string) - .withInput({ - goodbye: false, - world: 'world', - }) - .toCompileTo('cruel world!'); - }); - - it('zeros', () => { - expectTemplate('num1: {{num1}}, num2: {{num2}}') - .withInput({ - num1: 42, - num2: 0, - }) - .toCompileTo('num1: 42, num2: 0'); - - expectTemplate('num: {{.}}').withInput(0).toCompileTo('num: 0'); - - expectTemplate('num: {{num1/num2}}') - .withInput({ num1: { num2: 0 } }) - .toCompileTo('num: 0'); - }); - - it('false', () => { - /* eslint-disable no-new-wrappers */ - expectTemplate('val1: {{val1}}, val2: {{val2}}') - .withInput({ - val1: false, - val2: new Boolean(false), - }) - .toCompileTo('val1: false, val2: false'); - - expectTemplate('val: {{.}}').withInput(false).toCompileTo('val: false'); - - expectTemplate('val: {{val1/val2}}') - .withInput({ val1: { val2: false } }) - .toCompileTo('val: false'); - - expectTemplate('val1: {{{val1}}}, val2: {{{val2}}}') - .withInput({ - val1: false, - val2: new Boolean(false), - }) - .toCompileTo('val1: false, val2: false'); - - expectTemplate('val: {{{val1/val2}}}') - .withInput({ val1: { val2: false } }) - .toCompileTo('val: false'); - /* eslint-enable */ - }); - - it('should handle undefined and null', () => { - expectTemplate('{{awesome undefined null}}') - .withInput({ - awesome(_undefined: any, _null: any, options: any) { - return (_undefined === undefined) + ' ' + (_null === null) + ' ' + typeof options; - }, - }) - .toCompileTo('true true object'); - - expectTemplate('{{undefined}}') - .withInput({ - undefined() { - return 'undefined!'; - }, - }) - .toCompileTo('undefined!'); - - expectTemplate('{{null}}') - .withInput({ - null() { - return 'null!'; - }, - }) - .toCompileTo('null!'); - }); - - it('newlines', () => { - expectTemplate("Alan's\nTest").toCompileTo("Alan's\nTest"); - expectTemplate("Alan's\rTest").toCompileTo("Alan's\rTest"); - }); - - it('escaping text', () => { - expectTemplate("Awesome's").toCompileTo("Awesome's"); - expectTemplate('Awesome\\').toCompileTo('Awesome\\'); - expectTemplate('Awesome\\\\ foo').toCompileTo('Awesome\\\\ foo'); - expectTemplate('Awesome {{foo}}').withInput({ foo: '\\' }).toCompileTo('Awesome \\'); - expectTemplate(" ' ' ").toCompileTo(" ' ' "); - }); - - it('escaping expressions', () => { - expectTemplate('{{{awesome}}}').withInput({ awesome: "&'\\<>" }).toCompileTo("&'\\<>"); - - expectTemplate('{{&awesome}}').withInput({ awesome: "&'\\<>" }).toCompileTo("&'\\<>"); - - expectTemplate('{{awesome}}') - .withInput({ awesome: '&"\'`\\<>' }) - .toCompileTo('&"'`\\<>'); - - expectTemplate('{{awesome}}') - .withInput({ awesome: 'Escaped, <b> looks like: <b>' }) - .toCompileTo('Escaped, <b> looks like: &lt;b&gt;'); - }); - - it("functions returning safestrings shouldn't be escaped", () => { - expectTemplate('{{awesome}}') - .withInput({ - awesome() { - return new Handlebars.SafeString("&'\\<>"); - }, - }) - .toCompileTo("&'\\<>"); - }); - - it('functions', () => { - expectTemplate('{{awesome}}') - .withInput({ - awesome() { - return 'Awesome'; - }, - }) - .toCompileTo('Awesome'); - - expectTemplate('{{awesome}}') - .withInput({ - awesome() { - return this.more; - }, - more: 'More awesome', - }) - .toCompileTo('More awesome'); - }); - - it('functions with context argument', () => { - expectTemplate('{{awesome frank}}') - .withInput({ - awesome(context: any) { - return context; - }, - frank: 'Frank', - }) - .toCompileTo('Frank'); - }); - - it('pathed functions with context argument', () => { - expectTemplate('{{bar.awesome frank}}') - .withInput({ - bar: { - awesome(context: any) { - return context; - }, - }, - frank: 'Frank', - }) - .toCompileTo('Frank'); - }); - - it('depthed functions with context argument', () => { - expectTemplate('{{#with frank}}{{../awesome .}}{{/with}}') - .withInput({ - awesome(context: any) { - return context; - }, - frank: 'Frank', - }) - .toCompileTo('Frank'); - }); - - it('block functions with context argument', () => { - expectTemplate('{{#awesome 1}}inner {{.}}{{/awesome}}') - .withInput({ - awesome(context: any, options: any) { - return options.fn(context); - }, - }) - .toCompileTo('inner 1'); - }); - - it('depthed block functions with context argument', () => { - expectTemplate('{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}') - .withInput({ - value: true, - awesome(context: any, options: any) { - return options.fn(context); - }, - }) - .toCompileTo('inner 1'); - }); - - it('block functions without context argument', () => { - expectTemplate('{{#awesome}}inner{{/awesome}}') - .withInput({ - awesome(options: any) { - return options.fn(this); - }, - }) - .toCompileTo('inner'); - }); - - it('pathed block functions without context argument', () => { - expectTemplate('{{#foo.awesome}}inner{{/foo.awesome}}') - .withInput({ - foo: { - awesome() { - return this; - }, - }, - }) - .toCompileTo('inner'); - }); - - it('depthed block functions without context argument', () => { - expectTemplate('{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}') - .withInput({ - value: true, - awesome() { - return this; - }, - }) - .toCompileTo('inner'); - }); - - it('paths with hyphens', () => { - expectTemplate('{{foo-bar}}').withInput({ 'foo-bar': 'baz' }).toCompileTo('baz'); - - expectTemplate('{{foo.foo-bar}}') - .withInput({ foo: { 'foo-bar': 'baz' } }) - .toCompileTo('baz'); - - expectTemplate('{{foo/foo-bar}}') - .withInput({ foo: { 'foo-bar': 'baz' } }) - .toCompileTo('baz'); - }); - - it('nested paths', () => { - expectTemplate('Goodbye {{alan/expression}} world!') - .withInput({ alan: { expression: 'beautiful' } }) - .toCompileTo('Goodbye beautiful world!'); - }); - - it('nested paths with empty string value', () => { - expectTemplate('Goodbye {{alan/expression}} world!') - .withInput({ alan: { expression: '' } }) - .toCompileTo('Goodbye world!'); - }); - - it('literal paths', () => { - expectTemplate('Goodbye {{[@alan]/expression}} world!') - .withInput({ '@alan': { expression: 'beautiful' } }) - .toCompileTo('Goodbye beautiful world!'); - - expectTemplate('Goodbye {{[foo bar]/expression}} world!') - .withInput({ 'foo bar': { expression: 'beautiful' } }) - .toCompileTo('Goodbye beautiful world!'); - }); - - it('literal references', () => { - expectTemplate('Goodbye {{[foo bar]}} world!') - .withInput({ 'foo bar': 'beautiful' }) - .toCompileTo('Goodbye beautiful world!'); - - expectTemplate('Goodbye {{"foo bar"}} world!') - .withInput({ 'foo bar': 'beautiful' }) - .toCompileTo('Goodbye beautiful world!'); - - expectTemplate("Goodbye {{'foo bar'}} world!") - .withInput({ 'foo bar': 'beautiful' }) - .toCompileTo('Goodbye beautiful world!'); - - expectTemplate('Goodbye {{"foo[bar"}} world!') - .withInput({ 'foo[bar': 'beautiful' }) - .toCompileTo('Goodbye beautiful world!'); - - expectTemplate('Goodbye {{"foo\'bar"}} world!') - .withInput({ "foo'bar": 'beautiful' }) - .toCompileTo('Goodbye beautiful world!'); - - expectTemplate("Goodbye {{'foo\"bar'}} world!") - .withInput({ 'foo"bar': 'beautiful' }) - .toCompileTo('Goodbye beautiful world!'); - }); - - it("that current context path ({{.}}) doesn't hit helpers", () => { - expectTemplate('test: {{.}}') - .withInput(null) - // @ts-expect-error Setting the helper to a string instead of a function doesn't make sense normally, but here it doesn't matter - .withHelpers({ helper: 'awesome' }) - .toCompileTo('test: '); - }); - - it('complex but empty paths', () => { - expectTemplate('{{person/name}}') - .withInput({ person: { name: null } }) - .toCompileTo(''); - - expectTemplate('{{person/name}}').withInput({ person: {} }).toCompileTo(''); - }); - - it('this keyword in paths', () => { - expectTemplate('{{#goodbyes}}{{this}}{{/goodbyes}}') - .withInput({ goodbyes: ['goodbye', 'Goodbye', 'GOODBYE'] }) - .toCompileTo('goodbyeGoodbyeGOODBYE'); - - expectTemplate('{{#hellos}}{{this/text}}{{/hellos}}') - .withInput({ - hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }], - }) - .toCompileTo('helloHelloHELLO'); - }); - - it('this keyword nested inside path', () => { - expectTemplate('{{#hellos}}{{text/this/foo}}{{/hellos}}').toThrow( - 'Invalid path: text/this - 1:13' - ); - - expectTemplate('{{[this]}}').withInput({ this: 'bar' }).toCompileTo('bar'); - - expectTemplate('{{text/[this]}}') - .withInput({ text: { this: 'bar' } }) - .toCompileTo('bar'); - }); - - it('this keyword in helpers', () => { - const helpers = { - foo(value: any) { - return 'bar ' + value; - }, - }; - - expectTemplate('{{#goodbyes}}{{foo this}}{{/goodbyes}}') - .withInput({ goodbyes: ['goodbye', 'Goodbye', 'GOODBYE'] }) - .withHelpers(helpers) - .toCompileTo('bar goodbyebar Goodbyebar GOODBYE'); - - expectTemplate('{{#hellos}}{{foo this/text}}{{/hellos}}') - .withInput({ - hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }], - }) - .withHelpers(helpers) - .toCompileTo('bar hellobar Hellobar HELLO'); - }); - - it('this keyword nested inside helpers param', () => { - expectTemplate('{{#hellos}}{{foo text/this/foo}}{{/hellos}}').toThrow( - 'Invalid path: text/this - 1:17' - ); - - expectTemplate('{{foo [this]}}') - .withInput({ - foo(value: any) { - return value; - }, - this: 'bar', - }) - .toCompileTo('bar'); - - expectTemplate('{{foo text/[this]}}') - .withInput({ - foo(value: any) { - return value; - }, - text: { this: 'bar' }, - }) - .toCompileTo('bar'); - }); - - it('pass string literals', () => { - expectTemplate('{{"foo"}}').toCompileTo(''); - expectTemplate('{{"foo"}}').withInput({ foo: 'bar' }).toCompileTo('bar'); - - expectTemplate('{{#"foo"}}{{.}}{{/"foo"}}') - .withInput({ - foo: ['bar', 'baz'], - }) - .toCompileTo('barbaz'); - }); - - it('pass number literals', () => { - expectTemplate('{{12}}').toCompileTo(''); - expectTemplate('{{12}}').withInput({ '12': 'bar' }).toCompileTo('bar'); - expectTemplate('{{12.34}}').toCompileTo(''); - expectTemplate('{{12.34}}').withInput({ '12.34': 'bar' }).toCompileTo('bar'); - expectTemplate('{{12.34 1}}') - .withInput({ - '12.34'(arg: any) { - return 'bar' + arg; - }, - }) - .toCompileTo('bar1'); - }); - - it('pass boolean literals', () => { - expectTemplate('{{true}}').toCompileTo(''); - expectTemplate('{{true}}').withInput({ '': 'foo' }).toCompileTo(''); - expectTemplate('{{false}}').withInput({ false: 'foo' }).toCompileTo('foo'); - }); - - it('should handle literals in subexpression', () => { - expectTemplate('{{foo (false)}}') - .withInput({ - false() { - return 'bar'; - }, - }) - .withHelper('foo', function (arg) { - return arg; - }) - .toCompileTo('bar'); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.blocks.test.ts b/dev/lib/handlebars/src/spec/index.blocks.test.ts deleted file mode 100644 index 2d9a8707..00000000 --- a/dev/lib/handlebars/src/spec/index.blocks.test.ts +++ /dev/null @@ -1,366 +0,0 @@ -/* - * 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, { type HelperOptions } from '../..'; -import { expectTemplate } from '../__jest__/test_bench'; - -describe('blocks', () => { - it('array', () => { - const string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!'; - - expectTemplate(string) - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); - - expectTemplate(string) - .withInput({ - goodbyes: [], - world: 'world', - }) - .toCompileTo('cruel world!'); - }); - - it('array without data', () => { - expectTemplate('{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}') - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo('goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE'); - }); - - it('array with @index', () => { - expectTemplate('{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!') - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!'); - }); - - it('empty block', () => { - const string = '{{#goodbyes}}{{/goodbyes}}cruel {{world}}!'; - - expectTemplate(string) - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo('cruel world!'); - - expectTemplate(string) - .withInput({ - goodbyes: [], - world: 'world', - }) - .toCompileTo('cruel world!'); - }); - - it('block with complex lookup', () => { - expectTemplate('{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}') - .withInput({ - name: 'Alan', - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - }) - .toCompileTo('goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! '); - }); - - it('multiple blocks with complex lookup', () => { - expectTemplate('{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}') - .withInput({ - name: 'Alan', - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - }) - .toCompileTo('AlanAlanAlanAlanAlanAlan'); - }); - - it('block with complex lookup using nested context', () => { - expectTemplate('{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}').toThrow(Error); - }); - - it('block with deep nested complex lookup', () => { - expectTemplate( - '{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}' - ) - .withInput({ - omg: 'OMG!', - outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }], - }) - .toCompileTo('Goodbye cruel sad OMG!'); - }); - - it('works with cached blocks', () => { - expectTemplate('{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}') - .withCompileOptions({ data: false }) - .withInput({ - person: [ - { first: 'Alan', last: 'Johnson' }, - { first: 'Alan', last: 'Johnson' }, - ], - }) - .toCompileTo('Alan JohnsonAlan Johnson'); - }); - - describe('inverted sections', () => { - it('inverted sections with unset value', () => { - expectTemplate( - '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}' - ).toCompileTo('Right On!'); - }); - - it('inverted section with false value', () => { - expectTemplate('{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}') - .withInput({ goodbyes: false }) - .toCompileTo('Right On!'); - }); - - it('inverted section with empty set', () => { - expectTemplate('{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}') - .withInput({ goodbyes: [] }) - .toCompileTo('Right On!'); - }); - - it('block inverted sections', () => { - expectTemplate('{{#people}}{{name}}{{^}}{{none}}{{/people}}') - .withInput({ none: 'No people' }) - .toCompileTo('No people'); - }); - - it('chained inverted sections', () => { - expectTemplate('{{#people}}{{name}}{{else if none}}{{none}}{{/people}}') - .withInput({ none: 'No people' }) - .toCompileTo('No people'); - - expectTemplate( - '{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}' - ) - .withInput({ none: 'No people' }) - .toCompileTo('No people'); - - expectTemplate('{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}') - .withInput({ none: 'No people' }) - .toCompileTo('No people'); - }); - - it('chained inverted sections with mismatch', () => { - expectTemplate('{{#people}}{{name}}{{else if none}}{{none}}{{/if}}').toThrow(Error); - }); - - it('block inverted sections with empty arrays', () => { - expectTemplate('{{#people}}{{name}}{{^}}{{none}}{{/people}}') - .withInput({ - none: 'No people', - people: [], - }) - .toCompileTo('No people'); - }); - }); - - describe('standalone sections', () => { - it('block standalone else sections', () => { - expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n') - .withInput({ none: 'No people' }) - .toCompileTo('No people\n'); - - expectTemplate('{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n') - .withInput({ none: 'No people' }) - .toCompileTo('No people\n'); - - expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n') - .withInput({ none: 'No people' }) - .toCompileTo('No people\n'); - }); - - it('block standalone chained else sections', () => { - expectTemplate('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n') - .withInput({ none: 'No people' }) - .toCompileTo('No people\n'); - - expectTemplate('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n') - .withInput({ none: 'No people' }) - .toCompileTo('No people\n'); - }); - - it('should handle nesting', () => { - expectTemplate('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.') - .withInput({ - data: [1, 3, 5], - }) - .toCompileTo('1\n3\n5\nOK.'); - }); - }); - - describe('decorators', () => { - it('should apply mustache decorators', () => { - expectTemplate('{{#helper}}{{*decorator}}{{/helper}}') - .withHelper('helper', function (options: HelperOptions) { - return (options.fn as any).run; - }) - .withDecorator('decorator', function (fn) { - (fn as any).run = 'success'; - return fn; - }) - .toCompileTo('success'); - }); - - it('should apply allow undefined return', () => { - expectTemplate('{{#helper}}{{*decorator}}suc{{/helper}}') - .withHelper('helper', function (options: HelperOptions) { - return options.fn() + (options.fn as any).run; - }) - .withDecorator('decorator', function (fn) { - (fn as any).run = 'cess'; - }) - .toCompileTo('success'); - }); - - it('should apply block decorators', () => { - expectTemplate('{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}') - .withHelper('helper', function (options: HelperOptions) { - return (options.fn as any).run; - }) - .withDecorator('decorator', function (fn, props, container, options) { - (fn as any).run = options.fn(); - return fn; - }) - .toCompileTo('success'); - }); - - it('should support nested decorators', () => { - expectTemplate( - '{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}' - ) - .withHelper('helper', function (options: HelperOptions) { - return (options.fn as any).run; - }) - .withDecorators({ - decorator(fn, props, container, options) { - (fn as any).run = options.fn.nested + options.fn(); - return fn; - }, - nested(fn, props, container, options) { - props.nested = options.fn(); - }, - }) - .toCompileTo('success'); - }); - - it('should apply multiple decorators', () => { - expectTemplate( - '{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}' - ) - .withHelper('helper', function (options: HelperOptions) { - return (options.fn as any).run; - }) - .withDecorator('decorator', function (fn, props, container, options) { - (fn as any).run = ((fn as any).run || '') + options.fn(); - return fn; - }) - .toCompileTo('success'); - }); - - it('should access parent variables', () => { - expectTemplate('{{#helper}}{{*decorator foo}}{{/helper}}') - .withHelper('helper', function (options: HelperOptions) { - return (options.fn as any).run; - }) - .withDecorator('decorator', function (fn, props, container, options) { - (fn as any).run = options.args; - return fn; - }) - .withInput({ foo: 'success' }) - .toCompileTo('success'); - }); - - it('should work with root program', () => { - let run; - expectTemplate('{{*decorator "success"}}') - .withDecorator('decorator', function (fn, props, container, options) { - expect(options.args[0]).toEqual('success'); - run = true; - return fn; - }) - .withInput({ foo: 'success' }) - .toCompileTo(''); - expect(run).toEqual(true); - }); - - it('should fail when accessing variables from root', () => { - let run; - expectTemplate('{{*decorator foo}}') - .withDecorator('decorator', function (fn, props, container, options) { - expect(options.args[0]).toBeUndefined(); - run = true; - return fn; - }) - .withInput({ foo: 'fail' }) - .toCompileTo(''); - expect(run).toEqual(true); - }); - - describe('registration', () => { - beforeEach(() => { - global.kbnHandlebarsEnv = Handlebars.create(); - }); - - afterEach(() => { - global.kbnHandlebarsEnv = null; - }); - - it('unregisters', () => { - // @ts-expect-error: Cannot assign to 'decorators' because it is a read-only property. - kbnHandlebarsEnv!.decorators = {}; - - kbnHandlebarsEnv!.registerDecorator('foo', function () { - return 'fail'; - }); - - expect(!!kbnHandlebarsEnv!.decorators.foo).toEqual(true); - kbnHandlebarsEnv!.unregisterDecorator('foo'); - expect(kbnHandlebarsEnv!.decorators.foo).toBeUndefined(); - }); - - it('allows multiple globals', () => { - // @ts-expect-error: Cannot assign to 'decorators' because it is a read-only property. - kbnHandlebarsEnv!.decorators = {}; - - // @ts-expect-error: Expected 2 arguments, but got 1. - kbnHandlebarsEnv!.registerDecorator({ - foo() {}, - bar() {}, - }); - - expect(!!kbnHandlebarsEnv!.decorators.foo).toEqual(true); - expect(!!kbnHandlebarsEnv!.decorators.bar).toEqual(true); - kbnHandlebarsEnv!.unregisterDecorator('foo'); - kbnHandlebarsEnv!.unregisterDecorator('bar'); - expect(kbnHandlebarsEnv!.decorators.foo).toBeUndefined(); - expect(kbnHandlebarsEnv!.decorators.bar).toBeUndefined(); - }); - - it('fails with multiple and args', () => { - expect(() => { - kbnHandlebarsEnv!.registerDecorator( - // @ts-expect-error: Argument of type '{ world(): string; testHelper(): string; }' is not assignable to parameter of type 'string'. - { - world() { - return 'world!'; - }, - testHelper() { - return 'found it!'; - }, - }, - {} - ); - }).toThrow('Arg not supported with multiple decorators'); - }); - }); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.builtins.test.ts b/dev/lib/handlebars/src/spec/index.builtins.test.ts deleted file mode 100644 index c47ec29f..00000000 --- a/dev/lib/handlebars/src/spec/index.builtins.test.ts +++ /dev/null @@ -1,676 +0,0 @@ -/* - * 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. - */ - -/* eslint-disable max-classes-per-file */ - -import Handlebars from '../..'; -import { expectTemplate } from '../__jest__/test_bench'; - -describe('builtin helpers', () => { - describe('#if', () => { - it('if', () => { - const string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!'; - - expectTemplate(string) - .withInput({ - goodbye: true, - world: 'world', - }) - .toCompileTo('GOODBYE cruel world!'); - - expectTemplate(string) - .withInput({ - goodbye: 'dummy', - world: 'world', - }) - .toCompileTo('GOODBYE cruel world!'); - - expectTemplate(string) - .withInput({ - goodbye: false, - world: 'world', - }) - .toCompileTo('cruel world!'); - - expectTemplate(string).withInput({ world: 'world' }).toCompileTo('cruel world!'); - - expectTemplate(string) - .withInput({ - goodbye: ['foo'], - world: 'world', - }) - .toCompileTo('GOODBYE cruel world!'); - - expectTemplate(string) - .withInput({ - goodbye: [], - world: 'world', - }) - .toCompileTo('cruel world!'); - - expectTemplate(string) - .withInput({ - goodbye: 0, - world: 'world', - }) - .toCompileTo('cruel world!'); - - expectTemplate('{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!') - .withInput({ - goodbye: 0, - world: 'world', - }) - .toCompileTo('GOODBYE cruel world!'); - }); - - it('if with function argument', () => { - const string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!'; - - expectTemplate(string) - .withInput({ - goodbye() { - return true; - }, - world: 'world', - }) - .toCompileTo('GOODBYE cruel world!'); - - expectTemplate(string) - .withInput({ - goodbye() { - return this.world; - }, - world: 'world', - }) - .toCompileTo('GOODBYE cruel world!'); - - expectTemplate(string) - .withInput({ - goodbye() { - return false; - }, - world: 'world', - }) - .toCompileTo('cruel world!'); - - expectTemplate(string) - .withInput({ - goodbye() { - return this.foo; - }, - world: 'world', - }) - .toCompileTo('cruel world!'); - }); - - it('should not change the depth list', () => { - expectTemplate('{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}') - .withInput({ - foo: { goodbye: true }, - world: 'world', - }) - .toCompileTo('GOODBYE cruel world!'); - }); - }); - - describe('#with', () => { - it('with', () => { - expectTemplate('{{#with person}}{{first}} {{last}}{{/with}}') - .withInput({ - person: { - first: 'Alan', - last: 'Johnson', - }, - }) - .toCompileTo('Alan Johnson'); - }); - - it('with with function argument', () => { - expectTemplate('{{#with person}}{{first}} {{last}}{{/with}}') - .withInput({ - person() { - return { - first: 'Alan', - last: 'Johnson', - }; - }, - }) - .toCompileTo('Alan Johnson'); - }); - - it('with with else', () => { - expectTemplate( - '{{#with person}}Person is present{{else}}Person is not present{{/with}}' - ).toCompileTo('Person is not present'); - }); - - it('with provides block parameter', () => { - expectTemplate('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}') - .withInput({ - person: { - first: 'Alan', - last: 'Johnson', - }, - }) - .toCompileTo('Alan Johnson'); - }); - - it('works when data is disabled', () => { - expectTemplate('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}') - .withInput({ person: { first: 'Alan', last: 'Johnson' } }) - .withCompileOptions({ data: false }) - .toCompileTo('Alan Johnson'); - }); - }); - - describe('#each', () => { - it('each', () => { - const string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; - - expectTemplate(string) - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); - - expectTemplate(string) - .withInput({ - goodbyes: [], - world: 'world', - }) - .toCompileTo('cruel world!'); - }); - - it('each without data', () => { - expectTemplate('{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!') - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .withRuntimeOptions({ data: false }) - .withCompileOptions({ data: false }) - .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); - - expectTemplate('{{#each .}}{{.}}{{/each}}') - .withInput({ goodbyes: 'cruel', world: 'world' }) - .withRuntimeOptions({ data: false }) - .withCompileOptions({ data: false }) - .toCompileTo('cruelworld'); - }); - - it('each without context', () => { - expectTemplate('{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!') - .withInput(undefined) - .toCompileTo('cruel !'); - }); - - it('each with an object and @key', () => { - const string = '{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!'; - - function Clazz(this: any) { - this['<b>#1</b>'] = { text: 'goodbye' }; - this[2] = { text: 'GOODBYE' }; - } - Clazz.prototype.foo = 'fail'; - const hash = { goodbyes: new (Clazz as any)(), world: 'world' }; - - // Object property iteration order is undefined according to ECMA spec, - // so we need to check both possible orders - // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop - try { - expectTemplate(string) - .withInput(hash) - .toCompileTo('<b>#1</b>. goodbye! 2. GOODBYE! cruel world!'); - } catch (e) { - expectTemplate(string) - .withInput(hash) - .toCompileTo('2. GOODBYE! <b>#1</b>. goodbye! cruel world!'); - } - - expectTemplate(string) - .withInput({ - goodbyes: {}, - world: 'world', - }) - .toCompileTo('cruel world!'); - }); - - it('each with @index', () => { - expectTemplate('{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!') - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!'); - }); - - it('each with nested @index', () => { - expectTemplate( - '{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!' - ) - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo( - '0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!' - ); - }); - - it('each with block params', () => { - expectTemplate( - '{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!' - ) - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }], - world: 'world', - }) - .toCompileTo('0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!'); - }); - - // TODO: This test has been added to the `4.x` branch of the handlebars.js repo along with a code-fix, - // but a new version of the handlebars package containing this fix has not yet been published to npm. - // - // Before enabling this code, a new version of handlebars needs to be released and the corresponding - // updates needs to be applied to this implementation. - // - // See: https://github.com/handlebars-lang/handlebars.js/commit/30dbf0478109ded8f12bb29832135d480c17e367 - it.skip('each with block params and strict compilation', () => { - expectTemplate('{{#each goodbyes as |value index|}}{{index}}. {{value.text}}!{{/each}}') - .withCompileOptions({ strict: true }) - .withInput({ goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }] }) - .toCompileTo('0. goodbye!1. Goodbye!'); - }); - - it('each object with @index', () => { - expectTemplate('{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!') - .withInput({ - goodbyes: { - a: { text: 'goodbye' }, - b: { text: 'Goodbye' }, - c: { text: 'GOODBYE' }, - }, - world: 'world', - }) - .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!'); - }); - - it('each with @first', () => { - expectTemplate('{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo('goodbye! cruel world!'); - }); - - it('each with nested @first', () => { - expectTemplate( - '{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!' - ) - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo('(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!'); - }); - - it('each object with @first', () => { - expectTemplate('{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') - .withInput({ - goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } }, - world: 'world', - }) - .toCompileTo('goodbye! cruel world!'); - }); - - it('each with @last', () => { - expectTemplate('{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo('GOODBYE! cruel world!'); - }); - - it('each object with @last', () => { - expectTemplate('{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!') - .withInput({ - goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } }, - world: 'world', - }) - .toCompileTo('Goodbye! cruel world!'); - }); - - it('each with nested @last', () => { - expectTemplate( - '{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!' - ) - .withInput({ - goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }], - world: 'world', - }) - .toCompileTo('(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!'); - }); - - it('each with function argument', () => { - const string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; - - expectTemplate(string) - .withInput({ - goodbyes() { - return [{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }]; - }, - world: 'world', - }) - .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); - - expectTemplate(string) - .withInput({ - goodbyes: [], - world: 'world', - }) - .toCompileTo('cruel world!'); - }); - - it('each object when last key is an empty string', () => { - expectTemplate('{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!') - .withInput({ - goodbyes: { - a: { text: 'goodbye' }, - b: { text: 'Goodbye' }, - '': { text: 'GOODBYE' }, - }, - world: 'world', - }) - .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!'); - }); - - it('data passed to helpers', () => { - expectTemplate('{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}') - .withInput({ letters: ['a', 'b', 'c'] }) - .withHelper('detectDataInsideEach', function (options) { - return options.data && options.data.exclaim; - }) - .withRuntimeOptions({ - data: { - exclaim: '!', - }, - }) - .toCompileTo('a!b!c!'); - }); - - it('each on implicit context', () => { - expectTemplate('{{#each}}{{text}}! {{/each}}cruel world!').toThrow(Handlebars.Exception); - }); - - it('each on iterable', () => { - class Iterator { - private arr: any[]; - private index: number = 0; - - constructor(arr: any[]) { - this.arr = arr; - } - - next() { - const value = this.arr[this.index]; - const done = this.index === this.arr.length; - if (!done) { - this.index++; - } - return { value, done }; - } - } - - class Iterable { - private arr: any[]; - - constructor(arr: any[]) { - this.arr = arr; - } - - [Symbol.iterator]() { - return new Iterator(this.arr); - } - } - - const string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; - - expectTemplate(string) - .withInput({ - goodbyes: new Iterable([{ text: 'goodbye' }, { text: 'Goodbye' }, { text: 'GOODBYE' }]), - world: 'world', - }) - .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!'); - - expectTemplate(string) - .withInput({ - goodbyes: new Iterable([]), - world: 'world', - }) - .toCompileTo('cruel world!'); - }); - }); - - describe('#log', function () { - /* eslint-disable no-console */ - let $log: typeof console.log; - let $info: typeof console.info; - let $error: typeof console.error; - - beforeEach(function () { - $log = console.log; - $info = console.info; - $error = console.error; - - global.kbnHandlebarsEnv = Handlebars.create(); - }); - - afterEach(function () { - console.log = $log; - console.info = $info; - console.error = $error; - - global.kbnHandlebarsEnv = null; - }); - - it('should call logger at default level', function () { - let levelArg; - let logArg; - kbnHandlebarsEnv!.log = function (level, arg) { - levelArg = level; - logArg = arg; - }; - - expectTemplate('{{log blah}}').withInput({ blah: 'whee' }).toCompileTo(''); - expect(1).toEqual(levelArg); - expect('whee').toEqual(logArg); - }); - - it('should call logger at data level', function () { - let levelArg; - let logArg; - kbnHandlebarsEnv!.log = function (level, arg) { - levelArg = level; - logArg = arg; - }; - - expectTemplate('{{log blah}}') - .withInput({ blah: 'whee' }) - .withRuntimeOptions({ data: { level: '03' } }) - .withCompileOptions({ data: true }) - .toCompileTo(''); - expect('03').toEqual(levelArg); - expect('whee').toEqual(logArg); - }); - - it('should output to info', function () { - let calls = 0; - const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; - - console.info = function (info) { - expect('whee').toEqual(info); - calls++; - if (calls === callsExpected) { - console.info = $info; - console.log = $log; - } - }; - console.log = function (log) { - expect('whee').toEqual(log); - calls++; - if (calls === callsExpected) { - console.info = $info; - console.log = $log; - } - }; - - expectTemplate('{{log blah}}').withInput({ blah: 'whee' }).toCompileTo(''); - expect(calls).toEqual(callsExpected); - }); - - it('should log at data level', function () { - let calls = 0; - const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; - - console.error = function (log) { - expect('whee').toEqual(log); - calls++; - if (calls === callsExpected) console.error = $error; - }; - - expectTemplate('{{log blah}}') - .withInput({ blah: 'whee' }) - .withRuntimeOptions({ data: { level: '03' } }) - .withCompileOptions({ data: true }) - .toCompileTo(''); - expect(calls).toEqual(callsExpected); - }); - - it('should handle missing logger', function () { - let calls = 0; - const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; - - // @ts-expect-error - console.error = undefined; - console.log = function (log) { - expect('whee').toEqual(log); - calls++; - if (calls === callsExpected) console.log = $log; - }; - - expectTemplate('{{log blah}}') - .withInput({ blah: 'whee' }) - .withRuntimeOptions({ data: { level: '03' } }) - .withCompileOptions({ data: true }) - .toCompileTo(''); - expect(calls).toEqual(callsExpected); - }); - - it('should handle string log levels', function () { - let calls = 0; - const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; - - console.error = function (log) { - expect('whee').toEqual(log); - calls++; - }; - - expectTemplate('{{log blah}}') - .withInput({ blah: 'whee' }) - .withRuntimeOptions({ data: { level: 'error' } }) - .withCompileOptions({ data: true }) - .toCompileTo(''); - expect(calls).toEqual(callsExpected); - - calls = 0; - - expectTemplate('{{log blah}}') - .withInput({ blah: 'whee' }) - .withRuntimeOptions({ data: { level: 'ERROR' } }) - .withCompileOptions({ data: true }) - .toCompileTo(''); - expect(calls).toEqual(callsExpected); - }); - - it('should handle hash log levels [1]', function () { - let calls = 0; - const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; - - console.error = function (log) { - expect('whee').toEqual(log); - calls++; - }; - - expectTemplate('{{log blah level="error"}}').withInput({ blah: 'whee' }).toCompileTo(''); - expect(calls).toEqual(callsExpected); - }); - - it('should handle hash log levels [2]', function () { - let called = false; - - console.info = - console.log = - console.error = - console.debug = - function () { - called = true; - console.info = console.log = console.error = console.debug = $log; - }; - - expectTemplate('{{log blah level="debug"}}').withInput({ blah: 'whee' }).toCompileTo(''); - expect(false).toEqual(called); - }); - - it('should pass multiple log arguments', function () { - let calls = 0; - const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; - - console.info = console.log = function (log1, log2, log3) { - expect('whee').toEqual(log1); - expect('foo').toEqual(log2); - expect(1).toEqual(log3); - calls++; - if (calls === callsExpected) console.log = $log; - }; - - expectTemplate('{{log blah "foo" 1}}').withInput({ blah: 'whee' }).toCompileTo(''); - expect(calls).toEqual(callsExpected); - }); - - it('should pass zero log arguments', function () { - let calls = 0; - const callsExpected = process.env.AST || process.env.EVAL ? 1 : 2; - - console.info = console.log = function () { - expect(arguments.length).toEqual(0); - calls++; - if (calls === callsExpected) console.log = $log; - }; - - expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo(''); - expect(calls).toEqual(callsExpected); - }); - /* eslint-enable no-console */ - }); - - describe('#lookup', () => { - it('should lookup arbitrary content', () => { - expectTemplate('{{#each goodbyes}}{{lookup ../data .}}{{/each}}') - .withInput({ goodbyes: [0, 1], data: ['foo', 'bar'] }) - .toCompileTo('foobar'); - }); - - it('should not fail on undefined value', () => { - expectTemplate('{{#each goodbyes}}{{lookup ../bar .}}{{/each}}') - .withInput({ goodbyes: [0, 1], data: ['foo', 'bar'] }) - .toCompileTo(''); - }); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.compiler.test.ts b/dev/lib/handlebars/src/spec/index.compiler.test.ts deleted file mode 100644 index ef5c55f2..00000000 --- a/dev/lib/handlebars/src/spec/index.compiler.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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 { forEachCompileFunctionName } from '../__jest__/test_bench'; - -describe('compiler', () => { - forEachCompileFunctionName((compileName) => { - const compile = Handlebars[compileName].bind(Handlebars); - - describe(`#${compileName}`, () => { - it('should fail with invalid input', () => { - expect(function () { - compile(null); - }).toThrow( - `You must pass a string or Handlebars AST to Handlebars.${compileName}. You passed null` - ); - - expect(function () { - compile({}); - }).toThrow( - `You must pass a string or Handlebars AST to Handlebars.${compileName}. You passed [object Object]` - ); - }); - - it('should include the location in the error (row and column)', () => { - try { - compile(' \n {{#if}}\n{{/def}}')(); - expect(true).toEqual(false); - } catch (err) { - expect(err.message).toEqual("if doesn't match def - 2:5"); - if (Object.getOwnPropertyDescriptor(err, 'column')!.writable) { - // In Safari 8, the column-property is read-only. This means that even if it is set with defineProperty, - // its value won't change (https://github.com/jquery/esprima/issues/1290#issuecomment-132455482) - // Since this was neither working in Handlebars 3 nor in 4.0.5, we only check the column for other browsers. - expect(err.column).toEqual(5); - } - expect(err.lineNumber).toEqual(2); - } - }); - - it('should include the location as enumerable property', () => { - try { - compile(' \n {{#if}}\n{{/def}}')(); - expect(true).toEqual(false); - } catch (err) { - expect(Object.prototype.propertyIsEnumerable.call(err, 'column')).toEqual(true); - } - }); - - it('can utilize AST instance', () => { - expect( - compile({ - type: 'Program', - body: [{ type: 'ContentStatement', value: 'Hello' }], - })() - ).toEqual('Hello'); - }); - - it('can pass through an empty string', () => { - expect(compile('')()).toEqual(''); - }); - - it('should not modify the options.data property(GH-1327)', () => { - // The `data` property is supposed to be a boolean, but in this test we want to ignore that - const options = { data: [{ a: 'foo' }, { a: 'bar' }] as unknown as boolean }; - compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)(); - expect(JSON.stringify(options, null, 2)).toEqual( - JSON.stringify({ data: [{ a: 'foo' }, { a: 'bar' }] }, null, 2) - ); - }); - - it('should not modify the options.knownHelpers property(GH-1327)', () => { - const options = { knownHelpers: {} }; - compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)(); - expect(JSON.stringify(options, null, 2)).toEqual( - JSON.stringify({ knownHelpers: {} }, null, 2) - ); - }); - }); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.data.test.ts b/dev/lib/handlebars/src/spec/index.data.test.ts deleted file mode 100644 index 94d3b51c..00000000 --- a/dev/lib/handlebars/src/spec/index.data.test.ts +++ /dev/null @@ -1,269 +0,0 @@ -/* - * 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, { type HelperOptions } from '../..'; -import { expectTemplate } from '../__jest__/test_bench'; - -describe('data', () => { - it('passing in data to a compiled function that expects data - works with helpers', () => { - expectTemplate('{{hello}}') - .withCompileOptions({ data: true }) - .withHelper('hello', function (this: any, options) { - return options.data.adjective + ' ' + this.noun; - }) - .withRuntimeOptions({ data: { adjective: 'happy' } }) - .withInput({ noun: 'cat' }) - .toCompileTo('happy cat'); - }); - - it('data can be looked up via @foo', () => { - expectTemplate('{{@hello}}') - .withRuntimeOptions({ data: { hello: 'hello' } }) - .toCompileTo('hello'); - }); - - it('deep @foo triggers automatic top-level data', () => { - global.kbnHandlebarsEnv = Handlebars.create(); - const helpers = Handlebars.createFrame(kbnHandlebarsEnv!.helpers); - - helpers.let = function (options: HelperOptions) { - const frame = Handlebars.createFrame(options.data); - - for (const prop in options.hash) { - if (prop in options.hash) { - frame[prop] = options.hash[prop]; - } - } - return options.fn(this, { data: frame }); - }; - - expectTemplate( - '{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}' - ) - .withInput({ foo: true }) - .withHelpers(helpers) - .toCompileTo('Hello world'); - - global.kbnHandlebarsEnv = null; - }); - - it('parameter data can be looked up via @foo', () => { - expectTemplate('{{hello @world}}') - .withRuntimeOptions({ data: { world: 'world' } }) - .withHelper('hello', function (noun) { - return 'Hello ' + noun; - }) - .toCompileTo('Hello world'); - }); - - it('hash values can be looked up via @foo', () => { - expectTemplate('{{hello noun=@world}}') - .withRuntimeOptions({ data: { world: 'world' } }) - .withHelper('hello', function (options) { - return 'Hello ' + options.hash.noun; - }) - .toCompileTo('Hello world'); - }); - - it('nested parameter data can be looked up via @foo.bar', () => { - expectTemplate('{{hello @world.bar}}') - .withRuntimeOptions({ data: { world: { bar: 'world' } } }) - .withHelper('hello', function (noun) { - return 'Hello ' + noun; - }) - .toCompileTo('Hello world'); - }); - - it('nested parameter data does not fail with @world.bar', () => { - expectTemplate('{{hello @world.bar}}') - .withRuntimeOptions({ data: { foo: { bar: 'world' } } }) - .withHelper('hello', function (noun) { - return 'Hello ' + noun; - }) - .toCompileTo('Hello undefined'); - }); - - it('parameter data throws when using complex scope references', () => { - expectTemplate('{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}').toThrow(Error); - }); - - it('data can be functions', () => { - expectTemplate('{{@hello}}') - .withRuntimeOptions({ - data: { - hello() { - return 'hello'; - }, - }, - }) - .toCompileTo('hello'); - }); - - it('data can be functions with params', () => { - expectTemplate('{{@hello "hello"}}') - .withRuntimeOptions({ - data: { - hello(arg: any) { - return arg; - }, - }, - }) - .toCompileTo('hello'); - }); - - it('data is inherited downstream', () => { - expectTemplate( - '{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}' - ) - .withInput({ bar: { baz: 'hello world' } }) - .withCompileOptions({ data: true }) - .withHelper('let', function (this: any, options) { - const frame = Handlebars.createFrame(options.data); - for (const prop in options.hash) { - if (prop in options.hash) { - frame[prop] = options.hash[prop]; - } - } - return options.fn(this, { data: frame }); - }) - .withRuntimeOptions({ 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: 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 }) - .withHelper('hello', function (this: any, noun, options) { - return options.data.adjective + ' ' + noun + (this.exclaim ? '!' : ''); - }) - .withInput({ exclaim: true, world: 'world' }) - .withRuntimeOptions({ data: { adjective: 'happy' } }) - .toCompileTo('happy world!'); - }); - - it('passing in data to a compiled function that expects data - works with block helpers', () => { - expectTemplate('{{#hello}}{{world}}{{/hello}}') - .withCompileOptions({ - data: true, - }) - .withHelper('hello', function (this: any, options) { - return options.fn(this); - }) - .withHelper('world', function (this: any, options) { - return options.data.adjective + ' world' + (this.exclaim ? '!' : ''); - }) - .withInput({ exclaim: true }) - .withRuntimeOptions({ data: { adjective: 'happy' } }) - .toCompileTo('happy world!'); - }); - - it('passing in data to a compiled function that expects data - works with block helpers that use ..', () => { - expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}') - .withCompileOptions({ data: true }) - .withHelper('hello', function (options) { - return options.fn({ exclaim: '?' }); - }) - .withHelper('world', function (this: any, thing, options) { - return options.data.adjective + ' ' + thing + (this.exclaim || ''); - }) - .withInput({ exclaim: true, zomg: 'world' }) - .withRuntimeOptions({ data: { adjective: 'happy' } }) - .toCompileTo('happy world?'); - }); - - it('passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..', () => { - expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}') - .withCompileOptions({ data: true }) - .withHelper('hello', function (options) { - return options.data.accessData + ' ' + options.fn({ exclaim: '?' }); - }) - .withHelper('world', function (this: any, thing, options) { - return options.data.adjective + ' ' + thing + (this.exclaim || ''); - }) - .withInput({ exclaim: true, zomg: 'world' }) - .withRuntimeOptions({ data: { adjective: 'happy', accessData: '#win' } }) - .toCompileTo('#win happy world?'); - }); - - it('you can override inherited data when invoking a helper', () => { - expectTemplate('{{#hello}}{{world zomg}}{{/hello}}') - .withCompileOptions({ data: true }) - .withHelper('hello', function (options) { - return options.fn({ exclaim: '?', zomg: 'world' }, { data: { adjective: 'sad' } }); - }) - .withHelper('world', function (this: any, thing, options) { - return options.data.adjective + ' ' + thing + (this.exclaim || ''); - }) - .withInput({ exclaim: true, zomg: 'planet' }) - .withRuntimeOptions({ data: { adjective: 'happy' } }) - .toCompileTo('sad world?'); - }); - - it('you can override inherited data when invoking a helper with depth', () => { - expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}') - .withCompileOptions({ data: true }) - .withHelper('hello', function (options) { - return options.fn({ exclaim: '?' }, { data: { adjective: 'sad' } }); - }) - .withHelper('world', function (this: any, thing, options) { - return options.data.adjective + ' ' + thing + (this.exclaim || ''); - }) - .withInput({ exclaim: true, zomg: 'world' }) - .withRuntimeOptions({ data: { adjective: 'happy' } }) - .toCompileTo('sad world?'); - }); - - describe('@root', () => { - it('the root context can be looked up via @root', () => { - expectTemplate('{{@root.foo}}') - .withInput({ foo: 'hello' }) - .withRuntimeOptions({ data: {} }) - .toCompileTo('hello'); - - expectTemplate('{{@root.foo}}').withInput({ foo: 'hello' }).toCompileTo('hello'); - }); - - it('passed root values take priority', () => { - expectTemplate('{{@root.foo}}') - .withInput({ foo: 'should not be used' }) - .withRuntimeOptions({ data: { root: { foo: 'hello' } } }) - .toCompileTo('hello'); - }); - }); - - describe('nesting', () => { - it('the root context can be looked up via @root', () => { - expectTemplate( - '{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}' - ) - .withInput({ foo: 'hello' }) - .withHelper('helper', function (this: any, options) { - const frame = Handlebars.createFrame(options.data); - frame.depth = options.data.depth + 1; - return options.fn(this, { data: frame }); - }) - .withRuntimeOptions({ - data: { - depth: 0, - }, - }) - .toCompileTo('2 1 0'); - }); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.helpers.test.ts b/dev/lib/handlebars/src/spec/index.helpers.test.ts deleted file mode 100644 index 4cfa39bb..00000000 --- a/dev/lib/handlebars/src/spec/index.helpers.test.ts +++ /dev/null @@ -1,958 +0,0 @@ -/* - * 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, { type HelperOptions } from '../..'; -import { expectTemplate } from '../__jest__/test_bench'; - -beforeEach(() => { - global.kbnHandlebarsEnv = Handlebars.create(); -}); - -afterEach(() => { - global.kbnHandlebarsEnv = null; -}); - -describe('helpers', () => { - it('helper with complex lookup$', () => { - expectTemplate('{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}') - .withInput({ - prefix: '/root', - goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], - }) - .withHelper('link', function (this: any, prefix) { - return '<a href="' + prefix + '/' + this.url + '">' + this.text + '</a>'; - }) - .toCompileTo('<a href="/root/goodbye">Goodbye</a>'); - }); - - it('helper for raw block gets raw content', () => { - expectTemplate('{{{{raw}}}} {{test}} {{{{/raw}}}}') - .withInput({ test: 'hello' }) - .withHelper('raw', function (options: HelperOptions) { - return options.fn(); - }) - .toCompileTo(' {{test}} '); - }); - - it('helper for raw block gets parameters', () => { - expectTemplate('{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}') - .withInput({ test: 'hello' }) - .withHelper('raw', function (a, b, c, options: HelperOptions) { - const ret = options.fn() + a + b + c; - return ret; - }) - .toCompileTo(' {{test}} 123'); - }); - - describe('raw block parsing (with identity helper-function)', () => { - function runWithIdentityHelper(template: string, expected: string) { - expectTemplate(template) - .withHelper('identity', function (options: HelperOptions) { - return options.fn(); - }) - .toCompileTo(expected); - } - - it('helper for nested raw block gets raw content', () => { - runWithIdentityHelper( - '{{{{identity}}}} {{{{b}}}} {{{{/b}}}} {{{{/identity}}}}', - ' {{{{b}}}} {{{{/b}}}} ' - ); - }); - - it('helper for nested raw block works with empty content', () => { - runWithIdentityHelper('{{{{identity}}}}{{{{/identity}}}}', ''); - }); - - it.skip('helper for nested raw block works if nested raw blocks are broken', () => { - // This test was introduced in 4.4.4, but it was not the actual problem that lead to the patch release - // The test is deactivated, because in 3.x this template cases an exception and it also does not work in 4.4.3 - // If anyone can make this template work without breaking everything else, then go for it, - // but for now, this is just a known bug, that will be documented. - runWithIdentityHelper( - '{{{{identity}}}} {{{{a}}}} {{{{ {{{{/ }}}} }}}} {{{{/identity}}}}', - ' {{{{a}}}} {{{{ {{{{/ }}}} }}}} ' - ); - }); - - it('helper for nested raw block closes after first matching close', () => { - runWithIdentityHelper( - '{{{{identity}}}}abc{{{{/identity}}}} {{{{identity}}}}abc{{{{/identity}}}}', - 'abc abc' - ); - }); - - it('helper for nested raw block throw exception when with missing closing braces', () => { - const string = '{{{{a}}}} {{{{/a'; - expectTemplate(string).toThrow(); - }); - }); - - it('helper block with identical context', () => { - expectTemplate('{{#goodbyes}}{{name}}{{/goodbyes}}') - .withInput({ name: 'Alan' }) - .withHelper('goodbyes', function (this: any, options: HelperOptions) { - let out = ''; - const byes = ['Goodbye', 'goodbye', 'GOODBYE']; - for (let i = 0, j = byes.length; i < j; i++) { - out += byes[i] + ' ' + options.fn(this) + '! '; - } - return out; - }) - .toCompileTo('Goodbye Alan! goodbye Alan! GOODBYE Alan! '); - }); - - it('helper block with complex lookup expression', () => { - expectTemplate('{{#goodbyes}}{{../name}}{{/goodbyes}}') - .withInput({ name: 'Alan' }) - .withHelper('goodbyes', function (options: HelperOptions) { - let out = ''; - const byes = ['Goodbye', 'goodbye', 'GOODBYE']; - for (let i = 0, j = byes.length; i < j; i++) { - out += byes[i] + ' ' + options.fn({}) + '! '; - } - return out; - }) - .toCompileTo('Goodbye Alan! goodbye Alan! GOODBYE Alan! '); - }); - - it('helper with complex lookup and nested template', () => { - expectTemplate('{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}') - .withInput({ - prefix: '/root', - goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], - }) - .withHelper('link', function (this: any, prefix, options: HelperOptions) { - return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>'; - }) - .toCompileTo('<a href="/root/goodbye">Goodbye</a>'); - }); - - it('helper with complex lookup and nested template in VM+Compiler', () => { - expectTemplate('{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}') - .withInput({ - prefix: '/root', - goodbyes: [{ text: 'Goodbye', url: 'goodbye' }], - }) - .withHelper('link', function (this: any, prefix, options: HelperOptions) { - return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>'; - }) - .toCompileTo('<a href="/root/goodbye">Goodbye</a>'); - }); - - it('helper returning undefined value', () => { - expectTemplate(' {{nothere}}') - .withHelpers({ - nothere() {}, - }) - .toCompileTo(' '); - - expectTemplate(' {{#nothere}}{{/nothere}}') - .withHelpers({ - nothere() {}, - }) - .toCompileTo(' '); - }); - - it('block helper', () => { - expectTemplate('{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!') - .withInput({ world: 'world' }) - .withHelper('goodbyes', function (options: HelperOptions) { - return options.fn({ text: 'GOODBYE' }); - }) - .toCompileTo('GOODBYE! cruel world!'); - }); - - it('block helper staying in the same context', () => { - expectTemplate('{{#form}}<p>{{name}}</p>{{/form}}') - .withInput({ name: 'Yehuda' }) - .withHelper('form', function (this: any, options: HelperOptions) { - return '<form>' + options.fn(this) + '</form>'; - }) - .toCompileTo('<form><p>Yehuda</p></form>'); - }); - - it('block helper should have context in this', () => { - function link(this: any, options: HelperOptions) { - return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>'; - } - - expectTemplate('<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>') - .withInput({ - people: [ - { name: 'Alan', id: 1 }, - { name: 'Yehuda', id: 2 }, - ], - }) - .withHelper('link', link) - .toCompileTo( - '<ul><li><a href="/people/1">Alan</a></li><li><a href="/people/2">Yehuda</a></li></ul>' - ); - }); - - it('block helper for undefined value', () => { - expectTemplate("{{#empty}}shouldn't render{{/empty}}").toCompileTo(''); - }); - - it('block helper passing a new context', () => { - expectTemplate('{{#form yehuda}}<p>{{name}}</p>{{/form}}') - .withInput({ yehuda: { name: 'Yehuda' } }) - .withHelper('form', function (context, options: HelperOptions) { - return '<form>' + options.fn(context) + '</form>'; - }) - .toCompileTo('<form><p>Yehuda</p></form>'); - }); - - it('block helper passing a complex path context', () => { - expectTemplate('{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}') - .withInput({ yehuda: { name: 'Yehuda', cat: { name: 'Harold' } } }) - .withHelper('form', function (context, options: HelperOptions) { - return '<form>' + options.fn(context) + '</form>'; - }) - .toCompileTo('<form><p>Harold</p></form>'); - }); - - it('nested block helpers', () => { - expectTemplate('{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}') - .withInput({ - yehuda: { name: 'Yehuda' }, - }) - .withHelper('link', function (this: any, options: HelperOptions) { - return '<a href="' + this.name + '">' + options.fn(this) + '</a>'; - }) - .withHelper('form', function (context, options: HelperOptions) { - return '<form>' + options.fn(context) + '</form>'; - }) - .toCompileTo('<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>'); - }); - - it('block helper inverted sections', () => { - const string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}"; - function list(this: any, context: any, options: HelperOptions) { - if (context.length > 0) { - let out = '<ul>'; - for (let i = 0, j = context.length; i < j; i++) { - out += '<li>'; - out += options.fn(context[i]); - out += '</li>'; - } - out += '</ul>'; - return out; - } else { - return '<p>' + options.inverse(this) + '</p>'; - } - } - - // the meaning here may be kind of hard to catch, but list.not is always called, - // so we should see the output of both - expectTemplate(string) - .withInput({ people: [{ name: 'Alan' }, { name: 'Yehuda' }] }) - .withHelpers({ list }) - .toCompileTo('<ul><li>Alan</li><li>Yehuda</li></ul>'); - - expectTemplate(string) - .withInput({ people: [] }) - .withHelpers({ list }) - .toCompileTo("<p><em>Nobody's here</em></p>"); - - expectTemplate('{{#list people}}Hello{{^}}{{message}}{{/list}}') - .withInput({ - people: [], - message: "Nobody's here", - }) - .withHelpers({ list }) - .toCompileTo('<p>Nobody's here</p>'); - }); - - it('pathed lambas with parameters', () => { - const hash = { - helper: () => 'winning', - }; - // @ts-expect-error - hash.hash = hash; - - const helpers = { - './helper': () => 'fail', - }; - - expectTemplate('{{./helper 1}}').withInput(hash).withHelpers(helpers).toCompileTo('winning'); - expectTemplate('{{hash/helper 1}}').withInput(hash).withHelpers(helpers).toCompileTo('winning'); - }); - - describe('helpers hash', () => { - it('providing a helpers hash', () => { - expectTemplate('Goodbye {{cruel}} {{world}}!') - .withInput({ cruel: 'cruel' }) - .withHelpers({ - world() { - return 'world'; - }, - }) - .toCompileTo('Goodbye cruel world!'); - - expectTemplate('Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!') - .withInput({ iter: [{ cruel: 'cruel' }] }) - .withHelpers({ - world() { - return 'world'; - }, - }) - .toCompileTo('Goodbye cruel world!'); - }); - - it('in cases of conflict, helpers win', () => { - expectTemplate('{{{lookup}}}') - .withInput({ lookup: 'Explicit' }) - .withHelpers({ - lookup() { - return 'helpers'; - }, - }) - .toCompileTo('helpers'); - - expectTemplate('{{lookup}}') - .withInput({ lookup: 'Explicit' }) - .withHelpers({ - lookup() { - return 'helpers'; - }, - }) - .toCompileTo('helpers'); - }); - - it('the helpers hash is available is nested contexts', () => { - expectTemplate('{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}') - .withInput({ outer: { inner: { unused: [] } } }) - .withHelpers({ - helper() { - return 'helper'; - }, - }) - .toCompileTo('helper'); - }); - - it('the helper hash should augment the global hash', () => { - kbnHandlebarsEnv!.registerHelper('test_helper', function () { - return 'found it!'; - }); - - expectTemplate('{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}') - .withInput({ cruel: 'cruel' }) - .withHelpers({ - world() { - return 'world!'; - }, - }) - .toCompileTo('found it! Goodbye cruel world!!'); - }); - }); - - describe('registration', () => { - it('unregisters', () => { - deleteAllKeys(kbnHandlebarsEnv!.helpers); - - kbnHandlebarsEnv!.registerHelper('foo', function () { - return 'fail'; - }); - expect(kbnHandlebarsEnv!.helpers.foo).toBeDefined(); - kbnHandlebarsEnv!.unregisterHelper('foo'); - expect(kbnHandlebarsEnv!.helpers.foo).toBeUndefined(); - }); - - it('allows multiple globals', () => { - const ifHelper = kbnHandlebarsEnv!.helpers.if; - deleteAllKeys(kbnHandlebarsEnv!.helpers); - - kbnHandlebarsEnv!.registerHelper({ - if: ifHelper, - world() { - return 'world!'; - }, - testHelper() { - return 'found it!'; - }, - }); - - expectTemplate('{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}') - .withInput({ cruel: 'cruel' }) - .toCompileTo('found it! Goodbye cruel world!!'); - }); - - it('fails with multiple and args', () => { - expect(() => { - kbnHandlebarsEnv!.registerHelper( - // @ts-expect-error TypeScript is complaining about the invalid input just as the thrown error - { - world() { - return 'world!'; - }, - testHelper() { - return 'found it!'; - }, - }, - {} - ); - }).toThrow('Arg not supported with multiple helpers'); - }); - }); - - it('decimal number literals work', () => { - expectTemplate('Message: {{hello -1.2 1.2}}') - .withHelper('hello', function (times, times2) { - if (typeof times !== 'number') { - times = 'NaN'; - } - if (typeof times2 !== 'number') { - times2 = 'NaN'; - } - return 'Hello ' + times + ' ' + times2 + ' times'; - }) - .toCompileTo('Message: Hello -1.2 1.2 times'); - }); - - it('negative number literals work', () => { - expectTemplate('Message: {{hello -12}}') - .withHelper('hello', function (times) { - if (typeof times !== 'number') { - times = 'NaN'; - } - return 'Hello ' + times + ' times'; - }) - .toCompileTo('Message: Hello -12 times'); - }); - - describe('String literal parameters', () => { - it('simple literals work', () => { - expectTemplate('Message: {{hello "world" 12 true false}}') - .withHelper('hello', function (param, times, bool1, bool2) { - if (typeof times !== 'number') { - times = 'NaN'; - } - if (typeof bool1 !== 'boolean') { - bool1 = 'NaB'; - } - if (typeof bool2 !== 'boolean') { - bool2 = 'NaB'; - } - return 'Hello ' + param + ' ' + times + ' times: ' + bool1 + ' ' + bool2; - }) - .toCompileTo('Message: Hello world 12 times: true false'); - }); - - it('using a quote in the middle of a parameter raises an error', () => { - expectTemplate('Message: {{hello wo"rld"}}').toThrow(Error); - }); - - it('escaping a String is possible', () => { - expectTemplate('Message: {{{hello "\\"world\\""}}}') - .withHelper('hello', function (param) { - return 'Hello ' + param; - }) - .toCompileTo('Message: Hello "world"'); - }); - - it("it works with ' marks", () => { - expectTemplate('Message: {{{hello "Alan\'s world"}}}') - .withHelper('hello', function (param) { - return 'Hello ' + param; - }) - .toCompileTo("Message: Hello Alan's world"); - }); - }); - - describe('multiple parameters', () => { - it('simple multi-params work', () => { - expectTemplate('Message: {{goodbye cruel world}}') - .withInput({ cruel: 'cruel', world: 'world' }) - .withHelper('goodbye', function (cruel, world) { - return 'Goodbye ' + cruel + ' ' + world; - }) - .toCompileTo('Message: Goodbye cruel world'); - }); - - it('block multi-params work', () => { - expectTemplate('Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}') - .withInput({ cruel: 'cruel', world: 'world' }) - .withHelper('goodbye', function (cruel, world, options: HelperOptions) { - return options.fn({ greeting: 'Goodbye', adj: cruel, noun: world }); - }) - .toCompileTo('Message: Goodbye cruel world'); - }); - }); - - describe('hash', () => { - it('helpers can take an optional hash', () => { - expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" times=12}}') - .withHelper('goodbye', function (options: HelperOptions) { - return ( - 'GOODBYE ' + - options.hash.cruel + - ' ' + - options.hash.world + - ' ' + - options.hash.times + - ' TIMES' - ); - }) - .toCompileTo('GOODBYE CRUEL WORLD 12 TIMES'); - }); - - it('helpers can take an optional hash with booleans', () => { - function goodbye(options: HelperOptions) { - if (options.hash.print === true) { - return 'GOODBYE ' + options.hash.cruel + ' ' + options.hash.world; - } else if (options.hash.print === false) { - return 'NOT PRINTING'; - } else { - return 'THIS SHOULD NOT HAPPEN'; - } - } - - expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" print=true}}') - .withHelper('goodbye', goodbye) - .toCompileTo('GOODBYE CRUEL WORLD'); - - expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" print=false}}') - .withHelper('goodbye', goodbye) - .toCompileTo('NOT PRINTING'); - }); - - it('block helpers can take an optional hash', () => { - expectTemplate('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}') - .withHelper('goodbye', function (this: any, options: HelperOptions) { - return ( - 'GOODBYE ' + - options.hash.cruel + - ' ' + - options.fn(this) + - ' ' + - options.hash.times + - ' TIMES' - ); - }) - .toCompileTo('GOODBYE CRUEL world 12 TIMES'); - }); - - it('block helpers can take an optional hash with single quoted stings', () => { - expectTemplate('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}') - .withHelper('goodbye', function (this: any, options: HelperOptions) { - return ( - 'GOODBYE ' + - options.hash.cruel + - ' ' + - options.fn(this) + - ' ' + - options.hash.times + - ' TIMES' - ); - }) - .toCompileTo('GOODBYE CRUEL world 12 TIMES'); - }); - - it('block helpers can take an optional hash with booleans', () => { - function goodbye(this: any, options: HelperOptions) { - if (options.hash.print === true) { - return 'GOODBYE ' + options.hash.cruel + ' ' + options.fn(this); - } else if (options.hash.print === false) { - return 'NOT PRINTING'; - } else { - return 'THIS SHOULD NOT HAPPEN'; - } - } - - expectTemplate('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}') - .withHelper('goodbye', goodbye) - .toCompileTo('GOODBYE CRUEL world'); - - expectTemplate('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}') - .withHelper('goodbye', goodbye) - .toCompileTo('NOT PRINTING'); - }); - }); - - describe('helperMissing', () => { - it('if a context is not found, helperMissing is used', () => { - expectTemplate('{{hello}} {{link_to world}}').toThrow(/Missing helper: "link_to"/); - }); - - it('if a context is not found, custom helperMissing is used', () => { - expectTemplate('{{hello}} {{link_to world}}') - .withInput({ hello: 'Hello', world: 'world' }) - .withHelper('helperMissing', function (mesg, options: HelperOptions) { - if (options.name === 'link_to') { - return new Handlebars.SafeString('<a>' + mesg + '</a>'); - } - }) - .toCompileTo('Hello <a>world</a>'); - }); - - it('if a value is not found, custom helperMissing is used', () => { - expectTemplate('{{hello}} {{link_to}}') - .withInput({ hello: 'Hello', world: 'world' }) - .withHelper('helperMissing', function (options: HelperOptions) { - if (options.name === 'link_to') { - return new Handlebars.SafeString('<a>winning</a>'); - } - }) - .toCompileTo('Hello <a>winning</a>'); - }); - }); - - describe('knownHelpers', () => { - it('Known helper should render helper', () => { - expectTemplate('{{hello}}') - .withCompileOptions({ - knownHelpers: { hello: true }, - }) - .withHelper('hello', function () { - return 'foo'; - }) - .toCompileTo('foo'); - }); - - it('Unknown helper in knownHelpers only mode should be passed as undefined', () => { - expectTemplate('{{typeof hello}}') - .withCompileOptions({ - knownHelpers: { typeof: true }, - knownHelpersOnly: true, - }) - .withHelper('typeof', function (arg) { - return typeof arg; - }) - .withHelper('hello', function () { - return 'foo'; - }) - .toCompileTo('undefined'); - }); - - it('Builtin helpers available in knownHelpers only mode', () => { - expectTemplate('{{#unless foo}}bar{{/unless}}') - .withCompileOptions({ - knownHelpersOnly: true, - }) - .toCompileTo('bar'); - }); - - it('Field lookup works in knownHelpers only mode', () => { - expectTemplate('{{foo}}') - .withCompileOptions({ - knownHelpersOnly: true, - }) - .withInput({ foo: 'bar' }) - .toCompileTo('bar'); - }); - - it('Conditional blocks work in knownHelpers only mode', () => { - expectTemplate('{{#foo}}bar{{/foo}}') - .withCompileOptions({ - knownHelpersOnly: true, - }) - .withInput({ foo: 'baz' }) - .toCompileTo('bar'); - }); - - it('Invert blocks work in knownHelpers only mode', () => { - expectTemplate('{{^foo}}bar{{/foo}}') - .withCompileOptions({ - knownHelpersOnly: true, - }) - .withInput({ foo: false }) - .toCompileTo('bar'); - }); - - it('Functions are bound to the context in knownHelpers only mode', () => { - expectTemplate('{{foo}}') - .withCompileOptions({ - knownHelpersOnly: true, - }) - .withInput({ - foo() { - return this.bar; - }, - bar: 'bar', - }) - .toCompileTo('bar'); - }); - - it('Unknown helper call in knownHelpers only mode should throw', () => { - expectTemplate('{{typeof hello}}') - .withCompileOptions({ knownHelpersOnly: true }) - .toThrow(Error); - }); - }); - - describe('blockHelperMissing', () => { - it('lambdas are resolved by blockHelperMissing, not handlebars proper', () => { - expectTemplate('{{#truthy}}yep{{/truthy}}') - .withInput({ - truthy() { - return true; - }, - }) - .toCompileTo('yep'); - }); - - it('lambdas resolved by blockHelperMissing are bound to the context', () => { - expectTemplate('{{#truthy}}yep{{/truthy}}') - .withInput({ - truthy() { - return this.truthiness(); - }, - truthiness() { - return false; - }, - }) - .toCompileTo(''); - }); - }); - - describe('name field', () => { - const helpers = { - blockHelperMissing(...args: any[]) { - return 'missing: ' + args[args.length - 1].name; - }, - helperMissing(...args: any[]) { - return 'helper missing: ' + args[args.length - 1].name; - }, - helper(...args: any[]) { - return 'ran: ' + args[args.length - 1].name; - }, - }; - - it('should include in ambiguous mustache calls', () => { - expectTemplate('{{helper}}').withHelpers(helpers).toCompileTo('ran: helper'); - }); - - it('should include in helper mustache calls', () => { - expectTemplate('{{helper 1}}').withHelpers(helpers).toCompileTo('ran: helper'); - }); - - it('should include in ambiguous block calls', () => { - expectTemplate('{{#helper}}{{/helper}}').withHelpers(helpers).toCompileTo('ran: helper'); - }); - - it('should include in simple block calls', () => { - expectTemplate('{{#./helper}}{{/./helper}}') - .withHelpers(helpers) - .toCompileTo('missing: ./helper'); - }); - - it('should include in helper block calls', () => { - expectTemplate('{{#helper 1}}{{/helper}}').withHelpers(helpers).toCompileTo('ran: helper'); - }); - - it('should include in known helper calls', () => { - expectTemplate('{{helper}}') - .withCompileOptions({ - knownHelpers: { helper: true }, - knownHelpersOnly: true, - }) - .withHelpers(helpers) - .toCompileTo('ran: helper'); - }); - - it('should include full id', () => { - expectTemplate('{{#foo.helper}}{{/foo.helper}}') - .withInput({ foo: {} }) - .withHelpers(helpers) - .toCompileTo('missing: foo.helper'); - }); - - it('should include full id if a hash is passed', () => { - expectTemplate('{{#foo.helper bar=baz}}{{/foo.helper}}') - .withInput({ foo: {} }) - .withHelpers(helpers) - .toCompileTo('helper missing: foo.helper'); - }); - }); - - describe('name conflicts', () => { - it('helpers take precedence over same-named context properties', () => { - expectTemplate('{{goodbye}} {{cruel world}}') - .withHelper('goodbye', function (this: any) { - return this.goodbye.toUpperCase(); - }) - .withHelper('cruel', function (world) { - return 'cruel ' + world.toUpperCase(); - }) - .withInput({ - goodbye: 'goodbye', - world: 'world', - }) - .toCompileTo('GOODBYE cruel WORLD'); - }); - - it('helpers take precedence over same-named context properties$', () => { - expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}}') - .withHelper('goodbye', function (this: any, options: HelperOptions) { - return this.goodbye.toUpperCase() + options.fn(this); - }) - .withHelper('cruel', function (world) { - return 'cruel ' + world.toUpperCase(); - }) - .withInput({ - goodbye: 'goodbye', - world: 'world', - }) - .toCompileTo('GOODBYE cruel WORLD'); - }); - - it('Scoped names take precedence over helpers', () => { - expectTemplate('{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}') - .withHelper('goodbye', function (this: any) { - return this.goodbye.toUpperCase(); - }) - .withHelper('cruel', function (world) { - return 'cruel ' + world.toUpperCase(); - }) - .withInput({ - goodbye: 'goodbye', - world: 'world', - }) - .toCompileTo('goodbye cruel WORLD cruel GOODBYE'); - }); - - it('Scoped names take precedence over block helpers', () => { - expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}') - .withHelper('goodbye', function (this: any, options: HelperOptions) { - return this.goodbye.toUpperCase() + options.fn(this); - }) - .withHelper('cruel', function (world) { - return 'cruel ' + world.toUpperCase(); - }) - .withInput({ - goodbye: 'goodbye', - world: 'world', - }) - .toCompileTo('GOODBYE cruel WORLD goodbye'); - }); - }); - - describe('block params', () => { - it('should take presedence over context values', () => { - expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}') - .withInput({ value: 'foo' }) - .withHelper('goodbyes', function (options: HelperOptions) { - expect(options.fn.blockParams).toEqual(1); - return options.fn({ value: 'bar' }, { blockParams: [1, 2] }); - }) - .toCompileTo('1foo'); - }); - - it('should take presedence over helper values', () => { - expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}') - .withHelper('value', function () { - return 'foo'; - }) - .withHelper('goodbyes', function (options: HelperOptions) { - expect(options.fn.blockParams).toEqual(1); - return options.fn({}, { blockParams: [1, 2] }); - }) - .toCompileTo('1foo'); - }); - - it('should not take presedence over pathed values', () => { - expectTemplate('{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}') - .withInput({ value: 'bar' }) - .withHelper('value', function () { - return 'foo'; - }) - .withHelper('goodbyes', function (this: any, options: HelperOptions) { - expect(options.fn.blockParams).toEqual(1); - return options.fn(this, { blockParams: [1, 2] }); - }) - .toCompileTo('barfoo'); - }); - - it('should take presednece over parent block params', () => { - let value: number; - expectTemplate( - '{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}', - { - beforeEach() { - value = 1; - }, - } - ) - .withInput({ value: 'foo' }) - .withHelper('goodbyes', function (options: HelperOptions) { - return options.fn( - { value: 'bar' }, - { - blockParams: options.fn.blockParams === 1 ? [value++, value++] : undefined, - } - ); - }) - .toCompileTo('13foo'); - }); - - it('should allow block params on chained helpers', () => { - expectTemplate('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}') - .withInput({ value: 'foo' }) - .withHelper('goodbyes', function (options: HelperOptions) { - expect(options.fn.blockParams).toEqual(1); - return options.fn({ value: 'bar' }, { blockParams: [1, 2] }); - }) - .toCompileTo('1foo'); - }); - }); - - describe('built-in helpers malformed arguments ', () => { - it('if helper - too few arguments', () => { - expectTemplate('{{#if}}{{/if}}').toThrow(/#if requires exactly one argument/); - }); - - it('if helper - too many arguments, string', () => { - expectTemplate('{{#if test "string"}}{{/if}}').toThrow(/#if requires exactly one argument/); - }); - - it('if helper - too many arguments, undefined', () => { - expectTemplate('{{#if test undefined}}{{/if}}').toThrow(/#if requires exactly one argument/); - }); - - it('if helper - too many arguments, null', () => { - expectTemplate('{{#if test null}}{{/if}}').toThrow(/#if requires exactly one argument/); - }); - - it('unless helper - too few arguments', () => { - expectTemplate('{{#unless}}{{/unless}}').toThrow(/#unless requires exactly one argument/); - }); - - it('unless helper - too many arguments', () => { - expectTemplate('{{#unless test null}}{{/unless}}').toThrow( - /#unless requires exactly one argument/ - ); - }); - - it('with helper - too few arguments', () => { - expectTemplate('{{#with}}{{/with}}').toThrow(/#with requires exactly one argument/); - }); - - it('with helper - too many arguments', () => { - expectTemplate('{{#with test "string"}}{{/with}}').toThrow( - /#with requires exactly one argument/ - ); - }); - }); - - describe('the lookupProperty-option', () => { - it('should be passed to custom helpers', () => { - expectTemplate('{{testHelper}}') - .withHelper('testHelper', function testHelper(this: any, options: HelperOptions) { - return options.lookupProperty(this, 'testProperty'); - }) - .withInput({ testProperty: 'abc' }) - .toCompileTo('abc'); - }); - }); -}); - -function deleteAllKeys(obj: { [key: string]: any }) { - for (const key of Object.keys(obj)) { - delete obj[key]; - } -} diff --git a/dev/lib/handlebars/src/spec/index.partials.test.ts b/dev/lib/handlebars/src/spec/index.partials.test.ts deleted file mode 100644 index 65930d06..00000000 --- a/dev/lib/handlebars/src/spec/index.partials.test.ts +++ /dev/null @@ -1,591 +0,0 @@ -/* - * 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(() => { - kbnHandlebarsEnv!.registerPartial('undefined_test', undefined as any); - }).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: { - 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'); - }); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.regressions.test.ts b/dev/lib/handlebars/src/spec/index.regressions.test.ts deleted file mode 100644 index fc2065fe..00000000 --- a/dev/lib/handlebars/src/spec/index.regressions.test.ts +++ /dev/null @@ -1,379 +0,0 @@ -/* - * 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, { type HelperOptions } from '../..'; -import { expectTemplate, forEachCompileFunctionName } from '../__jest__/test_bench'; - -describe('Regressions', () => { - it('GH-94: Cannot read property of undefined', () => { - expectTemplate('{{#books}}{{title}}{{author.name}}{{/books}}') - .withInput({ - books: [ - { - title: 'The origin of species', - author: { - name: 'Charles Darwin', - }, - }, - { - title: 'Lazarillo de Tormes', - }, - ], - }) - .toCompileTo('The origin of speciesCharles DarwinLazarillo de Tormes'); - }); - - it("GH-150: Inverted sections print when they shouldn't", () => { - const string = '{{^set}}not set{{/set}} :: {{#set}}set{{/set}}'; - expectTemplate(string).toCompileTo('not set :: '); - expectTemplate(string).withInput({ set: undefined }).toCompileTo('not set :: '); - expectTemplate(string).withInput({ set: false }).toCompileTo('not set :: '); - expectTemplate(string).withInput({ set: true }).toCompileTo(' :: set'); - }); - - it('GH-158: Using array index twice, breaks the template', () => { - expectTemplate('{{arr.[0]}}, {{arr.[1]}}') - .withInput({ arr: [1, 2] }) - .toCompileTo('1, 2'); - }); - - it("bug reported by @fat where lambdas weren't being properly resolved", () => { - const string = - '<strong>This is a slightly more complicated {{thing}}.</strong>.\n' + - '{{! Just ignore this business. }}\n' + - 'Check this out:\n' + - '{{#hasThings}}\n' + - '<ul>\n' + - '{{#things}}\n' + - '<li class={{className}}>{{word}}</li>\n' + - '{{/things}}</ul>.\n' + - '{{/hasThings}}\n' + - '{{^hasThings}}\n' + - '\n' + - '<small>Nothing to check out...</small>\n' + - '{{/hasThings}}'; - - const data = { - thing() { - return 'blah'; - }, - things: [ - { className: 'one', word: '@fat' }, - { className: 'two', word: '@dhg' }, - { className: 'three', word: '@sayrer' }, - ], - hasThings() { - return true; - }, - }; - - const output = - '<strong>This is a slightly more complicated blah.</strong>.\n' + - 'Check this out:\n' + - '<ul>\n' + - '<li class=one>@fat</li>\n' + - '<li class=two>@dhg</li>\n' + - '<li class=three>@sayrer</li>\n' + - '</ul>.\n'; - - expectTemplate(string).withInput(data).toCompileTo(output); - }); - - it('GH-408: Multiple loops fail', () => { - expectTemplate('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}') - .withInput([ - { name: 'John Doe', location: { city: 'Chicago' } }, - { name: 'Jane Doe', location: { city: 'New York' } }, - ]) - .toCompileTo('John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe'); - }); - - it('GS-428: Nested if else rendering', () => { - const succeedingTemplate = - '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; - const failingTemplate = - '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; - - const helpers = { - blk(block: HelperOptions) { - return block.fn(''); - }, - inverse(block: HelperOptions) { - return block.inverse(''); - }, - }; - - expectTemplate(succeedingTemplate).withHelpers(helpers).toCompileTo(' Expected '); - expectTemplate(failingTemplate).withHelpers(helpers).toCompileTo(' Expected '); - }); - - it('GH-458: Scoped this identifier', () => { - expectTemplate('{{./foo}}').withInput({ foo: 'bar' }).toCompileTo('bar'); - }); - - it('GH-375: Unicode line terminators', () => { - expectTemplate('\u2028').toCompileTo('\u2028'); - }); - - it('GH-534: Object prototype aliases', () => { - /* eslint-disable no-extend-native */ - // @ts-expect-error - Object.prototype[0xd834] = true; - - expectTemplate('{{foo}}').withInput({ foo: 'bar' }).toCompileTo('bar'); - - // @ts-expect-error - delete Object.prototype[0xd834]; - /* eslint-enable no-extend-native */ - }); - - it('GH-437: Matching escaping', () => { - expectTemplate('{{{a}}').toThrow(/Parse error on/); - expectTemplate('{{a}}}').toThrow(/Parse error on/); - }); - - it('GH-676: Using array in escaping mustache fails', () => { - const data = { arr: [1, 2] }; - expectTemplate('{{arr}}').withInput(data).toCompileTo(data.arr.toString()); - }); - - it('Mustache man page', () => { - expectTemplate( - 'Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}' - ) - .withInput({ - name: 'Chris', - value: 10000, - taxed_value: 10000 - 10000 * 0.4, - in_ca: true, - }) - .toCompileTo('Hello Chris. You have just won $10000! Well, $6000, after taxes.'); - }); - - it('GH-731: zero context rendering', () => { - expectTemplate('{{#foo}} This is {{bar}} ~ {{/foo}}') - .withInput({ - foo: 0, - bar: 'OK', - }) - .toCompileTo(' This is ~ '); - }); - - it('GH-820: zero pathed rendering', () => { - expectTemplate('{{foo.bar}}').withInput({ foo: 0 }).toCompileTo(''); - }); - - it('GH-837: undefined values for helpers', () => { - expectTemplate('{{str bar.baz}}') - .withHelpers({ - str(value) { - return value + ''; - }, - }) - .toCompileTo('undefined'); - }); - - it('GH-926: Depths and de-dupe', () => { - expectTemplate( - '{{#if dater}}{{#each data}}{{../name}}{{/each}}{{else}}{{#each notData}}{{../name}}{{/each}}{{/if}}' - ) - .withInput({ - name: 'foo', - data: [1], - notData: [1], - }) - .toCompileTo('foo'); - }); - - it('GH-1021: Each empty string key', () => { - expectTemplate('{{#each data}}Key: {{@key}}\n{{/each}}') - .withInput({ - data: { - '': 'foo', - name: 'Chris', - value: 10000, - }, - }) - .toCompileTo('Key: \nKey: name\nKey: value\n'); - }); - - it('GH-1054: Should handle simple safe string responses', () => { - expectTemplate('{{#wrap}}{{>partial}}{{/wrap}}') - .withHelpers({ - wrap(options: HelperOptions) { - return new Handlebars.SafeString(options.fn()); - }, - }) - .withPartials({ - partial: '{{#wrap}}<partial>{{/wrap}}', - }) - .toCompileTo('<partial>'); - }); - - it('GH-1065: Sparse arrays', () => { - const array = []; - array[1] = 'foo'; - array[3] = 'bar'; - expectTemplate('{{#each array}}{{@index}}{{.}}{{/each}}') - .withInput({ array }) - .toCompileTo('1foo3bar'); - }); - - it('GH-1093: Undefined helper context', () => { - expectTemplate('{{#each obj}}{{{helper}}}{{.}}{{/each}}') - .withInput({ obj: { foo: undefined, bar: 'bat' } }) - .withHelpers({ - helper(this: any) { - // It's valid to execute a block against an undefined context, but - // helpers can not do so, so we expect to have an empty object here; - for (const name in this) { - if (Object.prototype.hasOwnProperty.call(this, name)) { - return 'found'; - } - } - // And to make IE happy, check for the known string as length is not enumerated. - return this === 'bat' ? 'found' : 'not'; - }, - }) - .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' + - ' 1. IF: {{#if true}}{{../name}}-{{../../name}}-{{../../../name}}{{/if}}\n' + - ' 2. MYIF: {{#myif true}}{{../name}}={{../../name}}={{../../../name}}{{/myif}}\n' + - '{{/each}}' - ) - .withInput({ array: [1], name: 'John' }) - .withHelpers({ - myif(conditional, options: HelperOptions) { - if (conditional) { - return options.fn(this); - } else { - return options.inverse(this); - } - }, - }) - .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({ - value: 'parent', - list: [null, 'a'], - }) - .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' }) - .withHelpers({ - helpa(options: HelperOptions) { - return options.hash.length; - }, - }) - .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}}') - .withHelper('foo', undefined as any) - .withInput({ foo: 'bar' }) - .toCompileTo('bar'); - }); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.security.test.ts b/dev/lib/handlebars/src/spec/index.security.test.ts deleted file mode 100644 index 878a0931..00000000 --- a/dev/lib/handlebars/src/spec/index.security.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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 } from '../__jest__/test_bench'; - -describe('security issues', () => { - describe('GH-1495: Prevent Remote Code Execution via constructor', () => { - it('should not allow constructors to be accessed', () => { - expectTemplate('{{lookup (lookup this "constructor") "name"}}').withInput({}).toCompileTo(''); - expectTemplate('{{constructor.name}}').withInput({}).toCompileTo(''); - }); - - it('GH-1603: should not allow constructors to be accessed (lookup via toString)', () => { - expectTemplate('{{lookup (lookup this (list "constructor")) "name"}}') - .withInput({}) - .withHelper('list', function (element) { - return [element]; - }) - .toCompileTo(''); - }); - - it('should allow the "constructor" property to be accessed if it is an "ownProperty"', () => { - expectTemplate('{{constructor.name}}') - .withInput({ constructor: { name: 'here we go' } }) - .toCompileTo('here we go'); - - expectTemplate('{{lookup (lookup this "constructor") "name"}}') - .withInput({ constructor: { name: 'here we go' } }) - .toCompileTo('here we go'); - }); - - it('should allow the "constructor" property to be accessed if it is an "own property"', () => { - expectTemplate('{{lookup (lookup this "constructor") "name"}}') - .withInput({ constructor: { name: 'here we go' } }) - .toCompileTo('here we go'); - }); - }); - - describe('GH-1558: Prevent explicit call of helperMissing-helpers', () => { - describe('without the option "allowExplicitCallOfHelperMissing"', () => { - it('should throw an exception when calling "{{helperMissing}}" ', () => { - expectTemplate('{{helperMissing}}').toThrow(Error); - }); - - it('should throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', () => { - expectTemplate('{{#helperMissing}}{{/helperMissing}}').toThrow(Error); - }); - - it('should throw an exception when calling "{{blockHelperMissing "abc" .}}" ', () => { - const functionCalls = []; - expect(() => { - const template = Handlebars.compile('{{blockHelperMissing "abc" .}}'); - template({ - fn() { - functionCalls.push('called'); - }, - }); - }).toThrow(Error); - expect(functionCalls.length).toEqual(0); - }); - - it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', () => { - expectTemplate('{{#blockHelperMissing .}}{{/blockHelperMissing}}') - .withInput({ - fn() { - return 'functionInData'; - }, - }) - .toThrow(Error); - }); - }); - }); - - describe('GH-1563', () => { - it('should not allow to access constructor after overriding via __defineGetter__', () => { - // @ts-expect-error - if ({}.__defineGetter__ == null || {}.__lookupGetter__ == null) { - return; // Browser does not support this exploit anyway - } - expectTemplate( - '{{__defineGetter__ "undefined" valueOf }}' + - '{{#with __lookupGetter__ }}' + - '{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' + - '{{constructor.name}}' + - '{{/with}}' - ) - .withInput({}) - .toThrow(/Missing helper: "__defineGetter__"/); - }); - }); - - describe('GH-1595: dangerous properties', () => { - const templates = [ - '{{constructor}}', - '{{__defineGetter__}}', - '{{__defineSetter__}}', - '{{__lookupGetter__}}', - '{{__proto__}}', - '{{lookup this "constructor"}}', - '{{lookup this "__defineGetter__"}}', - '{{lookup this "__defineSetter__"}}', - '{{lookup this "__lookupGetter__"}}', - '{{lookup this "__proto__"}}', - ]; - - templates.forEach((template) => { - describe('access should be denied to ' + template, () => { - it('by default', () => { - expectTemplate(template).withInput({}).toCompileTo(''); - }); - }); - }); - }); - - describe('escapes template variables', () => { - it('in default mode', () => { - expectTemplate("{{'a\\b'}}").withCompileOptions().withInput({ 'a\\b': 'c' }).toCompileTo('c'); - }); - - it('in strict mode', () => { - expectTemplate("{{'a\\b'}}") - .withCompileOptions({ strict: true }) - .withInput({ 'a\\b': 'c' }) - .toCompileTo('c'); - }); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.strict.test.ts b/dev/lib/handlebars/src/spec/index.strict.test.ts deleted file mode 100644 index a8f294b9..00000000 --- a/dev/lib/handlebars/src/spec/index.strict.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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 { expectTemplate } from '../__jest__/test_bench'; - -describe('strict', () => { - describe('strict mode', () => { - it('should error on missing property lookup', () => { - expectTemplate('{{hello}}') - .withCompileOptions({ strict: true }) - .toThrow(/"hello" not defined in/); - }); - - it('should error on missing child', () => { - expectTemplate('{{hello.bar}}') - .withCompileOptions({ strict: true }) - .withInput({ hello: { bar: 'foo' } }) - .toCompileTo('foo'); - - expectTemplate('{{hello.bar}}') - .withCompileOptions({ strict: true }) - .withInput({ hello: {} }) - .toThrow(/"bar" not defined in/); - }); - - it('should handle explicit undefined', () => { - expectTemplate('{{hello.bar}}') - .withCompileOptions({ strict: true }) - .withInput({ hello: { bar: undefined } }) - .toCompileTo(''); - }); - - it('should error on missing property lookup in known helpers mode', () => { - expectTemplate('{{hello}}') - .withCompileOptions({ - strict: true, - knownHelpersOnly: true, - }) - .toThrow(/"hello" not defined in/); - }); - - it('should error on missing context', () => { - expectTemplate('{{hello}}').withCompileOptions({ strict: true }).toThrow(Error); - }); - - it('should error on missing data lookup', () => { - const xt = expectTemplate('{{@hello}}').withCompileOptions({ - strict: true, - }); - - xt.toThrow(Error); - - xt.withRuntimeOptions({ data: { hello: 'foo' } }).toCompileTo('foo'); - }); - - it('should not run helperMissing for helper calls', () => { - expectTemplate('{{hello foo}}') - .withCompileOptions({ strict: true }) - .withInput({ foo: true }) - .toThrow(/"hello" not defined in/); - - expectTemplate('{{#hello foo}}{{/hello}}') - .withCompileOptions({ strict: true }) - .withInput({ foo: true }) - .toThrow(/"hello" not defined in/); - }); - - it('should throw on ambiguous blocks', () => { - expectTemplate('{{#hello}}{{/hello}}') - .withCompileOptions({ strict: true }) - .toThrow(/"hello" not defined in/); - - expectTemplate('{{^hello}}{{/hello}}') - .withCompileOptions({ strict: true }) - .toThrow(/"hello" not defined in/); - - expectTemplate('{{#hello.bar}}{{/hello.bar}}') - .withCompileOptions({ strict: true }) - .withInput({ hello: {} }) - .toThrow(/"bar" not defined in/); - }); - - it('should allow undefined parameters when passed to helpers', () => { - expectTemplate('{{#unless foo}}success{{/unless}}') - .withCompileOptions({ strict: true }) - .toCompileTo('success'); - }); - - it('should allow undefined hash when passed to helpers', () => { - expectTemplate('{{helper value=@foo}}') - .withCompileOptions({ - strict: true, - }) - .withHelpers({ - helper(options) { - expect('value' in options.hash).toEqual(true); - expect(options.hash.value).toBeUndefined(); - return 'success'; - }, - }) - .toCompileTo('success'); - }); - - it('should show error location on missing property lookup', () => { - expectTemplate('\n\n\n {{hello}}') - .withCompileOptions({ strict: true }) - .toThrow('"hello" not defined in [object Object] - 4:5'); - }); - - it('should error contains correct location properties on missing property lookup', () => { - try { - expectTemplate('\n\n\n {{hello}}') - .withCompileOptions({ strict: true }) - .toCompileTo('throw before asserting this'); - } catch (error) { - expect(error.lineNumber).toEqual(4); - expect(error.endLineNumber).toEqual(4); - expect(error.column).toEqual(5); - expect(error.endColumn).toEqual(10); - } - }); - }); - - describe('assume objects', () => { - it('should ignore missing property', () => { - expectTemplate('{{hello}}').withCompileOptions({ assumeObjects: true }).toCompileTo(''); - }); - - it('should ignore missing child', () => { - expectTemplate('{{hello.bar}}') - .withCompileOptions({ assumeObjects: true }) - .withInput({ hello: {} }) - .toCompileTo(''); - }); - - it('should error on missing object', () => { - expectTemplate('{{hello.bar}}').withCompileOptions({ assumeObjects: true }).toThrow(Error); - }); - - it('should error on missing context', () => { - expectTemplate('{{hello}}') - .withCompileOptions({ assumeObjects: true }) - .withInput(undefined) - .toThrow(Error); - }); - - it('should error on missing data lookup', () => { - expectTemplate('{{@hello.bar}}') - .withCompileOptions({ assumeObjects: true }) - .withInput(undefined) - .toThrow(Error); - }); - - it('should execute blockHelperMissing', () => { - expectTemplate('{{^hello}}foo{{/hello}}') - .withCompileOptions({ assumeObjects: true }) - .toCompileTo('foo'); - }); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.subexpressions.test.ts b/dev/lib/handlebars/src/spec/index.subexpressions.test.ts deleted file mode 100644 index 4dee24b7..00000000 --- a/dev/lib/handlebars/src/spec/index.subexpressions.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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, { type HelperOptions } from '../..'; -import { expectTemplate } from '../__jest__/test_bench'; - -describe('subexpressions', () => { - it('arg-less helper', () => { - expectTemplate('{{foo (bar)}}!') - .withHelpers({ - foo(val) { - return val + val; - }, - bar() { - return 'LOL'; - }, - }) - .toCompileTo('LOLLOL!'); - }); - - it('helper w args', () => { - expectTemplate('{{blog (equal a b)}}') - .withInput({ bar: 'LOL' }) - .withHelpers({ - blog(val) { - return 'val is ' + val; - }, - equal(x, y) { - return x === y; - }, - }) - .toCompileTo('val is true'); - }); - - it('mixed paths and helpers', () => { - expectTemplate('{{blog baz.bat (equal a b) baz.bar}}') - .withInput({ bar: 'LOL', baz: { bat: 'foo!', bar: 'bar!' } }) - .withHelpers({ - blog(val, that, theOther) { - return 'val is ' + val + ', ' + that + ' and ' + theOther; - }, - equal(x, y) { - return x === y; - }, - }) - .toCompileTo('val is foo!, true and bar!'); - }); - - it('supports much nesting', () => { - expectTemplate('{{blog (equal (equal true true) true)}}') - .withInput({ bar: 'LOL' }) - .withHelpers({ - blog(val) { - return 'val is ' + val; - }, - equal(x, y) { - return x === y; - }, - }) - .toCompileTo('val is true'); - }); - - it('GH-800 : Complex subexpressions', () => { - const context = { a: 'a', b: 'b', c: { c: 'c' }, d: 'd', e: { e: 'e' } }; - const helpers = { - dash(a: any, b: any) { - return a + '-' + b; - }, - concat(a: any, b: any) { - return a + b; - }, - }; - - expectTemplate("{{dash 'abc' (concat a b)}}") - .withInput(context) - .withHelpers(helpers) - .toCompileTo('abc-ab'); - - expectTemplate('{{dash d (concat a b)}}') - .withInput(context) - .withHelpers(helpers) - .toCompileTo('d-ab'); - - expectTemplate('{{dash c.c (concat a b)}}') - .withInput(context) - .withHelpers(helpers) - .toCompileTo('c-ab'); - - expectTemplate('{{dash (concat a b) c.c}}') - .withInput(context) - .withHelpers(helpers) - .toCompileTo('ab-c'); - - expectTemplate('{{dash (concat a e.e) c.c}}') - .withInput(context) - .withHelpers(helpers) - .toCompileTo('ae-c'); - }); - - it('provides each nested helper invocation its own options hash', () => { - let lastOptions: HelperOptions; - const helpers = { - equal(x: any, y: any, options: HelperOptions) { - if (!options || options === lastOptions) { - throw new Error('options hash was reused'); - } - lastOptions = options; - return x === y; - }, - }; - expectTemplate('{{equal (equal true true) true}}').withHelpers(helpers).toCompileTo('true'); - }); - - it('with hashes', () => { - expectTemplate("{{blog (equal (equal true true) true fun='yes')}}") - .withInput({ bar: 'LOL' }) - .withHelpers({ - blog(val) { - return 'val is ' + val; - }, - equal(x, y) { - return x === y; - }, - }) - .toCompileTo('val is true'); - }); - - it('as hashes', () => { - expectTemplate("{{blog fun=(equal (blog fun=1) 'val is 1')}}") - .withHelpers({ - blog(options) { - return 'val is ' + options.hash.fun; - }, - equal(x, y) { - return x === y; - }, - }) - .toCompileTo('val is true'); - }); - - it('multiple subexpressions in a hash', () => { - expectTemplate('{{input aria-label=(t "Name") placeholder=(t "Example User")}}') - .withHelpers({ - input(options) { - const hash = options.hash; - const ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); - const placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); - return new Handlebars.SafeString( - '<input aria-label="' + ariaLabel + '" placeholder="' + placeholder + '" />' - ); - }, - t(defaultString) { - return new Handlebars.SafeString(defaultString); - }, - }) - .toCompileTo('<input aria-label="Name" placeholder="Example User" />'); - }); - - it('multiple subexpressions in a hash with context', () => { - expectTemplate('{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}') - .withInput({ - item: { - field: 'Name', - placeholder: 'Example User', - }, - }) - .withHelpers({ - input(options) { - const hash = options.hash; - const ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); - const placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); - return new Handlebars.SafeString( - '<input aria-label="' + ariaLabel + '" placeholder="' + placeholder + '" />' - ); - }, - t(defaultString) { - return new Handlebars.SafeString(defaultString); - }, - }) - .toCompileTo('<input aria-label="Name" placeholder="Example User" />'); - }); - - it('subexpression functions on the context', () => { - expectTemplate('{{foo (bar)}}!') - .withInput({ - bar() { - return 'LOL'; - }, - }) - .withHelpers({ - foo(val) { - return val + val; - }, - }) - .toCompileTo('LOLLOL!'); - }); - - it("subexpressions can't just be property lookups", () => { - expectTemplate('{{foo (bar)}}!') - .withInput({ - bar: 'LOL', - }) - .withHelpers({ - foo(val) { - return val + val; - }, - }) - .toThrow(); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.utils.test.ts b/dev/lib/handlebars/src/spec/index.utils.test.ts deleted file mode 100644 index 6350bc7c..00000000 --- a/dev/lib/handlebars/src/spec/index.utils.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 } from '../__jest__/test_bench'; - -describe('utils', function () { - describe('#SafeString', function () { - it('constructing a safestring from a string and checking its type', function () { - const safe = new Handlebars.SafeString('testing 1, 2, 3'); - expect(safe).toBeInstanceOf(Handlebars.SafeString); - expect(safe.toString()).toEqual('testing 1, 2, 3'); - }); - - it('it should not escape SafeString properties', function () { - const name = new Handlebars.SafeString('<em>Sean O'Malley</em>'); - expectTemplate('{{name}}').withInput({ name }).toCompileTo('<em>Sean O'Malley</em>'); - }); - }); -}); diff --git a/dev/lib/handlebars/src/spec/index.whitespace_control.test.ts b/dev/lib/handlebars/src/spec/index.whitespace_control.test.ts deleted file mode 100644 index 1f7cf019..00000000 --- a/dev/lib/handlebars/src/spec/index.whitespace_control.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 { expectTemplate } from '../__jest__/test_bench'; - -describe('whitespace control', () => { - it('should strip whitespace around mustache calls', () => { - const hash = { foo: 'bar<' }; - expectTemplate(' {{~foo~}} ').withInput(hash).toCompileTo('bar<'); - expectTemplate(' {{~foo}} ').withInput(hash).toCompileTo('bar< '); - expectTemplate(' {{foo~}} ').withInput(hash).toCompileTo(' bar<'); - expectTemplate(' {{~&foo~}} ').withInput(hash).toCompileTo('bar<'); - expectTemplate(' {{~{foo}~}} ').withInput(hash).toCompileTo('bar<'); - expectTemplate('1\n{{foo~}} \n\n 23\n{{bar}}4').toCompileTo('1\n23\n4'); - }); - - describe('blocks', () => { - it('should strip whitespace around simple block calls', () => { - const hash = { foo: 'bar<' }; - - expectTemplate(' {{~#if foo~}} bar {{~/if~}} ').withInput(hash).toCompileTo('bar'); - expectTemplate(' {{#if foo~}} bar {{/if~}} ').withInput(hash).toCompileTo(' bar '); - expectTemplate(' {{~#if foo}} bar {{~/if}} ').withInput(hash).toCompileTo(' bar '); - expectTemplate(' {{#if foo}} bar {{/if}} ').withInput(hash).toCompileTo(' bar '); - - expectTemplate(' \n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\n ') - .withInput(hash) - .toCompileTo('bar'); - - expectTemplate(' a\n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\na ') - .withInput(hash) - .toCompileTo(' abara '); - }); - - it('should strip whitespace around inverse block calls', () => { - expectTemplate(' {{~^if foo~}} bar {{~/if~}} ').toCompileTo('bar'); - expectTemplate(' {{^if foo~}} bar {{/if~}} ').toCompileTo(' bar '); - expectTemplate(' {{~^if foo}} bar {{~/if}} ').toCompileTo(' bar '); - expectTemplate(' {{^if foo}} bar {{/if}} ').toCompileTo(' bar '); - expectTemplate(' \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n ').toCompileTo('bar'); - }); - - it('should strip whitespace around complex block calls', () => { - const hash = { foo: 'bar<' }; - - expectTemplate('{{#if foo~}} bar {{~^~}} baz {{~/if}}').withInput(hash).toCompileTo('bar'); - expectTemplate('{{#if foo~}} bar {{^~}} baz {{/if}}').withInput(hash).toCompileTo('bar '); - expectTemplate('{{#if foo}} bar {{~^~}} baz {{~/if}}').withInput(hash).toCompileTo(' bar'); - expectTemplate('{{#if foo}} bar {{^~}} baz {{/if}}').withInput(hash).toCompileTo(' bar '); - expectTemplate('{{#if foo~}} bar {{~else~}} baz {{~/if}}').withInput(hash).toCompileTo('bar'); - - expectTemplate('\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n') - .withInput(hash) - .toCompileTo('bar'); - - expectTemplate('\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n') - .withInput(hash) - .toCompileTo('bar<'); - - expectTemplate('{{#if foo~}} bar {{~^~}} baz {{~/if}}').toCompileTo('baz'); - expectTemplate('{{#if foo}} bar {{~^~}} baz {{/if}}').toCompileTo('baz '); - expectTemplate('{{#if foo~}} bar {{~^}} baz {{~/if}}').toCompileTo(' baz'); - expectTemplate('{{#if foo~}} bar {{~^}} baz {{/if}}').toCompileTo(' baz '); - expectTemplate('{{#if foo~}} bar {{~else~}} baz {{~/if}}').toCompileTo('baz'); - expectTemplate('\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n').toCompileTo( - 'baz' - ); - }); - }); - - 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' }) - .toCompileTo('barbar bar '); - }); -}); diff --git a/dev/lib/handlebars/src/symbols.ts b/dev/lib/handlebars/src/symbols.ts deleted file mode 100644 index 85a8f2f3..00000000 --- a/dev/lib/handlebars/src/symbols.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/kbn-handlebars/LICENSE` for more information. - */ - -export const kHelper = Symbol('helper'); -export const kAmbiguous = Symbol('ambiguous'); -export const kSimple = Symbol('simple'); diff --git a/dev/lib/handlebars/src/types.ts b/dev/lib/handlebars/src/types.ts deleted file mode 100644 index 583170cb..00000000 --- a/dev/lib/handlebars/src/types.ts +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/kbn-handlebars/LICENSE` for more information. - */ - -import { kHelper, kAmbiguous, kSimple } from './symbols'; - -// 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 Handlebars module with an extra `compileAST` function and fixed typings. - */ -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( - input: string | hbs.AST.Program, - options?: CompileOptions - ): TemplateDelegateFixed; - - // -------------------------------------------------------- - // 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. - // -------------------------------------------------------- - - /** - * A {@link https://handlebarsjs.com/api-reference/helpers.html helper-function} type. - * - * When registering a helper function, it should be of this type. - */ - export interface HelperDelegate extends HelperDelegateFixed {} // eslint-disable-line @typescript-eslint/no-empty-interface - - /** - * 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}. - */ - export interface TemplateDelegate<T = any> extends TemplateDelegateFixed<T> {} // eslint-disable-line @typescript-eslint/no-empty-interface - - /** - * Register one or more {@link https://handlebarsjs.com/api-reference/runtime.html#handlebars-registerpartial-name-partial partials}. - * - * @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: Record<string, TemplateFixed>): void; // Ensure `spec` object values can be strings -} - -/** - * Supported Handlebars compile options. - * - * This is a subset of all the compile options supported by the upstream - * Handlebars module. - */ -export type CompileOptions = Pick< - HandlebarsCompileOptions, - | 'data' - | 'knownHelpers' - | 'knownHelpersOnly' - | 'noEscape' - | 'strict' - | 'assumeObjects' - | 'preventIndent' - | 'explicitPartialContext' ->; - -/** - * Supported Handlebars runtime options - * - * This is a subset of all the runtime options supported by the upstream - * Handlebars module. - */ -export interface RuntimeOptions extends Pick<Handlebars.RuntimeOptions, 'data' | 'blockParams'> { - // 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; -} - -/** - * 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}. - */ -export interface HelperOptions extends Omit<Handlebars.HelperOptions, 'fn' | 'inverse'> { - 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>, - container: Container, - options: any -) => any; - -// ----------------------------------------------------------------------------- -// 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 VisitorHelper { - fn?: HelperDelegateFixed; - context: any[]; - params: any[]; - options: AmbiguousHelperOptions; -} - -export interface ResolvePartialOptions - 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 { - helpers: HelpersHash; - partials: PartialsHash; - decorators: DecoratorsHash; - strict: (obj: Record<string, any>, name: string, loc: hbs.AST.SourceLocation) => any; - lookupProperty: LookupProperty; - lambda: (current: any, context: any) => any; - data: (value: any, depth: number) => any; - hooks: { - helperMissing?: HelperDelegateFixed; - blockHelperMissing?: HelperDelegateFixed; - }; -} diff --git a/dev/lib/handlebars/src/utils.ts b/dev/lib/handlebars/src/utils.ts deleted file mode 100644 index f55bd98a..00000000 --- a/dev/lib/handlebars/src/utils.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/kbn-handlebars/LICENSE` for more information. - */ - -// @ts-expect-error: Could not find a declaration file for module -import { createFrame } from 'handlebars/dist/cjs/handlebars/utils'; - -import type { AmbiguousHelperOptions, DecoratorOptions } from './types'; - -export function isBlock(node: hbs.AST.Node): node is hbs.AST.BlockStatement { - return 'program' in node || 'inverse' in node; -} - -export function isDecorator( - node: hbs.AST.Node -): node is hbs.AST.Decorator | hbs.AST.DecoratorBlock { - return node.type === 'Decorator' || node.type === 'DecoratorBlock'; -} - -export function toDecoratorOptions(options: AmbiguousHelperOptions) { - // There's really no tests/documentation on this, but to match the upstream codebase we'll remove `lookupProperty` from the decorator context - delete (options as any).lookupProperty; - - return options as DecoratorOptions; -} - -export function noop() { - return ''; -} - -// liftet from handlebars lib/handlebars/runtime.js -export function initData(context: any, data: any) { - if (!data || !('root' in data)) { - data = data ? createFrame(data) : {}; - data.root = context; - } - return data; -} - -// liftet from handlebars lib/handlebars/compiler/compiler.js -export function transformLiteralToPath(node: { path: hbs.AST.PathExpression | hbs.AST.Literal }) { - const pathIsLiteral = 'parts' in node.path === false; - - if (pathIsLiteral) { - const literal = node.path; - // @ts-expect-error: Not all `hbs.AST.Literal` sub-types has an `original` property, but that's ok, in that case we just want `undefined` - const original = literal.original; - // Casting to string here to make false and 0 literal values play nicely with the rest - // of the system. - node.path = { - type: 'PathExpression', - data: false, - depth: 0, - parts: [original + ''], - original: original + '', - loc: literal.loc, - }; - } -} - -export function allowUnsafeEval() { - try { - new Function(); - return true; - } catch (e) { - return false; - } -} diff --git a/dev/lib/handlebars/src/visitor.ts b/dev/lib/handlebars/src/visitor.ts deleted file mode 100644 index 1842c8e5..00000000 --- a/dev/lib/handlebars/src/visitor.ts +++ /dev/null @@ -1,778 +0,0 @@ -/* - * Elasticsearch B.V licenses this file to you under the MIT License. - * See `packages/kbn-handlebars/LICENSE` for more information. - */ - -import Handlebars from 'handlebars'; -import { - createProtoAccessControl, - resultIsAllowed, - // @ts-expect-error: Could not find a declaration file for module -} from 'handlebars/dist/cjs/handlebars/internal/proto-access'; -// @ts-expect-error: Could not find a declaration file for module -import AST from 'handlebars/dist/cjs/handlebars/compiler/ast'; -// @ts-expect-error: Could not find a declaration file for module -import { indexOf, createFrame } from 'handlebars/dist/cjs/handlebars/utils'; -// @ts-expect-error: Could not find a declaration file for module -import { moveHelperToHooks } from 'handlebars/dist/cjs/handlebars/helpers'; - -import type { - AmbiguousHelperOptions, - CompileOptions, - Container, - DecoratorDelegate, - DecoratorsHash, - HelperOptions, - NodeType, - NonBlockHelperOptions, - ProcessableBlockStatementNode, - ProcessableNode, - ProcessableNodeWithPathParts, - ProcessableNodeWithPathPartsOrLiteral, - ProcessableStatementNode, - ResolvePartialOptions, - RuntimeOptions, - Template, - TemplateDelegate, - VisitorHelper, -} from './types'; -import { kAmbiguous, kHelper, kSimple } from './symbols'; -import { - initData, - isBlock, - isDecorator, - noop, - toDecoratorOptions, - transformLiteralToPath, -} from './utils'; - -export class ElasticHandlebarsVisitor extends Handlebars.Visitor { - private env: typeof Handlebars; - private contexts: any[] = []; - private output: any[] = []; - private template?: string; - private compileOptions: CompileOptions; - private runtimeOptions?: RuntimeOptions; - private blockParamNames: any[][] = []; - private blockParamValues: any[][] = []; - private ast?: hbs.AST.Program; - private container: Container; - private defaultHelperOptions: Pick<NonBlockHelperOptions, 'lookupProperty'>; - private processedRootDecorators = false; // Root decorators should not have access to input arguments. This flag helps us detect them. - private processedDecoratorsForProgram = new Set(); // It's important that a given program node only has its decorators run once, we use this Map to keep track of them - - constructor( - env: typeof Handlebars, - input: string | hbs.AST.Program, - options: CompileOptions = {} - ) { - super(); - - this.env = env; - - if (typeof input !== 'string' && input.type === 'Program') { - this.ast = input; - } else { - this.template = input as string; - } - - this.compileOptions = { data: true, ...options }; - this.compileOptions.knownHelpers = Object.assign( - Object.create(null), - { - helperMissing: true, - blockHelperMissing: true, - each: true, - if: true, - unless: true, - with: true, - log: true, - lookup: true, - }, - this.compileOptions.knownHelpers - ); - - const protoAccessControl = createProtoAccessControl({}); - - const container: Container = (this.container = { - helpers: {}, - partials: {}, - decorators: {}, - strict(obj, name, loc) { - if (!obj || !(name in obj)) { - throw new Handlebars.Exception('"' + name + '" not defined in ' + obj, { - loc, - } as hbs.AST.Node); - } - return container.lookupProperty(obj, name); - }, - // this function is lifted from the handlebars source and slightly modified (lib/handlebars/runtime.js) - lookupProperty(parent, propertyName) { - const result = parent[propertyName]; - if (result == null) { - return result; - } - if (Object.prototype.hasOwnProperty.call(parent, propertyName)) { - return result; - } - - if (resultIsAllowed(result, protoAccessControl, propertyName)) { - return result; - } - return undefined; - }, - // this function is lifted from the handlebars source and slightly modified (lib/handlebars/runtime.js) - lambda(current, context) { - return typeof current === 'function' ? current.call(context) : current; - }, - data(value: any, depth: number) { - while (value && depth--) { - value = value._parent; - } - return value; - }, - hooks: {}, - }); - - this.defaultHelperOptions = { - lookupProperty: container.lookupProperty, - }; - } - - render(context: any, options: RuntimeOptions = {}): string { - this.contexts = [context]; - this.output = []; - this.runtimeOptions = { ...options }; - this.container.helpers = { ...this.env.helpers, ...options.helpers }; - this.container.partials = { ...this.env.partials, ...options.partials }; - this.container.decorators = { - ...(this.env.decorators as DecoratorsHash), - ...options.decorators, - }; - this.container.hooks = {}; - this.processedRootDecorators = false; - this.processedDecoratorsForProgram.clear(); - - if (this.compileOptions.data) { - this.runtimeOptions.data = initData(context, this.runtimeOptions.data); - } - - const keepHelperInHelpers = false; - moveHelperToHooks(this.container, 'helperMissing', keepHelperInHelpers); - moveHelperToHooks(this.container, 'blockHelperMissing', keepHelperInHelpers); - - if (!this.ast) { - this.ast = Handlebars.parse(this.template!); - } - - // The `defaultMain` function contains the default behavior: - // - // Generate a "program" function based on the root `Program` in the AST and - // call it. This will start the processing of all the child nodes in the - // AST. - const defaultMain: TemplateDelegate = (_context) => { - const prog = this.generateProgramFunction(this.ast!); - return prog(_context, this.runtimeOptions); - }; - - // Run any decorators that might exist on the root: - // - // The `defaultMain` function is passed in, and if there are no root - // decorators, or if the decorators chooses to do so, the same function is - // returned from `processDecorators` and the default behavior is retained. - // - // Alternatively any of the root decorators might call the `defaultMain` - // function themselves, process its return value, and return a completely - // different `main` function. - const main = this.processDecorators(this.ast, defaultMain); - this.processedRootDecorators = true; - - // Call the `main` function and add the result to the final output. - const result = main(this.context, options); - - if (main === defaultMain) { - this.output.push(result); - return this.output.join(''); - } else { - // We normally expect the return value of `main` to be a string. However, - // if a decorator is used to override the `defaultMain` function, the - // return value can be any type. To match the upstream handlebars project - // behavior, we want the result of rendering the template to be the - // literal value returned by the decorator. - // - // Since the output array in this case always will be empty, we just - // return that single value instead of attempting to join all the array - // elements as strings. - return result; - } - } - - // ********************************************** // - // *** Visitor AST Traversal Functions *** // - // ********************************************** // - - Program(program: hbs.AST.Program) { - this.blockParamNames.unshift(program.blockParams); - super.Program(program); - this.blockParamNames.shift(); - } - - MustacheStatement(mustache: hbs.AST.MustacheStatement) { - this.processStatementOrExpression(mustache); - } - - BlockStatement(block: hbs.AST.BlockStatement) { - this.processStatementOrExpression(block); - } - - PartialStatement(partial: hbs.AST.PartialStatement) { - this.invokePartial(partial); - } - - PartialBlockStatement(partial: hbs.AST.PartialBlockStatement) { - this.invokePartial(partial); - } - - // This space is intentionally left blank: We want to override the Visitor - // class implementation of this method, but since we handle decorators - // separately before traversing the nodes, we just want to make this a no-op. - DecoratorBlock(decorator: hbs.AST.DecoratorBlock) {} - - // This space is intentionally left blank: We want to override the Visitor - // class implementation of this method, but since we handle decorators - // separately before traversing the nodes, we just want to make this a no-op. - Decorator(decorator: hbs.AST.Decorator) {} - - SubExpression(sexpr: hbs.AST.SubExpression) { - this.processStatementOrExpression(sexpr); - } - - PathExpression(path: hbs.AST.PathExpression) { - const blockParamId = - !path.depth && !AST.helpers.scopedId(path) && this.blockParamIndex(path.parts[0]); - - let result; - if (blockParamId) { - result = this.lookupBlockParam(blockParamId, path); - } else if (path.data) { - result = this.lookupData(this.runtimeOptions!.data, path); - } else { - result = this.resolvePath(this.contexts[path.depth], path); - } - - this.output.push(result); - } - - ContentStatement(content: hbs.AST.ContentStatement) { - this.output.push(content.value); - } - - StringLiteral(string: hbs.AST.StringLiteral) { - this.output.push(string.value); - } - - NumberLiteral(number: hbs.AST.NumberLiteral) { - this.output.push(number.value); - } - - BooleanLiteral(bool: hbs.AST.BooleanLiteral) { - this.output.push(bool.value); - } - - UndefinedLiteral() { - this.output.push(undefined); - } - - NullLiteral() { - this.output.push(null); - } - - // ********************************************** // - // *** Visitor AST Helper Functions *** // - // ********************************************** // - - /** - * Special code for decorators, since they have to be executed ahead of time (before the wrapping program). - * So we have to look into the program AST body and see if it contains any decorators that we have to process - * before we can finish processing of the wrapping program. - */ - private processDecorators(program: hbs.AST.Program, prog: TemplateDelegate) { - if (!this.processedDecoratorsForProgram.has(program)) { - this.processedDecoratorsForProgram.add(program); - const props = {}; - for (const node of program.body) { - if (isDecorator(node)) { - prog = this.processDecorator(node, prog, props); - } - } - } - - return prog; - } - - private processDecorator( - decorator: hbs.AST.DecoratorBlock | hbs.AST.Decorator, - prog: TemplateDelegate, - props: Record<string, any> - ) { - const options = this.setupDecoratorOptions(decorator); - - const result = this.container.lookupProperty<DecoratorDelegate>( - this.container.decorators, - options.name - )(prog, props, this.container, options); - - return Object.assign(result || prog, props); - } - - private processStatementOrExpression(node: ProcessableNodeWithPathPartsOrLiteral) { - // Calling `transformLiteralToPath` has side-effects! - // It converts a node from type `ProcessableNodeWithPathPartsOrLiteral` to `ProcessableNodeWithPathParts` - transformLiteralToPath(node); - - switch (this.classifyNode(node as ProcessableNodeWithPathParts)) { - case kSimple: - this.processSimpleNode(node as ProcessableNodeWithPathParts); - break; - case kHelper: - this.processHelperNode(node as ProcessableNodeWithPathParts); - break; - case kAmbiguous: - this.processAmbiguousNode(node as ProcessableNodeWithPathParts); - break; - } - } - - // Liftet from lib/handlebars/compiler/compiler.js (original name: classifySexpr) - private classifyNode(node: { path: hbs.AST.PathExpression }): NodeType { - const isSimple = AST.helpers.simpleId(node.path); - const isBlockParam = isSimple && !!this.blockParamIndex(node.path.parts[0]); - - // a mustache is an eligible helper if: - // * its id is simple (a single part, not `this` or `..`) - let isHelper = !isBlockParam && AST.helpers.helperExpression(node); - - // if a mustache is an eligible helper but not a definite - // helper, it is ambiguous, and will be resolved in a later - // pass or at runtime. - let isEligible = !isBlockParam && (isHelper || isSimple); - - // if ambiguous, we can possibly resolve the ambiguity now - // An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc. - if (isEligible && !isHelper) { - const name = node.path.parts[0]; - const options = this.compileOptions; - if (options.knownHelpers && options.knownHelpers[name]) { - isHelper = true; - } else if (options.knownHelpersOnly) { - isEligible = false; - } - } - - if (isHelper) { - return kHelper; - } else if (isEligible) { - return kAmbiguous; - } else { - return kSimple; - } - } - - // Liftet from lib/handlebars/compiler/compiler.js - private blockParamIndex(name: string): [number, any] | undefined { - for (let depth = 0, len = this.blockParamNames.length; depth < len; depth++) { - const blockParams = this.blockParamNames[depth]; - const param = blockParams && indexOf(blockParams, name); - if (blockParams && param >= 0) { - return [depth, param]; - } - } - } - - // Looks up the value of `parts` on the given block param and pushes - // it onto the stack. - private lookupBlockParam(blockParamId: [number, any], path: hbs.AST.PathExpression) { - const value = this.blockParamValues[blockParamId[0]][blockParamId[1]]; - return this.resolvePath(value, path, 1); - } - - // Push the data lookup operator - private lookupData(data: any, path: hbs.AST.PathExpression) { - if (path.depth) { - data = this.container.data(data, path.depth); - } - - return this.resolvePath(data, path); - } - - private processSimpleNode(node: ProcessableNodeWithPathParts) { - const path = node.path; - // @ts-expect-error strict is not a valid property on PathExpression, but we used in the same way it's also used in the original handlebars - path.strict = true; - const result = this.resolveNodes(path)[0]; - const lambdaResult = this.container.lambda(result, this.context); - - if (isBlock(node)) { - this.blockValue(node, lambdaResult); - } else { - this.output.push(lambdaResult); - } - } - - // The purpose of this opcode is to take a block of the form - // `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and - // replace it on the stack with the result of properly - // invoking blockHelperMissing. - private blockValue(node: hbs.AST.BlockStatement, value: any) { - const name = node.path.original; - const options = this.setupParams(node, name); - - const result = this.container.hooks.blockHelperMissing!.call(this.context, value, options); - - this.output.push(result); - } - - private processHelperNode(node: ProcessableNodeWithPathParts) { - const path = node.path; - const name = path.parts[0]; - - if (this.compileOptions.knownHelpers && this.compileOptions.knownHelpers[name]) { - this.invokeKnownHelper(node); - } else if (this.compileOptions.knownHelpersOnly) { - throw new Handlebars.Exception( - 'You specified knownHelpersOnly, but used the unknown helper ' + name, - node - ); - } else { - this.invokeHelper(node); - } - } - - // This operation is used when the helper is known to exist, - // so a `helperMissing` fallback is not required. - private invokeKnownHelper(node: ProcessableNodeWithPathParts) { - const name = node.path.parts[0]; - const helper = this.setupHelper(node, name); - // TypeScript: `helper.fn` might be `undefined` at this point, but to match the upstream behavior we call it without any guards - const result = helper.fn!.call(helper.context, ...helper.params, helper.options); - this.output.push(result); - } - - // Pops off the helper's parameters, invokes the helper, - // and pushes the helper's return value onto the stack. - // - // If the helper is not found, `helperMissing` is called. - private invokeHelper(node: ProcessableNodeWithPathParts) { - const path = node.path; - const name = path.original; - const isSimple = AST.helpers.simpleId(path); - const helper = this.setupHelper(node, name); - - const loc = isSimple && helper.fn ? node.loc : path.loc; - helper.fn = (isSimple && helper.fn) || this.resolveNodes(path)[0]; - - if (!helper.fn) { - if (this.compileOptions.strict) { - helper.fn = this.container.strict(helper.context, name, loc); - } else { - helper.fn = this.container.hooks.helperMissing; - } - } - - // TypeScript: `helper.fn` might be `undefined` at this point, but to match the upstream behavior we call it without any guards - const result = helper.fn!.call(helper.context, ...helper.params, helper.options); - - this.output.push(result); - } - - private invokePartial(partial: hbs.AST.PartialStatement | hbs.AST.PartialBlockStatement) { - const { params } = partial; - if (params.length > 1) { - throw new Handlebars.Exception( - `Unsupported number of partial arguments: ${params.length}`, - partial - ); - } - - const isDynamic = partial.name.type === 'SubExpression'; - const name = isDynamic - ? this.resolveNodes(partial.name).join('') - : (partial.name as hbs.AST.PathExpression).original; - - const options: AmbiguousHelperOptions & ResolvePartialOptions = this.setupParams(partial, name); - options.helpers = this.container.helpers; - options.partials = this.container.partials; - options.decorators = this.container.decorators; - - let partialBlock; - if ('fn' in options && options.fn !== noop) { - const { fn } = options; - const currentPartialBlock = options.data?.['partial-block']; - options.data = createFrame(options.data); - - // Wrapper function to get access to currentPartialBlock from the closure - partialBlock = options.data['partial-block'] = function partialBlockWrapper( - context: any, - wrapperOptions: { data?: HelperOptions['data'] } = {} - ) { - // Restore the partial-block from the closure for the execution of the block - // i.e. the part inside the block of the partial call. - wrapperOptions.data = createFrame(wrapperOptions.data); - wrapperOptions.data['partial-block'] = currentPartialBlock; - return fn(context, wrapperOptions); - }; - - if (fn.partials) { - options.partials = { ...options.partials, ...fn.partials }; - } - } - - let context = {}; - if (params.length === 0 && !this.compileOptions.explicitPartialContext) { - context = this.context; - } else if (params.length === 1) { - context = this.resolveNodes(params[0])[0]; - } - - if (Object.keys(options.hash).length > 0) { - // TODO: context can be an array, but maybe never when we have a hash??? - context = Object.assign({}, context, options.hash); - } - - const partialTemplate: Template | undefined = - this.container.partials[name] ?? - partialBlock ?? - // TypeScript note: We extend ResolvePartialOptions in our types.ts file - // to fix an error in the upstream type. When calling back into the - // upstream code, we just cast back to the non-extended type - Handlebars.VM.resolvePartial( - undefined, - undefined, - options as Handlebars.ResolvePartialOptions - ); - - if (partialTemplate === undefined) { - throw new Handlebars.Exception(`The partial ${name} could not be found`); - } - - let render; - if (typeof partialTemplate === 'string') { - render = this.env.compileAST(partialTemplate, this.compileOptions); - if (name in this.container.partials) { - this.container.partials[name] = render; - } - } else { - render = partialTemplate; - } - - let result = render(context, options); - - if ('indent' in partial) { - result = - partial.indent + - (this.compileOptions.preventIndent - ? result - : result.replace(/\n(?!$)/g, `\n${partial.indent}`)); // indent each line, ignoring any trailing linebreak - } - - this.output.push(result); - } - - private processAmbiguousNode(node: ProcessableNodeWithPathParts) { - const name = node.path.parts[0]; - const helper = this.setupHelper(node, name); - let { fn: helperFn } = helper; - - const loc = helperFn ? node.loc : node.path.loc; - helperFn = helperFn ?? this.resolveNodes(node.path)[0]; - - if (helperFn === undefined) { - if (this.compileOptions.strict) { - helperFn = this.container.strict(helper.context, name, loc); - } else { - helperFn = - helper.context != null - ? this.container.lookupProperty(helper.context, name) - : helper.context; - if (helperFn == null) helperFn = this.container.hooks.helperMissing; - } - } - - const helperResult = - typeof helperFn === 'function' - ? helperFn.call(helper.context, ...helper.params, helper.options) - : helperFn; - - if (isBlock(node)) { - const result = helper.fn - ? helperResult - : this.container.hooks.blockHelperMissing!.call(this.context, helperResult, helper.options); - if (result != null) { - this.output.push(result); - } - } else { - if ( - (node as hbs.AST.MustacheStatement).escaped === false || - this.compileOptions.noEscape === true || - typeof helperResult !== 'string' - ) { - this.output.push(helperResult); - } else { - this.output.push(Handlebars.escapeExpression(helperResult)); - } - } - } - - private setupHelper(node: ProcessableNode, helperName: string): VisitorHelper { - return { - fn: this.container.lookupProperty(this.container.helpers, helperName), - context: this.context, - params: this.resolveNodes(node.params), - options: this.setupParams(node, helperName), - }; - } - - private setupDecoratorOptions(decorator: hbs.AST.Decorator | hbs.AST.DecoratorBlock) { - // TypeScript: The types indicate that `decorator.path` technically can be an `hbs.AST.Literal`. However, the upstream codebase always treats it as an `hbs.AST.PathExpression`, so we do too. - const name = (decorator.path as hbs.AST.PathExpression).original; - const options = toDecoratorOptions(this.setupParams(decorator, name)); - - if (decorator.params.length > 0) { - if (!this.processedRootDecorators) { - // When processing the root decorators, temporarily remove the root context so it's not accessible to the decorator - const context = this.contexts.shift(); - options.args = this.resolveNodes(decorator.params); - this.contexts.unshift(context); - } else { - options.args = this.resolveNodes(decorator.params); - } - } else { - options.args = []; - } - - return options; - } - - private setupParams(node: ProcessableBlockStatementNode, name: string): HelperOptions; - private setupParams(node: ProcessableStatementNode, name: string): NonBlockHelperOptions; - private setupParams(node: ProcessableNode, name: string): AmbiguousHelperOptions; - private setupParams(node: ProcessableNode, name: string) { - const options: AmbiguousHelperOptions = { - name, - hash: this.getHash(node), - data: this.runtimeOptions!.data, - loc: { start: node.loc.start, end: node.loc.end }, - ...this.defaultHelperOptions, - }; - - if (isBlock(node)) { - (options as HelperOptions).fn = node.program - ? this.processDecorators(node.program, this.generateProgramFunction(node.program)) - : noop; - (options as HelperOptions).inverse = node.inverse - ? this.processDecorators(node.inverse, this.generateProgramFunction(node.inverse)) - : noop; - } - - return options; - } - - private generateProgramFunction(program: hbs.AST.Program) { - if (!program) return noop; - - const prog: TemplateDelegate = (nextContext: any, runtimeOptions: RuntimeOptions = {}) => { - runtimeOptions = { ...runtimeOptions }; - - // inherit data in blockParams from parent program - runtimeOptions.data = runtimeOptions.data || this.runtimeOptions!.data; - if (runtimeOptions.blockParams) { - runtimeOptions.blockParams = runtimeOptions.blockParams.concat( - this.runtimeOptions!.blockParams - ); - } - - // inherit partials from parent program - runtimeOptions.partials = runtimeOptions.partials || this.runtimeOptions!.partials; - - // stash parent program data - const tmpRuntimeOptions = this.runtimeOptions; - this.runtimeOptions = runtimeOptions; - const shiftContext = nextContext !== this.context; - if (shiftContext) this.contexts.unshift(nextContext); - this.blockParamValues.unshift(runtimeOptions.blockParams || []); - - // execute child program - const result = this.resolveNodes(program).join(''); - - // unstash parent program data - this.blockParamValues.shift(); - if (shiftContext) this.contexts.shift(); - this.runtimeOptions = tmpRuntimeOptions; - - // return result of child program - return result; - }; - - prog.blockParams = program.blockParams?.length ?? 0; - return prog; - } - - private getHash(statement: { hash?: hbs.AST.Hash }) { - const result: { [key: string]: any } = {}; - if (!statement.hash) return result; - for (const { key, value } of statement.hash.pairs) { - result[key] = this.resolveNodes(value)[0]; - } - return result; - } - - private resolvePath(obj: any, path: hbs.AST.PathExpression, index = 0) { - if (this.compileOptions.strict || this.compileOptions.assumeObjects) { - return this.strictLookup(obj, path); - } - - for (; index < path.parts.length; index++) { - if (obj == null) return; - obj = this.container.lookupProperty(obj, path.parts[index]); - } - - return obj; - } - - private strictLookup(obj: any, path: hbs.AST.PathExpression) { - // @ts-expect-error strict is not a valid property on PathExpression, but we used in the same way it's also used in the original handlebars - const requireTerminal = this.compileOptions.strict && path.strict; - const len = path.parts.length - (requireTerminal ? 1 : 0); - - for (let i = 0; i < len; i++) { - obj = this.container.lookupProperty(obj, path.parts[i]); - } - - if (requireTerminal) { - return this.container.strict(obj, path.parts[len], path.loc); - } else { - return obj; - } - } - - private resolveNodes(nodes: hbs.AST.Node | hbs.AST.Node[]): any[] { - const currentOutput = this.output; - this.output = []; - - if (Array.isArray(nodes)) { - this.acceptArray(nodes); - } else { - this.accept(nodes); - } - - const result = this.output; - - this.output = currentOutput; - - return result; - } - - private get context() { - return this.contexts[0]; - } -} diff --git a/dev/lib/handlebars/tsconfig.json b/dev/lib/handlebars/tsconfig.json deleted file mode 100644 index 3f139716..00000000 --- a/dev/lib/handlebars/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "outDir": "target/types", - "types": [ - "jest", - "node" - ] - }, - "include": [ - "**/*.ts" - ], - "exclude": [ - "target/**/*", - ] -} |