mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-11-04 08:21:11 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			272 lines
		
	
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			272 lines
		
	
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright The Forgejo Authors.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package markup
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"html/template"
 | 
						|
	"regexp"
 | 
						|
	"slices"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"code.gitea.io/gitea/modules/charset"
 | 
						|
	"code.gitea.io/gitea/modules/log"
 | 
						|
	"code.gitea.io/gitea/modules/setting"
 | 
						|
	"code.gitea.io/gitea/modules/translation"
 | 
						|
 | 
						|
	"golang.org/x/net/html"
 | 
						|
	"golang.org/x/net/html/atom"
 | 
						|
)
 | 
						|
 | 
						|
// filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2"
 | 
						|
var filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`)
 | 
						|
 | 
						|
type FilePreview struct {
 | 
						|
	fileContent []template.HTML
 | 
						|
	subTitle    template.HTML
 | 
						|
	lineOffset  int
 | 
						|
	urlFull     string
 | 
						|
	filePath    string
 | 
						|
	start       int
 | 
						|
	end         int
 | 
						|
}
 | 
						|
 | 
						|
func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Locale) *FilePreview {
 | 
						|
	preview := &FilePreview{}
 | 
						|
 | 
						|
	m := filePreviewPattern.FindStringSubmatchIndex(node.Data)
 | 
						|
	if m == nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Ensure that every group has a match
 | 
						|
	if slices.Contains(m, -1) {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	preview.urlFull = node.Data[m[0]:m[1]]
 | 
						|
 | 
						|
	// Ensure that we only use links to local repositories
 | 
						|
	if !strings.HasPrefix(preview.urlFull, setting.AppURL+setting.AppSubURL) {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	projPath := strings.TrimSuffix(node.Data[m[2]:m[3]], "/")
 | 
						|
 | 
						|
	commitSha := node.Data[m[4]:m[5]]
 | 
						|
	preview.filePath = node.Data[m[6]:m[7]]
 | 
						|
	hash := node.Data[m[8]:m[9]]
 | 
						|
 | 
						|
	preview.start = m[0]
 | 
						|
	preview.end = m[1]
 | 
						|
 | 
						|
	// If url ends in '.', it's very likely that it is not part of the
 | 
						|
	// actual url but used to finish a sentence.
 | 
						|
	if strings.HasSuffix(preview.urlFull, ".") {
 | 
						|
		preview.end--
 | 
						|
		preview.urlFull = preview.urlFull[:len(preview.urlFull)-1]
 | 
						|
		hash = hash[:len(hash)-1]
 | 
						|
	}
 | 
						|
 | 
						|
	projPathSegments := strings.Split(projPath, "/")
 | 
						|
	fileContent, err := DefaultProcessorHelper.GetRepoFileContent(
 | 
						|
		ctx.Ctx,
 | 
						|
		projPathSegments[len(projPathSegments)-2],
 | 
						|
		projPathSegments[len(projPathSegments)-1],
 | 
						|
		commitSha, preview.filePath,
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	lineSpecs := strings.Split(hash, "-")
 | 
						|
	lineCount := len(fileContent)
 | 
						|
 | 
						|
	commitLinkBuffer := new(bytes.Buffer)
 | 
						|
	err = html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black"))
 | 
						|
	if err != nil {
 | 
						|
		log.Error("failed to render commitLink: %v", err)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(lineSpecs) == 1 {
 | 
						|
		line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L"))
 | 
						|
		if line < 1 || line > lineCount {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
 | 
						|
		preview.fileContent = fileContent[line-1 : line]
 | 
						|
		preview.subTitle = locale.Tr(
 | 
						|
			"markup.filepreview.line", line,
 | 
						|
			template.HTML(commitLinkBuffer.String()),
 | 
						|
		)
 | 
						|
 | 
						|
		preview.lineOffset = line - 1
 | 
						|
	} else {
 | 
						|
		startLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L"))
 | 
						|
		endLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L"))
 | 
						|
 | 
						|
		if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
 | 
						|
		preview.fileContent = fileContent[startLine-1 : endLine]
 | 
						|
		preview.subTitle = locale.Tr(
 | 
						|
			"markup.filepreview.lines", startLine, endLine,
 | 
						|
			template.HTML(commitLinkBuffer.String()),
 | 
						|
		)
 | 
						|
 | 
						|
		preview.lineOffset = startLine - 1
 | 
						|
	}
 | 
						|
 | 
						|
	return preview
 | 
						|
}
 | 
						|
 | 
						|
func (p *FilePreview) CreateHTML(locale translation.Locale) *html.Node {
 | 
						|
	table := &html.Node{
 | 
						|
		Type: html.ElementNode,
 | 
						|
		Data: atom.Table.String(),
 | 
						|
		Attr: []html.Attribute{{Key: "class", Val: "file-preview"}},
 | 
						|
	}
 | 
						|
	tbody := &html.Node{
 | 
						|
		Type: html.ElementNode,
 | 
						|
		Data: atom.Tbody.String(),
 | 
						|
	}
 | 
						|
 | 
						|
	status := &charset.EscapeStatus{}
 | 
						|
	statuses := make([]*charset.EscapeStatus, len(p.fileContent))
 | 
						|
	for i, line := range p.fileContent {
 | 
						|
		statuses[i], p.fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext)
 | 
						|
		status = status.Or(statuses[i])
 | 
						|
	}
 | 
						|
 | 
						|
	for idx, code := range p.fileContent {
 | 
						|
		tr := &html.Node{
 | 
						|
			Type: html.ElementNode,
 | 
						|
			Data: atom.Tr.String(),
 | 
						|
		}
 | 
						|
 | 
						|
		lineNum := strconv.Itoa(p.lineOffset + idx + 1)
 | 
						|
 | 
						|
		tdLinesnum := &html.Node{
 | 
						|
			Type: html.ElementNode,
 | 
						|
			Data: atom.Td.String(),
 | 
						|
			Attr: []html.Attribute{
 | 
						|
				{Key: "class", Val: "lines-num"},
 | 
						|
			},
 | 
						|
		}
 | 
						|
		spanLinesNum := &html.Node{
 | 
						|
			Type: html.ElementNode,
 | 
						|
			Data: atom.Span.String(),
 | 
						|
			Attr: []html.Attribute{
 | 
						|
				{Key: "data-line-number", Val: lineNum},
 | 
						|
			},
 | 
						|
		}
 | 
						|
		tdLinesnum.AppendChild(spanLinesNum)
 | 
						|
		tr.AppendChild(tdLinesnum)
 | 
						|
 | 
						|
		if status.Escaped {
 | 
						|
			tdLinesEscape := &html.Node{
 | 
						|
				Type: html.ElementNode,
 | 
						|
				Data: atom.Td.String(),
 | 
						|
				Attr: []html.Attribute{
 | 
						|
					{Key: "class", Val: "lines-escape"},
 | 
						|
				},
 | 
						|
			}
 | 
						|
 | 
						|
			if statuses[idx].Escaped {
 | 
						|
				btnTitle := ""
 | 
						|
				if statuses[idx].HasInvisible {
 | 
						|
					btnTitle += locale.TrString("repo.invisible_runes_line") + " "
 | 
						|
				}
 | 
						|
				if statuses[idx].HasAmbiguous {
 | 
						|
					btnTitle += locale.TrString("repo.ambiguous_runes_line")
 | 
						|
				}
 | 
						|
 | 
						|
				escapeBtn := &html.Node{
 | 
						|
					Type: html.ElementNode,
 | 
						|
					Data: atom.Button.String(),
 | 
						|
					Attr: []html.Attribute{
 | 
						|
						{Key: "class", Val: "toggle-escape-button btn interact-bg"},
 | 
						|
						{Key: "title", Val: btnTitle},
 | 
						|
					},
 | 
						|
				}
 | 
						|
				tdLinesEscape.AppendChild(escapeBtn)
 | 
						|
			}
 | 
						|
 | 
						|
			tr.AppendChild(tdLinesEscape)
 | 
						|
		}
 | 
						|
 | 
						|
		tdCode := &html.Node{
 | 
						|
			Type: html.ElementNode,
 | 
						|
			Data: atom.Td.String(),
 | 
						|
			Attr: []html.Attribute{
 | 
						|
				{Key: "class", Val: "lines-code chroma"},
 | 
						|
			},
 | 
						|
		}
 | 
						|
		codeInner := &html.Node{
 | 
						|
			Type: html.ElementNode,
 | 
						|
			Data: atom.Code.String(),
 | 
						|
			Attr: []html.Attribute{{Key: "class", Val: "code-inner"}},
 | 
						|
		}
 | 
						|
		codeText := &html.Node{
 | 
						|
			Type: html.RawNode,
 | 
						|
			Data: string(code),
 | 
						|
		}
 | 
						|
		codeInner.AppendChild(codeText)
 | 
						|
		tdCode.AppendChild(codeInner)
 | 
						|
		tr.AppendChild(tdCode)
 | 
						|
 | 
						|
		tbody.AppendChild(tr)
 | 
						|
	}
 | 
						|
 | 
						|
	table.AppendChild(tbody)
 | 
						|
 | 
						|
	twrapper := &html.Node{
 | 
						|
		Type: html.ElementNode,
 | 
						|
		Data: atom.Div.String(),
 | 
						|
		Attr: []html.Attribute{{Key: "class", Val: "ui table"}},
 | 
						|
	}
 | 
						|
	twrapper.AppendChild(table)
 | 
						|
 | 
						|
	header := &html.Node{
 | 
						|
		Type: html.ElementNode,
 | 
						|
		Data: atom.Div.String(),
 | 
						|
		Attr: []html.Attribute{{Key: "class", Val: "header"}},
 | 
						|
	}
 | 
						|
	afilepath := &html.Node{
 | 
						|
		Type: html.ElementNode,
 | 
						|
		Data: atom.A.String(),
 | 
						|
		Attr: []html.Attribute{
 | 
						|
			{Key: "href", Val: p.urlFull},
 | 
						|
			{Key: "class", Val: "muted"},
 | 
						|
		},
 | 
						|
	}
 | 
						|
	afilepath.AppendChild(&html.Node{
 | 
						|
		Type: html.TextNode,
 | 
						|
		Data: p.filePath,
 | 
						|
	})
 | 
						|
	header.AppendChild(afilepath)
 | 
						|
 | 
						|
	psubtitle := &html.Node{
 | 
						|
		Type: html.ElementNode,
 | 
						|
		Data: atom.Span.String(),
 | 
						|
		Attr: []html.Attribute{{Key: "class", Val: "text small grey"}},
 | 
						|
	}
 | 
						|
	psubtitle.AppendChild(&html.Node{
 | 
						|
		Type: html.RawNode,
 | 
						|
		Data: string(p.subTitle),
 | 
						|
	})
 | 
						|
	header.AppendChild(psubtitle)
 | 
						|
 | 
						|
	node := &html.Node{
 | 
						|
		Type: html.ElementNode,
 | 
						|
		Data: atom.Div.String(),
 | 
						|
		Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}},
 | 
						|
	}
 | 
						|
	node.AppendChild(header)
 | 
						|
	node.AppendChild(twrapper)
 | 
						|
 | 
						|
	return node
 | 
						|
}
 |