go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/templates/bundle.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package templates 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "html/template" 22 "io" 23 "net/http" 24 "sync" 25 26 "github.com/julienschmidt/httprouter" 27 ) 28 29 // Loader knows how to load template sets. 30 type Loader func(context.Context, template.FuncMap) (map[string]*template.Template, error) 31 32 // Args contains data passed to the template. 33 type Args map[string]any 34 35 // MergeArgs combines multiple Args instances into one. Returns nil if all 36 // passed args are empty. 37 func MergeArgs(args ...Args) Args { 38 total := 0 39 for _, a := range args { 40 total += len(a) 41 } 42 if total == 0 { 43 return nil 44 } 45 res := make(Args, total) 46 for _, a := range args { 47 for k, v := range a { 48 res[k] = v 49 } 50 } 51 return res 52 } 53 54 // Bundle is a bunch of templates lazily loaded at the same time. They may share 55 // associated templates. Bundle is injected into the context. 56 type Bundle struct { 57 // Loader will be called once to attempt to load templates on the first use. 58 // 59 // There are some predefined loaders you can use, see AssetsLoader(...) 60 // for example. 61 Loader Loader 62 63 // DebugMode, if not nil, can return true to enable template reloading before 64 // each use. 65 // 66 // It disables the caching of compiled templates, essentially. Useful during 67 // development, where it can be set to luci/gae's info service 68 // "IsDevAppServer" method directly. 69 DebugMode func(context.Context) bool 70 71 // FuncMap contains functions accessible from templates. 72 // 73 // Will be passed to Loader on first use. Not used after that. 74 FuncMap template.FuncMap 75 76 // DefaultTemplate is a name of subtemplate to pass to ExecuteTemplate when 77 // rendering a template via Render(...) or MustRender(...). 78 // 79 // For example, if all templates in a bundle are built around some base 80 // template (that defined structure of the page), DefaultTemplate can be set 81 // to the name of that base template. 82 // 83 // If DefaultTemplate is empty, Render(...) will use Execute(...) instead of 84 // ExecuteTemplate(...). 85 DefaultTemplate string 86 87 // DefaultArgs generates default arguments to use when rendering templates. 88 // 89 // Additional arguments passed to Render will be merged on top of the 90 // default ones. DefaultArgs is called each time Render is called. 91 // 92 // Extra will be whatever is passed to Render(...) or MustRender(...). Usually 93 // (when installing the bundle into the context via WithTemplates(...) 94 // middleware) Extra contains information about the request being processed. 95 DefaultArgs func(c context.Context, e *Extra) (Args, error) 96 97 once sync.Once 98 templates map[string]*template.Template // result of call to Loader(...) 99 err error // error from Loader, if any 100 } 101 102 // Extra is passed to DefaultArgs, it contains additional information about the 103 // request being processed (usually populated by the middleware). 104 // 105 // Must be treated as read only. 106 type Extra struct { 107 Request *http.Request 108 Params httprouter.Params 109 } 110 111 // EnsureLoaded loads all the templates if they haven't been loaded yet. 112 func (b *Bundle) EnsureLoaded(c context.Context) error { 113 // Always reload in debug mode. Load only once in non-debug mode. 114 if dm := b.DebugMode; dm != nil && dm(c) { 115 b.templates, b.err = b.Loader(c, b.FuncMap) 116 } else { 117 b.once.Do(func() { 118 b.templates, b.err = b.Loader(c, b.FuncMap) 119 }) 120 } 121 return b.err 122 } 123 124 // Get returns the loaded template given its name or error if not found. 125 // 126 // The bundle must be loaded by this point (via call to EnsureLoaded). 127 func (b *Bundle) Get(name string) (*template.Template, error) { 128 if b.err != nil { 129 return nil, b.err 130 } 131 if templ := b.templates[name]; templ != nil { 132 return templ, nil 133 } 134 return nil, fmt.Errorf("template: no such template %q in the bundle", name) 135 } 136 137 // Render finds template with given name and calls its Execute or 138 // ExecuteTemplate method (depending on the value of DefaultTemplate). 139 // 140 // It passes the given context and Extra verbatim to DefaultArgs(...). 141 // If DefaultArgs(...) doesn't access either of them, it is fine to pass nil 142 // instead. 143 // 144 // It always renders output into byte buffer, to avoid partial results in case 145 // of errors. 146 // 147 // The bundle must be loaded by this point (via call to EnsureLoaded). 148 func (b *Bundle) Render(c context.Context, e *Extra, name string, args Args) ([]byte, error) { 149 templ, err := b.Get(name) 150 if err != nil { 151 return nil, err 152 } 153 154 var defArgs Args 155 if b.DefaultArgs != nil { 156 var err error 157 if defArgs, err = b.DefaultArgs(c, e); err != nil { 158 return nil, err 159 } 160 } 161 162 out := bytes.Buffer{} 163 if b.DefaultTemplate == "" { 164 err = templ.Execute(&out, MergeArgs(defArgs, args)) 165 } else { 166 err = templ.ExecuteTemplate(&out, b.DefaultTemplate, MergeArgs(defArgs, args)) 167 } 168 if err != nil { 169 return nil, err 170 } 171 172 return out.Bytes(), nil 173 } 174 175 // MustRender renders the template into the output writer or panics. 176 // 177 // It never writes partial output. It also panics if attempt to write to 178 // the output fails. 179 // 180 // The bundle must be loaded by this point (via call to EnsureLoaded). 181 func (b *Bundle) MustRender(c context.Context, e *Extra, out io.Writer, name string, args Args) { 182 blob, err := b.Render(c, e, name, args) 183 if err != nil { 184 panic(err) 185 } 186 _, err = out.Write(blob) 187 if err != nil { 188 panic(err) 189 } 190 }