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 (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 }