template.go (7104B)
1 package dati 2 3 /* 4 Copyright (C) 2021 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 "fmt" 23 mst "github.com/cbroglie/mustache" 24 hmpl "html/template" 25 "io" 26 "io/ioutil" 27 "os" 28 "path/filepath" 29 "reflect" 30 "strings" 31 tmpl "text/template" 32 ) 33 34 // SupportedTemplateLangs provides a list of supported languages for template files (lower-case) 35 var SupportedTemplateLangs = []string{"tmpl", "hmpl", "mst"} 36 37 // IsSupportedTemplateLang provides the index of `SupportedTemplateLangs` that `lang` is at. 38 // If `lang` is not in `SupportedTemplateLangs`, `-1` will be returned. 39 // File extensions can be passed in `lang`, the prefixed `.` will be trimmed. 40 func IsSupportedTemplateLang(lang string) int { 41 lang = strings.ToLower(lang) 42 if len(lang) > 0 && lang[0] == '.' { 43 lang = lang[1:] 44 } 45 for i, l := range SupportedTemplateLangs { 46 if lang == l { 47 return i 48 } 49 } 50 return -1 51 } 52 53 func getTemplateType(path string) string { 54 return strings.TrimPrefix(filepath.Ext(path), ".") 55 } 56 57 // Template is a wrapper to interface with any template parsed by dati. 58 // Ideally it would have just been an interface{} that defines Execute but 59 // the libaries being used aren't that uniform. 60 type Template struct { 61 Name string 62 T interface{} 63 } 64 65 // Execute executes `t` against `d`. Reflection is used to determine 66 // the template type and call it's execution fuction. 67 func (t *Template) Execute(d interface{}) (result bytes.Buffer, err error) { 68 var funcName string 69 var params []reflect.Value 70 tType := reflect.TypeOf(t.T) 71 if tType == nil { 72 err = fmt.Errorf("template.T is nil") 73 return 74 } 75 switch tType.String() { 76 case "*template.Template": // golang templates 77 funcName = "Execute" 78 params = []reflect.Value{reflect.ValueOf(&result), reflect.ValueOf(d)} 79 case "*mustache.Template": 80 funcName = "FRender" 81 params = []reflect.Value{reflect.ValueOf(&result), reflect.ValueOf(d)} 82 default: 83 err = fmt.Errorf("unable to infer template type '%s'", reflect.TypeOf(t.T).String()) 84 } 85 86 if err == nil { 87 rval := reflect.ValueOf(t.T).MethodByName(funcName).Call(params) 88 if !rval[0].IsNil() { // err != nil 89 err = rval[0].Interface().(error) 90 } 91 } 92 93 return 94 } 95 96 // LoadTemplateFilepath loads a Template from file `root`. All files in `partials` 97 // that have the same template type (identified by file extension) are also 98 // parsed and associated with the parsed root template. 99 func LoadTemplateFilepath(rootPath string, partialPaths ...string) (t Template, e error) { 100 var stat os.FileInfo 101 if stat, e = os.Stat(rootPath); e != nil { 102 return 103 } else if stat.IsDir() { 104 e = fmt.Errorf("rootPath path must be a file, not a directory: %s", rootPath) 105 return 106 } 107 108 lang := strings.TrimPrefix(filepath.Ext(rootPath), ".") 109 110 rootName := strings.TrimSuffix(filepath.Base(rootPath), filepath.Ext(rootPath)) 111 112 var root *os.File 113 if root, e = os.Open(rootPath); e != nil { 114 return 115 } 116 defer root.Close() 117 118 partials := make(map[string]io.Reader) 119 for _, path := range partialPaths { 120 name := filepath.Base(path) 121 if lang == "mst" { 122 name = strings.TrimSuffix(name, filepath.Ext(name)) 123 } 124 125 if stat, e = os.Stat(path); e != nil { 126 return 127 } 128 129 var p *os.File 130 if p, e = os.Open(path); e != nil { 131 return 132 } 133 defer p.Close() 134 partials[name] = p 135 } 136 137 return LoadTemplate(lang, rootName, root, partials) 138 } 139 140 // LoadTemplateString will convert `root` and `partials` data to io.StringReader variables and 141 // return a `LoadTemplate` call using them as parameters. 142 // The `partials` map should have the template name to assign the partial template to in the 143 // string key and the template data in as the value. 144 func LoadTemplateString(lang string, rootName string, root string, partials map[string]string) (t Template, e error) { 145 p := make(map[string]io.Reader) 146 for name, partial := range partials { 147 p[name] = strings.NewReader(partial) 148 } 149 return LoadTemplate(lang, rootName, strings.NewReader(root), p) 150 } 151 152 func loadTemplateTmpl(rootName string, root io.Reader, partials map[string]io.Reader) (*tmpl.Template, error) { 153 var template *tmpl.Template 154 155 if buf, err := ioutil.ReadAll(root); err != nil { 156 return nil, err 157 } else if template, err = tmpl.New(rootName).Parse(string(buf)); err != nil { 158 return nil, err 159 } 160 161 for name, partial := range partials { 162 if buf, err := ioutil.ReadAll(partial); err != nil { 163 return nil, err 164 } else if _, err = template.New(name).Parse(string(buf)); err != nil { 165 return nil, err 166 } 167 } 168 169 return template, nil 170 } 171 172 func loadTemplateHmpl(rootName string, root io.Reader, partials map[string]io.Reader) (*hmpl.Template, error) { 173 var template *hmpl.Template 174 175 if buf, err := ioutil.ReadAll(root); err != nil { 176 return nil, err 177 } else if template, err = hmpl.New(rootName).Parse(string(buf)); err != nil { 178 return nil, err 179 } 180 181 for name, partial := range partials { 182 if buf, err := ioutil.ReadAll(partial); err != nil { 183 return nil, err 184 } else if _, err = template.New(name).Parse(string(buf)); err != nil { 185 return nil, err 186 } 187 } 188 189 return template, nil 190 } 191 192 func loadTemplateMst(rootName string, root io.Reader, partials map[string]io.Reader) (*mst.Template, error) { 193 var template *mst.Template 194 195 mstpp := new(mst.StaticProvider) 196 mstpp.Partials = make(map[string]string) 197 for name, partial := range partials { 198 if buf, err := ioutil.ReadAll(partial); err != nil { 199 return nil, err 200 } else { 201 mstpp.Partials[name] = string(buf) 202 } 203 } 204 205 if buf, err := ioutil.ReadAll(root); err != nil { 206 return nil, err 207 } else if template, err = mst.ParseStringPartials(string(buf), mstpp); err != nil { 208 return nil, err 209 } 210 211 return template, nil 212 } 213 214 // LoadTemplate loads a Template from `root` of type `lang`, named 215 // `name`. `lang` must be an element in `SupportedTemplateLangs`. 216 // `name` is optional, if empty the template name will be "template". 217 // `root` should be a string of template, with syntax matching that of 218 // `lang`. `partials` should be a string of template, with syntax 219 // matching that of `lang`. 220 func LoadTemplate(lang string, rootName string, root io.Reader, partials map[string]io.Reader) (t Template, e error) { 221 if IsSupportedTemplateLang(lang) == -1 { 222 e = fmt.Errorf("invalid type '%s'", lang) 223 return 224 } 225 226 t.Name = rootName 227 switch lang { 228 case "tmpl": 229 t.T, e = loadTemplateTmpl(rootName, root, partials) 230 case "hmpl": 231 t.T, e = loadTemplateHmpl(rootName, root, partials) 232 case "mst": 233 t.T, e = loadTemplateMst(rootName, root, partials) 234 default: 235 e = fmt.Errorf("'%s' is not a supported template language", lang) 236 } 237 238 return 239 }