" block.
-// Intended for issue and PR titles, these containers should have styles for "" elements
-func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
- htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "$1
") // replace with HTML 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("%s",
- 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(""+
- "%s"+
- "%s"+
- "",
- 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(``, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
-}
-
-// RenderNote renders the contents of a git-notes file as a commit message.
-func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
- cleanMsg := template.HTMLEscapeString(msg)
- fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
- Ctx: ctx,
- URLPrefix: urlPrefix,
- Metas: metas,
- }, cleanMsg)
- if err != nil {
- log.Error("RenderNote: %v", err)
- return ""
- }
- return template.HTML(fullMessage)
-}
-
-// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
-func IsMultilineCommitMessage(msg string) bool {
- return strings.Count(strings.TrimSpace(msg), "\n") >= 1
-}
-
-// Actioner describes an action
-type Actioner interface {
- GetOpType() activities_model.ActionType
- GetActUserName() string
- GetRepoUserName() string
- GetRepoName() string
- GetRepoPath() string
- GetRepoLink() string
- GetBranch() string
- GetContent() string
- GetCreate() time.Time
- GetIssueInfos() []string
-}
-
-// ActionIcon accepts an action operation type and returns an icon class name.
-func ActionIcon(opType activities_model.ActionType) string {
- switch opType {
- case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
- return "repo"
- case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
- return "git-commit"
- case activities_model.ActionCreateIssue:
- return "issue-opened"
- case activities_model.ActionCreatePullRequest:
- return "git-pull-request"
- case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
- return "comment-discussion"
- case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
- return "git-merge"
- case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
- return "issue-closed"
- case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
- return "issue-reopened"
- case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
- return "mirror"
- case activities_model.ActionApprovePullRequest:
- return "check"
- case activities_model.ActionRejectPullRequest:
- return "diff"
- case activities_model.ActionPublishRelease:
- return "tag"
- case activities_model.ActionPullReviewDismissed:
- return "x"
- default:
- return "question"
- }
-}
-
-// ActionContent2Commits converts action content to push commits
-func ActionContent2Commits(act Actioner) *repository.PushCommits {
- push := repository.NewPushCommits()
-
- if act == nil || act.GetContent() == "" {
- return push
- }
-
- if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
- log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
- }
-
- if push.Len == 0 {
- push.Len = len(push.Commits)
- }
-
- return push
-}
-
-// DiffLineTypeToStr returns diff line type name
-func DiffLineTypeToStr(diffType int) string {
- switch diffType {
- case 2:
- return "add"
- case 3:
- return "del"
- case 4:
- return "tag"
- }
- return "same"
-}
-
-// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
-func MigrationIcon(hostname string) string {
- switch hostname {
- case "github.com":
- return "octicon-mark-github"
- default:
- return "gitea-git"
- }
-}
-
-type remoteAddress struct {
- Address string
- Username string
- Password string
-}
-
-func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
- a := remoteAddress{}
-
- remoteURL := m.OriginalURL
- if ignoreOriginalURL || remoteURL == "" {
- var err error
- remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
- if err != nil {
- log.Error("GetRemoteURL %v", err)
- return a
- }
- }
-
- u, err := giturl.Parse(remoteURL)
- if err != nil {
- log.Error("giturl.Parse %v", err)
- return a
- }
-
- if u.Scheme != "ssh" && u.Scheme != "file" {
- if u.User != nil {
- a.Username = u.User.Username()
- a.Password, _ = u.User.Password()
- }
- u.User = nil
- }
- a.Address = u.String()
-
- return a
-}
-
// Eval the expression and return the result, see the comment of eval.Expr for details.
// To use this helper function in templates, pass each token as a separate parameter.
//
diff --git a/modules/templates/util_avatar.go b/modules/templates/util_avatar.go
new file mode 100644
index 000000000..3badc97cb
--- /dev/null
+++ b/modules/templates/util_avatar.go
@@ -0,0 +1,84 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "context"
+ "fmt"
+ "html"
+ "html/template"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/avatars"
+ "code.gitea.io/gitea/models/organization"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ gitea_html "code.gitea.io/gitea/modules/html"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// AvatarHTML creates the HTML for an avatar
+func AvatarHTML(src string, size int, class, name string) template.HTML {
+ sizeStr := fmt.Sprintf(`%d`, size)
+
+ if name == "" {
+ name = "avatar"
+ }
+
+ return template.HTML(``)
+}
+
+// Avatar renders user avatars. args: user, size (int), class (string)
+func Avatar(ctx context.Context, item interface{}, others ...interface{}) template.HTML {
+ size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
+
+ switch t := item.(type) {
+ case *user_model.User:
+ src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
+ if src != "" {
+ return AvatarHTML(src, size, class, t.DisplayName())
+ }
+ case *repo_model.Collaborator:
+ src := t.AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
+ if src != "" {
+ return AvatarHTML(src, size, class, t.DisplayName())
+ }
+ case *organization.Organization:
+ src := t.AsUser().AvatarLinkWithSize(ctx, size*setting.Avatar.RenderedSizeFactor)
+ if src != "" {
+ return AvatarHTML(src, size, class, t.AsUser().DisplayName())
+ }
+ }
+
+ return template.HTML("")
+}
+
+// AvatarByAction renders user avatars from action. args: action, size (int), class (string)
+func AvatarByAction(ctx context.Context, action *activities_model.Action, others ...interface{}) template.HTML {
+ action.LoadActUser(ctx)
+ return Avatar(ctx, action.ActUser, others...)
+}
+
+// RepoAvatar renders repo avatars. args: repo, size(int), class (string)
+func RepoAvatar(repo *repo_model.Repository, others ...interface{}) template.HTML {
+ size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
+
+ src := repo.RelAvatarLink()
+ if src != "" {
+ return AvatarHTML(src, size, class, repo.FullName())
+ }
+ return template.HTML("")
+}
+
+// AvatarByEmail renders avatars by email address. args: email, name, size (int), class (string)
+func AvatarByEmail(ctx context.Context, email, name string, others ...interface{}) template.HTML {
+ size, class := gitea_html.ParseSizeAndClass(avatars.DefaultAvatarPixelSize, avatars.DefaultAvatarClass, others...)
+ src := avatars.GenerateEmailAvatarFastLink(ctx, email, size*setting.Avatar.RenderedSizeFactor)
+
+ if src != "" {
+ return AvatarHTML(src, size, class, name)
+ }
+
+ return template.HTML("")
+}
diff --git a/modules/templates/util_json.go b/modules/templates/util_json.go
new file mode 100644
index 000000000..71a4e23d3
--- /dev/null
+++ b/modules/templates/util_json.go
@@ -0,0 +1,35 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "bytes"
+
+ "code.gitea.io/gitea/modules/json"
+)
+
+type JsonUtils struct{} //nolint:revive
+
+var jsonUtils = JsonUtils{}
+
+func NewJsonUtils() *JsonUtils { //nolint:revive
+ return &jsonUtils
+}
+
+func (su *JsonUtils) EncodeToString(v any) string {
+ out, err := json.Marshal(v)
+ if err != nil {
+ return ""
+ }
+ return string(out)
+}
+
+func (su *JsonUtils) PrettyIndent(s string) string {
+ var out bytes.Buffer
+ err := json.Indent(&out, []byte(s), "", " ")
+ if err != nil {
+ return ""
+ }
+ return out.String()
+}
diff --git a/modules/templates/util_misc.go b/modules/templates/util_misc.go
new file mode 100644
index 000000000..599a0942c
--- /dev/null
+++ b/modules/templates/util_misc.go
@@ -0,0 +1,209 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+ "mime"
+ "path/filepath"
+ "strings"
+ "time"
+
+ activities_model "code.gitea.io/gitea/models/activities"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/git"
+ giturl "code.gitea.io/gitea/modules/git/url"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/svg"
+
+ "github.com/editorconfig/editorconfig-core-go/v2"
+)
+
+func SortArrow(normSort, revSort, urlSort string, isDefault bool) template.HTML {
+ // if needed
+ if len(normSort) == 0 || len(urlSort) == 0 {
+ return ""
+ }
+
+ if len(urlSort) == 0 && isDefault {
+ // if sort is sorted as default add arrow tho this table header
+ if isDefault {
+ return svg.RenderHTML("octicon-triangle-down", 16)
+ }
+ } else {
+ // if sort arg is in url test if it correlates with column header sort arguments
+ // the direction of the arrow should indicate the "current sort order", up means ASC(normal), down means DESC(rev)
+ if urlSort == normSort {
+ // the table is sorted with this header normal
+ return svg.RenderHTML("octicon-triangle-up", 16)
+ } else if urlSort == revSort {
+ // the table is sorted with this header reverse
+ return svg.RenderHTML("octicon-triangle-down", 16)
+ }
+ }
+ // the table is NOT sorted with this header
+ return ""
+}
+
+// IsMultilineCommitMessage checks to see if a commit message contains multiple lines.
+func IsMultilineCommitMessage(msg string) bool {
+ return strings.Count(strings.TrimSpace(msg), "\n") >= 1
+}
+
+// Actioner describes an action
+type Actioner interface {
+ GetOpType() activities_model.ActionType
+ GetActUserName() string
+ GetRepoUserName() string
+ GetRepoName() string
+ GetRepoPath() string
+ GetRepoLink() string
+ GetBranch() string
+ GetContent() string
+ GetCreate() time.Time
+ GetIssueInfos() []string
+}
+
+// ActionIcon accepts an action operation type and returns an icon class name.
+func ActionIcon(opType activities_model.ActionType) string {
+ switch opType {
+ case activities_model.ActionCreateRepo, activities_model.ActionTransferRepo, activities_model.ActionRenameRepo:
+ return "repo"
+ case activities_model.ActionCommitRepo, activities_model.ActionPushTag, activities_model.ActionDeleteTag, activities_model.ActionDeleteBranch:
+ return "git-commit"
+ case activities_model.ActionCreateIssue:
+ return "issue-opened"
+ case activities_model.ActionCreatePullRequest:
+ return "git-pull-request"
+ case activities_model.ActionCommentIssue, activities_model.ActionCommentPull:
+ return "comment-discussion"
+ case activities_model.ActionMergePullRequest, activities_model.ActionAutoMergePullRequest:
+ return "git-merge"
+ case activities_model.ActionCloseIssue, activities_model.ActionClosePullRequest:
+ return "issue-closed"
+ case activities_model.ActionReopenIssue, activities_model.ActionReopenPullRequest:
+ return "issue-reopened"
+ case activities_model.ActionMirrorSyncPush, activities_model.ActionMirrorSyncCreate, activities_model.ActionMirrorSyncDelete:
+ return "mirror"
+ case activities_model.ActionApprovePullRequest:
+ return "check"
+ case activities_model.ActionRejectPullRequest:
+ return "diff"
+ case activities_model.ActionPublishRelease:
+ return "tag"
+ case activities_model.ActionPullReviewDismissed:
+ return "x"
+ default:
+ return "question"
+ }
+}
+
+// ActionContent2Commits converts action content to push commits
+func ActionContent2Commits(act Actioner) *repository.PushCommits {
+ push := repository.NewPushCommits()
+
+ if act == nil || act.GetContent() == "" {
+ return push
+ }
+
+ if err := json.Unmarshal([]byte(act.GetContent()), push); err != nil {
+ log.Error("json.Unmarshal:\n%s\nERROR: %v", act.GetContent(), err)
+ }
+
+ if push.Len == 0 {
+ push.Len = len(push.Commits)
+ }
+
+ return push
+}
+
+// DiffLineTypeToStr returns diff line type name
+func DiffLineTypeToStr(diffType int) string {
+ switch diffType {
+ case 2:
+ return "add"
+ case 3:
+ return "del"
+ case 4:
+ return "tag"
+ }
+ return "same"
+}
+
+// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
+func MigrationIcon(hostname string) string {
+ switch hostname {
+ case "github.com":
+ return "octicon-mark-github"
+ default:
+ return "gitea-git"
+ }
+}
+
+type remoteAddress struct {
+ Address string
+ Username string
+ Password string
+}
+
+func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string, ignoreOriginalURL bool) remoteAddress {
+ a := remoteAddress{}
+
+ remoteURL := m.OriginalURL
+ if ignoreOriginalURL || remoteURL == "" {
+ var err error
+ remoteURL, err = git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
+ if err != nil {
+ log.Error("GetRemoteURL %v", err)
+ return a
+ }
+ }
+
+ u, err := giturl.Parse(remoteURL)
+ if err != nil {
+ log.Error("giturl.Parse %v", err)
+ return a
+ }
+
+ if u.Scheme != "ssh" && u.Scheme != "file" {
+ if u.User != nil {
+ a.Username = u.User.Username()
+ a.Password, _ = u.User.Password()
+ }
+ u.User = nil
+ }
+ a.Address = u.String()
+
+ return a
+}
+
+func FilenameIsImage(filename string) bool {
+ mimeType := mime.TypeByExtension(filepath.Ext(filename))
+ return strings.HasPrefix(mimeType, "image/")
+}
+
+func TabSizeClass(ec interface{}, filename string) string {
+ var (
+ value *editorconfig.Editorconfig
+ ok bool
+ )
+ if ec != nil {
+ if value, ok = ec.(*editorconfig.Editorconfig); !ok || value == nil {
+ return "tab-size-8"
+ }
+ def, err := value.GetDefinitionForFilename(filename)
+ if err != nil {
+ log.Error("tab size class: getting definition for filename: %v", err)
+ return "tab-size-8"
+ }
+ if def.TabWidth > 0 {
+ return fmt.Sprintf("tab-size-%d", def.TabWidth)
+ }
+ }
+ return "tab-size-8"
+}
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
new file mode 100644
index 000000000..a59ddd3f1
--- /dev/null
+++ b/modules/templates/util_render.go
@@ -0,0 +1,254 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "context"
+ "encoding/hex"
+ "fmt"
+ "html/template"
+ "math"
+ "net/url"
+ "regexp"
+ "strings"
+ "unicode"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/emoji"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/markup/markdown"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// RenderCommitMessage renders commit message with XSS-safe and special links.
+func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
+ return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
+}
+
+// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
+// default url, handling for special links.
+func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
+ cleanMsg := template.HTMLEscapeString(msg)
+ // we can safely assume that it will not return any error, since there
+ // shouldn't be any special HTML.
+ fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
+ Ctx: ctx,
+ URLPrefix: urlPrefix,
+ DefaultLink: urlDefault,
+ Metas: metas,
+ }, cleanMsg)
+ if err != nil {
+ log.Error("RenderCommitMessage: %v", err)
+ return ""
+ }
+ msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
+ if len(msgLines) == 0 {
+ return template.HTML("")
+ }
+ return template.HTML(msgLines[0])
+}
+
+// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
+// the provided default url, handling for special links without email to links.
+func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
+ msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
+ lineEnd := strings.IndexByte(msgLine, '\n')
+ if lineEnd > 0 {
+ msgLine = msgLine[:lineEnd]
+ }
+ msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace)
+ if len(msgLine) == 0 {
+ return template.HTML("")
+ }
+
+ // we can safely assume that it will not return any error, since there
+ // shouldn't be any special HTML.
+ renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
+ Ctx: ctx,
+ URLPrefix: urlPrefix,
+ DefaultLink: urlDefault,
+ Metas: metas,
+ }, template.HTMLEscapeString(msgLine))
+ if err != nil {
+ log.Error("RenderCommitMessageSubject: %v", err)
+ return template.HTML("")
+ }
+ return template.HTML(renderedMessage)
+}
+
+// RenderCommitBody extracts the body of a commit message without its title.
+func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
+ msgLine := strings.TrimRightFunc(msg, unicode.IsSpace)
+ lineEnd := strings.IndexByte(msgLine, '\n')
+ if lineEnd > 0 {
+ msgLine = msgLine[lineEnd+1:]
+ } else {
+ return template.HTML("")
+ }
+ msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace)
+ if len(msgLine) == 0 {
+ return template.HTML("")
+ }
+
+ renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
+ Ctx: ctx,
+ URLPrefix: urlPrefix,
+ Metas: metas,
+ }, template.HTMLEscapeString(msgLine))
+ if err != nil {
+ log.Error("RenderCommitMessage: %v", err)
+ return ""
+ }
+ return template.HTML(renderedMessage)
+}
+
+// Match text that is between back ticks.
+var codeMatcher = regexp.MustCompile("`([^`]+)`")
+
+// RenderCodeBlock renders "`…`" as highlighted "" block.
+// Intended for issue and PR titles, these containers should have styles for "" elements
+func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
+ htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), "$1
") // replace with HTML 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("%s",
+ 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(""+
+ "%s"+
+ "%s"+
+ "",
+ 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(``, 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 := ``
+ 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("%s ",
+ repoLink, label.ID, RenderLabel(ctx, label))
+ }
+ htmlCode += ""
+ return template.HTML(htmlCode)
+}
diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go
index 42d11fc99..459380aee 100644
--- a/modules/templates/util_string.go
+++ b/modules/templates/util_string.go
@@ -3,12 +3,18 @@
package templates
-import "strings"
+import (
+ "strings"
+
+ "code.gitea.io/gitea/modules/base"
+)
type StringUtils struct{}
+var stringUtils = StringUtils{}
+
func NewStringUtils() *StringUtils {
- return &StringUtils{}
+ return &stringUtils
}
func (su *StringUtils) HasPrefix(s, prefix string) bool {
@@ -22,3 +28,11 @@ func (su *StringUtils) Contains(s, substr string) bool {
func (su *StringUtils) Split(s, sep string) []string {
return strings.Split(s, sep)
}
+
+func (su *StringUtils) Join(a []string, sep string) string {
+ return strings.Join(a, sep)
+}
+
+func (su *StringUtils) EllipsisString(s string, max int) string {
+ return base.EllipsisString(s, max)
+}
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index 0aed59ffa..2c8fe724e 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -334,7 +334,7 @@
-
+
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index f0d0ad364..c4f77ec1a 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -365,7 +365,7 @@
{{$.locale.Tr "admin.config.log_mode"}}
{{.Name}} ({{.Provider}})
{{$.locale.Tr "admin.config.log_config"}}
- {{.Config | JsonPrettyPrint}}
+ {{JsonUtils.PrettyIndent .Config}}
{{end}}
{{$.locale.Tr "admin.config.router_log_mode"}}
@@ -378,7 +378,7 @@
{{$.locale.Tr "admin.config.log_mode"}}
{{.Name}} ({{.Provider}})
{{$.locale.Tr "admin.config.log_config"}}
- {{.Config | JsonPrettyPrint}}
+ {{JsonUtils.PrettyIndent .Config}}
{{end}}
{{else}}
{{$.locale.Tr "admin.config.routes_to_default_logger"}}
@@ -393,7 +393,7 @@
{{$.locale.Tr "admin.config.log_mode"}}
{{.Name}} ({{.Provider}})
{{$.locale.Tr "admin.config.log_config"}}
- {{.Config | JsonPrettyPrint}}
+ {{JsonUtils.PrettyIndent .Config}}
{{end}}
{{else}}
{{$.locale.Tr "admin.config.routes_to_default_logger"}}
@@ -412,7 +412,7 @@
{{$.locale.Tr "admin.config.log_mode"}}
{{.Name}} ({{.Provider}})
{{$.locale.Tr "admin.config.log_config"}}
- {{.Config | JsonPrettyPrint}}
+ {{JsonUtils.PrettyIndent .Config}}
{{end}}
{{else}}
{{$.locale.Tr "admin.config.routes_to_default_logger"}}
diff --git a/templates/admin/queue.tmpl b/templates/admin/queue.tmpl
index 3de01a32a..84eb8892e 100644
--- a/templates/admin/queue.tmpl
+++ b/templates/admin/queue.tmpl
@@ -174,7 +174,7 @@
{{.locale.Tr "admin.monitor.queue.configuration"}}
- {{.Queue.Configuration | JsonPrettyPrint}}
+ {{JsonUtils.PrettyIndent .Queue.Configuration}}
diff --git a/templates/package/shared/cleanup_rules/list.tmpl b/templates/package/shared/cleanup_rules/list.tmpl
index 09f95e4f4..10a073eb5 100644
--- a/templates/package/shared/cleanup_rules/list.tmpl
+++ b/templates/package/shared/cleanup_rules/list.tmpl
@@ -22,9 +22,9 @@
{{.Type.Name}}
{{if .Enabled}}{{$.locale.Tr "enabled"}}{{else}}{{$.locale.Tr "disabled"}}{{end}}
{{if .KeepCount}}{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.count"}}: {{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}}{{end}}
- {{if .KeepPattern}}{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}: {{EllipsisString .KeepPattern 100}}{{end}}
+ {{if .KeepPattern}}{{$.locale.Tr "packages.owner.settings.cleanuprules.keep.pattern"}}: {{StringUtils.EllipsisString .KeepPattern 100}}{{end}}
{{if .RemoveDays}}{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.days"}}: {{$.locale.Tr "tool.days" .RemoveDays}}{{end}}
- {{if .RemovePattern}}{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.pattern"}}: {{EllipsisString .RemovePattern 100}}{{end}}
+ {{if .RemovePattern}}{{$.locale.Tr "packages.owner.settings.cleanuprules.remove.pattern"}}: {{StringUtils.EllipsisString .RemovePattern 100}}{{end}}
{{else}}
diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl
index c47fa9d9c..de7c3a1dd 100644
--- a/templates/repo/home.tmpl
+++ b/templates/repo/home.tmpl
@@ -68,7 +68,13 @@
{{$l := Eval $n "-" 1}}
{{if and (eq $n 0) .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
-
{{svg "octicon-git-pull-request"}}
@@ -103,7 +109,17 @@
{{end}}
{{if ne $n 0}}
-
+
{{end}}
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index d00a4813d..d673e89a3 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -13,7 +13,7 @@
{{if .PageIsComparePull}}
- {{.locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}
+ {{.locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0| Escape) | Safe}}
{{end}}
{{if .Fields}}
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index b99e49a58..464f41be1 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -304,10 +304,12 @@
{{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}}
{{template "shared/user/authorlink" .Poster}}
- {{$parsedDeadline := .Content | ParseDeadline}}
- {{$from := DateTime "long" (index $parsedDeadline 1)}}
- {{$to := DateTime "long" (index $parsedDeadline 0)}}
- {{$.locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}}
+ {{$parsedDeadline := StringUtils.Split .Content "|"}}
+ {{if eq (len $parsedDeadline) 2}}
+ {{$from := DateTime "long" (index $parsedDeadline 1)}}
+ {{$to := DateTime "long" (index $parsedDeadline 0)}}
+ {{$.locale.Tr "repo.issues.due_date_modified" $to $from $createdStr | Safe}}
+ {{end}}
{{else if eq .Type 18}}
diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl
index fe8a6cfc5..2d34613dd 100644
--- a/templates/repo/release/new.tmpl
+++ b/templates/repo/release/new.tmpl
@@ -20,7 +20,7 @@
{{.tag_name}}@{{.tag_target}}
{{else}}
-
+
@
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
index 9540c872c..602cda361 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -61,13 +61,15 @@
{{else}}
{{if $entry.IsDir}}
{{$subJumpablePathName := $entry.GetSubJumpablePathName}}
- {{$subJumpablePath := SubJumpablePath $subJumpablePathName}}
{{svg "octicon-file-directory-fill"}}
- {{if eq (len $subJumpablePath) 2}}
- {{index $subJumpablePath 0}}{{index $subJumpablePath 1}}
+ {{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}}
+ {{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}}
+ {{if eq $subJumpablePathFieldLast 0}}
+ {{$subJumpablePathName}}
{{else}}
- {{index $subJumpablePath 0}}
+ {{$subJumpablePathPrefixes := slice $subJumpablePathFields 0 $subJumpablePathFieldLast}}
+ {{StringUtils.Join $subJumpablePathPrefixes "/"}}/{{index $subJumpablePathFields $subJumpablePathFieldLast}}
{{end}}
{{else}}
diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl
index c9edc59b1..94da2269b 100644
--- a/templates/shared/actions/runner_edit.tmpl
+++ b/templates/shared/actions/runner_edit.tmpl
@@ -37,7 +37,7 @@
-
+
{{.locale.Tr "actions.runners.custom_labels_helper"}}
diff --git a/templates/user/heatmap.tmpl b/templates/user/heatmap.tmpl
index 5d42a5435..b0ee0eeaa 100644
--- a/templates/user/heatmap.tmpl
+++ b/templates/user/heatmap.tmpl
@@ -1,6 +1,6 @@
{{if .HeatmapData}}