template.go 8.4 KB

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