mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-14 06:46:27 +03:00
templates: Offically make templates extensible (#5939)
* templates: Offically make templates extensible This supercedes #4757 (and #4568) by making template extensions configurable. The previous implementation was never documented AFAIK and had only 1 consumer, which I'll notify as a courtesy. * templates: Add 'maybe' function for optional components * Try to fix lint error
This commit is contained in:
parent
e89c9a45b9
commit
2f7ceb5774
3 changed files with 89 additions and 11 deletions
|
@ -15,6 +15,9 @@
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
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/caddyconfig/httpcaddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
@ -49,6 +52,29 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||||
if !h.Args(&t.FileRoot) {
|
if !h.Args(&t.FileRoot) {
|
||||||
return nil, h.ArgErr()
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
@ -319,7 +321,12 @@ type Templates struct {
|
||||||
// the opening and closing delimiters. Default: `["{{", "}}"]`
|
// the opening and closing delimiters. Default: `["{{", "}}"]`
|
||||||
Delimiters []string `json:"delimiters,omitempty"`
|
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
|
customFuncs []template.FuncMap
|
||||||
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Customfunctions is the interface for registering custom template functions.
|
// Customfunctions is the interface for registering custom template functions.
|
||||||
|
@ -338,17 +345,14 @@ func (Templates) CaddyModule() caddy.ModuleInfo {
|
||||||
|
|
||||||
// Provision provisions t.
|
// Provision provisions t.
|
||||||
func (t *Templates) Provision(ctx caddy.Context) error {
|
func (t *Templates) Provision(ctx caddy.Context) error {
|
||||||
fnModInfos := caddy.GetModules("http.handlers.templates.functions")
|
t.logger = ctx.Logger()
|
||||||
customFuncs := make([]template.FuncMap, 0, len(fnModInfos))
|
mods, err := ctx.LoadModule(t, "ExtensionsRaw")
|
||||||
for _, modInfo := range fnModInfos {
|
if err != nil {
|
||||||
mod := modInfo.New()
|
return fmt.Errorf("loading template extensions: %v", err)
|
||||||
fnMod, ok := mod.(CustomFunctions)
|
}
|
||||||
if !ok {
|
for _, modIface := range mods.(map[string]any) {
|
||||||
return fmt.Errorf("module %q does not satisfy the CustomFunctions interface", modInfo.ID)
|
t.customFuncs = append(t.customFuncs, modIface.(CustomFunctions).CustomTemplateFunctions())
|
||||||
}
|
|
||||||
customFuncs = append(customFuncs, fnMod.CustomTemplateFunctions())
|
|
||||||
}
|
}
|
||||||
t.customFuncs = customFuncs
|
|
||||||
|
|
||||||
if t.MIMETypes == nil {
|
if t.MIMETypes == nil {
|
||||||
t.MIMETypes = defaultMIMETypes
|
t.MIMETypes = defaultMIMETypes
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -37,6 +38,7 @@ import (
|
||||||
"github.com/yuin/goldmark/extension"
|
"github.com/yuin/goldmark/extension"
|
||||||
"github.com/yuin/goldmark/parser"
|
"github.com/yuin/goldmark/parser"
|
||||||
gmhtml "github.com/yuin/goldmark/renderer/html"
|
gmhtml "github.com/yuin/goldmark/renderer/html"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
@ -57,7 +59,7 @@ type TemplateContext struct {
|
||||||
// NewTemplate returns a new template intended to be evaluated with this
|
// NewTemplate returns a new template intended to be evaluated with this
|
||||||
// context, as it is initialized with configuration from this context.
|
// context, as it is initialized with configuration from this context.
|
||||||
func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
|
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
|
// customize delimiters, if applicable
|
||||||
if c.config != nil && len(c.config.Delimiters) == 2 {
|
if c.config != nil && len(c.config.Delimiters) == 2 {
|
||||||
|
@ -88,6 +90,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
|
||||||
"fileExists": c.funcFileExists,
|
"fileExists": c.funcFileExists,
|
||||||
"httpError": c.funcHTTPError,
|
"httpError": c.funcHTTPError,
|
||||||
"humanize": c.funcHumanize,
|
"humanize": c.funcHumanize,
|
||||||
|
"maybe": c.funcMaybe,
|
||||||
})
|
})
|
||||||
return c.tpl
|
return c.tpl
|
||||||
}
|
}
|
||||||
|
@ -492,6 +495,51 @@ func (c TemplateContext) funcHumanize(formatType, data string) (string, error) {
|
||||||
return "", fmt.Errorf("no know function was given")
|
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
|
// WrappedHeader wraps niladic functions so that they
|
||||||
// can be used in templates. (Template functions must
|
// can be used in templates. (Template functions must
|
||||||
// return a value.)
|
// return a value.)
|
||||||
|
|
Loading…
Reference in a new issue