123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- // 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"
- htmltpl "html/template"
- "strconv"
- texttpl "text/template"
- "github.com/gogf/gf/v2/container/gmap"
- "github.com/gogf/gf/v2/encoding/ghash"
- "github.com/gogf/gf/v2/errors/gcode"
- "github.com/gogf/gf/v2/errors/gerror"
- "github.com/gogf/gf/v2/internal/intlog"
- "github.com/gogf/gf/v2/os/gfile"
- "github.com/gogf/gf/v2/os/gfsnotify"
- "github.com/gogf/gf/v2/os/glog"
- "github.com/gogf/gf/v2/os/gmlock"
- "github.com/gogf/gf/v2/os/gres"
- "github.com/gogf/gf/v2/os/gspath"
- "github.com/gogf/gf/v2/text/gstr"
- "github.com/gogf/gf/v2/util/gutil"
- )
- const (
- // Template name for content parsing.
- templateNameForContentParsing = "TemplateContent"
- )
- // fileCacheItem is the cache item for template file.
- type fileCacheItem struct {
- path string
- folder string
- content string
- }
- var (
- // Templates cache map for template folder.
- // Note that there's no expiring logic for this map.
- templates = gmap.NewStrAnyMap(true)
- // Try-folders for resource template file searching.
- resourceTryFolders = []string{
- "template/", "template", "/template", "/template/",
- "resource/template/", "resource/template", "/resource/template", "/resource/template/",
- }
- // Prefix array for trying searching in local system.
- localSystemTryFolders = []string{"", "template/", "resource/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) {
- var usedParams Params
- if len(params) > 0 {
- usedParams = params[0]
- }
- return view.ParseOption(ctx, Option{
- File: file,
- Content: "",
- Orphan: false,
- Params: usedParams,
- })
- }
- // ParseDefault parses the default template file with params.
- func (view *View) ParseDefault(ctx context.Context, params ...Params) (result string, err error) {
- var usedParams Params
- if len(params) > 0 {
- usedParams = params[0]
- }
- return view.ParseOption(ctx, Option{
- File: view.config.DefaultFile,
- Content: "",
- Orphan: false,
- Params: usedParams,
- })
- }
- // 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) {
- var usedParams Params
- if len(params) > 0 {
- usedParams = params[0]
- }
- return view.ParseOption(ctx, Option{
- Content: content,
- Orphan: false,
- Params: usedParams,
- })
- }
- // Option for template parsing.
- type Option struct {
- File string // Template file path in absolute or relative to searching paths.
- Content string // Template content, it ignores `File` if `Content` is given.
- Orphan bool // If true, the `File` is considered as a single file parsing without files recursively parsing from its folder.
- Params Params // Template parameters map.
- }
- // ParseOption implements template parsing using Option.
- func (view *View) ParseOption(ctx context.Context, option Option) (result string, err error) {
- if option.Content != "" {
- return view.doParseContent(ctx, option.Content, option.Params)
- }
- if option.File == "" {
- return "", gerror.New(`template file cannot be empty`)
- }
- // It caches the file, folder and content to enhance performance.
- r := view.fileCacheMap.GetOrSetFuncLock(option.File, func() interface{} {
- var (
- path string
- folder string
- content string
- resource *gres.File
- )
- // Searching the absolute file path for `file`.
- path, folder, resource, err = view.searchFile(ctx, option.File)
- if err != nil {
- return nil
- }
- if resource != nil {
- content = string(resource.Content())
- } else {
- content = gfile.GetContentsWithCache(path)
- }
- // 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.Errorf(ctx, `%+v`, err)
- }
- }
- return &fileCacheItem{
- path: path,
- folder: folder,
- content: content,
- }
- })
- if r == nil {
- return
- }
- item := r.(*fileCacheItem)
- // It's not necessary continuing parsing if template content is empty.
- if item.content == "" {
- return "", nil
- }
- // If it's Orphan option, it just parses the single file by ParseContent.
- if option.Orphan {
- return view.doParseContent(ctx, item.content, option.Params)
- }
- // Get the template object instance for `folder`.
- var tpl interface{}
- 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.
- gmlock.LockFunc("gview.Parse:"+item.path, func() {
- if view.config.AutoEncode {
- tpl, err = tpl.(*htmltpl.Template).Parse(item.content)
- } else {
- tpl, err = tpl.(*texttpl.Template).Parse(item.content)
- }
- if err != nil && item.path != "" {
- err = gerror.Wrap(err, item.path)
- }
- })
- 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.
- // It needs to merge the values of the two maps into a new map.
- variables := gutil.MapMergeCopy(option.Params)
- if len(view.data) > 0 {
- gutil.MapMerge(variables, view.data)
- }
- view.setI18nLanguageFromCtx(ctx, variables)
- buffer := bytes.NewBuffer(nil)
- if view.config.AutoEncode {
- newTpl, err := tpl.(*htmltpl.Template).Clone()
- if err != nil {
- return "", err
- }
- if err = newTpl.Execute(buffer, variables); err != nil {
- return "", err
- }
- } else {
- if err = tpl.(*texttpl.Template).Execute(buffer, variables); err != nil {
- return "", err
- }
- }
- // TODO any graceful plan to replace "<no value>"?
- result = gstr.Replace(buffer.String(), "<no value>", "")
- result = view.i18nTranslate(ctx, result, variables)
- return result, nil
- }
- // doParseContent parses given template content `content` with template variables `params`
- // and returns the parsed content in []byte.
- func (view *View) doParseContent(ctx context.Context, content string, params Params) (string, error) {
- // It's not necessary continuing parsing if template content is empty.
- if content == "" {
- return "", nil
- }
- var (
- err error
- key = fmt.Sprintf("%s_%v_%v", templateNameForContentParsing, view.config.Delimiters, view.config.AutoEncode)
- tpl = templates.GetOrSetFuncLock(key, func() interface{} {
- if view.config.AutoEncode {
- return htmltpl.New(templateNameForContentParsing).Delims(
- view.config.Delimiters[0],
- view.config.Delimiters[1],
- ).Funcs(view.funcMap)
- }
- return texttpl.New(templateNameForContentParsing).Delims(
- view.config.Delimiters[0],
- view.config.Delimiters[1],
- ).Funcs(view.funcMap)
- })
- )
- // Using memory lock to ensure concurrent safety for content parsing.
- hash := strconv.FormatUint(ghash.DJB64([]byte(content)), 10)
- gmlock.LockFunc("gview.ParseContent:"+hash, func() {
- if view.config.AutoEncode {
- tpl, err = tpl.(*htmltpl.Template).Parse(content)
- } else {
- tpl, err = tpl.(*texttpl.Template).Parse(content)
- }
- })
- if err != nil {
- err = gerror.Wrapf(err, `template parsing failed`)
- return "", err
- }
- // Note that the template variable assignment cannot change the value
- // of the existing `params` or view.data because both variables are pointers.
- // 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)
- if view.config.AutoEncode {
- var newTpl *htmltpl.Template
- newTpl, err = tpl.(*htmltpl.Template).Clone()
- if err != nil {
- err = gerror.Wrapf(err, `template clone failed`)
- return "", err
- }
- if err = newTpl.Execute(buffer, variables); err != nil {
- err = gerror.Wrapf(err, `template parsing failed`)
- return "", err
- }
- } else {
- if err = tpl.(*texttpl.Template).Execute(buffer, variables); err != nil {
- err = gerror.Wrapf(err, `template parsing failed`)
- return "", err
- }
- }
- // TODO any graceful plan to replace "<no value>"?
- result := gstr.Replace(buffer.String(), "<no value>", "")
- result = view.i18nTranslate(ctx, result, variables)
- return result, nil
- }
- // getTemplate returns the template object associated with given template file `path`.
- // 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
- // if the template files under `path` changes (recursively).
- func (view *View) getTemplate(filePath, folderPath, pattern string) (tpl interface{}, err error) {
- var (
- mapKey = fmt.Sprintf("%s_%v", filePath, view.config.Delimiters)
- mapFunc = func() interface{} {
- tplName := filePath
- if view.config.AutoEncode {
- tpl = htmltpl.New(tplName).Delims(
- view.config.Delimiters[0],
- view.config.Delimiters[1],
- ).Funcs(view.funcMap)
- } else {
- tpl = texttpl.New(tplName).Delims(
- view.config.Delimiters[0],
- view.config.Delimiters[1],
- ).Funcs(view.funcMap)
- }
- // Firstly checking the resource manager.
- if !gres.IsEmpty() {
- if files := gres.ScanDirFile(folderPath, pattern, true); len(files) > 0 {
- if view.config.AutoEncode {
- var t = tpl.(*htmltpl.Template)
- for _, v := range files {
- _, err = t.New(v.FileInfo().Name()).Parse(string(v.Content()))
- if err != nil {
- err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err)
- return nil
- }
- }
- } else {
- var t = tpl.(*texttpl.Template)
- for _, v := range files {
- _, err = t.New(v.FileInfo().Name()).Parse(string(v.Content()))
- if err != nil {
- err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err)
- return nil
- }
- }
- }
- return tpl
- }
- }
- // Secondly checking the file system,
- // and then automatically parsing all its sub-files recursively.
- var files []string
- files, err = gfile.ScanDir(folderPath, pattern, true)
- if err != nil {
- return nil
- }
- if view.config.AutoEncode {
- t := tpl.(*htmltpl.Template)
- for _, file := range files {
- if _, err = t.Parse(gfile.GetContents(file)); err != nil {
- err = view.formatTemplateObjectCreatingError(file, tplName, err)
- return nil
- }
- }
- } else {
- t := tpl.(*texttpl.Template)
- for _, file := range files {
- if _, err = t.Parse(gfile.GetContents(file)); err != nil {
- err = view.formatTemplateObjectCreatingError(file, tplName, err)
- return nil
- }
- }
- }
- return tpl
- }
- )
- result := templates.GetOrSetFuncLock(mapKey, mapFunc)
- if result != nil {
- return result, nil
- }
- return
- }
- // formatTemplateObjectCreatingError formats the error that created from creating template object.
- func (view *View) formatTemplateObjectCreatingError(filePath, tplName string, err error) error {
- if err != nil {
- return gerror.NewSkip(1, gstr.Replace(err.Error(), tplName, filePath))
- }
- return nil
- }
- // searchFile returns the found absolute path for `file` and its template folder path.
- // Note that, the returned `folder` is the template folder path, but not the folder of
- // the returned template file `path`.
- func (view *View) searchFile(ctx context.Context, file string) (path string, folder string, resource *gres.File, err error) {
- var tempPath string
- // Firstly checking the resource manager.
- if !gres.IsEmpty() {
- // Try folders.
- for _, tryFolder := range resourceTryFolders {
- tempPath = tryFolder + file
- if resource = gres.Get(tempPath); resource != nil {
- path = resource.Name()
- folder = tryFolder
- return
- }
- }
- // Search folders.
- view.searchPaths.RLockFunc(func(array []string) {
- for _, searchPath := range array {
- for _, tryFolder := range resourceTryFolders {
- tempPath = searchPath + tryFolder + file
- if resFile := gres.Get(tempPath); resFile != nil {
- path = resFile.Name()
- folder = searchPath + tryFolder
- return
- }
- }
- }
- })
- }
- // Secondly checking the file system.
- if path == "" {
- // Absolute path.
- path = gfile.RealPath(file)
- if path != "" {
- folder = gfile.Dir(path)
- return
- }
- // In search paths.
- view.searchPaths.RLockFunc(func(array []string) {
- for _, searchPath := range array {
- searchPath = gstr.TrimRight(searchPath, `\/`)
- for _, tryFolder := range localSystemTryFolders {
- relativePath := gstr.TrimRight(
- gfile.Join(tryFolder, file),
- `\/`,
- )
- if path, _ = gspath.Search(searchPath, relativePath); path != "" {
- folder = gfile.Join(searchPath, tryFolder)
- return
- }
- }
- }
- })
- }
- // Error checking.
- if path == "" {
- buffer := bytes.NewBuffer(nil)
- if view.searchPaths.Len() > 0 {
- buffer.WriteString(fmt.Sprintf("cannot find template file \"%s\" in following paths:", file))
- view.searchPaths.RLockFunc(func(array []string) {
- index := 1
- for _, searchPath := range array {
- searchPath = gstr.TrimRight(searchPath, `\/`)
- for _, tryFolder := range localSystemTryFolders {
- buffer.WriteString(fmt.Sprintf(
- "\n%d. %s",
- index, gfile.Join(searchPath, tryFolder),
- ))
- index++
- }
- }
- })
- } else {
- buffer.WriteString(fmt.Sprintf("cannot find template file \"%s\" with no path set/add", file))
- }
- if errorPrint() {
- glog.Error(ctx, buffer.String())
- }
- err = gerror.NewCodef(gcode.CodeInvalidParameter, `template file "%s" not found`, file)
- }
- return
- }
|