375 lines · 8.8 KB · Go
RawDownload
// Copyright The Forgejo Authors.// SPDX-License-Identifier: MIT package markup import (	"bufio"	"bytes"	"html/template"	"io"	"net/url"	"regexp"	"slices"	"strconv"	"strings" 	"forgejo.org/modules/charset"	"forgejo.org/modules/highlight"	"forgejo.org/modules/log"	"forgejo.org/modules/setting"	"forgejo.org/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	title       template.HTML	subTitle    template.HTML	lineOffset  int	start       int	end         int	isTruncated bool} func NewFilePreviews(ctx *RenderContext, node *html.Node, locale translation.Locale) []*FilePreview {	if setting.FilePreviewMaxLines == 0 {		// Feature is disabled		return nil	} 	mAll := filePreviewPattern.FindAllStringSubmatchIndex(node.Data, -1)	if mAll == nil {		return nil	} 	result := make([]*FilePreview, 0) 	for _, m := range mAll {		if slices.Contains(m, -1) {			continue		} 		preview := newFilePreview(ctx, node, locale, m)		if preview != nil {			result = append(result, preview)		}	} 	return result} func newFilePreview(ctx *RenderContext, node *html.Node, locale translation.Locale, m []int) *FilePreview {	preview := &FilePreview{} 	urlFull := node.Data[m[0]:m[1]] 	// Ensure that we only use links to local repositories	if !strings.HasPrefix(urlFull, setting.AppURL) {		return nil	} 	projPath := strings.TrimPrefix(strings.TrimSuffix(node.Data[m[0]:m[3]], "/"), setting.AppURL) 	commitSha := node.Data[m[4]:m[5]]	filePath := node.Data[m[6]:m[7]]	urlFullSource := urlFull	if strings.HasSuffix(filePath, "?display=source") {		filePath = strings.TrimSuffix(filePath, "?display=source")	} else if Type(filePath) != "" {		urlFullSource = node.Data[m[0]:m[6]] + filePath + "?display=source#" + node.Data[m[8]:m[1]]	}	filePath, err := url.QueryUnescape(filePath)	if err != nil {		return nil	}	hash := node.Data[m[8]:m[9]] 	preview.start = m[0]	preview.end = m[1] 	projPathSegments := strings.Split(projPath, "/")	if len(projPathSegments) != 2 {		return nil	} 	ownerName := projPathSegments[len(projPathSegments)-2]	repoName := projPathSegments[len(projPathSegments)-1] 	var language string	fileBlob, err := DefaultProcessorHelper.GetRepoFileBlob(		ctx.Ctx,		ownerName,		repoName,		commitSha, filePath,		&language,	)	if err != nil {		return nil	} 	titleBuffer := new(bytes.Buffer) 	isExternRef := ownerName != ctx.Metas["user"] || repoName != ctx.Metas["repo"]	if isExternRef {		err = html.Render(titleBuffer, createLink(node.Data[m[0]:m[3]], ownerName+"/"+repoName, ""))		if err != nil {			log.Error("failed to render repoLink: %v", err)		}		titleBuffer.WriteString(" &ndash; ")	} 	err = html.Render(titleBuffer, createLink(urlFullSource, filePath, "muted"))	if err != nil {		log.Error("failed to render filepathLink: %v", err)	} 	preview.title = template.HTML(titleBuffer.String()) 	lineSpecs := strings.Split(hash, "-") 	commitLinkBuffer := new(bytes.Buffer)	commitLinkText := commitSha[0:7]	if isExternRef {		commitLinkText = ownerName + "/" + repoName + "@" + commitLinkText	} 	err = html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitLinkText, "text black"))	if err != nil {		log.Error("failed to render commitLink: %v", err)	} 	var startLine, endLine int 	if len(lineSpecs) == 1 {		startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L"))		endLine = startLine		preview.subTitle = locale.Tr(			"markup.filepreview.line", startLine,			template.HTML(commitLinkBuffer.String()),		) 		preview.lineOffset = startLine - 1	} else {		startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L"))		endLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L"))		preview.subTitle = locale.Tr(			"markup.filepreview.lines", startLine, endLine,			template.HTML(commitLinkBuffer.String()),		) 		preview.lineOffset = startLine - 1	} 	lineCount := endLine - (startLine - 1)	if startLine < 1 || endLine < 1 || lineCount < 1 {		return nil	} 	if setting.FilePreviewMaxLines > 0 && lineCount > setting.FilePreviewMaxLines {		preview.isTruncated = true		lineCount = setting.FilePreviewMaxLines	} 	dataRc, err := fileBlob.DataAsync()	if err != nil {		return nil	}	defer dataRc.Close() 	reader := bufio.NewReader(dataRc) 	// skip all lines until we find our startLine	for i := 1; i < startLine; i++ {		_, err := reader.ReadBytes('\n')		if err != nil {			return nil		}	} 	// capture the lines we're interested in	lineBuffer := new(bytes.Buffer)	for i := 0; i < lineCount; i++ {		buf, err := reader.ReadBytes('\n')		if err == nil || err == io.EOF {			lineBuffer.Write(buf)		}		if err != nil {			break		}	} 	// highlight the file...	fileContent, _, err := highlight.File(fileBlob.Name(), language, lineBuffer.Bytes())	if err != nil {		log.Error("highlight.File failed, fallback to plain text: %v", err)		fileContent = highlight.PlainText(lineBuffer.Bytes())	}	preview.fileContent = fileContent 	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"}},	} 	ptitle := &html.Node{		Type: html.ElementNode,		Data: atom.Div.String(),	}	ptitle.AppendChild(&html.Node{		Type: html.RawNode,		Data: string(p.title),	})	header.AppendChild(ptitle) 	psubtitle := &html.Node{		Type: html.ElementNode,		Data: atom.Span.String(),		Attr: []html.Attribute{{Key: "class", Val: "text 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) 	if p.isTruncated {		warning := &html.Node{			Type: html.ElementNode,			Data: atom.Div.String(),			Attr: []html.Attribute{{Key: "class", Val: "ui warning message tw-text-left"}},		}		warning.AppendChild(&html.Node{			Type: html.TextNode,			Data: locale.TrString("markup.filepreview.truncated"),		})		node.AppendChild(warning)	} 	node.AppendChild(twrapper) 	return node}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t