Skip to content

Commit 726ea72

Browse files
committed
fix
1 parent 1ef2eb5 commit 726ea72

16 files changed

+342
-7
lines changed

modules/indexer/code/search.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error {
7070
return nil
7171
}
7272

73-
func HighlightSearchResultCode(filename string, lineNums []int, code string) []ResultLine {
73+
func HighlightSearchResultCode(filename, language string, lineNums []int, code string) []ResultLine {
7474
// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
75-
hl, _ := highlight.Code(filename, "", code)
75+
hl, _ := highlight.Code(filename, language, code)
7676
highlightedLines := strings.Split(string(hl), "\n")
7777

7878
// The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n`
@@ -122,7 +122,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res
122122
UpdatedUnix: result.UpdatedUnix,
123123
Language: result.Language,
124124
Color: result.Color,
125-
Lines: HighlightSearchResultCode(result.Filename, lineNums, formattedLinesBuffer.String()),
125+
Lines: HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()),
126126
}, nil
127127
}
128128

modules/markup/html.go

+1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ type processor func(ctx *RenderContext, node *html.Node)
171171
var defaultProcessors = []processor{
172172
fullIssuePatternProcessor,
173173
comparePatternProcessor,
174+
codePreviewPatternProcessor,
174175
fullHashPatternProcessor,
175176
shortLinkProcessor,
176177
linkProcessor,

modules/markup/html_codepreview.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package markup
5+
6+
import (
7+
"html/template"
8+
"net/url"
9+
"regexp"
10+
"strconv"
11+
"strings"
12+
13+
"code.gitea.io/gitea/modules/httplib"
14+
15+
"golang.org/x/net/html"
16+
)
17+
18+
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
19+
var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
20+
21+
type RenderCodePreviewOptions struct {
22+
FullURL string
23+
OwnerName string
24+
RepoName string
25+
CommitID string
26+
FilePath string
27+
28+
LineStart, LineStop int
29+
}
30+
31+
func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) {
32+
m := codePreviewPattern.FindStringSubmatchIndex(node.Data)
33+
if m == nil {
34+
return 0, 0, "", nil
35+
}
36+
37+
opts := RenderCodePreviewOptions{
38+
FullURL: node.Data[m[0]:m[1]],
39+
OwnerName: node.Data[m[2]:m[3]],
40+
RepoName: node.Data[m[4]:m[5]],
41+
CommitID: node.Data[m[6]:m[7]],
42+
FilePath: node.Data[m[8]:m[9]],
43+
}
44+
if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) {
45+
return 0, 0, "", nil
46+
}
47+
u, err := url.Parse(opts.FilePath)
48+
if err != nil {
49+
return 0, 0, "", err
50+
}
51+
opts.FilePath = strings.TrimPrefix(u.Path, "/")
52+
53+
lineStartStr, lineStopStr, _ := strings.Cut(node.Data[m[10]:m[11]], "-")
54+
lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
55+
lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
56+
opts.LineStart, opts.LineStop = lineStart, lineStop
57+
h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
58+
return m[0], m[1], h, err
59+
}
60+
61+
func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
62+
for node != nil {
63+
urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node)
64+
if err != nil || h == "" {
65+
node = node.NextSibling
66+
continue
67+
}
68+
next := node.NextSibling
69+
nodeText := node.Data
70+
node.Data = nodeText[:urlPosStart]
71+
node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next)
72+
node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: nodeText[urlPosEnd:]}, next)
73+
node = next
74+
}
75+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package markup_test
5+
6+
import (
7+
"context"
8+
"html/template"
9+
"strings"
10+
"testing"
11+
12+
"code.gitea.io/gitea/modules/git"
13+
"code.gitea.io/gitea/modules/markup"
14+
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
func TestRenderCodePreview(t *testing.T) {
19+
markup.Init(&markup.ProcessorHelper{
20+
RenderRepoFileCodePreview: func(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
21+
return "<div>code preview</div>", nil
22+
},
23+
})
24+
test := func(input, expected string) {
25+
buffer, err := markup.RenderString(&markup.RenderContext{
26+
Ctx: git.DefaultContext,
27+
Type: "markdown",
28+
}, input)
29+
assert.NoError(t, err)
30+
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
31+
}
32+
test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "<p><div>code preview</div></p>")
33+
test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `<p><a href="http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20" rel="nofollow">http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20</a></p>`)
34+
}

