diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index b3896bc31..91f86da5f 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2338,6 +2338,8 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Set the maximum number of characters in a mermaid source. (Set to -1 to disable limits) ;MERMAID_MAX_SOURCE_CHARACTERS = 5000 +;; Set the maximum number of lines allowed for a filepreview. (Set to -1 to disable limits; set to 0 to disable the feature) +;FILEPREVIEW_MAX_LINES = 50 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index 3e76dcb8a..32683c317 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -4,6 +4,7 @@ package markup import ( + "bufio" "bytes" "html/template" "regexp" @@ -12,6 +13,7 @@ import ( "strings" "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" @@ -31,9 +33,15 @@ type FilePreview struct { filePath string start int end int + isTruncated bool } func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Locale) *FilePreview { + if (setting.FilePreviewMaxLines == 0) { + // Feature is disabled + return nil + } + preview := &FilePreview{} m := filePreviewPattern.FindStringSubmatchIndex(node.Data) @@ -63,18 +71,20 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca preview.end = m[1] projPathSegments := strings.Split(projPath, "/") - fileContent, err := DefaultProcessorHelper.GetRepoFileContent( + var language string + fileBlob, err := DefaultProcessorHelper.GetRepoFileBlob( ctx.Ctx, projPathSegments[len(projPathSegments)-2], projPathSegments[len(projPathSegments)-1], commitSha, preview.filePath, + &language, ) if err != nil { return nil } lineSpecs := strings.Split(hash, "-") - lineCount := len(fileContent) + // lineCount := len(fileContent) commitLinkBuffer := new(bytes.Buffer) err = html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black")) @@ -82,28 +92,31 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca log.Error("failed to render commitLink: %v", err) } - if len(lineSpecs) == 1 { - line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) - if line < 1 || line > lineCount { - return nil - } + var startLine, endLine int - preview.fileContent = fileContent[line-1 : line] + if len(lineSpecs) == 1 { + startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) + endLine = startLine + // if line < 1 || line > lineCount { + // return nil + // } + + // preview.fileContent = fileContent[line-1 : line] preview.subTitle = locale.Tr( - "markup.filepreview.line", line, + "markup.filepreview.line", startLine, template.HTML(commitLinkBuffer.String()), ) - preview.lineOffset = line - 1 + preview.lineOffset = startLine - 1 } else { - startLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L")) - endLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L")) + 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 nil - } + // if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine { + // return nil + // } - preview.fileContent = fileContent[startLine-1 : endLine] + // preview.fileContent = fileContent[startLine-1 : endLine] preview.subTitle = locale.Tr( "markup.filepreview.lines", startLine, endLine, template.HTML(commitLinkBuffer.String()), @@ -112,6 +125,50 @@ func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca preview.lineOffset = startLine - 1 } + lineCount := endLine - (startLine-1) + if startLine < 1 || endLine < 1 || lineCount < 1 { + return nil + } + + if setting.FilePreviewMaxLines > 0 && lineCount > setting.FilePreviewMaxLines { + preview.isTruncated = true + lineCount = setting.FilePreviewMaxLines + } + + dataRc, err := fileBlob.DataAsync() + if err != nil { + return nil + } + defer dataRc.Close() + + reader := bufio.NewReader(dataRc) + + // skip all lines until we find our startLine + for i := 1; i < startLine; i++ { + _, err := reader.ReadBytes('\n') + if err != nil { + return nil + } + } + + // capture the lines we're interested in + lineBuffer := new(bytes.Buffer) + for i := 0; i < lineCount; i++ { + buf, err := reader.ReadBytes('\n') + if err != nil { + break; + } + lineBuffer.Write(buf) + } + + // highlight the file... + fileContent, _, err := highlight.File(fileBlob.Name(), language, lineBuffer.Bytes()) + if err != nil { + log.Error("highlight.File failed, fallback to plain text: %v", err) + fileContent = highlight.PlainText(lineBuffer.Bytes()) + } + preview.fileContent = fileContent + return preview } @@ -258,6 +315,20 @@ func (p *FilePreview) CreateHTML(locale translation.Locale) *html.Node { Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}}, } node.AppendChild(header) + + if (p.isTruncated) { + warning := &html.Node{ + Type: html.ElementNode, + Data: atom.Div.String(), + Attr: []html.Attribute{{Key: "class", Val: "ui warning message tw-text-left"}}, + } + warning.AppendChild(&html.Node{ + Type: html.TextNode, + Data: locale.TrString("markup.filepreview.truncated"), + }) + node.AppendChild(warning) + } + node.AppendChild(twrapper) return node diff --git a/modules/markup/html.go b/modules/markup/html.go index 9a04e02fb..4c74a81ba 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -1059,7 +1059,7 @@ func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { if ctx.Metas == nil { return } - if DefaultProcessorHelper.GetRepoFileContent == nil { + if DefaultProcessorHelper.GetRepoFileBlob == nil { return } diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index b6d742e5c..b08c9eb23 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -8,7 +8,7 @@ import ( "context" "errors" "fmt" - "html/template" + // "html/template" "io" "net/url" "path/filepath" @@ -32,7 +32,7 @@ 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) + GetRepoFileBlob func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, 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 c37027b84..1048f0e37 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -135,6 +135,7 @@ func createDefaultPolicy() *bluemonday.Policy { 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") + policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui warning message tw-text-left$")).OnElements("div") // Allow generally safe attributes generalSafeAttrs := []string{ diff --git a/modules/setting/markup.go b/modules/setting/markup.go index 6c2246342..e893c1c2f 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -15,6 +15,7 @@ var ( ExternalMarkupRenderers []*MarkupRenderer ExternalSanitizerRules []MarkupSanitizerRule MermaidMaxSourceCharacters int + FilePreviewMaxLines int ) const ( @@ -62,6 +63,7 @@ func loadMarkupFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "markdown", &Markdown) MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000) + FilePreviewMaxLines = rootCfg.Section("markup").Key("FILEPREVIEW_MAX_LINES").MustInt(50) ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10) ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index efaf8b72c..8533cf065 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3711,3 +3711,4 @@ submodule = Submodule [markup] filepreview.line = Line %[1]d in %[2]s filepreview.lines = Lines %[1]d to %[2]d in %[3]s +filepreview.truncated = Preview has been truncated diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go index df96f25ce..98a7824a6 100644 --- a/services/markup/processorhelper.go +++ b/services/markup/processorhelper.go @@ -6,15 +6,17 @@ package markup import ( "context" "fmt" - "html/template" - "io" + + // "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/gitrepo" - "code.gitea.io/gitea/modules/highlight" + // "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" gitea_context "code.gitea.io/gitea/services/context" @@ -39,7 +41,7 @@ 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) { + GetRepoFileBlob: func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) { repo, err := repo.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) if err != nil { return nil, err @@ -70,9 +72,11 @@ func ProcessorHelper() *markup.ProcessorHelper { return nil, err } - 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) + if language != nil { + *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) @@ -80,7 +84,9 @@ func ProcessorHelper() *markup.ProcessorHelper { return nil, err } - dataRc, err := blob.DataAsync() + return blob, nil + + /*dataRc, err := blob.DataAsync() if err != nil { return nil, err } @@ -97,7 +103,7 @@ func ProcessorHelper() *markup.ProcessorHelper { fileContent = highlight.PlainText(buf) } - return fileContent, nil + return fileContent, nil*/ }, } } diff --git a/web_src/css/markup/filepreview.css b/web_src/css/markup/filepreview.css index 69360e2a7..d2ec16ea8 100644 --- a/web_src/css/markup/filepreview.css +++ b/web_src/css/markup/filepreview.css @@ -25,6 +25,12 @@ background: var(--color-box-header); } +.markup .file-preview-box .warning { + border-radius: 0; + margin: 0; + padding: .5rem .5rem .5rem 1rem; +} + .markup .file-preview-box .header > a { display: block; }