diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index a290d38979..20261eb959 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -5,46 +5,25 @@
 package templates
 
 import (
-	"bytes"
 	"context"
-	"encoding/hex"
 	"fmt"
 	"html"
 	"html/template"
-	"math"
-	"mime"
 	"net/url"
-	"path/filepath"
 	"regexp"
 	"strings"
 	"time"
-	"unicode"
 
-	activities_model "code.gitea.io/gitea/models/activities"
-	"code.gitea.io/gitea/models/avatars"
-	issues_model "code.gitea.io/gitea/models/issues"
-	"code.gitea.io/gitea/models/organization"
-	repo_model "code.gitea.io/gitea/models/repo"
 	system_model "code.gitea.io/gitea/models/system"
-	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/emoji"
-	"code.gitea.io/gitea/modules/git"
-	giturl "code.gitea.io/gitea/modules/git/url"
-	gitea_html "code.gitea.io/gitea/modules/html"
-	"code.gitea.io/gitea/modules/json"
-	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/markup"
-	"code.gitea.io/gitea/modules/markup/markdown"
-	"code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/svg"
 	"code.gitea.io/gitea/modules/templates/eval"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/gitdiff"
-
-	"github.com/editorconfig/editorconfig-core-go/v2"
 )
 
 // Used from static.go && dynamic.go
@@ -53,6 +32,8 @@ var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
 // NewFuncMap returns functions for injecting to templates
 func NewFuncMap() []template.FuncMap {
 	return []template.FuncMap{map[string]interface{}{
+		"DumpVar": dumpVar,
+
 		// -----------------------------------------------------------------
 		// html/template related functions
 		"dict":        dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
@@ -63,6 +44,7 @@ func NewFuncMap() []template.FuncMap {
 		"JSEscape":    template.JSEscapeString,
 		"Str2html":    Str2html, // TODO: rename it to SanitizeHTML
 		"URLJoin":     util.URLJoin,
+		"DotEscape":   DotEscape,
 
 		"PathEscape":         url.PathEscape,
 		"PathEscapeSegments": util.PathEscapeSegments,
@@ -70,30 +52,7 @@ func NewFuncMap() []template.FuncMap {
 		// utils
 		"StringUtils": NewStringUtils,
 		"SliceUtils":  NewSliceUtils,
-
-		// -----------------------------------------------------------------
-		// string / json
-		// TODO: move string helper functions to StringUtils
-		"Join":           strings.Join,
-		"DotEscape":      DotEscape,
-		"EllipsisString": base.EllipsisString,
-		"DumpVar":        dumpVar,
-
-		"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()
-		},
+		"JsonUtils":   NewJsonUtils,
 
 		// -----------------------------------------------------------------
 		// svg / avatar / icon
@@ -107,31 +66,7 @@ func NewFuncMap() []template.FuncMap {
 		"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 ""
-		},
+		"SortArrow": SortArrow,
 
 		// -----------------------------------------------------------------
 		// time / number / format
@@ -242,32 +177,9 @@ func NewFuncMap() []template.FuncMap {
 		"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)
-		},
+		"RenderMarkdownToHtml": RenderMarkdownToHtml,
+		"RenderLabel":          RenderLabel,
+		"RenderLabels":         RenderLabels,
 
 		// -----------------------------------------------------------------
 		// misc
@@ -278,124 +190,11 @@ func NewFuncMap() []template.FuncMap {
 		"CommentMustAsDiff":        gitdiff.CommentMustAsDiff,
 		"MirrorRemoteAddress":      mirrorRemoteAddress,
 
-		"ParseDeadline": func(deadline string) []string {
-			return strings.Split(deadline, "|")
-		},
-		"FilenameIsImage": func(filename string) bool {
-			mimeType := mime.TypeByExtension(filepath.Ext(filename))
-			return strings.HasPrefix(mimeType, "image/")
-		},
-		"TabSizeClass": func(ec interface{}, filename string) string {
-			var (
-				value *editorconfig.Editorconfig
-				ok    bool
-			)
-			if ec != nil {
-				if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
-					return "tab-size-8"
-				}
-				def, err := value.GetDefinitionForFilename(filename)
-				if err != nil {
-					log.Error("tab size class: getting definition for filename: %v", err)
-					return "tab-size-8"
-				}
-				if def.TabWidth > 0 {
-					return fmt.Sprintf("tab-size-%d", def.TabWidth)
-				}
-			}
-			return "tab-size-8"
-		},
-		"SubJumpablePath": func(str string) []string {
-			var path []string
-			index := strings.LastIndex(str, "/")
-			if index != -1 && index != len(str) {
-				path = append(path, str[0:index+1], str[index+1:])
-			} else {
-				path = append(path, str)
-			}
-			return path
-		},
-		"CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string {
-			var curBranch string
-			if repo.ID != baseRepo.ID {
-				curBranch += fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
-			}
-			curBranch += util.PathEscapeSegments(branchName)
-
-			return fmt.Sprintf("%s/compare/%s...%s",
-				baseRepo.Link(),
-				util.PathEscapeSegments(baseRepo.DefaultBranch),
-				curBranch,
-			)
-		},
+		"FilenameIsImage": FilenameIsImage,
+		"TabSizeClass":    TabSizeClass,
 	}}
 }
 
