From d789d33229b3998bb33f1505d122504c8039f23d Mon Sep 17 00:00:00 2001
From: Mai-Lapyst
Date: Sat, 16 Mar 2024 08:09:49 +0100
Subject: [PATCH] Split filePreviewPatternProcessor into a new type FilePreview
and some functions to make code more maintainable
---
modules/markup/file_preview.go | 269 +++++++++++++++++++++++++++++++++
modules/markup/html.go | 245 +-----------------------------
2 files changed, 276 insertions(+), 238 deletions(-)
create mode 100644 modules/markup/file_preview.go
diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go
new file mode 100644
index 0000000000..646bf83630
--- /dev/null
+++ b/modules/markup/file_preview.go
@@ -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
+}
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 1e83dad701..2e38c05f58 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -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: "
",
}, nextSibling)
- node.Parent.InsertBefore(preview, nextSibling)
+ node.Parent.InsertBefore(preview_node, nextSibling)
node.Parent.InsertBefore(&html.Node{
Type: html.RawNode,
Data: "" + after,