You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

html_codepreview.go 3.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package markup
  4. import (
  5. "html/template"
  6. "net/url"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "code.gitea.io/gitea/modules/httplib"
  11. "code.gitea.io/gitea/modules/log"
  12. "golang.org/x/net/html"
  13. )
  14. // codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
  15. var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
  16. type RenderCodePreviewOptions struct {
  17. FullURL string
  18. OwnerName string
  19. RepoName string
  20. CommitID string
  21. FilePath string
  22. LineStart, LineStop int
  23. }
  24. func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) {
  25. m := codePreviewPattern.FindStringSubmatchIndex(node.Data)
  26. if m == nil {
  27. return 0, 0, "", nil
  28. }
  29. opts := RenderCodePreviewOptions{
  30. FullURL: node.Data[m[0]:m[1]],
  31. OwnerName: node.Data[m[2]:m[3]],
  32. RepoName: node.Data[m[4]:m[5]],
  33. CommitID: node.Data[m[6]:m[7]],
  34. FilePath: node.Data[m[8]:m[9]],
  35. }
  36. if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) {
  37. return 0, 0, "", nil
  38. }
  39. u, err := url.Parse(opts.FilePath)
  40. if err != nil {
  41. return 0, 0, "", err
  42. }
  43. opts.FilePath = strings.TrimPrefix(u.Path, "/")
  44. lineStartStr, lineStopStr, _ := strings.Cut(node.Data[m[10]:m[11]], "-")
  45. lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
  46. lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
  47. opts.LineStart, opts.LineStop = lineStart, lineStop
  48. h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
  49. return m[0], m[1], h, err
  50. }
  51. func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
  52. nodeStop := node.NextSibling
  53. for node != nodeStop {
  54. if node.Type != html.TextNode {
  55. node = node.NextSibling
  56. continue
  57. }
  58. urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node)
  59. if err != nil || h == "" {
  60. if err != nil {
  61. log.Error("Unable to render code preview: %v", err)
  62. }
  63. node = node.NextSibling
  64. continue
  65. }
  66. next := node.NextSibling
  67. textBefore := node.Data[:urlPosStart]
  68. textAfter := node.Data[urlPosEnd:]
  69. // "textBefore" could be empty if there is only a URL in the text node, then an empty node (p, or li) will be left here.
  70. // However, the empty node can't be simply removed, because:
  71. // 1. the following processors will still try to access it (need to double-check undefined behaviors)
  72. // 2. the new node is inserted as "<p>{TextBefore}<div NewNode/>{TextAfter}</p>" (the parent could also be "li")
  73. // then it is resolved as: "<p>{TextBefore}</p><div NewNode/><p>{TextAfter}</p>",
  74. // so unless it could correctly replace the parent "p/li" node, it is very difficult to eliminate the "TextBefore" empty node.
  75. node.Data = textBefore
  76. node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next)
  77. if textAfter != "" {
  78. node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: textAfter}, next)
  79. }
  80. node = next
  81. }
  82. }