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  }