github.com/astaxie/beego@v1.12.3/template.go (about)

     1  // Copyright 2014 beego Author. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package beego
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"html/template"
    21  	"io"
    22  	"io/ioutil"
    23  	"net/http"
    24  	"os"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strings"
    28  	"sync"
    29  
    30  	"github.com/astaxie/beego/logs"
    31  	"github.com/astaxie/beego/utils"
    32  )
    33  
    34  var (
    35  	beegoTplFuncMap           = make(template.FuncMap)
    36  	beeViewPathTemplateLocked = false
    37  	// beeViewPathTemplates caching map and supported template file extensions per view
    38  	beeViewPathTemplates = make(map[string]map[string]*template.Template)
    39  	templatesLock        sync.RWMutex
    40  	// beeTemplateExt stores the template extension which will build
    41  	beeTemplateExt = []string{"tpl", "html", "gohtml"}
    42  	// beeTemplatePreprocessors stores associations of extension -> preprocessor handler
    43  	beeTemplateEngines = map[string]templatePreProcessor{}
    44  	beeTemplateFS      = defaultFSFunc
    45  )
    46  
    47  // ExecuteTemplate applies the template with name  to the specified data object,
    48  // writing the output to wr.
    49  // A template will be executed safely in parallel.
    50  func ExecuteTemplate(wr io.Writer, name string, data interface{}) error {
    51  	return ExecuteViewPathTemplate(wr, name, BConfig.WebConfig.ViewsPath, data)
    52  }
    53  
    54  // ExecuteViewPathTemplate applies the template with name and from specific viewPath to the specified data object,
    55  // writing the output to wr.
    56  // A template will be executed safely in parallel.
    57  func ExecuteViewPathTemplate(wr io.Writer, name string, viewPath string, data interface{}) error {
    58  	if BConfig.RunMode == DEV {
    59  		templatesLock.RLock()
    60  		defer templatesLock.RUnlock()
    61  	}
    62  	if beeTemplates, ok := beeViewPathTemplates[viewPath]; ok {
    63  		if t, ok := beeTemplates[name]; ok {
    64  			var err error
    65  			if t.Lookup(name) != nil {
    66  				err = t.ExecuteTemplate(wr, name, data)
    67  			} else {
    68  				err = t.Execute(wr, data)
    69  			}
    70  			if err != nil {
    71  				logs.Trace("template Execute err:", err)
    72  			}
    73  			return err
    74  		}
    75  		panic("can't find templatefile in the path:" + viewPath + "/" + name)
    76  	}
    77  	panic("Unknown view path:" + viewPath)
    78  }
    79  
    80  func init() {
    81  	beegoTplFuncMap["dateformat"] = DateFormat
    82  	beegoTplFuncMap["date"] = Date
    83  	beegoTplFuncMap["compare"] = Compare
    84  	beegoTplFuncMap["compare_not"] = CompareNot
    85  	beegoTplFuncMap["not_nil"] = NotNil
    86  	beegoTplFuncMap["not_null"] = NotNil
    87  	beegoTplFuncMap["substr"] = Substr
    88  	beegoTplFuncMap["html2str"] = HTML2str
    89  	beegoTplFuncMap["str2html"] = Str2html
    90  	beegoTplFuncMap["htmlquote"] = Htmlquote
    91  	beegoTplFuncMap["htmlunquote"] = Htmlunquote
    92  	beegoTplFuncMap["renderform"] = RenderForm
    93  	beegoTplFuncMap["assets_js"] = AssetsJs
    94  	beegoTplFuncMap["assets_css"] = AssetsCSS
    95  	beegoTplFuncMap["config"] = GetConfig
    96  	beegoTplFuncMap["map_get"] = MapGet
    97  
    98  	// Comparisons
    99  	beegoTplFuncMap["eq"] = eq // ==
   100  	beegoTplFuncMap["ge"] = ge // >=
   101  	beegoTplFuncMap["gt"] = gt // >
   102  	beegoTplFuncMap["le"] = le // <=
   103  	beegoTplFuncMap["lt"] = lt // <
   104  	beegoTplFuncMap["ne"] = ne // !=
   105  
   106  	beegoTplFuncMap["urlfor"] = URLFor // build a URL to match a Controller and it's method
   107  }
   108  
   109  // AddFuncMap let user to register a func in the template.
   110  func AddFuncMap(key string, fn interface{}) error {
   111  	beegoTplFuncMap[key] = fn
   112  	return nil
   113  }
   114  
   115  type templatePreProcessor func(root, path string, funcs template.FuncMap) (*template.Template, error)
   116  
   117  type templateFile struct {
   118  	root  string
   119  	files map[string][]string
   120  }
   121  
   122  // visit will make the paths into two part,the first is subDir (without tf.root),the second is full path(without tf.root).
   123  // if tf.root="views" and
   124  // paths is "views/errors/404.html",the subDir will be "errors",the file will be "errors/404.html"
   125  // paths is "views/admin/errors/404.html",the subDir will be "admin/errors",the file will be "admin/errors/404.html"
   126  func (tf *templateFile) visit(paths string, f os.FileInfo, err error) error {
   127  	if f == nil {
   128  		return err
   129  	}
   130  	if f.IsDir() || (f.Mode()&os.ModeSymlink) > 0 {
   131  		return nil
   132  	}
   133  	if !HasTemplateExt(paths) {
   134  		return nil
   135  	}
   136  
   137  	replace := strings.NewReplacer("\\", "/")
   138  	file := strings.TrimLeft(replace.Replace(paths[len(tf.root):]), "/")
   139  	subDir := filepath.Dir(file)
   140  
   141  	tf.files[subDir] = append(tf.files[subDir], file)
   142  	return nil
   143  }
   144  
   145  // HasTemplateExt return this path contains supported template extension of beego or not.
   146  func HasTemplateExt(paths string) bool {
   147  	for _, v := range beeTemplateExt {
   148  		if strings.HasSuffix(paths, "."+v) {
   149  			return true
   150  		}
   151  	}
   152  	return false
   153  }
   154  
   155  // AddTemplateExt add new extension for template.
   156  func AddTemplateExt(ext string) {
   157  	for _, v := range beeTemplateExt {
   158  		if v == ext {
   159  			return
   160  		}
   161  	}
   162  	beeTemplateExt = append(beeTemplateExt, ext)
   163  }
   164  
   165  // AddViewPath adds a new path to the supported view paths.
   166  //Can later be used by setting a controller ViewPath to this folder
   167  //will panic if called after beego.Run()
   168  func AddViewPath(viewPath string) error {
   169  	if beeViewPathTemplateLocked {
   170  		if _, exist := beeViewPathTemplates[viewPath]; exist {
   171  			return nil //Ignore if viewpath already exists
   172  		}
   173  		panic("Can not add new view paths after beego.Run()")
   174  	}
   175  	beeViewPathTemplates[viewPath] = make(map[string]*template.Template)
   176  	return BuildTemplate(viewPath)
   177  }
   178  
   179  func lockViewPaths() {
   180  	beeViewPathTemplateLocked = true
   181  }
   182  
   183  // BuildTemplate will build all template files in a directory.
   184  // it makes beego can render any template file in view directory.
   185  func BuildTemplate(dir string, files ...string) error {
   186  	var err error
   187  	fs := beeTemplateFS()
   188  	f, err := fs.Open(dir)
   189  	if err != nil {
   190  		if os.IsNotExist(err) {
   191  			return nil
   192  		}
   193  		return errors.New("dir open err")
   194  	}
   195  	defer f.Close()
   196  
   197  	beeTemplates, ok := beeViewPathTemplates[dir]
   198  	if !ok {
   199  		panic("Unknown view path: " + dir)
   200  	}
   201  	self := &templateFile{
   202  		root:  dir,
   203  		files: make(map[string][]string),
   204  	}
   205  	err = Walk(fs, dir, func(path string, f os.FileInfo, err error) error {
   206  		return self.visit(path, f, err)
   207  	})
   208  	if err != nil {
   209  		fmt.Printf("Walk() returned %v\n", err)
   210  		return err
   211  	}
   212  	buildAllFiles := len(files) == 0
   213  	for _, v := range self.files {
   214  		for _, file := range v {
   215  			if buildAllFiles || utils.InSlice(file, files) {
   216  				templatesLock.Lock()
   217  				ext := filepath.Ext(file)
   218  				var t *template.Template
   219  				if len(ext) == 0 {
   220  					t, err = getTemplate(self.root, fs, file, v...)
   221  				} else if fn, ok := beeTemplateEngines[ext[1:]]; ok {
   222  					t, err = fn(self.root, file, beegoTplFuncMap)
   223  				} else {
   224  					t, err = getTemplate(self.root, fs, file, v...)
   225  				}
   226  				if err != nil {
   227  					logs.Error("parse template err:", file, err)
   228  					templatesLock.Unlock()
   229  					return err
   230  				}
   231  				beeTemplates[file] = t
   232  				templatesLock.Unlock()
   233  			}
   234  		}
   235  	}
   236  	return nil
   237  }
   238  
   239  func getTplDeep(root string, fs http.FileSystem, file string, parent string, t *template.Template) (*template.Template, [][]string, error) {
   240  	var fileAbsPath string
   241  	var rParent string
   242  	var err error
   243  	if strings.HasPrefix(file, "../") {
   244  		rParent = filepath.Join(filepath.Dir(parent), file)
   245  		fileAbsPath = filepath.Join(root, filepath.Dir(parent), file)
   246  	} else {
   247  		rParent = file
   248  		fileAbsPath = filepath.Join(root, file)
   249  	}
   250  	f, err := fs.Open(fileAbsPath)
   251  	if err != nil {
   252  		panic("can't find template file:" + file)
   253  	}
   254  	defer f.Close()
   255  	data, err := ioutil.ReadAll(f)
   256  	if err != nil {
   257  		return nil, [][]string{}, err
   258  	}
   259  	t, err = t.New(file).Parse(string(data))
   260  	if err != nil {
   261  		return nil, [][]string{}, err
   262  	}
   263  	reg := regexp.MustCompile(BConfig.WebConfig.TemplateLeft + "[ ]*template[ ]+\"([^\"]+)\"")
   264  	allSub := reg.FindAllStringSubmatch(string(data), -1)
   265  	for _, m := range allSub {
   266  		if len(m) == 2 {
   267  			tl := t.Lookup(m[1])
   268  			if tl != nil {
   269  				continue
   270  			}
   271  			if !HasTemplateExt(m[1]) {
   272  				continue
   273  			}
   274  			_, _, err = getTplDeep(root, fs, m[1], rParent, t)
   275  			if err != nil {
   276  				return nil, [][]string{}, err
   277  			}
   278  		}
   279  	}
   280  	return t, allSub, nil
   281  }
   282  
   283  func getTemplate(root string, fs http.FileSystem, file string, others ...string) (t *template.Template, err error) {
   284  	t = template.New(file).Delims(BConfig.WebConfig.TemplateLeft, BConfig.WebConfig.TemplateRight).Funcs(beegoTplFuncMap)
   285  	var subMods [][]string
   286  	t, subMods, err = getTplDeep(root, fs, file, "", t)
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  	t, err = _getTemplate(t, root, fs, subMods, others...)
   291  
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  	return
   296  }
   297  
   298  func _getTemplate(t0 *template.Template, root string, fs http.FileSystem, subMods [][]string, others ...string) (t *template.Template, err error) {
   299  	t = t0
   300  	for _, m := range subMods {
   301  		if len(m) == 2 {
   302  			tpl := t.Lookup(m[1])
   303  			if tpl != nil {
   304  				continue
   305  			}
   306  			//first check filename
   307  			for _, otherFile := range others {
   308  				if otherFile == m[1] {
   309  					var subMods1 [][]string
   310  					t, subMods1, err = getTplDeep(root, fs, otherFile, "", t)
   311  					if err != nil {
   312  						logs.Trace("template parse file err:", err)
   313  					} else if len(subMods1) > 0 {
   314  						t, err = _getTemplate(t, root, fs, subMods1, others...)
   315  					}
   316  					break
   317  				}
   318  			}
   319  			//second check define
   320  			for _, otherFile := range others {
   321  				var data []byte
   322  				fileAbsPath := filepath.Join(root, otherFile)
   323  				f, err := fs.Open(fileAbsPath)
   324  				if err != nil {
   325  					f.Close()
   326  					logs.Trace("template file parse error, not success open file:", err)
   327  					continue
   328  				}
   329  				data, err = ioutil.ReadAll(f)
   330  				f.Close()
   331  				if err != nil {
   332  					logs.Trace("template file parse error, not success read file:", err)
   333  					continue
   334  				}
   335  				reg := regexp.MustCompile(BConfig.WebConfig.TemplateLeft + "[ ]*define[ ]+\"([^\"]+)\"")
   336  				allSub := reg.FindAllStringSubmatch(string(data), -1)
   337  				for _, sub := range allSub {
   338  					if len(sub) == 2 && sub[1] == m[1] {
   339  						var subMods1 [][]string
   340  						t, subMods1, err = getTplDeep(root, fs, otherFile, "", t)
   341  						if err != nil {
   342  							logs.Trace("template parse file err:", err)
   343  						} else if len(subMods1) > 0 {
   344  							t, err = _getTemplate(t, root, fs, subMods1, others...)
   345  							if err != nil {
   346  								logs.Trace("template parse file err:", err)
   347  							}
   348  						}
   349  						break
   350  					}
   351  				}
   352  			}
   353  		}
   354  
   355  	}
   356  	return
   357  }
   358  
   359  type templateFSFunc func() http.FileSystem
   360  
   361  func defaultFSFunc() http.FileSystem {
   362  	return FileSystem{}
   363  }
   364  
   365  // SetTemplateFSFunc set default filesystem function
   366  func SetTemplateFSFunc(fnt templateFSFunc) {
   367  	beeTemplateFS = fnt
   368  }
   369  
   370  // SetViewsPath sets view directory path in beego application.
   371  func SetViewsPath(path string) *App {
   372  	BConfig.WebConfig.ViewsPath = path
   373  	return BeeApp
   374  }
   375  
   376  // SetStaticPath sets static directory path and proper url pattern in beego application.
   377  // if beego.SetStaticPath("static","public"), visit /static/* to load static file in folder "public".
   378  func SetStaticPath(url string, path string) *App {
   379  	if !strings.HasPrefix(url, "/") {
   380  		url = "/" + url
   381  	}
   382  	if url != "/" {
   383  		url = strings.TrimRight(url, "/")
   384  	}
   385  	BConfig.WebConfig.StaticDir[url] = path
   386  	return BeeApp
   387  }
   388  
   389  // DelStaticPath removes the static folder setting in this url pattern in beego application.
   390  func DelStaticPath(url string) *App {
   391  	if !strings.HasPrefix(url, "/") {
   392  		url = "/" + url
   393  	}
   394  	if url != "/" {
   395  		url = strings.TrimRight(url, "/")
   396  	}
   397  	delete(BConfig.WebConfig.StaticDir, url)
   398  	return BeeApp
   399  }
   400  
   401  // AddTemplateEngine add a new templatePreProcessor which support extension
   402  func AddTemplateEngine(extension string, fn templatePreProcessor) *App {
   403  	AddTemplateExt(extension)
   404  	beeTemplateEngines[extension] = fn
   405  	return BeeApp
   406  }