Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions engine_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
19 changes: 14 additions & 5 deletions internal/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Context struct {
GlobalComputed goja.Value
LocalComputed goja.Value
RecursiveComputed goja.Value
OutFile string
}

type tmplContext struct {
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions testdata/templates/outfile_inner.stmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tmpl={{.OutFile}}
{{captureOutFile}}
2 changes: 2 additions & 0 deletions testdata/templates/outfile_outer.stmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tmpl={{.OutFile}}
{{captureOutFile}}{{templateFile "templates/outfile_inner.stmpl" "out/inner.txt" nil}}{{captureOutFile}}
Loading