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>
This commit is contained in:
parent
5a5ab8ef5a
commit
241b74f6c5
|
@ -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 "|"}}
|
||||||
{{$from := DateTime "long" (index $parsedDeadline 1)}}
|
{{if eq (len $parsedDeadline) 2}}
|
||||||
{{$to := DateTime "long" (index $parsedDeadline 0)}}
|
{{$from := DateTime "long" (index $parsedDeadline 1)}}
|
||||||
{{$.locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}}
|
{{$to := DateTime "long" (index $parsedDeadline 0)}}
|
||||||
|
{{$.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"}}"
|
||||||
|
|
Loading…
Reference in a new issue