From 4ec21c3c1fb241be275c162f022dbec24353715c Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 10 Nov 2023 09:20:00 +0000 Subject: [PATCH 01/14] feat: added methods that allow a template to be rendered multiple times with accumulated data from each run --- .golangci.yaml | 1 + README.md | 26 +++++++- engine.go | 82 ++++++++++++++++++++++--- go.mod | 14 ++--- go.sum | 29 +++++---- internal/template/template.go | 86 ++++++++++++++++++--------- internal/template/template_test.go | 8 +-- templating.go | 45 +++++++++++++- testdata/expected/test.txt | 5 +- testdata/templates/test.stmpl | 4 +- testdata/templates/testMultiple.stmpl | 10 ++++ 11 files changed, 243 insertions(+), 67 deletions(-) create mode 100644 testdata/templates/testMultiple.stmpl diff --git a/.golangci.yaml b/.golangci.yaml index dfd31b3..61bf4f2 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -15,6 +15,7 @@ linters: - golint - maligned - gci + - depguard # deprecated/archived - interfacer - scopelint diff --git a/README.md b/README.md index bfba5ae..3300892 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,10 @@ A number of methods are available to start the engine, including: * `templateFile` (string) - The path to the template file to start the engine from. * `data` (any) - Context data to provide to templates and scripts. Available as `{{.Global}}` in templates and `context.Global` in scripts. +Note: there are also version of the above methods that have a `Multiple` suffix, these methods will render the template multiple times (based on numTimes argument), +each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be +found in the `testdata/templates/testMultiple.stmpl` file. + ### Controlling the flow of templating The engine allows you to control the flow of templating from within templates and scripts themselves. This means from a single entry point you can start multiple templates and scripts. @@ -145,12 +149,21 @@ This is done by calling the following functions from within templates and script * `templateString(templateFile string, data any) (string, error)` - Start a template file and return the rendered template as a string. * `templateFile` (string) - The path to the template file to start the engine from. * `data` (any) - Context data to provide to templates and scripts. Available as `{{.Local}}` in templates and `context.Local` in scripts. +* `templateStringInput(templateName string, templateString string, data any) (string, error)` - Template the input string and return the rendered template as a string. + * `templateName` (string) - The name of the template to render. + * `templateString` (string) - An input template string to template. + * `data` (any) - Context data to provide to templates and scripts. Available as `{{.Local}}` in templates and `context.Local` in scripts. + +Note: there are also version of the above methods that have a `Multiple` suffix, these methods will render the template multiple times (based on numTimes argument), +each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be +found in the `testdata/templates/testMultiple.stmpl` file. This allows for example: ```gotemplate {{ templateFile "tmpl.stmpl" "out.txt" .Local }}{{/* Template another file */}} {{ templateString "tmpl.stmpl" .Local }}{{/* Template another file and include the rendered output in this templates rendered output */}} +{{ templateStringInput "Hello {{ .Local.name }}" .Local }}{{/* Template a string and include the rendered output in this templates rendered output */}} ``` ### Registering templating functions @@ -184,8 +197,9 @@ sjs``` The `sjs` snippet can be used anywhere within your template (including multiple snippets) and will be replaced with any "rendered" output returned when using the `render` function. Naive transformation of typescript code is supported through [esbuild](https://esbuild.github.io/api/#transformation). This means that you can directly import typescript code and use type annotations in place of any JavaScript. However, be aware: - * EasyTemplate will not perform type checking itself. Type annotations are transformed into commented out code. - * Scripts/Snippets are not bundled, but executed as a single module on the global scope. This means no `import` statements are possible. [Instead, the global `require` function](#importing-javascript) is available to directly execute JS/TS code. + +* EasyTemplate will not perform type checking itself. Type annotations are transformed into commented out code. +* Scripts/Snippets are not bundled, but executed as a single module on the global scope. This means no `import` statements are possible. [Instead, the global `require` function](#importing-javascript) is available to directly execute JS/TS code. ### Context data @@ -305,6 +319,10 @@ The following functions are available to JavaScript from the templating engine: * `templateString(templateString, data)` - Render a template and return the rendered output. * `templateString` (string) - The template string to render. * `data` (object) - Data available to the template as `Local` context ie `{name: "John"}` is available as `{{ .Local.name }}`. +* `templateStringInput(templateName, templateString, data)` - Render a template and return the rendered output. + * `templateName` (string) - The name of the template to render. + * `templateString` (string) - The template string to render. + * `data` (object) - Data available to the template as `Local` context ie `{name: "John"}` is available as `{{ .Local.name }}`. * `render(output)` - Render the output to the template file, if called multiples times the output will be appended to the previous output as a new line. The cumulative output will replace the current `sjs` block in the template file. * `output` (string) - The output to render. * `require(filePath)` - Import a JavaScript file into the global scope. @@ -312,3 +330,7 @@ The following functions are available to JavaScript from the templating engine: * `registerTemplateFunc(name, func)` - Register a template function to be used in the template files. * `name` (string) - The name of the function to register. * `func` (function) - The function to register. + +Note: there are also versions of the above template methods that have a `Multiple` suffix, these methods will render the template multiple times (based on numTimes argument), +each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be +found in the `testdata/templates/testMultiple.stmpl` file. diff --git a/engine.go b/engine.go index e618aad..f855f5d 100644 --- a/engine.go +++ b/engine.go @@ -128,12 +128,15 @@ func New(opts ...Opt) *Engine { t.ReadFunc = e.readFile e.jsFuncs = map[string]func(call CallContext) goja.Value{ - "require": e.require, - "templateFile": e.templateFileJS, - "templateString": e.templateStringJS, - "templateStringInput": e.templateStringInputJS, - "registerTemplateFunc": e.registerTemplateFunc, - "unregisterTemplateFunc": e.unregisterTemplateFunc, + "require": e.require, + "templateFile": e.templateFileJS, + "templateFileMultiple": e.templateFileMultipleJS, + "templateString": e.templateStringJS, + "templateStringMultiple": e.templateStringMultipleJS, + "templateStringInput": e.templateStringInputJS, + "templateStringInputMultiple": e.templateStringInputMultipleJS, + "registerTemplateFunc": e.registerTemplateFunc, + "unregisterTemplateFunc": e.unregisterTemplateFunc, } for _, opt := range opts { @@ -205,6 +208,17 @@ func (e *Engine) RunTemplate(templateFile string, outFile string, data any) erro return e.templator.TemplateFile(vm, templateFile, outFile, data) } +// RunTemplateMultiple runs the provided template file numTimes, with the provided data, starting the template engine and templating the provided template to a file. +// The same LocalComputed context is available to each run. +func (e *Engine) RunTemplateMultiple(templateFile string, outFile string, data any, numTimes int) error { + vm, err := e.init(data) + if err != nil { + return err + } + + return e.templator.TemplateFileMultiple(vm, templateFile, outFile, data, numTimes) +} + // RunTemplateString runs the provided template file, with the provided data, starting the template engine and templating the provided template, returning the rendered result. func (e *Engine) RunTemplateString(templateFile string, data any) (string, error) { vm, err := e.init(data) @@ -215,6 +229,17 @@ func (e *Engine) RunTemplateString(templateFile string, data any) (string, error return e.templator.TemplateString(vm, templateFile, data) } +// RunTemplateStringMultiple runs the provided template file numTimes, with the provided data, starting the template engine and templating the provided template, returning the rendered result. +// The same LocalComputed context is available to each run. +func (e *Engine) RunTemplateStringMultiple(templateFile string, data any, numTimes int) (string, error) { + vm, err := e.init(data) + if err != nil { + return "", err + } + + return e.templator.TemplateStringMultiple(vm, templateFile, data, numTimes) +} + // RunTemplateStringInput runs the provided input template string, with the provided data, starting the template engine and templating the provided template, returning the rendered result. func (e *Engine) RunTemplateStringInput(name, template string, data any) (string, error) { vm, err := e.init(data) @@ -222,7 +247,18 @@ func (e *Engine) RunTemplateStringInput(name, template string, data any) (string return "", err } - return e.templator.TemplateStringInput(vm, name, template, data) + return e.templator.TemplateStringInput(vm, name, template, data, 1) +} + +// RunTemplateStringInputMultiple runs the provided input template string numTimes, with the provided data, starting the template engine and templating the provided template, returning the rendered result. +// The same LocalComputed context is available to each run. +func (e *Engine) RunTemplateStringInputMultiple(name, template string, data any, numTimes int) (string, error) { + vm, err := e.init(data) + if err != nil { + return "", err + } + + return e.templator.TemplateStringInput(vm, name, template, data, numTimes) } //nolint:funlen @@ -270,6 +306,16 @@ func (e *Engine) init(data any) (*vm.VM, error) { return "", nil } }(v) + e.templator.TmplFuncs["templateFileMultiple"] = func(v *vm.VM) func(string, string, any, int) (string, error) { + return func(templateFile, outFile string, data any, numTimes int) (string, error) { + err := e.templator.TemplateFileMultiple(v, templateFile, outFile, data, numTimes) + if err != nil { + return "", err + } + + return "", nil + } + }(v) e.templator.TmplFuncs["templateString"] = func(v *vm.VM) func(string, any) (string, error) { return func(templateFile string, data any) (string, error) { templated, err := e.templator.TemplateString(v, templateFile, data) @@ -280,9 +326,29 @@ func (e *Engine) init(data any) (*vm.VM, error) { return templated, nil } }(v) + e.templator.TmplFuncs["templateStringMultiple"] = func(v *vm.VM) func(string, any, int) (string, error) { + return func(templateFile string, data any, numTimes int) (string, error) { + templated, err := e.templator.TemplateStringMultiple(v, templateFile, data, numTimes) + if err != nil { + return "", err + } + + return templated, nil + } + }(v) e.templator.TmplFuncs["templateStringInput"] = func(v *vm.VM) func(string, string, any) (string, error) { return func(name, template string, data any) (string, error) { - templated, err := e.templator.TemplateStringInput(v, name, template, data) + templated, err := e.templator.TemplateStringInput(v, name, template, data, 1) + if err != nil { + return "", err + } + + return templated, nil + } + }(v) + e.templator.TmplFuncs["templateStringInputMultiple"] = func(v *vm.VM) func(string, string, any, int) (string, error) { + return func(name, template string, data any, numTimes int) (string, error) { + templated, err := e.templator.TemplateStringInput(v, name, template, data, numTimes) if err != nil { return "", err } diff --git a/go.mod b/go.mod index 387f2eb..c6e209b 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/speakeasy-api/easytemplate go 1.19 require ( - github.com/dop251/goja v0.0.0-20231024180952-594410467bc6 - github.com/dop251/goja_nodejs v0.0.0-20221211191749-434192f0843e - github.com/evanw/esbuild v0.17.8 + github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d + github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab + github.com/evanw/esbuild v0.19.5 github.com/go-sourcemap/sourcemap v2.1.3+incompatible github.com/golang/mock v1.6.0 github.com/stretchr/testify v1.8.1 @@ -13,10 +13,10 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dlclark/regexp2 v1.7.0 // indirect - github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect + github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/text v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8ed121f..9964c38 100644 --- a/go.sum +++ b/go.sum @@ -6,24 +6,25 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= -github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs= -github.com/dop251/goja v0.0.0-20231024180952-594410467bc6 h1:U9bRrSlYCu0P8hMulhIdYpr5HUao66tKPdNgD88Zi5M= -github.com/dop251/goja v0.0.0-20231024180952-594410467bc6/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= +github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw= +github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= -github.com/dop251/goja_nodejs v0.0.0-20221211191749-434192f0843e h1:DJ5cKH4HUYevCd09vMIbwc8U02eBKLFR2q1O1hSAJcY= -github.com/dop251/goja_nodejs v0.0.0-20221211191749-434192f0843e/go.mod h1:0tlktQL7yHfYEtjcRGi/eiOkbDR5XF7gyFFvbC5//E0= -github.com/evanw/esbuild v0.17.8 h1:QzE7cRRq7y3qH7ZKGN0/nUGbdZPyikfNaMaCMNUufnU= -github.com/evanw/esbuild v0.17.8/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk= +github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab h1:LrVf0AFnp5WiGKJ0a6cFf4RwNIN327uNUeVGJtmAFEE= +github.com/dop251/goja_nodejs v0.0.0-20231022114343-5c1f9037c9ab/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0= +github.com/evanw/esbuild v0.19.5 h1:9ildZqajUJzDAwNf9MyQsLh2RdDRKTq3kcyyzhE39us= +github.com/evanw/esbuild v0.19.5/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ= +github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -56,7 +57,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -70,18 +70,17 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/internal/template/template.go b/internal/template/template.go index a89eb3b..5c12608 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -77,6 +77,20 @@ func (t *Templator) TemplateFile(vm VM, templateFile, outFile string, inputData return nil } +// TemplateFileMultiple will template the provided file numTimes and write the output to outFile. +func (t *Templator) TemplateFileMultiple(vm VM, templateFile, outFile string, inputData any, numTimes int) error { + output, err := t.TemplateString(vm, templateFile, inputData) + if err != nil { + return err + } + + if err := t.WriteFunc(outFile, []byte(output)); err != nil { + return fmt.Errorf("failed to write file %s: %w", outFile, err) + } + + return nil +} + type inlineScriptContext struct { renderedContent []string } @@ -100,11 +114,21 @@ func (t *Templator) TemplateString(vm VM, templatePath string, inputData any) (o return "", fmt.Errorf("failed to read template file: %w", err) } - return t.TemplateStringInput(vm, templatePath, string(data), inputData) + return t.TemplateStringInput(vm, templatePath, string(data), inputData, 1) +} + +// TemplateStringMultiple will template the provided file numTimes and return the output as a string. +func (t *Templator) TemplateStringMultiple(vm VM, templatePath string, inputData any, numTimes int) (out string, err error) { + data, err := t.ReadFunc(templatePath) + if err != nil { + return "", fmt.Errorf("failed to read template file: %w", err) + } + + return t.TemplateStringInput(vm, templatePath, string(data), inputData, numTimes) } // TemplateStringInput will template the provided input string and return the output as a string. -func (t *Templator) TemplateStringInput(vm VM, name string, input string, inputData any) (out string, err error) { +func (t *Templator) TemplateStringInput(vm VM, name string, input string, inputData any, numTimes int) (out string, err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("failed to render template: %s", e) @@ -116,37 +140,43 @@ func (t *Templator) TemplateStringInput(vm VM, name string, input string, inputD return "", utils.HandleJSError("failed to create local computed context", err) } - context := &Context{ - Global: t.contextData, - GlobalComputed: t.globalComputed, - Local: inputData, - LocalComputed: localComputed, - } - currentContext := vm.Get("context") - if err := vm.Set("context", context); err != nil { - return "", fmt.Errorf("failed to set context: %w", err) - } + for i := 0; i < numTimes; i++ { + context := &Context{ + Global: t.contextData, + GlobalComputed: t.globalComputed, + Local: inputData, + LocalComputed: localComputed, + } - evaluated, replacedLines, err := t.evaluateInlineScripts(vm, name, input) - if err != nil { - return "", err - } + if err := vm.Set("context", context); err != nil { + return "", fmt.Errorf("failed to set context: %w", err) + } + + evaluated, replacedLines, err := t.evaluateInlineScripts(vm, name, input) + if err != nil { + return "", err + } - // Get the computed context back as it might have been modified by the inline script - localComputed = getComputedContext(vm) + // Get the computed context back as it might have been modified by the inline script + localComputed = getComputedContext(vm) - tmplCtx := &tmplContext{ - Global: context.Global, - Local: context.Local, - GlobalComputed: context.GlobalComputed.Export(), - LocalComputed: localComputed.Export(), - } + tmplCtx := &tmplContext{ + Global: context.Global, + Local: context.Local, + GlobalComputed: context.GlobalComputed.Export(), + LocalComputed: localComputed.Export(), + } - out, err = t.execTemplate(name, evaluated, tmplCtx, replacedLines) - if err != nil { - return "", err + out, err = t.execTemplate(name, evaluated, tmplCtx, replacedLines) + if err != nil { + return "", err + } + + // Set the output as the input for the next iteration and update the computed context + input = out + localComputed = getComputedContext(vm) } // Reset the context back to the previous one @@ -213,7 +243,7 @@ func getComputedContext(vm VM) goja.Value { func (t *Templator) execTemplate(name string, tmplContent string, data any, replacedLines int) (string, error) { tmp, err := template.New(name).Funcs(t.TmplFuncs).Parse(tmplContent) if err != nil { - return "", fmt.Errorf("failed to parse template: %w", err) + return "", fmt.Errorf("failed to parse template: %w\n----------------------------\n%s\n----------------------------", err, tmplContent) } var buf bytes.Buffer diff --git a/internal/template/template_test.go b/internal/template/template_test.go index 390ec57..b30dbd6 100644 --- a/internal/template/template_test.go +++ b/internal/template/template_test.go @@ -58,8 +58,8 @@ func TestTemplator_TemplateFile_Success(t *testing.T) { vm.EXPECT().Run("localCreateComputedContextObject", `createComputedContextObject();`).Return(goja.Undefined(), nil).Times(1) vm.EXPECT().Get("context").Return(goja.Undefined()).Times(1) vm.EXPECT().Set("context", context).Return(nil).Times(1) - vm.EXPECT().Get("context").Return(contextVal).Times(1) - vm.EXPECT().ToObject(contextVal).Return(contextVal.ToObject(o)).Times(1) + vm.EXPECT().Get("context").Return(contextVal).Times(2) + vm.EXPECT().ToObject(contextVal).Return(contextVal.ToObject(o)).Times(2) vm.EXPECT().Set("context", goja.Undefined()).Return(nil).Times(1) tr := &template.Templator{ @@ -165,8 +165,8 @@ func TestTemplator_TemplateString_Success(t *testing.T) { vm.EXPECT().Set("render", goja.Undefined()).Return(nil).Times(1) } - vm.EXPECT().Get("context").Return(contextVal).Times(1) - vm.EXPECT().ToObject(contextVal).Return(contextVal.ToObject(o)).Times(1) + vm.EXPECT().Get("context").Return(contextVal).Times(2) + vm.EXPECT().ToObject(contextVal).Return(contextVal.ToObject(o)).Times(2) vm.EXPECT().Set("context", goja.Undefined()).Return(nil).Times(1) tr := &template.Templator{ diff --git a/templating.go b/templating.go index 72f353d..eec8722 100644 --- a/templating.go +++ b/templating.go @@ -14,6 +14,20 @@ func (e *Engine) templateFileJS(call CallContext) goja.Value { return goja.Undefined() } +//nolint:gomnd +func (e *Engine) templateFileMultipleJS(call CallContext) goja.Value { + templateFile := call.Argument(0).String() + outFile := call.Argument(1).String() + inputData := call.Argument(2).Export() + numTimes := call.Argument(3).ToInteger() + + if err := e.templator.TemplateFileMultiple(call.VM, templateFile, outFile, inputData, int(numTimes)); err != nil { + panic(call.VM.NewGoError(err)) + } + + return goja.Undefined() +} + func (e *Engine) templateStringJS(call CallContext) goja.Value { inputData := call.Argument(1).Export() @@ -25,10 +39,39 @@ func (e *Engine) templateStringJS(call CallContext) goja.Value { return call.VM.ToValue(output) } +//nolint:gomnd +func (e *Engine) templateStringMultipleJS(call CallContext) goja.Value { + templatePath := call.Argument(0).String() + inputData := call.Argument(1).Export() + numTimes := call.Argument(2).ToInteger() + + output, err := e.templator.TemplateStringMultiple(call.VM, templatePath, inputData, int(numTimes)) + if err != nil { + panic(call.VM.NewGoError(err)) + } + + return call.VM.ToValue(output) +} + func (e *Engine) templateStringInputJS(call CallContext) goja.Value { inputData := call.Argument(2).Export() //nolint:gomnd - output, err := e.templator.TemplateStringInput(call.VM, call.Argument(0).String(), call.Argument(1).String(), inputData) + output, err := e.templator.TemplateStringInput(call.VM, call.Argument(0).String(), call.Argument(1).String(), inputData, 1) + if err != nil { + panic(call.VM.NewGoError(err)) + } + + return call.VM.ToValue(output) +} + +//nolint:gomnd +func (e *Engine) templateStringInputMultipleJS(call CallContext) goja.Value { + name := call.Argument(0).String() + input := call.Argument(1).String() + inputData := call.Argument(2).Export() + numTimes := call.Argument(3).ToInteger() + + output, err := e.templator.TemplateStringInput(call.VM, name, input, inputData, int(numTimes)) if err != nil { panic(call.VM.NewGoError(err)) } diff --git a/testdata/expected/test.txt b/testdata/expected/test.txt index 80e330b..9d1ccac 100644 --- a/testdata/expected/test.txt +++ b/testdata/expected/test.txt @@ -1,4 +1,7 @@ global from test.js -34.000 \ No newline at end of file +34.000 +Hello User +Hello World it's a wonderful day! + diff --git a/testdata/templates/test.stmpl b/testdata/templates/test.stmpl index b16162f..b05969d 100644 --- a/testdata/templates/test.stmpl +++ b/testdata/templates/test.stmpl @@ -1,4 +1,6 @@ {{.Global.Test}} {{.Local.Test}} {{templateFile "templates/test2.stmpl" "test2.txt" "from test.stmpl"}} -{{toFloatWithPrecision .Local.Value 3}} \ No newline at end of file +{{toFloatWithPrecision .Local.Value 3}} +{{templateStringInput "hello" "Hello {{.Local}}" "User"}} +{{templateStringMultiple "templates/testMultiple.stmpl" nil 3}} \ No newline at end of file diff --git a/testdata/templates/testMultiple.stmpl b/testdata/templates/testMultiple.stmpl new file mode 100644 index 0000000..95f61b4 --- /dev/null +++ b/testdata/templates/testMultiple.stmpl @@ -0,0 +1,10 @@ +{{"{{\"{{.LocalComputed.AccumulatedString}}\"}}"}}{{/* Double escaped so it will unfurl fully by the third iteration to template*/}} +```sjs +if (!context.LocalComputed.AccumulatedString) { + context.LocalComputed.AccumulatedString = ""; +} +context.LocalComputed.AccumulatedString += "Hello World"; +sjs``` +{{"```"}}{{"sjs"}}{{/* Escaped so it will only be executed on the second iteration */}} +context.LocalComputed.AccumulatedString += " it's a wonderful day!"; +{{"sjs"}}{{"```"}} \ No newline at end of file From 970bb271eec7594fa3aee8f30a1e1d13ea6a47ed Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 10 Nov 2023 09:28:29 +0000 Subject: [PATCH 02/14] fix: linting --- engine.go | 2 +- engine_integration_test.go | 2 +- internal/template/template.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/engine.go b/engine.go index f855f5d..e0287a5 100644 --- a/engine.go +++ b/engine.go @@ -261,7 +261,7 @@ func (e *Engine) RunTemplateStringInputMultiple(name, template string, data any, return e.templator.TemplateStringInput(vm, name, template, data, numTimes) } -//nolint:funlen +//nolint:funlen,cyclop func (e *Engine) init(data any) (*vm.VM, error) { if e.ran { return nil, ErrAlreadyRan diff --git a/engine_integration_test.go b/engine_integration_test.go index 4f18573..bbc65a1 100644 --- a/engine_integration_test.go +++ b/engine_integration_test.go @@ -54,7 +54,7 @@ func TestEngine_RunScript_Success(t *testing.T) { err = e.RunScript("scripts/test.js", map[string]interface{}{ "Test": "global", }) - assert.NoError(t, err) + require.NoError(t, err) assert.Empty(t, expectedFiles, "not all expected files were written") } diff --git a/internal/template/template.go b/internal/template/template.go index 5c12608..f8c77a9 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -79,7 +79,7 @@ func (t *Templator) TemplateFile(vm VM, templateFile, outFile string, inputData // TemplateFileMultiple will template the provided file numTimes and write the output to outFile. func (t *Templator) TemplateFileMultiple(vm VM, templateFile, outFile string, inputData any, numTimes int) error { - output, err := t.TemplateString(vm, templateFile, inputData) + output, err := t.TemplateStringMultiple(vm, templateFile, inputData, numTimes) if err != nil { return err } From 8ff981131ddf44f810c488316c56c6cd252e873b Mon Sep 17 00:00:00 2001 From: Tristan Cartledge <108070248+TristanSpeakEasy@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:36:47 +0000 Subject: [PATCH 03/14] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3300892..b181cff 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ A number of methods are available to start the engine, including: * `templateFile` (string) - The path to the template file to start the engine from. * `data` (any) - Context data to provide to templates and scripts. Available as `{{.Global}}` in templates and `context.Global` in scripts. -Note: there are also version of the above methods that have a `Multiple` suffix, these methods will render the template multiple times (based on numTimes argument), +Note: there are also versions of the above methods that have a `Multiple` suffix, these methods will render the template multiple times (based on the `numTimes` argument), each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be found in the `testdata/templates/testMultiple.stmpl` file. From e183de7686bfc266a9716d485ebb443ca85bce92 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge <108070248+TristanSpeakEasy@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:37:12 +0000 Subject: [PATCH 04/14] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b181cff..801e8cb 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ This is done by calling the following functions from within templates and script * `templateString` (string) - An input template string to template. * `data` (any) - Context data to provide to templates and scripts. Available as `{{.Local}}` in templates and `context.Local` in scripts. -Note: there are also version of the above methods that have a `Multiple` suffix, these methods will render the template multiple times (based on numTimes argument), +Note: there are also versions of the above methods that have a `Multiple` suffix, these methods will render the template multiple times (based on the `numTimes` argument), each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be found in the `testdata/templates/testMultiple.stmpl` file. From c67b984991337accc14a655f417e708ba6f3dc9a Mon Sep 17 00:00:00 2001 From: Tristan Cartledge <108070248+TristanSpeakEasy@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:37:41 +0000 Subject: [PATCH 05/14] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 801e8cb..d352350 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,6 @@ The following functions are available to JavaScript from the templating engine: * `name` (string) - The name of the function to register. * `func` (function) - The function to register. -Note: there are also versions of the above template methods that have a `Multiple` suffix, these methods will render the template multiple times (based on numTimes argument), +Note: there are also versions of the above template methods that have a `Multiple` suffix, these methods will render the template multiple times (based on the `numTimes` argument), each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be found in the `testdata/templates/testMultiple.stmpl` file. From 56ecf702e5006c9b1cf93af60853afb669691192 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 10 Nov 2023 09:50:28 +0000 Subject: [PATCH 06/14] fix --- internal/template/template.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/template/template.go b/internal/template/template.go index f8c77a9..c630770 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -243,7 +243,7 @@ func getComputedContext(vm VM) goja.Value { func (t *Templator) execTemplate(name string, tmplContent string, data any, replacedLines int) (string, error) { tmp, err := template.New(name).Funcs(t.TmplFuncs).Parse(tmplContent) if err != nil { - return "", fmt.Errorf("failed to parse template: %w\n----------------------------\n%s\n----------------------------", err, tmplContent) + return "", fmt.Errorf("failed to parse template: %w", err) } var buf bytes.Buffer @@ -269,7 +269,7 @@ func adjustLineNumber(name string, err error, replacedLines int) error { return matches[0], nil //nolint:nilerr } - return strings.Replace(matches[0], matches[1], fmt.Sprintf("%d", currentLineNumber+replacedLines), 1), nil + return strings.Replace(matches[0], matches[1], strconv.Itoa(currentLineNumber+replacedLines), 1), nil }) if rErr == nil { err = fmt.Errorf(errMsg) From 6f337dbfbfe438d4d96654592a379cd00ef2d793 Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Fri, 10 Nov 2023 12:27:37 +0000 Subject: [PATCH 07/14] control execution count from js --- engine.go | 11 ++++++ internal/template/template.go | 36 +++++++++++++++++++ templating.go | 9 +++++ testdata/expected/test.txt | 2 +- testdata/templates/test.stmpl | 3 +- testdata/templates/testMultiple.stmpl | 4 +-- testdata/templates/testMultipleIsolated.stmpl | 11 ++++++ 7 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 testdata/templates/testMultipleIsolated.stmpl diff --git a/engine.go b/engine.go index e0287a5..7d27a33 100644 --- a/engine.go +++ b/engine.go @@ -129,6 +129,7 @@ func New(opts ...Opt) *Engine { e.jsFuncs = map[string]func(call CallContext) goja.Value{ "require": e.require, + "recurse": e.recurseJS, "templateFile": e.templateFileJS, "templateFileMultiple": e.templateFileMultipleJS, "templateString": e.templateStringJS, @@ -356,6 +357,16 @@ func (e *Engine) init(data any) (*vm.VM, error) { return templated, nil } }(v) + e.templator.TmplFuncs["recurse"] = func(v *vm.VM) func(int) (string, error) { + return func(numTimes int) (string, error) { + templated, err := e.templator.Recurse(v, numTimes) + if err != nil { + return "", err + } + + return templated, nil + } + }(v) if _, err := v.Run("initCreateComputedContextObject", `function createComputedContextObject() { return {}; }`); err != nil { return nil, utils.HandleJSError("failed to init createComputedContextObject", err) diff --git a/internal/template/template.go b/internal/template/template.go index c630770..e4c795b 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -6,6 +6,7 @@ package template import ( "bytes" "fmt" + "math" "regexp" "strconv" "strings" @@ -176,6 +177,9 @@ func (t *Templator) TemplateStringInput(vm VM, name string, input string, inputD // Set the output as the input for the next iteration and update the computed context input = out + if i == 0 { + input, numTimes = t.applyRecurseCanary(input, numTimes) + } localComputed = getComputedContext(vm) } @@ -256,6 +260,38 @@ func (t *Templator) execTemplate(name string, tmplContent string, data any, repl return buf.String(), nil } +func RecurseCanary() []string { + return []string{ + "5f9c88133f5cc60a84009b841192a2d8f83dd901e8112fe77284e461b4039ccd", + "f34e13e176147bebc0e4d644fd45d5727462c3b574b3ffc00df1b4683c15e54c", + "6f6fd98465b1ef727f0692a3d08f70cb4b94ab987a97d0ee206d3d33d2032887", + "f1cd9fe131302d6f37b17084fa8b7679debb6bff79853171e439483bbc2cb846", + "2290a2c385c9f40a2af9f56095630ab8b80e067cdf8fc92beef93c2b16c2b8aa", + } +} + +// Recurse will let the engine know how many times the template should execute +func (t *Templator) Recurse(vm VM, numTimes int) (out string, err error) { + if numTimes < 1 || numTimes > len(RecurseCanary()) { + return "", fmt.Errorf("recurse(%v) invalid: %v outside bounds 1..%v", numTimes, numTimes, len(RecurseCanary())) + } + + return RecurseCanary()[numTimes-1], nil +} + +func (t *Templator) applyRecurseCanary(input string, recurseCount int) (string, int) { + canaryList := RecurseCanary() + for i, canary := range canaryList { + if strings.Contains(input, canary) { + // max of recurseCount, len(canaryList) - i + recurseCount = int(math.Max(float64(recurseCount), float64(i+2))) + } + input = strings.ReplaceAll(input, canary, "") + } + + return input, recurseCount +} + func adjustLineNumber(name string, err error, replacedLines int) error { lineNumRegex, rErr := regexp.Compile(fmt.Sprintf(`template: %s:(\d+)`, regexp.QuoteMeta(name))) if rErr == nil { diff --git a/templating.go b/templating.go index eec8722..fbd35ba 100644 --- a/templating.go +++ b/templating.go @@ -64,6 +64,15 @@ func (e *Engine) templateStringInputJS(call CallContext) goja.Value { return call.VM.ToValue(output) } +func (e *Engine) recurseJS(call CallContext) goja.Value { + output, err := e.templator.Recurse(call.VM, int(call.Argument(0).ToInteger())) + if err != nil { + panic(call.VM.NewGoError(err)) + } + + return call.VM.ToValue(output) +} + //nolint:gomnd func (e *Engine) templateStringInputMultipleJS(call CallContext) goja.Value { name := call.Argument(0).String() diff --git a/testdata/expected/test.txt b/testdata/expected/test.txt index 9d1ccac..978b1b3 100644 --- a/testdata/expected/test.txt +++ b/testdata/expected/test.txt @@ -4,4 +4,4 @@ from test.js 34.000 Hello User Hello World it's a wonderful day! - +Hello World it's a wonderful day! (Isolated) \ No newline at end of file diff --git a/testdata/templates/test.stmpl b/testdata/templates/test.stmpl index b05969d..68ec600 100644 --- a/testdata/templates/test.stmpl +++ b/testdata/templates/test.stmpl @@ -3,4 +3,5 @@ {{templateFile "templates/test2.stmpl" "test2.txt" "from test.stmpl"}} {{toFloatWithPrecision .Local.Value 3}} {{templateStringInput "hello" "Hello {{.Local}}" "User"}} -{{templateStringMultiple "templates/testMultiple.stmpl" nil 3}} \ No newline at end of file +{{templateStringMultiple "templates/testMultiple.stmpl" nil 3}} +{{templateString "templates/testMultipleIsolated.stmpl" nil}} \ No newline at end of file diff --git a/testdata/templates/testMultiple.stmpl b/testdata/templates/testMultiple.stmpl index 95f61b4..48233d6 100644 --- a/testdata/templates/testMultiple.stmpl +++ b/testdata/templates/testMultiple.stmpl @@ -1,10 +1,10 @@ -{{"{{\"{{.LocalComputed.AccumulatedString}}\"}}"}}{{/* Double escaped so it will unfurl fully by the third iteration to template*/}} +{{"{{\"{{.LocalComputed.AccumulatedString}}\" -}}"}}{{/* Double escaped so it will unfurl fully by the third iteration to template*/}} ```sjs if (!context.LocalComputed.AccumulatedString) { context.LocalComputed.AccumulatedString = ""; } context.LocalComputed.AccumulatedString += "Hello World"; sjs``` -{{"```"}}{{"sjs"}}{{/* Escaped so it will only be executed on the second iteration */}} +{{- "```"}}{{"sjs"}}{{/* Escaped so it will only be executed on the second iteration */}} context.LocalComputed.AccumulatedString += " it's a wonderful day!"; {{"sjs"}}{{"```"}} \ No newline at end of file diff --git a/testdata/templates/testMultipleIsolated.stmpl b/testdata/templates/testMultipleIsolated.stmpl new file mode 100644 index 0000000..8e2a4f8 --- /dev/null +++ b/testdata/templates/testMultipleIsolated.stmpl @@ -0,0 +1,11 @@ +{{- recurse 2 -}} +{{"{{\"{{.LocalComputed.AccumulatedString}}\"}}" -}}{{/* Double escaped so it will unfurl fully by the third iteration to template*/}} +```sjs +if (!context.LocalComputed.AccumulatedString) { + context.LocalComputed.AccumulatedString = ""; +} +context.LocalComputed.AccumulatedString += "Hello World"; +sjs``` +{{- "```"}}{{"sjs"}}{{/* Escaped so it will only be executed on the second iteration */}} +context.LocalComputed.AccumulatedString += " it's a wonderful day! (Isolated)"; +{{"sjs"}}{{"```"}} \ No newline at end of file From f8edc38f74c7a6f22e82eb2e29a13cdd84c6603b Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Fri, 10 Nov 2023 12:30:57 +0000 Subject: [PATCH 08/14] improve comment --- internal/template/template.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/template/template.go b/internal/template/template.go index e4c795b..e0b534f 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -279,17 +279,18 @@ func (t *Templator) Recurse(vm VM, numTimes int) (out string, err error) { return RecurseCanary()[numTimes-1], nil } -func (t *Templator) applyRecurseCanary(input string, recurseCount int) (string, int) { +func (t *Templator) applyRecurseCanary(input string, execCount int) (string, int) { canaryList := RecurseCanary() for i, canary := range canaryList { if strings.Contains(input, canary) { - // max of recurseCount, len(canaryList) - i - recurseCount = int(math.Max(float64(recurseCount), float64(i+2))) + // recurse 1 means canary[0] is found, and execCount is now 2 + // if more than 1 "recurse" invocation in template, use the largest + execCount = int(math.Max(float64(execCount), float64(i+2))) } input = strings.ReplaceAll(input, canary, "") } - return input, recurseCount + return input, execCount } func adjustLineNumber(name string, err error, replacedLines int) error { From 49749526c12f8d3da37a8f6e18fa47bb0908d93c Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Fri, 10 Nov 2023 12:33:29 +0000 Subject: [PATCH 09/14] lint --- internal/template/template.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/template/template.go b/internal/template/template.go index e0b534f..7cbfba4 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -260,7 +260,7 @@ func (t *Templator) execTemplate(name string, tmplContent string, data any, repl return buf.String(), nil } -func RecurseCanary() []string { +func recurseCanary() []string { return []string{ "5f9c88133f5cc60a84009b841192a2d8f83dd901e8112fe77284e461b4039ccd", "f34e13e176147bebc0e4d644fd45d5727462c3b574b3ffc00df1b4683c15e54c", @@ -270,22 +270,23 @@ func RecurseCanary() []string { } } -// Recurse will let the engine know how many times the template should execute +// Recurse will let the engine know how many times the template should execute. func (t *Templator) Recurse(vm VM, numTimes int) (out string, err error) { - if numTimes < 1 || numTimes > len(RecurseCanary()) { - return "", fmt.Errorf("recurse(%v) invalid: %v outside bounds 1..%v", numTimes, numTimes, len(RecurseCanary())) + if numTimes < 1 || numTimes > len(recurseCanary()) { + return "", fmt.Errorf("recurse(%v) invalid: %v outside bounds 1..%v", numTimes, numTimes, len(recurseCanary())) } - return RecurseCanary()[numTimes-1], nil + return recurseCanary()[numTimes-1], nil } func (t *Templator) applyRecurseCanary(input string, execCount int) (string, int) { - canaryList := RecurseCanary() + canaryList := recurseCanary() + const recurseArgumentToExecCount = 2 for i, canary := range canaryList { if strings.Contains(input, canary) { // recurse 1 means canary[0] is found, and execCount is now 2 // if more than 1 "recurse" invocation in template, use the largest - execCount = int(math.Max(float64(execCount), float64(i+2))) + execCount = int(math.Max(float64(execCount), float64(i+recurseArgumentToExecCount))) } input = strings.ReplaceAll(input, canary, "") } From 822ce99598f5ebc4efdfcd8c57cfa7693bad517b Mon Sep 17 00:00:00 2001 From: Thomas Rooney Date: Fri, 10 Nov 2023 12:36:56 +0000 Subject: [PATCH 10/14] lint --- engine.go | 2 +- internal/template/template.go | 2 +- internal/utils/utils.go | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/engine.go b/engine.go index 7d27a33..1491e56 100644 --- a/engine.go +++ b/engine.go @@ -262,7 +262,7 @@ func (e *Engine) RunTemplateStringInputMultiple(name, template string, data any, return e.templator.TemplateStringInput(vm, name, template, data, numTimes) } -//nolint:funlen,cyclop +//nolint:funlen,cyclop,gocognit func (e *Engine) init(data any) (*vm.VM, error) { if e.ran { return nil, ErrAlreadyRan diff --git a/internal/template/template.go b/internal/template/template.go index 7cbfba4..d08cf81 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -271,7 +271,7 @@ func recurseCanary() []string { } // Recurse will let the engine know how many times the template should execute. -func (t *Templator) Recurse(vm VM, numTimes int) (out string, err error) { +func (t *Templator) Recurse(_ VM, numTimes int) (out string, err error) { if numTimes < 1 || numTimes > len(recurseCanary()) { return "", fmt.Errorf("recurse(%v) invalid: %v outside bounds 1..%v", numTimes, numTimes, len(recurseCanary())) } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index a52b68b..39b9872 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -14,6 +14,7 @@ func ReplaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str result := "" lastIndex := 0 + //nolint:mirror for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { groups := []string{} for i := 0; i < len(v); i += 2 { From 45c54f438840bd9e149f318ef29f57ee6c3d114f Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 10 Nov 2023 16:45:00 +0000 Subject: [PATCH 11/14] fix --- README.md | 27 +-- engine.go | 84 +------- internal/template/template.go | 191 +++++++++++------- internal/template/template_test.go | 35 ++-- templating.go | 45 +---- testdata/expected/test.txt | 6 +- testdata/templates/recursiveSubTemplate.stmpl | 12 ++ testdata/templates/test.stmpl | 3 +- testdata/templates/testMultiple.stmpl | 15 +- testdata/templates/testMultipleIsolated.stmpl | 11 - .../templates/testMultipleSubTemplate.stmpl | 4 + 11 files changed, 192 insertions(+), 241 deletions(-) create mode 100644 testdata/templates/recursiveSubTemplate.stmpl delete mode 100644 testdata/templates/testMultipleIsolated.stmpl create mode 100644 testdata/templates/testMultipleSubTemplate.stmpl diff --git a/README.md b/README.md index d352350..2f545b1 100644 --- a/README.md +++ b/README.md @@ -132,10 +132,6 @@ A number of methods are available to start the engine, including: * `templateFile` (string) - The path to the template file to start the engine from. * `data` (any) - Context data to provide to templates and scripts. Available as `{{.Global}}` in templates and `context.Global` in scripts. -Note: there are also versions of the above methods that have a `Multiple` suffix, these methods will render the template multiple times (based on the `numTimes` argument), -each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be -found in the `testdata/templates/testMultiple.stmpl` file. - ### Controlling the flow of templating The engine allows you to control the flow of templating from within templates and scripts themselves. This means from a single entry point you can start multiple templates and scripts. @@ -153,10 +149,8 @@ This is done by calling the following functions from within templates and script * `templateName` (string) - The name of the template to render. * `templateString` (string) - An input template string to template. * `data` (any) - Context data to provide to templates and scripts. Available as `{{.Local}}` in templates and `context.Local` in scripts. - -Note: there are also versions of the above methods that have a `Multiple` suffix, these methods will render the template multiple times (based on the `numTimes` argument), -each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be -found in the `testdata/templates/testMultiple.stmpl` file. +* `recurse(recursions int) string` - Recurse the current template file, recursions is the number of times to recurse the template file. + * `recursions` (int) - The number of times to recurse the template file. This allows for example: @@ -166,6 +160,19 @@ This allows for example: {{ templateStringInput "Hello {{ .Local.name }}" .Local }}{{/* Template a string and include the rendered output in this templates rendered output */}} ``` +#### Recursive templating + +It is possible with the `recurse` function in a template to render the same template multiple times. This can be useful when data to render parts of the template are only available after you have rendered it at least once. + +For example: + +```go +{{- recurse 1 -}} +{{"{{.L}}"}} +{{ }} + +``` + ### Registering templating functions The engine allows you to register custom templating functions from Go which can be used within the templates. @@ -330,7 +337,3 @@ The following functions are available to JavaScript from the templating engine: * `registerTemplateFunc(name, func)` - Register a template function to be used in the template files. * `name` (string) - The name of the function to register. * `func` (function) - The function to register. - -Note: there are also versions of the above template methods that have a `Multiple` suffix, these methods will render the template multiple times (based on the `numTimes` argument), -each template run will have access to the same LocalComputed context allowing for data to be accumulated across runs and rendered out later. Examples of this can be -found in the `testdata/templates/testMultiple.stmpl` file. diff --git a/engine.go b/engine.go index 1491e56..bf1b7cf 100644 --- a/engine.go +++ b/engine.go @@ -128,16 +128,13 @@ func New(opts ...Opt) *Engine { t.ReadFunc = e.readFile e.jsFuncs = map[string]func(call CallContext) goja.Value{ - "require": e.require, - "recurse": e.recurseJS, - "templateFile": e.templateFileJS, - "templateFileMultiple": e.templateFileMultipleJS, - "templateString": e.templateStringJS, - "templateStringMultiple": e.templateStringMultipleJS, - "templateStringInput": e.templateStringInputJS, - "templateStringInputMultiple": e.templateStringInputMultipleJS, - "registerTemplateFunc": e.registerTemplateFunc, - "unregisterTemplateFunc": e.unregisterTemplateFunc, + "require": e.require, + "recurse": e.recurseJS, + "templateFile": e.templateFileJS, + "templateString": e.templateStringJS, + "templateStringInput": e.templateStringInputJS, + "registerTemplateFunc": e.registerTemplateFunc, + "unregisterTemplateFunc": e.unregisterTemplateFunc, } for _, opt := range opts { @@ -209,17 +206,6 @@ func (e *Engine) RunTemplate(templateFile string, outFile string, data any) erro return e.templator.TemplateFile(vm, templateFile, outFile, data) } -// RunTemplateMultiple runs the provided template file numTimes, with the provided data, starting the template engine and templating the provided template to a file. -// The same LocalComputed context is available to each run. -func (e *Engine) RunTemplateMultiple(templateFile string, outFile string, data any, numTimes int) error { - vm, err := e.init(data) - if err != nil { - return err - } - - return e.templator.TemplateFileMultiple(vm, templateFile, outFile, data, numTimes) -} - // RunTemplateString runs the provided template file, with the provided data, starting the template engine and templating the provided template, returning the rendered result. func (e *Engine) RunTemplateString(templateFile string, data any) (string, error) { vm, err := e.init(data) @@ -230,17 +216,6 @@ func (e *Engine) RunTemplateString(templateFile string, data any) (string, error return e.templator.TemplateString(vm, templateFile, data) } -// RunTemplateStringMultiple runs the provided template file numTimes, with the provided data, starting the template engine and templating the provided template, returning the rendered result. -// The same LocalComputed context is available to each run. -func (e *Engine) RunTemplateStringMultiple(templateFile string, data any, numTimes int) (string, error) { - vm, err := e.init(data) - if err != nil { - return "", err - } - - return e.templator.TemplateStringMultiple(vm, templateFile, data, numTimes) -} - // RunTemplateStringInput runs the provided input template string, with the provided data, starting the template engine and templating the provided template, returning the rendered result. func (e *Engine) RunTemplateStringInput(name, template string, data any) (string, error) { vm, err := e.init(data) @@ -248,18 +223,7 @@ func (e *Engine) RunTemplateStringInput(name, template string, data any) (string return "", err } - return e.templator.TemplateStringInput(vm, name, template, data, 1) -} - -// RunTemplateStringInputMultiple runs the provided input template string numTimes, with the provided data, starting the template engine and templating the provided template, returning the rendered result. -// The same LocalComputed context is available to each run. -func (e *Engine) RunTemplateStringInputMultiple(name, template string, data any, numTimes int) (string, error) { - vm, err := e.init(data) - if err != nil { - return "", err - } - - return e.templator.TemplateStringInput(vm, name, template, data, numTimes) + return e.templator.TemplateStringInput(vm, name, template, data) } //nolint:funlen,cyclop,gocognit @@ -307,16 +271,6 @@ func (e *Engine) init(data any) (*vm.VM, error) { return "", nil } }(v) - e.templator.TmplFuncs["templateFileMultiple"] = func(v *vm.VM) func(string, string, any, int) (string, error) { - return func(templateFile, outFile string, data any, numTimes int) (string, error) { - err := e.templator.TemplateFileMultiple(v, templateFile, outFile, data, numTimes) - if err != nil { - return "", err - } - - return "", nil - } - }(v) e.templator.TmplFuncs["templateString"] = func(v *vm.VM) func(string, any) (string, error) { return func(templateFile string, data any) (string, error) { templated, err := e.templator.TemplateString(v, templateFile, data) @@ -327,29 +281,9 @@ func (e *Engine) init(data any) (*vm.VM, error) { return templated, nil } }(v) - e.templator.TmplFuncs["templateStringMultiple"] = func(v *vm.VM) func(string, any, int) (string, error) { - return func(templateFile string, data any, numTimes int) (string, error) { - templated, err := e.templator.TemplateStringMultiple(v, templateFile, data, numTimes) - if err != nil { - return "", err - } - - return templated, nil - } - }(v) e.templator.TmplFuncs["templateStringInput"] = func(v *vm.VM) func(string, string, any) (string, error) { return func(name, template string, data any) (string, error) { - templated, err := e.templator.TemplateStringInput(v, name, template, data, 1) - if err != nil { - return "", err - } - - return templated, nil - } - }(v) - e.templator.TmplFuncs["templateStringInputMultiple"] = func(v *vm.VM) func(string, string, any, int) (string, error) { - return func(name, template string, data any, numTimes int) (string, error) { - templated, err := e.templator.TemplateStringInput(v, name, template, data, numTimes) + templated, err := e.templator.TemplateStringInput(v, name, template, data) if err != nil { return "", err } diff --git a/internal/template/template.go b/internal/template/template.go index d08cf81..b7b4960 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -6,7 +6,6 @@ package template import ( "bytes" "fmt" - "math" "regexp" "strconv" "strings" @@ -28,17 +27,19 @@ var sjsRegex = regexp.MustCompile("(?ms)(```sjs\\s*\\n*(.*?)sjs```)") // Context is the context that is passed templates or js. type Context struct { - Global any - Local any - GlobalComputed goja.Value - LocalComputed goja.Value + Global any + Local any + GlobalComputed goja.Value + LocalComputed goja.Value + RecursiveComputed goja.Value } type tmplContext struct { - Global any - Local any - GlobalComputed any - LocalComputed any + Global any + Local any + GlobalComputed any + LocalComputed any + RecursiveComputed any } // VM represents a virtual machine that can be used to run js. @@ -78,20 +79,6 @@ func (t *Templator) TemplateFile(vm VM, templateFile, outFile string, inputData return nil } -// TemplateFileMultiple will template the provided file numTimes and write the output to outFile. -func (t *Templator) TemplateFileMultiple(vm VM, templateFile, outFile string, inputData any, numTimes int) error { - output, err := t.TemplateStringMultiple(vm, templateFile, inputData, numTimes) - if err != nil { - return err - } - - if err := t.WriteFunc(outFile, []byte(output)); err != nil { - return fmt.Errorf("failed to write file %s: %w", outFile, err) - } - - return nil -} - type inlineScriptContext struct { renderedContent []string } @@ -115,21 +102,11 @@ func (t *Templator) TemplateString(vm VM, templatePath string, inputData any) (o return "", fmt.Errorf("failed to read template file: %w", err) } - return t.TemplateStringInput(vm, templatePath, string(data), inputData, 1) -} - -// TemplateStringMultiple will template the provided file numTimes and return the output as a string. -func (t *Templator) TemplateStringMultiple(vm VM, templatePath string, inputData any, numTimes int) (out string, err error) { - data, err := t.ReadFunc(templatePath) - if err != nil { - return "", fmt.Errorf("failed to read template file: %w", err) - } - - return t.TemplateStringInput(vm, templatePath, string(data), inputData, numTimes) + return t.TemplateStringInput(vm, templatePath, string(data), inputData) } // TemplateStringInput will template the provided input string and return the output as a string. -func (t *Templator) TemplateStringInput(vm VM, name string, input string, inputData any, numTimes int) (out string, err error) { +func (t *Templator) TemplateStringInput(vm VM, name string, input string, inputData any) (out string, err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("failed to render template: %s", e) @@ -143,12 +120,29 @@ func (t *Templator) TemplateStringInput(vm VM, name string, input string, inputD currentContext := vm.Get("context") - for i := 0; i < numTimes; i++ { + currentRecursiveComputed := getRecursiveComputedContext(vm) + localRecursiveComputed := currentRecursiveComputed + + numIterations := 1 + numRecursions, err := t.isRecursiveTemplate(input) + if err != nil { + return "", err + } + if numRecursions > 0 { + numIterations = numRecursions + 1 + localRecursiveComputed, err = vm.Run("recursiveCreateComputedContextObject", `createComputedContextObject();`) + if err != nil { + return "", utils.HandleJSError("failed to create recursive computed context", err) + } + } + + for i := 0; i < numIterations; i++ { context := &Context{ - Global: t.contextData, - GlobalComputed: t.globalComputed, - Local: inputData, - LocalComputed: localComputed, + Global: t.contextData, + GlobalComputed: t.globalComputed, + Local: inputData, + LocalComputed: localComputed, + RecursiveComputed: localRecursiveComputed, } if err := vm.Set("context", context); err != nil { @@ -161,13 +155,14 @@ func (t *Templator) TemplateStringInput(vm VM, name string, input string, inputD } // Get the computed context back as it might have been modified by the inline script - localComputed = getComputedContext(vm) + localComputed = getLocalComputedContext(vm) tmplCtx := &tmplContext{ - Global: context.Global, - Local: context.Local, - GlobalComputed: context.GlobalComputed.Export(), - LocalComputed: localComputed.Export(), + Global: context.Global, + Local: context.Local, + GlobalComputed: context.GlobalComputed.Export(), + LocalComputed: localComputed.Export(), + RecursiveComputed: localRecursiveComputed.Export(), } out, err = t.execTemplate(name, evaluated, tmplCtx, replacedLines) @@ -176,11 +171,16 @@ func (t *Templator) TemplateStringInput(vm VM, name string, input string, inputD } // Set the output as the input for the next iteration and update the computed context - input = out - if i == 0 { - input, numTimes = t.applyRecurseCanary(input, numTimes) + var cont bool + input, cont, err = t.applyRecurseCanary(out) + if err != nil { + return "", err + } + // If the output is the same as the input, or no canary found, we don't need to continue + if !cont || evaluated == out { + break } - localComputed = getComputedContext(vm) + localRecursiveComputed = getRecursiveComputedContext(vm) } // Reset the context back to the previous one @@ -235,7 +235,7 @@ func (t *Templator) execSJSBlock(v VM, js, templatePath string, jsBlockLineNumbe return strings.Join(c.renderedContent, "\n"), nil } -func getComputedContext(vm VM) goja.Value { +func getLocalComputedContext(vm VM) goja.Value { // Get the local context back as it might have been modified by the inline script contextVal := vm.Get("context") @@ -244,6 +244,17 @@ func getComputedContext(vm VM) goja.Value { return computedVal } +func getRecursiveComputedContext(vm VM) goja.Value { + contextVal := vm.Get("context") + if contextVal == goja.Undefined() { + return goja.Undefined() + } + + computedVal := vm.ToObject(contextVal).Get("RecursiveComputed") + + return computedVal +} + func (t *Templator) execTemplate(name string, tmplContent string, data any, replacedLines int) (string, error) { tmp, err := template.New(name).Funcs(t.TmplFuncs).Parse(tmplContent) if err != nil { @@ -260,38 +271,72 @@ func (t *Templator) execTemplate(name string, tmplContent string, data any, repl return buf.String(), nil } -func recurseCanary() []string { - return []string{ - "5f9c88133f5cc60a84009b841192a2d8f83dd901e8112fe77284e461b4039ccd", - "f34e13e176147bebc0e4d644fd45d5727462c3b574b3ffc00df1b4683c15e54c", - "6f6fd98465b1ef727f0692a3d08f70cb4b94ab987a97d0ee206d3d33d2032887", - "f1cd9fe131302d6f37b17084fa8b7679debb6bff79853171e439483bbc2cb846", - "2290a2c385c9f40a2af9f56095630ab8b80e067cdf8fc92beef93c2b16c2b8aa", +// Recurse will let the engine know how many times the template should execute. +func (t *Templator) Recurse(_ VM, numTimes int) (out string, err error) { + if numTimes < 1 { + return "", fmt.Errorf("recurse(%d) invalid: must recurse at least once", numTimes) } + + return fmt.Sprintf(canaryPlaceholder, strconv.Itoa(numTimes-1)), nil } -// Recurse will let the engine know how many times the template should execute. -func (t *Templator) Recurse(_ VM, numTimes int) (out string, err error) { - if numTimes < 1 || numTimes > len(recurseCanary()) { - return "", fmt.Errorf("recurse(%v) invalid: %v outside bounds 1..%v", numTimes, numTimes, len(recurseCanary())) +var ( + canaryPlaceholder = "~-~SPEAKEASY_RECURSE_CANARY_%s~-~" + canaryRegex = regexp.MustCompile(fmt.Sprintf(canaryPlaceholder, `(\d+)`)) + recurseRegex = regexp.MustCompile(`^{{-* *recurse (\d+) *-*}}$`) +) + +func (t *Templator) isRecursiveTemplate(input string) (int, error) { + lines := strings.Split(strings.ReplaceAll(input, "\r\n", "\n"), "\n") + + matches := recurseRegex.FindAllStringSubmatch(lines[0], -1) + if len(matches) != 1 { + return -1, nil } - return recurseCanary()[numTimes-1], nil + num, err := strconv.Atoi(matches[0][1]) + if err != nil { + return 0, err + } + + return num, nil } -func (t *Templator) applyRecurseCanary(input string, execCount int) (string, int) { - canaryList := recurseCanary() - const recurseArgumentToExecCount = 2 - for i, canary := range canaryList { - if strings.Contains(input, canary) { - // recurse 1 means canary[0] is found, and execCount is now 2 - // if more than 1 "recurse" invocation in template, use the largest - execCount = int(math.Max(float64(execCount), float64(i+recurseArgumentToExecCount))) - } - input = strings.ReplaceAll(input, canary, "") +func (t *Templator) getCanaryNum(input string) (int, error) { + matches := canaryRegex.FindAllStringSubmatch(input, -1) + if len(matches) == 0 { + return -1, nil + } + + if len(matches) > 1 { + return 0, fmt.Errorf("recurse canary found more than once in template. recurse should only be declared once per template") + } + + num, err := strconv.Atoi(matches[0][1]) + if err != nil { + return 0, err + } + + return num, nil +} + +func (t *Templator) applyRecurseCanary(input string) (string, bool, error) { + num, err := t.getCanaryNum(input) + if err != nil { + return "", false, err + } + + if num == -1 { + return input, false, nil + } + + replacementString := "" + + if num > 0 { + replacementString = fmt.Sprintf(canaryPlaceholder, strconv.Itoa(num-1)) } - return input, execCount + return strings.Replace(input, fmt.Sprintf(canaryPlaceholder, strconv.Itoa(num)), replacementString, 1), true, nil } func adjustLineNumber(name string, err error, replacedLines int) error { diff --git a/internal/template/template_test.go b/internal/template/template_test.go index b30dbd6..ed211e2 100644 --- a/internal/template/template_test.go +++ b/internal/template/template_test.go @@ -8,6 +8,7 @@ import ( "github.com/speakeasy-api/easytemplate/internal/template" "github.com/speakeasy-api/easytemplate/internal/template/mocks" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTemplator_TemplateFile_Success(t *testing.T) { @@ -47,19 +48,20 @@ func TestTemplator_TemplateFile_Success(t *testing.T) { vm := mocks.NewMockVM(ctrl) context := &template.Context{ - Global: tt.fields.contextData, - GlobalComputed: goja.Undefined(), - Local: tt.args.inputData, - LocalComputed: goja.Undefined(), + Global: tt.fields.contextData, + GlobalComputed: goja.Undefined(), + Local: tt.args.inputData, + LocalComputed: goja.Undefined(), + RecursiveComputed: goja.Undefined(), } o := goja.New() contextVal := o.ToValue(context) vm.EXPECT().Run("localCreateComputedContextObject", `createComputedContextObject();`).Return(goja.Undefined(), nil).Times(1) - vm.EXPECT().Get("context").Return(goja.Undefined()).Times(1) + vm.EXPECT().Get("context").Return(goja.Undefined()).Times(2) vm.EXPECT().Set("context", context).Return(nil).Times(1) - vm.EXPECT().Get("context").Return(contextVal).Times(2) - vm.EXPECT().ToObject(contextVal).Return(contextVal.ToObject(o)).Times(2) + vm.EXPECT().Get("context").Return(contextVal).Times(1) + vm.EXPECT().ToObject(contextVal).Return(contextVal.ToObject(o)).Times(1) vm.EXPECT().Set("context", goja.Undefined()).Return(nil).Times(1) tr := &template.Templator{ @@ -75,7 +77,7 @@ func TestTemplator_TemplateFile_Success(t *testing.T) { } tr.SetContextData(tt.fields.contextData, goja.Undefined()) err := tr.TemplateFile(vm, tt.args.templatePath, tt.args.outFile, tt.args.inputData) - assert.NoError(t, err) + require.NoError(t, err) }) } } @@ -146,16 +148,17 @@ func TestTemplator_TemplateString_Success(t *testing.T) { vm := mocks.NewMockVM(ctrl) context := &template.Context{ - Global: tt.fields.contextData, - GlobalComputed: goja.Undefined(), - Local: tt.args.inputData, - LocalComputed: goja.Undefined(), + Global: tt.fields.contextData, + GlobalComputed: goja.Undefined(), + Local: tt.args.inputData, + LocalComputed: goja.Undefined(), + RecursiveComputed: goja.Undefined(), } o := goja.New() contextVal := o.ToValue(context) vm.EXPECT().Run("localCreateComputedContextObject", `createComputedContextObject();`).Return(goja.Undefined(), nil).Times(1) - vm.EXPECT().Get("context").Return(goja.Undefined()).Times(1) + vm.EXPECT().Get("context").Return(goja.Undefined()).Times(2) vm.EXPECT().Set("context", context).Return(nil).Times(1) if tt.fields.includedJS != "" { @@ -165,8 +168,8 @@ func TestTemplator_TemplateString_Success(t *testing.T) { vm.EXPECT().Set("render", goja.Undefined()).Return(nil).Times(1) } - vm.EXPECT().Get("context").Return(contextVal).Times(2) - vm.EXPECT().ToObject(contextVal).Return(contextVal.ToObject(o)).Times(2) + vm.EXPECT().Get("context").Return(contextVal).Times(1) + vm.EXPECT().ToObject(contextVal).Return(contextVal.ToObject(o)).Times(1) vm.EXPECT().Set("context", goja.Undefined()).Return(nil).Times(1) tr := &template.Templator{ @@ -178,7 +181,7 @@ func TestTemplator_TemplateString_Success(t *testing.T) { } tr.SetContextData(tt.fields.contextData, goja.Undefined()) out, err := tr.TemplateString(vm, tt.args.templatePath, tt.args.inputData) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, tt.wantOut, out) }) } diff --git a/templating.go b/templating.go index fbd35ba..2634320 100644 --- a/templating.go +++ b/templating.go @@ -14,20 +14,6 @@ func (e *Engine) templateFileJS(call CallContext) goja.Value { return goja.Undefined() } -//nolint:gomnd -func (e *Engine) templateFileMultipleJS(call CallContext) goja.Value { - templateFile := call.Argument(0).String() - outFile := call.Argument(1).String() - inputData := call.Argument(2).Export() - numTimes := call.Argument(3).ToInteger() - - if err := e.templator.TemplateFileMultiple(call.VM, templateFile, outFile, inputData, int(numTimes)); err != nil { - panic(call.VM.NewGoError(err)) - } - - return goja.Undefined() -} - func (e *Engine) templateStringJS(call CallContext) goja.Value { inputData := call.Argument(1).Export() @@ -39,24 +25,10 @@ func (e *Engine) templateStringJS(call CallContext) goja.Value { return call.VM.ToValue(output) } -//nolint:gomnd -func (e *Engine) templateStringMultipleJS(call CallContext) goja.Value { - templatePath := call.Argument(0).String() - inputData := call.Argument(1).Export() - numTimes := call.Argument(2).ToInteger() - - output, err := e.templator.TemplateStringMultiple(call.VM, templatePath, inputData, int(numTimes)) - if err != nil { - panic(call.VM.NewGoError(err)) - } - - return call.VM.ToValue(output) -} - func (e *Engine) templateStringInputJS(call CallContext) goja.Value { inputData := call.Argument(2).Export() //nolint:gomnd - output, err := e.templator.TemplateStringInput(call.VM, call.Argument(0).String(), call.Argument(1).String(), inputData, 1) + output, err := e.templator.TemplateStringInput(call.VM, call.Argument(0).String(), call.Argument(1).String(), inputData) if err != nil { panic(call.VM.NewGoError(err)) } @@ -72,18 +44,3 @@ func (e *Engine) recurseJS(call CallContext) goja.Value { return call.VM.ToValue(output) } - -//nolint:gomnd -func (e *Engine) templateStringInputMultipleJS(call CallContext) goja.Value { - name := call.Argument(0).String() - input := call.Argument(1).String() - inputData := call.Argument(2).Export() - numTimes := call.Argument(3).ToInteger() - - output, err := e.templator.TemplateStringInput(call.VM, name, input, inputData, int(numTimes)) - if err != nil { - panic(call.VM.NewGoError(err)) - } - - return call.VM.ToValue(output) -} diff --git a/testdata/expected/test.txt b/testdata/expected/test.txt index 978b1b3..cb12292 100644 --- a/testdata/expected/test.txt +++ b/testdata/expected/test.txt @@ -3,5 +3,7 @@ from test.js 34.000 Hello User -Hello World it's a wonderful day! -Hello World it's a wonderful day! (Isolated) \ No newline at end of file +Hello World it's a wonderful day! The End! +A recursive template with its own scope +I have my own recursive scope! +I'm a sub template that will add to my parents template recursive context diff --git a/testdata/templates/recursiveSubTemplate.stmpl b/testdata/templates/recursiveSubTemplate.stmpl new file mode 100644 index 0000000..a559d31 --- /dev/null +++ b/testdata/templates/recursiveSubTemplate.stmpl @@ -0,0 +1,12 @@ +{{recurse 2 -}} +A recursive template with its own scope +{{"{{\"{{.RecursiveComputed.AccumulatedString}}\"}}"}}{{/* Double escaped so it will unfurl fully by the third iteration to template*/}} +```sjs +if (!context.RecursiveComputed.AccumulatedString) { + context.RecursiveComputed.AccumulatedString = ""; +} +context.RecursiveComputed.AccumulatedString += "I have my "; +sjs``` +{{- "```"}}{{"sjs"}}{{/* Escaped so it will only be executed on the second iteration */}} +context.RecursiveComputed.AccumulatedString += "own recursive scope!"; +{{"sjs"}}{{"```"}} \ No newline at end of file diff --git a/testdata/templates/test.stmpl b/testdata/templates/test.stmpl index 68ec600..b96a356 100644 --- a/testdata/templates/test.stmpl +++ b/testdata/templates/test.stmpl @@ -3,5 +3,4 @@ {{templateFile "templates/test2.stmpl" "test2.txt" "from test.stmpl"}} {{toFloatWithPrecision .Local.Value 3}} {{templateStringInput "hello" "Hello {{.Local}}" "User"}} -{{templateStringMultiple "templates/testMultiple.stmpl" nil 3}} -{{templateString "templates/testMultipleIsolated.stmpl" nil}} \ No newline at end of file +{{templateString "templates/testMultiple.stmpl" nil}} \ No newline at end of file diff --git a/testdata/templates/testMultiple.stmpl b/testdata/templates/testMultiple.stmpl index 48233d6..08cdeef 100644 --- a/testdata/templates/testMultiple.stmpl +++ b/testdata/templates/testMultiple.stmpl @@ -1,10 +1,13 @@ -{{"{{\"{{.LocalComputed.AccumulatedString}}\" -}}"}}{{/* Double escaped so it will unfurl fully by the third iteration to template*/}} +{{- recurse 2 -}} +{{"{{\"{{.RecursiveComputed.AccumulatedString}}\"}}"}}{{/* Double escaped so it will unfurl fully by the third iteration to template*/}} ```sjs -if (!context.LocalComputed.AccumulatedString) { - context.LocalComputed.AccumulatedString = ""; +if (!context.RecursiveComputed.AccumulatedString) { + context.RecursiveComputed.AccumulatedString = ""; } -context.LocalComputed.AccumulatedString += "Hello World"; +context.RecursiveComputed.AccumulatedString += "Hello World"; sjs``` {{- "```"}}{{"sjs"}}{{/* Escaped so it will only be executed on the second iteration */}} -context.LocalComputed.AccumulatedString += " it's a wonderful day!"; -{{"sjs"}}{{"```"}} \ No newline at end of file +context.RecursiveComputed.AccumulatedString += " The End!"; +{{"sjs"}}{{"```"}} +{{templateString "templates/recursiveSubTemplate.stmpl" nil}} +{{templateString "templates/testMultipleSubTemplate.stmpl" nil}} \ No newline at end of file diff --git a/testdata/templates/testMultipleIsolated.stmpl b/testdata/templates/testMultipleIsolated.stmpl deleted file mode 100644 index 8e2a4f8..0000000 --- a/testdata/templates/testMultipleIsolated.stmpl +++ /dev/null @@ -1,11 +0,0 @@ -{{- recurse 2 -}} -{{"{{\"{{.LocalComputed.AccumulatedString}}\"}}" -}}{{/* Double escaped so it will unfurl fully by the third iteration to template*/}} -```sjs -if (!context.LocalComputed.AccumulatedString) { - context.LocalComputed.AccumulatedString = ""; -} -context.LocalComputed.AccumulatedString += "Hello World"; -sjs``` -{{- "```"}}{{"sjs"}}{{/* Escaped so it will only be executed on the second iteration */}} -context.LocalComputed.AccumulatedString += " it's a wonderful day! (Isolated)"; -{{"sjs"}}{{"```"}} \ No newline at end of file diff --git a/testdata/templates/testMultipleSubTemplate.stmpl b/testdata/templates/testMultipleSubTemplate.stmpl new file mode 100644 index 0000000..51ab583 --- /dev/null +++ b/testdata/templates/testMultipleSubTemplate.stmpl @@ -0,0 +1,4 @@ +I'm a sub template that will add to my parents template recursive context +```sjs +context.RecursiveComputed.AccumulatedString += " it's a wonderful day!"; +sjs``` \ No newline at end of file From 1f3051f75b6182e12d6a116fb2cf7a91623d87d0 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 10 Nov 2023 16:46:09 +0000 Subject: [PATCH 12/14] fix --- engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine.go b/engine.go index bf1b7cf..27a8189 100644 --- a/engine.go +++ b/engine.go @@ -226,7 +226,7 @@ func (e *Engine) RunTemplateStringInput(name, template string, data any) (string return e.templator.TemplateStringInput(vm, name, template, data) } -//nolint:funlen,cyclop,gocognit +//nolint:funlen func (e *Engine) init(data any) (*vm.VM, error) { if e.ran { return nil, ErrAlreadyRan From 854639dbb8919f76dad495639937d50134e0da34 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 10 Nov 2023 16:50:47 +0000 Subject: [PATCH 13/14] fix --- internal/template/template.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/template/template.go b/internal/template/template.go index b7b4960..9feba05 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -106,6 +106,8 @@ func (t *Templator) TemplateString(vm VM, templatePath string, inputData any) (o } // TemplateStringInput will template the provided input string and return the output as a string. +// +//nolint:funlen func (t *Templator) TemplateStringInput(vm VM, name string, input string, inputData any) (out string, err error) { defer func() { if e := recover(); e != nil { From b00acb0f852b49f7107540f6fd00725469af7360 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge Date: Fri, 10 Nov 2023 16:56:48 +0000 Subject: [PATCH 14/14] fix --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2f545b1..0bbb811 100644 --- a/README.md +++ b/README.md @@ -168,11 +168,15 @@ For example: ```go {{- recurse 1 -}} -{{"{{.L}}"}} -{{ }} - +{{"{{.RecursiveComputed.Names}}"}}{{/* Render the names of the customers after we have iterated over them later */}} +{{range .Local.Customers}} +{{- addName .RecursiveComputed.Names (print .FirstName " " .LastName) -}} +{{.FirstName}} {{.LastName}} +{{end}} ``` +Note: The `recurse` function must be called as the first thing in the template on its own line. + ### Registering templating functions The engine allows you to register custom templating functions from Go which can be used within the templates.