mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-03 21:08:25 +00:00 
			
		
		
		
	Change all license headers to comply with REUSE specification. Fix #16132 Co-authored-by: flynnnnnnnnnn <flynnnnnnnnnn@github> Co-authored-by: John Olheiser <john.olheiser@gmail.com>
		
			
				
	
	
		
			249 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			249 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package templates
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"context"
 | 
						|
	"fmt"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"code.gitea.io/gitea/modules/log"
 | 
						|
	"code.gitea.io/gitea/modules/setting"
 | 
						|
	"code.gitea.io/gitea/modules/watcher"
 | 
						|
 | 
						|
	"github.com/unrolled/render"
 | 
						|
)
 | 
						|
 | 
						|
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 (.*)`)
 | 
						|
)
 | 
						|
 | 
						|
// 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, *render.Render) {
 | 
						|
	rendererInterface := ctx.Value(rendererKey)
 | 
						|
	if rendererInterface != nil {
 | 
						|
		renderer, ok := rendererInterface.(*render.Render)
 | 
						|
		if ok {
 | 
						|
			return ctx, renderer
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	rendererType := "static"
 | 
						|
	if !setting.IsProd {
 | 
						|
		rendererType = "auto-reloading"
 | 
						|
	}
 | 
						|
	log.Log(1, log.DEBUG, "Creating "+rendererType+" HTML Renderer")
 | 
						|
 | 
						|
	compilingTemplates := true
 | 
						|
	defer func() {
 | 
						|
		if !compilingTemplates {
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		panicked := recover()
 | 
						|
		if panicked == nil {
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		// OK try to handle the panic...
 | 
						|
		err, ok := panicked.(error)
 | 
						|
		if ok {
 | 
						|
			handlePanicError(err)
 | 
						|
		}
 | 
						|
		log.Fatal("PANIC: Unable to compile templates!\n%v\n\nStacktrace:\n%s", panicked, log.Stack(2))
 | 
						|
	}()
 | 
						|
 | 
						|
	renderer := render.New(render.Options{
 | 
						|
		Extensions:                []string{".tmpl"},
 | 
						|
		Directory:                 "templates",
 | 
						|
		Funcs:                     NewFuncMap(),
 | 
						|
		Asset:                     GetAsset,
 | 
						|
		AssetNames:                GetTemplateAssetNames,
 | 
						|
		UseMutexLock:              !setting.IsProd,
 | 
						|
		IsDevelopment:             false,
 | 
						|
		DisableHTTPErrorRendering: true,
 | 
						|
	})
 | 
						|
	compilingTemplates = false
 | 
						|
	if !setting.IsProd {
 | 
						|
		watcher.CreateWatcher(ctx, "HTML Templates", &watcher.CreateWatcherOpts{
 | 
						|
			PathsCallback:   walkTemplateFiles,
 | 
						|
			BetweenCallback: renderer.CompileTemplates,
 | 
						|
		})
 | 
						|
	}
 | 
						|
	return context.WithValue(ctx, rendererKey, renderer), renderer
 | 
						|
}
 | 
						|
 | 
						|
func handlePanicError(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 := getLineFromAsset(templateName, lineNumber, "")
 | 
						|
 | 
						|
	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 := getLineFromAsset(templateName, lineNumber, functionName)
 | 
						|
 | 
						|
	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 := getLineFromAsset(templateName, lineNumber, unexpected)
 | 
						|
 | 
						|
	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 := getLineFromAsset(templateName, lineNumber, unexpected)
 | 
						|
 | 
						|
	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"
 | 
						|
 | 
						|
func getLineFromAsset(templateName string, targetLineNum int, target string) 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 != "" {
 | 
						|
		idx := bytes.Index(lineBs, []byte(target))
 | 
						|
 | 
						|
		if idx >= 0 {
 | 
						|
			// take the current line and replace preceding text with whitespace (except for tab)
 | 
						|
			for i := range lineBs[:idx] {
 | 
						|
				if lineBs[i] != '\t' {
 | 
						|
					lineBs[i] = ' '
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			// write the preceding "space"
 | 
						|
			_, _ = sb.Write(lineBs[:idx])
 | 
						|
 | 
						|
			// Now write the ^^ pointer
 | 
						|
			_, _ = sb.WriteString(strings.Repeat("^", len(target)))
 | 
						|
			_ = sb.WriteByte('\n')
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Finally write the footer
 | 
						|
	sb.WriteString(dashSeparator)
 | 
						|
 | 
						|
	return sb.String()
 | 
						|
}
 |