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.)