loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'Render inline file permalinks' (#2669) from Mai-Lapyst/forgejo:markup-add-filepreview into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2669
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

+577 -4
+2
custom/conf/app.example.ini
··· 2338 2338 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 2339 2339 ;; Set the maximum number of characters in a mermaid source. (Set to -1 to disable limits) 2340 2340 ;MERMAID_MAX_SOURCE_CHARACTERS = 5000 2341 + ;; Set the maximum number of lines allowed for a filepreview. (Set to -1 to disable limits; set to 0 to disable the feature) 2342 + ;FILEPREVIEW_MAX_LINES = 50 2341 2343 2342 2344 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 2343 2345 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+323
modules/markup/file_preview.go
··· 1 + // Copyright The Forgejo Authors. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package markup 5 + 6 + import ( 7 + "bufio" 8 + "bytes" 9 + "html/template" 10 + "regexp" 11 + "slices" 12 + "strconv" 13 + "strings" 14 + 15 + "code.gitea.io/gitea/modules/charset" 16 + "code.gitea.io/gitea/modules/highlight" 17 + "code.gitea.io/gitea/modules/log" 18 + "code.gitea.io/gitea/modules/setting" 19 + "code.gitea.io/gitea/modules/translation" 20 + 21 + "golang.org/x/net/html" 22 + "golang.org/x/net/html/atom" 23 + ) 24 + 25 + // filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2" 26 + var filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`) 27 + 28 + type FilePreview struct { 29 + fileContent []template.HTML 30 + subTitle template.HTML 31 + lineOffset int 32 + urlFull string 33 + filePath string 34 + start int 35 + end int 36 + isTruncated bool 37 + } 38 + 39 + func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Locale) *FilePreview { 40 + if setting.FilePreviewMaxLines == 0 { 41 + // Feature is disabled 42 + return nil 43 + } 44 + 45 + preview := &FilePreview{} 46 + 47 + m := filePreviewPattern.FindStringSubmatchIndex(node.Data) 48 + if m == nil { 49 + return nil 50 + } 51 + 52 + // Ensure that every group has a match 53 + if slices.Contains(m, -1) { 54 + return nil 55 + } 56 + 57 + preview.urlFull = node.Data[m[0]:m[1]] 58 + 59 + // Ensure that we only use links to local repositories 60 + if !strings.HasPrefix(preview.urlFull, setting.AppURL+setting.AppSubURL) { 61 + return nil 62 + } 63 + 64 + projPath := strings.TrimSuffix(node.Data[m[2]:m[3]], "/") 65 + 66 + commitSha := node.Data[m[4]:m[5]] 67 + preview.filePath = node.Data[m[6]:m[7]] 68 + hash := node.Data[m[8]:m[9]] 69 + 70 + preview.start = m[0] 71 + preview.end = m[1] 72 + 73 + projPathSegments := strings.Split(projPath, "/") 74 + var language string 75 + fileBlob, err := DefaultProcessorHelper.GetRepoFileBlob( 76 + ctx.Ctx, 77 + projPathSegments[len(projPathSegments)-2], 78 + projPathSegments[len(projPathSegments)-1], 79 + commitSha, preview.filePath, 80 + &language, 81 + ) 82 + if err != nil { 83 + return nil 84 + } 85 + 86 + lineSpecs := strings.Split(hash, "-") 87 + 88 + commitLinkBuffer := new(bytes.Buffer) 89 + err = html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) 90 + if err != nil { 91 + log.Error("failed to render commitLink: %v", err) 92 + } 93 + 94 + var startLine, endLine int 95 + 96 + if len(lineSpecs) == 1 { 97 + startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) 98 + endLine = startLine 99 + preview.subTitle = locale.Tr( 100 + "markup.filepreview.line", startLine, 101 + template.HTML(commitLinkBuffer.String()), 102 + ) 103 + 104 + preview.lineOffset = startLine - 1 105 + } else { 106 + startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) 107 + endLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) 108 + preview.subTitle = locale.Tr( 109 + "markup.filepreview.lines", startLine, endLine, 110 + template.HTML(commitLinkBuffer.String()), 111 + ) 112 + 113 + preview.lineOffset = startLine - 1 114 + } 115 + 116 + lineCount := endLine - (startLine - 1) 117 + if startLine < 1 || endLine < 1 || lineCount < 1 { 118 + return nil 119 + } 120 + 121 + if setting.FilePreviewMaxLines > 0 && lineCount > setting.FilePreviewMaxLines { 122 + preview.isTruncated = true 123 + lineCount = setting.FilePreviewMaxLines 124 + } 125 + 126 + dataRc, err := fileBlob.DataAsync() 127 + if err != nil { 128 + return nil 129 + } 130 + defer dataRc.Close() 131 + 132 + reader := bufio.NewReader(dataRc) 133 + 134 + // skip all lines until we find our startLine 135 + for i := 1; i < startLine; i++ { 136 + _, err := reader.ReadBytes('\n') 137 + if err != nil { 138 + return nil 139 + } 140 + } 141 + 142 + // capture the lines we're interested in 143 + lineBuffer := new(bytes.Buffer) 144 + for i := 0; i < lineCount; i++ { 145 + buf, err := reader.ReadBytes('\n') 146 + if err != nil { 147 + break 148 + } 149 + lineBuffer.Write(buf) 150 + } 151 + 152 + // highlight the file... 153 + fileContent, _, err := highlight.File(fileBlob.Name(), language, lineBuffer.Bytes()) 154 + if err != nil { 155 + log.Error("highlight.File failed, fallback to plain text: %v", err) 156 + fileContent = highlight.PlainText(lineBuffer.Bytes()) 157 + } 158 + preview.fileContent = fileContent 159 + 160 + return preview 161 + } 162 + 163 + func (p *FilePreview) CreateHTML(locale translation.Locale) *html.Node { 164 + table := &html.Node{ 165 + Type: html.ElementNode, 166 + Data: atom.Table.String(), 167 + Attr: []html.Attribute{{Key: "class", Val: "file-preview"}}, 168 + } 169 + tbody := &html.Node{ 170 + Type: html.ElementNode, 171 + Data: atom.Tbody.String(), 172 + } 173 + 174 + status := &charset.EscapeStatus{} 175 + statuses := make([]*charset.EscapeStatus, len(p.fileContent)) 176 + for i, line := range p.fileContent { 177 + statuses[i], p.fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext) 178 + status = status.Or(statuses[i]) 179 + } 180 + 181 + for idx, code := range p.fileContent { 182 + tr := &html.Node{ 183 + Type: html.ElementNode, 184 + Data: atom.Tr.String(), 185 + } 186 + 187 + lineNum := strconv.Itoa(p.lineOffset + idx + 1) 188 + 189 + tdLinesnum := &html.Node{ 190 + Type: html.ElementNode, 191 + Data: atom.Td.String(), 192 + Attr: []html.Attribute{ 193 + {Key: "class", Val: "lines-num"}, 194 + }, 195 + } 196 + spanLinesNum := &html.Node{ 197 + Type: html.ElementNode, 198 + Data: atom.Span.String(), 199 + Attr: []html.Attribute{ 200 + {Key: "data-line-number", Val: lineNum}, 201 + }, 202 + } 203 + tdLinesnum.AppendChild(spanLinesNum) 204 + tr.AppendChild(tdLinesnum) 205 + 206 + if status.Escaped { 207 + tdLinesEscape := &html.Node{ 208 + Type: html.ElementNode, 209 + Data: atom.Td.String(), 210 + Attr: []html.Attribute{ 211 + {Key: "class", Val: "lines-escape"}, 212 + }, 213 + } 214 + 215 + if statuses[idx].Escaped { 216 + btnTitle := "" 217 + if statuses[idx].HasInvisible { 218 + btnTitle += locale.TrString("repo.invisible_runes_line") + " " 219 + } 220 + if statuses[idx].HasAmbiguous { 221 + btnTitle += locale.TrString("repo.ambiguous_runes_line") 222 + } 223 + 224 + escapeBtn := &html.Node{ 225 + Type: html.ElementNode, 226 + Data: atom.Button.String(), 227 + Attr: []html.Attribute{ 228 + {Key: "class", Val: "toggle-escape-button btn interact-bg"}, 229 + {Key: "title", Val: btnTitle}, 230 + }, 231 + } 232 + tdLinesEscape.AppendChild(escapeBtn) 233 + } 234 + 235 + tr.AppendChild(tdLinesEscape) 236 + } 237 + 238 + tdCode := &html.Node{ 239 + Type: html.ElementNode, 240 + Data: atom.Td.String(), 241 + Attr: []html.Attribute{ 242 + {Key: "class", Val: "lines-code chroma"}, 243 + }, 244 + } 245 + codeInner := &html.Node{ 246 + Type: html.ElementNode, 247 + Data: atom.Code.String(), 248 + Attr: []html.Attribute{{Key: "class", Val: "code-inner"}}, 249 + } 250 + codeText := &html.Node{ 251 + Type: html.RawNode, 252 + Data: string(code), 253 + } 254 + codeInner.AppendChild(codeText) 255 + tdCode.AppendChild(codeInner) 256 + tr.AppendChild(tdCode) 257 + 258 + tbody.AppendChild(tr) 259 + } 260 + 261 + table.AppendChild(tbody) 262 + 263 + twrapper := &html.Node{ 264 + Type: html.ElementNode, 265 + Data: atom.Div.String(), 266 + Attr: []html.Attribute{{Key: "class", Val: "ui table"}}, 267 + } 268 + twrapper.AppendChild(table) 269 + 270 + header := &html.Node{ 271 + Type: html.ElementNode, 272 + Data: atom.Div.String(), 273 + Attr: []html.Attribute{{Key: "class", Val: "header"}}, 274 + } 275 + afilepath := &html.Node{ 276 + Type: html.ElementNode, 277 + Data: atom.A.String(), 278 + Attr: []html.Attribute{ 279 + {Key: "href", Val: p.urlFull}, 280 + {Key: "class", Val: "muted"}, 281 + }, 282 + } 283 + afilepath.AppendChild(&html.Node{ 284 + Type: html.TextNode, 285 + Data: p.filePath, 286 + }) 287 + header.AppendChild(afilepath) 288 + 289 + psubtitle := &html.Node{ 290 + Type: html.ElementNode, 291 + Data: atom.Span.String(), 292 + Attr: []html.Attribute{{Key: "class", Val: "text small grey"}}, 293 + } 294 + psubtitle.AppendChild(&html.Node{ 295 + Type: html.RawNode, 296 + Data: string(p.subTitle), 297 + }) 298 + header.AppendChild(psubtitle) 299 + 300 + node := &html.Node{ 301 + Type: html.ElementNode, 302 + Data: atom.Div.String(), 303 + Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}}, 304 + } 305 + node.AppendChild(header) 306 + 307 + if p.isTruncated { 308 + warning := &html.Node{ 309 + Type: html.ElementNode, 310 + Data: atom.Div.String(), 311 + Attr: []html.Attribute{{Key: "class", Val: "ui warning message tw-text-left"}}, 312 + } 313 + warning.AppendChild(&html.Node{ 314 + Type: html.TextNode, 315 + Data: locale.TrString("markup.filepreview.truncated"), 316 + }) 317 + node.AppendChild(warning) 318 + } 319 + 320 + node.AppendChild(twrapper) 321 + 322 + return node 323 + }
+42
modules/markup/html.go
··· 171 171 var defaultProcessors = []processor{ 172 172 fullIssuePatternProcessor, 173 173 comparePatternProcessor, 174 + filePreviewPatternProcessor, 174 175 fullHashPatternProcessor, 175 176 shortLinkProcessor, 176 177 linkProcessor, ··· 1051 1052 } 1052 1053 replaceContent(node, start, end, createCodeLink(urlFull, text, "compare")) 1053 1054 node = node.NextSibling.NextSibling 1055 + } 1056 + } 1057 + 1058 + func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { 1059 + if ctx.Metas == nil { 1060 + return 1061 + } 1062 + if DefaultProcessorHelper.GetRepoFileBlob == nil { 1063 + return 1064 + } 1065 + 1066 + next := node.NextSibling 1067 + for node != nil && node != next { 1068 + locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale) 1069 + if !ok { 1070 + locale = translation.NewLocale("en-US") 1071 + } 1072 + 1073 + preview := NewFilePreview(ctx, node, locale) 1074 + if preview == nil { 1075 + return 1076 + } 1077 + 1078 + previewNode := preview.CreateHTML(locale) 1079 + 1080 + // Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div 1081 + before := node.Data[:preview.start] 1082 + after := node.Data[preview.end:] 1083 + node.Data = before 1084 + nextSibling := node.NextSibling 1085 + node.Parent.InsertBefore(&html.Node{ 1086 + Type: html.RawNode, 1087 + Data: "</p>", 1088 + }, nextSibling) 1089 + node.Parent.InsertBefore(previewNode, nextSibling) 1090 + node.Parent.InsertBefore(&html.Node{ 1091 + Type: html.RawNode, 1092 + Data: "<p>" + after, 1093 + }, nextSibling) 1094 + 1095 + node = node.NextSibling 1054 1096 } 1055 1097 } 1056 1098
+67
modules/markup/html_test.go
··· 17 17 "code.gitea.io/gitea/modules/markup" 18 18 "code.gitea.io/gitea/modules/markup/markdown" 19 19 "code.gitea.io/gitea/modules/setting" 20 + "code.gitea.io/gitea/modules/translation" 20 21 "code.gitea.io/gitea/modules/util" 21 22 22 23 "github.com/stretchr/testify/assert" 24 + "github.com/stretchr/testify/require" 23 25 ) 24 26 25 27 var localMetas = map[string]string{ ··· 676 678 assert.NoError(t, err) 677 679 assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String()) 678 680 } 681 + 682 + func TestRender_FilePreview(t *testing.T) { 683 + setting.StaticRootPath = "../../" 684 + setting.Names = []string{"english"} 685 + setting.Langs = []string{"en-US"} 686 + translation.InitLocales(context.Background()) 687 + 688 + setting.AppURL = markup.TestAppURL 689 + markup.Init(&markup.ProcessorHelper{ 690 + GetRepoFileBlob: func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) { 691 + gitRepo, err := git.OpenRepository(git.DefaultContext, "./tests/repo/repo1_filepreview") 692 + require.NoError(t, err) 693 + defer gitRepo.Close() 694 + 695 + commit, err := gitRepo.GetCommit("HEAD") 696 + require.NoError(t, err) 697 + 698 + blob, err := commit.GetBlobByPath("path/to/file.go") 699 + require.NoError(t, err) 700 + 701 + return blob, nil 702 + }, 703 + }) 704 + 705 + sha := "190d9492934af498c3f669d6a2431dc5459e5b20" 706 + commitFilePreview := util.URLJoin(markup.TestRepoURL, "src", "commit", sha, "path", "to", "file.go") + "#L2-L3" 707 + 708 + test := func(input, expected string) { 709 + buffer, err := markup.RenderString(&markup.RenderContext{ 710 + Ctx: git.DefaultContext, 711 + RelativePath: ".md", 712 + Metas: localMetas, 713 + }, input) 714 + assert.NoError(t, err) 715 + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) 716 + } 717 + 718 + test( 719 + commitFilePreview, 720 + `<p></p>`+ 721 + `<div class="file-preview-box">`+ 722 + `<div class="header">`+ 723 + `<a href="http://localhost:3000/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20/path/to/file.go#L2-L3" class="muted" rel="nofollow">path/to/file.go</a>`+ 724 + `<span class="text small grey">`+ 725 + `Lines 2 to 3 in <a href="http://localhost:3000/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20" class="text black" rel="nofollow">190d949</a>`+ 726 + `</span>`+ 727 + `</div>`+ 728 + `<div class="ui table">`+ 729 + `<table class="file-preview">`+ 730 + `<tbody>`+ 731 + `<tr>`+ 732 + `<td class="lines-num"><span data-line-number="2"></span></td>`+ 733 + `<td class="lines-code chroma"><code class="code-inner"><span class="nx">B</span>`+"\n"+`</code></td>`+ 734 + `</tr>`+ 735 + `<tr>`+ 736 + `<td class="lines-num"><span data-line-number="3"></span></td>`+ 737 + `<td class="lines-code chroma"><code class="code-inner"><span class="nx">C</span>`+"\n"+`</code></td>`+ 738 + `</tr>`+ 739 + `</tbody>`+ 740 + `</table>`+ 741 + `</div>`+ 742 + `</div>`+ 743 + `<p></p>`, 744 + ) 745 + }
+1
modules/markup/renderer.go
··· 31 31 32 32 type ProcessorHelper struct { 33 33 IsUsernameMentionable func(ctx context.Context, username string) bool 34 + GetRepoFileBlob func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) 34 35 35 36 ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute 36 37 }
+17
modules/markup/sanitizer.go
··· 113 113 // Allow 'color' and 'background-color' properties for the style attribute on text elements. 114 114 policy.AllowStyles("color", "background-color").OnElements("span", "p") 115 115 116 + // Allow classes for file preview links... 117 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^(lines-num|lines-code chroma)$")).OnElements("td") 118 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^code-inner$")).OnElements("code") 119 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview-box$")).OnElements("div") 120 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui table$")).OnElements("div") 121 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div") 122 + policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span") 123 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span") 124 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table") 125 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td") 126 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button") 127 + policy.AllowAttrs("title").OnElements("button") 128 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span") 129 + policy.AllowAttrs("data-tooltip-content").OnElements("span") 130 + policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a") 131 + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui warning message tw-text-left$")).OnElements("div") 132 + 116 133 // Allow generally safe attributes 117 134 generalSafeAttrs := []string{ 118 135 "abbr", "accept", "accept-charset",
+1
modules/markup/tests/repo/repo1_filepreview/HEAD
··· 1 + ref: refs/heads/master
+6
modules/markup/tests/repo/repo1_filepreview/config
··· 1 + [core] 2 + repositoryformatversion = 0 3 + filemode = true 4 + bare = true 5 + [remote "origin"] 6 + url = /home/mai/projects/codeark/forgejo/forgejo/modules/markup/tests/repo/repo1_filepreview/../../__test_repo
+1
modules/markup/tests/repo/repo1_filepreview/description
··· 1 + Unnamed repository; edit this file 'description' to name the repository.
+6
modules/markup/tests/repo/repo1_filepreview/info/exclude
··· 1 + # git ls-files --others --exclude-from=.git/info/exclude 2 + # Lines that start with '#' are comments. 3 + # For a project mostly in C, the following would be a good set of 4 + # exclude patterns (uncomment them if you want to use them): 5 + # *.[oa] 6 + # *~
modules/markup/tests/repo/repo1_filepreview/objects/19/0d9492934af498c3f669d6a2431dc5459e5b20

