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