1 // Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
2 // Revel Framework source code and usage is governed by a MIT style
3 // license that can be found in the LICENSE file.
22 // ErrorCSSClass httml CSS error class name
23 var ErrorCSSClass = "hasError"
25 // TemplateLoader object handles loading and parsing of templates.
26 // Everything below the application's views directory is treated as a template.
27 type TemplateLoader struct {
28 // Paths to search for templates, in priority order.
30 // load version seed for templates
32 // A templateRuntime of looked up template results
33 runtimeLoader atomic.Value
34 // Lock to prevent concurrent map writes
35 templateMutex sync.Mutex
38 type Template interface {
39 // The name of the template.
40 Name() string // Name of template
41 // The content of the template as a string (Used in error handling).
42 Content() []string // Content
43 // Called by the server to render the template out the io.Writer, context contains the view args to be passed to the template.
44 Render(wr io.Writer, context interface{}) error
45 // The full path to the file on the disk.
46 Location() string // Disk location
49 var invalidSlugPattern = regexp.MustCompile(`[^a-z0-9 _-]`)
50 var whiteSpacePattern = regexp.MustCompile(`\s+`)
51 var templateLog = RevelLog.New("section", "template")
53 // TemplateOutputArgs returns the result of the template rendered using the passed in arguments.
54 func TemplateOutputArgs(templatePath string, args map[string]interface{}) (data []byte, err error) {
56 lang, _ := args[CurrentLocaleViewArg].(string)
57 template, err := MainTemplateLoader.TemplateLang(templatePath, lang)
61 tr := &RenderTemplateResult{
65 b, err := tr.ToBytes()
72 func NewTemplateLoader(paths []string) *TemplateLoader {
73 loader := &TemplateLoader{
79 // WatchDir returns true of directory doesn't start with . (dot)
81 func (loader *TemplateLoader) WatchDir(info os.FileInfo) bool {
82 // Watch all directories, except the ones starting with a dot.
83 return !strings.HasPrefix(info.Name(), ".")
86 // WatchFile returns true of file doesn't start with . (dot)
88 func (loader *TemplateLoader) WatchFile(basename string) bool {
89 // Watch all files, except the ones starting with a dot.
90 return !strings.HasPrefix(basename, ".")
93 // DEPRECATED Use TemplateLang, will be removed in future release
94 func (loader *TemplateLoader) Template(name string) (tmpl Template, err error) {
95 runtimeLoader := loader.runtimeLoader.Load().(*templateRuntime)
96 return runtimeLoader.TemplateLang(name, "")
99 func (loader *TemplateLoader) TemplateLang(name, lang string) (tmpl Template, err error) {
100 runtimeLoader := loader.runtimeLoader.Load().(*templateRuntime)
101 return runtimeLoader.TemplateLang(name, lang)
104 // Refresh method scans the views directory and parses all templates as Go Templates.
105 // If a template fails to parse, the error is set on the loader.
106 // (It's awkward to refresh a single Go Template)
107 func (loader *TemplateLoader) Refresh() (err *Error) {
108 loader.templateMutex.Lock()
109 defer loader.templateMutex.Unlock()
111 loader.loadVersionSeed++
112 runtimeLoader := &templateRuntime{loader: loader,
113 version: loader.loadVersionSeed,
114 templateMap: map[string]Template{}}
116 templateLog.Debug("Refresh: Refreshing templates from ", "path", loader.paths)
117 if err = loader.initializeEngines(runtimeLoader, Config.StringDefault("template.engines", GO_TEMPLATE)); err != nil {
120 for _, engine := range runtimeLoader.templatesAndEngineList {
121 engine.Event(TEMPLATE_REFRESH_REQUESTED, nil)
123 RaiseEvent(TEMPLATE_REFRESH_REQUESTED, nil)
125 for _, engine := range runtimeLoader.templatesAndEngineList {
126 engine.Event(TEMPLATE_REFRESH_COMPLETED, nil)
128 RaiseEvent(TEMPLATE_REFRESH_COMPLETED, nil)
130 // Reset the runtimeLoader
131 loader.runtimeLoader.Store(runtimeLoader)
134 // Resort the paths, make sure the revel path is the last path,
135 // so anything can override it
136 revelTemplatePath := filepath.Join(RevelPath, "templates")
137 // Go through the paths
138 for i, o := range loader.paths {
139 if o == revelTemplatePath && i != len(loader.paths)-1 {
140 loader.paths[i] = loader.paths[len(loader.paths)-1]
141 loader.paths[len(loader.paths)-1] = revelTemplatePath
144 templateLog.Debug("Refresh: Refreshing templates from", "path", loader.paths)
146 runtimeLoader.compileError = nil
147 runtimeLoader.TemplatePaths = map[string]string{}
149 for _, basePath := range loader.paths {
150 // Walk only returns an error if the template loader is completely unusable
151 // (namely, if one of the TemplateFuncs does not have an acceptable signature).
153 // Handling symlinked directories
154 var fullSrcDir string
155 f, err := os.Lstat(basePath)
156 if err == nil && f.Mode()&os.ModeSymlink == os.ModeSymlink {
157 fullSrcDir, err = filepath.EvalSymlinks(basePath)
159 templateLog.Panic("Refresh: Eval symlinks error ", "error", err)
162 fullSrcDir = basePath
165 var templateWalker filepath.WalkFunc
167 templateWalker = func(path string, info os.FileInfo, err error) error {
169 templateLog.Error("Refresh: error walking templates:", "error", err)
173 // Walk into watchable directories
175 if !loader.WatchDir(info) {
176 return filepath.SkipDir
181 // Only add watchable
182 if !loader.WatchFile(info.Name()) {
186 fileBytes, err := runtimeLoader.findAndAddTemplate(path, fullSrcDir, basePath)
188 // Add in this template name to the list of templates unable to be compiled
189 runtimeLoader.compileErrorNameList = append(runtimeLoader.compileErrorNameList, filepath.ToSlash(path[len(fullSrcDir)+1:]))
191 // Store / report the first error encountered.
192 if err != nil && runtimeLoader.compileError == nil {
193 runtimeLoader.compileError, _ = err.(*Error)
195 if nil == runtimeLoader.compileError {
196 _, line, description := ParseTemplateError(err)
198 runtimeLoader.compileError = &Error{
199 Title: "Template Compilation Error",
201 Description: description,
203 SourceLines: strings.Split(string(fileBytes), "\n"),
206 templateLog.Errorf("Refresh: Template compilation error (In %s around line %d):\n\t%s",
207 path, runtimeLoader.compileError.Line, err.Error())
208 } else if nil != err { //&& strings.HasPrefix(templateName, "errors/") {
210 if compileError, ok := err.(*Error); ok {
211 templateLog.Errorf("Template compilation error (In %s around line %d):\n\t%s",
212 path, compileError.Line, err.Error())
214 templateLog.Errorf("Template compilation error (In %s ):\n\t%s",
221 if _, err = os.Lstat(fullSrcDir); os.IsNotExist(err) {
222 // #1058 Given views/template path is not exists
223 // so no need to walk, move on to next path
227 funcErr := Walk(fullSrcDir, templateWalker)
229 // If there was an error with the Funcs, set it and return immediately.
231 runtimeLoader.compileError = NewErrorFromPanic(funcErr)
232 return runtimeLoader.compileError
236 // Note: compileError may or may not be set.
237 return runtimeLoader.compileError
240 type templateRuntime struct {
241 loader *TemplateLoader
242 // load version for templates
244 // Template data and implementation
245 templatesAndEngineList []TemplateEngine
246 // If an error was encountered parsing the templates, it is stored here.
248 // A list of the names of the templates with errors
249 compileErrorNameList []string
250 // Map from template name to the path from whence it was loaded.
251 TemplatePaths map[string]string
252 // A map of looked up template results
253 templateMap map[string]Template
256 // Checks to see if template exists in templatePaths, if so it is skipped (templates are imported in order
257 // reads the template file into memory, replaces namespace keys with module (if found
258 func (runtimeLoader *templateRuntime) findAndAddTemplate(path, fullSrcDir, basePath string) (fileBytes []byte, err error) {
259 templateName := filepath.ToSlash(path[len(fullSrcDir)+1:])
260 // Convert template names to use forward slashes, even on Windows.
261 if os.PathSeparator == '\\' {
262 templateName = strings.Replace(templateName, `\`, `/`, -1) // `
265 // Check to see if template was found
266 if place, found := runtimeLoader.TemplatePaths[templateName]; found {
267 templateLog.Debug("findAndAddTemplate: Not Loading, template is already exists: ", "name", templateName, "old",
272 fileBytes, err = ioutil.ReadFile(path)
274 templateLog.Error("findAndAddTemplate: Failed reading file:", "path", path, "error", err)
277 // Parse template file and replace the "_LOCAL_|" in the template with the module name
278 // allow for namespaces to be renamed "_LOCAL_(.*?)|"
279 if module := ModuleFromPath(path, false); module != nil {
280 fileBytes = namespaceReplace(fileBytes, module)
283 // if we have an engine picked for this template process it now
284 baseTemplate := NewBaseTemplate(templateName, path, basePath, fileBytes)
286 // Try to find a default engine for the file
287 for _, engine := range runtimeLoader.templatesAndEngineList {
288 if engine.Handles(baseTemplate) {
289 _, err = runtimeLoader.loadIntoEngine(engine, baseTemplate)
294 // Try all engines available
295 var defaultError error
296 for _, engine := range runtimeLoader.templatesAndEngineList {
297 if loaded, loaderr := runtimeLoader.loadIntoEngine(engine, baseTemplate); loaded {
300 templateLog.Debugf("findAndAddTemplate: Engine '%s' unable to compile %s %s", engine.Name(), path, loaderr.Error())
301 if defaultError == nil {
302 defaultError = loaderr
307 // Assign the error from the first parser
310 // No engines could be found return the err
312 err = fmt.Errorf("Failed to parse template file using engines %s", path)
318 func (runtimeLoader *templateRuntime) loadIntoEngine(engine TemplateEngine, baseTemplate *TemplateView) (loaded bool, err error) {
319 if loadedTemplate, found := runtimeLoader.templateMap[baseTemplate.TemplateName]; found {
320 // Duplicate template found in map
321 templateLog.Debug("template already exists in map: ", baseTemplate.TemplateName, " in engine ", engine.Name(), "\r\n\told file:",
322 loadedTemplate.Location(), "\r\n\tnew file:", baseTemplate.FilePath)
326 if loadedTemplate := engine.Lookup(baseTemplate.TemplateName); loadedTemplate != nil {
327 // Duplicate template found for engine
328 templateLog.Debug("loadIntoEngine: template already exists: ", "template", baseTemplate.TemplateName, "inengine ", engine.Name(), "old",
329 loadedTemplate.Location(), "new", baseTemplate.FilePath)
333 if err = engine.ParseAndAdd(baseTemplate); err == nil {
334 if tmpl := engine.Lookup(baseTemplate.TemplateName); tmpl != nil {
335 runtimeLoader.templateMap[baseTemplate.TemplateName] = tmpl
337 runtimeLoader.TemplatePaths[baseTemplate.TemplateName] = baseTemplate.FilePath
338 templateLog.Debugf("loadIntoEngine:Engine '%s' compiled %s", engine.Name(), baseTemplate.FilePath)
341 templateLog.Debug("loadIntoEngine: Engine failed to compile", "engine", engine.Name(), "file", baseTemplate.FilePath, "error", err)
346 // Parse the line, and description from an error message like:
347 // html/template:Application/Register.html:36: no such template "footer.html"
348 func ParseTemplateError(err error) (templateName string, line int, description string) {
349 if e, ok := err.(*Error); ok {
350 return "", e.Line, e.Description
353 description = err.Error()
354 i := regexp.MustCompile(`:\d+:`).FindStringIndex(description)
356 line, err = strconv.Atoi(description[i[0]+1 : i[1]-1])
358 templateLog.Error("ParseTemplateError: Failed to parse line number from error message:", "error", err)
360 templateName = description[:i[0]]
361 if colon := strings.Index(templateName, ":"); colon != -1 {
362 templateName = templateName[colon+1:]
364 templateName = strings.TrimSpace(templateName)
365 description = description[i[1]+1:]
367 return templateName, line, description
370 // Template returns the Template with the given name. The name is the template's path
371 // relative to a template loader root.
373 // An Error is returned if there was any problem with any of the templates. (In
374 // this case, if a template is returned, it may still be usable.)
375 func (runtimeLoader *templateRuntime) TemplateLang(name, lang string) (tmpl Template, err error) {
376 if runtimeLoader.compileError != nil {
377 for _, errName := range runtimeLoader.compileErrorNameList {
379 return nil, runtimeLoader.compileError
384 // Fetch the template from the map
385 tmpl = runtimeLoader.templateLoad(name, lang)
387 err = fmt.Errorf("Template %s not found.", name)
393 // Load and also updates map if name is not found (to speed up next lookup)
394 func (runtimeLoader *templateRuntime) templateLoad(name, lang string) (tmpl Template) {
398 // Look up and return the template.
399 langName = name + "." + lang
400 tmpl, found = runtimeLoader.templateMap[langName]
404 tmpl, found = runtimeLoader.templateMap[name]
406 tmpl, found = runtimeLoader.templateMap[name]
413 // Neither name is found
414 // Look up and return the template.
415 for _, engine := range runtimeLoader.templatesAndEngineList {
416 if tmpl = engine.Lookup(langName); tmpl != nil {
420 if tmpl = engine.Lookup(name); tmpl != nil {
430 // If we found anything store it in the map, we need to copy so we do not
431 // run into concurrency issues
432 runtimeLoader.loader.templateMutex.Lock()
433 defer runtimeLoader.loader.templateMutex.Unlock()
435 // In case another thread has loaded the map, reload the atomic value and check
436 newRuntimeLoader := runtimeLoader.loader.runtimeLoader.Load().(*templateRuntime)
437 if newRuntimeLoader.version != runtimeLoader.version {
441 newTemplateMap := map[string]Template{}
442 for k, v := range newRuntimeLoader.templateMap {
443 newTemplateMap[k] = v
445 newTemplateMap[langName] = tmpl
446 if _, found := newTemplateMap[name]; !found {
447 newTemplateMap[name] = tmpl
449 runtimeCopy := &templateRuntime{}
450 *runtimeCopy = *newRuntimeLoader
451 runtimeCopy.templateMap = newTemplateMap
453 // Set the atomic value
454 runtimeLoader.loader.runtimeLoader.Store(runtimeCopy)
458 func (i *TemplateView) Location() string {
462 func (i *TemplateView) Content() (content []string) {
463 if i.FileBytes != nil {
465 buffer := bytes.NewBuffer(i.FileBytes)
466 reader := bufio.NewScanner(buffer)
468 content = append(content, string(reader.Bytes()))
475 func NewBaseTemplate(templateName, filePath, basePath string, fileBytes []byte) *TemplateView {
476 return &TemplateView{TemplateName: templateName, FilePath: filePath, FileBytes: fileBytes, BasePath: basePath}