modules/markup/renderer.go

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"errors"
1010
"fmt"
11+
"html/template"
1112
"io"
1213
"net/url"
1314
"path/filepath"
@@ -33,6 +34,8 @@ type ProcessorHelper struct {
3334
IsUsernameMentionable func(ctx context.Context, username string) bool
3435

3536
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
37+
38+
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
3639
}
3740

3841
var DefaultProcessorHelper ProcessorHelper

modules/markup/sanitizer.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ func createDefaultPolicy() *bluemonday.Policy {
5858
policy := bluemonday.UGCPolicy()
5959

6060
// For JS code copy and Mermaid loading state
61-
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
61+
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).Globally()
62+
63+
// For code preview
64+
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+$`)).Globally()
65+
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).Globally()
66+
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).Globally()
67+
policy.AllowAttrs("data-line-number").OnElements("span")
6268

6369
// For color preview
6470
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")

routers/web/repo/search.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func Search(ctx *context.Context) {
8181
// UpdatedUnix: not supported yet
8282
// Language: not supported yet
8383
// Color: not supported yet
84-
Lines: code_indexer.HighlightSearchResultCode(r.Filename, r.LineNumbers, strings.Join(r.LineCodes, "\n")),
84+
Lines: code_indexer.HighlightSearchResultCode(r.Filename, "", r.LineNumbers, strings.Join(r.LineCodes, "\n")),
8585
})
8686
}
8787
}

services/contexttest/context_tests.go

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
6363
base.Locale = &translation.MockLocale{}
6464

6565
ctx := context.NewWebContext(base, opt.Render, nil)
66+
ctx.AppendContextValue(context.WebContextKey, ctx)
6667
ctx.PageData = map[string]any{}
6768
ctx.Data["PageStartTime"] = time.Now()
6869
chiCtx := chi.NewRouteContext()

services/markup/main_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ import (
1111

1212
func TestMain(m *testing.M) {
1313
unittest.MainTest(m, &unittest.TestOptions{
14-
FixtureFiles: []string{"user.yml"},
14+
FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"},
1515
})
1616
}

services/markup/processorhelper.go

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
func ProcessorHelper() *markup.ProcessorHelper {
1515
return &markup.ProcessorHelper{
1616
ElementDir: "auto", // set dir="auto" for necessary (eg: <p>, <h?>, etc) tags
17+
18+
RenderRepoFileCodePreview: renderRepoFileCodePreview,
1719
IsUsernameMentionable: func(ctx context.Context, username string) bool {
1820
mentionedUser, err := user.GetUserByName(ctx, username)
1921
if err != nil {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package markup
5+
6+
import (
7+
"bufio"
8+
"context"
9+
"fmt"
10+
"html/template"
11+
"strings"
12+
13+
"code.gitea.io/gitea/models/perm/access"
14+
"code.gitea.io/gitea/models/repo"
15+
"code.gitea.io/gitea/models/unit"
16+
"code.gitea.io/gitea/modules/gitrepo"
17+
"code.gitea.io/gitea/modules/indexer/code"
18+
"code.gitea.io/gitea/modules/markup"
19+
"code.gitea.io/gitea/modules/setting"
20+
gitea_context "code.gitea.io/gitea/services/context"
21+
"code.gitea.io/gitea/services/repository/files"
22+
)
23+
24+
func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
25+
opts.LineStop = max(opts.LineStop, 1)
26+
lineCount := opts.LineStop - opts.LineStart + 1
27+
if lineCount <= 0 || lineCount > 140 /* GitHub at most show 140 lines */ {
28+
lineCount = 10
29+
opts.LineStop = opts.LineStart + lineCount
30+
}
31+
32+
dbRepo, err := repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName)
33+
if err != nil {
34+
return "", err
35+
}
36+
37+
webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
38+
if !ok {
39+
return "", fmt.Errorf("context is not a web context")
40+
}
41+
doer := webCtx.Doer
42+
43+
perms, err := access.GetUserRepoPermission(ctx, dbRepo, doer)
44+
if err != nil {
45+
return "", err
46+
}
47+
if !perms.CanRead(unit.TypeCode) {
48+
return "", fmt.Errorf("no permission")
49+
}
50+
51+
gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)
52+
if err != nil {
53+
return "", err
54+
}
55+
defer gitRepo.Close()
56+
57+
commit, err := gitRepo.GetCommit(opts.CommitID)
58+
if err != nil {
59+
return "", err
60+
}
61+
62+
language, _ := files.TryGetContentLanguage(gitRepo, opts.CommitID, opts.FilePath)
63+
blob, err := commit.GetBlobByPath(opts.FilePath)
64+
if err != nil {
65+
return "", err
66+
}
67+
68+
if blob.Size() > setting.UI.MaxDisplayFileSize {
69+
return "", fmt.Errorf("file is too large")
70+
}
71+
72+
dataRc, err := blob.DataAsync()
73+
if err != nil {
74+
return "", err
75+
}
76+
defer dataRc.Close()
77+
78+
reader := bufio.NewReader(dataRc)
79+
80+
for i := 1; i < opts.LineStart; i++ {
81+
if _, err = reader.ReadBytes('\n'); err != nil {
82+
return "", err
83+
}
84+
}
85+
86+
lineNums := make([]int, 0, lineCount)
87+
lineCodes := make([]string, 0, lineCount)
88+
for i := opts.LineStart; i <= opts.LineStop; i++ {
89+
if line, err := reader.ReadString('\n'); err != nil {
90+
break
91+
} else {
92+
lineNums = append(lineNums, i)
93+
lineCodes = append(lineCodes, line)
94+
}
95+
}
96+
highlightLines := code.HighlightSearchResultCode(opts.FilePath, language, lineNums, strings.Join(lineCodes, ""))
97+
return webCtx.RenderToHTML("base/markup_codepreview", map[string]any{
98+
"FullURL": opts.FullURL,
99+
"FilePath": opts.FilePath,
100+
"LineStart": opts.LineStart,
101+
"LineStop": opts.LineStop,
102+
"RepoLink": dbRepo.Link(),
103+
"CommitID": opts.CommitID,
104+
"HighlightLines": highlightLines,
105+
})
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package markup
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/unittest"
10+
"code.gitea.io/gitea/modules/markup"
11+
"code.gitea.io/gitea/modules/templates"
12+
"code.gitea.io/gitea/services/contexttest"
13+
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestProcessorHelperCodePreview(t *testing.T) {
18+
assert.NoError(t, unittest.PrepareTestDatabase())
19+
20+
ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
21+
htm, err := renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
22+
FullURL: "http://full",
23+
OwnerName: "user2",
24+
RepoName: "repo1",
25+
CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
26+
FilePath: "/README.md",
27+
LineStart: 1,
28+
LineStop: 10,
29+
})
30+
assert.NoError(t, err)
31+
assert.Equal(t, `<div class="code-preview-container">
32+
<div class="code-preview-header">
33+
<a href="http://full" class="muted" rel="nofollow">/README.md</a>
34+
Lines 1 to 10 in
35+
<a href="/user2/repo1/src/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d" rel="nofollow">65f1bf27bc</a>
36+
</div>
37+
<table>
38+
<tbody><tr>
39+
<td class="lines-num"><span data-line-number="1"></span></td>
40+
<td class="lines-code chroma"><span class="gh"># repo1</td>
41+
</tr><tr>
42+
<td class="lines-num"><span data-line-number="2"></span></td>
43+
<td class="lines-code chroma"></span><span class="gh"></span></td>
44+
</tr></tbody>
45+
</table>
46+
</div>
47+
`, string(htm))
48+
49+
ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
50+
_, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
51+
FullURL: "http://full",
52+
OwnerName: "user15",
53+
RepoName: "big_test_private_1",
54+
CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
55+
FilePath: "/README.md",
56+
LineStart: 1,
57+
LineStop: 10,
58+
})
59+
assert.ErrorContains(t, err, "no permission")
60+
}
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<div class="code-preview-container">
2+
<div class="code-preview-header">
3+
<a href="{{.FullURL}}" class="muted" rel="nofollow">{{.FilePath}}</a>
4+
Lines {{.LineStart}} to {{.LineStop}} in
5+
<a href="{{.RepoLink}}/src/commit/{{.CommitID}}" rel="nofollow">{{.CommitID | ShortSha}}</a>
6+
</div>
7+
<table>
8+
<tbody>
9+
{{- range .HighlightLines -}}
10+
<tr>
11+
<td class="lines-num"><span data-line-number="{{.Num}}"></span></td>
12+
<td class="lines-code chroma">{{.FormattedContent}}</td>
13+
</tr>
14+
{{- end -}}
15+
</tbody>
16+
</table>
17+
</div>

0 commit comments

Comments
 (0)