Repository: caddyserver/caddy (language: go)

Issue:
Make `templates` pluggable
Context: https://caddy.community/t/including-files-scripts/15050/4

Right now, the `templates` module is inflexible in that only functions the functions registered by Caddy itself are available.

A good refactor would be to make it possible to pass an optional array of modules in the `http.handlers.templates.functions.*` namespace which get invoked at `Provision()` time to register additional functions.


Discussion / hints:
I am interested in working on this.

The `TemplateContext` is responsible for parsing and executing templates. It knows which functions are valid. It's a part of the `http.handlers.templates` module. That module then must get custom functions at `Provision` time for hand-off to that context.

Modules would be defined under `http.handlers.templates.functions` (e.g. `http.handlers.templates.functions.bash`) which allows for a definition of how to execute the function (e.g. call exec.Command with bash and the script filepath). Adding new function types can be done by adding a module under `http.handlers.templates.functions.*`, which can be added to a server as a Caddy plugin.

Is my understanding of the problem correct?
Yep, that's right!

I think you can use `caddy.GetModules("http.handlers.templates.functions")` to fetch a list of all the registered modules in that namespace, and if they conform to the right interface, you can load them in. 

https://github.com/caddyserver/caddy/blob/af7321511c960120d61cfd22e031f43071d5a9c2/modules.go#L202

That way, plugging in new functions would be zero-config, they would just added in if they're registered.

Fix patch:
diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go
index 8f2ae9dcfc3..5aff27f5b68 100644
--- a/modules/caddyhttp/templates/templates.go
+++ b/modules/caddyhttp/templates/templates.go
@@ -21,6 +21,7 @@ import (
 	"net/http"
 	"strconv"
 	"strings"
+	"text/template"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -36,6 +37,8 @@ func init() {
 //
 // ⚠️ Template functions/actions are still experimental, so they are subject to change.
 //
+// Custom template functions can be registered by creating a plugin module under the `http.handlers.templates.functions.*` namespace that implements the `CustomFunctions` interface.
+//
 // [All Sprig functions](https://masterminds.github.io/sprig/) are supported.
 //
 // In addition to the standard functions and the Sprig library, Caddy adds
@@ -249,6 +252,14 @@ type Templates struct {
 	// The template action delimiters. If set, must be precisely two elements:
 	// the opening and closing delimiters. Default: `["{{", "}}"]`
 	Delimiters []string `json:"delimiters,omitempty"`
+
+	customFuncs []template.FuncMap
+}
+
+// Customfunctions is the interface for registering custom template functions.
+type CustomFunctions interface {
+	// CustomTemplateFunctions should return the mapping from custom function names to implementations.
+	CustomTemplateFunctions() template.FuncMap
 }
 
 // CaddyModule returns the Caddy module information.
@@ -261,6 +272,18 @@ func (Templates) CaddyModule() caddy.ModuleInfo {
 
 // Provision provisions t.
 func (t *Templates) Provision(ctx caddy.Context) error {
+	fnModInfos := caddy.GetModules("http.handlers.templates.functions")
+	customFuncs := make([]template.FuncMap, len(fnModInfos), 0)
+	for _, modInfo := range fnModInfos {
+		mod := modInfo.New()
+		fnMod, ok := mod.(CustomFunctions)
+		if !ok {
+			return fmt.Errorf("module %q does not satisfy the CustomFunctions interface", modInfo.ID)
+		}
+		customFuncs = append(customFuncs, fnMod.CustomTemplateFunctions())
+	}
+	t.customFuncs = customFuncs
+
 	if t.MIMETypes == nil {
 		t.MIMETypes = defaultMIMETypes
 	}
@@ -331,10 +354,11 @@ func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Reque
 	}
 
 	ctx := &TemplateContext{
-		Root:       fs,
-		Req:        r,
-		RespHeader: WrappedHeader{rr.Header()},
-		config:     t,
+		Root:        fs,
+		Req:         r,
+		RespHeader:  WrappedHeader{rr.Header()},
+		config:      t,
+		CustomFuncs: t.customFuncs,
 	}
 
 	err := ctx.executeTemplateInBuffer(r.URL.Path, rr.Buffer())
diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go
index 4f3cbf50a80..7843455a5a3 100644
--- a/modules/caddyhttp/templates/tplcontext.go
+++ b/modules/caddyhttp/templates/tplcontext.go
@@ -40,10 +40,11 @@ import (
 
 // TemplateContext is the TemplateContext with which HTTP templates are executed.
 type TemplateContext struct {
-	Root       http.FileSystem
-	Req        *http.Request
-	Args       []interface{} // defined by arguments to funcInclude
-	RespHeader WrappedHeader
+	Root        http.FileSystem
+	Req         *http.Request
+	Args        []interface{} // defined by arguments to funcInclude
+	RespHeader  WrappedHeader
+	CustomFuncs []template.FuncMap // functions added by plugins
 
 	config *Templates
 	tpl    *template.Template
@@ -62,6 +63,11 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
 	// add sprig library
 	c.tpl.Funcs(sprigFuncMap)
 
+	// add all custom functions
+	for _, funcMap := range c.CustomFuncs {
+		c.tpl.Funcs(funcMap)
+	}
+
 	// add our own library
 	c.tpl.Funcs(template.FuncMap{
 		"include":          c.funcInclude,


Test patch: