@marcel91200 / caddy Mirror · read-only / commits / 18f34290
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 20:45 UTC

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

Matt Holt committed Nov 28, 2023 at 09:39 UTC 18f34290d26d10b6dd62c848b6bd5180d56d7f3a
3 files changed +89 -11
modules/caddyhttp/templates/caddyfile.go
+26
@@ -15,6 +15,9 @@
15 package templates
16
17 import (
18 + "github.com/caddyserver/caddy/v2"
19 + "github.com/caddyserver/caddy/v2/caddyconfig"
20 + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
21 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
22 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
23 )
@@ -49,6 +52,29 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
52 if !h.Args(&t.FileRoot) {
53 return nil, h.ArgErr()
54 }
55 + case "extensions":
56 + if h.NextArg() {
57 + return nil, h.ArgErr()
58 + }
59 + if t.ExtensionsRaw != nil {
60 + return nil, h.Err("extensions already specified")
61 + }
62 + for nesting := h.Nesting(); h.NextBlock(nesting); {
63 + extensionModuleName := h.Val()
64 + modID := "http.handlers.templates.functions." + extensionModuleName
65 + unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
66 + if err != nil {
67 + return nil, err
68 + }
69 + cf, ok := unm.(CustomFunctions)
70 + if !ok {
71 + return nil, h.Errf("module %s (%T) does not provide template functions", modID, unm)
72 + }
73 + if t.ExtensionsRaw == nil {
74 + t.ExtensionsRaw = make(caddy.ModuleMap)
75 + }
76 + t.ExtensionsRaw[extensionModuleName] = caddyconfig.JSON(cf, nil)
77 + }
78 }
79 }
80 }
modules/caddyhttp/templates/templates.go
+14 -10
@@ -23,6 +23,8 @@ import (
23 "strings"
24 "text/template"
25
26 + "go.uber.org/zap"
27 +
28 "github.com/caddyserver/caddy/v2"
29 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
30 )
@@ -319,7 +321,12 @@ type Templates struct {
321 // the opening and closing delimiters. Default: `["{{", "}}"]`
322 Delimiters []string `json:"delimiters,omitempty"`
323
324 + // Extensions adds functions to the template's func map. These often
325 + // act as components on web pages, for example.
326 + ExtensionsRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=http.handlers.templates.functions"`
327 +
328 customFuncs []template.FuncMap
329 + logger *zap.Logger
330 }
331
332 // Customfunctions is the interface for registering custom template functions.
@@ -338,17 +345,14 @@ func (Templates) CaddyModule() caddy.ModuleInfo {
345
346 // Provision provisions t.
347 func (t *Templates) Provision(ctx caddy.Context) error {
341 - fnModInfos := caddy.GetModules("http.handlers.templates.functions")
342 - customFuncs := make([]template.FuncMap, 0, len(fnModInfos))
343 - for _, modInfo := range fnModInfos {
344 - mod := modInfo.New()
345 - fnMod, ok := mod.(CustomFunctions)
346 - if !ok {
347 - return fmt.Errorf("module %q does not satisfy the CustomFunctions interface", modInfo.ID)
348 - }
349 - customFuncs = append(customFuncs, fnMod.CustomTemplateFunctions())
348 + t.logger = ctx.Logger()
349 + mods, err := ctx.LoadModule(t, "ExtensionsRaw")
350 + if err != nil {
351 + return fmt.Errorf("loading template extensions: %v", err)
352 + }
353 + for _, modIface := range mods.(map[string]any) {
354 + t.customFuncs = append(t.customFuncs, modIface.(CustomFunctions).CustomTemplateFunctions())
355 }
351 - t.customFuncs = customFuncs
356
357 if t.MIMETypes == nil {
358 t.MIMETypes = defaultMIMETypes
modules/caddyhttp/templates/tplcontext.go
+49 -1
@@ -23,6 +23,7 @@ import (
23 "net/http"
24 "os"
25 "path"
26 + "reflect"
27 "strconv"
28 "strings"
29 "sync"
@@ -37,6 +38,7 @@ import (
38 "github.com/yuin/goldmark/extension"
39 "github.com/yuin/goldmark/parser"
40 gmhtml "github.com/yuin/goldmark/renderer/html"
41 + "go.uber.org/zap"
42
43 "github.com/caddyserver/caddy/v2"
44 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -57,7 +59,7 @@ type TemplateContext struct {
59 // NewTemplate returns a new template intended to be evaluated with this
60 // context, as it is initialized with configuration from this context.
61 func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
60 - c.tpl = template.New(tplName)
62 + c.tpl = template.New(tplName).Option("missingkey=zero")
63
64 // customize delimiters, if applicable
65 if c.config != nil && len(c.config.Delimiters) == 2 {
@@ -88,6 +90,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
90 "fileExists": c.funcFileExists,
91 "httpError": c.funcHTTPError,
92 "humanize": c.funcHumanize,
93 + "maybe": c.funcMaybe,
94 })
95 return c.tpl
96 }
@@ -492,6 +495,51 @@ func (c TemplateContext) funcHumanize(formatType, data string) (string, error) {
495 return "", fmt.Errorf("no know function was given")
496 }
497
498 +// funcMaybe invokes the plugged-in function named functionName if it is plugged in
499 +// (is a module in the 'http.handlers.templates.functions' namespace). If it is not
500 +// available, a log message is emitted.
501 +//
502 +// The first argument is the function name, and the rest of the arguments are
503 +// passed on to the actual function.
504 +//
505 +// This function is useful for executing templates that use components that may be
506 +// considered as optional in some cases (like during local development) where you do
507 +// not want to require everyone to have a custom Caddy build to be able to execute
508 +// your template.
509 +//
510 +// NOTE: This function is EXPERIMENTAL and subject to change or removal.
511 +func (c TemplateContext) funcMaybe(functionName string, args ...any) (any, error) {
512 + for _, funcMap := range c.CustomFuncs {
513 + if fn, ok := funcMap[functionName]; ok {
514 + val := reflect.ValueOf(fn)
515 + if val.Kind() != reflect.Func {
516 + continue
517 + }
518 + argVals := make([]reflect.Value, len(args))
519 + for i, arg := range args {
520 + argVals[i] = reflect.ValueOf(arg)
521 + }
522 + returnVals := val.Call(argVals)
523 + switch len(returnVals) {
524 + case 0:
525 + return "", nil
526 + case 1:
527 + return returnVals[0].Interface(), nil
528 + case 2:
529 + var err error
530 + if !returnVals[1].IsNil() {
531 + err = returnVals[1].Interface().(error)
532 + }
533 + return returnVals[0].Interface(), err
534 + default:
535 + return nil, fmt.Errorf("maybe %s: invalid number of return values: %d", functionName, len(returnVals))
536 + }
537 + }
538 + }
539 + c.config.logger.Named("maybe").Warn("template function could not be found; ignoring invocation", zap.String("name", functionName))
540 + return "", nil
541 +}
542 +
543 // WrappedHeader wraps niladic functions so that they
544 // can be used in templates. (Template functions must
545 // return a value.)