github.com/wangyougui/gf/v2@v2.6.5/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/wangyougui/gf.
     6  
     7  package gview
     8  
     9  import (
    10  	"bytes"
    11  	"context"
    12  	"fmt"
    13  	htmltpl "html/template"
    14  	"strconv"
    15  	texttpl "text/template"
    16  
    17  	"github.com/wangyougui/gf/v2/container/gmap"
    18  	"github.com/wangyougui/gf/v2/encoding/ghash"
    19  	"github.com/wangyougui/gf/v2/errors/gcode"
    20  	"github.com/wangyougui/gf/v2/errors/gerror"
    21  	"github.com/wangyougui/gf/v2/internal/intlog"
    22  	"github.com/wangyougui/gf/v2/os/gfile"
    23  	"github.com/wangyougui/gf/v2/os/gfsnotify"
    24  	"github.com/wangyougui/gf/v2/os/glog"
    25  	"github.com/wangyougui/gf/v2/os/gmlock"
    26  	"github.com/wangyougui/gf/v2/os/gres"
    27  	"github.com/wangyougui/gf/v2/os/gspath"
    28  	"github.com/wangyougui/gf/v2/text/gstr"
    29  	"github.com/wangyougui/gf/v2/util/gutil"
    30  )
    31  
    32  const (
    33  	// Template name for content parsing.
    34  	templateNameForContentParsing = "TemplateContent"
    35  )
    36  
    37  // fileCacheItem is the cache item for template file.
    38  type fileCacheItem struct {
    39  	path    string
    40  	folder  string
    41  	content string
    42  }
    43  
    44  var (
    45  	// Templates cache map for template folder.
    46  	// Note that there's no expiring logic for this map.
    47  	templates = gmap.NewStrAnyMap(true)
    48  
    49  	// Try-folders for resource template file searching.
    50  	resourceTryFolders = []string{
    51  		"template/", "template", "/template", "/template/",
    52  		"resource/template/", "resource/template", "/resource/template", "/resource/template/",
    53  	}
    54  
    55  	// Prefix array for trying searching in local system.
    56  	localSystemTryFolders = []string{"", "template/", "resource/template"}
    57  )
    58  
    59  // Parse parses given template file `file` with given template variables `params`
    60  // and returns the parsed template content.
    61  func (view *View) Parse(ctx context.Context, file string, params ...Params) (result string, err error) {
    62  	var usedParams Params
    63  	if len(params) > 0 {
    64  		usedParams = params[0]
    65  	}
    66  	return view.ParseOption(ctx, Option{
    67  		File:    file,
    68  		Content: "",
    69  		Orphan:  false,
    70  		Params:  usedParams,
    71  	})
    72  }
    73  
    74  // ParseDefault parses the default template file with params.
    75  func (view *View) ParseDefault(ctx context.Context, params ...Params) (result string, err error) {
    76  	var usedParams Params
    77  	if len(params) > 0 {
    78  		usedParams = params[0]
    79  	}
    80  	return view.ParseOption(ctx, Option{
    81  		File:    view.config.DefaultFile,
    82  		Content: "",
    83  		Orphan:  false,
    84  		Params:  usedParams,
    85  	})
    86  }
    87  
    88  // ParseContent parses given template content `content`  with template variables `params`
    89  // and returns the parsed content in []byte.
    90  func (view *View) ParseContent(ctx context.Context, content string, params ...Params) (string, error) {
    91  	var usedParams Params
    92  	if len(params) > 0 {
    93  		usedParams = params[0]
    94  	}
    95  	return view.ParseOption(ctx, Option{
    96  		Content: content,
    97  		Orphan:  false,
    98  		Params:  usedParams,
    99  	})
   100  }
   101  
   102  // Option for template parsing.
   103  type Option struct {
   104  	File    string // Template file path in absolute or relative to searching paths.
   105  	Content string // Template content, it ignores `File` if `Content` is given.
   106  	Orphan  bool   // If true, the `File` is considered as a single file parsing without files recursively parsing from its folder.
   107  	Params  Params // Template parameters map.
   108  }
   109  
   110  // ParseOption implements template parsing using Option.
   111  func (view *View) ParseOption(ctx context.Context, option Option) (result string, err error) {
   112  	if option.Content != "" {
   113  		return view.doParseContent(ctx, option.Content, option.Params)
   114  	}
   115  	if option.File == "" {
   116  		return "", gerror.New(`template file cannot be empty`)
   117  	}
   118  	// It caches the file, folder and content to enhance performance.
   119  	r := view.fileCacheMap.GetOrSetFuncLock(option.File, func() interface{} {
   120  		var (
   121  			path     string
   122  			folder   string
   123  			content  string
   124  			resource *gres.File
   125  		)
   126  		// Searching the absolute file path for `file`.
   127  		path, folder, resource, err = view.searchFile(ctx, option.File)
   128  		if err != nil {
   129  			return nil
   130  		}
   131  		if resource != nil {
   132  			content = string(resource.Content())
   133  		} else {
   134  			content = gfile.GetContentsWithCache(path)
   135  		}
   136  		// Monitor template files changes using fsnotify asynchronously.
   137  		if resource == nil {
   138  			if _, err = gfsnotify.AddOnce("gview.Parse:"+folder, folder, func(event *gfsnotify.Event) {
   139  				// CLEAR THEM ALL.
   140  				view.fileCacheMap.Clear()
   141  				templates.Clear()
   142  				gfsnotify.Exit()
   143  			}); err != nil {
   144  				intlog.Errorf(ctx, `%+v`, err)
   145  			}
   146  		}
   147  		return &fileCacheItem{
   148  			path:    path,
   149  			folder:  folder,
   150  			content: content,
   151  		}
   152  	})
   153  	if r == nil {
   154  		return
   155  	}
   156  	item := r.(*fileCacheItem)
   157  	// It's not necessary continuing parsing if template content is empty.
   158  	if item.content == "" {
   159  		return "", nil
   160  	}
   161  	// If it's Orphan option, it just parses the single file by ParseContent.
   162  	if option.Orphan {
   163  		return view.doParseContent(ctx, item.content, option.Params)
   164  	}
   165  	// Get the template object instance for `folder`.
   166  	var tpl interface{}
   167  	tpl, err = view.getTemplate(item.path, item.folder, fmt.Sprintf(`*%s`, gfile.Ext(item.path)))
   168  	if err != nil {
   169  		return "", err
   170  	}
   171  	// Using memory lock to ensure concurrent safety for template parsing.
   172  	gmlock.LockFunc("gview.Parse:"+item.path, func() {
   173  		if view.config.AutoEncode {
   174  			tpl, err = tpl.(*htmltpl.Template).Parse(item.content)
   175  		} else {
   176  			tpl, err = tpl.(*texttpl.Template).Parse(item.content)
   177  		}
   178  		if err != nil && item.path != "" {
   179  			err = gerror.Wrap(err, item.path)
   180  		}
   181  	})
   182  	if err != nil {
   183  		return "", err
   184  	}
   185  	// Note that the template variable assignment cannot change the value
   186  	// of the existing `params` or view.data because both variables are pointers.
   187  	// It needs to merge the values of the two maps into a new map.
   188  	variables := gutil.MapMergeCopy(option.Params)
   189  	if len(view.data) > 0 {
   190  		gutil.MapMerge(variables, view.data)
   191  	}
   192  	view.setI18nLanguageFromCtx(ctx, variables)
   193  
   194  	buffer := bytes.NewBuffer(nil)
   195  	if view.config.AutoEncode {
   196  		newTpl, err := tpl.(*htmltpl.Template).Clone()
   197  		if err != nil {
   198  			return "", err
   199  		}
   200  		if err = newTpl.Execute(buffer, variables); err != nil {
   201  			return "", err
   202  		}
   203  	} else {
   204  		if err = tpl.(*texttpl.Template).Execute(buffer, variables); err != nil {
   205  			return "", err
   206  		}
   207  	}
   208  
   209  	// TODO any graceful plan to replace "<no value>"?
   210  	result = gstr.Replace(buffer.String(), "<no value>", "")
   211  	result = view.i18nTranslate(ctx, result, variables)
   212  	return result, nil
   213  }
   214  
   215  // doParseContent parses given template content `content`  with template variables `params`
   216  // and returns the parsed content in []byte.
   217  func (view *View) doParseContent(ctx context.Context, content string, params Params) (string, error) {
   218  	// It's not necessary continuing parsing if template content is empty.
   219  	if content == "" {
   220  		return "", nil
   221  	}
   222  	var (
   223  		err error
   224  		key = fmt.Sprintf("%s_%v_%v", templateNameForContentParsing, view.config.Delimiters, view.config.AutoEncode)
   225  		tpl = templates.GetOrSetFuncLock(key, func() interface{} {
   226  			if view.config.AutoEncode {
   227  				return htmltpl.New(templateNameForContentParsing).Delims(
   228  					view.config.Delimiters[0],
   229  					view.config.Delimiters[1],
   230  				).Funcs(view.funcMap)
   231  			}
   232  			return texttpl.New(templateNameForContentParsing).Delims(
   233  				view.config.Delimiters[0],
   234  				view.config.Delimiters[1],
   235  			).Funcs(view.funcMap)
   236  		})
   237  	)
   238  	// Using memory lock to ensure concurrent safety for content parsing.
   239  	hash := strconv.FormatUint(ghash.DJB64([]byte(content)), 10)
   240  	gmlock.LockFunc("gview.ParseContent:"+hash, func() {
   241  		if view.config.AutoEncode {
   242  			tpl, err = tpl.(*htmltpl.Template).Parse(content)
   243  		} else {
   244  			tpl, err = tpl.(*texttpl.Template).Parse(content)
   245  		}
   246  	})
   247  	if err != nil {
   248  		err = gerror.Wrapf(err, `template parsing failed`)
   249  		return "", err
   250  	}
   251  	// Note that the template variable assignment cannot change the value
   252  	// of the existing `params` or view.data because both variables are pointers.
   253  	// It needs to merge the values of the two maps into a new map.
   254  	variables := gutil.MapMergeCopy(params)
   255  	if len(view.data) > 0 {
   256  		gutil.MapMerge(variables, view.data)
   257  	}
   258  	view.setI18nLanguageFromCtx(ctx, variables)
   259  
   260  	buffer := bytes.NewBuffer(nil)
   261  	if view.config.AutoEncode {
   262  		var newTpl *htmltpl.Template
   263  		newTpl, err = tpl.(*htmltpl.Template).Clone()
   264  		if err != nil {
   265  			err = gerror.Wrapf(err, `template clone failed`)
   266  			return "", err
   267  		}
   268  		if err = newTpl.Execute(buffer, variables); err != nil {
   269  			err = gerror.Wrapf(err, `template parsing failed`)
   270  			return "", err
   271  		}
   272  	} else {
   273  		if err = tpl.(*texttpl.Template).Execute(buffer, variables); err != nil {
   274  			err = gerror.Wrapf(err, `template parsing failed`)
   275  			return "", err
   276  		}
   277  	}
   278  	// TODO any graceful plan to replace "<no value>"?
   279  	result := gstr.Replace(buffer.String(), "<no value>", "")
   280  	result = view.i18nTranslate(ctx, result, variables)
   281  	return result, nil
   282  }
   283  
   284  // getTemplate returns the template object associated with given template file `path`.
   285  // It uses template cache to enhance performance, that is, it will return the same template object
   286  // with the same given `path`. It will also automatically refresh the template cache
   287  // if the template files under `path` changes (recursively).
   288  func (view *View) getTemplate(filePath, folderPath, pattern string) (tpl interface{}, err error) {
   289  	var (
   290  		mapKey  = fmt.Sprintf("%s_%v", filePath, view.config.Delimiters)
   291  		mapFunc = func() interface{} {
   292  			tplName := filePath
   293  			if view.config.AutoEncode {
   294  				tpl = htmltpl.New(tplName).Delims(
   295  					view.config.Delimiters[0],
   296  					view.config.Delimiters[1],
   297  				).Funcs(view.funcMap)
   298  			} else {
   299  				tpl = texttpl.New(tplName).Delims(
   300  					view.config.Delimiters[0],
   301  					view.config.Delimiters[1],
   302  				).Funcs(view.funcMap)
   303  			}
   304  			// Firstly checking the resource manager.
   305  			if !gres.IsEmpty() {
   306  				if files := gres.ScanDirFile(folderPath, pattern, true); len(files) > 0 {
   307  					if view.config.AutoEncode {
   308  						var t = tpl.(*htmltpl.Template)
   309  						for _, v := range files {
   310  							_, err = t.New(v.FileInfo().Name()).Parse(string(v.Content()))
   311  							if err != nil {
   312  								err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err)
   313  								return nil
   314  							}
   315  						}
   316  					} else {
   317  						var t = tpl.(*texttpl.Template)
   318  						for _, v := range files {
   319  							_, err = t.New(v.FileInfo().Name()).Parse(string(v.Content()))
   320  							if err != nil {
   321  								err = view.formatTemplateObjectCreatingError(v.Name(), tplName, err)
   322  								return nil
   323  							}
   324  						}
   325  					}
   326  					return tpl
   327  				}
   328  			}
   329  
   330  			// Secondly checking the file system,
   331  			// and then automatically parsing all its sub-files recursively.
   332  			var files []string
   333  			files, err = gfile.ScanDir(folderPath, pattern, true)
   334  			if err != nil {
   335  				return nil
   336  			}
   337  			if view.config.AutoEncode {
   338  				t := tpl.(*htmltpl.Template)
   339  				for _, file := range files {
   340  					if _, err = t.Parse(gfile.GetContents(file)); err != nil {
   341  						err = view.formatTemplateObjectCreatingError(file, tplName, err)
   342  						return nil
   343  					}
   344  				}
   345  			} else {
   346  				t := tpl.(*texttpl.Template)
   347  				for _, file := range files {
   348  					if _, err = t.Parse(gfile.GetContents(file)); err != nil {
   349  						err = view.formatTemplateObjectCreatingError(file, tplName, err)
   350  						return nil
   351  					}
   352  				}
   353  			}
   354  			return tpl
   355  		}
   356  	)
   357  	result := templates.GetOrSetFuncLock(mapKey, mapFunc)
   358  	if result != nil {
   359  		return result, nil
   360  	}
   361  	return
   362  }
   363  
   364  // formatTemplateObjectCreatingError formats the error that created from creating template object.
   365  func (view *View) formatTemplateObjectCreatingError(filePath, tplName string, err error) error {
   366  	if err != nil {
   367  		return gerror.NewSkip(1, gstr.Replace(err.Error(), tplName, filePath))
   368  	}
   369  	return nil
   370  }
   371  
   372  // searchFile returns the found absolute path for `file` and its template folder path.
   373  // Note that, the returned `folder` is the template folder path, but not the folder of
   374  // the returned template file `path`.
   375  func (view *View) searchFile(ctx context.Context, file string) (path string, folder string, resource *gres.File, err error) {
   376  	var tempPath string
   377  	// Firstly checking the resource manager.
   378  	if !gres.IsEmpty() {
   379  		// Try folders.
   380  		for _, tryFolder := range resourceTryFolders {
   381  			tempPath = tryFolder + file
   382  			if resource = gres.Get(tempPath); resource != nil {
   383  				path = resource.Name()
   384  				folder = tryFolder
   385  				return
   386  			}
   387  		}
   388  		// Search folders.
   389  		view.searchPaths.RLockFunc(func(array []string) {
   390  			for _, searchPath := range array {
   391  				for _, tryFolder := range resourceTryFolders {
   392  					tempPath = searchPath + tryFolder + file
   393  					if resFile := gres.Get(tempPath); resFile != nil {
   394  						path = resFile.Name()
   395  						folder = searchPath + tryFolder
   396  						return
   397  					}
   398  				}
   399  			}
   400  		})
   401  	}
   402  
   403  	// Secondly checking the file system.
   404  	if path == "" {
   405  		// Absolute path.
   406  		path = gfile.RealPath(file)
   407  		if path != "" {
   408  			folder = gfile.Dir(path)
   409  			return
   410  		}
   411  		// In search paths.
   412  		view.searchPaths.RLockFunc(func(array []string) {
   413  			for _, searchPath := range array {
   414  				searchPath = gstr.TrimRight(searchPath, `\/`)
   415  				for _, tryFolder := range localSystemTryFolders {
   416  					relativePath := gstr.TrimRight(
   417  						gfile.Join(tryFolder, file),
   418  						`\/`,
   419  					)
   420  					if path, _ = gspath.Search(searchPath, relativePath); path != "" {
   421  						folder = gfile.Join(searchPath, tryFolder)
   422  						return
   423  					}
   424  				}
   425  			}
   426  		})
   427  	}
   428  
   429  	// Error checking.
   430  	if path == "" {
   431  		buffer := bytes.NewBuffer(nil)
   432  		if view.searchPaths.Len() > 0 {
   433  			buffer.WriteString(fmt.Sprintf("cannot find template file \"%s\" in following paths:", file))
   434  			view.searchPaths.RLockFunc(func(array []string) {
   435  				index := 1
   436  				for _, searchPath := range array {
   437  					searchPath = gstr.TrimRight(searchPath, `\/`)
   438  					for _, tryFolder := range localSystemTryFolders {
   439  						buffer.WriteString(fmt.Sprintf(
   440  							"\n%d. %s",
   441  							index, gfile.Join(searchPath, tryFolder),
   442  						))
   443  						index++
   444  					}
   445  				}
   446  			})
   447  		} else {
   448  			buffer.WriteString(fmt.Sprintf("cannot find template file \"%s\" with no path set/add", file))
   449  		}
   450  		if errorPrint() {
   451  			glog.Error(ctx, buffer.String())
   452  		}
   453  		err = gerror.NewCodef(gcode.CodeInvalidParameter, `template file "%s" not found`, file)
   454  	}
   455  	return
   456  }