summaryrefslogtreecommitdiffstats
path: root/modules/templates/helper.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/templates/helper.go')
-rw-r--r--modules/templates/helper.go557
1 files changed, 10 insertions, 547 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.
//