mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Improve template helper (#24417)
It seems that we really need the "context function" soon. So we should clean up the helper functions first. Major changes: * Improve StringUtils and add JsonUtils * Remove one-time-use helper functions like CompareLink * Move other code (no change) to util_avatar/util_render/util_misc (no need to propose changes for them) I have tested the changed templates:     --------- Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
		@@ -5,46 +5,25 @@
 | 
				
			|||||||
package templates
 | 
					package templates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
					 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"encoding/hex"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"html"
 | 
						"html"
 | 
				
			||||||
	"html/template"
 | 
						"html/template"
 | 
				
			||||||
	"math"
 | 
					 | 
				
			||||||
	"mime"
 | 
					 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"path/filepath"
 | 
					 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"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"
 | 
						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/base"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/emoji"
 | 
						"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"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/repository"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/svg"
 | 
						"code.gitea.io/gitea/modules/svg"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/templates/eval"
 | 
						"code.gitea.io/gitea/modules/templates/eval"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
						"code.gitea.io/gitea/modules/timeutil"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
	"code.gitea.io/gitea/services/gitdiff"
 | 
						"code.gitea.io/gitea/services/gitdiff"
 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/editorconfig/editorconfig-core-go/v2"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Used from static.go && dynamic.go
 | 
					// Used from static.go && dynamic.go
 | 
				
			||||||
@@ -53,6 +32,8 @@ 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{}{
 | 
				
			||||||
 | 
							"DumpVar": dumpVar,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// -----------------------------------------------------------------
 | 
							// -----------------------------------------------------------------
 | 
				
			||||||
		// html/template related functions
 | 
							// html/template related functions
 | 
				
			||||||
		"dict":        dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
 | 
							"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,
 | 
							"JSEscape":    template.JSEscapeString,
 | 
				
			||||||
		"Str2html":    Str2html, // TODO: rename it to SanitizeHTML
 | 
							"Str2html":    Str2html, // TODO: rename it to SanitizeHTML
 | 
				
			||||||
		"URLJoin":     util.URLJoin,
 | 
							"URLJoin":     util.URLJoin,
 | 
				
			||||||
 | 
							"DotEscape":   DotEscape,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		"PathEscape":         url.PathEscape,
 | 
							"PathEscape":         url.PathEscape,
 | 
				
			||||||
		"PathEscapeSegments": util.PathEscapeSegments,
 | 
							"PathEscapeSegments": util.PathEscapeSegments,
 | 
				
			||||||
@@ -70,30 +52,7 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		// utils
 | 
							// utils
 | 
				
			||||||
		"StringUtils": NewStringUtils,
 | 
							"StringUtils": NewStringUtils,
 | 
				
			||||||
		"SliceUtils":  NewSliceUtils,
 | 
							"SliceUtils":  NewSliceUtils,
 | 
				
			||||||
 | 
							"JsonUtils":   NewJsonUtils,
 | 
				
			||||||
		// -----------------------------------------------------------------
 | 
					 | 
				
			||||||
		// 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()
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// -----------------------------------------------------------------
 | 
							// -----------------------------------------------------------------
 | 
				
			||||||
		// svg / avatar / icon
 | 
							// svg / avatar / icon
 | 
				
			||||||
@@ -107,31 +66,7 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		"MigrationIcon":  MigrationIcon,
 | 
							"MigrationIcon":  MigrationIcon,
 | 
				
			||||||
		"ActionIcon":     ActionIcon,
 | 
							"ActionIcon":     ActionIcon,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		"SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
 | 
							"SortArrow": SortArrow,
 | 
				
			||||||
			// 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
 | 
							// time / number / format
 | 
				
			||||||
@@ -242,32 +177,9 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		"ReactionToEmoji":  ReactionToEmoji,
 | 
							"ReactionToEmoji":  ReactionToEmoji,
 | 
				
			||||||
		"RenderNote":       RenderNote,
 | 
							"RenderNote":       RenderNote,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		"RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML {
 | 
							"RenderMarkdownToHtml": RenderMarkdownToHtml,
 | 
				
			||||||
			output, err := markdown.RenderString(&markup.RenderContext{
 | 
							"RenderLabel":          RenderLabel,
 | 
				
			||||||
				Ctx:       ctx,
 | 
							"RenderLabels":         RenderLabels,
 | 
				
			||||||
				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
 | 
							// misc
 | 
				
			||||||
@@ -278,124 +190,11 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		"CommentMustAsDiff":        gitdiff.CommentMustAsDiff,
 | 
							"CommentMustAsDiff":        gitdiff.CommentMustAsDiff,
 | 
				
			||||||
		"MirrorRemoteAddress":      mirrorRemoteAddress,
 | 
							"MirrorRemoteAddress":      mirrorRemoteAddress,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		"ParseDeadline": func(deadline string) []string {
 | 
							"FilenameIsImage": FilenameIsImage,
 | 
				
			||||||
			return strings.Split(deadline, "|")
 | 
							"TabSizeClass":    TabSizeClass,
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"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,
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}}
 | 
						}}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 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
 | 
					// Safe render raw as HTML
 | 
				
			||||||
func Safe(raw string) template.HTML {
 | 
					func Safe(raw string) template.HTML {
 | 
				
			||||||
	return template.HTML(raw)
 | 
						return template.HTML(raw)
 | 
				
			||||||
@@ -411,342 +210,6 @@ func DotEscape(raw string) string {
 | 
				
			|||||||
	return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
 | 
						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.
 | 
					// 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.
 | 
					// To use this helper function in templates, pass each token as a separate parameter.
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										84
									
								
								modules/templates/util_avatar.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								modules/templates/util_avatar.go
									
									
									
									
									
										Normal file
									
								
							@@ -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("")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								modules/templates/util_json.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								modules/templates/util_json.go
									
									
									
									
									
										Normal file
									
								
							@@ -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()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										209
									
								
								modules/templates/util_misc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								modules/templates/util_misc.go
									
									
									
									
									
										Normal file
									
								
							@@ -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"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										254
									
								
								modules/templates/util_render.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								modules/templates/util_render.go
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,12 +3,18 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
package templates
 | 
					package templates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import "strings"
 | 
					import (
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/base"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type StringUtils struct{}
 | 
					type StringUtils struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var stringUtils = StringUtils{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewStringUtils() *StringUtils {
 | 
					func NewStringUtils() *StringUtils {
 | 
				
			||||||
	return &StringUtils{}
 | 
						return &stringUtils
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (su *StringUtils) HasPrefix(s, prefix string) bool {
 | 
					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 {
 | 
					func (su *StringUtils) Split(s, sep string) []string {
 | 
				
			||||||
	return strings.Split(s, sep)
 | 
						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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -334,7 +334,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
					<div class="field">
 | 
										<div class="field">
 | 
				
			||||||
						<label for="oauth2_scopes">{{.locale.Tr "admin.auths.oauth2_scopes"}}</label>
 | 
											<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>
 | 
				
			||||||
					<div class="field">
 | 
										<div class="field">
 | 
				
			||||||
						<label for="oauth2_required_claim_name">{{.locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
 | 
											<label for="oauth2_required_claim_name">{{.locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -365,7 +365,7 @@
 | 
				
			|||||||
					<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 | 
										<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 | 
				
			||||||
					<dd>{{.Name}} ({{.Provider}})</dd>
 | 
										<dd>{{.Name}} ({{.Provider}})</dd>
 | 
				
			||||||
					<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
 | 
										<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
 | 
				
			||||||
					<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd>
 | 
										<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd>
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
				<div class="ui divider"></div>
 | 
									<div class="ui divider"></div>
 | 
				
			||||||
				<dt>{{$.locale.Tr "admin.config.router_log_mode"}}</dt>
 | 
									<dt>{{$.locale.Tr "admin.config.router_log_mode"}}</dt>
 | 
				
			||||||
@@ -378,7 +378,7 @@
 | 
				
			|||||||
							<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 | 
												<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 | 
				
			||||||
							<dd>{{.Name}} ({{.Provider}})</dd>
 | 
												<dd>{{.Name}} ({{.Provider}})</dd>
 | 
				
			||||||
							<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
 | 
												<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
 | 
				
			||||||
							<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd>
 | 
												<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd>
 | 
				
			||||||
						{{end}}
 | 
											{{end}}
 | 
				
			||||||
					{{else}}
 | 
										{{else}}
 | 
				
			||||||
						<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd>
 | 
											<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd>
 | 
				
			||||||
@@ -393,7 +393,7 @@
 | 
				
			|||||||
							<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 | 
												<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 | 
				
			||||||
							<dd>{{.Name}} ({{.Provider}})</dd>
 | 
												<dd>{{.Name}} ({{.Provider}})</dd>
 | 
				
			||||||
							<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
 | 
												<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
 | 
				
			||||||
							<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd>
 | 
												<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd>
 | 
				
			||||||
						{{end}}
 | 
											{{end}}
 | 
				
			||||||
					{{else}}
 | 
										{{else}}
 | 
				
			||||||
						<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd>
 | 
											<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd>
 | 
				
			||||||
@@ -412,7 +412,7 @@
 | 
				
			|||||||
							<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 | 
												<dt>{{$.locale.Tr "admin.config.log_mode"}}</dt>
 | 
				
			||||||
							<dd>{{.Name}} ({{.Provider}})</dd>
 | 
												<dd>{{.Name}} ({{.Provider}})</dd>
 | 
				
			||||||
							<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
 | 
												<dt>{{$.locale.Tr "admin.config.log_config"}}</dt>
 | 
				
			||||||
							<dd><pre>{{.Config | JsonPrettyPrint}}</pre></dd>
 | 
												<dd><pre>{{JsonUtils.PrettyIndent .Config}}</pre></dd>
 | 
				
			||||||
						{{end}}
 | 
											{{end}}
 | 
				
			||||||
					{{else}}
 | 
										{{else}}
 | 
				
			||||||
						<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd>
 | 
											<dd>{{$.locale.Tr "admin.config.routes_to_default_logger"}}</dd>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -174,7 +174,7 @@
 | 
				
			|||||||
			{{.locale.Tr "admin.monitor.queue.configuration"}}
 | 
								{{.locale.Tr "admin.monitor.queue.configuration"}}
 | 
				
			||||||
		</h4>
 | 
							</h4>
 | 
				
			||||||
		<div class="ui attached segment">
 | 
							<div class="ui attached segment">
 | 
				
			||||||
			<pre>{{.Queue.Configuration | JsonPrettyPrint}}</pre>
 | 
								<pre>{{JsonUtils.PrettyIndent .Queue.Configuration}}</pre>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,9 +22,9 @@
 | 
				
			|||||||
					<a class="item" href="{{$.Link}}/rules/{{.ID}}"><strong>{{.Type.Name}}</strong></a>
 | 
										<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>
 | 
										<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 .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 .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>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		{{else}}
 | 
							{{else}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,7 +68,13 @@
 | 
				
			|||||||
				{{$l := Eval $n "-" 1}}
 | 
									{{$l := Eval $n "-" 1}}
 | 
				
			||||||
				<!-- If home page, show new pr. If not, show breadcrumb -->
 | 
									<!-- If home page, show new pr. If not, show breadcrumb -->
 | 
				
			||||||
				{{if and (eq $n 0) .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
 | 
									{{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}}">
 | 
											data-tooltip-content="{{if .PullRequestCtx.Allowed}}{{.locale.Tr "repo.pulls.compare_changes"}}{{else}}{{.locale.Tr "action.compare_branch"}}{{end}}">
 | 
				
			||||||
						{{svg "octicon-git-pull-request"}}
 | 
											{{svg "octicon-git-pull-request"}}
 | 
				
			||||||
					</a>
 | 
										</a>
 | 
				
			||||||
@@ -103,7 +109,17 @@
 | 
				
			|||||||
					</a>
 | 
										</a>
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
				{{if ne $n 0}}
 | 
									{{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}}
 | 
									{{end}}
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div class="gt-df gt-ac">
 | 
								<div class="gt-df gt-ac">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@
 | 
				
			|||||||
					<div class="field">
 | 
										<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">
 | 
											<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}}
 | 
											{{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}}
 | 
											{{end}}
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
					{{if .Fields}}
 | 
										{{if .Fields}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -304,10 +304,12 @@
 | 
				
			|||||||
				{{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}}
 | 
									{{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}}
 | 
				
			||||||
				<span class="text grey muted-links">
 | 
									<span class="text grey muted-links">
 | 
				
			||||||
					{{template "shared/user/authorlink" .Poster}}
 | 
										{{template "shared/user/authorlink" .Poster}}
 | 
				
			||||||
					{{$parsedDeadline := .Content | ParseDeadline}}
 | 
										{{$parsedDeadline := StringUtils.Split .Content "|"}}
 | 
				
			||||||
 | 
										{{if eq (len $parsedDeadline) 2}}
 | 
				
			||||||
						{{$from := DateTime "long" (index $parsedDeadline 1)}}
 | 
											{{$from := DateTime "long" (index $parsedDeadline 1)}}
 | 
				
			||||||
						{{$to := DateTime "long" (index $parsedDeadline 0)}}
 | 
											{{$to := DateTime "long" (index $parsedDeadline 0)}}
 | 
				
			||||||
						{{$.locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}}
 | 
											{{$.locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}}
 | 
				
			||||||
 | 
										{{end}}
 | 
				
			||||||
				</span>
 | 
									</span>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		{{else if eq .Type 18}}
 | 
							{{else if eq .Type 18}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@
 | 
				
			|||||||
						<b>{{.tag_name}}</b><span class="at">@</span><strong>{{.tag_target}}</strong>
 | 
											<b>{{.tag_name}}</b><span class="at">@</span><strong>{{.tag_target}}</strong>
 | 
				
			||||||
					{{else}}
 | 
										{{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" 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">
 | 
											<div id="tag-target-selector" class="gt-dib">
 | 
				
			||||||
							<span class="at">@</span>
 | 
												<span class="at">@</span>
 | 
				
			||||||
							<div class="ui selection dropdown">
 | 
												<div class="ui selection dropdown">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,13 +61,15 @@
 | 
				
			|||||||
						{{else}}
 | 
											{{else}}
 | 
				
			||||||
							{{if $entry.IsDir}}
 | 
												{{if $entry.IsDir}}
 | 
				
			||||||
								{{$subJumpablePathName := $entry.GetSubJumpablePathName}}
 | 
													{{$subJumpablePathName := $entry.GetSubJumpablePathName}}
 | 
				
			||||||
								{{$subJumpablePath := SubJumpablePath $subJumpablePathName}}
 | 
					 | 
				
			||||||
								{{svg "octicon-file-directory-fill"}}
 | 
													{{svg "octicon-file-directory-fill"}}
 | 
				
			||||||
								<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}">
 | 
													<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}">
 | 
				
			||||||
									{{if eq (len $subJumpablePath) 2}}
 | 
														{{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}}
 | 
				
			||||||
										<span class="color-text-light-2">{{index  $subJumpablePath 0}}</span>{{index  $subJumpablePath 1}}
 | 
														{{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}}
 | 
				
			||||||
 | 
														{{if eq $subJumpablePathFieldLast 0}}
 | 
				
			||||||
 | 
															{{$subJumpablePathName}}
 | 
				
			||||||
									{{else}}
 | 
														{{else}}
 | 
				
			||||||
										{{index $subJumpablePath 0}}
 | 
															{{$subJumpablePathPrefixes := slice $subJumpablePathFields 0 $subJumpablePathFieldLast}}
 | 
				
			||||||
 | 
															<span class="color-text-light-2">{{StringUtils.Join $subJumpablePathPrefixes "/"}}</span>/{{index $subJumpablePathFields $subJumpablePathFieldLast}}
 | 
				
			||||||
									{{end}}
 | 
														{{end}}
 | 
				
			||||||
								</a>
 | 
													</a>
 | 
				
			||||||
							{{else}}
 | 
												{{else}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,7 +37,7 @@
 | 
				
			|||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div class="field" data-tooltip-content="Labels are comma-separated. Whitespace at the beginning, end, and around the commas are ignored.">
 | 
								<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>
 | 
									<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>
 | 
									<p class="help">{{.locale.Tr "actions.runners.custom_labels_helper"}}</p>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{{if .HeatmapData}}
 | 
					{{if .HeatmapData}}
 | 
				
			||||||
	<div id="user-heatmap"
 | 
						<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-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-no-contributions="{{.locale.Tr "heatmap.no_contributions"}}"
 | 
				
			||||||
		data-locale-more="{{.locale.Tr "heatmap.more"}}"
 | 
							data-locale-more="{{.locale.Tr "heatmap.more"}}"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user