gitea源码

html_node.go 3.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package markup
  4. import (
  5. "strings"
  6. "golang.org/x/net/html"
  7. )
  8. func isAnchorIDUserContent(s string) bool {
  9. // blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
  10. // old logic: blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
  11. return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-")
  12. }
  13. func isAnchorIDFootnote(s string) bool {
  14. return strings.HasPrefix(s, "fnref:user-content-") || strings.HasPrefix(s, "fn:user-content-")
  15. }
  16. func isAnchorHrefFootnote(s string) bool {
  17. return strings.HasPrefix(s, "#fnref:user-content-") || strings.HasPrefix(s, "#fn:user-content-")
  18. }
  19. func processNodeAttrID(node *html.Node) {
  20. // Add user-content- to IDs and "#" links if they don't already have them,
  21. // and convert the link href to a relative link to the host root
  22. for idx, attr := range node.Attr {
  23. if attr.Key == "id" {
  24. if !isAnchorIDUserContent(attr.Val) {
  25. node.Attr[idx].Val = "user-content-" + attr.Val
  26. }
  27. }
  28. }
  29. }
  30. func processFootnoteNode(ctx *RenderContext, node *html.Node) {
  31. for idx, attr := range node.Attr {
  32. if (attr.Key == "id" && isAnchorIDFootnote(attr.Val)) ||
  33. (attr.Key == "href" && isAnchorHrefFootnote(attr.Val)) {
  34. if footnoteContextID := ctx.RenderOptions.Metas["footnoteContextId"]; footnoteContextID != "" {
  35. node.Attr[idx].Val = attr.Val + "-" + footnoteContextID
  36. }
  37. continue
  38. }
  39. }
  40. }
  41. func processNodeA(ctx *RenderContext, node *html.Node) {
  42. for idx, attr := range node.Attr {
  43. if attr.Key == "href" {
  44. if anchorID, ok := strings.CutPrefix(attr.Val, "#"); ok {
  45. if !isAnchorIDUserContent(attr.Val) {
  46. node.Attr[idx].Val = "#user-content-" + anchorID
  47. }
  48. } else {
  49. node.Attr[idx].Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeDefault)
  50. }
  51. }
  52. }
  53. }
  54. func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
  55. next = img.NextSibling
  56. attrSrc, hasLazy := "", false
  57. for i, imgAttr := range img.Attr {
  58. hasLazy = hasLazy || imgAttr.Key == "loading" && imgAttr.Val == "lazy"
  59. if imgAttr.Key != "src" {
  60. attrSrc = imgAttr.Val
  61. continue
  62. }
  63. imgSrcOrigin := imgAttr.Val
  64. isLinkable := imgSrcOrigin != "" && !strings.HasPrefix(imgSrcOrigin, "data:")
  65. // By default, the "<img>" tag should also be clickable,
  66. // because frontend uses `<img>` to paste the re-scaled image into the Markdown,
  67. // so it must match the default Markdown image behavior.
  68. cnt := 0
  69. for p := img.Parent; isLinkable && p != nil && cnt < 2; p = p.Parent {
  70. if hasParentAnchor := p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
  71. isLinkable = false
  72. break
  73. }
  74. cnt++
  75. }
  76. if isLinkable {
  77. wrapper := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
  78. {Key: "href", Val: ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeDefault)},
  79. {Key: "target", Val: "_blank"},
  80. }}
  81. parent := img.Parent
  82. imgNext := img.NextSibling
  83. parent.RemoveChild(img)
  84. parent.InsertBefore(wrapper, imgNext)
  85. wrapper.AppendChild(img)
  86. }
  87. imgAttr.Val = ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeMedia)
  88. imgAttr.Val = camoHandleLink(imgAttr.Val)
  89. img.Attr[i] = imgAttr
  90. }
  91. if !RenderBehaviorForTesting.DisableAdditionalAttributes && !hasLazy && !strings.HasPrefix(attrSrc, "data:") {
  92. img.Attr = append(img.Attr, html.Attribute{Key: "loading", Val: "lazy"})
  93. }
  94. return next
  95. }
  96. func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
  97. next = node.NextSibling
  98. for i, attr := range node.Attr {
  99. if attr.Key != "src" {
  100. continue
  101. }
  102. if IsNonEmptyRelativePath(attr.Val) {
  103. attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
  104. }
  105. attr.Val = camoHandleLink(attr.Val)
  106. node.Attr[i] = attr
  107. }
  108. return next
  109. }