gf/os/gview/gview_parse.go

384 lines
12 KiB
Go
Raw Normal View History

2021-01-17 21:46:25 +08:00
// Copyright GoFrame Author(https://goframe.org). 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"
"context"
"fmt"
2019-10-31 23:37:33 +08:00
"github.com/gogf/gf/encoding/ghash"
"github.com/gogf/gf/errors/gcode"
2020-12-15 00:09:55 +08:00
"github.com/gogf/gf/errors/gerror"
2019-10-31 23:37:33 +08:00
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/os/gfsnotify"
"github.com/gogf/gf/os/gmlock"
2019-12-17 21:06:34 +08:00
"github.com/gogf/gf/text/gstr"
2019-10-31 23:37:33 +08:00
"github.com/gogf/gf/util/gconv"
"github.com/gogf/gf/util/gutil"
2019-12-26 11:03:59 +08:00
htmltpl "html/template"
2019-10-31 23:37:33 +08:00
"strconv"
2019-08-16 00:29:14 +08:00
"strings"
2019-12-26 11:03:59 +08:00
texttpl "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-06-05 21:58:27 +08:00
const (
// Template name for content parsing.
2020-12-15 00:09:55 +08:00
templateNameForContentParsing = "TemplateContent"
2019-06-05 21:58:27 +08:00
)
2019-12-26 11:03:59 +08:00
// fileCacheItem is the cache item for template file.
type fileCacheItem struct {
path string
folder string
content string
}
var (
// Templates cache map for template folder.
2019-12-10 21:14:15 +08:00
// Note that there's no expiring logic for this map.
templates = gmap.NewStrAnyMap(true)
2019-12-10 21:14:15 +08:00
// Try-folders for resource template file searching.
resourceTryFolders = []string{"template/", "template", "/template", "/template/"}
)
// Parse parses given template file <file> with given template variables <params>
// and returns the parsed template content.
func (view *View) Parse(ctx context.Context, file string, params ...Params) (result string, err error) {
2019-12-26 11:03:59 +08:00
var tpl interface{}
// 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{} {
var (
path string
folder string
content string
resource *gres.File
)
2019-10-31 23:37:33 +08:00
// Searching the absolute file path for <file>.
path, folder, resource, err = view.searchFile(file)
if err != nil {
return nil
}
if resource != nil {
content = gconv.UnsafeBytesToStr(resource.Content())
} else {
content = gfile.GetContentsWithCache(path)
}
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(ctx, err)
2019-10-31 23:37:33 +08:00
}
2019-08-16 00:29:14 +08:00
}
return &fileCacheItem{
path: path,
folder: folder,
content: content,
}
})
2019-10-31 23:37:33 +08:00
if r == nil {
return
}
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
}
// Get the template object instance for <folder>.
2019-12-10 21:14:15 +08:00
tpl, err = view.getTemplate(item.path, item.folder, fmt.Sprintf(`*%s`, gfile.Ext(item.path)))
if err != nil {
return "", err
}
// Using memory lock to ensure concurrent safety for template parsing.
2019-12-10 21:14:15 +08:00
gmlock.LockFunc("gview.Parse:"+item.path, func() {
2019-12-26 11:03:59 +08:00
if view.config.AutoEncode {
tpl, err = tpl.(*htmltpl.Template).Parse(item.content)
} else {
tpl, err = tpl.(*texttpl.Template).Parse(item.content)
}
2020-12-15 00:09:55 +08:00
if err != nil && item.path != "" {
err = gerror.WrapCode(gcode.CodeInternalError, err, item.path)
2020-12-15 00:09:55 +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.
variables := gutil.MapMergeCopy(params...)
if len(view.data) > 0 {
gutil.MapMerge(variables, view.data)
}
view.setI18nLanguageFromCtx(ctx, variables)
buffer := bytes.NewBuffer(nil)
2019-12-26 11:03:59 +08:00
if view.config.AutoEncode {
newTpl, err := tpl.(*htmltpl.Template).Clone()
if err != nil {
return "", err
}
if err := newTpl.Execute(buffer, variables); err != nil {
2019-12-26 11:03:59 +08:00
return "", err
}
} else {
if err := tpl.(*texttpl.Template).Execute(buffer, variables); err != nil {
return "", err
}
}
2019-12-26 11:03:59 +08:00
2019-10-31 23:37:33 +08:00
// TODO any graceful plan to replace "<no value>"?
2019-12-17 21:06:34 +08:00
result = gstr.Replace(buffer.String(), "<no value>", "")
result = view.i18nTranslate(ctx, result, variables)
2019-12-17 21:06:34 +08:00
return result, nil
}
// ParseDefault parses the default template file with params.
func (view *View) ParseDefault(ctx context.Context, params ...Params) (result string, err error) {
return view.Parse(ctx, view.config.DefaultFile, params...)
}
// ParseContent parses given template content <content> with template variables <params>
// and returns the parsed content in []byte.
func (view *View) ParseContent(ctx context.Context, 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)
2020-12-15 00:09:55 +08:00
key := fmt.Sprintf("%s_%v_%v", templateNameForContentParsing, view.config.Delimiters, view.config.AutoEncode)
tpl := templates.GetOrSetFuncLock(key, func() interface{} {
2019-12-26 11:03:59 +08:00
if view.config.AutoEncode {
2020-12-15 00:09:55 +08:00
return htmltpl.New(templateNameForContentParsing).Delims(
view.config.Delimiters[0],
view.config.Delimiters[1],
).Funcs(view.funcMap)
}
2020-12-15 00:09:55 +08:00
return texttpl.New(templateNameForContentParsing).Delims(
view.config.Delimiters[0],
view.config.Delimiters[1],
).Funcs(view.funcMap)
2019-12-26 11:03:59 +08:00
})
2019-06-05 21:58:27 +08:00
// 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-12-26 11:03:59 +08:00
if view.config.AutoEncode {
tpl, err = tpl.(*htmltpl.Template).Parse(content)
} else {
tpl, err = tpl.(*texttpl.Template).Parse(content)
}
2019-06-05 21:58:27 +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.
variables := gutil.MapMergeCopy(params...)
if len(view.data) > 0 {
gutil.MapMerge(variables, view.data)
}
view.setI18nLanguageFromCtx(ctx, variables)
buffer := bytes.NewBuffer(nil)
2019-12-26 11:03:59 +08:00
if view.config.AutoEncode {
newTpl, err := tpl.(*htmltpl.Template).Clone()
if err != nil {
return "", err
}
if err := newTpl.Execute(buffer, variables); err != nil {
2019-12-26 11:03:59 +08:00
return "", err
}
} else {
if err := tpl.(*texttpl.Template).Execute(buffer, variables); err != nil {
return "", err
}
}
2019-10-31 23:37:33 +08:00
// TODO any graceful plan to replace "<no value>"?
2019-12-17 21:06:34 +08:00
result := gstr.Replace(buffer.String(), "<no value>", "")
result = view.i18nTranslate(ctx, result, variables)
2019-12-17 21:06:34 +08:00
return result, nil
}
2019-10-31 23:37:33 +08:00
2019-12-10 21:14:15 +08:00
// getTemplate returns the template object associated with given template file <path>.
2019-10-31 23:37:33 +08:00
// It uses template cache to enhance performance, that is, it will return the same template object
// 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).
2019-12-26 11:03:59 +08:00
func (view *View) getTemplate(filePath, folderPath, pattern string) (tpl interface{}, err error) {
2019-12-10 21:14:15 +08:00
// Key for template cache.
key := fmt.Sprintf("%s_%v", filePath, view.config.Delimiters)
result := templates.GetOrSetFuncLock(key, func() interface{} {
tplName := filePath
2019-12-26 11:03:59 +08:00
if view.config.AutoEncode {
tpl = htmltpl.New(tplName).Delims(
view.config.Delimiters[0],
view.config.Delimiters[1],
).Funcs(view.funcMap)
2019-12-26 11:03:59 +08:00
} else {
tpl = texttpl.New(tplName).Delims(
view.config.Delimiters[0],
view.config.Delimiters[1],
).Funcs(view.funcMap)
2019-12-26 11:03:59 +08:00
}
2019-10-31 23:37:33 +08:00
// Firstly checking the resource manager.
if !gres.IsEmpty() {
2019-12-10 21:14:15 +08:00
if files := gres.ScanDirFile(folderPath, pattern, true); len(files) > 0 {
2019-10-31 23:37:33 +08:00
var err error
if view.config.AutoEncode {
t := tpl.(*htmltpl.Template)
for _, v := range files {
_, err = t.New(v.FileInfo().Name()).Parse(string(v.Content()))
2019-12-26 11:03:59 +08:00
if err != nil {
err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err)
return nil
2019-12-26 11:03:59 +08:00
}
}
} else {
t := tpl.(*texttpl.Template)
for _, v := range files {
_, err = t.New(v.FileInfo().Name()).Parse(string(v.Content()))
2019-12-26 11:03:59 +08:00
if err != nil {
err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err)
return nil
2019-12-26 11:03:59 +08:00
}
2019-10-31 23:37:33 +08:00
}
}
return tpl
}
}
// Secondly checking the file system.
2020-12-15 00:09:55 +08:00
var (
files []string
)
2019-12-10 21:14:15 +08:00
files, err = gfile.ScanDir(folderPath, pattern, true)
2019-10-31 23:37:33 +08:00
if err != nil {
return nil
}
2019-12-26 11:03:59 +08:00
if view.config.AutoEncode {
2020-12-15 00:09:55 +08:00
t := tpl.(*htmltpl.Template)
for _, file := range files {
if _, err = t.Parse(gfile.GetContents(file)); err != nil {
err = view.formatTemplateObjectCreatingError(file, tplName, err)
2020-12-15 00:09:55 +08:00
return nil
}
2019-12-26 11:03:59 +08:00
}
} else {
2020-12-15 00:09:55 +08:00
t := tpl.(*texttpl.Template)
for _, file := range files {
if _, err = t.Parse(gfile.GetContents(file)); err != nil {
err = view.formatTemplateObjectCreatingError(file, tplName, err)
2020-12-15 00:09:55 +08:00
return nil
}
2019-12-26 11:03:59 +08:00
}
2019-10-31 23:37:33 +08:00
}
return tpl
})
if result != nil {
2019-12-26 11:03:59 +08:00
return result, nil
2019-10-31 23:37:33 +08:00
}
return
}
// formatTemplateObjectCreatingError formats the error that creted from creating template object.
func (view *View) formatTemplateObjectCreatingError(filePath, tplName string, err error) error {
if err != nil {
return gerror.NewCodeSkip(gcode.CodeInternalError, 1, gstr.Replace(err.Error(), tplName, filePath))
}
return nil
}
// searchFile returns the found absolute path for <file> and its template folder path.
2019-12-10 21:14:15 +08:00
// Note that, the returned <folder> is the template folder path, but not the folder of
// the returned template file <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() {
2019-12-10 21:14:15 +08:00
// Try folders.
for _, folderPath := range resourceTryFolders {
if resource = gres.Get(folderPath + file); resource != nil {
2019-10-31 23:37:33 +08:00
path = resource.Name()
2019-12-10 21:14:15 +08:00
folder = folderPath
2019-10-31 23:37:33 +08:00
return
}
}
2019-12-10 21:14:15 +08:00
// Search folders.
2019-10-31 23:37:33 +08:00
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) {
2019-12-10 21:14:15 +08:00
for _, folderPath := range array {
folderPath = strings.TrimRight(folderPath, gfile.Separator)
if path, _ = gspath.Search(folderPath, file); path != "" {
folder = folderPath
2019-10-31 23:37:33 +08:00
break
}
2019-12-10 21:14:15 +08:00
if path, _ = gspath.Search(folderPath+gfile.Separator+"template", file); path != "" {
folder = folderPath + gfile.Separator + "template"
2019-10-31 23:37:33 +08:00
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
2019-12-10 21:14:15 +08:00
for _, folderPath := range array {
folderPath = strings.TrimRight(folderPath, "/")
if folderPath == "" {
folderPath = "/"
2019-10-31 23:37:33 +08:00
}
2019-12-10 21:14:15 +08:00
buffer.WriteString(fmt.Sprintf("\n%d. %s", index, folderPath))
2019-10-31 23:37:33 +08:00
index++
2019-12-10 21:14:15 +08:00
buffer.WriteString(fmt.Sprintf("\n%d. %s", index, strings.TrimRight(folderPath, "/")+gfile.Separator+"template"))
2019-10-31 23:37:33 +08:00
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 = gerror.NewCodef(gcode.CodeInvalidParameter, `template file "%s" not found`, file)
2019-10-31 23:37:33 +08:00
}
return
}