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,
+ `` +
+ `` +
+ `` +
+ `
` +
+ `
` +
+ `` +
+ `` +
+ ` | ` +
+ `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,
- `` +
- `` +
- `` +
- `
` +
- `
` +
- `` +
- `` +
- ` | ` +
- `A` + "\n" + ` | ` +
- `
` +
- `` +
- ` | ` +
- `B` + "\n" + ` | ` +
- `
` +
- `` +
- `
` +
- `
` +
- `
` +
- ``,
+ ``+
+ ``+
+ ``+
+ `
`+
+ `
`+
+ ``+
+ ``+
+ ` | `+
+ `A`+"\n"+` | `+
+ `
`+
+ ``+
+ ` | `+
+ `B`+"\n"+` | `+
+ `
`+
+ ``+
+ `
`+
+ `
`+
+ `
`+
+ ``,
)
}