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, `

+
+ /README.md + Lines 1 to 10 in + 65f1bf27bc +
+ + + + + + + + +
# repo1
+
+`, 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 @@ +
+
+ {{.FilePath}} + Lines {{.LineStart}} to {{.LineStop}} in + {{.CommitID | ShortSha}} +
+ + + {{- range .HighlightLines -}} + + + + + {{- end -}} + +
{{.FormattedContent}}
+
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; +}