gitea源码

html_commit.go 5.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package markup
  4. import (
  5. "slices"
  6. "strings"
  7. "code.gitea.io/gitea/modules/base"
  8. "code.gitea.io/gitea/modules/references"
  9. "code.gitea.io/gitea/modules/util"
  10. "golang.org/x/net/html"
  11. "golang.org/x/net/html/atom"
  12. )
  13. type anyHashPatternResult struct {
  14. PosStart int
  15. PosEnd int
  16. FullURL string
  17. CommitID string
  18. SubPath string
  19. QueryHash string
  20. }
  21. func createCodeLink(href, content, class string) *html.Node {
  22. a := &html.Node{
  23. Type: html.ElementNode,
  24. Data: atom.A.String(),
  25. Attr: []html.Attribute{{Key: "href", Val: href}},
  26. }
  27. if class != "" {
  28. a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
  29. }
  30. text := &html.Node{
  31. Type: html.TextNode,
  32. Data: content,
  33. }
  34. code := &html.Node{
  35. Type: html.ElementNode,
  36. Data: atom.Code.String(),
  37. }
  38. code.AppendChild(text)
  39. a.AppendChild(code)
  40. return a
  41. }
  42. func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
  43. m := globalVars().anyHashPattern.FindStringSubmatchIndex(s)
  44. if m == nil {
  45. return ret, false
  46. }
  47. ret.PosStart, ret.PosEnd = m[0], m[1]
  48. ret.FullURL = s[ret.PosStart:ret.PosEnd]
  49. if strings.HasSuffix(ret.FullURL, ".") {
  50. // if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence.
  51. ret.PosEnd--
  52. ret.FullURL = ret.FullURL[:len(ret.FullURL)-1]
  53. for i := range m {
  54. m[i] = min(m[i], ret.PosEnd)
  55. }
  56. }
  57. ret.CommitID = s[m[2]:m[3]]
  58. if m[5] > 0 {
  59. ret.SubPath = s[m[4]:m[5]]
  60. }
  61. lastStart, lastEnd := m[len(m)-2], m[len(m)-1]
  62. if lastEnd > 0 {
  63. ret.QueryHash = s[lastStart:lastEnd][1:]
  64. }
  65. return ret, true
  66. }
  67. // fullHashPatternProcessor renders SHA containing URLs
  68. func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
  69. if ctx.RenderOptions.Metas == nil {
  70. return
  71. }
  72. nodeStop := node.NextSibling
  73. for node != nodeStop {
  74. if node.Type != html.TextNode {
  75. node = node.NextSibling
  76. continue
  77. }
  78. ret, ok := anyHashPatternExtract(node.Data)
  79. if !ok {
  80. node = node.NextSibling
  81. continue
  82. }
  83. text := base.ShortSha(ret.CommitID)
  84. if ret.SubPath != "" {
  85. text += ret.SubPath
  86. }
  87. if ret.QueryHash != "" {
  88. text += " (" + ret.QueryHash + ")"
  89. }
  90. replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit"))
  91. node = node.NextSibling.NextSibling
  92. }
  93. }
  94. func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
  95. if ctx.RenderOptions.Metas == nil {
  96. return
  97. }
  98. nodeStop := node.NextSibling
  99. for node != nodeStop {
  100. if node.Type != html.TextNode {
  101. node = node.NextSibling
  102. continue
  103. }
  104. m := globalVars().comparePattern.FindStringSubmatchIndex(node.Data)
  105. if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match
  106. node = node.NextSibling
  107. continue
  108. }
  109. urlFull := node.Data[m[0]:m[1]]
  110. text1 := base.ShortSha(node.Data[m[2]:m[3]])
  111. textDots := base.ShortSha(node.Data[m[4]:m[5]])
  112. text2 := base.ShortSha(node.Data[m[6]:m[7]])
  113. hash := ""
  114. if m[9] > 0 {
  115. hash = node.Data[m[8]:m[9]][1:]
  116. }
  117. start := m[0]
  118. end := m[1]
  119. // If url ends in '.', it's very likely that it is not part of the
  120. // actual url but used to finish a sentence.
  121. if strings.HasSuffix(urlFull, ".") {
  122. end--
  123. urlFull = urlFull[:len(urlFull)-1]
  124. if hash != "" {
  125. hash = hash[:len(hash)-1]
  126. } else if text2 != "" {
  127. text2 = text2[:len(text2)-1]
  128. }
  129. }
  130. text := text1 + textDots + text2
  131. if hash != "" {
  132. text += " (" + hash + ")"
  133. }
  134. replaceContent(node, start, end, createCodeLink(urlFull, text, "compare"))
  135. node = node.NextSibling.NextSibling
  136. }
  137. }
  138. // hashCurrentPatternProcessor renders SHA1 strings to corresponding links that
  139. // are assumed to be in the same repository.
  140. func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
  141. if ctx.RenderOptions.Metas == nil || ctx.RenderOptions.Metas["user"] == "" || ctx.RenderOptions.Metas["repo"] == "" || ctx.RenderHelper == nil {
  142. return
  143. }
  144. start := 0
  145. next := node.NextSibling
  146. for node != nil && node != next && start < len(node.Data) {
  147. m := globalVars().hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:])
  148. if m == nil {
  149. return
  150. }
  151. m[2] += start
  152. m[3] += start
  153. hash := node.Data[m[2]:m[3]]
  154. // The regex does not lie, it matches the hash pattern.
  155. // However, a regex cannot know if a hash actually exists or not.
  156. // We could assume that a SHA1 hash should probably contain alphas AND numerics
  157. // but that is not always the case.
  158. // Although unlikely, deadbeef and 1234567 are valid short forms of SHA1 hash
  159. // as used by git and github for linking and thus we have to do similar.
  160. // Because of this, we check to make sure that a matched hash is actually
  161. // a commit in the repository before making it a link.
  162. if !ctx.RenderHelper.IsCommitIDExisting(hash) {
  163. start = m[3]
  164. continue
  165. }
  166. link := "/:root/" + util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash)
  167. replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
  168. start = 0
  169. node = node.NextSibling.NextSibling
  170. }
  171. }
  172. func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
  173. next := node.NextSibling
  174. for node != nil && node != next {
  175. found, ref := references.FindRenderizableCommitCrossReference(node.Data)
  176. if !found {
  177. return
  178. }
  179. refText := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
  180. linkHref := "/:root/" + util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha)
  181. link := createLink(ctx, linkHref, refText, "commit")
  182. replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
  183. node = node.NextSibling.NextSibling
  184. }
  185. }