gitea源码

html_issue.go 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package markup
  4. import (
  5. "strconv"
  6. "strings"
  7. "code.gitea.io/gitea/modules/httplib"
  8. "code.gitea.io/gitea/modules/log"
  9. "code.gitea.io/gitea/modules/references"
  10. "code.gitea.io/gitea/modules/regexplru"
  11. "code.gitea.io/gitea/modules/templates/vars"
  12. "code.gitea.io/gitea/modules/translation"
  13. "code.gitea.io/gitea/modules/util"
  14. "golang.org/x/net/html"
  15. "golang.org/x/net/html/atom"
  16. )
  17. type RenderIssueIconTitleOptions struct {
  18. OwnerName string
  19. RepoName string
  20. LinkHref string
  21. IssueIndex int64
  22. }
  23. func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
  24. if ctx.RenderOptions.Metas == nil {
  25. return
  26. }
  27. next := node.NextSibling
  28. for node != nil && node != next {
  29. m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data)
  30. if m == nil {
  31. return
  32. }
  33. mDiffView := globalVars().filesChangedFullPattern.FindStringSubmatchIndex(node.Data)
  34. // leave it as it is if the link is from "Files Changed" tab in PR Diff View https://domain/org/repo/pulls/27/files
  35. if mDiffView != nil {
  36. return
  37. }
  38. link := node.Data[m[0]:m[1]]
  39. if !httplib.IsCurrentGiteaSiteURL(ctx, link) {
  40. return
  41. }
  42. text := "#" + node.Data[m[2]:m[3]]
  43. // if m[4] and m[5] is not -1, then link is to a comment
  44. // indicate that in the text by appending (comment)
  45. if m[4] != -1 && m[5] != -1 {
  46. if locale, ok := ctx.Value(translation.ContextKey).(translation.Locale); ok {
  47. text += " " + locale.TrString("repo.from_comment")
  48. } else {
  49. text += " (comment)"
  50. }
  51. }
  52. // extract repo and org name from matched link like
  53. // http://localhost:3000/gituser/myrepo/issues/1
  54. linkParts := strings.Split(link, "/")
  55. matchOrg := linkParts[len(linkParts)-4]
  56. matchRepo := linkParts[len(linkParts)-3]
  57. if matchOrg == ctx.RenderOptions.Metas["user"] && matchRepo == ctx.RenderOptions.Metas["repo"] {
  58. replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
  59. } else {
  60. text = matchOrg + "/" + matchRepo + text
  61. replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
  62. }
  63. node = node.NextSibling.NextSibling
  64. }
  65. }
  66. func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref *references.RenderizableReference) *html.Node {
  67. if DefaultRenderHelperFuncs.RenderRepoIssueIconTitle == nil {
  68. return nil
  69. }
  70. issueIndex, _ := strconv.ParseInt(ref.Issue, 10, 64)
  71. h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{
  72. OwnerName: ref.Owner,
  73. RepoName: ref.Name,
  74. LinkHref: ctx.RenderHelper.ResolveLink(linkHref, LinkTypeDefault),
  75. IssueIndex: issueIndex,
  76. })
  77. if err != nil {
  78. log.Error("RenderRepoIssueIconTitle failed: %v", err)
  79. return nil
  80. }
  81. if h == "" {
  82. return nil
  83. }
  84. return &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(h))}
  85. }
  86. func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
  87. if ctx.RenderOptions.Metas == nil {
  88. return
  89. }
  90. // crossLinkOnly: do not parse "#123", only parse "owner/repo#123"
  91. // if there is no repo in the context, then the "#123" format can't be parsed
  92. // old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki
  93. crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true"
  94. var ref *references.RenderizableReference
  95. next := node.NextSibling
  96. for node != nil && node != next {
  97. _, hasExtTrackFormat := ctx.RenderOptions.Metas["format"]
  98. // Repos with external issue trackers might still need to reference local PRs
  99. // We need to concern with the first one that shows up in the text, whichever it is
  100. isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric
  101. refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
  102. switch ctx.RenderOptions.Metas["style"] {
  103. case "", IssueNameStyleNumeric:
  104. ref = refNumeric
  105. case IssueNameStyleAlphanumeric:
  106. ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
  107. case IssueNameStyleRegexp:
  108. pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"])
  109. if err != nil {
  110. return
  111. }
  112. ref = references.FindRenderizableReferenceRegexp(node.Data, pattern)
  113. }
  114. // Repos with external issue trackers might still need to reference local PRs
  115. // We need to concern with the first one that shows up in the text, whichever it is
  116. if hasExtTrackFormat && !isNumericStyle && refNumeric != nil {
  117. // If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that
  118. // Allow a free-pass when non-numeric pattern wasn't found.
  119. if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start {
  120. ref = refNumeric
  121. }
  122. }
  123. if ref == nil {
  124. return
  125. }
  126. var link *html.Node
  127. refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
  128. if hasExtTrackFormat && !ref.IsPull {
  129. ctx.RenderOptions.Metas["index"] = ref.Issue
  130. res, err := vars.Expand(ctx.RenderOptions.Metas["format"], ctx.RenderOptions.Metas)
  131. if err != nil {
  132. // here we could just log the error and continue the rendering
  133. log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
  134. }
  135. link = createLink(ctx, res, refText, "ref-issue ref-external-issue")
  136. } else {
  137. // Path determines the type of link that will be rendered. It's unknown at this point whether
  138. // the linked item is actually a PR or an issue. Luckily it's of no real consequence because
  139. // Gitea will redirect on click as appropriate.
  140. issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner)
  141. issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name)
  142. issuePath := util.Iif(ref.IsPull, "pulls", "issues")
  143. linkHref := "/:root/" + util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue)
  144. // at the moment, only render the issue index in a full line (or simple line) as icon+title
  145. // otherwise it would be too noisy for "take #1 as an example" in a sentence
  146. if node.Parent.DataAtom == atom.Li && ref.RefLocation.Start < 20 && ref.RefLocation.End == len(node.Data) {
  147. link = createIssueLinkContentWithSummary(ctx, linkHref, ref)
  148. }
  149. if link == nil {
  150. link = createLink(ctx, linkHref, refText, "ref-issue")
  151. }
  152. }
  153. if ref.Action == references.XRefActionNone {
  154. replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
  155. node = node.NextSibling.NextSibling
  156. continue
  157. }
  158. // Decorate action keywords if actionable
  159. var keyword *html.Node
  160. if references.IsXrefActionable(ref, hasExtTrackFormat) {
  161. keyword = createKeyword(ctx, node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
  162. } else {
  163. keyword = &html.Node{
  164. Type: html.TextNode,
  165. Data: node.Data[ref.ActionLocation.Start:ref.ActionLocation.End],
  166. }
  167. }
  168. spaces := &html.Node{
  169. Type: html.TextNode,
  170. Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start],
  171. }
  172. replaceContentList(node, ref.ActionLocation.Start, ref.RefLocation.End, []*html.Node{keyword, spaces, link})
  173. node = node.NextSibling.NextSibling.NextSibling.NextSibling
  174. }
  175. }