Split filePreviewPatternProcessor into a new type FilePreview and some functions to make code more maintainable

This commit is contained in:
Mai-Lapyst 2024-03-16 08:09:49 +01:00
parent 562e5cdf32
commit d789d33229
No known key found for this signature in database
GPG key ID: F88D929C09E239F8
2 changed files with 276 additions and 238 deletions

View file

@ -0,0 +1,269 @@
package markup
import (
"bytes"
"html/template"
"regexp"
"slices"
"strconv"
"strings"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
var (
// filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2"
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)
html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black"))
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: "id", Val: "L" + lineNum},
{Key: "class", Val: "lines-num"},
},
}
spanLinesNum := &html.Node{
Type: html.ElementNode,
Data: atom.Span.String(),
Attr: []html.Attribute{
{Key: "id", Val: "L" + lineNum},
{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: "rel", Val: "L" + lineNum},
{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)
preview_node := &html.Node{
Type: html.ElementNode,
Data: atom.Div.String(),
Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}},
}
preview_node.AppendChild(header)
preview_node.AppendChild(twrapper)
return preview_node
}

View file

@ -5,19 +5,15 @@ package markup
import (
"bytes"
"html/template"
"io"
"net/url"
"path"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"sync"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
@ -65,9 +61,6 @@ var (
validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`)
// filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2"
filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`)
// While this email regex is definitely not perfect and I'm sure you can come up
// with edge cases, it is still accepted by the CommonMark specification, as
// well as the HTML5 spec:
@ -1072,252 +1065,28 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
next := node.NextSibling
for node != nil && node != next {
m := filePreviewPattern.FindStringSubmatchIndex(node.Data)
if m == nil {
return
}
// Ensure that every group has a match
if slices.Contains(m, -1) {
return
}
urlFull := node.Data[m[0]:m[1]]
// Ensure that we only use links to local repositories
if !strings.HasPrefix(urlFull, setting.AppURL+setting.AppSubURL) {
return
}
projPath := strings.TrimSuffix(node.Data[m[2]:m[3]], "/")
commitSha := node.Data[m[4]:m[5]]
filePath := node.Data[m[6]:m[7]]
hash := node.Data[m[8]:m[9]]
start := m[0]
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(urlFull, ".") {
end--
urlFull = urlFull[:len(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, filePath,
)
if err != nil {
return
}
lineSpecs := strings.Split(hash, "-")
lineCount := len(fileContent)
commitLinkBuffer := new(bytes.Buffer)
html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black"))
var subTitle template.HTML
var lineOffset int
locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale)
if !ok {
locale = translation.NewLocale("en-US")
}
if len(lineSpecs) == 1 {
line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L"))
if line < 1 || line > lineCount {
return
}
fileContent = fileContent[line-1 : line]
subTitle = locale.Tr(
"markup.filepreview.line", line,
template.HTML(commitLinkBuffer.String()),
)
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
}
fileContent = fileContent[startLine-1 : endLine]
subTitle = locale.Tr(
"markup.filepreview.lines", startLine, endLine,
template.HTML(commitLinkBuffer.String()),
)
lineOffset = startLine - 1
preview := NewFilePreview(ctx, node, locale)
if preview == nil {
return
}
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(fileContent))
for i, line := range fileContent {
statuses[i], fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext)
status = status.Or(statuses[i])
}
for idx, code := range fileContent {
tr := &html.Node{
Type: html.ElementNode,
Data: atom.Tr.String(),
}
lineNum := strconv.Itoa(lineOffset + idx + 1)
tdLinesnum := &html.Node{
Type: html.ElementNode,
Data: atom.Td.String(),
Attr: []html.Attribute{
{Key: "id", Val: "L" + lineNum},
{Key: "class", Val: "lines-num"},
},
}
spanLinesNum := &html.Node{
Type: html.ElementNode,
Data: atom.Span.String(),
Attr: []html.Attribute{
{Key: "id", Val: "L" + lineNum},
{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: "rel", Val: "L" + lineNum},
{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: urlFull},
{Key: "class", Val: "muted"},
},
}
afilepath.AppendChild(&html.Node{
Type: html.TextNode,
Data: 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(subTitle),
})
header.AppendChild(psubtitle)
preview := &html.Node{
Type: html.ElementNode,
Data: atom.Div.String(),
Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}},
}
preview.AppendChild(header)
preview.AppendChild(twrapper)
preview_node := preview.CreateHtml(locale)
// Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div
before := node.Data[:start]
after := node.Data[end:]
before := node.Data[:preview.start]
after := node.Data[preview.end:]
node.Data = before
nextSibling := node.NextSibling
node.Parent.InsertBefore(&html.Node{
Type: html.RawNode,
Data: "</p>",
}, nextSibling)
node.Parent.InsertBefore(preview, nextSibling)
node.Parent.InsertBefore(preview_node, nextSibling)
node.Parent.InsertBefore(&html.Node{
Type: html.RawNode,
Data: "<p>" + after,