summaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2023-04-29 20:02:29 +0800
committerGitHub <noreply@github.com>2023-04-29 08:02:29 -0400
commit241b74f6c536c1d7de3b4e79e552bf1a3264cc6d (patch)
treeb6e482d056eeb1d00ca48de9161dc1123f6bbaf4 /modules
parent5a5ab8ef5ac5fbdb893707933f06ff6bcd8e834a (diff)
downloadgitea-241b74f6c536c1d7de3b4e79e552bf1a3264cc6d.tar.gz
gitea-241b74f6c536c1d7de3b4e79e552bf1a3264cc6d.zip
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: ![image](https://user-images.githubusercontent.com/2114189/235283862-608dbf6b-2da3-4d06-8157-b523ca93edb4.png) ![image](https://user-images.githubusercontent.com/2114189/235283888-1dfc0471-e622-4d64-9d76-7859819580d3.png) ![image](https://user-images.githubusercontent.com/2114189/235283903-d559f14d-4abb-4a50-915f-2b9cbc381a7a.png) ![image](https://user-images.githubusercontent.com/2114189/235283955-b7b5adea-aca3-4758-b38a-3aae3f7c6048.png) --------- Co-authored-by: Giteabot <teabot@gitea.io>
Diffstat (limited to 'modules')
-rw-r--r--modules/templates/helper.go557
-rw-r--r--modules/templates/util_avatar.go84
-rw-r--r--modules/templates/util_json.go35
-rw-r--r--modules/templates/util_misc.go209
-rw-r--r--modules/templates/util_render.go254
-rw-r--r--modules/templates/util_string.go18
6 files changed, 608 insertions, 549 deletions
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index a290d38979..20261eb959 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -5,46 +5,25 @@
package templates
import (
- "bytes"
"context"
- "encoding/hex"
"fmt"
"html"
"html/template"
- "math"
- "mime"
"net/url"
- "path/filepath"
"regexp"
"strings"
"time"
- "unicode"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/avatars"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
- user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/emoji"
- "code.gitea.io/gitea/modules/git"
- giturl "code.gitea.io/gitea/modules/git/url"
- gitea_html "code.gitea.io/gitea/modules/html"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/templates/eval"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/gitdiff"
-
- "github.com/editorconfig/editorconfig-core-go/v2"
)
// Used from static.go && dynamic.go
@@ -53,6 +32,8 @@ var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}[\s]*$`)
// NewFuncMap returns functions for injecting to templates
func NewFuncMap() []template.FuncMap {
return []template.FuncMap{map[string]interface{}{
+ "DumpVar": dumpVar,
+
// -----------------------------------------------------------------
// html/template related functions
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
@@ -63,6 +44,7 @@ func NewFuncMap() []template.FuncMap {
"JSEscape": template.JSEscapeString,
"Str2html": Str2html, // TODO: rename it to SanitizeHTML
"URLJoin": util.URLJoin,
+ "DotEscape": DotEscape,
"PathEscape": url.PathEscape,
"PathEscapeSegments": util.PathEscapeSegments,
@@ -70,30 +52,7 @@ func NewFuncMap() []template.FuncMap {
// utils
"StringUtils": NewStringUtils,
"SliceUtils": NewSliceUtils,
-
- // -----------------------------------------------------------------
- // string / json
- // TODO: move string helper functions to StringUtils
- "Join": strings.Join,
- "DotEscape": DotEscape,
- "EllipsisString": base.EllipsisString,
- "DumpVar": dumpVar,
-
- "Json": func(in interface{}) string {
- out, err := json.Marshal(in)
- if err != nil {
- return ""
- }
- return string(out)
- },
- "JsonPrettyPrint": func(in string) string {
- var out bytes.Buffer
- err := json.Indent(&out, []byte(in), "", " ")
- if err != nil {
- return ""
- }
- return out.String()
- },
+ "JsonUtils": NewJsonUtils,
// -----------------------------------------------------------------
// svg / avatar / icon
@@ -107,31 +66,7 @@ func NewFuncMap() []template.FuncMap {
"MigrationIcon": MigrationIcon,
"ActionIcon": ActionIcon,
- "SortArrow": func(normSort, revSort, urlSort string, isDefault bool) template.HTML {
- // if needed
- if len(normSort) == 0 || len(urlSort) == 0 {
- return ""
- }
-
- if len(urlSort) == 0 && isDefault {
- // if sort is sorted as default add arrow tho this table header
- if isDefault {
- return svg.RenderHTML("octicon-triangle-down", 16)
- }
- } else {
- // if sort arg is in url test if it correlates with column header sort arguments
- // the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
- if urlSort == normSort {
- // the table is sorted with this header normal
- return svg.RenderHTML("octicon-triangle-up", 16)
- } else if urlSort == revSort {
- // the table is sorted with this header reverse
- return svg.RenderHTML("octicon-triangle-down", 16)
- }
- }
- // the table is NOT sorted with this header
- return ""
- },
+ "SortArrow": SortArrow,
// -----------------------------------------------------------------
// time / number / format
@@ -242,32 +177,9 @@ func NewFuncMap() []template.FuncMap {
"ReactionToEmoji": ReactionToEmoji,
"RenderNote": RenderNote,
- "RenderMarkdownToHtml": func(ctx context.Context, input string) template.HTML {
- output, err := markdown.RenderString(&markup.RenderContext{
- Ctx: ctx,
- URLPrefix: setting.AppSubURL,
- }, input)
- if err != nil {
- log.Error("RenderString: %v", err)
- }
- return template.HTML(output)
- },
- "RenderLabel": func(ctx context.Context, label *issues_model.Label) template.HTML {
- return template.HTML(RenderLabel(ctx, label))
- },
- "RenderLabels": func(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
- htmlCode := `<span class="labels-list">`
- for _, label := range labels {
- // Protect against nil value in labels - shouldn't happen but would cause a panic if so
- if label == nil {
- continue
- }
- htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
- repoLink, label.ID, RenderLabel(ctx, label))
- }
- htmlCode += "</span>"
- return template.HTML(htmlCode)
- },
+ "RenderMarkdownToHtml": RenderMarkdownToHtml,
+ "RenderLabel": RenderLabel,
+ "RenderLabels": RenderLabels,
// -----------------------------------------------------------------
// misc
@@ -278,124 +190,11 @@ func NewFuncMap() []template.FuncMap {
"CommentMustAsDiff": gitdiff.CommentMustAsDiff,
"MirrorRemoteAddress": mirrorRemoteAddress,
- "ParseDeadline": func(deadline string) []string {
- return strings.Split(deadline, "|")
- },
- "FilenameIsImage": func(filename string) bool {
- mimeType := mime.TypeByExtension(filepath.Ext(filename))
- return strings.HasPrefix(mimeType, "image/")
- },
- "TabSizeClass": func(ec interface{}, filename string) string {
- var (
- value *editorconfig.Editorconfig
- ok bool
- )
- if ec != nil {
- if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
- return "tab-size-8"
- }
- def, err := value.GetDefinitionForFilename(filename)
- if err != nil {
- log.Error("tab size class: getting definition for filename: %v", err)
- return "tab-size-8"
- }
- if def.TabWidth > 0 {
- return fmt.Sprintf("tab-size-%d", def.TabWidth)
- }
- }
- return "tab-size-8"
- },
- "SubJumpablePath": func(str string) []string {
- var path []string
- index := strings.LastIndex(str, "/")
- if index != -1 && index != len(str) {
- path = append(path, str[0:index+1], str[index+1:])
- } else {
- path = append(path, str)
- }
- return path
- },
- "CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string {
- var curBranch string
- if repo.ID != baseRepo.ID {
- curBranch += fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
- }
- curBranch += util.PathEscapeSegments(branchName)
-
- return fmt.Sprintf("%s/compare/%s...%s",
- baseRepo.Link(),
- util.PathEscapeSegments(baseRepo.DefaultBranch),
- curBranch,
- )
- },
+ "FilenameIsImage": FilenameIsImage,
+ "TabSizeClass": TabSizeClass,
}}
}
-// AvatarHTML creates the HTML for an avatar
-func AvatarHTML(src string, size int, class, name string) template.HTML {
- sizeStr := fmt.Sprintf(`%d`, size)
-
- if name == "" {
- name = "avatar"
- }
-
- return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
-}
-
-// Avatar renders user avatars. args: user, size (int), class (string)
-func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML {
- size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
-
- switch t := item.(type) {
- case *user_model.User:
- src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
- if src != "" {
- return AvatarHTML(src, size, class, t.DisplayName())
- }
- case *repo_model.Collaborator:
- src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
- if src != "" {
- return AvatarHTML(src, size, class, t.DisplayName())
- }
- case *organization.Organization:
- src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
- if src != "" {
- return AvatarHTML(src, size, class, t.AsUser().DisplayName())
- }
- }
-
- return template.HTML("")
-}
-
-// AvatarByAction renders user avatars from action. args: action, size (int), class (string)
-func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML {
- action.LoadActUser(ctx)
- return Avatar(ctx, action.ActUser, others...)
-}
-
-// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
-func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
- size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
-
- src := repo.RelAvatarLink()
- if src != "" {
- return AvatarHTML(src, size, class, repo.FullName())
- }
- return template.HTML("")
-}
-
-// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
-func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML {
- size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
- src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor)
-
- if src != "" {
- return AvatarHTML(src, size, class, name)
- }
-
- return template.HTML("")
-}
-
// Safe render raw as HTML
func Safe(raw string) template.HTML {
return template.HTML(raw)
@@ -411,342 +210,6 @@ func DotEscape(raw string) string {
return strings.ReplaceAll(raw, ".", "\u200d.\u200d")
}
-// RenderCommitMessage renders commit message with XSS-safe and special links.
-func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
- return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
-}
-
-// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
-// default url, handling for special links.
-func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
- cleanMsg := template.HTMLEscapeString(msg)
- // we can safely assume that it will not return any error, since there
- // shouldn't be any special HTML.
- fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
- Ctx: ctx,
- URLPrefix: urlPrefix,
- DefaultLink: urlDefault,
- Metas: metas,
- }, cleanMsg)
- if err != nil {
- log.Error("RenderCommitMessage: %v", err)
- return ""
- }
- msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
- if len(msgLines) == 0 {
- return template.HTML("")
- }
- return template.HTML(msgLines[0])
-}
-
-// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
-// the provided default url, handling for special links without email to links.
-func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
- msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
- lineEnd := strings.IndexByte(msgLine, '\n')
- if lineEnd > 0 {
- msgLine = msgLine[:lineEnd]
- }
- msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
- if len(msgLine) == 0 {
- return template.HTML("")
- }
-
- // we can safely assume that it will not return any error, since there
- // shouldn't be any special HTML.
- renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
- Ctx: ctx,
- URLPrefix: urlPrefix,
- DefaultLink: urlDefault,
- Metas: metas,
- }, template.HTMLEscapeString(msgLine))
- if err != nil {
- log.Error("RenderCommitMessageSubject: %v", err)
- return template.HTML("")
- }
- return template.HTML(renderedMessage)
-}
-
-// RenderCommitBody extracts the body of a commit message without its title.
-func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
- msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
- lineEnd := strings.IndexByte(msgLine, '\n')
- if lineEnd > 0 {
- msgLine = msgLine[lineEnd+1:]
- } else {
- return template.HTML("")
- }
- msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
- if len(msgLine) == 0 {
- return template.HTML("")
- }
-
- renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
- Ctx: ctx,
- URLPrefix: urlPrefix,
- Metas: metas,
- }, template.HTMLEscapeString(msgLine))
- if err != nil {
- log.Error("RenderCommitMessage: %v", err)
- return ""
- }
- return template.HTML(renderedMessage)
-}
-
-// Match text that is between back ticks.
-var codeMatcher = regexp.MustCompile("`([^`]+)`")
-
-// RenderCodeBlock renders "`…`" as highlighted "<code>" block.
-// Intended for issue and PR titles, these containers should have styles for "<code>" elements
-func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
- htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "<code>$1</code>") // replace with HTML <code> tags
- return template.HTML(htmlWithCodeTags)
-}
-
-// RenderIssueTitle renders issue/pull title with defined post processors
-func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML {
- renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
- Ctx: ctx,
- URLPrefix: urlPrefix,
- Metas: metas,
- }, template.HTMLEscapeString(text))
- if err != nil {
- log.Error("RenderIssueTitle: %v", err)
- return template.HTML("")
- }
- return template.HTML(renderedText)
-}
-
-// RenderLabel renders a label
-func RenderLabel(ctx context.Context, label *issues_model.Label) string {
- labelScope := label.ExclusiveScope()
-
- textColor := "#111"
- if label.UseLightTextColor() {
- textColor = "#eee"
- }
-
- description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
-
- if labelScope == "" {
- // Regular label
- return fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' title='%s'>%s</div>",
- textColor, label.Color, description, RenderEmoji(ctx, label.Name))
- }
-
- // Scoped label
- scopeText := RenderEmoji(ctx, labelScope)
- itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
-
- itemColor := label.Color
- scopeColor := label.Color
- if r, g, b, err := label.ColorRGB(); err == nil {
- // Make scope and item background colors slightly darker and lighter respectively.
- // More contrast needed with higher luminance, empirically tweaked.
- luminance := (0.299*r + 0.587*g + 0.114*b) / 255
- contrast := 0.01 + luminance*0.03
- // Ensure we add the same amount of contrast also near 0 and 1.
- darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
- lighten := contrast + math.Max(contrast-luminance, 0.0)
- // Compute factor to keep RGB values proportional.
- darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
- lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
-
- scopeBytes := []byte{
- uint8(math.Min(math.Round(r*darkenFactor), 255)),
- uint8(math.Min(math.Round(g*darkenFactor), 255)),
- uint8(math.Min(math.Round(b*darkenFactor), 255)),
- }
- itemBytes := []byte{
- uint8(math.Min(math.Round(r*lightenFactor), 255)),
- uint8(math.Min(math.Round(g*lightenFactor), 255)),
- uint8(math.Min(math.Round(b*lightenFactor), 255)),
- }
-
- itemColor = "#" + hex.EncodeToString(itemBytes)
- scopeColor = "#" + hex.EncodeToString(scopeBytes)
- }
-
- return fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
- "<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
- "<div class='ui label scope-right' style='color: %s !important; background-color: %s !important''>%s</div>"+
- "</span>",
- description,
- textColor, scopeColor, scopeText,
- textColor, itemColor, itemText)
-}
-
-// RenderEmoji renders html text with emoji post processors
-func RenderEmoji(ctx context.Context, text string) template.HTML {
- renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
- template.HTMLEscapeString(text))
- if err != nil {
- log.Error("RenderEmoji: %v", err)
- return template.HTML("")
- }
- return template.HTML(renderedText)
-}
-
-// ReactionToEmoji renders emoji for use in reactions
-func ReactionToEmoji(reaction string) template.HTML {
- val := emoji.FromCode(reaction)
- if val != nil {
- return template.HTML(val.Emoji)
- }
- val = emoji.FromAlias(reaction)
- if val != nil {
- return template.HTML(val.Emoji)
- }
- return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
-}
-
-// RenderNote renders the contents of a git-notes file as a commit message.
-func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
- cleanMsg := template.HTMLEscapeString(msg)
- fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
- Ctx: ctx,
- URLPrefix: urlPrefix,
- Metas: metas,
- }, cleanMsg)
- if err != nil {
- log.Error("RenderNote: %v", err)
- return ""
- }
- return template.HTML(fullMessage)
-}
-
-// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
-func IsMultilineCommitMessage(msg string) bool {
- return strings.Count(strings.TrimSpace(msg), "\n") >= 1
-}
-
-// Actioner describes an action
-type Actioner interface {
- GetOpType() activities_model.ActionType
- GetActUserName() string
- GetRepoUserName() string
- GetRepoName() string
- GetRepoPath() string
- GetRepoLink() string
- GetBranch() string
- GetContent() string
- GetCreate() time.Time
- GetIssueInfos() []string
-}
-
-// ActionIcon accepts an action operation type and returns an icon class name.
-func ActionIcon(opType activities_model.ActionType) string {
- switch opType {
- case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
- return "repo"
- case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
- return "git-commit"
- case activities_model.ActionCreateIssue:
- return "issue-opened"
- case activities_model.ActionCreatePullRequest:
- return "git-pull-request"
- case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
- return "comment-discussion"
- case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
- return "git-merge"
- case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
- return "issue-closed"
- case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
- return "issue-reopened"
- case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
- return "mirror"
- case activities_model.ActionApprovePullRequest:
- return "check"
- case activities_model.ActionRejectPullRequest:
- return "diff"
- case activities_model.ActionPublishRelease:
- return "tag"
- case activities_model.ActionPullReviewDismissed:
- return "x"
- default:
- return "question"
- }
-}
-
-// ActionContent2Commits converts action content to push commits
-func ActionContent2Commits(act Actioner) *repository.PushCommits {
- push := repository.NewPushCommits()
-
- if act == nil || act.GetContent() == "" {
- return push
- }
-
- if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
- log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
- }
-
- if push.Len == 0 {
- push.Len = len(push.Commits)
- }
-
- return push
-}
-
-// DiffLineTypeToStr returns diff line type name
-func DiffLineTypeToStr(diffType int) string {
- switch diffType {
- case 2:
- return "add"
- case 3:
- return "del"
- case 4:
- return "tag"
- }
- return "same"
-}
-
-// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
-func MigrationIcon(hostname string) string {
- switch hostname {
- case "github.com":
- return "octicon-mark-github"
- default:
- return "gitea-git"
- }
-}
-
-type remoteAddress struct {
- Address string
- Username string
- Password string
-}
-
-func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
- a := remoteAddress{}
-
- remoteURL := m.OriginalURL
- if ignoreOriginalURL || remoteURL == "" {
- var err error
- remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
- if err != nil {
- log.Error("GetRemoteURL %v", err)
- return a
- }
- }
-
- u, err := giturl.Parse(remoteURL)
- if err != nil {
- log.Error("giturl.Parse %v", err)
- return a
- }
-
- if u.Scheme != "ssh" && u.Scheme != "file" {
- if u.User != nil {
- a.Username = u.User.Username()
- a.Password, _ = u.User.Password()
- }
- u.User = nil
- }
- a.Address = u.String()
-
- return a
-}
-
// Eval the expression and return the result, see the comment of eval.Expr for details.
// To use this helper function in templates, pass each token as a separate parameter.
//
diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go
new file mode 100644
index 0000000000..3badc97cb9
--- /dev/null
+++ b/modules/templates/util_avatar.go
@@ -0,0 +1,84 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "context"
+ "fmt"
+ "html"
+ "html/template"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/avatars"
+ "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ gitea_html "code.gitea.io/gitea/modules/html"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// AvatarHTML creates the HTML for an avatar
+func AvatarHTML(src string, size int, class, name string) template.HTML {
+ sizeStr := fmt.Sprintf(`%d`, size)
+
+ if name == "" {
+ name = "avatar"
+ }
+
+ return template.HTML(`<img class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
+}
+
+// Avatar renders user avatars. args: user, size (int), class (string)
+func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML {
+ size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
+
+ switch t := item.(type) {
+ case *user_model.User:
+ src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
+ if src != "" {
+ return AvatarHTML(src, size, class, t.DisplayName())
+ }
+ case *repo_model.Collaborator:
+ src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
+ if src != "" {
+ return AvatarHTML(src, size, class, t.DisplayName())
+ }
+ case *organization.Organization:
+ src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
+ if src != "" {
+ return AvatarHTML(src, size, class, t.AsUser().DisplayName())
+ }
+ }
+
+ return template.HTML("")
+}
+
+// AvatarByAction renders user avatars from action. args: action, size (int), class (string)
+func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML {
+ action.LoadActUser(ctx)
+ return Avatar(ctx, action.ActUser, others...)
+}
+
+// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
+func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
+ size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
+
+ src := repo.RelAvatarLink()
+ if src != "" {
+ return AvatarHTML(src, size, class, repo.FullName())
+ }
+ return template.HTML("")
+}
+
+// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
+func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML {
+ size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
+ src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor)
+
+ if src != "" {
+ return AvatarHTML(src, size, class, name)
+ }
+
+ return template.HTML("")
+}
diff --git a/modules/templates/util_json.go b/modules/templates/util_json.go
new file mode 100644
index 0000000000..71a4e23d36
--- /dev/null
+++ b/modules/templates/util_json.go
@@ -0,0 +1,35 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "bytes"
+
+ "code.gitea.io/gitea/modules/json"
+)
+
+type JsonUtils struct{} //nolint:revive
+
+var jsonUtils = JsonUtils{}
+
+func NewJsonUtils() *JsonUtils { //nolint:revive
+ return &jsonUtils
+}
+
+func (su *JsonUtils) EncodeToString(v any) string {
+ out, err := json.Marshal(v)
+ if err != nil {
+ return ""
+ }
+ return string(out)
+}
+
+func (su *JsonUtils) PrettyIndent(s string) string {
+ var out bytes.Buffer
+ err := json.Indent(&out, []byte(s), "", " ")
+ if err != nil {
+ return ""
+ }
+ return out.String()
+}
diff --git a/modules/templates/util_misc.go b/modules/templates/util_misc.go
new file mode 100644
index 0000000000..599a0942ce
--- /dev/null
+++ b/modules/templates/util_misc.go
@@ -0,0 +1,209 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+ "mime"
+ "path/filepath"
+ "strings"
+ "time"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/git"
+ giturl "code.gitea.io/gitea/modules/git/url"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/svg"
+
+ "github.com/editorconfig/editorconfig-core-go/v2"
+)
+
+func SortArrow(normSort, revSort, urlSort string, isDefault bool) template.HTML {
+ // if needed
+ if len(normSort) == 0 || len(urlSort) == 0 {
+ return ""
+ }
+
+ if len(urlSort) == 0 && isDefault {
+ // if sort is sorted as default add arrow tho this table header
+ if isDefault {
+ return svg.RenderHTML("octicon-triangle-down", 16)
+ }
+ } else {
+ // if sort arg is in url test if it correlates with column header sort arguments
+ // the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
+ if urlSort == normSort {
+ // the table is sorted with this header normal
+ return svg.RenderHTML("octicon-triangle-up", 16)
+ } else if urlSort == revSort {
+ // the table is sorted with this header reverse
+ return svg.RenderHTML("octicon-triangle-down", 16)
+ }
+ }
+ // the table is NOT sorted with this header
+ return ""
+}
+
+// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
+func IsMultilineCommitMessage(msg string) bool {
+ return strings.Count(strings.TrimSpace(msg), "\n") >= 1
+}
+
+// Actioner describes an action
+type Actioner interface {
+ GetOpType() activities_model.ActionType
+ GetActUserName() string
+ GetRepoUserName() string
+ GetRepoName() string
+ GetRepoPath() string
+ GetRepoLink() string
+ GetBranch() string
+ GetContent() string
+ GetCreate() time.Time
+ GetIssueInfos() []string
+}
+
+// ActionIcon accepts an action operation type and returns an icon class name.
+func ActionIcon(opType activities_model.ActionType) string {
+ switch opType {
+ case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
+ return "repo"
+ case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
+ return "git-commit"
+ case activities_model.ActionCreateIssue:
+ return "issue-opened"
+ case activities_model.ActionCreatePullRequest:
+ return "git-pull-request"
+ case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
+ return "comment-discussion"
+ case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
+ return "git-merge"
+ case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
+ return "issue-closed"
+ case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
+ return "issue-reopened"
+ case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
+ return "mirror"
+ case activities_model.ActionApprovePullRequest:
+ return "check"
+ case activities_model.ActionRejectPullRequest:
+ return "diff"
+ case activities_model.ActionPublishRelease:
+ return "tag"
+ case activities_model.ActionPullReviewDismissed:
+ return "x"
+ default:
+ return "question"
+ }
+}
+
+// ActionContent2Commits converts action content to push commits
+func ActionContent2Commits(act Actioner) *repository.PushCommits {
+ push := repository.NewPushCommits()
+
+ if act == nil || act.GetContent() == "" {
+ return push
+ }
+
+ if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
+ log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
+ }
+
+ if push.Len == 0 {
+ push.Len = len(push.Commits)
+ }
+
+ return push
+}
+
+// DiffLineTypeToStr returns diff line type name
+func DiffLineTypeToStr(diffType int) string {
+ switch diffType {
+ case 2:
+ return "add"
+ case 3:
+ return "del"
+ case 4:
+ return "tag"
+ }
+ return "same"
+}
+
+// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
+func MigrationIcon(hostname string) string {
+ switch hostname {
+ case "github.com":
+ return "octicon-mark-github"
+ default:
+ return "gitea-git"
+ }
+}
+
+type remoteAddress struct {
+ Address string
+ Username string
+ Password string
+}
+
+func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
+ a := remoteAddress{}
+
+ remoteURL := m.OriginalURL
+ if ignoreOriginalURL || remoteURL == "" {
+ var err error
+ remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
+ if err != nil {
+ log.Error("GetRemoteURL %v", err)
+ return a
+ }
+ }
+
+ u, err := giturl.Parse(remoteURL)
+ if err != nil {
+ log.Error("giturl.Parse %v", err)
+ return a
+ }
+
+ if u.Scheme != "ssh" && u.Scheme != "file" {
+ if u.User != nil {
+ a.Username = u.User.Username()
+ a.Password, _ = u.User.Password()
+ }
+ u.User = nil
+ }
+ a.Address = u.String()
+
+ return a
+}
+
+func FilenameIsImage(filename string) bool {
+ mimeType := mime.TypeByExtension(filepath.Ext(filename))
+ return strings.HasPrefix(mimeType, "image/")
+}
+
+func TabSizeClass(ec interface{}, filename string) string {
+ var (
+ value *editorconfig.Editorconfig
+ ok bool
+ )
+ if ec != nil {
+ if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
+ return "tab-size-8"
+ }
+ def, err := value.GetDefinitionForFilename(filename)
+ if err != nil {
+ log.Error("tab size class: getting definition for filename: %v", err)
+ return "tab-size-8"
+ }
+ if def.TabWidth > 0 {
+ return fmt.Sprintf("tab-size-%d", def.TabWidth)
+ }
+ }
+ return "tab-size-8"
+}
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
new file mode 100644
index 0000000000..a59ddd3f17
--- /dev/null
+++ b/modules/templates/util_render.go
@@ -0,0 +1,254 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "context"
+ "encoding/hex"
+ "fmt"
+ "html/template"
+ "math"
+ "net/url"
+ "regexp"
+ "strings"
+ "unicode"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/emoji"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// RenderCommitMessage renders commit message with XSS-safe and special links.
+func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
+ return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
+}
+
+// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
+// default url, handling for special links.
+func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
+ cleanMsg := template.HTMLEscapeString(msg)
+ // we can safely assume that it will not return any error, since there
+ // shouldn't be any special HTML.
+ fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
+ Ctx: ctx,
+ URLPrefix: urlPrefix,
+ DefaultLink: urlDefault,
+ Metas: metas,
+ }, cleanMsg)
+ if err != nil {
+ log.Error("RenderCommitMessage: %v", err)
+ return ""
+ }
+ msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
+ if len(msgLines) == 0 {
+ return template.HTML("")
+ }
+ return template.HTML(msgLines[0])
+}
+
+// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
+// the provided default url, handling for special links without email to links.
+func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
+ msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
+ lineEnd := strings.IndexByte(msgLine, '\n')
+ if lineEnd > 0 {
+ msgLine = msgLine[:lineEnd]
+ }
+ msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
+ if len(msgLine) == 0 {
+ return template.HTML("")
+ }
+
+ // we can safely assume that it will not return any error, since there
+ // shouldn't be any special HTML.
+ renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
+ Ctx: ctx,
+ URLPrefix: urlPrefix,
+ DefaultLink: urlDefault,
+ Metas: metas,
+ }, template.HTMLEscapeString(msgLine))
+ if err != nil {
+ log.Error("RenderCommitMessageSubject: %v", err)
+ return template.HTML("")
+ }
+ return template.HTML(renderedMessage)
+}
+
+// RenderCommitBody extracts the body of a commit message without its title.
+func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
+ msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
+ lineEnd := strings.IndexByte(msgLine, '\n')
+ if lineEnd > 0 {
+ msgLine = msgLine[lineEnd+1:]
+ } else {
+ return template.HTML("")
+ }
+ msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
+ if len(msgLine) == 0 {
+ return template.HTML("")
+ }
+
+ renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
+ Ctx: ctx,
+ URLPrefix: urlPrefix,
+ Metas: metas,
+ }, template.HTMLEscapeString(msgLine))
+ if err != nil {
+ log.Error("RenderCommitMessage: %v", err)
+ return ""
+ }
+ return template.HTML(renderedMessage)
+}
+
+// Match text that is between back ticks.
+var codeMatcher = regexp.MustCompile("`([^`]+)`")
+
+// RenderCodeBlock renders "`…`" as highlighted "<code>" block.
+// Intended for issue and PR titles, these containers should have styles for "<code>" elements
+func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
+ htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "<code>$1</code>") // replace with HTML <code> tags
+ return template.HTML(htmlWithCodeTags)
+}
+
+// RenderIssueTitle renders issue/pull title with defined post processors
+func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML {
+ renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
+ Ctx: ctx,
+ URLPrefix: urlPrefix,
+ Metas: metas,
+ }, template.HTMLEscapeString(text))
+ if err != nil {
+ log.Error("RenderIssueTitle: %v", err)
+ return template.HTML("")
+ }
+ return template.HTML(renderedText)
+}
+
+// RenderLabel renders a label
+func RenderLabel(ctx context.Context, label *issues_model.Label) template.HTML {
+ labelScope := label.ExclusiveScope()
+
+ textColor := "#111"
+ if label.UseLightTextColor() {
+ textColor = "#eee"
+ }
+
+ description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description))
+
+ if labelScope == "" {
+ // Regular label
+ s := fmt.Sprintf("<div class='ui label' style='color: %s !important; background-color: %s !important' title='%s'>%s</div>",
+ textColor, label.Color, description, RenderEmoji(ctx, label.Name))
+ return template.HTML(s)
+ }
+
+ // Scoped label
+ scopeText := RenderEmoji(ctx, labelScope)
+ itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:])
+
+ itemColor := label.Color
+ scopeColor := label.Color
+ if r, g, b, err := label.ColorRGB(); err == nil {
+ // Make scope and item background colors slightly darker and lighter respectively.
+ // More contrast needed with higher luminance, empirically tweaked.
+ luminance := (0.299*r + 0.587*g + 0.114*b) / 255
+ contrast := 0.01 + luminance*0.03
+ // Ensure we add the same amount of contrast also near 0 and 1.
+ darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
+ lighten := contrast + math.Max(contrast-luminance, 0.0)
+ // Compute factor to keep RGB values proportional.
+ darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
+ lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
+
+ scopeBytes := []byte{
+ uint8(math.Min(math.Round(r*darkenFactor), 255)),
+ uint8(math.Min(math.Round(g*darkenFactor), 255)),
+ uint8(math.Min(math.Round(b*darkenFactor), 255)),
+ }
+ itemBytes := []byte{
+ uint8(math.Min(math.Round(r*lightenFactor), 255)),
+ uint8(math.Min(math.Round(g*lightenFactor), 255)),
+ uint8(math.Min(math.Round(b*lightenFactor), 255)),
+ }
+
+ itemColor = "#" + hex.EncodeToString(itemBytes)
+ scopeColor = "#" + hex.EncodeToString(scopeBytes)
+ }
+
+ s := fmt.Sprintf("<span class='ui label scope-parent' title='%s'>"+
+ "<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+
+ "<div class='ui label scope-right' style='color: %s !important; background-color: %s !important''>%s</div>"+
+ "</span>",
+ description,
+ textColor, scopeColor, scopeText,
+ textColor, itemColor, itemText)
+ return template.HTML(s)
+}
+
+// RenderEmoji renders html text with emoji post processors
+func RenderEmoji(ctx context.Context, text string) template.HTML {
+ renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx},
+ template.HTMLEscapeString(text))
+ if err != nil {
+ log.Error("RenderEmoji: %v", err)
+ return template.HTML("")
+ }
+ return template.HTML(renderedText)
+}
+
+// ReactionToEmoji renders emoji for use in reactions
+func ReactionToEmoji(reaction string) template.HTML {
+ val := emoji.FromCode(reaction)
+ if val != nil {
+ return template.HTML(val.Emoji)
+ }
+ val = emoji.FromAlias(reaction)
+ if val != nil {
+ return template.HTML(val.Emoji)
+ }
+ return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
+}
+
+// RenderNote renders the contents of a git-notes file as a commit message.
+func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
+ cleanMsg := template.HTMLEscapeString(msg)
+ fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
+ Ctx: ctx,
+ URLPrefix: urlPrefix,
+ Metas: metas,
+ }, cleanMsg)
+ if err != nil {
+ log.Error("RenderNote: %v", err)
+ return ""
+ }
+ return template.HTML(fullMessage)
+}
+
+func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive
+ output, err := markdown.RenderString(&markup.RenderContext{
+ Ctx: ctx,
+ URLPrefix: setting.AppSubURL,
+ }, input)
+ if err != nil {
+ log.Error("RenderString: %v", err)
+ }
+ return template.HTML(output)
+}
+
+func RenderLabels(ctx context.Context, labels []*issues_model.Label, repoLink string) template.HTML {
+ htmlCode := `<span class="labels-list">`
+ for _, label := range labels {
+ // Protect against nil value in labels - shouldn't happen but would cause a panic if so
+ if label == nil {
+ continue
+ }
+ htmlCode += fmt.Sprintf("<a href='%s/issues?labels=%d'>%s</a> ",
+ repoLink, label.ID, RenderLabel(ctx, label))
+ }
+ htmlCode += "</span>"
+ return template.HTML(htmlCode)
+}
diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go
index 42d11fc990..459380aee5 100644
--- a/modules/templates/util_string.go
+++ b/modules/templates/util_string.go
@@ -3,12 +3,18 @@
package templates
-import "strings"
+import (
+ "strings"
+
+ "code.gitea.io/gitea/modules/base"
+)
type StringUtils struct{}
+var stringUtils = StringUtils{}
+
func NewStringUtils() *StringUtils {
- return &StringUtils{}
+ return &stringUtils
}
func (su *StringUtils) HasPrefix(s, prefix string) bool {
@@ -22,3 +28,11 @@ func (su *StringUtils) Contains(s, substr string) bool {
func (su *StringUtils) Split(s, sep string) []string {
return strings.Split(s, sep)
}
+
+func (su *StringUtils) Join(a []string, sep string) string {
+ return strings.Join(a, sep)
+}
+
+func (su *StringUtils) EllipsisString(s string, max int) string {
+ return base.EllipsisString(s, max)
+}