gitea源码

html_link.go 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package markup
  4. import (
  5. "net/url"
  6. "path"
  7. "strings"
  8. "code.gitea.io/gitea/modules/markup/common"
  9. "code.gitea.io/gitea/modules/util"
  10. "golang.org/x/net/html"
  11. "golang.org/x/net/html/atom"
  12. )
  13. func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
  14. next := node.NextSibling
  15. for node != nil && node != next {
  16. m := globalVars().shortLinkPattern.FindStringSubmatchIndex(node.Data)
  17. if m == nil {
  18. return
  19. }
  20. content := node.Data[m[2]:m[3]]
  21. tail := node.Data[m[4]:m[5]]
  22. props := make(map[string]string)
  23. // MediaWiki uses [[link|text]], while GitHub uses [[text|link]]
  24. // It makes page handling terrible, but we prefer GitHub syntax
  25. // And fall back to MediaWiki only when it is obvious from the look
  26. // Of text and link contents
  27. sl := strings.SplitSeq(content, "|")
  28. for v := range sl {
  29. if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
  30. // There is no equal in this argument; this is a mandatory arg
  31. if props["name"] == "" {
  32. if IsFullURLString(v) {
  33. // If we clearly see it is a link, we save it so
  34. // But first we need to ensure, that if both mandatory args provided
  35. // look like links, we stick to GitHub syntax
  36. if props["link"] != "" {
  37. props["name"] = props["link"]
  38. }
  39. props["link"] = strings.TrimSpace(v)
  40. } else {
  41. props["name"] = v
  42. }
  43. } else {
  44. props["link"] = strings.TrimSpace(v)
  45. }
  46. } else {
  47. // There is an equal; optional argument.
  48. sep := strings.IndexByte(v, '=')
  49. key, val := v[:sep], html.UnescapeString(v[sep+1:])
  50. // When parsing HTML, x/net/html will change all quotes which are
  51. // not used for syntax into UTF-8 quotes. So checking val[0] won't
  52. // be enough, since that only checks a single byte.
  53. if len(val) > 1 {
  54. if (strings.HasPrefix(val, "“") && strings.HasSuffix(val, "”")) ||
  55. (strings.HasPrefix(val, "‘") && strings.HasSuffix(val, "’")) {
  56. const lenQuote = len("‘")
  57. val = val[lenQuote : len(val)-lenQuote]
  58. } else if (strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"")) ||
  59. (strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'")) {
  60. val = val[1 : len(val)-1]
  61. } else if strings.HasPrefix(val, "'") && strings.HasSuffix(val, "’") {
  62. const lenQuote = len("‘")
  63. val = val[1 : len(val)-lenQuote]
  64. }
  65. }
  66. props[key] = val
  67. }
  68. }
  69. var name, link string
  70. if props["link"] != "" {
  71. link = props["link"]
  72. } else if props["name"] != "" {
  73. link = props["name"]
  74. }
  75. if props["title"] != "" {
  76. name = props["title"]
  77. } else if props["name"] != "" {
  78. name = props["name"]
  79. } else {
  80. name = link
  81. }
  82. name += tail
  83. image := false
  84. ext := path.Ext(link)
  85. switch ext {
  86. // fast path: empty string, ignore
  87. case "":
  88. // leave image as false
  89. case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg":
  90. image = true
  91. }
  92. childNode := &html.Node{}
  93. linkNode := &html.Node{
  94. FirstChild: childNode,
  95. LastChild: childNode,
  96. Type: html.ElementNode,
  97. Data: "a",
  98. DataAtom: atom.A,
  99. }
  100. childNode.Parent = linkNode
  101. absoluteLink := IsFullURLString(link)
  102. if !absoluteLink {
  103. if image {
  104. link = strings.ReplaceAll(link, " ", "+")
  105. } else {
  106. // the hacky wiki name encoding: space to "-"
  107. link = strings.ReplaceAll(link, " ", "-") // FIXME: it should support dashes in the link, eg: "the-dash-support.-"
  108. }
  109. if !strings.Contains(link, "/") {
  110. link = url.PathEscape(link) // FIXME: it doesn't seem right and it might cause double-escaping
  111. }
  112. }
  113. if image {
  114. title := props["title"]
  115. if title == "" {
  116. title = props["alt"]
  117. }
  118. if title == "" {
  119. title = path.Base(name)
  120. }
  121. alt := props["alt"]
  122. if alt == "" {
  123. alt = name
  124. }
  125. // make the childNode an image - if we can, we also place the alt
  126. childNode.Type = html.ElementNode
  127. childNode.Data = "img"
  128. childNode.DataAtom = atom.Img
  129. childNode.Attr = []html.Attribute{
  130. {Key: "src", Val: link},
  131. {Key: "title", Val: title},
  132. {Key: "alt", Val: alt},
  133. }
  134. if alt == "" {
  135. childNode.Attr = childNode.Attr[:2]
  136. }
  137. } else {
  138. childNode.Type = html.TextNode
  139. childNode.Data = name
  140. }
  141. linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
  142. replaceContent(node, m[0], m[1], linkNode)
  143. node = node.NextSibling.NextSibling
  144. }
  145. }
  146. // linkProcessor creates links for any HTTP or HTTPS URL not captured by
  147. // markdown.
  148. func linkProcessor(ctx *RenderContext, node *html.Node) {
  149. next := node.NextSibling
  150. for node != nil && node != next {
  151. m := common.GlobalVars().LinkRegex.FindStringIndex(node.Data)
  152. if m == nil {
  153. return
  154. }
  155. uri := node.Data[m[0]:m[1]]
  156. remaining := node.Data[m[1]:]
  157. if util.IsLikelyEllipsisLeftPart(remaining) {
  158. return
  159. }
  160. replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/))
  161. node = node.NextSibling.NextSibling
  162. }
  163. }
  164. // descriptionLinkProcessor creates links for DescriptionHTML
  165. func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) {
  166. next := node.NextSibling
  167. for node != nil && node != next {
  168. m := common.GlobalVars().LinkRegex.FindStringIndex(node.Data)
  169. if m == nil {
  170. return
  171. }
  172. uri := node.Data[m[0]:m[1]]
  173. replaceContent(node, m[0], m[1], createDescriptionLink(uri, uri))
  174. node = node.NextSibling.NextSibling
  175. }
  176. }
  177. func createDescriptionLink(href, content string) *html.Node {
  178. textNode := &html.Node{
  179. Type: html.TextNode,
  180. Data: content,
  181. }
  182. linkNode := &html.Node{
  183. FirstChild: textNode,
  184. LastChild: textNode,
  185. Type: html.ElementNode,
  186. Data: "a",
  187. DataAtom: atom.A,
  188. Attr: []html.Attribute{
  189. {Key: "href", Val: href},
  190. {Key: "target", Val: "_blank"},
  191. {Key: "rel", Val: "noopener noreferrer"},
  192. },
  193. }
  194. textNode.Parent = linkNode
  195. return linkNode
  196. }