From 5fda9610f907482497a7b32abb2ab2f85a4e6906 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Fri, 17 Sep 2021 14:00:36 -0500 Subject: [PATCH] templates: Add 'import' action (#4321) Related to (closed) Issue #2094 on template inheritance. This PR adds a new function called "import" which works like "include", except it only takes one argument and passes it to the referenced file to be used as "." in that file. * Update tplcontext.go Add {{ render "/path/to/file.ext" $data }} via funcRender * Update tplcontext.go * Refactor funcInclude, add funcImport to enable {{block}} and {{template}} * Fix funcImport return of nil showing up in html * Update godocs for and --- modules/caddyhttp/templates/templates.go | 18 +++++- modules/caddyhttp/templates/tplcontext.go | 75 ++++++++++++++++------- 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 6fed8c8974f..8f2ea2c1eab 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -91,9 +91,25 @@ func init() { // {{httpInclude "/foo/bar?q=val"}} // ``` // +// ##### `import` +// +// Imports the contents of another file and adds any template definitions to the template stack. If there are no defitions, the filepath will be the defition name. Any {{ define }} blocks will be accessible by {{ template }} or {{ block }}. Imports must happen before the template or block action is called +// +// **filename.html** +// ``` +// {{ define "main" }} +// content +// {{ end }} +// +// **index.html** +// ``` +// {{ import "/path/to/file.html" }} +// {{ template "main" }} +// ``` +// // ##### `include` // -// Includes the contents of another file. Optionally can pass key-value pairs as arguments to be accessed by the included file. +// Includes the contents of another file and renders in-place. Optionally can pass key-value pairs as arguments to be accessed by the included file. // // ``` // {{include "path/to/file.html"}} // no arguments diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index 14ac31b7f28..71886c9a0c5 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -46,24 +46,26 @@ type TemplateContext struct { RespHeader WrappedHeader config *Templates + tpl *template.Template } // 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 { - tpl := template.New(tplName) +func (c *TemplateContext) NewTemplate(tplName string) *template.Template { + c.tpl = template.New(tplName) // customize delimiters, if applicable if c.config != nil && len(c.config.Delimiters) == 2 { - tpl.Delims(c.config.Delimiters[0], c.config.Delimiters[1]) + c.tpl.Delims(c.config.Delimiters[0], c.config.Delimiters[1]) } // add sprig library - tpl.Funcs(sprigFuncMap) + c.tpl.Funcs(sprigFuncMap) // add our own library - tpl.Funcs(template.FuncMap{ + c.tpl.Funcs(template.FuncMap{ "include": c.funcInclude, + "import": c.funcImport, "httpInclude": c.funcHTTPInclude, "stripHTML": c.funcStripHTML, "markdown": c.funcMarkdown, @@ -74,8 +76,7 @@ func (c TemplateContext) NewTemplate(tplName string) *template.Template { "fileExists": c.funcFileExists, "httpError": c.funcHTTPError, }) - - return tpl + return c.tpl } // OriginalReq returns the original, unmodified, un-rewritten request as @@ -85,18 +86,36 @@ func (c TemplateContext) OriginalReq() http.Request { return or } -// funcInclude returns the contents of filename relative to the site root. +// funcInclude returns the contents of filename relative to the site root and renders it in place. // Note that included files are NOT escaped, so you should only include // trusted files. If it is not trusted, be sure to use escaping functions // in your template. func (c TemplateContext) funcInclude(filename string, args ...interface{}) (string, error) { + bodyBuf, err := c.readFileToBuffer(filename) + + if err != nil { + return "", err + } + + c.Args = args + + err = c.executeTemplateInBuffer(filename, bodyBuf) + if err != nil { + return "", err + } + + return bodyBuf.String(), nil +} + +// readFileToBuffer returns the contents of filename relative to root as a buffer +func (c TemplateContext) readFileToBuffer(filename string) (*bytes.Buffer, error) { if c.Root == nil { - return "", fmt.Errorf("root file system not specified") + return nil, fmt.Errorf("root file system not specified") } file, err := c.Root.Open(filename) if err != nil { - return "", err + return nil, err } defer file.Close() @@ -106,17 +125,10 @@ func (c TemplateContext) funcInclude(filename string, args ...interface{}) (stri _, err = io.Copy(bodyBuf, file) if err != nil { - return "", err - } - - c.Args = args - - err = c.executeTemplateInBuffer(filename, bodyBuf) - if err != nil { - return "", err + return nil, err } - return bodyBuf.String(), nil + return bodyBuf, nil } // funcHTTPInclude returns the body of a virtual (lightweight) request @@ -167,17 +179,34 @@ func (c TemplateContext) funcHTTPInclude(uri string) (string, error) { return buf.String(), nil } -func (c TemplateContext) executeTemplateInBuffer(tplName string, buf *bytes.Buffer) error { - tpl := c.NewTemplate(tplName) +// funcImport parses the filename into the current template stack. The imported +// file will be rendered within the current template by calling {{ block }} or +// {{ template }} from the standard template library. If the imported file has +// no {{ define }} blocks, the name of the import will be the path +func (c *TemplateContext) funcImport(filename string) (string, error) { + bodyBuf, err := c.readFileToBuffer(filename) + if err != nil { + return "", err + } + + _, err = c.tpl.Parse(bodyBuf.String()) + if err != nil { + return "", err + } + return "", nil +} + +func (c *TemplateContext) executeTemplateInBuffer(tplName string, buf *bytes.Buffer) error { + c.NewTemplate(tplName) - parsedTpl, err := tpl.Parse(buf.String()) + _, err := c.tpl.Parse(buf.String()) if err != nil { return err } buf.Reset() // reuse buffer for output - return parsedTpl.Execute(buf, c) + return c.tpl.Execute(buf, c) } func (c TemplateContext) funcPlaceholder(name string) string {