mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
10 KiB
278 lines
10 KiB
// Copyright 2023 The Gitea Authors. All rights reserved. |
|
// SPDX-License-Identifier: MIT |
|
|
|
package templates |
|
|
|
import ( |
|
"encoding/hex" |
|
"fmt" |
|
"html/template" |
|
"math" |
|
"net/url" |
|
"regexp" |
|
"strings" |
|
"unicode" |
|
|
|
issues_model "code.gitea.io/gitea/models/issues" |
|
"code.gitea.io/gitea/models/renderhelper" |
|
"code.gitea.io/gitea/models/repo" |
|
"code.gitea.io/gitea/modules/emoji" |
|
"code.gitea.io/gitea/modules/htmlutil" |
|
"code.gitea.io/gitea/modules/log" |
|
"code.gitea.io/gitea/modules/markup" |
|
"code.gitea.io/gitea/modules/markup/markdown" |
|
"code.gitea.io/gitea/modules/reqctx" |
|
"code.gitea.io/gitea/modules/setting" |
|
"code.gitea.io/gitea/modules/svg" |
|
"code.gitea.io/gitea/modules/translation" |
|
"code.gitea.io/gitea/modules/util" |
|
"code.gitea.io/gitea/services/webtheme" |
|
) |
|
|
|
type RenderUtils struct { |
|
ctx reqctx.RequestContext |
|
} |
|
|
|
func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { |
|
return &RenderUtils{ctx: ctx} |
|
} |
|
|
|
// RenderCommitMessage renders commit message with XSS-safe and special links. |
|
func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) 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. |
|
// "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed. |
|
fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg) |
|
if err != nil { |
|
log.Error("PostProcessCommitMessage: %v", err) |
|
return "" |
|
} |
|
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") |
|
if len(msgLines) == 0 { |
|
return "" |
|
} |
|
return renderCodeBlock(template.HTML(msgLines[0])) |
|
} |
|
|
|
// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to |
|
// the provided default url, handling for special links without email to links. |
|
func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, repo *repo.Repository) 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 "" |
|
} |
|
|
|
// we can safely assume that it will not return any error, since there shouldn't be any special HTML. |
|
renderedMessage, err := markup.PostProcessCommitMessageSubject(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), urlDefault, template.HTMLEscapeString(msgLine)) |
|
if err != nil { |
|
log.Error("PostProcessCommitMessageSubject: %v", err) |
|
return "" |
|
} |
|
return renderCodeBlock(template.HTML(renderedMessage)) |
|
} |
|
|
|
// RenderCommitBody extracts the body of a commit message without its title. |
|
func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) template.HTML { |
|
msgLine := strings.TrimSpace(msg) |
|
lineEnd := strings.IndexByte(msgLine, '\n') |
|
if lineEnd > 0 { |
|
msgLine = msgLine[lineEnd+1:] |
|
} else { |
|
return "" |
|
} |
|
msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace) |
|
if len(msgLine) == 0 { |
|
return "" |
|
} |
|
|
|
renderedMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(msgLine)) |
|
if err != nil { |
|
log.Error("PostProcessCommitMessage: %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 |
|
func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { |
|
htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), `<code class="inline-code-block">$1</code>`) // replace with HTML <code> tags |
|
return template.HTML(htmlWithCodeTags) |
|
} |
|
|
|
// RenderIssueTitle renders issue/pull title with defined post processors |
|
func (ut *RenderUtils) RenderIssueTitle(text string, repo *repo.Repository) template.HTML { |
|
renderedText, err := markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(text)) |
|
if err != nil { |
|
log.Error("PostProcessIssueTitle: %v", err) |
|
return "" |
|
} |
|
return renderCodeBlock(template.HTML(renderedText)) |
|
} |
|
|
|
// RenderIssueSimpleTitle only renders with emoji and inline code block |
|
func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML { |
|
ret := ut.RenderEmoji(text) |
|
ret = renderCodeBlock(ret) |
|
return ret |
|
} |
|
|
|
func (ut *RenderUtils) RenderLabelWithLink(label *issues_model.Label, link any) template.HTML { |
|
var attrHref template.HTML |
|
switch link.(type) { |
|
case template.URL, string: |
|
attrHref = htmlutil.HTMLFormat(`href="%s"`, link) |
|
default: |
|
panic(fmt.Sprintf("unexpected type %T for link", link)) |
|
} |
|
return ut.renderLabelWithTag(label, "a", attrHref) |
|
} |
|
|
|
func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { |
|
return ut.renderLabelWithTag(label, "span", "") |
|
} |
|
|
|
// RenderLabel renders a label |
|
func (ut *RenderUtils) renderLabelWithTag(label *issues_model.Label, tagName, tagAttrs template.HTML) template.HTML { |
|
locale := ut.ctx.Value(translation.ContextKey).(translation.Locale) |
|
var extraCSSClasses string |
|
textColor := util.ContrastColor(label.Color) |
|
labelScope := label.ExclusiveScope() |
|
descriptionText := emoji.ReplaceAliases(label.Description) |
|
|
|
if label.IsArchived() { |
|
extraCSSClasses = "archived-label" |
|
descriptionText = fmt.Sprintf("(%s) %s", locale.TrString("archived"), descriptionText) |
|
} |
|
|
|
if labelScope == "" { |
|
// Regular label |
|
return htmlutil.HTMLFormat(`<%s %s class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s"><span class="gt-ellipsis">%s</span></%s>`, |
|
tagName, tagAttrs, extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name), tagName) |
|
} |
|
|
|
// Scoped label |
|
scopeHTML := ut.RenderEmoji(labelScope) |
|
itemHTML := ut.RenderEmoji(label.Name[len(labelScope)+1:]) |
|
|
|
// Make scope and item background colors slightly darker and lighter respectively. |
|
// More contrast needed with higher luminance, empirically tweaked. |
|
luminance := util.GetRelativeLuminance(label.Color) |
|
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 the 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) |
|
|
|
r, g, b := util.HexToRBGColor(label.Color) |
|
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) |
|
|
|
if label.ExclusiveOrder > 0 { |
|
// <scope> | <label> | <order> |
|
return htmlutil.HTMLFormat(`<%s %s class="ui label %s scope-parent" data-tooltip-content title="%s">`+ |
|
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+ |
|
`<div class="ui label scope-middle" style="color: %s !important; background-color: %s !important">%s</div>`+ |
|
`<div class="ui label scope-right">%d</div>`+ |
|
`</%s>`, |
|
tagName, tagAttrs, |
|
extraCSSClasses, descriptionText, |
|
textColor, scopeColor, scopeHTML, |
|
textColor, itemColor, itemHTML, |
|
label.ExclusiveOrder, |
|
tagName) |
|
} |
|
|
|
// <scope> | <label> |
|
return htmlutil.HTMLFormat(`<%s %s class="ui label %s scope-parent" data-tooltip-content 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>`+ |
|
`</%s>`, |
|
tagName, tagAttrs, |
|
extraCSSClasses, descriptionText, |
|
textColor, scopeColor, scopeHTML, |
|
textColor, itemColor, itemHTML, |
|
tagName) |
|
} |
|
|
|
// RenderEmoji renders html text with emoji post processors |
|
func (ut *RenderUtils) RenderEmoji(text string) template.HTML { |
|
renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text)) |
|
if err != nil { |
|
log.Error("RenderEmoji: %v", err) |
|
return "" |
|
} |
|
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))) |
|
} |
|
|
|
func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive // variable naming triggers on Html, wants HTML |
|
output, err := markdown.RenderString(markup.NewRenderContext(ut.ctx).WithMetas(markup.ComposeSimpleDocumentMetas()), input) |
|
if err != nil { |
|
log.Error("RenderString: %v", err) |
|
} |
|
return output |
|
} |
|
|
|
func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML { |
|
isPullRequest := issue != nil && issue.IsPull |
|
baseLink := fmt.Sprintf("%s/%s", repoLink, util.Iif(isPullRequest, "pulls", "issues")) |
|
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 |
|
} |
|
link := fmt.Sprintf("%s?labels=%d", baseLink, label.ID) |
|
htmlCode += string(ut.RenderLabelWithLink(label, template.URL(link))) |
|
} |
|
htmlCode += "</span>" |
|
return template.HTML(htmlCode) |
|
} |
|
|
|
func (ut *RenderUtils) RenderThemeItem(info *webtheme.ThemeMetaInfo, iconSize int) template.HTML { |
|
svgName := "octicon-paintbrush" |
|
switch info.ColorScheme { |
|
case "dark": |
|
svgName = "octicon-moon" |
|
case "light": |
|
svgName = "octicon-sun" |
|
case "auto": |
|
svgName = "gitea-eclipse" |
|
} |
|
icon := svg.RenderHTML(svgName, iconSize) |
|
extraIcon := svg.RenderHTML(info.GetExtraIconName(), iconSize) |
|
return htmlutil.HTMLFormat(`<div class="theme-menu-item" data-tooltip-content="%s">%s %s %s</div>`, info.GetDescription(), icon, info.DisplayName, extraIcon) |
|
}
|
|
|