From 36d106f39d21e692129976bc38f44556068e3756 Mon Sep 17 00:00:00 2001 From: 2ynn Date: Fri, 5 Jun 2026 21:23:34 -0400 Subject: [PATCH] feat: expose currently templated file path --- engine_integration_test.go | 55 ++++++++++++++++++++++++++ internal/template/template.go | 19 ++++++--- testdata/templates/outfile_inner.stmpl | 2 + testdata/templates/outfile_outer.stmpl | 2 + 4 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 testdata/templates/outfile_inner.stmpl create mode 100644 testdata/templates/outfile_outer.stmpl diff --git a/engine_integration_test.go b/engine_integration_test.go index 00ed919..a0cc625 100644 --- a/engine_integration_test.go +++ b/engine_integration_test.go @@ -162,3 +162,58 @@ func TestEngine_GoRuntimePanicIncludesStackTrace(t *testing.T) { // Should still be unwrappable to ErrNativePanic. assert.ErrorIs(t, panicErr, easytemplate.ErrNativePanic) } + +func TestEngine_ContextOutFile(t *testing.T) { + // Verifies that context.OutFile is populated during a templateFile render, + // restored across nested templateFile calls, and cleared after the + // outermost render returns. JS helpers can use it to derive paths + // relative to the file currently being templated. + var ( + captured []string + eng *easytemplate.Engine + ) + + var writes []string + eng = easytemplate.New( + easytemplate.WithSearchLocations([]string{"./testdata"}), + easytemplate.WithWriteFunc(func(outFile string, data []byte) error { + writes = append(writes, string(data)) + return nil + }), + easytemplate.WithTemplateFuncs(map[string]any{ + "captureOutFile": func() string { + ctx := eng.Runtime().Get("context").ToObject(eng.Runtime()) + captured = append(captured, ctx.Get("OutFile").String()) + return "" + }, + }), + ) + e := eng + + require.NoError(t, e.Init(context.Background(), nil)) + + // Before any templateFile call, OutFile should be empty. + rootCtx := e.Runtime().Get("context").ToObject(e.Runtime()) + assert.Empty(t, rootCtx.Get("OutFile").String()) + + // templateFile sets OutFile for the duration of the render. + require.NoError(t, e.TemplateFile(context.Background(), + "templates/outfile_outer.stmpl", "out/outer.txt", nil)) + + // After return: restored to empty. + assert.Empty(t, rootCtx.Get("OutFile").String()) + + // During the outer render, OutFile == "out/outer.txt". The outer template + // nests a templateFile call that should overwrite to "out/inner.txt" + // then restore "out/outer.txt". + assert.Equal(t, []string{ + "out/outer.txt", // top of outer template + "out/inner.txt", // top of inner template (nested) + "out/outer.txt", // resumed outer template after nested call + }, captured) + + // {{.OutFile}} must also resolve inside text/template renders. + require.Len(t, writes, 2) + assert.Contains(t, writes[1], "tmpl=out/outer.txt") // outer.txt written last + assert.Contains(t, writes[0], "tmpl=out/inner.txt") // inner.txt written first +} diff --git a/internal/template/template.go b/internal/template/template.go index 3fad61c..8ff44bc 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -34,6 +34,7 @@ type Context struct { GlobalComputed goja.Value LocalComputed goja.Value RecursiveComputed goja.Value + OutFile string } type tmplContext struct { @@ -42,6 +43,7 @@ type tmplContext struct { GlobalComputed any LocalComputed any RecursiveComputed any + OutFile string } // VM represents a virtual machine that can be used to run js. @@ -61,6 +63,7 @@ type Templator struct { contextData any globalComputed goja.Value baseTemplate *template.Template + outFile string } // RebuildBaseTemplate creates a new base template from the current TmplFuncs. @@ -78,6 +81,10 @@ func (t *Templator) SetContextData(contextData any, globalComputed goja.Value) { // TemplateFile will template a file and write the output to outFile. func (t *Templator) TemplateFile(ctx context.Context, vm VM, templateFile, outFile string, inputData any) error { + lastOutFile := t.outFile + t.outFile = outFile + defer func() { t.outFile = lastOutFile }() + output, err := t.TemplateString(ctx, vm, templateFile, inputData) if err != nil { return err @@ -132,6 +139,11 @@ func (t *Templator) TemplateStringInput(ctx context.Context, vm VM, name string, } currentContext := vm.Get("context") + defer func() { + if resetErr := vm.Set("context", currentContext); resetErr != nil && err == nil { + err = fmt.Errorf("failed to reset context: %w", resetErr) + } + }() currentRecursiveComputed := getRecursiveComputedContext(vm) localRecursiveComputed := currentRecursiveComputed @@ -156,6 +168,7 @@ func (t *Templator) TemplateStringInput(ctx context.Context, vm VM, name string, Local: inputData, LocalComputed: localComputed, RecursiveComputed: localRecursiveComputed, + OutFile: t.outFile, } if err := vm.Set("context", context); err != nil { @@ -176,6 +189,7 @@ func (t *Templator) TemplateStringInput(ctx context.Context, vm VM, name string, GlobalComputed: context.GlobalComputed.Export(), LocalComputed: localComputed.Export(), RecursiveComputed: localRecursiveComputed.Export(), + OutFile: context.OutFile, } out, err = t.execTemplate(name, evaluated, tmplCtx, replacedLines) @@ -196,11 +210,6 @@ func (t *Templator) TemplateStringInput(ctx context.Context, vm VM, name string, localRecursiveComputed = getRecursiveComputedContext(vm) } - // Reset the context back to the previous one - if err := vm.Set("context", currentContext); err != nil { - return "", fmt.Errorf("failed to reset context: %w", err) - } - return out, nil } diff --git a/testdata/templates/outfile_inner.stmpl b/testdata/templates/outfile_inner.stmpl new file mode 100644 index 0000000..d7b86b6 --- /dev/null +++ b/testdata/templates/outfile_inner.stmpl @@ -0,0 +1,2 @@ +tmpl={{.OutFile}} +{{captureOutFile}} diff --git a/testdata/templates/outfile_outer.stmpl b/testdata/templates/outfile_outer.stmpl new file mode 100644 index 0000000..3d12e57 --- /dev/null +++ b/testdata/templates/outfile_outer.stmpl @@ -0,0 +1,2 @@ +tmpl={{.OutFile}} +{{captureOutFile}}{{templateFile "templates/outfile_inner.stmpl" "out/inner.txt" nil}}{{captureOutFile}}