mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01:00 
			
		
		
		
	None of the features of `unrolled/render` package is used. 
The Golang builtin "html/template" just works well. Then we can improve
our HTML render to resolve the "$.root.locale.Tr" problem as much as
possible.
Next step: we can have a template render pool (by Clone), then we can
inject global functions with dynamic context to every `Execute` calls.
Then we can use `{{Locale.Tr ....}}` directly in all templates , no need
to pass the `$.root.locale` again and again.
		
	
		
			
				
	
	
		
			273 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			273 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2022 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package templates
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"html/template"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"path/filepath"
 | |
| 	"regexp"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync/atomic"
 | |
| 
 | |
| 	"code.gitea.io/gitea/modules/log"
 | |
| 	"code.gitea.io/gitea/modules/setting"
 | |
| 	"code.gitea.io/gitea/modules/watcher"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	rendererKey interface{} = "templatesHtmlRenderer"
 | |
| 
 | |
| 	templateError    = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`)
 | |
| 	notDefinedError  = regexp.MustCompile(`^template: (.*):([0-9]+): function "(.*)" not defined`)
 | |
| 	unexpectedError  = regexp.MustCompile(`^template: (.*):([0-9]+): unexpected "(.*)" in operand`)
 | |
| 	expectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): expected end; found (.*)`)
 | |
| )
 | |
| 
 | |
| type HTMLRender struct {
 | |
| 	templates atomic.Pointer[template.Template]
 | |
| }
 | |
| 
 | |
| func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error {
 | |
| 	if respWriter, ok := w.(http.ResponseWriter); ok {
 | |
| 		if respWriter.Header().Get("Content-Type") == "" {
 | |
| 			respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
 | |
| 		}
 | |
| 		respWriter.WriteHeader(status)
 | |
| 	}
 | |
| 	return h.templates.Load().ExecuteTemplate(w, name, data)
 | |
| }
 | |
| 
 | |
| func (h *HTMLRender) TemplateLookup(t string) *template.Template {
 | |
| 	return h.templates.Load().Lookup(t)
 | |
| }
 | |
| 
 | |
| func (h *HTMLRender) CompileTemplates() error {
 | |
| 	dirPrefix := "templates/"
 | |
| 	tmpls := template.New("")
 | |
| 	for _, path := range GetTemplateAssetNames() {
 | |
| 		name := path[len(dirPrefix):]
 | |
| 		name = strings.TrimSuffix(name, ".tmpl")
 | |
| 		tmpl := tmpls.New(filepath.ToSlash(name))
 | |
| 		for _, fm := range NewFuncMap() {
 | |
| 			tmpl.Funcs(fm)
 | |
| 		}
 | |
| 		buf, err := GetAsset(path)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		if _, err = tmpl.Parse(string(buf)); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	h.templates.Store(tmpls)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // HTMLRenderer returns the current html renderer for the context or creates and stores one within the context for future use
 | |
| func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
 | |
| 	if renderer, ok := ctx.Value(rendererKey).(*HTMLRender); ok {
 | |
| 		return ctx, renderer
 | |
| 	}
 | |
| 
 | |
| 	rendererType := "static"
 | |
| 	if !setting.IsProd {
 | |
| 		rendererType = "auto-reloading"
 | |
| 	}
 | |
| 	log.Log(1, log.DEBUG, "Creating "+rendererType+" HTML Renderer")
 | |
| 
 | |
| 	renderer := &HTMLRender{}
 | |
| 	if err := renderer.CompileTemplates(); err != nil {
 | |
| 		handleFatalError(err)
 | |
| 	}
 | |
| 	if !setting.IsProd {
 | |
| 		watcher.CreateWatcher(ctx, "HTML Templates", &watcher.CreateWatcherOpts{
 | |
| 			PathsCallback: walkTemplateFiles,
 | |
| 			BetweenCallback: func() {
 | |
| 				if err := renderer.CompileTemplates(); err != nil {
 | |
| 					log.Error("Template error: %v\n%s", err, log.Stack(2))
 | |
| 				}
 | |
| 			},
 | |
| 		})
 | |
| 	}
 | |
| 	return context.WithValue(ctx, rendererKey, renderer), renderer
 | |
| }
 | |
| 
 | |
| func handleFatalError(err error) {
 | |
| 	wrapFatal(handleNotDefinedPanicError(err))
 | |
| 	wrapFatal(handleUnexpected(err))
 | |
| 	wrapFatal(handleExpectedEnd(err))
 | |
| 	wrapFatal(handleGenericTemplateError(err))
 | |
| }
 | |
| 
 | |
| func wrapFatal(format string, args []interface{}) {
 | |
| 	if format == "" {
 | |
| 		return
 | |
| 	}
 | |
| 	log.FatalWithSkip(1, format, args...)
 | |
| }
 | |
| 
 | |
| func handleGenericTemplateError(err error) (string, []interface{}) {
 | |
| 	groups := templateError.FindStringSubmatch(err.Error())
 | |
| 	if len(groups) != 4 {
 | |
| 		return "", nil
 | |
| 	}
 | |
| 
 | |
| 	templateName, lineNumberStr, message := groups[1], groups[2], groups[3]
 | |
| 
 | |
| 	filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
 | |
| 	if assetErr != nil {
 | |
| 		return "", nil
 | |
| 	}
 | |
| 
 | |
| 	lineNumber, _ := strconv.Atoi(lineNumberStr)
 | |
| 
 | |
| 	line := GetLineFromTemplate(templateName, lineNumber, "", -1)
 | |
| 
 | |
| 	return "PANIC: Unable to compile templates!\n%s in template file %s at line %d:\n\n%s\nStacktrace:\n\n%s", []interface{}{message, filename, lineNumber, log.NewColoredValue(line, log.Reset), log.Stack(2)}
 | |
| }
 | |
| 
 | |
| func handleNotDefinedPanicError(err error) (string, []interface{}) {
 | |
| 	groups := notDefinedError.FindStringSubmatch(err.Error())
 | |
| 	if len(groups) != 4 {
 | |
| 		return "", nil
 | |
| 	}
 | |
| 
 | |
| 	templateName, lineNumberStr, functionName := groups[1], groups[2], groups[3]
 | |
| 
 | |
| 	functionName, _ = strconv.Unquote(`"` + functionName + `"`)
 | |
| 
 | |
| 	filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
 | |
| 	if assetErr != nil {
 | |
| 		return "", nil
 | |
| 	}
 | |
| 
 | |
| 	lineNumber, _ := strconv.Atoi(lineNumberStr)
 | |
| 
 | |
| 	line := GetLineFromTemplate(templateName, lineNumber, functionName, -1)
 | |
| 
 | |
| 	return "PANIC: Unable to compile templates!\nUndefined function %q in template file %s at line %d:\n\n%s", []interface{}{functionName, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
 | |
| }
 | |
| 
 | |
| func handleUnexpected(err error) (string, []interface{}) {
 | |
| 	groups := unexpectedError.FindStringSubmatch(err.Error())
 | |
| 	if len(groups) != 4 {
 | |
| 		return "", nil
 | |
| 	}
 | |
| 
 | |
| 	templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
 | |
| 	unexpected, _ = strconv.Unquote(`"` + unexpected + `"`)
 | |
| 
 | |
| 	filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
 | |
| 	if assetErr != nil {
 | |
| 		return "", nil
 | |
| 	}
 | |
| 
 | |
| 	lineNumber, _ := strconv.Atoi(lineNumberStr)
 | |
| 
 | |
| 	line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
 | |
| 
 | |
| 	return "PANIC: Unable to compile templates!\nUnexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
 | |
| }
 | |
| 
 | |
| func handleExpectedEnd(err error) (string, []interface{}) {
 | |
| 	groups := expectedEndError.FindStringSubmatch(err.Error())
 | |
| 	if len(groups) != 4 {
 | |
| 		return "", nil
 | |
| 	}
 | |
| 
 | |
| 	templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3]
 | |
| 
 | |
| 	filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl")
 | |
| 	if assetErr != nil {
 | |
| 		return "", nil
 | |
| 	}
 | |
| 
 | |
| 	lineNumber, _ := strconv.Atoi(lineNumberStr)
 | |
| 
 | |
| 	line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1)
 | |
| 
 | |
| 	return "PANIC: Unable to compile templates!\nMissing end with unexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)}
 | |
| }
 | |
