github.com/gogf/gf@v1.16.9/os/gview/gview_parse.go (about)

     1  // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the MIT License.
     4  // If a copy of the MIT was not distributed with this file,
     5  // You can obtain one at https://github.com/gogf/gf.
     6  
     7  package gview
     8  
     9  import (
    10  	"bytes"
    11  	"context"
    12  	"fmt"
    13  	htmltpl "html/template"
    14  	"strconv"
    15  	"strings"
    16  	texttpl "text/template"
    17  
    18  	"github.com/gogf/gf/encoding/ghash"
    19  	"github.com/gogf/gf/errors/gcode"
    20  	"github.com/gogf/gf/errors/gerror"
    21  	"github.com/gogf/gf/internal/intlog"
    22  	"github.com/gogf/gf/os/gfsnotify"
    23  	"github.com/gogf/gf/os/gmlock"
    24  	"github.com/gogf/gf/text/gstr"
    25  	"github.com/gogf/gf/util/gutil"
    26  
    27  	"github.com/gogf/gf/os/gres"
    28  
    29  	"github.com/gogf/gf/container/gmap"
    30  	"github.com/gogf/gf/os/gfile"
    31  	"github.com/gogf/gf/os/glog"
    32  	"github.com/gogf/gf/os/gspath"
    33  )
    34  
    35  const (
    36  	// Template name for content parsing.
    37  	templateNameForContentParsing = "TemplateContent"
    38  )
    39  
    40  // fileCacheItem is the cache item for template file.
    41  type fileCacheItem struct {
    42  	path    string
    43  	folder  string
    44  	content string
    45  }
    46  
    47  var (
    48  	// Templates cache map for template folder.
    49  	// Note that there's no expiring logic for this map.
    50  	templates = gmap.NewStrAnyMap(true)
    51  
    52  	// Try-folders for resource template file searching.
    53  	resourceTryFolders = []string{"template/", "template", "/template", "/template/"}
    54  )
    55  
    56  // Parse parses given template file `file` with given template variables `params`
    57  // and returns the parsed template content.
    58  func (view *View) Parse(ctx context.Context, file string, params ...Params) (result string, err error) {
    59  	var tpl interface{}
    60  	// It caches the file, folder and its content to enhance performance.
    61  	r := view.fileCacheMap.GetOrSetFuncLock(file, func() interface{} {
    62  		var (
    63  			path     string
    64  			folder   string
    65  			content  string
    66  			resource *gres.File
    67  		)
    68  		// Searching the absolute file path for `file`.
    69  		path, folder, resource, err = view.searchFile(file)
    70  		if err != nil {
    71  			return nil
    72  		}
    73  		if resource != nil {
    74  			content = string(resource.Content())
    75  		} else {
    76  			content = gfile.GetContentsWithCache(path)
    77  		}
    78  		// Monitor template files changes using fsnotify asynchronously.
    79  		if resource == nil {
    80  			if _, err := gfsnotify.AddOnce("gview.Parse:"+folder, folder, func(event *gfsnotify.Event) {
    81  				// CLEAR THEM ALL.
    82  				view.fileCacheMap.Clear()
    83  				templates.Clear()
    84  				gfsnotify.Exit()
    85  			}); err != nil {
    86  				intlog.Error(ctx, err)
    87  			}
    88  		}
    89  		return &fileCacheItem{
    90  			path:    path,
    91  			folder:  folder,
    92  			content: content,
    93  		}
    94  	})
    95  	if r == nil {
    96  		return
    97  	}
    98  	item := r.(*fileCacheItem)
    99  	// It's not necessary continuing parsing if template content is empty.
   100  	if item.content == "" {
   101  		return "", nil
   102  	}
   103  	// Get the template object instance for `folder`.
   104  	tpl, err = view.getTemplate(item.path, item.folder, fmt.Sprintf(`*%s`, gfile.Ext(item.path)))
   105  	if err != nil {
   106  		return "", err
   107  	}
   108  	// Using memory lock to ensure concurrent safety for template parsing.
   109  	gmlock.LockFunc("gview.Parse:"+item.path, func() {
   110  		if view.config.AutoEncode {
   111  			tpl, err = tpl.(*htmltpl.Template).Parse(item.content)
   112  		} else {
   113  			tpl, err = tpl.(*texttpl.Template).Parse(item.content)
   114  		}
   115  		if err != nil && item.path != "" {
   116  			err = gerror.WrapCode(gcode.CodeInternalError, err, item.path)
   117  		}
   118  	})
   119  	if err != nil {
   120  		return "", err
   121  	}
   122  	// Note that the template variable assignment cannot change the value
   123  	// of the existing `params` or view.data because both variables are pointers.
   124  	// It needs to merge the values of the two maps into a new map.
   125  	variables := gutil.MapMergeCopy(params...)
   126  	if len(view.data) > 0 {
   127  		gutil.MapMerge(variables, view.data)
   128  	}
   129  	view.setI18nLanguageFromCtx(ctx, variables)
   130  
   131  	buffer := bytes.NewBuffer(nil)
   132  	if view.config.AutoEncode {
   133  		newTpl, err := tpl.(*htmltpl.Template).Clone()
   134  		if err != nil {
   135  			return "", err
   136  		}
   137  		if err := newTpl.Execute(buffer, variables); err != nil {
   138  			return "", err
   139  		}
   140  	} else {
   141  		if err := tpl.(*texttpl.Template).Execute(buffer, variables); err != nil {
   142  			return "", err
   143  		}
   144  	}
   145  
   146  	// TODO any graceful plan to replace "<no value>"?
   147  	result = gstr.Replace(buffer.String(), "<no value>", "")
   148  	result = view.i18nTranslate(ctx, result, variables)
   149  	return result, nil
   150  }
   151  
   152  // ParseDefault parses the default template file with params.
   153  func (view *View) ParseDefault(ctx context.Context, params ...Params) (result string, err error) {
   154  	return view.Parse(ctx, view.config.DefaultFile, params...)
   155  }
   156  
   157  // ParseContent parses given template content `content`  with template variables `params`
   158  // and returns the parsed content in []byte.
   159  func (view *View) ParseContent(ctx context.Context, content string, params ...Params) (string, error) {
   160  	// It's not necessary continuing parsing if template content is empty.
   161  	if content == "" {
   162  		return "", nil
   163  	}
   164  	err := (error)(nil)
   165  	key := fmt.Sprintf("%s_%v_%v", templateNameForContentParsing, view.config.Delimiters, view.config.AutoEncode)
   166  	tpl := templates.GetOrSetFuncLock(key, func() interface{} {
   167  		if view.config.AutoEncode {
   168  			return htmltpl.New(templateNameForContentParsing).Delims(
   169  				view.config.Delimiters[0],
   170  				view.config.Delimiters[1],
   171  			).Funcs(view.funcMap)
   172  		}
   173  		return texttpl.New(templateNameForContentParsing).Delims(
   174  			view.config.Delimiters[0],
   175  			view.config.Delimiters[1],
   176  		).Funcs(view.funcMap)
   177  	})
   178  	// Using memory lock to ensure concurrent safety for content parsing.
   179  	hash := strconv.FormatUint(ghash.DJBHash64([]byte(content)), 10)
   180  	gmlock.LockFunc("gview.ParseContent:"+hash, func() {
   181  		if view.config.AutoEncode {
   182  			tpl, err = tpl.(*htmltpl.Template).Parse(content)
   183  		} else {
   184  			tpl, err = tpl.(*texttpl.Template).Parse(content)
   185  		}
   186  	})
   187  	if err != nil {
   188  		return "", err
   189  	}
   190  	// Note that the template variable assignment cannot change the value
   191  	// of the existing `params` or view.data because both variables are pointers.
   192  	// It needs to merge the values of the two maps into a new map.
   193  	variables := gutil.MapMergeCopy(params...)
   194  	if len(view.data) > 0 {
   195  		gutil.MapMerge(variables, view.data)
   196  	}
   197  	view.setI18nLanguageFromCtx(ctx, variables)
   198  
   199  	buffer := bytes.NewBuffer(nil)
   200  	if view.config.AutoEncode {
   201  		newTpl, err := tpl.(*htmltpl.Template).Clone()
   202  		if err != nil {
   203  			return "", err
   204  		}
   205  		if err := newTpl.Execute(buffer, variables); err != nil {
   206  			return "", err
   207  		}
   208  	} else {
   209  		if err := tpl.(*texttpl.Template).Execute(buffer, variables); err != nil {
   210  			return "", err
   211  		}
   212  	}
   213  	// TODO any graceful plan to replace "<no value>"?
   214  	result := gstr.Replace(buffer.String(), "<no value>", "")
   215  	result = view.i18nTranslate(ctx, result, variables)
   216  	return result, nil
   217  }
   218  
   219  // getTemplate returns the template object associated with given template file `path`.
   220  // It uses template cache to enhance performance, that is, it will return the same template object
   221  // with the same given `path`. It will also automatically refresh the template cache
   222  // if the template files under `path` changes (recursively).
   223  func (view *View) getTemplate(filePath, folderPath, pattern string) (tpl interface{}, err error) {
   224  	// Key for template cache.
   225  	key := fmt.Sprintf("%s_%v", filePath, view.config.Delimiters)
   226  	result := templates.GetOrSetFuncLock(key, func() interface{} {
   227  		tplName := filePath
   228  		if view.config.AutoEncode {
   229  			tpl = htmltpl.New(tplName).Delims(
   230  				view.config.Delimiters[0],
   231  				view.config.Delimiters[1],
   232  			).Funcs(view.funcMap)
   233  		} else {
   234  			tpl = texttpl.New(tplName).Delims(
   235  				view.config.Delimiters[0],
   236  				view.config.Delimiters[1],
   237  			).Funcs(view.funcMap)
   238  		}
   239  		// Firstly checking the resource manager.
   240  		if !gres.IsEmpty() {
   241  			if files := gres.ScanDirFile(folderPath, pattern, true); len(files) > 0 {
   242  				var err error
   243  				if view.config.AutoEncode {
   244  					t := tpl.(*htmltpl.Template)
   245  					for _, v := range files {
   246  						_, err = t.New(v.FileInfo().Name()).Parse(string(v.Content()))
   247  						if err != nil {
   248  							err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err)
   249  							return nil
   250  						}
   251  					}
   252  				} else {
   253  					t := tpl.(*texttpl.Template)
   254  					for _, v := range files {
   255  						_, err = t.New(v.FileInfo().Name()).Parse(string(v.Content()))
   256  						if err != nil {
   257  							err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err)
   258  							return nil
   259  						}
   260  					}
   261  				}
   262  				return tpl
   263  			}
   264  		}
   265  
   266  		// Secondly checking the file system.
   267  		var (
   268  			files []string
   269  		)
   270  		files, err = gfile.ScanDir(folderPath, pattern, true)
   271  		if err != nil {
   272  			return nil
   273  		}
   274  		if view.config.AutoEncode {
   275  			t := tpl.(*htmltpl.Template)
   276  			for _, file := range files {
   277  				if _, err = t.Parse(gfile.GetContents(file)); err != nil {
   278  					err = view.formatTemplateObjectCreatingError(file, tplName, err)
   279  					return nil
   280  				}
   281  			}
   282  		} else {
   283  			t := tpl.(*texttpl.Template)
   284  			for _, file := range files {
   285  				if _, err = t.Parse(gfile.GetContents(file)); err != nil {
   286  					err = view.formatTemplateObjectCreatingError(file, tplName, err)
   287  					return nil
   288  				}
   289  			}
   290  		}
   291  		return tpl
   292  	})
   293  	if result != nil {
   294  		return result, nil
   295  	}
   296  	return
   297  }
   298  
   299  // formatTemplateObjectCreatingError formats the error that creted from creating template object.
   300  func (view *View) formatTemplateObjectCreatingError(filePath, tplName string, err error) error {
   301  	if err != nil {
   302  		return gerror.NewCodeSkip(gcode.CodeInternalError, 1, gstr.Replace(err.Error(), tplName, filePath))
   303  	}
   304  	return nil
   305  }
   306  
   307  // searchFile returns the found absolute path for `file` and its template folder path.
   308  // Note that, the returned `folder` is the template folder path, but not the folder of
   309  // the returned template file `path`.
   310  func (view *View) searchFile(file string) (path string, folder string, resource *gres.File, err error) {
   311  	// Firstly checking the resource manager.
   312  	if !gres.IsEmpty() {
   313  		// Try folders.
   314  		for _, folderPath := range resourceTryFolders {
   315  			if resource = gres.Get(folderPath + file); resource != nil {
   316  				path = resource.Name()
   317  				folder = folderPath
   318  				return
   319  			}
   320  		}
   321  		// Search folders.
   322  		view.paths.RLockFunc(func(array []string) {
   323  			for _, v := range array {
   324  				v = strings.TrimRight(v, "/"+gfile.Separator)
   325  				if resource = gres.Get(v + "/" + file); resource != nil {
   326  					path = resource.Name()
   327  					folder = v
   328  					break
   329  				}
   330  				if resource = gres.Get(v + "/template/" + file); resource != nil {
   331  					path = resource.Name()
   332  					folder = v + "/template"
   333  					break
   334  				}
   335  			}
   336  		})
   337  	}
   338  
   339  	// Secondly checking the file system.
   340  	if path == "" {
   341  		view.paths.RLockFunc(func(array []string) {
   342  			for _, folderPath := range array {
   343  				folderPath = strings.TrimRight(folderPath, gfile.Separator)
   344  				if path, _ = gspath.Search(folderPath, file); path != "" {
   345  					folder = folderPath
   346  					break
   347  				}
   348  				if path, _ = gspath.Search(folderPath+gfile.Separator+"template", file); path != "" {
   349  					folder = folderPath + gfile.Separator + "template"
   350  					break
   351  				}
   352  			}
   353  		})
   354  	}
   355  
   356  	// Error checking.
   357  	if path == "" {
   358  		buffer := bytes.NewBuffer(nil)
   359  		if view.paths.Len() > 0 {
   360  			buffer.WriteString(fmt.Sprintf("[gview] cannot find template file \"%s\" in following paths:", file))
   361  			view.paths.RLockFunc(func(array []string) {
   362  				index := 1
   363  				for _, folderPath := range array {
   364  					folderPath = strings.TrimRight(folderPath, "/")
   365  					if folderPath == "" {
   366  						folderPath = "/"
   367  					}
   368  					buffer.WriteString(fmt.Sprintf("\n%d. %s", index, folderPath))
   369  					index++
   370  					buffer.WriteString(fmt.Sprintf("\n%d. %s", index, strings.TrimRight(folderPath, "/")+gfile.Separator+"template"))
   371  					index++
   372  				}
   373  			})
   374  		} else {
   375  			buffer.WriteString(fmt.Sprintf("[gview] cannot find template file \"%s\" with no path set/add", file))
   376  		}
   377  		if errorPrint() {
   378  			glog.Error(buffer.String())
   379  		}
   380  		err = gerror.NewCodef(gcode.CodeInvalidParameter, `template file "%s" not found`, file)
   381  	}
   382  	return
   383  }