mirror of https://github.com/go-gitea/gitea
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>pull/24409/head^2
parent
5a5ab8ef5a
commit
241b74f6c5
@ -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("") |
||||
} |
@ -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() |
||||
} |
@ -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" |
||||
} |
@ -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) |
||||
} |
Loading…
Reference in new issue