dati

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

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 }