From 726ea72fbd1508b665afa4b8ec2aab8ccb9ab44d Mon Sep 17 00:00:00 2001
From: wxiaoguang
Date: Tue, 2 Apr 2024 01:41:04 +0800
Subject: [PATCH] fix
---
modules/indexer/code/search.go | 6 +-
modules/markup/html.go | 1 +
modules/markup/html_codepreview.go | 75 +++++++++++++
modules/markup/html_codepreview_test.go | 34 ++++++
modules/markup/renderer.go | 3 +
modules/markup/sanitizer.go | 8 +-
routers/web/repo/search.go | 2 +-
services/contexttest/context_tests.go | 1 +
services/markup/main_test.go | 2 +-
services/markup/processorhelper.go | 2 +
.../markup/processorhelper_codepreview.go | 106 ++++++++++++++++++
.../processorhelper_codepreview_test.go | 60 ++++++++++
templates/base/markup_codepreview.tmpl | 17 +++
web_src/css/base.css | 5 +-
web_src/css/index.css | 1 +
web_src/css/markup/codepreview.css | 26 +++++
16 files changed, 342 insertions(+), 7 deletions(-)
create mode 100644 modules/markup/html_codepreview.go
create mode 100644 modules/markup/html_codepreview_test.go
create mode 100644 services/markup/processorhelper_codepreview.go
create mode 100644 services/markup/processorhelper_codepreview_test.go
create mode 100644 templates/base/markup_codepreview.tmpl
create mode 100644 web_src/css/markup/codepreview.css
diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go
index 5f35e8073b6cb..2d4f5ae7c63aa 100644
--- a/modules/indexer/code/search.go
+++ b/modules/indexer/code/search.go
@@ -70,9 +70,9 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error {
return nil
}
-func HighlightSearchResultCode(filename string, lineNums []int, code string) []ResultLine {
+func HighlightSearchResultCode(filename, language string, lineNums []int, code string) []ResultLine {
// we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting
- hl, _ := highlight.Code(filename, "", code)
+ hl, _ := highlight.Code(filename, language, code)
highlightedLines := strings.Split(string(hl), "\n")
// 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
UpdatedUnix: result.UpdatedUnix,
Language: result.Language,
Color: result.Color,
- Lines: HighlightSearchResultCode(result.Filename, lineNums, formattedLinesBuffer.String()),
+ Lines: HighlightSearchResultCode(result.Filename, result.Language, lineNums, formattedLinesBuffer.String()),
}, nil
}
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 21bd6206e0eb7..56aa1cb49cf9c 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -171,6 +171,7 @@ type processor func(ctx *RenderContext, node *html.Node)
var defaultProcessors = []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
+ codePreviewPatternProcessor,
fullHashPatternProcessor,
shortLinkProcessor,
linkProcessor,
diff --git a/modules/markup/html_codepreview.go b/modules/markup/html_codepreview.go
new file mode 100644
index 0000000000000..61c0d51dfd78f
--- /dev/null
+++ b/modules/markup/html_codepreview.go
@@ -0,0 +1,75 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+ "html/template"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/modules/httplib"
+
+ "golang.org/x/net/html"
+)
+
+// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
+var codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
+
+type RenderCodePreviewOptions struct {
+ FullURL string
+ OwnerName string
+ RepoName string
+ CommitID string
+ FilePath string
+
+ LineStart, LineStop int
+}
+
+func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosStop int, htm template.HTML, err error) {
+ m := codePreviewPattern.FindStringSubmatchIndex(node.Data)
+ if m == nil {
+ return 0, 0, "", nil
+ }
+
+ opts := RenderCodePreviewOptions{
+ FullURL: node.Data[m[0]:m[1]],
+ OwnerName: node.Data[m[2]:m[3]],
+ RepoName: node.Data[m[4]:m[5]],
+ CommitID: node.Data[m[6]:m[7]],
+ FilePath: node.Data[m[8]:m[9]],
+ }
+ if !httplib.IsCurrentGiteaSiteURL(opts.FullURL) {
+ return 0, 0, "", nil
+ }
+ u, err := url.Parse(opts.FilePath)
+ if err != nil {
+ return 0, 0, "", err
+ }
+ opts.FilePath = strings.TrimPrefix(u.Path, "/")
+
+ lineStartStr, lineStopStr, _ := strings.Cut(node.Data[m[10]:m[11]], "-")
+ lineStart, _ := strconv.Atoi(strings.TrimPrefix(lineStartStr, "L"))
+ lineStop, _ := strconv.Atoi(strings.TrimPrefix(lineStopStr, "L"))
+ opts.LineStart, opts.LineStop = lineStart, lineStop
+ h, err := DefaultProcessorHelper.RenderRepoFileCodePreview(ctx.Ctx, opts)
+ return m[0], m[1], h, err
+}
+
+func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
+ for node != nil {
+ urlPosStart, urlPosEnd, h, err := renderCodeBlock(ctx, node)
+ if err != nil || h == "" {
+ node = node.NextSibling
+ continue
+ }
+ next := node.NextSibling
+ nodeText := node.Data
+ node.Data = nodeText[:urlPosStart]
+ node.Parent.InsertBefore(&html.Node{Type: html.RawNode, Data: string(h)}, next)
+ node.Parent.InsertBefore(&html.Node{Type: html.TextNode, Data: nodeText[urlPosEnd:]}, next)
+ node = next
+ }
+}
diff --git a/modules/markup/html_codepreview_test.go b/modules/markup/html_codepreview_test.go
new file mode 100644
index 0000000000000..d33630d0401b4
--- /dev/null
+++ b/modules/markup/html_codepreview_test.go
@@ -0,0 +1,34 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup_test
+
+import (
+ "context"
+ "html/template"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/markup"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRenderCodePreview(t *testing.T) {
+ markup.Init(&markup.ProcessorHelper{
+ RenderRepoFileCodePreview: func(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
+ return "code preview
", nil
+ },
+ })
+ test := func(input, expected string) {
+ buffer, err := markup.RenderString(&markup.RenderContext{
+ Ctx: git.DefaultContext,
+ Type: "markdown",
+ }, input)
+ assert.NoError(t, err)
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
+ }
+ test("http://localhost:3000/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", "code preview
")
+ test("http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20", `http://other/owner/repo/src/commit/0123456789/foo/bar.md#L10-L20
`)
+}
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 0f0bf557403e4..005fcc278b973 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -8,6 +8,7 @@ import (
"context"
"errors"
"fmt"
+ "html/template"
"io"
"net/url"
"path/filepath"
@@ -33,6 +34,8 @@ type ProcessorHelper struct {
IsUsernameMentionable func(ctx context.Context, username string) bool
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
+
+ RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
}
var DefaultProcessorHelper ProcessorHelper
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 79a2ba0dfb8d2..a5a3df830ec31 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -58,7 +58,13 @@ func createDefaultPolicy() *bluemonday.Policy {
policy := bluemonday.UGCPolicy()
// For JS code copy and Mermaid loading state
- policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).Globally()
+
+ // For code preview
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+$`)).Globally()
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).Globally()
+ policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).Globally()
+ policy.AllowAttrs("data-line-number").OnElements("span")
// For color preview
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index 9d65427b8f348..46f0208453585 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -81,7 +81,7 @@ func Search(ctx *context.Context) {
// UpdatedUnix: not supported yet
// Language: not supported yet
// Color: not supported yet
- Lines: code_indexer.HighlightSearchResultCode(r.Filename, r.LineNumbers, strings.Join(r.LineCodes, "\n")),
+ Lines: code_indexer.HighlightSearchResultCode(r.Filename, "", r.LineNumbers, strings.Join(r.LineCodes, "\n")),
})
}
}
diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go
index d3e6de7efe4f9..3064c56590d45 100644
--- a/services/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -63,6 +63,7 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
base.Locale = &translation.MockLocale{}
ctx := context.NewWebContext(base, opt.Render, nil)
+ ctx.AppendContextValue(context.WebContextKey, ctx)
ctx.PageData = map[string]any{}
ctx.Data["PageStartTime"] = time.Now()
chiCtx := chi.NewRouteContext()
diff --git a/services/markup/main_test.go b/services/markup/main_test.go
index 89fe3e7e3461a..5553ebc058948 100644
--- a/services/markup/main_test.go
+++ b/services/markup/main_test.go
@@ -11,6 +11,6 @@ import (
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
- FixtureFiles: []string{"user.yml"},
+ FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"},
})
}
diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index a4378678a08b5..68487fb8dbb57 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -14,6 +14,8 @@ import (
func ProcessorHelper() *markup.ProcessorHelper {
return &markup.ProcessorHelper{
ElementDir: "auto", // set dir="auto" for necessary (eg: , , etc) tags
+
+ RenderRepoFileCodePreview: renderRepoFileCodePreview,
IsUsernameMentionable: func(ctx context.Context, username string) bool {
mentionedUser, err := user.GetUserByName(ctx, username)
if err != nil {
diff --git a/services/markup/processorhelper_codepreview.go b/services/markup/processorhelper_codepreview.go
new file mode 100644
index 0000000000000..19532fe00fe93
--- /dev/null
+++ b/services/markup/processorhelper_codepreview.go
@@ -0,0 +1,106 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "html/template"
+ "strings"
+
+ "code.gitea.io/gitea/models/perm/access"
+ "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/indexer/code"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/setting"
+ gitea_context "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePreviewOptions) (template.HTML, error) {
+ opts.LineStop = max(opts.LineStop, 1)
+ lineCount := opts.LineStop - opts.LineStart + 1
+ if lineCount <= 0 || lineCount > 140 /* GitHub at most show 140 lines */ {
+ lineCount = 10
+ opts.LineStop = opts.LineStart + lineCount
+ }
+
+ dbRepo, err := repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName)
+ if err != nil {
+ return "", err
+ }
+
+ webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
+ if !ok {
+ return "", fmt.Errorf("context is not a web context")
+ }
+ doer := webCtx.Doer
+
+ perms, err := access.GetUserRepoPermission(ctx, dbRepo, doer)
+ if err != nil {
+ return "", err
+ }
+ if !perms.CanRead(unit.TypeCode) {
+ return "", fmt.Errorf("no permission")
+ }
+
+ gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)
+ if err != nil {
+ return "", err
+ }
+ defer gitRepo.Close()
+
+ commit, err := gitRepo.GetCommit(opts.CommitID)
+ if err != nil {
+ return "", err
+ }
+
+ language, _ := files.TryGetContentLanguage(gitRepo, opts.CommitID, opts.FilePath)
+ blob, err := commit.GetBlobByPath(opts.FilePath)
+ if err != nil {
+ return "", err
+ }
+
+ if blob.Size() > setting.UI.MaxDisplayFileSize {
+ return "", fmt.Errorf("file is too large")
+ }
+
+ dataRc, err := blob.DataAsync()
+ if err != nil {
+ return "", err
+ }
+ defer dataRc.Close()
+
+ reader := bufio.NewReader(dataRc)
+
+ for i := 1; i < opts.LineStart; i++ {
+ if _, err = reader.ReadBytes('\n'); err != nil {
+ return "", err
+ }
+ }
+
+ lineNums := make([]int, 0, lineCount)
+ lineCodes := make([]string, 0, lineCount)
+ for i := opts.LineStart; i <= opts.LineStop; i++ {
+ if line, err := reader.ReadString('\n'); err != nil {
+ break
+ } else {
+ lineNums = append(lineNums, i)
+ lineCodes = append(lineCodes, line)
+ }
+ }
+ highlightLines := code.HighlightSearchResultCode(opts.FilePath, language, lineNums, strings.Join(lineCodes, ""))
+ return webCtx.RenderToHTML("base/markup_codepreview", map[string]any{
+ "FullURL": opts.FullURL,
+ "FilePath": opts.FilePath,
+ "LineStart": opts.LineStart,
+ "LineStop": opts.LineStop,
+ "RepoLink": dbRepo.Link(),
+ "CommitID": opts.CommitID,
+ "HighlightLines": highlightLines,
+ })
+}
diff --git a/services/markup/processorhelper_codepreview_test.go b/services/markup/processorhelper_codepreview_test.go
new file mode 100644
index 0000000000000..a23c63ae5f8a6
--- /dev/null
+++ b/services/markup/processorhelper_codepreview_test.go
@@ -0,0 +1,60 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestProcessorHelperCodePreview(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ htm, err := renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+ FullURL: "http://full",
+ OwnerName: "user2",
+ RepoName: "repo1",
+ CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ FilePath: "/README.md",
+ LineStart: 1,
+ LineStop: 10,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, `
+`, string(htm))
+
+ ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ _, err = renderRepoFileCodePreview(ctx, markup.RenderCodePreviewOptions{
+ FullURL: "http://full",
+ OwnerName: "user15",
+ RepoName: "big_test_private_1",
+ CommitID: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ FilePath: "/README.md",
+ LineStart: 1,
+ LineStop: 10,
+ })
+ assert.ErrorContains(t, err, "no permission")
+}
diff --git a/templates/base/markup_codepreview.tmpl b/templates/base/markup_codepreview.tmpl
new file mode 100644
index 0000000000000..142742f8afe9c
--- /dev/null
+++ b/templates/base/markup_codepreview.tmpl
@@ -0,0 +1,17 @@
+
+
+
+
+ {{- range .HighlightLines -}}
+
+ |
+ {{.FormattedContent}} |
+
+ {{- end -}}
+
+
+
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 96c90ee692ec4..05ddba3223bb5 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1186,10 +1186,13 @@ overflow-menu .ui.label {
content: attr(data-line-number);
line-height: 20px !important;
padding: 0 10px;
- cursor: pointer;
display: block;
}
+.code-view .lines-num span::after {
+ cursor: pointer;
+}
+
.lines-type-marker {
vertical-align: top;
}
diff --git a/web_src/css/index.css b/web_src/css/index.css
index 40b1d3c8811d3..7be8065dc780e 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -41,6 +41,7 @@
@import "./markup/content.css";
@import "./markup/codecopy.css";
+@import "./markup/codepreview.css";
@import "./markup/asciicast.css";
@import "./chroma/base.css";
diff --git a/web_src/css/markup/codepreview.css b/web_src/css/markup/codepreview.css
new file mode 100644
index 0000000000000..f668e2d730c60
--- /dev/null
+++ b/web_src/css/markup/codepreview.css
@@ -0,0 +1,26 @@
+.markup .code-preview-container {
+ border: 1px solid var(--color-secondary);
+}
+
+.markup .code-preview-container .code-preview-header {
+ border-bottom: 1px solid var(--color-secondary);
+ padding: 0.5em;
+ font-size: 11px;
+}
+
+.markup .code-preview-container table {
+ width: 100%;
+ margin: 0.25em 0;
+ max-height: 100px;
+ overflow-y: scroll;
+}
+
+/* override the polluted styles from the content.css: ".markup table ..." */
+.markup .code-preview-container table tr {
+ border: 0 !important;
+}
+.markup .code-preview-container table th,
+.markup .code-preview-container table td {
+ border: 0 !important;
+ padding: 0 0 0 5px !important;
+}