2019-04-22 15:47:59 +08:00
|
|
|
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
|
|
|
//
|
|
|
|
// This Source Code Form is subject to the terms of the MIT License.
|
|
|
|
// If a copy of the MIT was not distributed with this file,
|
|
|
|
// You can obtain one at https://github.com/gogf/gf.
|
|
|
|
|
|
|
|
package gview
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2019-10-31 23:37:33 +08:00
|
|
|
"github.com/gogf/gf/encoding/ghash"
|
|
|
|
"github.com/gogf/gf/internal/intlog"
|
|
|
|
"github.com/gogf/gf/os/gfcache"
|
|
|
|
"github.com/gogf/gf/os/gfsnotify"
|
|
|
|
"github.com/gogf/gf/os/gmlock"
|
|
|
|
"github.com/gogf/gf/text/gstr"
|
|
|
|
"github.com/gogf/gf/util/gconv"
|
|
|
|
"strconv"
|
2019-08-16 00:29:14 +08:00
|
|
|
"strings"
|
2019-04-22 15:47:59 +08:00
|
|
|
"text/template"
|
2019-07-29 21:01:19 +08:00
|
|
|
|
2019-08-16 00:29:14 +08:00
|
|
|
"github.com/gogf/gf/os/gres"
|
|
|
|
|
2019-07-29 21:01:19 +08:00
|
|
|
"github.com/gogf/gf/container/gmap"
|
|
|
|
"github.com/gogf/gf/os/gfile"
|
|
|
|
"github.com/gogf/gf/os/glog"
|
|
|
|
"github.com/gogf/gf/os/gspath"
|
2019-04-22 15:47:59 +08:00
|
|
|
)
|
|
|
|
|
2019-06-05 21:58:27 +08:00
|
|
|
const (
|
|
|
|
// Template name for content parsing.
|
|
|
|
gCONTENT_TEMPLATE_NAME = "template content"
|
|
|
|
)
|
|
|
|
|
2019-04-22 15:47:59 +08:00
|
|
|
var (
|
|
|
|
// Templates cache map for template folder.
|
2019-06-11 20:57:43 +08:00
|
|
|
// TODO Note that there's no expiring logic for this map.
|
2019-09-03 23:18:54 +08:00
|
|
|
templates = gmap.NewStrAnyMap(true)
|
|
|
|
resourceTryFiles = []string{"template/", "template", "/template", "/template/"}
|
2019-04-22 15:47:59 +08:00
|
|
|
)
|
|
|
|
|
2019-11-04 22:42:49 +08:00
|
|
|
// fileCacheItem is the cache item for template file.
|
|
|
|
type fileCacheItem struct {
|
|
|
|
path string
|
|
|
|
folder string
|
|
|
|
content string
|
|
|
|
}
|
|
|
|
|
2019-11-20 12:09:26 +08:00
|
|
|
// Parse parses given template file <file> with given template variables <params>
|
|
|
|
// and returns the parsed template content.
|
2019-10-31 23:37:33 +08:00
|
|
|
func (view *View) Parse(file string, params ...Params) (result string, err error) {
|
|
|
|
var tpl *template.Template
|
2019-11-04 22:42:49 +08:00
|
|
|
// It caches the file, folder and its content to enhance performance.
|
2019-10-31 23:37:33 +08:00
|
|
|
r := view.fileCacheMap.GetOrSetFuncLock(file, func() interface{} {
|
2019-11-04 22:42:49 +08:00
|
|
|
var path, folder, content string
|
2019-10-31 23:37:33 +08:00
|
|
|
var resource *gres.File
|
|
|
|
// Searching the absolute file path for <file>.
|
|
|
|
path, folder, resource, err = view.searchFile(file)
|
2019-04-22 15:47:59 +08:00
|
|
|
if err != nil {
|
2019-11-05 17:33:06 +08:00
|
|
|
return nil
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
2019-11-04 22:42:49 +08:00
|
|
|
if resource != nil {
|
|
|
|
content = gconv.UnsafeBytesToStr(resource.Content())
|
|
|
|
} else {
|
|
|
|
content = gfcache.GetContents(path)
|
2019-06-13 20:29:40 +08:00
|
|
|
}
|
2019-10-31 23:37:33 +08:00
|
|
|
// Monitor template files changes using fsnotify asynchronously.
|
|
|
|
if resource == nil {
|
|
|
|
if _, err := gfsnotify.AddOnce("gview.Parse:"+folder, folder, func(event *gfsnotify.Event) {
|
|
|
|
// CLEAR THEM ALL.
|
|
|
|
view.fileCacheMap.Clear()
|
|
|
|
templates.Clear()
|
|
|
|
gfsnotify.Exit()
|
|
|
|
}); err != nil {
|
|
|
|
intlog.Error(err)
|
|
|
|
}
|
2019-08-16 00:29:14 +08:00
|
|
|
}
|
2019-11-04 22:42:49 +08:00
|
|
|
return &fileCacheItem{
|
|
|
|
path: path,
|
|
|
|
folder: folder,
|
|
|
|
content: content,
|
|
|
|
}
|
2019-06-05 20:22:57 +08:00
|
|
|
})
|
2019-10-31 23:37:33 +08:00
|
|
|
if r == nil {
|
|
|
|
return
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
2019-11-04 22:42:49 +08:00
|
|
|
item := r.(*fileCacheItem)
|
2019-12-08 22:55:32 +08:00
|
|
|
// It's not necessary continuing parsing if template content is empty.
|
|
|
|
if item.content == "" {
|
|
|
|
return "", nil
|
|
|
|
}
|
2019-11-04 22:42:49 +08:00
|
|
|
// Get the template object instance for <folder>.
|
|
|
|
tpl, err = view.getTemplate(item.folder, fmt.Sprintf(`*%s`, gfile.Ext(item.path)))
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
// Using memory lock to ensure concurrent safety for template parsing.
|
|
|
|
gmlock.LockFunc("gview.Parse:"+item.folder, func() {
|
|
|
|
tpl, err = tpl.Parse(item.content)
|
|
|
|
})
|
|
|
|
|
2019-04-22 15:47:59 +08:00
|
|
|
// Note that the template variable assignment cannot change the value
|
|
|
|
// of the existing <params> or view.data because both variables are pointers.
|
2019-10-31 23:37:33 +08:00
|
|
|
// It needs to merge the values of the two maps into a new map.
|
|
|
|
var variables map[string]interface{}
|
2019-04-25 22:14:20 +08:00
|
|
|
length := len(view.data)
|
|
|
|
if len(params) > 0 {
|
|
|
|
length += len(params[0])
|
|
|
|
}
|
|
|
|
if length > 0 {
|
2019-09-12 00:00:59 +08:00
|
|
|
variables = make(map[string]interface{}, length)
|
2019-04-25 22:14:20 +08:00
|
|
|
}
|
2019-04-22 15:47:59 +08:00
|
|
|
if len(view.data) > 0 {
|
|
|
|
if len(params) > 0 {
|
2019-10-31 23:37:33 +08:00
|
|
|
if variables == nil {
|
|
|
|
variables = make(map[string]interface{})
|
|
|
|
}
|
2019-04-25 22:14:20 +08:00
|
|
|
for k, v := range params[0] {
|
2019-09-12 00:00:59 +08:00
|
|
|
variables[k] = v
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
|
|
|
for k, v := range view.data {
|
2019-09-12 00:00:59 +08:00
|
|
|
variables[k] = v
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
|
|
|
} else {
|
2019-09-12 00:00:59 +08:00
|
|
|
variables = view.data
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
|
|
|
} else {
|
2019-04-25 22:14:20 +08:00
|
|
|
if len(params) > 0 {
|
2019-09-12 00:00:59 +08:00
|
|
|
variables = params[0]
|
2019-04-25 22:14:20 +08:00
|
|
|
}
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
|
|
|
buffer := bytes.NewBuffer(nil)
|
2019-09-12 00:00:59 +08:00
|
|
|
if err := tpl.Execute(buffer, variables); err != nil {
|
2019-04-22 15:47:59 +08:00
|
|
|
return "", err
|
|
|
|
}
|
2019-10-31 23:37:33 +08:00
|
|
|
// TODO any graceful plan to replace "<no value>"?
|
|
|
|
result = gstr.Replace(buffer.String(), "<no value>", "")
|
2019-09-12 00:00:59 +08:00
|
|
|
result = view.i18nTranslate(result, variables)
|
2019-08-31 18:04:12 +08:00
|
|
|
return result, nil
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
|
|
|
|
2019-11-19 21:50:17 +08:00
|
|
|
// ParseDefault parses the default template file with params.
|
|
|
|
func (view *View) ParseDefault(params ...Params) (result string, err error) {
|
|
|
|
return view.Parse(view.defaultFile, params...)
|
|
|
|
}
|
|
|
|
|
2019-11-20 12:09:26 +08:00
|
|
|
// ParseContent parses given template content <content> with template variables <params>
|
2019-04-22 15:47:59 +08:00
|
|
|
// and returns the parsed content in []byte.
|
2019-06-19 09:06:52 +08:00
|
|
|
func (view *View) ParseContent(content string, params ...Params) (string, error) {
|
2019-12-08 22:55:32 +08:00
|
|
|
// It's not necessary continuing parsing if template content is empty.
|
|
|
|
if content == "" {
|
|
|
|
return "", nil
|
|
|
|
}
|
2019-06-05 21:58:27 +08:00
|
|
|
err := (error)(nil)
|
2019-11-28 14:29:58 +08:00
|
|
|
key := fmt.Sprintf("%s_%v", gCONTENT_TEMPLATE_NAME, view.delimiters)
|
|
|
|
tpl := templates.GetOrSetFuncLock(key, func() interface{} {
|
|
|
|
return template.New(key).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap)
|
2019-06-05 21:58:27 +08:00
|
|
|
}).(*template.Template)
|
|
|
|
// Using memory lock to ensure concurrent safety for content parsing.
|
2019-10-31 23:37:33 +08:00
|
|
|
hash := strconv.FormatUint(ghash.DJBHash64([]byte(content)), 10)
|
|
|
|
gmlock.LockFunc("gview.ParseContent:"+hash, func() {
|
2019-06-05 21:58:27 +08:00
|
|
|
tpl, err = tpl.Parse(content)
|
|
|
|
})
|
2019-04-22 15:47:59 +08:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
// Note that the template variable assignment cannot change the value
|
|
|
|
// of the existing <params> or view.data because both variables are pointers.
|
2019-10-31 23:37:33 +08:00
|
|
|
// It needs to merge the values of the two maps into a new map.
|
|
|
|
var variables map[string]interface{}
|
2019-04-25 22:14:20 +08:00
|
|
|
length := len(view.data)
|
|
|
|
if len(params) > 0 {
|
|
|
|
length += len(params[0])
|
|
|
|
}
|
|
|
|
if length > 0 {
|
2019-09-12 00:00:59 +08:00
|
|
|
variables = make(map[string]interface{}, length)
|
2019-04-25 22:14:20 +08:00
|
|
|
}
|
2019-04-22 15:47:59 +08:00
|
|
|
if len(view.data) > 0 {
|
|
|
|
if len(params) > 0 {
|
2019-10-31 23:37:33 +08:00
|
|
|
if variables == nil {
|
|
|
|
variables = make(map[string]interface{})
|
|
|
|
}
|
2019-04-25 22:14:20 +08:00
|
|
|
for k, v := range params[0] {
|
2019-09-12 00:00:59 +08:00
|
|
|
variables[k] = v
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
|
|
|
for k, v := range view.data {
|
2019-09-12 00:00:59 +08:00
|
|
|
variables[k] = v
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
|
|
|
} else {
|
2019-09-12 00:00:59 +08:00
|
|
|
variables = view.data
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
|
|
|
} else {
|
2019-04-25 22:14:20 +08:00
|
|
|
if len(params) > 0 {
|
2019-09-12 00:00:59 +08:00
|
|
|
variables = params[0]
|
2019-04-25 22:14:20 +08:00
|
|
|
}
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
|
|
|
buffer := bytes.NewBuffer(nil)
|
2019-09-12 00:00:59 +08:00
|
|
|
if err := tpl.Execute(buffer, variables); err != nil {
|
2019-04-22 15:47:59 +08:00
|
|
|
return "", err
|
|
|
|
}
|
2019-10-31 23:37:33 +08:00
|
|
|
// TODO any graceful plan to replace "<no value>"?
|
2019-08-31 18:04:12 +08:00
|
|
|
result := gstr.Replace(buffer.String(), "<no value>", "")
|
2019-09-12 00:00:59 +08:00
|
|
|
result = view.i18nTranslate(result, variables)
|
2019-08-31 18:04:12 +08:00
|
|
|
return result, nil
|
2019-04-22 15:47:59 +08:00
|
|
|
}
|
2019-10-31 23:37:33 +08:00
|
|
|
|
|
|
|
// getTemplate returns the template object associated with given template folder <path>.
|
|
|
|
// It uses template cache to enhance performance, that is, it will return the same template object
|
2019-11-20 12:09:26 +08:00
|
|
|
// with the same given <path>. It will also automatically refresh the template cache
|
2019-10-31 23:37:33 +08:00
|
|
|
// if the template files under <path> changes (recursively).
|
|
|
|
func (view *View) getTemplate(path string, pattern string) (tpl *template.Template, err error) {
|
2019-11-28 14:29:58 +08:00
|
|
|
key := fmt.Sprintf("%s_%v", path, view.delimiters)
|
|
|
|
result := templates.GetOrSetFuncLock(key, func() interface{} {
|
|
|
|
tpl = template.New(key).Delims(view.delimiters[0], view.delimiters[1]).Funcs(view.funcMap)
|
2019-10-31 23:37:33 +08:00
|
|
|
// Firstly checking the resource manager.
|
|
|
|
if !gres.IsEmpty() {
|
|
|
|
if files := gres.ScanDirFile(path, pattern, true); len(files) > 0 {
|
|
|
|
var err error
|
|
|
|
for _, v := range files {
|
|
|
|
_, err = tpl.New(v.FileInfo().Name()).Parse(string(v.Content()))
|
|
|
|
if err != nil {
|
|
|
|
glog.Error(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return tpl
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Secondly checking the file system.
|
2019-11-28 14:29:58 +08:00
|
|
|
var files []string
|
2019-10-31 23:37:33 +08:00
|
|
|
files, err = gfile.ScanDir(path, pattern, true)
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if tpl, err = tpl.ParseFiles(files...); err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return tpl
|
|
|
|
})
|
2019-11-28 14:29:58 +08:00
|
|
|
if result != nil {
|
|
|
|
return result.(*template.Template), nil
|
2019-10-31 23:37:33 +08:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-11-20 12:09:26 +08:00
|
|
|
// searchFile returns the found absolute path for <file> and its template folder path.
|
2019-10-31 23:37:33 +08:00
|
|
|
func (view *View) searchFile(file string) (path string, folder string, resource *gres.File, err error) {
|
|
|
|
// Firstly checking the resource manager.
|
|
|
|
if !gres.IsEmpty() {
|
|
|
|
for _, v := range resourceTryFiles {
|
|
|
|
if resource = gres.Get(v + file); resource != nil {
|
|
|
|
path = resource.Name()
|
|
|
|
folder = v
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
view.paths.RLockFunc(func(array []string) {
|
|
|
|
for _, v := range array {
|
|
|
|
v = strings.TrimRight(v, "/"+gfile.Separator)
|
|
|
|
if resource = gres.Get(v + "/" + file); resource != nil {
|
|
|
|
path = resource.Name()
|
|
|
|
folder = v
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if resource = gres.Get(v + "/template/" + file); resource != nil {
|
|
|
|
path = resource.Name()
|
|
|
|
folder = v + "/template"
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Secondly checking the file system.
|
|
|
|
if path == "" {
|
|
|
|
view.paths.RLockFunc(func(array []string) {
|
|
|
|
for _, v := range array {
|
|
|
|
v = strings.TrimRight(v, gfile.Separator)
|
|
|
|
if path, _ = gspath.Search(v, file); path != "" {
|
|
|
|
folder = v
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if path, _ = gspath.Search(v+gfile.Separator+"template", file); path != "" {
|
|
|
|
folder = v + gfile.Separator + "template"
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error checking.
|
|
|
|
if path == "" {
|
|
|
|
buffer := bytes.NewBuffer(nil)
|
|
|
|
if view.paths.Len() > 0 {
|
|
|
|
buffer.WriteString(fmt.Sprintf("[gview] cannot find template file \"%s\" in following paths:", file))
|
|
|
|
view.paths.RLockFunc(func(array []string) {
|
|
|
|
index := 1
|
|
|
|
for _, v := range array {
|
|
|
|
v = strings.TrimRight(v, "/")
|
|
|
|
if v == "" {
|
|
|
|
v = "/"
|
|
|
|
}
|
|
|
|
buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v))
|
|
|
|
index++
|
|
|
|
buffer.WriteString(fmt.Sprintf("\n%d. %s", index, strings.TrimRight(v, "/")+gfile.Separator+"template"))
|
|
|
|
index++
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
buffer.WriteString(fmt.Sprintf("[gview] cannot find template file \"%s\" with no path set/add", file))
|
|
|
|
}
|
|
|
|
if errorPrint() {
|
|
|
|
glog.Error(buffer.String())
|
|
|
|
}
|
|
|
|
err = errors.New(fmt.Sprintf(`template file "%s" not found`, file))
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|