| 
 | |
| const dashSeparator = "----------------------------------------------------------------------\n"
 | |
| 
 | |
| // GetLineFromTemplate returns a line from a template with some context
 | |
| func GetLineFromTemplate(templateName string, targetLineNum int, target string, position int) string {
 | |
| 	bs, err := GetAsset("templates/" + templateName + ".tmpl")
 | |
| 	if err != nil {
 | |
| 		return fmt.Sprintf("(unable to read template file: %v)", err)
 | |
| 	}
 | |
| 
 | |
| 	sb := &strings.Builder{}
 | |
| 
 | |
| 	// Write the header
 | |
| 	sb.WriteString(dashSeparator)
 | |
| 
 | |
| 	var lineBs []byte
 | |
| 
 | |
| 	// Iterate through the lines from the asset file to find the target line
 | |
| 	for start, currentLineNum := 0, 1; currentLineNum <= targetLineNum && start < len(bs); currentLineNum++ {
 | |
| 		// Find the next new line
 | |
| 		end := bytes.IndexByte(bs[start:], '\n')
 | |
| 
 | |
| 		// adjust the end to be a direct pointer in to []byte
 | |
| 		if end < 0 {
 | |
| 			end = len(bs)
 | |
| 		} else {
 | |
| 			end += start
 | |
| 		}
 | |
| 
 | |
| 		// set lineBs to the current line []byte
 | |
| 		lineBs = bs[start:end]
 | |
| 
 | |
| 		// move start to after the current new line position
 | |
| 		start = end + 1
 | |
| 
 | |
| 		// Write 2 preceding lines + the target line
 | |
| 		if targetLineNum-currentLineNum < 3 {
 | |
| 			_, _ = sb.Write(lineBs)
 | |
| 			_ = sb.WriteByte('\n')
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// If there is a provided target to look for in the line add a pointer to it
 | |
| 	// e.g.                                                        ^^^^^^^
 | |
| 	if target != "" {
 | |
| 		targetPos := bytes.Index(lineBs, []byte(target))
 | |
| 		if targetPos >= 0 {
 | |
| 			position = targetPos
 | |
| 		}
 | |
| 	}
 | |
| 	if position >= 0 {
 | |
| 		// take the current line and replace preceding text with whitespace (except for tab)
 | |
| 		for i := range lineBs[:position] {
 | |
| 			if lineBs[i] != '\t' {
 | |
| 				lineBs[i] = ' '
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// write the preceding "space"
 | |
| 		_, _ = sb.Write(lineBs[:position])
 | |
| 
 | |
| 		// Now write the ^^ pointer
 | |
| 		targetLen := len(target)
 | |
| 		if targetLen == 0 {
 | |
| 			targetLen = 1
 | |
| 		}
 | |
| 		_, _ = sb.WriteString(strings.Repeat("^", targetLen))
 | |
| 		_ = sb.WriteByte('\n')
 | |
| 	}
 | |
| 
 | |
| 	// Finally write the footer
 | |
| 	sb.WriteString(dashSeparator)
 | |
| 
 | |
| 	return sb.String()
 | |
| }
 |