dati

A Go library/binary to parse & execute data against template langauges.
git clone git://src.gearsix.net/datidati.zip
Log | Files | Refs | Atom | README | LICENSE

template.go (raw) (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 }