code.gitea.io/gitea@v1.22.3/modules/templates/util_render.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package templates 5 6 import ( 7 "context" 8 "encoding/hex" 9 "fmt" 10 "html/template" 11 "math" 12 "net/url" 13 "regexp" 14 "strings" 15 "unicode" 16 17 issues_model "code.gitea.io/gitea/models/issues" 18 "code.gitea.io/gitea/modules/emoji" 19 "code.gitea.io/gitea/modules/log" 20 "code.gitea.io/gitea/modules/markup" 21 "code.gitea.io/gitea/modules/markup/markdown" 22 "code.gitea.io/gitea/modules/setting" 23 "code.gitea.io/gitea/modules/translation" 24 "code.gitea.io/gitea/modules/util" 25 ) 26 27 // RenderCommitMessage renders commit message with XSS-safe and special links. 28 func RenderCommitMessage(ctx context.Context, msg string, metas map[string]string) template.HTML { 29 cleanMsg := template.HTMLEscapeString(msg) 30 // we can safely assume that it will not return any error, since there 31 // shouldn't be any special HTML. 32 fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ 33 Ctx: ctx, 34 Metas: metas, 35 }, cleanMsg) 36 if err != nil { 37 log.Error("RenderCommitMessage: %v", err) 38 return "" 39 } 40 msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") 41 if len(msgLines) == 0 { 42 return template.HTML("") 43 } 44 return RenderCodeBlock(template.HTML(msgLines[0])) 45 } 46 47 // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to 48 // the provided default url, handling for special links without email to links. 49 func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML { 50 msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace) 51 lineEnd := strings.IndexByte(msgLine, '\n') 52 if lineEnd > 0 { 53 msgLine = msgLine[:lineEnd] 54 } 55 msgLine = strings.TrimRightFunc(msgLine, unicode.IsSpace) 56 if len(msgLine) == 0 { 57 return template.HTML("") 58 } 59 60 // we can safely assume that it will not return any error, since there 61 // shouldn't be any special HTML. 62 renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{ 63 Ctx: ctx, 64 DefaultLink: urlDefault, 65 Metas: metas, 66 }, template.HTMLEscapeString(msgLine)) 67 if err != nil { 68 log.Error("RenderCommitMessageSubject: %v", err) 69 return template.HTML("") 70 } 71 return RenderCodeBlock(template.HTML(renderedMessage)) 72 } 73 74 // RenderCommitBody extracts the body of a commit message without its title. 75 func RenderCommitBody(ctx context.Context, msg string, metas map[string]string) template.HTML { 76 msgLine := strings.TrimSpace(msg) 77 lineEnd := strings.IndexByte(msgLine, '\n') 78 if lineEnd > 0 { 79 msgLine = msgLine[lineEnd+1:] 80 } else { 81 return "" 82 } 83 msgLine = strings.TrimLeftFunc(msgLine, unicode.IsSpace) 84 if len(msgLine) == 0 { 85 return "" 86 } 87 88 renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ 89 Ctx: ctx, 90 Metas: metas, 91 }, template.HTMLEscapeString(msgLine)) 92 if err != nil { 93 log.Error("RenderCommitMessage: %v", err) 94 return "" 95 } 96 return template.HTML(renderedMessage) 97 } 98 99 // Match text that is between back ticks. 100 var codeMatcher = regexp.MustCompile("`([^`]+)`") 101 102 // RenderCodeBlock renders "`…`" as highlighted "<code>" block, intended for issue and PR titles 103 func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { 104 htmlWithCodeTags := codeMatcher.ReplaceAllString(string(htmlEscapedTextToRender), `<code class="inline-code-block">$1</code>`) // replace with HTML <code> tags 105 return template.HTML(htmlWithCodeTags) 106 } 107 108 // RenderIssueTitle renders issue/pull title with defined post processors 109 func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) template.HTML { 110 renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ 111 Ctx: ctx, 112 Metas: metas, 113 }, template.HTMLEscapeString(text)) 114 if err != nil { 115 log.Error("RenderIssueTitle: %v", err) 116 return template.HTML("") 117 } 118 return template.HTML(renderedText) 119 } 120 121 // RenderLabel renders a label 122 // locale is needed due to an import cycle with our context providing the `Tr` function 123 func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML { 124 var extraCSSClasses string 125 textColor := util.ContrastColor(label.Color) 126 labelScope := label.ExclusiveScope() 127 descriptionText := emoji.ReplaceAliases(label.Description) 128 129 if label.IsArchived() { 130 extraCSSClasses = "archived-label" 131 descriptionText = fmt.Sprintf("(%s) %s", locale.TrString("archived"), descriptionText) 132 } 133 134 if labelScope == "" { 135 // Regular label 136 return HTMLFormat(`<div class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s">%s</div>`, 137 extraCSSClasses, textColor, label.Color, descriptionText, RenderEmoji(ctx, label.Name)) 138 } 139 140 // Scoped label 141 scopeHTML := RenderEmoji(ctx, labelScope) 142 itemHTML := RenderEmoji(ctx, label.Name[len(labelScope)+1:]) 143 144 // Make scope and item background colors slightly darker and lighter respectively. 145 // More contrast needed with higher luminance, empirically tweaked. 146 luminance := util.GetRelativeLuminance(label.Color) 147 contrast := 0.01 + luminance*0.03 148 // Ensure we add the same amount of contrast also near 0 and 1. 149 darken := contrast + math.Max(luminance+contrast-1.0, 0.0) 150 lighten := contrast + math.Max(contrast-luminance, 0.0) 151 // Compute factor to keep RGB values proportional. 152 darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0) 153 lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0) 154 155 r, g, b := util.HexToRBGColor(label.Color) 156 scopeBytes := []byte{ 157 uint8(math.Min(math.Round(r*darkenFactor), 255)), 158 uint8(math.Min(math.Round(g*darkenFactor), 255)), 159 uint8(math.Min(math.Round(b*darkenFactor), 255)), 160 } 161 itemBytes := []byte{ 162 uint8(math.Min(math.Round(r*lightenFactor), 255)), 163 uint8(math.Min(math.Round(g*lightenFactor), 255)), 164 uint8(math.Min(math.Round(b*lightenFactor), 255)), 165 } 166 167 itemColor := "#" + hex.EncodeToString(itemBytes) 168 scopeColor := "#" + hex.EncodeToString(scopeBytes) 169 170 return HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+ 171 `<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+ 172 `<div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div>`+ 173 `</span>`, 174 extraCSSClasses, descriptionText, 175 textColor, scopeColor, scopeHTML, 176 textColor, itemColor, itemHTML) 177 } 178 179 // RenderEmoji renders html text with emoji post processors 180 func RenderEmoji(ctx context.Context, text string) template.HTML { 181 renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx}, 182 template.HTMLEscapeString(text)) 183 if err != nil { 184 log.Error("RenderEmoji: %v", err) 185 return template.HTML("") 186 } 187 return template.HTML(renderedText) 188 } 189 190 // ReactionToEmoji renders emoji for use in reactions 191 func ReactionToEmoji(reaction string) template.HTML { 192 val := emoji.FromCode(reaction) 193 if val != nil { 194 return template.HTML(val.Emoji) 195 } 196 val = emoji.FromAlias(reaction) 197 if val != nil { 198 return template.HTML(val.Emoji) 199 } 200 return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction))) 201 } 202 203 func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive 204 output, err := markdown.RenderString(&markup.RenderContext{ 205 Ctx: ctx, 206 Metas: map[string]string{"mode": "document"}, 207 }, input) 208 if err != nil { 209 log.Error("RenderString: %v", err) 210 } 211 return output 212 } 213 214 func RenderLabels(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML { 215 isPullRequest := issue != nil && issue.IsPull 216 baseLink := fmt.Sprintf("%s/%s", repoLink, util.Iif(isPullRequest, "pulls", "issues")) 217 htmlCode := `<span class="labels-list">` 218 for _, label := range labels { 219 // Protect against nil value in labels - shouldn't happen but would cause a panic if so 220 if label == nil { 221 continue 222 } 223 htmlCode += fmt.Sprintf(`<a href="%s?labels=%d">%s</a>`, baseLink, label.ID, RenderLabel(ctx, locale, label)) 224 } 225 htmlCode += "</span>" 226 return template.HTML(htmlCode) 227 }