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.

util_render.go 8.6KB

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