-// AvatarHTML creates the HTML for an avatar
-func AvatarHTML(src string, size int, class, name string) template.HTML {
-	sizeStr := fmt.Sprintf(`%d`, size)
-
-	if name == "" {
-		name = "avatar"
-	}
-
-	return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
-}
-
-// Avatar renders user avatars. args: user, size (int), class (string)
-func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML {
-	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
-
-	switch t := item.(type) {
-	case *user_model.User:
-		src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
-		if src != "" {
-			return AvatarHTML(src, size, class, t.DisplayName())
-		}
-	case *repo_model.Collaborator:
-		src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
-		if src != "" {
-			return AvatarHTML(src, size, class, t.DisplayName())
-		}
-	case *organization.Organization:
-		src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
-		if src != "" {
-			return AvatarHTML(src, size, class, t.AsUser().DisplayName())
-		}
-	}
-
-	return template.HTML("")
-}
-
-// AvatarByAction renders user avatars from action. args: action, size (int), class (string)
-func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML {
-	action.LoadActUser(ctx)
-	return Avatar(ctx, action.ActUser, others...)
-}
-
-// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
-func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
-	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
-
-	src := repo.RelAvatarLink()
-	if src != "" {
-		return AvatarHTML(src, size, class, repo.FullName())
-	}
-	return template.HTML("")
-}
-
-// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
-func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML {
-	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
-	src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor)
-
-	if src != "" {
-		return AvatarHTML(src, size, class, name)
-	}
-
-	return template.HTML("")
-}
-
 // Safe render raw as HTML
 func Safe(raw string) template.HTML {
 	return template.HTML(raw)
@@ -411,342 +210,6 @@ func DotEscape(raw string) string {
 	return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
 }
 
-// RenderCommitMessage renders commit message with XSS-safe and special links.
-func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
-	return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
-}
-
-// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
-// default url, handling for special links.
-func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
-	cleanMsg := template.HTMLEscapeString(msg)
-	// we can safely assume that it will not return any error, since there
-	// shouldn't be any special HTML.
-	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
-		Ctx:         ctx,
-		URLPrefix:   urlPrefix,
-		DefaultLink: urlDefault,
-		Metas:       metas,
-	}, cleanMsg)
-	if err != nil {
-		log.Error("RenderCommitMessage: %v", err)
-		return ""
-	}
-	msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
-	if len(msgLines) == 0 {
-		return template.HTML("")
-	}
-	return template.HTML(msgLines[0])
-}
-
-// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
-// the provided default url, handling for special links without email to links.
-func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
-	msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
-	lineEnd := strings.IndexByte(msgLine, '\n')
-	if lineEnd > 0 {
-		msgLine = msgLine[:lineEnd]
-	}
-	msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
-	if len(msgLine) == 0 {
-		return template.HTML("")
-	}
-
-	// we can safely assume that it will not return any error, since there
-	// shouldn't be any special HTML.
-	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
-		Ctx:         ctx,
-		URLPrefix:   urlPrefix,
-		DefaultLink: urlDefault,
-		Metas:       metas,
-	}, template.HTMLEscapeString(msgLine))
-	if err != nil {
-		log.Error("RenderCommitMessageSubject: %v", err)
-		return template.HTML("")
-	}
-	return template.HTML(renderedMessage)
-}
-
-// RenderCommitBody extracts the body of a commit message without its title.
-func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
-	msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
-	lineEnd := strings.IndexByte(msgLine, '\n')
-	if lineEnd > 0 {
-		msgLine = msgLine[lineEnd+1:]
-	} else {
-		return template.HTML("")
-	}
-	msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
-	if len(msgLine) == 0 {
-		return template.HTML("")
-	}
-
-	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: urlPrefix,
-		Metas:     metas,
-	}, template.HTMLEscapeString(msgLine))
-	if err != nil {
-		log.Error("RenderCommitMessage: %v", err)
-		return ""
-	}
-	return template.HTML(renderedMessage)
-}
-
-// Match text that is between back ticks.
-var codeMatcher = regexp.MustCompile("`([^`]+)`")
-
-// RenderCodeBlock renders "`…`" as highlighted "<code>" block.
-// Intended for issue and PR titles, these containers should have styles for "<code>" elements
-func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
-	htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "<code>$1</code>") // replace with HTML <code> tags
-	return template.HTML(htmlWithCodeTags)
-}
-
-// RenderIssueTitle renders issue/pull title with defined post processors
-func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML {
-	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: urlPrefix,
-		Metas:     metas,
-	}, template.HTMLEscapeString(text))
-	if err != nil {
-		log.Error("RenderIssueTitle: %v", err)
-		return template.HTML("")
-	}
-	return template.HTML(renderedText)
-}
-
-// RenderLabel renders a label
-func RenderLabel(ctx context.Context, label *issues_model.Label) string {
-	labelScope := label.ExclusiveScope()
-
-	textColor := "#111"
-	if label.UseLightTextColor() {
-		textColor = "#eee"
-	}
-
-	description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
-
-	if labelScope == "" {
-		// Regular label
-		return fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' title='%s'>%s</div>",
-			textColor, label.Color, description, RenderEmoji(ctx, label.Name))
-	}
-
-	// Scoped label
-	scopeText := RenderEmoji(ctx, labelScope)
-	itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
-
-	itemColor := label.Color
-	scopeColor := label.Color
-	if r, g, b, err := label.ColorRGB(); err == nil {
-		// Make scope and item background colors slightly darker and lighter respectively.
-		// More contrast needed with higher luminance, empirically tweaked.
-		luminance := (0.299*r + 0.587*g + 0.114*b) / 255
-		contrast := 0.01 + luminance*0.03
-		// Ensure we add the same amount of contrast also near 0 and 1.
-		darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
-		lighten := contrast + math.Max(contrast-luminance, 0.0)
-		// Compute factor to keep RGB values proportional.
-		darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
-		lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
-
-		scopeBytes := []byte{
-			uint8(math.Min(math.Round(r*darkenFactor), 255)),
-			uint8(math.Min(math.Round(g*darkenFactor), 255)),
-			uint8(math.Min(math.Round(b*darkenFactor), 255)),
-		}
-		itemBytes := []byte{
-			uint8(math.Min(math.Round(r*lightenFactor), 255)),
-			uint8(math.Min(math.Round(g*lightenFactor), 255)),
-			uint8(math.Min(math.Round(b*lightenFactor), 255)),
-		}
-
-		itemColor = "#" + hex.EncodeToString(itemBytes)
-		scopeColor = "#" + hex.EncodeToString(scopeBytes)
-	}
-
-	return fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
-		"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
-		"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important''>%s</div>"+
-		"</span>",
-		description,
-		textColor, scopeColor, scopeText,
-		textColor, itemColor, itemText)
-}
-
-// RenderEmoji renders html text with emoji post processors
-func RenderEmoji(ctx context.Context, text string) template.HTML {
-	renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
-		template.HTMLEscapeString(text))
-	if err != nil {
-		log.Error("RenderEmoji: %v", err)
-		return template.HTML("")
-	}
-	return template.HTML(renderedText)
-}
-
-// ReactionToEmoji renders emoji for use in reactions
-func ReactionToEmoji(reaction string) template.HTML {
-	val := emoji.FromCode(reaction)
-	if val != nil {
-		return template.HTML(val.Emoji)
-	}
-	val = emoji.FromAlias(reaction)
-	if val != nil {
-		return template.HTML(val.Emoji)
-	}
-	return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
-}
-
-// RenderNote renders the contents of a git-notes file as a commit message.
-func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
-	cleanMsg := template.HTMLEscapeString(msg)
-	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: urlPrefix,
-		Metas:     metas,
-	}, cleanMsg)
-	if err != nil {
-		log.Error("RenderNote: %v", err)
-		return ""
-	}
-	return template.HTML(fullMessage)
-}
-
-// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
-func IsMultilineCommitMessage(msg string) bool {
-	return strings.Count(strings.TrimSpace(msg), "\n") >= 1
-}
-
-// Actioner describes an action
-type Actioner interface {
-	GetOpType() activities_model.ActionType
-	GetActUserName() string
-	GetRepoUserName() string
-	GetRepoName() string
-	GetRepoPath() string
-	GetRepoLink() string
-	GetBranch() string
-	GetContent() string
-	GetCreate() time.Time
-	GetIssueInfos() []string
-}
-
-// ActionIcon accepts an action operation type and returns an icon class name.
-func ActionIcon(opType activities_model.ActionType) string {
-	switch opType {
-	case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
-		return "repo"
-	case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
-		return "git-commit"
-	case activities_model.ActionCreateIssue:
-		return "issue-opened"
-	case activities_model.ActionCreatePullRequest:
-		return "git-pull-request"
-	case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
-		return "comment-discussion"
-	case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
-		return "git-merge"
-	case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
-		return "issue-closed"
-	case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
-		return "issue-reopened"
-	case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
-		return "mirror"
-	case activities_model.ActionApprovePullRequest:
-		return "check"
-	case activities_model.ActionRejectPullRequest:
-		return "diff"
-	case activities_model.ActionPublishRelease:
-		return "tag"
-	case activities_model.ActionPullReviewDismissed:
-		return "x"
-	default:
-		return "question"
-	}
-}
-
-// ActionContent2Commits converts action content to push commits
-func ActionContent2Commits(act Actioner) *repository.PushCommits {
-	push := repository.NewPushCommits()
-
-	if act == nil || act.GetContent() == "" {
-		return push
-	}
-
-	if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
-		log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
-	}
-
-	if push.Len == 0 {
-		push.Len = len(push.Commits)
-	}
-
-	return push
-}
-
-// DiffLineTypeToStr returns diff line type name
-func DiffLineTypeToStr(diffType int) string {
-	switch diffType {
-	case 2:
-		return "add"
-	case 3:
-		return "del"
-	case 4:
-		return "tag"
-	}
-	return "same"
-}
-
-// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
-func MigrationIcon(hostname string) string {
-	switch hostname {
-	case "github.com":
-		return "octicon-mark-github"
-	default:
-		return "gitea-git"
-	}
-}
-
-type remoteAddress struct {
-	Address  string
-	Username string
-	Password string
-}
-
-func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
-	a := remoteAddress{}
-
-	remoteURL := m.OriginalURL
-	if ignoreOriginalURL || remoteURL == "" {
-		var err error
-		remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
-		if err != nil {
-			log.Error("GetRemoteURL %v", err)
-			return a
-		}
-	}
-
-	u, err := giturl.Parse(remoteURL)
-	if err != nil {
-		log.Error("giturl.Parse %v", err)
-		return a
-	}
-
-	if u.Scheme != "ssh" && u.Scheme != "file" {
-		if u.User != nil {
-			a.Username = u.User.Username()
-			a.Password, _ = u.User.Password()
-		}
-		u.User = nil
-	}
-	a.Address = u.String()
-
-	return a
-}
-
 // Eval the expression and return the result, see the comment of eval.Expr for details.
 // To use this helper function in templates, pass each token as a separate parameter.
 //
diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go
new file mode 100644
index 0000000000..3badc97cb9
--- /dev/null
+++ b/modules/templates/util_avatar.go
@@ -0,0 +1,84 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+	"context"
+	"fmt"
+	"html"
+	"html/template"
+
+	activities_model "code.gitea.io/gitea/models/activities"
+	"code.gitea.io/gitea/models/avatars"
+	"code.gitea.io/gitea/models/organization"
+	repo_model "code.gitea.io/gitea/models/repo"
+	user_model "code.gitea.io/gitea/models/user"
+	gitea_html "code.gitea.io/gitea/modules/html"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+// AvatarHTML creates the HTML for an avatar
+func AvatarHTML(src string, size int, class, name string) template.HTML {
+	sizeStr := fmt.Sprintf(`%d`, size)
+
+	if name == "" {
+		name = "avatar"
+	}
+
+	return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
+}
+
+// Avatar renders user avatars. args: user, size (int), class (string)
+func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML {
+	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
+
+	switch t := item.(type) {
+	case *user_model.User:
+		src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
+		if src != "" {
+			return AvatarHTML(src, size, class, t.DisplayName())
+		}
+	case *repo_model.Collaborator:
+		src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
+		if src != "" {
+			return AvatarHTML(src, size, class, t.DisplayName())
+		}
+	case *organization.Organization:
+		src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
+		if src != "" {
+			return AvatarHTML(src, size, class, t.AsUser().DisplayName())
+		}
+	}
+
+	return template.HTML("")
+}
+
+// AvatarByAction renders user avatars from action. args: action, size (int), class (string)
+func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML {
+	action.LoadActUser(ctx)
+	return Avatar(ctx, action.ActUser, others...)
+}
+
+// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
+func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
+	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
+
+	src := repo.RelAvatarLink()
+	if src != "" {
+		return AvatarHTML(src, size, class, repo.FullName())
+	}
+	return template.HTML("")
+}
+
+// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
+func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML {
+	size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
+	src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor)
+
+	if src != "" {
+		return AvatarHTML(src, size, class, name)
+	}
+
+	return template.HTML("")
+}
diff --git a/modules/templates/util_json.go b/modules/templates/util_json.go
new file mode 100644
index 0000000000..71a4e23d36
--- /dev/null
+++ b/modules/templates/util_json.go
@@ -0,0 +1,35 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+	"bytes"
+
+	"code.gitea.io/gitea/modules/json"
+)
+
+type JsonUtils struct{} //nolint:revive
+
+var jsonUtils = JsonUtils{}
+
+func NewJsonUtils() *JsonUtils { //nolint:revive
+	return &jsonUtils
+}
+
+func (su *JsonUtils) EncodeToString(v any) string {
+	out, err := json.Marshal(v)
+	if err != nil {
+		return ""
+	}
+	return string(out)
+}
+
+func (su *JsonUtils) PrettyIndent(s string) string {
+	var out bytes.Buffer
+	err := json.Indent(&out, []byte(s), "", "  ")
+	if err != nil {
+		return ""
+	}
+	return out.String()
+}
diff --git a/modules/templates/util_misc.go b/modules/templates/util_misc.go
new file mode 100644
index 0000000000..599a0942ce
--- /dev/null
+++ b/modules/templates/util_misc.go
@@ -0,0 +1,209 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+	"context"
+	"fmt"
+	"html/template"
+	"mime"
+	"path/filepath"
+	"strings"
+	"time"
+
+	activities_model "code.gitea.io/gitea/models/activities"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/modules/git"
+	giturl "code.gitea.io/gitea/modules/git/url"
+	"code.gitea.io/gitea/modules/json"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/repository"
+	"code.gitea.io/gitea/modules/svg"
+
+	"github.com/editorconfig/editorconfig-core-go/v2"
+)
+
+func SortArrow(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 ""
+}
+
+// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
+func IsMultilineCommitMessage(msg string) bool {
+	return strings.Count(strings.TrimSpace(msg), "\n") >= 1
+}
+
+// Actioner describes an action
+type Actioner interface {
+	GetOpType() activities_model.ActionType
+	GetActUserName() string
+	GetRepoUserName() string
+	GetRepoName() string
+	GetRepoPath() string
+	GetRepoLink() string
+	GetBranch() string
+	GetContent() string
+	GetCreate() time.Time
+	GetIssueInfos() []string
+}
+
+// ActionIcon accepts an action operation type and returns an icon class name.
+func ActionIcon(opType activities_model.ActionType) string {
+	switch opType {
+	case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
+		return "repo"
+	case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
+		return "git-commit"
+	case activities_model.ActionCreateIssue:
+		return "issue-opened"
+	case activities_model.ActionCreatePullRequest:
+		return "git-pull-request"
+	case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
+		return "comment-discussion"
+	case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
+		return "git-merge"
+	case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
+		return "issue-closed"
+	case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
+		return "issue-reopened"
+	case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
+		return "mirror"
+	case activities_model.ActionApprovePullRequest:
+		return "check"
+	case activities_model.ActionRejectPullRequest:
+		return "diff"
+	case activities_model.ActionPublishRelease:
+		return "tag"
+	case activities_model.ActionPullReviewDismissed:
+		return "x"
+	default:
+		return "question"
+	}
+}
+
+// ActionContent2Commits converts action content to push commits
+func ActionContent2Commits(act Actioner) *repository.PushCommits {
+	push := repository.NewPushCommits()
+
+	if act == nil || act.GetContent() == "" {
+		return push
+	}
+
+	if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
+		log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
+	}
+
+	if push.Len == 0 {
+		push.Len = len(push.Commits)
+	}
+
+	return push
+}
+
+// DiffLineTypeToStr returns diff line type name
+func DiffLineTypeToStr(diffType int) string {
+	switch diffType {
+	case 2:
+		return "add"
+	case 3:
+		return "del"
+	case 4:
+		return "tag"
+	}
+	return "same"
+}
+
+// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
+func MigrationIcon(hostname string) string {
+	switch hostname {
+	case "github.com":
+		return "octicon-mark-github"
+	default:
+		return "gitea-git"
+	}
+}
+
+type remoteAddress struct {
+	Address  string
+	Username string
+	Password string
+}
+
+func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
+	a := remoteAddress{}
+
+	remoteURL := m.OriginalURL
+	if ignoreOriginalURL || remoteURL == "" {
+		var err error
+		remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
+		if err != nil {
+			log.Error("GetRemoteURL %v", err)
+			return a
+		}
+	}
+
+	u, err := giturl.Parse(remoteURL)
+	if err != nil {
+		log.Error("giturl.Parse %v", err)
+		return a
+	}
+
+	if u.Scheme != "ssh" && u.Scheme != "file" {
+		if u.User != nil {
+			a.Username = u.User.Username()
+			a.Password, _ = u.User.Password()
+		}
+		u.User = nil
+	}
+	a.Address = u.String()
+
+	return a
+}
+
+func FilenameIsImage(filename string) bool {
+	mimeType := mime.TypeByExtension(filepath.Ext(filename))
+	return strings.HasPrefix(mimeType, "image/")
+}
+
+func TabSizeClass(ec interface{}, filename string) string {
+	var (
+		value *editorconfig.Editorconfig
+		ok    bool
+	)
+	if ec != nil {
+		if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
+			return "tab-size-8"
+		}
+		def, err := value.GetDefinitionForFilename(filename)
+		if err != nil {
+			log.Error("tab size class: getting definition for filename: %v", err)
+			return "tab-size-8"
+		}
+		if def.TabWidth > 0 {
+			return fmt.Sprintf("tab-size-%d", def.TabWidth)
+		}
+	}
+	return "tab-size-8"
+}
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
new file mode 100644
index 0000000000..a59ddd3f17
--- /dev/null
+++ b/modules/templates/util_render.go
@@ -0,0 +1,254 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+	"context"
+	"encoding/hex"
+	"fmt"
+	"html/template"
+	"math"
+	"net/url"
+	"regexp"
+	"strings"
+	"unicode"
+
+	issues_model "code.gitea.io/gitea/models/issues"
+	"code.gitea.io/gitea/modules/emoji"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/markup/markdown"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+// RenderCommitMessage renders commit message with XSS-safe and special links.
+func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
+	return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
+}
+
+// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
+// default url, handling for special links.
+func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
+	cleanMsg := template.HTMLEscapeString(msg)
+	// we can safely assume that it will not return any error, since there
+	// shouldn't be any special HTML.
+	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
+		Ctx:         ctx,
+		URLPrefix:   urlPrefix,
+		DefaultLink: urlDefault,
+		Metas:       metas,
+	}, cleanMsg)
+	if err != nil {
+		log.Error("RenderCommitMessage: %v", err)
+		return ""
+	}
+	msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
+	if len(msgLines) == 0 {
+		return template.HTML("")
+	}
+	return template.HTML(msgLines[0])
+}
+
+// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
+// the provided default url, handling for special links without email to links.
+func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
+	msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
+	lineEnd := strings.IndexByte(msgLine, '\n')
+	if lineEnd > 0 {
+		msgLine = msgLine[:lineEnd]
+	}
+	msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
+	if len(msgLine) == 0 {
+		return template.HTML("")
+	}
+
+	// we can safely assume that it will not return any error, since there
+	// shouldn't be any special HTML.
+	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
+		Ctx:         ctx,
+		URLPrefix:   urlPrefix,
+		DefaultLink: urlDefault,
+		Metas:       metas,
+	}, template.HTMLEscapeString(msgLine))
+	if err != nil {
+		log.Error("RenderCommitMessageSubject: %v", err)
+		return template.HTML("")
+	}
+	return template.HTML(renderedMessage)
+}
+
+// RenderCommitBody extracts the body of a commit message without its title.
+func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
+	msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
+	lineEnd := strings.IndexByte(msgLine, '\n')
+	if lineEnd > 0 {
+		msgLine = msgLine[lineEnd+1:]
+	} else {
+		return template.HTML("")
+	}
+	msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
+	if len(msgLine) == 0 {
+		return template.HTML("")
+	}
+
+	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
+		Ctx:       ctx,
+		URLPrefix: urlPrefix,
+		Metas:     metas,
+	}, template.HTMLEscapeString(msgLine))
+	if err != nil {
+		log.Error("RenderCommitMessage: %v", err)
+		return ""
+	}
+	return template.HTML(renderedMessage)
+}
+
+// Match text that is between back ticks.
+var codeMatcher = regexp.MustCompile("`([^`]+)`")
+
+// RenderCodeBlock renders "`…`" as highlighted "<code>" block.
+// Intended for issue and PR titles, these containers should have styles for "<code>" elements
+func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
+	htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "<code>$1</code>") // replace with HTML <code> tags
+	return template.HTML(htmlWithCodeTags)
+}
+
+// RenderIssueTitle renders issue/pull title with defined post processors
+func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML {
+	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
+		Ctx:       ctx,
+		URLPrefix: urlPrefix,
+		Metas:     metas,
+	}, template.HTMLEscapeString(text))
+	if err != nil {
+		log.Error("RenderIssueTitle: %v", err)
+		return template.HTML("")
+	}
+	return template.HTML(renderedText)
+}
+
+// RenderLabel renders a label
+func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
+	labelScope := label.ExclusiveScope()
+
+	textColor := "#111"
+	if label.UseLightTextColor() {
+		textColor = "#eee"
+	}
+
+	description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
+
+	if labelScope == "" {
+		// Regular label
+		s := fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' title='%s'>%s</div>",
+			textColor, label.Color, description, RenderEmoji(ctx, label.Name))
+		return template.HTML(s)
+	}
+
+	// Scoped label
+	scopeText := RenderEmoji(ctx, labelScope)
+	itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
+
+	itemColor := label.Color
+	scopeColor := label.Color
+	if r, g, b, err := label.ColorRGB(); err == nil {
+		// Make scope and item background colors slightly darker and lighter respectively.
+		// More contrast needed with higher luminance, empirically tweaked.
+		luminance := (0.299*r + 0.587*g + 0.114*b) / 255
+		contrast := 0.01 + luminance*0.03
+		// Ensure we add the same amount of contrast also near 0 and 1.
+		darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
+		lighten := contrast + math.Max(contrast-luminance, 0.0)
+		// Compute factor to keep RGB values proportional.
+		darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
+		lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
+
+		scopeBytes := []byte{
+			uint8(math.Min(math.Round(r*darkenFactor), 255)),
+			uint8(math.Min(math.Round(g*darkenFactor), 255)),
+			uint8(math.Min(math.Round(b*darkenFactor), 255)),
+		}
+		itemBytes := []byte{
+			uint8(math.Min(math.Round(r*lightenFactor), 255)),
+			uint8(math.Min(math.Round(g*lightenFactor), 255)),
+			uint8(math.Min(math.Round(b*lightenFactor), 255)),
+		}
+
+		itemColor = "#" + hex.EncodeToString(itemBytes)
+		scopeColor = "#" + hex.EncodeToString(scopeBytes)
+	}
+
+	s := fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
+		"<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
+		"<div class='ui label scope-right' style='color: %s !important; background-color: %s !important''>%s</div>"+
+		"</span>",
+		description,
+		textColor, scopeColor, scopeText,
+		textColor, itemColor, itemText)
+	return template.HTML(s)
+}
+
+// RenderEmoji renders html text with emoji post processors
+func RenderEmoji(ctx context.Context, text string) template.HTML {
+	renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
+		template.HTMLEscapeString(text))
+	if err != nil {
+		log.Error("RenderEmoji: %v", err)
+		return template.HTML("")
+	}
+	return template.HTML(renderedText)
+}
+
+// ReactionToEmoji renders emoji for use in reactions
+func ReactionToEmoji(reaction string) template.HTML {
+	val := emoji.FromCode(reaction)
+	if val != nil {
+		return template.HTML(val.Emoji)
+	}
+	val = emoji.FromAlias(reaction)
+	if val != nil {
+		return template.HTML(val.Emoji)
+	}
+	return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
+}
+
+// RenderNote renders the contents of a git-notes file as a commit message.
+func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
+	cleanMsg := template.HTMLEscapeString(msg)
+	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
+		Ctx:       ctx,
+		URLPrefix: urlPrefix,
+		Metas:     metas,
+	}, cleanMsg)
+	if err != nil {
+		log.Error("RenderNote: %v", err)
+		return ""
+	}
+	return template.HTML(fullMessage)
+}
+
+func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive
+	output, err := markdown.RenderString(&markup.RenderContext{
+		Ctx:       ctx,
+		URLPrefix: setting.AppSubURL,
+	}, input)
+	if err != nil {
+		log.Error("RenderString: %v", err)
+	}
+	return template.HTML(output)
+}
+
+func RenderLabels(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)
+}
diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go
index 42d11fc990..459380aee5 100644
--- a/modules/templates/util_string.go
+++ b/modules/templates/util_string.go
@@ -3,12 +3,18 @@
 
 package templates
 
