// 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" issues_model "gitea.dev/models/issues" "gitea.dev/models/renderhelper" "gitea.dev/models/repo" "gitea.dev/modules/charset" "gitea.dev/modules/emoji" "gitea.dev/modules/git" "gitea.dev/modules/htmlutil" "gitea.dev/modules/log" "gitea.dev/modules/markup" "gitea.dev/modules/markup/markdown" "gitea.dev/modules/reqctx" "gitea.dev/modules/setting" "gitea.dev/modules/svg" "gitea.dev/modules/translation" "gitea.dev/modules/util" "gitea.dev/services/webtheme" ) type RenderUtils struct { ctx reqctx.RequestContext } func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { return &RenderUtils{ctx: ctx} } // RenderCommitMessage renders commit message title (only title) func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { msgLine := strings.TrimSpace(msg) msgLine, _, _ = strings.Cut(msgLine, "\n") msgLine = strings.TrimSpace(msgLine) rendered := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), htmlutil.EscapeString(msgLine)) return renderCodeBlock(rendered) } // 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.TrimSpace(msg) msgLine, _, _ = strings.Cut(msgLine, "\n") msgLine = strings.TrimSpace(msgLine) rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) rendered := markup.PostProcessCommitMessageSubject(rctx, urlDefault, htmlutil.EscapeString(msgLine)) return renderCodeBlock(rendered) } // RenderCommitBody extracts the body of a commit message without its title. func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) template.HTML { _, body, _ := strings.Cut(strings.TrimSpace(msg), "\n") body = strings.TrimSpace(body) if body == "" { return "" } rctx := renderhelper.NewRenderContextRepoComment(ut.ctx, repo) renderedMessage := markup.PostProcessCommitMessage(rctx, htmlutil.EscapeString(body)) return renderedMessage } // Match text that is between back ticks. var codeMatcher = regexp.MustCompile("`([^`]+)`") // renderCodeBlock renders "`…`" as highlighted "" block, intended for issue and PR titles 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 (ut *RenderUtils) RenderIssueTitle(text string, repo *repo.Repository) template.HTML { // wrap "`…`" in before post-processing so code-span content stays literal, like comment bodies htmlWithCode := renderCodeBlock(htmlutil.EscapeString(text)) return markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), htmlWithCode) } // RenderIssueSimpleTitle only renders with emoji and inline code block func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML { // see RenderIssueTitle: wrap code spans before processing emoji htmlWithCode := renderCodeBlock(htmlutil.EscapeString(text)) return markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), htmlWithCode) } func (ut *RenderUtils) RenderLabel(label *issues_model.Label) 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`, extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name)) } // 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 { // |