From 4c574aa5e8b98fef3e4d467baee4c2181d679b83 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Sat, 10 Feb 2024 19:16:10 +0100 Subject: [PATCH 01/15] Render inline code references --- modules/markup/html.go | 266 +++++++++++++++++++++ modules/markup/renderer.go | 4 + modules/markup/sanitizer.go | 15 ++ services/markup/processorhelper.go | 100 ++++++++ web_src/css/markup/content.css | 39 ++- web_src/css/repo/linebutton.css | 3 +- web_src/js/features/repo-unicode-escape.js | 4 +- 7 files changed, 420 insertions(+), 11 deletions(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index 56e1a1c54eda0..65b6fa639f966 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -10,10 +10,12 @@ import ( "path" "path/filepath" "regexp" + "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" @@ -62,6 +64,9 @@ var ( // fullURLPattern matches full URL like "mailto:...", "https://..." and "ssh+git://..." fullURLPattern = 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]{7,64})/(\S+)#(L\d+(?:-L\d+)?)`) + // emailRegex is definitely not perfect with edge cases, // it is still accepted by the CommonMark specification, as well as the HTML5 spec: // http://spec.commonmark.org/0.28/#email-address @@ -171,6 +176,7 @@ type processor func(ctx *RenderContext, node *html.Node) var defaultProcessors = []processor{ fullIssuePatternProcessor, comparePatternProcessor, + filePreviewPatternProcessor, fullHashPatternProcessor, shortLinkProcessor, linkProcessor, @@ -1054,6 +1060,266 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) { } } +func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { + if ctx.Metas == nil { + return + } + if DefaultProcessorHelper.GetRepoFileContent == nil || DefaultProcessorHelper.GetLocale == nil { + return + } + + next := node.NextSibling + for node != nil && node != next { + m := filePreviewPattern.FindStringSubmatchIndex(node.Data) + if m == nil { + return + } + + // Ensure that every group (m[0]...m[9]) has a match + for i := 0; i < 10; i++ { + if m[i] == -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 := node.Data[m[2]:m[3]] + projPath = strings.TrimSuffix(projPath, "/") + + 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) + + var subTitle string + var lineOffset int + + if len(lineSpecs) == 1 { + line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) + if line < 1 || line > lineCount { + return + } + + fileContent = fileContent[line-1 : line] + subTitle = "Line " + strconv.Itoa(line) + + 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 = "Lines " + strconv.Itoa(startLine) + " to " + strconv.Itoa(endLine) + + lineOffset = startLine - 1 + } + + 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(), + } + + locale, err := DefaultProcessorHelper.GetLocale(ctx.Ctx) + if err != nil { + return + } + + status := &charset.EscapeStatus{} + statuses := make([]*charset.EscapeStatus, len(fileContent)) + for i, line := range fileContent { + statuses[i], fileContent[i] = charset.EscapeControlHTML(line, locale) + 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.Tr("repo.invisible_runes_line") + " " + } + if statuses[idx].HasAmbiguous { + btnTitle += locale.Tr("repo.ambiguous_runes_line") + } + + escapeBtn := &html.Node{ + Type: html.ElementNode, + Data: atom.A.String(), + Attr: []html.Attribute{ + {Key: "class", Val: "toggle-escape-button btn interact-bg"}, + {Key: "title", Val: btnTitle}, + {Key: "href", Val: "javascript:void(0)"}, + }, + } + 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}}, + } + 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.TextNode, + Data: subTitle + " in ", + }) + psubtitle.AppendChild(createLink(urlFull[m[0]:m[5]], commitSha[0:7], "")) + 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) + + // Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div + before := node.Data[:start] + after := node.Data[end:] + node.Data = before + nextSibling := node.NextSibling + node.Parent.InsertBefore(&html.Node{ + Type: html.RawNode, + Data: "

", + }, nextSibling) + node.Parent.InsertBefore(preview, nextSibling) + if after != "" { + node.Parent.InsertBefore(&html.Node{ + Type: html.RawNode, + Data: "

" + after, + }, nextSibling) + } + + node = node.NextSibling + } +} + // emojiShortCodeProcessor for rendering text like :smile: into emoji func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { start := 0 diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index 5a7adcc553226..37d3fde58cb77 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" @@ -16,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "github.com/yuin/goldmark/ast" @@ -31,6 +33,8 @@ const ( type ProcessorHelper struct { IsUsernameMentionable func(ctx context.Context, username string) bool + GetRepoFileContent func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) + GetLocale func(ctx context.Context) (translation.Locale, error) ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute } diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index ffc33c3b8e5b9..92df7515728d3 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -120,6 +120,21 @@ func createDefaultPolicy() *bluemonday.Policy { // Allow 'color' and 'background-color' properties for the style attribute on text elements. policy.AllowStyles("color", "background-color").OnElements("span", "p") + // Allow classes for file preview links... + policy.AllowAttrs("class").Matching(regexp.MustCompile("^(lines-num|lines-code chroma)$")).OnElements("td") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^code-inner$")).OnElements("code") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview-box$")).OnElements("div") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui table$")).OnElements("div") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div") + policy.AllowAttrs("data-line-number").OnElements("span") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span") + policy.AllowAttrs("rel").OnElements("td") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("a") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span") + policy.AllowAttrs("data-tooltip-content").OnElements("span") + // Allow generally safe attributes generalSafeAttrs := []string{ "abbr", "accept", "accept-charset", diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index a4378678a08b5..39dcb7f219b53 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -5,9 +5,19 @@ package markup import ( "context" + "fmt" + "html/template" + "io" + "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/highlight" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/translation" gitea_context "code.gitea.io/gitea/services/context" ) @@ -29,5 +39,95 @@ func ProcessorHelper() *markup.ProcessorHelper { // when using gitea context (web context), use user's visibility and user's permission to check return user.IsUserVisibleToViewer(giteaCtx, mentionedUser, giteaCtx.Doer) }, + GetRepoFileContent: func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) { + repo, err := repo.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) + if err != nil { + return nil, err + } + + var user *user.User + + giteaCtx, ok := ctx.(*gitea_context.Context) + if ok { + user = giteaCtx.Doer + } + + perms, err := access.GetUserRepoPermission(ctx, repo, user) + if err != nil { + return nil, err + } + if !perms.CanRead(unit.TypeCode) { + return nil, fmt.Errorf("cannot access repository code") + } + + gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) + if err != nil { + return nil, err + } + + commit, err := gitRepo.GetCommit(commitSha) + if err != nil { + return nil, err + } + + language := "" + indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(commitSha) + if err == nil { + defer deleteTemporaryFile() + + filename2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{ + CachedOnly: true, + Attributes: []string{"linguist-language", "gitlab-language"}, + Filenames: []string{filePath}, + IndexFile: indexFilename, + WorkTree: worktree, + }) + if err != nil { + log.Error("Unable to load attributes for %-v:%s. Error: %v", repo, filePath, err) + } + + language = filename2attribute2info[filePath]["linguist-language"] + if language == "" || language == "unspecified" { + language = filename2attribute2info[filePath]["gitlab-language"] + } + if language == "unspecified" { + language = "" + } + } + + blob, err := commit.GetBlobByPath(filePath) + if err != nil { + return nil, err + } + + dataRc, err := blob.DataAsync() + if err != nil { + return nil, err + } + defer dataRc.Close() + + buf, _ := io.ReadAll(dataRc) + + fileContent, _, err := highlight.File(blob.Name(), language, buf) + if err != nil { + log.Error("highlight.File failed, fallback to plain text: %v", err) + fileContent = highlight.PlainText(buf) + } + + return fileContent, nil + }, + GetLocale: func(ctx context.Context) (translation.Locale, error) { + giteaCtx, ok := ctx.(*gitea_context.Context) + if ok { + return giteaCtx.Locale, nil + } + + giteaBaseCtx, ok := ctx.(*gitea_context.Base) + if ok { + return giteaBaseCtx.Locale, nil + } + + return nil, fmt.Errorf("could not retrive locale from context") + }, } } diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 5eeef078a505b..a60aade90f311 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -128,7 +128,7 @@ .markup ul, .markup ol, .markup dl, -.markup table, +.markup table:not(.file-preview), .markup pre { margin-top: 0; margin-bottom: 16px; @@ -281,7 +281,7 @@ margin-bottom: 0; } -.markup table { +.markup table:not(.file-preview) { display: block; width: 100%; width: max-content; @@ -289,21 +289,21 @@ overflow: auto; } -.markup table th { +.markup table:not(.file-preview) th { font-weight: var(--font-weight-semibold); } -.markup table th, -.markup table td { +.markup table:not(.file-preview) th, +.markup table:not(.file-preview) td { padding: 6px 13px !important; border: 1px solid var(--color-secondary) !important; } -.markup table tr { +.markup table:not(.file-preview) tr { border-top: 1px solid var(--color-secondary); } -.markup table tr:nth-child(2n) { +.markup table:not(.file-preview) tr:nth-child(2n) { background-color: var(--color-markup-table-row); } @@ -451,7 +451,8 @@ text-decoration: inherit; } -.markup pre > code { +.markup pre > code, +.markup .file-preview code { padding: 0; margin: 0; font-size: 100%; @@ -585,3 +586,25 @@ .file-view.markup.orgmode li.indeterminate > p { display: inline-block; } + +.markup .file-preview-box { + margin-bottom: 16px; +} + +.markup .file-preview-box div:nth-child(1) { + padding: .5rem; + padding-left: 1rem; + border: 1px solid var(--color-secondary); + border-bottom: none; + border-radius: 0.28571429rem 0.28571429rem 0 0; + background: var(--color-box-header); +} + +.markup .file-preview-box div:nth-child(1) > a { + display: block; +} + +.markup .file-preview-box .table { + margin-top: 0; + border-radius: 0 0 0.28571429rem 0.28571429rem; +} diff --git a/web_src/css/repo/linebutton.css b/web_src/css/repo/linebutton.css index 1e5e51eac5df3..7780d6a263b8a 100644 --- a/web_src/css/repo/linebutton.css +++ b/web_src/css/repo/linebutton.css @@ -1,4 +1,5 @@ -.code-view .lines-num:hover { +.code-view .lines-num:hover, +.file-preview .lines-num:hover { color: var(--color-text-dark) !important; } diff --git a/web_src/js/features/repo-unicode-escape.js b/web_src/js/features/repo-unicode-escape.js index d878532001611..9f0c745223e47 100644 --- a/web_src/js/features/repo-unicode-escape.js +++ b/web_src/js/features/repo-unicode-escape.js @@ -7,8 +7,8 @@ export function initUnicodeEscapeButton() { e.preventDefault(); - const fileContent = btn.closest('.file-content, .non-diff-file-content'); - const fileView = fileContent?.querySelectorAll('.file-code, .file-view'); + const fileContent = btn.closest('.file-content, .non-diff-file-content, .file-preview-box'); + const fileView = fileContent?.querySelectorAll('.file-code, .file-view, .file-preview'); if (btn.matches('.escape-button')) { for (const el of fileView) el.classList.add('unicode-escaped'); hideElem(btn); From dd690b54370d222c6135b49b95be972a109b23c1 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Sun, 11 Feb 2024 13:24:57 +0100 Subject: [PATCH 02/15] Fix spelling mistake --- services/markup/processorhelper.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index 39dcb7f219b53..d0235fa0cd4f7 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -127,7 +127,7 @@ func ProcessorHelper() *markup.ProcessorHelper { return giteaBaseCtx.Locale, nil } - return nil, fmt.Errorf("could not retrive locale from context") + return nil, fmt.Errorf("could not retrieve locale from context") }, } } From e2dd5104e2e3af26a600abc2564d930d07d6aff0 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Thu, 15 Feb 2024 16:59:43 +0100 Subject: [PATCH 03/15] Move filepreview css to own file; clean up some rules --- web_src/css/index.css | 1 + web_src/css/markup/content.css | 36 ++++++------------------------ web_src/css/markup/filepreview.css | 35 +++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 web_src/css/markup/filepreview.css diff --git a/web_src/css/index.css b/web_src/css/index.css index ab925a4aa06ed..8d2780ba422b6 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -30,6 +30,7 @@ @import "./markup/content.css"; @import "./markup/codecopy.css"; @import "./markup/asciicast.css"; +@import "./markup/filepreview.css"; @import "./chroma/base.css"; @import "./codemirror/base.css"; diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index a60aade90f311..430b4802d6a74 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -128,7 +128,7 @@ .markup ul, .markup ol, .markup dl, -.markup table:not(.file-preview), +.markup table, .markup pre { margin-top: 0; margin-bottom: 16px; @@ -281,7 +281,7 @@ margin-bottom: 0; } -.markup table:not(.file-preview) { +.markup table { display: block; width: 100%; width: max-content; @@ -289,21 +289,21 @@ overflow: auto; } -.markup table:not(.file-preview) th { +.markup table th { font-weight: var(--font-weight-semibold); } -.markup table:not(.file-preview) th, -.markup table:not(.file-preview) td { +.markup table th, +.markup table td { padding: 6px 13px !important; border: 1px solid var(--color-secondary) !important; } -.markup table:not(.file-preview) tr { +.markup table tr { border-top: 1px solid var(--color-secondary); } -.markup table:not(.file-preview) tr:nth-child(2n) { +.markup table tr:nth-child(2n) { background-color: var(--color-markup-table-row); } @@ -586,25 +586,3 @@ .file-view.markup.orgmode li.indeterminate > p { display: inline-block; } - -.markup .file-preview-box { - margin-bottom: 16px; -} - -.markup .file-preview-box div:nth-child(1) { - padding: .5rem; - padding-left: 1rem; - border: 1px solid var(--color-secondary); - border-bottom: none; - border-radius: 0.28571429rem 0.28571429rem 0 0; - background: var(--color-box-header); -} - -.markup .file-preview-box div:nth-child(1) > a { - display: block; -} - -.markup .file-preview-box .table { - margin-top: 0; - border-radius: 0 0 0.28571429rem 0.28571429rem; -} diff --git a/web_src/css/markup/filepreview.css b/web_src/css/markup/filepreview.css new file mode 100644 index 0000000000000..6ea0bb9ffd06a --- /dev/null +++ b/web_src/css/markup/filepreview.css @@ -0,0 +1,35 @@ +.markup table.file-preview { + margin-bottom: 0px; +} + +.markup table.file-preview td { + padding: 0 10px 0 10px !important; + border: none !important; +} + +.markup table.file-preview tr { + border-top: none; + background-color: inherit !important; +} + +.markup .file-preview-box { + margin-bottom: 16px; +} + +.markup .file-preview-box .header { + padding: .5rem; + padding-left: 1rem; + border: 1px solid var(--color-secondary); + border-bottom: none; + border-radius: 0.28571429rem 0.28571429rem 0 0; + background: var(--color-box-header); +} + +.markup .file-preview-box .header > a { + display: block; +} + +.markup .file-preview-box .table { + margin-top: 0; + border-radius: 0 0 0.28571429rem 0.28571429rem; +} From 9becb5e030fd2a56dda233b893d21062e8f86835 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Thu, 15 Feb 2024 17:07:32 +0100 Subject: [PATCH 04/15] Change locale.Tr to TrString to fix errors --- modules/markup/html.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index 65b6fa639f966..e189b61a82dec 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -1209,10 +1209,10 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { if statuses[idx].Escaped { btnTitle := "" if statuses[idx].HasInvisible { - btnTitle += locale.Tr("repo.invisible_runes_line") + " " + btnTitle += locale.TrString("repo.invisible_runes_line") + " " } if statuses[idx].HasAmbiguous { - btnTitle += locale.Tr("repo.ambiguous_runes_line") + btnTitle += locale.TrString("repo.ambiguous_runes_line") } escapeBtn := &html.Node{ From cb21f626e20777b30650934ab6e740624a16d60b Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Thu, 15 Feb 2024 17:17:57 +0100 Subject: [PATCH 05/15] Refactor to use TryGetContentLanguage instead of dealing with linguist seperatly --- services/markup/processorhelper.go | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index d0235fa0cd4f7..e42f13bfb279f 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/translation" gitea_context "code.gitea.io/gitea/services/context" + file_service "code.gitea.io/gitea/services/repository/files" ) func ProcessorHelper() *markup.ProcessorHelper { @@ -70,29 +71,9 @@ func ProcessorHelper() *markup.ProcessorHelper { return nil, err } - language := "" - indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(commitSha) - if err == nil { - defer deleteTemporaryFile() - - filename2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{ - CachedOnly: true, - Attributes: []string{"linguist-language", "gitlab-language"}, - Filenames: []string{filePath}, - IndexFile: indexFilename, - WorkTree: worktree, - }) - if err != nil { - log.Error("Unable to load attributes for %-v:%s. Error: %v", repo, filePath, err) - } - - language = filename2attribute2info[filePath]["linguist-language"] - if language == "" || language == "unspecified" { - language = filename2attribute2info[filePath]["gitlab-language"] - } - if language == "unspecified" { - language = "" - } + language, err := file_service.TryGetContentLanguage(gitRepo, commitSha, filePath) + if err != nil { + log.Error("Unable to get file language for %-v:%s. Error: %v", repo, filePath, err) } blob, err := commit.GetBlobByPath(filePath) From 1ad7faaf90d3624458d329e327c108731dbfff2f Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Thu, 15 Feb 2024 17:24:28 +0100 Subject: [PATCH 06/15] Fix linting issues --- web_src/css/markup/filepreview.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/css/markup/filepreview.css b/web_src/css/markup/filepreview.css index 6ea0bb9ffd06a..69360e2a70fe8 100644 --- a/web_src/css/markup/filepreview.css +++ b/web_src/css/markup/filepreview.css @@ -1,9 +1,9 @@ .markup table.file-preview { - margin-bottom: 0px; + margin-bottom: 0; } .markup table.file-preview td { - padding: 0 10px 0 10px !important; + padding: 0 10px !important; border: none !important; } From 74b732681aa642ec1dae7a0659e120485d7a5913 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Mon, 26 Feb 2024 09:07:58 +0100 Subject: [PATCH 07/15] Change Colors of links in header of filepreview --- modules/markup/html.go | 7 +++++-- modules/markup/sanitizer.go | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index e189b61a82dec..ddc17239e6d87 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -1271,7 +1271,10 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { afilepath := &html.Node{ Type: html.ElementNode, Data: atom.A.String(), - Attr: []html.Attribute{{Key: "href", Val: urlFull}}, + Attr: []html.Attribute{ + {Key: "href", Val: urlFull}, + {Key: "class", Val: "muted"}, + }, } afilepath.AppendChild(&html.Node{ Type: html.TextNode, @@ -1288,7 +1291,7 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { Type: html.TextNode, Data: subTitle + " in ", }) - psubtitle.AppendChild(createLink(urlFull[m[0]:m[5]], commitSha[0:7], "")) + psubtitle.AppendChild(createLink(urlFull[m[0]:m[5]], commitSha[0:7], "text black")) header.AppendChild(psubtitle) preview := &html.Node{ diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 92df7515728d3..55ee577c59e41 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -134,6 +134,7 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("a") policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span") policy.AllowAttrs("data-tooltip-content").OnElements("span") + policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a") // Allow generally safe attributes generalSafeAttrs := []string{ From 95289c024798c3b8cfad2cd1dd1ace31c33db56c Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Mon, 26 Feb 2024 09:11:07 +0100 Subject: [PATCH 08/15] Add some more regex validations to attributes of elements inside filepreview --- modules/markup/sanitizer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 55ee577c59e41..76b8cc6e312b1 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -126,9 +126,9 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview-box$")).OnElements("div") policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui table$")).OnElements("div") policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div") - policy.AllowAttrs("data-line-number").OnElements("span") + policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span") policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span") - policy.AllowAttrs("rel").OnElements("td") + policy.AllowAttrs("rel").Matching(regexp.MustCompile("^L[0-9]+$")).OnElements("td") policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table") policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td") policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("a") From 97b1a46e3a571ea7d5614cd3288109ef13ecbaa9 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Mon, 26 Feb 2024 09:16:10 +0100 Subject: [PATCH 09/15] Make the toggle-escape-button really a button --- modules/markup/html.go | 3 +-- modules/markup/sanitizer.go | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index ddc17239e6d87..30c09f62d544f 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -1217,11 +1217,10 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { escapeBtn := &html.Node{ Type: html.ElementNode, - Data: atom.A.String(), + Data: atom.Button.String(), Attr: []html.Attribute{ {Key: "class", Val: "toggle-escape-button btn interact-bg"}, {Key: "title", Val: btnTitle}, - {Key: "href", Val: "javascript:void(0)"}, }, } tdLinesEscape.AppendChild(escapeBtn) diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 76b8cc6e312b1..2633bd3376186 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -131,7 +131,9 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("rel").Matching(regexp.MustCompile("^L[0-9]+$")).OnElements("td") policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table") policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td") - policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("a") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button") + policy.AllowAttrs("title").OnElements("button") + policy.AllowAttrs() policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span") policy.AllowAttrs("data-tooltip-content").OnElements("span") policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a") From 1b9a0a430b4caf83504a0669385ed9d056c0f832 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Mon, 26 Feb 2024 20:24:05 +0100 Subject: [PATCH 10/15] Remove empty policy.AllowAttrs --- modules/markup/sanitizer.go | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 2633bd3376186..73e17060a7740 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -133,7 +133,6 @@ func createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td") policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button") policy.AllowAttrs("title").OnElements("button") - policy.AllowAttrs() policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span") policy.AllowAttrs("data-tooltip-content").OnElements("span") policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a") From e43bbdd0ee6981ac6f2c6133445a894c31da3977 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Fri, 1 Mar 2024 07:55:10 +0100 Subject: [PATCH 11/15] Use gitrepo module instead of directly the git module --- services/markup/processorhelper.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index e42f13bfb279f..134b1b515291a 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -13,7 +13,7 @@ import ( "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -61,7 +61,7 @@ func ProcessorHelper() *markup.ProcessorHelper { return nil, fmt.Errorf("cannot access repository code") } - gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) + gitRepo, err := gitrepo.OpenRepository(ctx, repo) if err != nil { return nil, err } From e8d905269f0d27cd4de6bb046351b04964455f05 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Thu, 7 Mar 2024 04:54:28 +0100 Subject: [PATCH 12/15] Log error when DefaultProcessorHelper.GetLocale returns an error --- modules/markup/html.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/markup/html.go b/modules/markup/html.go index 30c09f62d544f..d390c38c4c9f9 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -1160,6 +1160,7 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { locale, err := DefaultProcessorHelper.GetLocale(ctx.Ctx) if err != nil { + log.Error("Unable to get locale. Error: %v", err) return } From 110546805242337eeb4e6b4c5a8ed61ead42e83b Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Thu, 7 Mar 2024 04:55:05 +0100 Subject: [PATCH 13/15] Fix rendering cleanup of the paragraph surrounding a file-preview link --- modules/markup/html.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index d390c38c4c9f9..ccf3ff7e8325a 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -1312,12 +1312,10 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { Data: "

", }, nextSibling) node.Parent.InsertBefore(preview, nextSibling) - if after != "" { - node.Parent.InsertBefore(&html.Node{ - Type: html.RawNode, - Data: "

" + after, - }, nextSibling) - } + node.Parent.InsertBefore(&html.Node{ + Type: html.RawNode, + Data: "

" + after, + }, nextSibling) node = node.NextSibling } From ce9bfa24328f9b09f1e72b8f358b17d0dc58b718 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Thu, 7 Mar 2024 04:55:29 +0100 Subject: [PATCH 14/15] Add test for file-preview link rendering --- modules/markup/html_test.go | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index ccb63c6baba8b..18c05c7d2a728 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -5,6 +5,7 @@ package markup_test import ( "context" + "html/template" "io" "os" "strings" @@ -13,10 +14,12 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -688,3 +691,57 @@ func TestIsFullURL(t *testing.T) { assert.True(t, markup.IsFullURLString("mailto:test@example.com")) assert.False(t, markup.IsFullURLString("/foo:bar")) } + +func TestRender_FilePreview(t *testing.T) { + setting.AppURL = markup.TestAppURL + markup.Init(&markup.ProcessorHelper{ + GetRepoFileContent: func(ctx context.Context, ownerName string, repoName string, commitSha string, filePath string) ([]template.HTML, error) { + buf := []byte( "A\nB\nC\nD\n" ) + return highlight.PlainText(buf), nil + }, + GetLocale: func(ctx context.Context) (translation.Locale, error) { + return translation.NewLocale("en-US"), nil + }, + }) + + sha := "b6dd6210eaebc915fd5be5579c58cce4da2e2579" + commitFilePreview := util.URLJoin(markup.TestRepoURL, "src", "commit", sha, "path", "to", "file.go") + "#L1-L2" + + test := func(input, expected string) { + buffer, err := markup.RenderString(&markup.RenderContext{ + Ctx: git.DefaultContext, + RelativePath: ".md", + Metas: localMetas, + }, input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + + test( + commitFilePreview, + `

` + + `
` + + `
` + + `path/to/file.go` + + `` + + `Lines 1 to 2 in b6dd621` + + `` + + `
` + + `
` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `
A` + "\n" + `
B` + "\n" + `
` + + `
` + + `
` + + `

`, + ) +} From 69a6aa0cc643c502d4e0009ff3b777b9bec2c08e Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Thu, 7 Mar 2024 05:00:10 +0100 Subject: [PATCH 15/15] Run make fmt --- modules/markup/html_test.go | 56 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 18c05c7d2a728..c19ae1e13f2c9 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -695,8 +695,8 @@ func TestIsFullURL(t *testing.T) { func TestRender_FilePreview(t *testing.T) { setting.AppURL = markup.TestAppURL markup.Init(&markup.ProcessorHelper{ - GetRepoFileContent: func(ctx context.Context, ownerName string, repoName string, commitSha string, filePath string) ([]template.HTML, error) { - buf := []byte( "A\nB\nC\nD\n" ) + GetRepoFileContent: func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) { + buf := []byte("A\nB\nC\nD\n") return highlight.PlainText(buf), nil }, GetLocale: func(ctx context.Context) (translation.Locale, error) { @@ -709,9 +709,9 @@ func TestRender_FilePreview(t *testing.T) { test := func(input, expected string) { buffer, err := markup.RenderString(&markup.RenderContext{ - Ctx: git.DefaultContext, + Ctx: git.DefaultContext, RelativePath: ".md", - Metas: localMetas, + Metas: localMetas, }, input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) @@ -719,29 +719,29 @@ func TestRender_FilePreview(t *testing.T) { test( commitFilePreview, - `

` + - `
` + - `
` + - `path/to/file.go` + - `` + - `Lines 1 to 2 in b6dd621` + - `` + - `
` + - `
` + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `
A` + "\n" + `
B` + "\n" + `
` + - `
` + - `
` + - `

`, + `

`+ + `
`+ + `
`+ + `path/to/file.go`+ + ``+ + `Lines 1 to 2 in b6dd621`+ + ``+ + `
`+ + `
`+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + ``+ + `
A`+"\n"+`
B`+"\n"+`
`+ + `
`+ + `
`+ + `

`, ) }