-import "strings"
+import (
+	"strings"
+
+	"code.gitea.io/gitea/modules/base"
+)
 
 type StringUtils struct{}
 
+var stringUtils = StringUtils{}
+
 func NewStringUtils() *StringUtils {
-	return &StringUtils{}
+	return &stringUtils
 }
 
 func (su *StringUtils) HasPrefix(s, prefix string) bool {
@@ -22,3 +28,11 @@ func (su *StringUtils) Contains(s, substr string) bool {
 func (su *StringUtils) Split(s, sep string) []string {
 	return strings.Split(s, sep)
 }
+
+func (su *StringUtils) Join(a []string, sep string) string {
+	return strings.Join(a, sep)
+}
+
+func (su *StringUtils) EllipsisString(s string, max int) string {
+	return base.EllipsisString(s, max)
+}
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 0aed59ffab..2c8fe724e2 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -334,7 +334,7 @@
 
 					<div class="field">
 						<label for="oauth2_scopes">{{.locale.Tr "admin.auths.oauth2_scopes"}}</label>
-						<input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{Join $cfg.Scopes ","}}{{end}}">
+						<input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{StringUtils.Join $cfg.Scopes ","}}{{end}}">
 					</div>
 					<div class="field">
 						<label for="oauth2_required_claim_name">{{.locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index f0d0ad3643..c4f77ec1ae 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -365,7 +365,7 @@
 					<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 					<dd>{{.Name}} ({{.Provider}})</dd>
 					<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
-					<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd>
+					<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd>
 				{{end}}
 				<div class="ui divider"></div>
 				<dt>{{$.locale.Tr "admin.config.router_log_mode"}}</dt>
@@ -378,7 +378,7 @@
 							<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 							<dd>{{.Name}} ({{.Provider}})</dd>
 							<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
-							<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd>
+							<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd>
 						{{end}}
 					{{else}}
 						<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd>
@@ -393,7 +393,7 @@
 							<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 							<dd>{{.Name}} ({{.Provider}})</dd>
 							<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
-							<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd>
+							<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd>
 						{{end}}
 					{{else}}
 						<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd>
@@ -412,7 +412,7 @@
 							<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 							<dd>{{.Name}} ({{.Provider}})</dd>
 							<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
-							<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd>
+							<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd>
 						{{end}}
 					{{else}}
 						<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd>
diff --git a/templates/admin/queue.tmpl b/templates/admin/queue.tmpl
index 3de01a32ab..84eb8892ef 100644
--- a/templates/admin/queue.tmpl
+++ b/templates/admin/queue.tmpl
@@ -174,7 +174,7 @@
 			{{.locale.Tr "admin.monitor.queue.configuration"}}
 		</h4>
 		<div class="ui attached segment">
-			<pre>{{.Queue.Configuration | JsonPrettyPrint}}</pre>
+			<pre>{{JsonUtils.PrettyIndent .Queue.Configuration}}</pre>
 		</div>
 	</div>
 
diff --git a/templates/package/shared/cleanup_rules/list.tmpl b/templates/package/shared/cleanup_rules/list.tmpl
index 09f95e4f4a..10a073eb55 100644
--- a/templates/package/shared/cleanup_rules/list.tmpl
+++ b/templates/package/shared/cleanup_rules/list.tmpl
@@ -22,9 +22,9 @@
 					<a class="item" href="{{$.Link}}/rules/{{.ID}}"><strong>{{.Type.Name}}</strong></a>
 					<div><i>{{if .Enabled}}{{$.locale.Tr "enabled"}}{{else}}{{$.locale.Tr "disabled"}}{{end}}</i></div>
 					{{if .KeepCount}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.count"}}:</i> {{if eq .KeepCount 1}}{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.count.1"}}{{else}}{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.count.n" .KeepCount}}{{end}}</div>{{end}}
-					{{if .KeepPattern}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}:</i> {{EllipsisString .KeepPattern 100}}</div>{{end}}
+					{{if .KeepPattern}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}:</i> {{StringUtils.EllipsisString .KeepPattern 100}}</div>{{end}}
 					{{if .RemoveDays}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.days"}}:</i> {{$.locale.Tr "tool.days" .RemoveDays}}</div>{{end}}
-					{{if .RemovePattern}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.pattern"}}:</i> {{EllipsisString .RemovePattern 100}}</div>{{end}}
+					{{if .RemovePattern}}<div><i>{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.pattern"}}:</i> {{StringUtils.EllipsisString .RemovePattern 100}}</div>{{end}}
 				</div>
 			</div>
 		{{else}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index c47fa9d9ca..de7c3a1dd0 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -68,7 +68,13 @@
 				{{$l := Eval $n "-" 1}}
 				<!-- If home page, show new pr. If not, show breadcrumb -->
 				{{if and (eq $n 0) .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
-					<a id="new-pull-request" role="button" class="ui compact basic button" href="{{CompareLink .BaseRepo .Repository .BranchName}}"
+					{{$cmpBranch := ""}}
+					{{if ne .Repository.ID .BaseRepo.ID}}
+						{{$cmpBranch = printf "%s/%s:" (.Repository.OwnerName|PathEscape) (.Repository.Name|PathEscape)}}
+					{{end}}
+					{{$cmpBranch = printf "%s%s" $cmpBranch (.BranchName|PathEscapeSegments)}}
+					{{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}}
+					<a id="new-pull-request" role="button" class="ui compact basic button" href="{{$compareLink}}"
 						data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{.locale.Tr "action.compare_branch"}}{{end}}">
 						{{svg "octicon-git-pull-request"}}
 					</a>
@@ -103,7 +109,17 @@
 					</a>
 				{{end}}
 				{{if ne $n 0}}
-					<span class="ui breadcrumb repo-path gt-ml-2"><a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{EllipsisString .Repository.Name 30}}</a>{{range $i, $v := .TreeNames}}<span class="divider">/</span>{{if eq $i $l}}<span class="active section" title="{{$v}}">{{EllipsisString $v 30}}</span>{{else}}{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{EllipsisString $v 30}}</a></span>{{end}}{{end}}</span>
+					<span class="ui breadcrumb repo-path gt-ml-2">
+						<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
+						{{- range $i, $v := .TreeNames -}}
+							<span class="divider">/</span>
+							{{- if eq $i $l -}}
+								<span class="active section" title="{{$v}}">{{StringUtils.EllipsisString $v 30}}</span>
+							{{- else -}}
+								{{$p := index $.Paths $i}}<span class="section"><a href="{{$.BranchLink}}/{{PathEscapeSegments $p}}" title="{{$v}}">{{StringUtils.EllipsisString $v 30}}</a></span>
+							{{- end -}}
+						{{- end -}}
+					</span>
 				{{end}}
 			</div>
 			<div class="gt-df gt-ac">
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index d00a4813d2..d673e89a39 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -13,7 +13,7 @@
 					<div class="field">
 						<input name="title" id="issue_title" placeholder="{{.locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" tabindex="3" autofocus required maxlength="255" autocomplete="off">
 						{{if .PageIsComparePull}}
-							<div class="title_wip_desc" data-wip-prefixes="{{Json .PullRequestWorkInProgressPrefixes}}">{{.locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}</div>
+							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{.locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}</div>
 						{{end}}
 					</div>
 					{{if .Fields}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index b99e49a586..464f41be1a 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -304,10 +304,12 @@
 				{{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}}
 				<span class="text grey muted-links">
 					{{template "shared/user/authorlink" .Poster}}
-					{{$parsedDeadline := .Content | ParseDeadline}}
-					{{$from := DateTime "long" (index $parsedDeadline 1)}}
-					{{$to := DateTime "long" (index $parsedDeadline 0)}}
-					{{$.locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}}
+					{{$parsedDeadline := StringUtils.Split .Content "|"}}
+					{{if eq (len $parsedDeadline) 2}}
+						{{$from := DateTime "long" (index $parsedDeadline 1)}}
+						{{$to := DateTime "long" (index $parsedDeadline 0)}}
+						{{$.locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}}
+					{{end}}
 				</span>
 			</div>
 		{{else if eq .Type 18}}
diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl
index fe8a6cfc55..2d34613dde 100644
--- a/templates/repo/release/new.tmpl
+++ b/templates/repo/release/new.tmpl
@@ -20,7 +20,7 @@
 						<b>{{.tag_name}}</b><span class="at">@</span><strong>{{.tag_target}}</strong>
 					{{else}}
 						<input id="tag-name" name="tag_name" value="{{.tag_name}}" aria-label="{{.locale.Tr "repo.release.tag_name"}}" placeholder="{{.locale.Tr "repo.release.tag_name"}}" autofocus required maxlength="255">
-						<input id="tag-name-editor" type="hidden" data-existing-tags={{Json .Tags}} data-tag-helper={{.locale.Tr "repo.release.tag_helper"}} data-tag-helper-new={{.locale.Tr "repo.release.tag_helper_new"}} data-tag-helper-existing={{.locale.Tr "repo.release.tag_helper_existing"}}>
+						<input id="tag-name-editor" type="hidden" data-existing-tags="{{JsonUtils.EncodeToString .Tags}}" data-tag-helper="{{.locale.Tr "repo.release.tag_helper"}}" data-tag-helper-new="{{.locale.Tr "repo.release.tag_helper_new"}}" data-tag-helper-existing="{{.locale.Tr "repo.release.tag_helper_existing"}}">
 						<div id="tag-target-selector" class="gt-dib">
 							<span class="at">@</span>
 							<div class="ui selection dropdown">
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
index 9540c872c2..602cda3614 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -61,13 +61,15 @@
 						{{else}}
 							{{if $entry.IsDir}}
 								{{$subJumpablePathName := $entry.GetSubJumpablePathName}}
-								{{$subJumpablePath := SubJumpablePath $subJumpablePathName}}
 								{{svg "octicon-file-directory-fill"}}
 								<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}">
-									{{if eq (len $subJumpablePath) 2}}
-										<span class="color-text-light-2">{{index  $subJumpablePath 0}}</span>{{index  $subJumpablePath 1}}
+									{{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}}
+									{{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}}
+									{{if eq $subJumpablePathFieldLast 0}}
+										{{$subJumpablePathName}}
 									{{else}}
-										{{index $subJumpablePath 0}}
+										{{$subJumpablePathPrefixes := slice $subJumpablePathFields 0 $subJumpablePathFieldLast}}
+										<span class="color-text-light-2">{{StringUtils.Join $subJumpablePathPrefixes "/"}}</span>/{{index $subJumpablePathFields $subJumpablePathFieldLast}}
 									{{end}}
 								</a>
 							{{else}}
diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl
index c9edc59b1d..94da2269b7 100644
--- a/templates/shared/actions/runner_edit.tmpl
+++ b/templates/shared/actions/runner_edit.tmpl
@@ -37,7 +37,7 @@
 			</div>
 			<div class="field" data-tooltip-content="Labels are comma-separated. Whitespace at the beginning, end, and around the commas are ignored.">
 				<label for="custom_labels">{{.locale.Tr "actions.runners.custom_labels"}}</label>
-				<input id="custom_labels" name="custom_labels" value="{{Join .Runner.CustomLabels `,`}}">
+				<input id="custom_labels" name="custom_labels" value="{{StringUtils.Join .Runner.CustomLabels `,`}}">
 				<p class="help">{{.locale.Tr "actions.runners.custom_labels_helper"}}</p>
 			</div>
 
diff --git a/templates/user/heatmap.tmpl b/templates/user/heatmap.tmpl
index 5d42a5435b..b0ee0eeaac 100644
--- a/templates/user/heatmap.tmpl
+++ b/templates/user/heatmap.tmpl
@@ -1,6 +1,6 @@
 {{if .HeatmapData}}
 	<div id="user-heatmap"
-		data-heatmap-data="{{Json .HeatmapData}}"
+		data-heatmap-data="{{JsonUtils.EncodeToString .HeatmapData}}"
 		data-locale-total-contributions="{{$.locale.Tr "heatmap.number_of_contributions_in_the_last_12_months" ($.locale.PrettyNumber .HeatmapTotalContributions)}}"
 		data-locale-no-contributions="{{.locale.Tr "heatmap.no_contributions"}}"
 		data-locale-more="{{.locale.Tr "heatmap.more"}}"