Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

templates: Offically make templates extensible #5939

Merged
merged 3 commits into from
Nov 28, 2023
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
26 changes: 26 additions & 0 deletions modules/caddyhttp/templates/caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
package templates

import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
Expand Down Expand Up @@ -49,6 +52,29 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
if !h.Args(&t.FileRoot) {
return nil, h.ArgErr()
}
case "extensions":
if h.NextArg() {
return nil, h.ArgErr()
}
if t.ExtensionsRaw != nil {
return nil, h.Err("extensions already specified")
}
for nesting := h.Nesting(); h.NextBlock(nesting); {
extensionModuleName := h.Val()
modID := "http.handlers.templates.functions." + extensionModuleName
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
if err != nil {
return nil, err
}
cf, ok := unm.(CustomFunctions)
if !ok {
return nil, h.Errf("module %s (%T) does not provide template functions", modID, unm)
}
if t.ExtensionsRaw == nil {
t.ExtensionsRaw = make(caddy.ModuleMap)
}
t.ExtensionsRaw[extensionModuleName] = caddyconfig.JSON(cf, nil)
}
}
}
}
Expand Down
24 changes: 14 additions & 10 deletions modules/caddyhttp/templates/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"strings"
"text/template"

"go.uber.org/zap"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
Expand Down Expand Up @@ -319,7 +321,12 @@ type Templates struct {
// the opening and closing delimiters. Default: `["{{", "}}"]`
Delimiters []string `json:"delimiters,omitempty"`

// Extensions adds functions to the template's func map. These often
// act as components on web pages, for example.
ExtensionsRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=http.handlers.templates.functions"`

customFuncs []template.FuncMap
logger *zap.Logger
}

// Customfunctions is the interface for registering custom template functions.
Expand All @@ -338,17 +345,14 @@ 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, 0, len(fnModInfos))
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.logger = ctx.Logger()
mods, err := ctx.LoadModule(t, "ExtensionsRaw")
if err != nil {
return fmt.Errorf("loading template extensions: %v", err)
}
for _, modIface := range mods.(map[string]any) {
t.customFuncs = append(t.customFuncs, modIface.(CustomFunctions).CustomTemplateFunctions())
}
t.customFuncs = customFuncs

if t.MIMETypes == nil {
t.MIMETypes = defaultMIMETypes
Expand Down
50 changes: 49 additions & 1 deletion modules/caddyhttp/templates/tplcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net/http"
"os"
"path"
"reflect"
"strconv"
"strings"
"sync"
Expand All @@ -37,6 +38,7 @@ import (
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
gmhtml "github.com/yuin/goldmark/renderer/html"
"go.uber.org/zap"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
Expand All @@ -57,7 +59,7 @@ type TemplateContext struct {
// NewTemplate returns a new template intended to be evaluated with this
// context, as it is initialized with configuration from this context.
func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
c.tpl = template.New(tplName)
c.tpl = template.New(tplName).Option("missingkey=zero")

// customize delimiters, if applicable
if c.config != nil && len(c.config.Delimiters) == 2 {
Expand Down Expand Up @@ -88,6 +90,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
"fileExists": c.funcFileExists,
"httpError": c.funcHTTPError,
"humanize": c.funcHumanize,
"maybe": c.funcMaybe,
})
return c.tpl
}
Expand Down Expand Up @@ -492,6 +495,51 @@ func (c TemplateContext) funcHumanize(formatType, data string) (string, error) {
return "", fmt.Errorf("no know function was given")
}

// funcMaybe invokes the plugged-in function named functionName if it is plugged in
// (is a module in the 'http.handlers.templates.functions' namespace). If it is not
// available, a log message is emitted.
//
// The first argument is the function name, and the rest of the arguments are
// passed on to the actual function.
//
// This function is useful for executing templates that use components that may be
// considered as optional in some cases (like during local development) where you do
// not want to require everyone to have a custom Caddy build to be able to execute
// your template.
//
// NOTE: This function is EXPERIMENTAL and subject to change or removal.
func (c TemplateContext) funcMaybe(functionName string, args ...any) (any, error) {
for _, funcMap := range c.CustomFuncs {
if fn, ok := funcMap[functionName]; ok {
val := reflect.ValueOf(fn)
if val.Kind() != reflect.Func {
continue
}
argVals := make([]reflect.Value, len(args))
for i, arg := range args {
argVals[i] = reflect.ValueOf(arg)
}
returnVals := val.Call(argVals)
switch len(returnVals) {
case 0:
return "", nil
case 1:
return returnVals[0].Interface(), nil
case 2:
var err error
if !returnVals[1].IsNil() {
err = returnVals[1].Interface().(error)
}
return returnVals[0].Interface(), err
default:
return nil, fmt.Errorf("maybe %s: invalid number of return values: %d", functionName, len(returnVals))
}
}
}
c.config.logger.Named("maybe").Warn("template function could not be found; ignoring invocation", zap.String("name", functionName))
return "", nil
}

// WrappedHeader wraps niladic functions so that they
// can be used in templates. (Template functions must
// return a value.)
Expand Down
Loading