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.0KB

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