diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go
index a37007d75b..993df717e1 100644
--- a/modules/markup/file_preview.go
+++ b/modules/markup/file_preview.go
@@ -35,32 +35,44 @@ type FilePreview struct {
isTruncated bool
}
-func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Locale) *FilePreview {
+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{}
- m := filePreviewPattern.FindStringSubmatchIndex(node.Data)
- if m == nil {
- return nil
- }
-
- // Ensure that every group has a match
- if slices.Contains(m, -1) {
- return nil
- }
-
urlFull := node.Data[m[0]:m[1]]
// Ensure that we only use links to local repositories
- if !strings.HasPrefix(urlFull, setting.AppURL+setting.AppSubURL) {
+ if !strings.HasPrefix(urlFull, setting.AppURL) {
return nil
}
- projPath := strings.TrimSuffix(node.Data[m[2]:m[3]], "/")
+ 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]]
@@ -70,6 +82,10 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca
preview.end = m[1]
projPathSegments := strings.Split(projPath, "/")
+ if len(projPathSegments) != 2 {
+ return nil
+ }
+
ownerName := projPathSegments[len(projPathSegments)-2]
repoName := projPathSegments[len(projPathSegments)-1]
diff --git a/modules/markup/html.go b/modules/markup/html.go
index d19eb37013..b4fd008aa7 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -1063,38 +1063,46 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
return
}
+ locale := translation.NewLocale("en-US")
+ if ctx.Ctx != nil {
+ ctxLocale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale)
+ if ok {
+ locale = ctxLocale
+ }
+ }
+
next := node.NextSibling
for node != nil && node != next {
- locale := translation.NewLocale("en-US")
- if ctx.Ctx != nil {
- ctxLocale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale)
- if ok {
- locale = ctxLocale
+ previews := NewFilePreviews(ctx, node, locale)
+ if previews == nil {
+ node = node.NextSibling
+ continue
+ }
+
+ offset := 0
+ for _, preview := range previews {
+ previewNode := preview.CreateHTML(locale)
+
+ // Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div
+ before := node.Data[:(preview.start - offset)]
+ after := node.Data[(preview.end - offset):]
+ afterPrefix := "
"
+ offset = preview.end - len(afterPrefix)
+ node.Data = before
+ nextSibling := node.NextSibling
+ node.Parent.InsertBefore(&html.Node{
+ Type: html.RawNode,
+ Data: "
",
+ }, nextSibling)
+ node.Parent.InsertBefore(previewNode, nextSibling)
+ afterNode := &html.Node{
+ Type: html.RawNode,
+ Data: afterPrefix + after,
}
+ node.Parent.InsertBefore(afterNode, nextSibling)
+ node = afterNode
}
- preview := NewFilePreview(ctx, node, locale)
- if preview == nil {
- return
- }
-
- previewNode := preview.CreateHTML(locale)
-
- // Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div
- 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(previewNode, nextSibling)
- node.Parent.InsertBefore(&html.Node{
- Type: html.RawNode,
- Data: "" + after,
- }, nextSibling)
-
node = node.NextSibling
}
}
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 2c5ed8c7b8..a1a99c1a7f 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
@@ -680,9 +681,9 @@ func TestIssue18471(t *testing.T) {
}
func TestRender_FilePreview(t *testing.T) {
- setting.StaticRootPath = "../../"
- setting.Names = []string{"english"}
- setting.Langs = []string{"en-US"}
+ defer test.MockVariableValue(&setting.StaticRootPath, "../../")()
+ defer test.MockVariableValue(&setting.Names, []string{"english"})()
+ defer test.MockVariableValue(&setting.Langs, []string{"en-US"})()
translation.InitLocales(context.Background())
setting.AppURL = markup.TestAppURL
@@ -705,7 +706,7 @@ func TestRender_FilePreview(t *testing.T) {
sha := "190d9492934af498c3f669d6a2431dc5459e5b20"
commitFilePreview := util.URLJoin(markup.TestRepoURL, "src", "commit", sha, "path", "to", "file.go") + "#L2-L3"
- test := func(input, expected string, metas map[string]string) {
+ testRender := func(input, expected string, metas map[string]string) {
buffer, err := markup.RenderString(&markup.RenderContext{
Ctx: git.DefaultContext,
RelativePath: ".md",
@@ -715,69 +716,287 @@ func TestRender_FilePreview(t *testing.T) {
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
- test(
- commitFilePreview,
- `
`+
- ``+
- ``+
- `
`+
- `
`+
- ``+
- ``+
- ` | `+
- `B`+"\n"+` | `+
- `
`+
- ``+
- ` | `+
- `C`+"\n"+` | `+
- `
`+
- ``+
- `
`+
- `
`+
- `
`+
- ``,
- localMetas,
- )
+ t.Run("single", func(t *testing.T) {
+ testRender(
+ commitFilePreview,
+ ``+
+ ``+
+ ``+
+ `
`+
+ `
`+
+ ``+
+ ``+
+ ` | `+
+ `B`+"\n"+` | `+
+ `
`+
+ ``+
+ ` | `+
+ `C`+"\n"+` | `+
+ `
`+
+ ``+
+ `
`+
+ `
`+
+ `
`+
+ ``,
+ localMetas,
+ )
+ })
- test(
- commitFilePreview,
- ``+
- ``+
- ``+
- `
`+
- `
`+
- ``+
- ``+
- ` | `+
- `B`+"\n"+` | `+
- `
`+
- ``+
- ` | `+
- `C`+"\n"+` | `+
- `
`+
- ``+
- `
`+
- `
`+
- `
`+
- ``,
- map[string]string{
- "user": "gogits",
- "repo": "gogs2",
- },
- )
+ t.Run("cross-repo", func(t *testing.T) {
+ testRender(
+ commitFilePreview,
+ ``+
+ ``+
+ ``+
+ `
`+
+ `
`+
+ ``+
+ ``+
+ ` | `+
+ `B`+"\n"+` | `+
+ `
`+
+ ``+
+ ` | `+
+ `C`+"\n"+` | `+
+ `
`+
+ ``+
+ `
`+
+ `
`+
+ `
`+
+ ``,
+ map[string]string{
+ "user": "gogits",
+ "repo": "gogs2",
+ },
+ )
+ })
+
+ t.Run("AppSubURL", func(t *testing.T) {
+ urlWithSub := util.URLJoin(markup.TestAppURL, "sub", markup.TestOrgRepo, "src", "commit", sha, "path", "to", "file.go") + "#L2-L3"
+
+ testRender(
+ urlWithSub,
+ `190d949293/path/to/file.go (L2-L3)
`,
+ localMetas,
+ )
+
+ defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL+"sub/")()
+ defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
+
+ testRender(
+ urlWithSub,
+ ``+
+ ``+
+ ``+
+ `
`+
+ `
`+
+ ``+
+ ``+
+ ` | `+
+ `B`+"\n"+` | `+
+ `
`+
+ ``+
+ ` | `+
+ `C`+"\n"+` | `+
+ `
`+
+ ``+
+ `
`+
+ `
`+
+ `
`+
+ ``,
+ localMetas,
+ )
+
+ testRender(
+ "first without sub "+commitFilePreview+" second "+urlWithSub,
+ `first without sub 190d949293/path/to/file.go (L2-L3)
second
`+
+ ``+
+ ``+
+ `
`+
+ `
`+
+ ``+
+ ``+
+ ` | `+
+ `B`+"\n"+` | `+
+ `
`+
+ ``+
+ ` | `+
+ `C`+"\n"+` | `+
+ `
`+
+ ``+
+ `
`+
+ `
`+
+ `
`+
+ ``,
+ localMetas,
+ )
+ })
+
+ t.Run("multiples", func(t *testing.T) {
+ testRender(
+ "first "+commitFilePreview+" second "+commitFilePreview,
+ `first
`+
+ ``+
+ ``+
+ `
`+
+ `
`+
+ ``+
+ ``+
+ ` | `+
+ `B`+"\n"+` | `+
+ `
`+
+ ``+
+ ` | `+
+ `C`+"\n"+` | `+
+ `
`+
+ ``+
+ `
`+
+ `
`+
+ `
`+
+ ` second
`+
+ ``+
+ ``+
+ `
`+
+ `
`+
+ ``+
+ ``+
+ ` | `+
+ `B`+"\n"+` | `+
+ `
`+
+ ``+
+ ` | `+
+ `C`+"\n"+` | `+
+ `
`+
+ ``+
+ `
`+
+ `
`+
+ `
`+
+ ``,
+ localMetas,
+ )
+
+ testRender(
+ "first "+commitFilePreview+" second "+commitFilePreview+" third "+commitFilePreview,
+ `first
`+
+ ``+
+ ``+
+ `
`+
+ `
`+
+ ``+
+ ``+
+ ` | `+
+ `B`+"\n"+` | `+
+ `
`+
+ ``+
+ ` | `+
+ `C`+"\n"+` | `+
+ `
`+
+ ``+
+ `
`+
+ `
`+
+ `
`+
+ ` second
`+
+ ``+
+ ``+
+ `
`+
+ `
`+
+ ``+
+ ``+
+ ` | `+
+ `B`+"\n"+` | `+
+ `
`+
+ ``+
+ ` | `+
+ `C`+"\n"+` | `+
+ `
`+
+ ``+
+ `
`+
+ `
`+
+ `
`+
+ ` third
`+
+ ``+
+ ``+
+ `
`+
+ `
`+
+ ``+
+ ``+
+ ` | `+
+ `B`+"\n"+` | `+
+ `
`+
+ ``+
+ ` | `+
+ `C`+"\n"+` | `+
+ `
`+
+ ``+
+ `
`+
+ `
`+
+ `
`+
+ ``,
+ localMetas,
+ )
+ })
}