This is a binary file and will not be displayed.

modules/markup/tests/repo/repo1_filepreview/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904

This is a binary file and will not be displayed.

modules/markup/tests/repo/repo1_filepreview/objects/83/57a737d04385bb7f2ab59ff184be94756e7972

This is a binary file and will not be displayed.

modules/markup/tests/repo/repo1_filepreview/objects/84/22d40f12717e1ebd5cef2449f6c09d1f775969

This is a binary file and will not be displayed.

modules/markup/tests/repo/repo1_filepreview/objects/d4/490327def9658be036d6a52c4417d84e74dd4c

This is a binary file and will not be displayed.

+1
modules/markup/tests/repo/repo1_filepreview/objects/ee/2b1253d9cf407796e2e724926cbe3a974b214d
··· 1 + x+)JMU06e040031QH��I�K�ghQ��/TX'�7潊��s��#3��
+1
modules/markup/tests/repo/repo1_filepreview/refs/heads/master
··· 1 + 190d9492934af498c3f669d6a2431dc5459e5b20
+2
modules/setting/markup.go
··· 15 15 ExternalMarkupRenderers []*MarkupRenderer 16 16 ExternalSanitizerRules []MarkupSanitizerRule 17 17 MermaidMaxSourceCharacters int 18 + FilePreviewMaxLines int 18 19 ) 19 20 20 21 const ( ··· 62 63 mustMapSetting(rootCfg, "markdown", &Markdown) 63 64 64 65 MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000) 66 + FilePreviewMaxLines = rootCfg.Section("markup").Key("FILEPREVIEW_MAX_LINES").MustInt(50) 65 67 ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10) 66 68 ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10) 67 69
+5
options/locale/locale_en-US.ini
··· 3725 3725 executable_file = Executable file 3726 3726 symbolic_link = Symbolic link 3727 3727 submodule = Submodule 3728 + 3729 + [markup] 3730 + filepreview.line = Line %[1]d in %[2]s 3731 + filepreview.lines = Lines %[1]d to %[2]d in %[3]s 3732 + filepreview.truncated = Preview has been truncated
+54
services/markup/processorhelper.go
··· 5 5 6 6 import ( 7 7 "context" 8 + "fmt" 8 9 10 + "code.gitea.io/gitea/models/perm/access" 11 + "code.gitea.io/gitea/models/repo" 12 + "code.gitea.io/gitea/models/unit" 9 13 "code.gitea.io/gitea/models/user" 14 + "code.gitea.io/gitea/modules/git" 15 + "code.gitea.io/gitea/modules/gitrepo" 16 + "code.gitea.io/gitea/modules/log" 10 17 "code.gitea.io/gitea/modules/markup" 11 18 gitea_context "code.gitea.io/gitea/services/context" 19 + file_service "code.gitea.io/gitea/services/repository/files" 12 20 ) 13 21 14 22 func ProcessorHelper() *markup.ProcessorHelper { ··· 28 36 29 37 // when using gitea context (web context), use user's visibility and user's permission to check 30 38 return user.IsUserVisibleToViewer(giteaCtx, mentionedUser, giteaCtx.Doer) 39 + }, 40 + GetRepoFileBlob: func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) { 41 + repo, err := repo.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) 42 + if err != nil { 43 + return nil, err 44 + } 45 + 46 + var user *user.User 47 + 48 + giteaCtx, ok := ctx.(*gitea_context.Context) 49 + if ok { 50 + user = giteaCtx.Doer 51 + } 52 + 53 + perms, err := access.GetUserRepoPermission(ctx, repo, user) 54 + if err != nil { 55 + return nil, err 56 + } 57 + if !perms.CanRead(unit.TypeCode) { 58 + return nil, fmt.Errorf("cannot access repository code") 59 + } 60 + 61 + gitRepo, err := gitrepo.OpenRepository(ctx, repo) 62 + if err != nil { 63 + return nil, err 64 + } 65 + defer gitRepo.Close() 66 + 67 + commit, err := gitRepo.GetCommit(commitSha) 68 + if err != nil { 69 + return nil, err 70 + } 71 + 72 + if language != nil { 73 + *language, err = file_service.TryGetContentLanguage(gitRepo, commitSha, filePath) 74 + if err != nil { 75 + log.Error("Unable to get file language for %-v:%s. Error: %v", repo, filePath, err) 76 + } 77 + } 78 + 79 + blob, err := commit.GetBlobByPath(filePath) 80 + if err != nil { 81 + return nil, err 82 + } 83 + 84 + return blob, nil 31 85 }, 32 86 } 33 87 }
+1
web_src/css/index.css
··· 40 40 @import "./markup/content.css"; 41 41 @import "./markup/codecopy.css"; 42 42 @import "./markup/asciicast.css"; 43 + @import "./markup/filepreview.css"; 43 44 44 45 @import "./chroma/base.css"; 45 46 @import "./codemirror/base.css";
+2 -1
web_src/css/markup/content.css
··· 451 451 text-decoration: inherit; 452 452 } 453 453 454 - .markup pre > code { 454 + .markup pre > code, 455 + .markup .file-preview code { 455 456 padding: 0; 456 457 margin: 0; 457 458 font-size: 100%;
+41
web_src/css/markup/filepreview.css
··· 1 + .markup table.file-preview { 2 + margin-bottom: 0; 3 + } 4 + 5 + .markup table.file-preview td { 6 + padding: 0 10px !important; 7 + border: none !important; 8 + } 9 + 10 + .markup table.file-preview tr { 11 + border-top: none; 12 + background-color: inherit !important; 13 + } 14 + 15 + .markup .file-preview-box { 16 + margin-bottom: 16px; 17 + } 18 + 19 + .markup .file-preview-box .header { 20 + padding: .5rem; 21 + padding-left: 1rem; 22 + border: 1px solid var(--color-secondary); 23 + border-bottom: none; 24 + border-radius: 0.28571429rem 0.28571429rem 0 0; 25 + background: var(--color-box-header); 26 + } 27 + 28 + .markup .file-preview-box .warning { 29 + border-radius: 0; 30 + margin: 0; 31 + padding: .5rem .5rem .5rem 1rem; 32 + } 33 + 34 + .markup .file-preview-box .header > a { 35 + display: block; 36 + } 37 + 38 + .markup .file-preview-box .table { 39 + margin-top: 0; 40 + border-radius: 0 0 0.28571429rem 0.28571429rem; 41 + }
+2 -1
web_src/css/repo/linebutton.css
··· 1 - .code-view .lines-num:hover { 1 + .code-view .lines-num:hover, 2 + .file-preview .lines-num:hover { 2 3 color: var(--color-text-dark) !important; 3 4 } 4 5
+2 -2
web_src/js/features/repo-unicode-escape.js
··· 7 7 8 8 e.preventDefault(); 9 9 10 - const fileContent = btn.closest('.file-content, .non-diff-file-content'); 11 - const fileView = fileContent?.querySelectorAll('.file-code, .file-view'); 10 + const fileContent = btn.closest('.file-content, .non-diff-file-content, .file-preview-box'); 11 + const fileView = fileContent?.querySelectorAll('.file-code, .file-view, .file-preview'); 12 12 if (btn.matches('.escape-button')) { 13 13 for (const el of fileView) el.classList.add('unicode-escaped'); 14 14 hideElem(btn);