mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 12:26:05 +01:00 
			
		
		
		
	Group template helper functions, remove Printf, improve template error messages (#23982)
				
					
				
			Follow #23328 Major changes: * Group the function in `templates/help.go` by their purposes. It could make future work easier. * Remove the `Printf` helper function, there is already a builtin `printf`. * Remove `DiffStatsWidth`, replace with `Eval` in template * Rename the `NewTextFuncMap` to `mailSubjectTextFuncMap`, it's for subject text template only, no need to make it support HTML functions. ---- And fine tune template error messages, to make it more friendly to developers and users.   --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
		@@ -47,7 +47,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Render represents a template render
 | 
					// Render represents a template render
 | 
				
			||||||
type Render interface {
 | 
					type Render interface {
 | 
				
			||||||
	TemplateLookup(tmpl string) *template.Template
 | 
						TemplateLookup(tmpl string) (*template.Template, error)
 | 
				
			||||||
	HTML(w io.Writer, status int, name string, data interface{}) error
 | 
						HTML(w io.Writer, status int, name string, data interface{}) error
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -228,7 +228,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil {
 | 
						if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil {
 | 
				
			||||||
		if status == http.StatusInternalServerError && name == base.TplName("status/500") {
 | 
							if status == http.StatusInternalServerError && name == base.TplName("status/500") {
 | 
				
			||||||
			ctx.PlainText(http.StatusInternalServerError, "Unable to find status/500 template")
 | 
								ctx.PlainText(http.StatusInternalServerError, "Unable to find HTML templates, the template system is not initialized, or Gitea can't find your template files.")
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if execErr, ok := err.(texttemplate.ExecError); ok {
 | 
							if execErr, ok := err.(texttemplate.ExecError); ok {
 | 
				
			||||||
@@ -247,7 +247,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
 | 
				
			|||||||
				if errorTemplateName != string(name) {
 | 
									if errorTemplateName != string(name) {
 | 
				
			||||||
					filename += " (subtemplate of " + string(name) + ")"
 | 
										filename += " (subtemplate of " + string(name) + ")"
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				err = fmt.Errorf("%w\nin template file %s:\n%s", err, filename, templates.GetLineFromTemplate(errorTemplateName, line, target, pos))
 | 
									err = fmt.Errorf("failed to render %s, error: %w:\n%s", filename, err, templates.GetLineFromTemplate(errorTemplateName, line, target, pos))
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				filename, filenameErr := templates.GetAssetFilename("templates/" + execErr.Name + ".tmpl")
 | 
									filename, filenameErr := templates.GetAssetFilename("templates/" + execErr.Name + ".tmpl")
 | 
				
			||||||
				if filenameErr != nil {
 | 
									if filenameErr != nil {
 | 
				
			||||||
@@ -256,7 +256,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
 | 
				
			|||||||
				if execErr.Name != string(name) {
 | 
									if execErr.Name != string(name) {
 | 
				
			||||||
					filename += " (subtemplate of " + string(name) + ")"
 | 
										filename += " (subtemplate of " + string(name) + ")"
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				err = fmt.Errorf("%w\nin template file %s", err, filename)
 | 
									err = fmt.Errorf("failed to render %s, error: %w", filename, err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		ctx.ServerError("Render failed", err)
 | 
							ctx.ServerError("Render failed", err)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,20 +6,13 @@
 | 
				
			|||||||
package templates
 | 
					package templates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"html/template"
 | 
					 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
	texttmpl "text/template"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	subjectTemplates = texttmpl.New("")
 | 
					 | 
				
			||||||
	bodyTemplates    = template.New("")
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// GetAsset returns asset content via name
 | 
					// GetAsset returns asset content via name
 | 
				
			||||||
func GetAsset(name string) ([]byte, error) {
 | 
					func GetAsset(name string) ([]byte, error) {
 | 
				
			||||||
	bs, err := os.ReadFile(filepath.Join(setting.CustomPath, name))
 | 
						bs, err := os.ReadFile(filepath.Join(setting.CustomPath, name))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,6 @@ import (
 | 
				
			|||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	texttmpl "text/template"
 | 
					 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
	"unicode"
 | 
						"unicode"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -55,6 +54,134 @@ var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
 | 
				
			|||||||
// NewFuncMap returns functions for injecting to templates
 | 
					// NewFuncMap returns functions for injecting to templates
 | 
				
			||||||
func NewFuncMap() []template.FuncMap {
 | 
					func NewFuncMap() []template.FuncMap {
 | 
				
			||||||
	return []template.FuncMap{map[string]interface{}{
 | 
						return []template.FuncMap{map[string]interface{}{
 | 
				
			||||||
 | 
							// -----------------------------------------------------------------
 | 
				
			||||||
 | 
							// html/template related functions
 | 
				
			||||||
 | 
							"dict":        dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
 | 
				
			||||||
 | 
							"Eval":        Eval,
 | 
				
			||||||
 | 
							"Safe":        Safe,
 | 
				
			||||||
 | 
							"Escape":      html.EscapeString,
 | 
				
			||||||
 | 
							"QueryEscape": url.QueryEscape,
 | 
				
			||||||
 | 
							"JSEscape":    template.JSEscapeString,
 | 
				
			||||||
 | 
							"Str2html":    Str2html, // TODO: rename it to SanitizeHTML
 | 
				
			||||||
 | 
							"URLJoin":     util.URLJoin,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							"PathEscape":         url.PathEscape,
 | 
				
			||||||
 | 
							"PathEscapeSegments": util.PathEscapeSegments,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// -----------------------------------------------------------------
 | 
				
			||||||
 | 
							// string / json
 | 
				
			||||||
 | 
							"Join":           strings.Join,
 | 
				
			||||||
 | 
							"DotEscape":      DotEscape,
 | 
				
			||||||
 | 
							"HasPrefix":      strings.HasPrefix,
 | 
				
			||||||
 | 
							"EllipsisString": base.EllipsisString,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							"Json": func(in interface{}) string {
 | 
				
			||||||
 | 
								out, err := json.Marshal(in)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return ""
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return string(out)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"JsonPrettyPrint": func(in string) string {
 | 
				
			||||||
 | 
								var out bytes.Buffer
 | 
				
			||||||
 | 
								err := json.Indent(&out, []byte(in), "", "  ")
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return ""
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return out.String()
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// -----------------------------------------------------------------
 | 
				
			||||||
 | 
							// svg / avatar / icon
 | 
				
			||||||
 | 
							"svg":            svg.RenderHTML,
 | 
				
			||||||
 | 
							"avatar":         Avatar,
 | 
				
			||||||
 | 
							"avatarHTML":     AvatarHTML,
 | 
				
			||||||
 | 
							"avatarByAction": AvatarByAction,
 | 
				
			||||||
 | 
							"avatarByEmail":  AvatarByEmail,
 | 
				
			||||||
 | 
							"repoAvatar":     RepoAvatar,
 | 
				
			||||||
 | 
							"EntryIcon":      base.EntryIcon,
 | 
				
			||||||
 | 
							"MigrationIcon":  MigrationIcon,
 | 
				
			||||||
 | 
							"ActionIcon":     ActionIcon,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							"SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
 | 
				
			||||||
 | 
								// if needed
 | 
				
			||||||
 | 
								if len(normSort) == 0 || len(urlSort) == 0 {
 | 
				
			||||||
 | 
									return ""
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if len(urlSort) == 0 && isDefault {
 | 
				
			||||||
 | 
									// if sort is sorted as default add arrow tho this table header
 | 
				
			||||||
 | 
									if isDefault {
 | 
				
			||||||
 | 
										return svg.RenderHTML("octicon-triangle-down", 16)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									// if sort arg is in url test if it correlates with column header sort arguments
 | 
				
			||||||
 | 
									// the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
 | 
				
			||||||
 | 
									if urlSort == normSort {
 | 
				
			||||||
 | 
										// the table is sorted with this header normal
 | 
				
			||||||
 | 
										return svg.RenderHTML("octicon-triangle-up", 16)
 | 
				
			||||||
 | 
									} else if urlSort == revSort {
 | 
				
			||||||
 | 
										// the table is sorted with this header reverse
 | 
				
			||||||
 | 
										return svg.RenderHTML("octicon-triangle-down", 16)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// the table is NOT sorted with this header
 | 
				
			||||||
 | 
								return ""
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// -----------------------------------------------------------------
 | 
				
			||||||
 | 
							// time / number / format
 | 
				
			||||||
 | 
							"FileSize":      base.FileSize,
 | 
				
			||||||
 | 
							"LocaleNumber":  LocaleNumber,
 | 
				
			||||||
 | 
							"CountFmt":      base.FormatNumberSI,
 | 
				
			||||||
 | 
							"TimeSince":     timeutil.TimeSince,
 | 
				
			||||||
 | 
							"TimeSinceUnix": timeutil.TimeSinceUnix,
 | 
				
			||||||
 | 
							"Sec2Time":      util.SecToTime,
 | 
				
			||||||
 | 
							"DateFmtLong": func(t time.Time) string {
 | 
				
			||||||
 | 
								return t.Format(time.RFC1123Z)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"LoadTimes": func(startTime time.Time) string {
 | 
				
			||||||
 | 
								return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// -----------------------------------------------------------------
 | 
				
			||||||
 | 
							// slice
 | 
				
			||||||
 | 
							"containGeneric": func(arr, v interface{}) bool {
 | 
				
			||||||
 | 
								arrV := reflect.ValueOf(arr)
 | 
				
			||||||
 | 
								if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String {
 | 
				
			||||||
 | 
									return strings.Contains(arr.(string), v.(string))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if arrV.Kind() == reflect.Slice {
 | 
				
			||||||
 | 
									for i := 0; i < arrV.Len(); i++ {
 | 
				
			||||||
 | 
										iV := arrV.Index(i)
 | 
				
			||||||
 | 
										if !iV.CanInterface() {
 | 
				
			||||||
 | 
											continue
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										if iV.Interface() == v {
 | 
				
			||||||
 | 
											return true
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"contain": func(s []int64, id int64) bool {
 | 
				
			||||||
 | 
								for i := 0; i < len(s); i++ {
 | 
				
			||||||
 | 
									if s[i] == id {
 | 
				
			||||||
 | 
										return true
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"Iterate": func(arg interface{}) (items []int64) {
 | 
				
			||||||
 | 
								count, _ := util.ToInt64(arg)
 | 
				
			||||||
 | 
								for i := int64(0); i < count; i++ {
 | 
				
			||||||
 | 
									items = append(items, i)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return items
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// -----------------------------------------------------------------
 | 
				
			||||||
 | 
							// setting
 | 
				
			||||||
		"AppName": func() string {
 | 
							"AppName": func() string {
 | 
				
			||||||
			return setting.AppName
 | 
								return setting.AppName
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
@@ -89,56 +216,12 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		"ShowFooterTemplateLoadTime": func() bool {
 | 
							"ShowFooterTemplateLoadTime": func() bool {
 | 
				
			||||||
			return setting.ShowFooterTemplateLoadTime
 | 
								return setting.ShowFooterTemplateLoadTime
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		"LoadTimes": func(startTime time.Time) string {
 | 
					 | 
				
			||||||
			return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"AllowedReactions": func() []string {
 | 
							"AllowedReactions": func() []string {
 | 
				
			||||||
			return setting.UI.Reactions
 | 
								return setting.UI.Reactions
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		"CustomEmojis": func() map[string]string {
 | 
							"CustomEmojis": func() map[string]string {
 | 
				
			||||||
			return setting.UI.CustomEmojisMap
 | 
								return setting.UI.CustomEmojisMap
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		"Safe":          Safe,
 | 
					 | 
				
			||||||
		"JSEscape":      JSEscape,
 | 
					 | 
				
			||||||
		"Str2html":      Str2html,
 | 
					 | 
				
			||||||
		"TimeSince":     timeutil.TimeSince,
 | 
					 | 
				
			||||||
		"TimeSinceUnix": timeutil.TimeSinceUnix,
 | 
					 | 
				
			||||||
		"FileSize":      base.FileSize,
 | 
					 | 
				
			||||||
		"LocaleNumber":  LocaleNumber,
 | 
					 | 
				
			||||||
		"EntryIcon":     base.EntryIcon,
 | 
					 | 
				
			||||||
		"MigrationIcon": MigrationIcon,
 | 
					 | 
				
			||||||
		"ActionIcon":    ActionIcon,
 | 
					 | 
				
			||||||
		"DateFmtLong": func(t time.Time) string {
 | 
					 | 
				
			||||||
			return t.Format(time.RFC1123Z)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"CountFmt":                       base.FormatNumberSI,
 | 
					 | 
				
			||||||
		"EllipsisString":                 base.EllipsisString,
 | 
					 | 
				
			||||||
		"DiffLineTypeToStr":              DiffLineTypeToStr,
 | 
					 | 
				
			||||||
		"ShortSha":                       base.ShortSha,
 | 
					 | 
				
			||||||
		"ActionContent2Commits":          ActionContent2Commits,
 | 
					 | 
				
			||||||
		"PathEscape":                     url.PathEscape,
 | 
					 | 
				
			||||||
		"PathEscapeSegments":             util.PathEscapeSegments,
 | 
					 | 
				
			||||||
		"URLJoin":                        util.URLJoin,
 | 
					 | 
				
			||||||
		"RenderCommitMessage":            RenderCommitMessage,
 | 
					 | 
				
			||||||
		"RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject,
 | 
					 | 
				
			||||||
		"RenderCommitBody":               RenderCommitBody,
 | 
					 | 
				
			||||||
		"RenderCodeBlock":                RenderCodeBlock,
 | 
					 | 
				
			||||||
		"RenderIssueTitle":               RenderIssueTitle,
 | 
					 | 
				
			||||||
		"RenderEmoji":                    RenderEmoji,
 | 
					 | 
				
			||||||
		"RenderEmojiPlain":               emoji.ReplaceAliases,
 | 
					 | 
				
			||||||
		"ReactionToEmoji":                ReactionToEmoji,
 | 
					 | 
				
			||||||
		"RenderNote":                     RenderNote,
 | 
					 | 
				
			||||||
		"RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML {
 | 
					 | 
				
			||||||
			output, err := markdown.RenderString(&markup.RenderContext{
 | 
					 | 
				
			||||||
				Ctx:       ctx,
 | 
					 | 
				
			||||||
				URLPrefix: setting.AppSubURL,
 | 
					 | 
				
			||||||
			}, input)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				log.Error("RenderString: %v", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return template.HTML(output)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"IsMultilineCommitMessage": IsMultilineCommitMessage,
 | 
					 | 
				
			||||||
		"ThemeColorMetaTag": func() string {
 | 
							"ThemeColorMetaTag": func() string {
 | 
				
			||||||
			return setting.UI.ThemeColorMetaTag
 | 
								return setting.UI.ThemeColorMetaTag
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
@@ -157,6 +240,82 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		"EnableTimetracking": func() bool {
 | 
							"EnableTimetracking": func() bool {
 | 
				
			||||||
			return setting.Service.EnableTimetracking
 | 
								return setting.Service.EnableTimetracking
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							"DisableGitHooks": func() bool {
 | 
				
			||||||
 | 
								return setting.DisableGitHooks
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"DisableWebhooks": func() bool {
 | 
				
			||||||
 | 
								return setting.DisableWebhooks
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"DisableImportLocal": func() bool {
 | 
				
			||||||
 | 
								return !setting.ImportLocalPaths
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"DefaultTheme": func() string {
 | 
				
			||||||
 | 
								return setting.UI.DefaultTheme
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"NotificationSettings": func() map[string]interface{} {
 | 
				
			||||||
 | 
								return map[string]interface{}{
 | 
				
			||||||
 | 
									"MinTimeout":            int(setting.UI.Notification.MinTimeout / time.Millisecond),
 | 
				
			||||||
 | 
									"TimeoutStep":           int(setting.UI.Notification.TimeoutStep / time.Millisecond),
 | 
				
			||||||
 | 
									"MaxTimeout":            int(setting.UI.Notification.MaxTimeout / time.Millisecond),
 | 
				
			||||||
 | 
									"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond),
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"MermaidMaxSourceCharacters": func() int {
 | 
				
			||||||
 | 
								return setting.MermaidMaxSourceCharacters
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// -----------------------------------------------------------------
 | 
				
			||||||
 | 
							// render
 | 
				
			||||||
 | 
							"RenderCommitMessage":            RenderCommitMessage,
 | 
				
			||||||
 | 
							"RenderCommitMessageLinkSubject": RenderCommitMessageLinkSubject,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							"RenderCommitBody": RenderCommitBody,
 | 
				
			||||||
 | 
							"RenderCodeBlock":  RenderCodeBlock,
 | 
				
			||||||
 | 
							"RenderIssueTitle": RenderIssueTitle,
 | 
				
			||||||
 | 
							"RenderEmoji":      RenderEmoji,
 | 
				
			||||||
 | 
							"RenderEmojiPlain": emoji.ReplaceAliases,
 | 
				
			||||||
 | 
							"ReactionToEmoji":  ReactionToEmoji,
 | 
				
			||||||
 | 
							"RenderNote":       RenderNote,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							"RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML {
 | 
				
			||||||
 | 
								output, err := markdown.RenderString(&markup.RenderContext{
 | 
				
			||||||
 | 
									Ctx:       ctx,
 | 
				
			||||||
 | 
									URLPrefix: setting.AppSubURL,
 | 
				
			||||||
 | 
								}, input)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									log.Error("RenderString: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return template.HTML(output)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML {
 | 
				
			||||||
 | 
								return template.HTML(RenderLabel(ctx, label))
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
 | 
				
			||||||
 | 
								htmlCode := `<span class="labels-list">`
 | 
				
			||||||
 | 
								for _, label := range labels {
 | 
				
			||||||
 | 
									// Protect against nil value in labels - shouldn't happen but would cause a panic if so
 | 
				
			||||||
 | 
									if label == nil {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
 | 
				
			||||||
 | 
										repoLink, label.ID, RenderLabel(ctx, label))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								htmlCode += "</span>"
 | 
				
			||||||
 | 
								return template.HTML(htmlCode)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// -----------------------------------------------------------------
 | 
				
			||||||
 | 
							// misc
 | 
				
			||||||
 | 
							"DiffLineTypeToStr":        DiffLineTypeToStr,
 | 
				
			||||||
 | 
							"ShortSha":                 base.ShortSha,
 | 
				
			||||||
 | 
							"ActionContent2Commits":    ActionContent2Commits,
 | 
				
			||||||
 | 
							"IsMultilineCommitMessage": IsMultilineCommitMessage,
 | 
				
			||||||
 | 
							"CommentMustAsDiff":        gitdiff.CommentMustAsDiff,
 | 
				
			||||||
 | 
							"MirrorRemoteAddress":      mirrorRemoteAddress,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							"ParseDeadline": func(deadline string) []string {
 | 
				
			||||||
 | 
								return strings.Split(deadline, "|")
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		"FilenameIsImage": func(filename string) bool {
 | 
							"FilenameIsImage": func(filename string) bool {
 | 
				
			||||||
			mimeType := mime.TypeByExtension(filepath.Ext(filename))
 | 
								mimeType := mime.TypeByExtension(filepath.Ext(filename))
 | 
				
			||||||
			return strings.HasPrefix(mimeType, "image/")
 | 
								return strings.HasPrefix(mimeType, "image/")
 | 
				
			||||||
@@ -191,142 +350,6 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			return path
 | 
								return path
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		"DiffStatsWidth": func(adds, dels int) string {
 | 
					 | 
				
			||||||
			return fmt.Sprintf("%f", float64(adds)/(float64(adds)+float64(dels))*100)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"Json": func(in interface{}) string {
 | 
					 | 
				
			||||||
			out, err := json.Marshal(in)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return ""
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return string(out)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"JsonPrettyPrint": func(in string) string {
 | 
					 | 
				
			||||||
			var out bytes.Buffer
 | 
					 | 
				
			||||||
			err := json.Indent(&out, []byte(in), "", "  ")
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return ""
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return out.String()
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"DisableGitHooks": func() bool {
 | 
					 | 
				
			||||||
			return setting.DisableGitHooks
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"DisableWebhooks": func() bool {
 | 
					 | 
				
			||||||
			return setting.DisableWebhooks
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"DisableImportLocal": func() bool {
 | 
					 | 
				
			||||||
			return !setting.ImportLocalPaths
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"Printf":   fmt.Sprintf,
 | 
					 | 
				
			||||||
		"Escape":   Escape,
 | 
					 | 
				
			||||||
		"Sec2Time": util.SecToTime,
 | 
					 | 
				
			||||||
		"ParseDeadline": func(deadline string) []string {
 | 
					 | 
				
			||||||
			return strings.Split(deadline, "|")
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"DefaultTheme": func() string {
 | 
					 | 
				
			||||||
			return setting.UI.DefaultTheme
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"dict":                dict,
 | 
					 | 
				
			||||||
		"CommentMustAsDiff":   gitdiff.CommentMustAsDiff,
 | 
					 | 
				
			||||||
		"MirrorRemoteAddress": mirrorRemoteAddress,
 | 
					 | 
				
			||||||
		"NotificationSettings": func() map[string]interface{} {
 | 
					 | 
				
			||||||
			return map[string]interface{}{
 | 
					 | 
				
			||||||
				"MinTimeout":            int(setting.UI.Notification.MinTimeout / time.Millisecond),
 | 
					 | 
				
			||||||
				"TimeoutStep":           int(setting.UI.Notification.TimeoutStep / time.Millisecond),
 | 
					 | 
				
			||||||
				"MaxTimeout":            int(setting.UI.Notification.MaxTimeout / time.Millisecond),
 | 
					 | 
				
			||||||
				"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond),
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"containGeneric": func(arr, v interface{}) bool {
 | 
					 | 
				
			||||||
			arrV := reflect.ValueOf(arr)
 | 
					 | 
				
			||||||
			if arrV.Kind() == reflect.String && reflect.ValueOf(v).Kind() == reflect.String {
 | 
					 | 
				
			||||||
				return strings.Contains(arr.(string), v.(string))
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if arrV.Kind() == reflect.Slice {
 | 
					 | 
				
			||||||
				for i := 0; i < arrV.Len(); i++ {
 | 
					 | 
				
			||||||
					iV := arrV.Index(i)
 | 
					 | 
				
			||||||
					if !iV.CanInterface() {
 | 
					 | 
				
			||||||
						continue
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
					if iV.Interface() == v {
 | 
					 | 
				
			||||||
						return true
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return false
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"contain": func(s []int64, id int64) bool {
 | 
					 | 
				
			||||||
			for i := 0; i < len(s); i++ {
 | 
					 | 
				
			||||||
				if s[i] == id {
 | 
					 | 
				
			||||||
					return true
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return false
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"svg":            svg.RenderHTML,
 | 
					 | 
				
			||||||
		"avatar":         Avatar,
 | 
					 | 
				
			||||||
		"avatarHTML":     AvatarHTML,
 | 
					 | 
				
			||||||
		"avatarByAction": AvatarByAction,
 | 
					 | 
				
			||||||
		"avatarByEmail":  AvatarByEmail,
 | 
					 | 
				
			||||||
		"repoAvatar":     RepoAvatar,
 | 
					 | 
				
			||||||
		"SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
 | 
					 | 
				
			||||||
			// if needed
 | 
					 | 
				
			||||||
			if len(normSort) == 0 || len(urlSort) == 0 {
 | 
					 | 
				
			||||||
				return ""
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if len(urlSort) == 0 && isDefault {
 | 
					 | 
				
			||||||
				// if sort is sorted as default add arrow tho this table header
 | 
					 | 
				
			||||||
				if isDefault {
 | 
					 | 
				
			||||||
					return svg.RenderHTML("octicon-triangle-down", 16)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				// if sort arg is in url test if it correlates with column header sort arguments
 | 
					 | 
				
			||||||
				// the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
 | 
					 | 
				
			||||||
				if urlSort == normSort {
 | 
					 | 
				
			||||||
					// the table is sorted with this header normal
 | 
					 | 
				
			||||||
					return svg.RenderHTML("octicon-triangle-up", 16)
 | 
					 | 
				
			||||||
				} else if urlSort == revSort {
 | 
					 | 
				
			||||||
					// the table is sorted with this header reverse
 | 
					 | 
				
			||||||
					return svg.RenderHTML("octicon-triangle-down", 16)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			// the table is NOT sorted with this header
 | 
					 | 
				
			||||||
			return ""
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML {
 | 
					 | 
				
			||||||
			return template.HTML(RenderLabel(ctx, label))
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
 | 
					 | 
				
			||||||
			htmlCode := `<span class="labels-list">`
 | 
					 | 
				
			||||||
			for _, label := range labels {
 | 
					 | 
				
			||||||
				// Protect against nil value in labels - shouldn't happen but would cause a panic if so
 | 
					 | 
				
			||||||
				if label == nil {
 | 
					 | 
				
			||||||
					continue
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
 | 
					 | 
				
			||||||
					repoLink, label.ID, RenderLabel(ctx, label))
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			htmlCode += "</span>"
 | 
					 | 
				
			||||||
			return template.HTML(htmlCode)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"MermaidMaxSourceCharacters": func() int {
 | 
					 | 
				
			||||||
			return setting.MermaidMaxSourceCharacters
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"Join":        strings.Join,
 | 
					 | 
				
			||||||
		"QueryEscape": url.QueryEscape,
 | 
					 | 
				
			||||||
		"DotEscape":   DotEscape,
 | 
					 | 
				
			||||||
		"Iterate": func(arg interface{}) (items []int64) {
 | 
					 | 
				
			||||||
			count, _ := util.ToInt64(arg)
 | 
					 | 
				
			||||||
			for i := int64(0); i < count; i++ {
 | 
					 | 
				
			||||||
				items = append(items, i)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return items
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"HasPrefix": strings.HasPrefix,
 | 
					 | 
				
			||||||
		"CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string {
 | 
							"CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string {
 | 
				
			||||||
			var curBranch string
 | 
								var curBranch string
 | 
				
			||||||
			if repo.ID != baseRepo.ID {
 | 
								if repo.ID != baseRepo.ID {
 | 
				
			||||||
@@ -340,45 +363,6 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
				curBranch,
 | 
									curBranch,
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		"Eval": Eval,
 | 
					 | 
				
			||||||
	}}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// NewTextFuncMap returns functions for injecting to text templates
 | 
					 | 
				
			||||||
// It's a subset of those used for HTML and other templates
 | 
					 | 
				
			||||||
func NewTextFuncMap() []texttmpl.FuncMap {
 | 
					 | 
				
			||||||
	return []texttmpl.FuncMap{map[string]interface{}{
 | 
					 | 
				
			||||||
		"AppName": func() string {
 | 
					 | 
				
			||||||
			return setting.AppName
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"AppSubUrl": func() string {
 | 
					 | 
				
			||||||
			return setting.AppSubURL
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"AppUrl": func() string {
 | 
					 | 
				
			||||||
			return setting.AppURL
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"AppVer": func() string {
 | 
					 | 
				
			||||||
			return setting.AppVer
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"AppDomain": func() string { // documented in mail-templates.md
 | 
					 | 
				
			||||||
			return setting.Domain
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"TimeSince":     timeutil.TimeSince,
 | 
					 | 
				
			||||||
		"TimeSinceUnix": timeutil.TimeSinceUnix,
 | 
					 | 
				
			||||||
		"DateFmtLong": func(t time.Time) string {
 | 
					 | 
				
			||||||
			return t.Format(time.RFC1123Z)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"EllipsisString": base.EllipsisString,
 | 
					 | 
				
			||||||
		"URLJoin":        util.URLJoin,
 | 
					 | 
				
			||||||
		"Printf":         fmt.Sprintf,
 | 
					 | 
				
			||||||
		"Escape":         Escape,
 | 
					 | 
				
			||||||
		"Sec2Time":       util.SecToTime,
 | 
					 | 
				
			||||||
		"ParseDeadline": func(deadline string) []string {
 | 
					 | 
				
			||||||
			return strings.Split(deadline, "|")
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"dict":        dict,
 | 
					 | 
				
			||||||
		"QueryEscape": url.QueryEscape,
 | 
					 | 
				
			||||||
		"Eval":        Eval,
 | 
					 | 
				
			||||||
	}}
 | 
						}}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -457,16 +441,6 @@ func Str2html(raw string) template.HTML {
 | 
				
			|||||||
	return template.HTML(markup.Sanitize(raw))
 | 
						return template.HTML(markup.Sanitize(raw))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Escape escapes a HTML string
 | 
					 | 
				
			||||||
func Escape(raw string) string {
 | 
					 | 
				
			||||||
	return html.EscapeString(raw)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// JSEscape escapes a JS string
 | 
					 | 
				
			||||||
func JSEscape(raw string) string {
 | 
					 | 
				
			||||||
	return template.JSEscapeString(raw)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
 | 
					// DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls
 | 
				
			||||||
func DotEscape(raw string) string {
 | 
					func DotEscape(raw string) string {
 | 
				
			||||||
	return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
 | 
						return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
 | 
				
			||||||
@@ -771,25 +745,6 @@ func MigrationIcon(hostname string) string {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
 | 
					 | 
				
			||||||
	// Split template into subject and body
 | 
					 | 
				
			||||||
	var subjectContent []byte
 | 
					 | 
				
			||||||
	bodyContent := content
 | 
					 | 
				
			||||||
	loc := mailSubjectSplit.FindIndex(content)
 | 
					 | 
				
			||||||
	if loc != nil {
 | 
					 | 
				
			||||||
		subjectContent = content[0:loc[0]]
 | 
					 | 
				
			||||||
		bodyContent = content[loc[1]:]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if _, err := stpl.New(name).
 | 
					 | 
				
			||||||
		Parse(string(subjectContent)); err != nil {
 | 
					 | 
				
			||||||
		log.Warn("Failed to parse template [%s/subject]: %v", name, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if _, err := btpl.New(name).
 | 
					 | 
				
			||||||
		Parse(string(bodyContent)); err != nil {
 | 
					 | 
				
			||||||
		log.Warn("Failed to parse template [%s/body]: %v", name, err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type remoteAddress struct {
 | 
					type remoteAddress struct {
 | 
				
			||||||
	Address  string
 | 
						Address  string
 | 
				
			||||||
	Username string
 | 
						Username string
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ package templates
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
@@ -15,9 +16,11 @@ import (
 | 
				
			|||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sync/atomic"
 | 
						"sync/atomic"
 | 
				
			||||||
 | 
						texttemplate "text/template"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/watcher"
 | 
						"code.gitea.io/gitea/modules/watcher"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -34,6 +37,8 @@ type HTMLRender struct {
 | 
				
			|||||||
	templates atomic.Pointer[template.Template]
 | 
						templates atomic.Pointer[template.Template]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error {
 | 
					func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}) error {
 | 
				
			||||||
	if respWriter, ok := w.(http.ResponseWriter); ok {
 | 
						if respWriter, ok := w.(http.ResponseWriter); ok {
 | 
				
			||||||
		if respWriter.Header().Get("Content-Type") == "" {
 | 
							if respWriter.Header().Get("Content-Type") == "" {
 | 
				
			||||||
@@ -41,11 +46,23 @@ func (h *HTMLRender) HTML(w io.Writer, status int, name string, data interface{}
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		respWriter.WriteHeader(status)
 | 
							respWriter.WriteHeader(status)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return h.templates.Load().ExecuteTemplate(w, name, data)
 | 
						t, err := h.TemplateLookup(name)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return texttemplate.ExecError{Name: name, Err: err}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return t.Execute(w, data)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (h *HTMLRender) TemplateLookup(t string) *template.Template {
 | 
					func (h *HTMLRender) TemplateLookup(name string) (*template.Template, error) {
 | 
				
			||||||
	return h.templates.Load().Lookup(t)
 | 
						tmpls := h.templates.Load()
 | 
				
			||||||
 | 
						if tmpls == nil {
 | 
				
			||||||
 | 
							return nil, ErrTemplateNotInitialized
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tmpl := tmpls.Lookup(name)
 | 
				
			||||||
 | 
						if tmpl == nil {
 | 
				
			||||||
 | 
							return nil, util.ErrNotExist
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return tmpl, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (h *HTMLRender) CompileTemplates() error {
 | 
					func (h *HTMLRender) CompileTemplates() error {
 | 
				
			||||||
@@ -237,6 +254,12 @@ func GetLineFromTemplate(templateName string, targetLineNum int, target string,
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// FIXME: this algorithm could provide incorrect results and mislead the developers.
 | 
				
			||||||
 | 
						// For example: Undefined function "file" in template .....
 | 
				
			||||||
 | 
						//     {{Func .file.Addition file.Deletion .file.Addition}}
 | 
				
			||||||
 | 
						//             ^^^^          ^(the real error is here)
 | 
				
			||||||
 | 
						// The pointer is added to the first one, but the second one is the real incorrect one.
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
	// If there is a provided target to look for in the line add a pointer to it
 | 
						// If there is a provided target to look for in the line add a pointer to it
 | 
				
			||||||
	// e.g.                                                        ^^^^^^^
 | 
						// e.g.                                                        ^^^^^^^
 | 
				
			||||||
	if target != "" {
 | 
						if target != "" {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,16 +11,53 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	texttmpl "text/template"
 | 
						texttmpl "text/template"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/watcher"
 | 
						"code.gitea.io/gitea/modules/watcher"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
 | 
				
			||||||
 | 
					func mailSubjectTextFuncMap() texttmpl.FuncMap {
 | 
				
			||||||
 | 
						return texttmpl.FuncMap{
 | 
				
			||||||
 | 
							"dict": dict,
 | 
				
			||||||
 | 
							"Eval": Eval,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							"EllipsisString": base.EllipsisString,
 | 
				
			||||||
 | 
							"AppName": func() string {
 | 
				
			||||||
 | 
								return setting.AppName
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							"AppDomain": func() string { // documented in mail-templates.md
 | 
				
			||||||
 | 
								return setting.Domain
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) {
 | 
				
			||||||
 | 
						// Split template into subject and body
 | 
				
			||||||
 | 
						var subjectContent []byte
 | 
				
			||||||
 | 
						bodyContent := content
 | 
				
			||||||
 | 
						loc := mailSubjectSplit.FindIndex(content)
 | 
				
			||||||
 | 
						if loc != nil {
 | 
				
			||||||
 | 
							subjectContent = content[0:loc[0]]
 | 
				
			||||||
 | 
							bodyContent = content[loc[1]:]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, err := stpl.New(name).
 | 
				
			||||||
 | 
							Parse(string(subjectContent)); err != nil {
 | 
				
			||||||
 | 
							log.Warn("Failed to parse template [%s/subject]: %v", name, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if _, err := btpl.New(name).
 | 
				
			||||||
 | 
							Parse(string(bodyContent)); err != nil {
 | 
				
			||||||
 | 
							log.Warn("Failed to parse template [%s/body]: %v", name, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Mailer provides the templates required for sending notification mails.
 | 
					// Mailer provides the templates required for sending notification mails.
 | 
				
			||||||
func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
 | 
					func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
 | 
				
			||||||
	for _, funcs := range NewTextFuncMap() {
 | 
						subjectTemplates := texttmpl.New("")
 | 
				
			||||||
		subjectTemplates.Funcs(funcs)
 | 
						bodyTemplates := template.New("")
 | 
				
			||||||
	}
 | 
					
 | 
				
			||||||
 | 
						subjectTemplates.Funcs(mailSubjectTextFuncMap())
 | 
				
			||||||
	for _, funcs := range NewFuncMap() {
 | 
						for _, funcs := range NewFuncMap() {
 | 
				
			||||||
		bodyTemplates.Funcs(funcs)
 | 
							bodyTemplates.Funcs(funcs)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -133,8 +133,8 @@ func (rw *mockResponseWriter) Push(target string, opts *http.PushOptions) error
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type mockRender struct{}
 | 
					type mockRender struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (tr *mockRender) TemplateLookup(tmpl string) *template.Template {
 | 
					func (tr *mockRender) TemplateLookup(tmpl string) (*template.Template, error) {
 | 
				
			||||||
	return nil
 | 
						return nil, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (tr *mockRender) HTML(w io.Writer, status int, _ string, _ interface{}) error {
 | 
					func (tr *mockRender) HTML(w io.Writer, status int, _ string, _ interface{}) error {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -578,12 +578,15 @@ func GrantApplicationOAuth(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
 | 
					// OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
 | 
				
			||||||
func OIDCWellKnown(ctx *context.Context) {
 | 
					func OIDCWellKnown(ctx *context.Context) {
 | 
				
			||||||
	t := ctx.Render.TemplateLookup("user/auth/oidc_wellknown")
 | 
						t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("unable to find template", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	ctx.Resp.Header().Set("Content-Type", "application/json")
 | 
						ctx.Resp.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
	ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
 | 
						ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
 | 
				
			||||||
	if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
 | 
						if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
 | 
				
			||||||
		log.Error("%v", err)
 | 
							ctx.ServerError("unable to execute template", err)
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,11 +4,8 @@
 | 
				
			|||||||
package web
 | 
					package web
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/base"
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// tplSwaggerV1Json swagger v1 json template
 | 
					// tplSwaggerV1Json swagger v1 json template
 | 
				
			||||||
@@ -16,10 +13,13 @@ const tplSwaggerV1Json base.TplName = "swagger/v1_json"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// SwaggerV1Json render swagger v1 json
 | 
					// SwaggerV1Json render swagger v1 json
 | 
				
			||||||
func SwaggerV1Json(ctx *context.Context) {
 | 
					func SwaggerV1Json(ctx *context.Context) {
 | 
				
			||||||
	t := ctx.Render.TemplateLookup(string(tplSwaggerV1Json))
 | 
						t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							ctx.ServerError("unable to find template", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	ctx.Resp.Header().Set("Content-Type", "application/json")
 | 
						ctx.Resp.Header().Set("Content-Type", "application/json")
 | 
				
			||||||
	if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
 | 
						if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
 | 
				
			||||||
		log.Error("%v", err)
 | 
							ctx.ServerError("unable to execute template", err)
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,7 +42,7 @@
 | 
				
			|||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
				{{template "repo/issue/view_content/add_reaction" dict "ctxData" $.root "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
 | 
									{{template "repo/issue/view_content/add_reaction" dict "ctxData" $.root "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
 | 
				
			||||||
				{{template "repo/issue/view_content/context_menu" dict "ctxData" $.root "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
 | 
									{{template "repo/issue/view_content/context_menu" dict "ctxData" $.root "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
@@ -60,7 +60,7 @@
 | 
				
			|||||||
		{{$reactions := .Reactions.GroupByType}}
 | 
							{{$reactions := .Reactions.GroupByType}}
 | 
				
			||||||
		{{if $reactions}}
 | 
							{{if $reactions}}
 | 
				
			||||||
			<div class="ui attached segment reactions">
 | 
								<div class="ui attached segment reactions">
 | 
				
			||||||
			{{template "repo/issue/view_content/reactions" dict "ctxData" $.root "ActionURL" (Printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions}}
 | 
								{{template "repo/issue/view_content/reactions" dict "ctxData" $.root "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID) "Reactions" $reactions}}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
{{Eval .file.Addition "+" .file.Deletion}}
 | 
					{{Eval .file.Addition "+" .file.Deletion}}
 | 
				
			||||||
<span class="diff-stats-bar gt-mx-3" data-tooltip-content="{{.root.locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion | Str2html}}">
 | 
					<span class="diff-stats-bar gt-mx-3" data-tooltip-content="{{.root.locale.Tr "repo.diff.stats_desc_file" (Eval .file.Addition "+" .file.Deletion) .file.Addition .file.Deletion | Str2html}}">
 | 
				
			||||||
	<div class="diff-stats-add-bar" style="width: {{DiffStatsWidth .file.Addition .file.Deletion}}%"></div>
 | 
						{{/* if the denominator is zero, then the float result is "width: NaNpx", as before, it just works */}}
 | 
				
			||||||
 | 
						<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .file.Addition "/" "(" .file.Addition "+" .file.Deletion "+" 0.0 ")"}}%"></div>
 | 
				
			||||||
</span>
 | 
					</span>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,7 +64,7 @@
 | 
				
			|||||||
								{{end}}
 | 
													{{end}}
 | 
				
			||||||
							{{end}}
 | 
												{{end}}
 | 
				
			||||||
							{{if not $.Repository.IsArchived}}
 | 
												{{if not $.Repository.IsArchived}}
 | 
				
			||||||
								{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
 | 
													{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index)}}
 | 
				
			||||||
								{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" .Issue "delete" false "issue" true "diff" false "IsCommentPoster" $.IsIssuePoster}}
 | 
													{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" .Issue "delete" false "issue" true "diff" false "IsCommentPoster" $.IsIssuePoster}}
 | 
				
			||||||
							{{end}}
 | 
												{{end}}
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
@@ -86,7 +86,7 @@
 | 
				
			|||||||
					{{$reactions := .Issue.Reactions.GroupByType}}
 | 
										{{$reactions := .Issue.Reactions.GroupByType}}
 | 
				
			||||||
					{{if $reactions}}
 | 
										{{if $reactions}}
 | 
				
			||||||
						<div class="ui attached segment reactions" role="note">
 | 
											<div class="ui attached segment reactions" role="note">
 | 
				
			||||||
							{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "Reactions" $reactions}}
 | 
												{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "Reactions" $reactions}}
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,7 +64,7 @@
 | 
				
			|||||||
								</div>
 | 
													</div>
 | 
				
			||||||
							{{end}}
 | 
												{{end}}
 | 
				
			||||||
							{{if not $.Repository.IsArchived}}
 | 
												{{if not $.Repository.IsArchived}}
 | 
				
			||||||
								{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
 | 
													{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
 | 
				
			||||||
								{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
 | 
													{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
 | 
				
			||||||
							{{end}}
 | 
												{{end}}
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
@@ -86,7 +86,7 @@
 | 
				
			|||||||
					{{$reactions := .Reactions.GroupByType}}
 | 
										{{$reactions := .Reactions.GroupByType}}
 | 
				
			||||||
					{{if $reactions}}
 | 
										{{if $reactions}}
 | 
				
			||||||
						<div class="ui attached segment reactions" role="note">
 | 
											<div class="ui attached segment reactions" role="note">
 | 
				
			||||||
							{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
 | 
												{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					{{end}}
 | 
										{{end}}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
@@ -436,7 +436,7 @@
 | 
				
			|||||||
										</div>
 | 
															</div>
 | 
				
			||||||
									{{end}}
 | 
														{{end}}
 | 
				
			||||||
									{{if not $.Repository.IsArchived}}
 | 
														{{if not $.Repository.IsArchived}}
 | 
				
			||||||
											{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
 | 
																{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
 | 
				
			||||||
											{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
 | 
																{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" false "issue" true "diff" false "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
 | 
				
			||||||
									{{end}}
 | 
														{{end}}
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
@@ -458,7 +458,7 @@
 | 
				
			|||||||
						{{$reactions := .Reactions.GroupByType}}
 | 
											{{$reactions := .Reactions.GroupByType}}
 | 
				
			||||||
						{{if $reactions}}
 | 
											{{if $reactions}}
 | 
				
			||||||
							<div class="ui attached segment reactions">
 | 
												<div class="ui attached segment reactions">
 | 
				
			||||||
									{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
 | 
														{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
						{{end}}
 | 
											{{end}}
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
@@ -563,7 +563,7 @@
 | 
				
			|||||||
																	</div>
 | 
																						</div>
 | 
				
			||||||
																{{end}}
 | 
																					{{end}}
 | 
				
			||||||
																{{if not $.Repository.IsArchived}}
 | 
																					{{if not $.Repository.IsArchived}}
 | 
				
			||||||
																	{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
 | 
																						{{template "repo/issue/view_content/add_reaction" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID)}}
 | 
				
			||||||
																	{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
 | 
																						{{template "repo/issue/view_content/context_menu" dict "ctxData" $ "item" . "delete" true "issue" true "diff" true "IsCommentPoster" (and $.IsSigned (eq $.SignedUserID .PosterID))}}
 | 
				
			||||||
																{{end}}
 | 
																					{{end}}
 | 
				
			||||||
															</div>
 | 
																				</div>
 | 
				
			||||||
@@ -582,7 +582,7 @@
 | 
				
			|||||||
														{{$reactions := .Reactions.GroupByType}}
 | 
																			{{$reactions := .Reactions.GroupByType}}
 | 
				
			||||||
														{{if $reactions}}
 | 
																			{{if $reactions}}
 | 
				
			||||||
															<div class="ui attached segment reactions">
 | 
																				<div class="ui attached segment reactions">
 | 
				
			||||||
																{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
 | 
																					{{template "repo/issue/view_content/reactions" dict "ctxData" $ "ActionURL" (printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions}}
 | 
				
			||||||
															</div>
 | 
																				</div>
 | 
				
			||||||
														{{end}}
 | 
																			{{end}}
 | 
				
			||||||
													</div>
 | 
																		</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,9 +6,9 @@
 | 
				
			|||||||
	<div class="menu">
 | 
						<div class="menu">
 | 
				
			||||||
		{{$referenceUrl := ""}}
 | 
							{{$referenceUrl := ""}}
 | 
				
			||||||
		{{if .issue}}
 | 
							{{if .issue}}
 | 
				
			||||||
			{{$referenceUrl = Printf "%s#%s" .ctxData.Issue.Link .item.HashTag}}
 | 
								{{$referenceUrl = printf "%s#%s" .ctxData.Issue.Link .item.HashTag}}
 | 
				
			||||||
		{{else}}
 | 
							{{else}}
 | 
				
			||||||
			{{$referenceUrl = Printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
 | 
								{{$referenceUrl = printf "%s/files#%s" .ctxData.Issue.Link .item.HashTag}}
 | 
				
			||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
		<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{AppSubUrl}}{{$referenceUrl}}">{{.ctxData.locale.Tr "repo.issues.context.copy_link"}}</div>
 | 
							<div class="item context js-aria-clickable" data-clipboard-text-type="url" data-clipboard-text="{{AppSubUrl}}{{$referenceUrl}}">{{.ctxData.locale.Tr "repo.issues.context.copy_link"}}</div>
 | 
				
			||||||
		<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{.ctxData.locale.Tr "repo.issues.context.quote_reply"}}</div>
 | 
							<div class="item context js-aria-clickable quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.HashTag}}-raw">{{.ctxData.locale.Tr "repo.issues.context.quote_reply"}}</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
		{{.locale.Tr "repo.issues.context.reference_issue"}}
 | 
							{{.locale.Tr "repo.issues.context.reference_issue"}}
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div class="content" style="text-align:left">
 | 
						<div class="content" style="text-align:left">
 | 
				
			||||||
		<form class="ui form" action="{{Printf "%s/issues/new" .Repository.Link}}" method="post">
 | 
							<form class="ui form" action="{{printf "%s/issues/new" .Repository.Link}}" method="post">
 | 
				
			||||||
			{{.CsrfTokenHtml}}
 | 
								{{.CsrfTokenHtml}}
 | 
				
			||||||
			<div class="ui segment content">
 | 
								<div class="ui segment content">
 | 
				
			||||||
				<div class="field">
 | 
									<div class="field">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,13 +6,13 @@
 | 
				
			|||||||
			<div class="ui three stackable cards">
 | 
								<div class="ui three stackable cards">
 | 
				
			||||||
				{{range .Services}}
 | 
									{{range .Services}}
 | 
				
			||||||
					<a class="ui card gt-df gt-ac" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
 | 
										<a class="ui card gt-df gt-ac" href="{{AppSubUrl}}/repo/migrate?service_type={{.}}&org={{$.Org}}&mirror={{$.Mirror}}">
 | 
				
			||||||
						{{svg (Printf "gitea-%s" .Name) 184}}
 | 
											{{svg (printf "gitea-%s" .Name) 184}}
 | 
				
			||||||
						<div class="content">
 | 
											<div class="content">
 | 
				
			||||||
							<div class="header gt-tc">
 | 
												<div class="header gt-tc">
 | 
				
			||||||
								{{.Title}}
 | 
													{{.Title}}
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
							<div class="description gt-tc">
 | 
												<div class="description gt-tc">
 | 
				
			||||||
								{{(Printf "repo.migrate.%s.description" .Name) | $.locale.Tr}}
 | 
													{{(printf "repo.migrate.%s.description" .Name) | $.locale.Tr}}
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					</a>
 | 
										</a>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user