template.go (8510B)
1 package dati 2 3 /* 4 Copyright (C) 2023 gearsix <gearsix@tuta.io> 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 at your option) any later version. 10 11 This program is distributed in the hope that it will be useful, 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU General Public License for more details. 15 16 You should have received a copy of the GNU General Public License 17 along with this program. If not, see <https://www.gnu.org/licenses/>. 18 */ 19 20 import ( 21 "bytes" 22 "errors" 23 "fmt" 24 htmpl "html/template" 25 "io" 26 "io/ioutil" 27 "os" 28 "path/filepath" 29 "reflect" 30 "strings" 31 tmpl "text/template" 32 33 mst "github.com/cbroglie/mustache" 34 ) 35 36 // TemplateLanguage provides a list of supported languages for 37 // Template files (lower-case) 38 type TemplateLanguage string 39 40 func (t TemplateLanguage) String() string { 41 return string(t) 42 } 43 44 const ( 45 TMPL TemplateLanguage = "tmpl" 46 HTMPL TemplateLanguage = "htmpl" 47 MST TemplateLanguage = "mst" 48 ) 49 50 var ( 51 ErrUnsupportedTemplate = func(format string) error { 52 return fmt.Errorf("template language '%s' is not supported", format) 53 } 54 ErrUnknownTemplateType = func(templateType string) error { 55 return fmt.Errorf("unable to infer template type '%s'", templateType) 56 } 57 ErrRootPathIsDir = func(path string) error { 58 return fmt.Errorf("rootPath path must be a file, not a directory (%s)", path) 59 } 60 ErrNilTemplate = errors.New("template is nil") 61 ) 62 63 // IsTemplateLanguage will return a bool if the file found at `path` 64 // is a known *TemplateLanguage*, based upon it's file extension. 65 func IsTemplateLanguage(path string) bool { 66 return ReadTemplateLangauge(path) != "" 67 } 68 69 // ReadTemplateLanguage returns the *TemplateLanguage* that the file 70 // extension of `path` matches. If the file extension of `path` does 71 // not match any *TemplateLanguage*, then an "" is returned. 72 func ReadTemplateLangauge(path string) TemplateLanguage { 73 if len(path) == 0 { 74 return "" 75 } 76 77 ext := filepath.Ext(path) 78 if len(ext) == 0 { 79 ext = path // assume `path` is the name of the format 80 } 81 82 ext = strings.ToLower(ext) 83 if len(ext) > 0 && ext[0] == '.' { 84 ext = ext[1:] 85 } 86 87 for _, fmt := range []TemplateLanguage{TMPL, HTMPL, MST} { 88 if fmt.String() == ext { 89 return fmt 90 } 91 } 92 return "" 93 } 94 95 // Template is a wrapper to interface with any template parsed by dati. 96 // Ideally it would have just been an interface{} that defines Execute but 97 // the libaries being used aren't that uniform. 98 type Template struct { 99 Name string 100 T interface{} 101 } 102 103 // Execute executes `t` against `d`. Reflection is used to determine 104 // the template type and call it's execution fuction. 105 func (t *Template) Execute(data interface{}) (result bytes.Buffer, err error) { 106 var funcName string 107 var params []reflect.Value 108 tType := reflect.TypeOf(t.T) 109 if tType == nil { 110 err = ErrNilTemplate 111 return 112 } 113 switch tType.String() { 114 case "*template.Template": // golang templates 115 funcName = "Execute" 116 params = []reflect.Value{reflect.ValueOf(&result), reflect.ValueOf(data)} 117 case "*mustache.Template": 118 funcName = "FRender" 119 params = []reflect.Value{reflect.ValueOf(&result), reflect.ValueOf(data)} 120 default: 121 err = ErrUnknownTemplateType(reflect.TypeOf(t.T).String()) 122 } 123 124 if err == nil { 125 rval := reflect.ValueOf(t.T).MethodByName(funcName).Call(params) 126 if !rval[0].IsNil() { // err != nil 127 err = rval[0].Interface().(error) 128 } 129 } 130 131 return 132 } 133 134 // ExecuteToFile writes the result of `(*Template).Execute(data)` to the file at `path` (if no errors occurred). 135 // If `force` is true, any existing file at `path` will be overwritten. 136 func (t *Template) ExecuteToFile(data interface{}, path string, force bool) (f *os.File, err error) { 137 if f, err = os.Open(path); os.IsNotExist(err) { 138 f, err = os.Create(path) 139 } else if !force { 140 err = os.ErrExist 141 } else { // overwrite existing file data 142 if err = f.Truncate(0); err == nil { 143 _, err = f.Seek(0, 0) 144 } 145 } 146 147 if err != nil { 148 return 149 } 150 151 var out bytes.Buffer 152 if out, err = t.Execute(data); err != nil { 153 f = nil 154 } else { 155 _, err = f.Write(out.Bytes()) 156 } 157 158 return 159 } 160 161 // LoadTemplateFilepath loads a Template from file `root`. All files in `partials` 162 // that have the same template type (identified by file extension) are also 163 // parsed and associated with the parsed root template. 164 func LoadTemplateFile(rootPath string, partialPaths ...string) (t Template, err error) { 165 var stat os.FileInfo 166 if stat, err = os.Stat(rootPath); err != nil { 167 return 168 } else if stat.IsDir() { 169 err = ErrRootPathIsDir(rootPath) 170 return 171 } 172 173 lang := ReadTemplateLangauge(rootPath) 174 175 rootName := strings.TrimSuffix(filepath.Base(rootPath), filepath.Ext(rootPath)) 176 177 var root *os.File 178 if root, err = os.Open(rootPath); err != nil { 179 return 180 } 181 defer root.Close() 182 183 partials := make(map[string]io.Reader) 184 for _, path := range partialPaths { 185 name := filepath.Base(path) 186 if lang == "mst" { 187 name = strings.TrimSuffix(name, filepath.Ext(name)) 188 } 189 190 if _, err = os.Stat(path); err != nil { 191 return 192 } 193 194 var partial *os.File 195 if partial, err = os.Open(path); err != nil { 196 return 197 } 198 defer partial.Close() 199 partials[name] = partial 200 } 201 202 return LoadTemplate(lang, rootName, root, partials) 203 } 204 205 // LoadTemplateString will convert `root` and `partials` data to io.StringReader variables and 206 // return a `LoadTemplate` call using them as parameters. 207 // The `partials` map should have the template name to assign the partial template to in the 208 // string key and the template data in as the value. 209 func LoadTemplateString(lang TemplateLanguage, rootName string, root string, partials map[string]string) (t Template, e error) { 210 p := make(map[string]io.Reader) 211 for name, partial := range partials { 212 p[name] = strings.NewReader(partial) 213 } 214 return LoadTemplate(lang, rootName, strings.NewReader(root), p) 215 } 216 217 // LoadTemplate loads a Template from `root` of type `lang`, named `name`. 218 // `lang` must be an element in `SupportedTemplateLangs`. 219 // `name` is optional, if empty the template name will be "template". 220 // `root` should be a string of template, with syntax matching that of `lang`. 221 // `partials` should be a string of template, with syntax matching that of `lang`. 222 func LoadTemplate(lang TemplateLanguage, rootName string, root io.Reader, partials map[string]io.Reader) (t Template, err error) { 223 t.Name = rootName 224 225 switch TemplateLanguage(lang) { 226 case TMPL: 227 t.T, err = loadTemplateTmpl(rootName, root, partials) 228 case HTMPL: 229 t.T, err = loadTemplateHtmpl(rootName, root, partials) 230 case MST: 231 t.T, err = loadTemplateMst(rootName, root, partials) 232 default: 233 err = ErrUnsupportedTemplate(lang.String()) 234 } 235 236 return 237 } 238 239 func loadTemplateTmpl(rootName string, root io.Reader, partials map[string]io.Reader) (*tmpl.Template, error) { 240 var template *tmpl.Template 241 242 if buf, err := ioutil.ReadAll(root); err != nil { 243 return nil, err 244 } else if template, err = tmpl.New(rootName).Parse(string(buf)); err != nil { 245 return nil, err 246 } 247 248 for name, partial := range partials { 249 if buf, err := ioutil.ReadAll(partial); err != nil { 250 return nil, err 251 } else if _, err = template.New(name).Parse(string(buf)); err != nil { 252 return nil, err 253 } 254 } 255 256 return template, nil 257 } 258 259 func loadTemplateHtmpl(rootName string, root io.Reader, partials map[string]io.Reader) (*htmpl.Template, error) { 260 var template *htmpl.Template 261 262 if buf, err := ioutil.ReadAll(root); err != nil { 263 return nil, err 264 } else if template, err = htmpl.New(rootName).Parse(string(buf)); err != nil { 265 return nil, err 266 } 267 268 for name, partial := range partials { 269 if buf, err := ioutil.ReadAll(partial); err != nil { 270 return nil, err 271 } else if _, err = template.New(name).Parse(string(buf)); err != nil { 272 return nil, err 273 } 274 } 275 276 return template, nil 277 } 278 279 func loadTemplateMst(rootName string, root io.Reader, partials map[string]io.Reader) (*mst.Template, error) { 280 var template *mst.Template 281 282 mstprv := new(mst.StaticProvider) 283 mstprv.Partials = make(map[string]string) 284 for name, partial := range partials { 285 if buf, err := ioutil.ReadAll(partial); err != nil { 286 return nil, err 287 } else { 288 mstprv.Partials[name] = string(buf) 289 } 290 } 291 292 if buf, err := ioutil.ReadAll(root); err != nil { 293 return nil, err 294 } else if template, err = mst.ParseStringPartials(string(buf), mstprv); err != nil { 295 return nil, err 296 } 297 298 return template, nil 299 }