gitea源码

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