loading up the forgejo repo on tangled to test page performance
at forgejo 9.3 kB view raw
1// Copyright 2023 The Gitea Authors. All rights reserved. 2// SPDX-License-Identifier: MIT 3 4package templates 5 6import ( 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 "forgejo.org/models/issues" 18 "forgejo.org/modules/emoji" 19 "forgejo.org/modules/log" 20 "forgejo.org/modules/markup" 21 "forgejo.org/modules/markup/markdown" 22 "forgejo.org/modules/setting" 23 "forgejo.org/modules/translation" 24 "forgejo.org/modules/util" 25) 26 27// RenderCommitMessage renders commit message with XSS-safe and special links. 28func 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. 49func 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. 75func 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. 100var codeMatcher = regexp.MustCompile("`([^`]+)`") 101 102// RenderCodeBlock renders "`…`" as highlighted "<code>" block, intended for issue and PR titles 103func 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 108const ( 109 activeLabelOpacity = uint8(255) 110 archivedLabelOpacity = uint8(127) 111) 112 113func GetLabelOpacityByte(isArchived bool) uint8 { 114 if isArchived { 115 return archivedLabelOpacity 116 } 117 return activeLabelOpacity 118} 119 120// RenderIssueTitle renders issue/pull title with defined post processors 121func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) template.HTML { 122 renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ 123 Ctx: ctx, 124 Metas: metas, 125 }, template.HTMLEscapeString(text)) 126 if err != nil { 127 log.Error("RenderIssueTitle: %v", err) 128 return template.HTML("") 129 } 130 return template.HTML(renderedText) 131} 132 133// RenderRefIssueTitle renders referenced issue/pull title with defined post processors 134func RenderRefIssueTitle(ctx context.Context, text string) template.HTML { 135 renderedText, err := markup.RenderRefIssueTitle(&markup.RenderContext{Ctx: ctx}, template.HTMLEscapeString(text)) 136 if err != nil { 137 log.Error("RenderRefIssueTitle: %v", err) 138 return "" 139 } 140 141 return template.HTML(renderedText) 142} 143 144// RenderLabel renders a label 145// locale is needed due to an import cycle with our context providing the `Tr` function 146func RenderLabel(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML { 147 var ( 148 archivedCSSClass string 149 textColor = util.ContrastColor(label.Color) 150 labelScope = label.ExclusiveScope() 151 ) 152 153 description := emoji.ReplaceAliases(template.HTMLEscapeString(label.Description)) 154 155 if label.IsArchived() { 156 archivedCSSClass = "archived-label" 157 description = locale.TrString("repo.issues.archived_label_description", description) 158 } 159 160 if labelScope == "" { 161 // Regular label 162 163 labelColor := label.Color + hex.EncodeToString([]byte{GetLabelOpacityByte(label.IsArchived())}) 164 s := fmt.Sprintf("<div class='ui label %s' style='color: %s !important; background-color: %s !important;' data-tooltip-content title='%s'>%s</div>", 165 archivedCSSClass, textColor, labelColor, description, RenderEmoji(ctx, label.Name)) 166 return template.HTML(s) 167 } 168 169 // Scoped label 170 scopeText := RenderEmoji(ctx, labelScope) 171 itemText := RenderEmoji(ctx, label.Name[len(labelScope)+1:]) 172 173 // Make scope and item background colors slightly darker and lighter respectively. 174 // More contrast needed with higher luminance, empirically tweaked. 175 luminance := util.GetRelativeLuminance(label.Color) 176 contrast := 0.01 + luminance*0.03 177 // Ensure we add the same amount of contrast also near 0 and 1. 178 darken := contrast + math.Max(luminance+contrast-1.0, 0.0) 179 lighten := contrast + math.Max(contrast-luminance, 0.0) 180 // Compute factor to keep RGB values proportional. 181 darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0) 182 lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0) 183 184 opacity := GetLabelOpacityByte(label.IsArchived()) 185 r, g, b := util.HexToRBGColor(label.Color) 186 scopeBytes := []byte{ 187 uint8(math.Min(math.Round(r*darkenFactor), 255)), 188 uint8(math.Min(math.Round(g*darkenFactor), 255)), 189 uint8(math.Min(math.Round(b*darkenFactor), 255)), 190 opacity, 191 } 192 itemBytes := []byte{ 193 uint8(math.Min(math.Round(r*lightenFactor), 255)), 194 uint8(math.Min(math.Round(g*lightenFactor), 255)), 195 uint8(math.Min(math.Round(b*lightenFactor), 255)), 196 opacity, 197 } 198 199 scopeColor := "#" + hex.EncodeToString(scopeBytes) 200 itemColor := "#" + hex.EncodeToString(itemBytes) 201 202 s := fmt.Sprintf("<span class='ui label %s scope-parent' data-tooltip-content title='%s'>"+ 203 "<div class='ui label scope-left' style='color: %s !important; background-color: %s !important'>%s</div>"+ 204 "<div class='ui label scope-right' style='color: %s !important; background-color: %s !important'>%s</div>"+ 205 "</span>", 206 archivedCSSClass, description, 207 textColor, scopeColor, scopeText, 208 textColor, itemColor, itemText) 209 return template.HTML(s) 210} 211 212// RenderEmoji renders html text with emoji post processors 213func RenderEmoji(ctx context.Context, text string) template.HTML { 214 renderedText, err := markup.RenderEmoji(&markup.RenderContext{Ctx: ctx}, 215 template.HTMLEscapeString(text)) 216 if err != nil { 217 log.Error("RenderEmoji: %v", err) 218 return template.HTML("") 219 } 220 return template.HTML(renderedText) 221} 222 223// ReactionToEmoji renders emoji for use in reactions 224func ReactionToEmoji(reaction string) template.HTML { 225 val := emoji.FromCode(reaction) 226 if val != nil { 227 return template.HTML(val.Emoji) 228 } 229 val = emoji.FromAlias(reaction) 230 if val != nil { 231 return template.HTML(val.Emoji) 232 } 233 return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction))) 234} 235 236func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive 237 output, err := markdown.RenderString(&markup.RenderContext{ 238 Ctx: ctx, 239 Metas: map[string]string{"mode": "document"}, 240 }, input) 241 if err != nil { 242 log.Error("RenderString: %v", err) 243 } 244 return output 245} 246 247func RenderLabels(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string, isPull bool) template.HTML { 248 htmlCode := `<span class="labels-list">` 249 for _, label := range labels { 250 // Protect against nil value in labels - shouldn't happen but would cause a panic if so 251 if label == nil { 252 continue 253 } 254 255 issuesOrPull := "issues" 256 if isPull { 257 issuesOrPull = "pulls" 258 } 259 htmlCode += fmt.Sprintf("<a href='%s/%s?labels=%d' rel='nofollow'>%s</a> ", 260 repoLink, issuesOrPull, label.ID, RenderLabel(ctx, locale, label)) 261 } 262 htmlCode += "</span>" 263 return template.HTML(htmlCode) 264} 265 266func RenderReviewRequest(users []issues_model.RequestReviewTarget) template.HTML { 267 usernames := make([]string, 0, len(users)) 268 for _, user := range users { 269 usernames = append(usernames, template.HTMLEscapeString(user.Name())) 270 } 271 272 htmlCode := `<span class="review-request-list">` 273 htmlCode += strings.Join(usernames, ", ") 274 htmlCode += "</span>" 275 return template.HTML(htmlCode) 276}