diff --git a/modules/charset/escape.go b/modules/charset/escape.go index ce3df61b93b..bdb4b5f309d 100644 --- a/modules/charset/escape.go +++ b/modules/charset/escape.go @@ -28,23 +28,21 @@ func EscapeOptionsForView() EscapeOptions { } } -func EscapeControlHTMLTo(html template.HTML, locale translation.Locale, w htmlutil.HTMLWriter, opts ...EscapeOptions) *EscapeStatus { - if !setting.UI.AmbiguousUnicodeDetection { - w.WriteHTML(html) - return &EscapeStatus{} - } - escaped, _ := EscapeControlReader(strings.NewReader(string(html)), w.OriginWriter(), locale, opts...) - return escaped -} - // EscapeControlHTML escapes the Unicode control sequences in a provided html document func EscapeControlHTML(html template.HTML, locale translation.Locale, opts ...EscapeOptions) (escaped *EscapeStatus, output template.HTML) { - sb, w := htmlutil.NewHTMLStringWriter() - escaped = EscapeControlHTMLTo(html, locale, w, opts...) - return escaped, template.HTML(sb.String()) + if !setting.UI.AmbiguousUnicodeDetection { + return &EscapeStatus{}, html + } + w := &htmlutil.HTMLBuilder{} + escaped, _ = EscapeControlReader(strings.NewReader(string(html)), w, locale, opts...) + return escaped, w.HTMLString() } // EscapeControlReader escapes the Unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus -func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, opts ...EscapeOptions) (*EscapeStatus, error) { - return escapeStream(locale, reader, writer, opts...) +func EscapeControlReader(reader io.Reader, writer htmlutil.HTMLWriter, locale translation.Locale, opts ...EscapeOptions) (*EscapeStatus, error) { + if !setting.UI.AmbiguousUnicodeDetection { + _, err := io.Copy(writer.OriginWriter(), reader) + return &EscapeStatus{}, err + } + return escapeStream(locale, reader, writer.OriginWriter(), opts...) } diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go index 9791cbd933a..905001fddb1 100644 --- a/modules/charset/escape_test.go +++ b/modules/charset/escape_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "gitea.dev/modules/htmlutil" "gitea.dev/modules/setting" "gitea.dev/modules/test" "gitea.dev/modules/translation" @@ -145,7 +146,7 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`, func TestEscapeControlReader(t *testing.T) { for _, tt := range escapeControlTests { t.Run(tt.name, func(t *testing.T) { - output := &strings.Builder{} + output := &htmlutil.HTMLBuilder{} status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{}) assert.NoError(t, err) assert.Equal(t, tt.status, *status) diff --git a/modules/htmlutil/html.go b/modules/htmlutil/html.go index f1e3a87b08a..8d7c1350b9c 100644 --- a/modules/htmlutil/html.go +++ b/modules/htmlutil/html.go @@ -90,18 +90,27 @@ func EscapeString(s string) template.HTML { } type HTMLWriter interface { + Err() error OriginWriter() io.Writer WriteString(s string) HTMLWriter WriteHTML(s template.HTML) HTMLWriter WriteFormatf(fmt template.HTML, args ...any) HTMLWriter - Err() error } +var ( + _ HTMLWriter = (*htmlWriter)(nil) + _ HTMLWriter = (*HTMLBuilder)(nil) +) + type htmlWriter struct { w io.Writer errs []error } +func (h *htmlWriter) Err() error { + return errors.Join(h.errs...) +} + func (h *htmlWriter) OriginWriter() io.Writer { return h.w } @@ -127,10 +136,6 @@ func (h *htmlWriter) WriteFormatf(fmt template.HTML, args ...any) HTMLWriter { return h } -func (h *htmlWriter) Err() error { - return errors.Join(h.errs...) -} - func NewHTMLWriter(w io.Writer) HTMLWriter { return &htmlWriter{w: w} } @@ -144,17 +149,29 @@ type HTMLBuilder struct { sb strings.Builder } -func (b *HTMLBuilder) WriteString(s string) *HTMLBuilder { +func (b *HTMLBuilder) Err() error { + return nil +} + +func (b *HTMLBuilder) OriginWriter() io.Writer { + return &b.sb +} + +func (b *HTMLBuilder) Reset() { + b.sb.Reset() +} + +func (b *HTMLBuilder) WriteString(s string) HTMLWriter { b.sb.WriteString(template.HTMLEscapeString(s)) return b } -func (b *HTMLBuilder) WriteHTML(s template.HTML) *HTMLBuilder { +func (b *HTMLBuilder) WriteHTML(s template.HTML) HTMLWriter { b.sb.WriteString(string(s)) return b } -func (b *HTMLBuilder) WriteFormatf(fmt template.HTML, args ...any) *HTMLBuilder { +func (b *HTMLBuilder) WriteFormatf(fmt template.HTML, args ...any) HTMLWriter { _, _ = HTMLPrintf(&b.sb, fmt, args...) return b } diff --git a/modules/util/string.go b/modules/util/string.go index a0cc71798a1..0d1532b7d61 100644 --- a/modules/util/string.go +++ b/modules/util/string.go @@ -4,6 +4,7 @@ package util import ( + "iter" "strings" "unsafe" ) @@ -131,3 +132,10 @@ func AsciiEqualFold(s, t string) bool { } return true } + +func StringSplitSeq[T, S ~string](s T, sep S) iter.Seq[T] { + f := strings.SplitSeq(string(s), string(sep)) + return func(yield func(T) bool) { + f(func(v string) bool { return yield(T(v)) }) + } +} diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index 46e3522f127..98f0f7530a7 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -6,7 +6,7 @@ package setting import ( "bytes" "fmt" - gotemplate "html/template" + "html/template" "io" "net/http" "net/url" @@ -20,6 +20,7 @@ import ( "gitea.dev/modules/git" "gitea.dev/modules/git/attribute" "gitea.dev/modules/git/pipeline" + "gitea.dev/modules/htmlutil" "gitea.dev/modules/lfs" "gitea.dev/modules/log" repo_module "gitea.dev/modules/repository" @@ -267,57 +268,48 @@ func LFSFileGet(ctx *context.Context) { buf = buf[:n] st := typesniffer.DetectContentType(buf) - // FIXME: there is no IsPlainText set, but template uses it ctx.Data["IsTextFile"] = st.IsText() ctx.Data["FileSize"] = meta.Size ctx.Data["RawFileLink"] = fmt.Sprintf("%s/%s/%s.git/info/lfs/objects/%s", setting.AppSubURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid)) switch { - case st.IsRepresentableAsText(): - if meta.Size >= setting.UI.MaxDisplayFileSize { - ctx.Data["IsFileTooLarge"] = true - break - } - - if st.IsSvgImage() { - ctx.Data["IsImageFile"] = true - } - - rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) - - // Building code view blocks with line number on server side. - // FIXME: the logic is not right here: it first calls EscapeControlReader then calls HTMLEscapeString: double-escaping - escapedContent := &bytes.Buffer{} - ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent, ctx.Locale) - - var output bytes.Buffer - lines := strings.Split(escapedContent.String(), "\n") - // Remove blank line at the end of file - if len(lines) > 0 && lines[len(lines)-1] == "" { - lines = lines[:len(lines)-1] - } - for index, line := range lines { - line = gotemplate.HTMLEscapeString(line) - if index != len(lines)-1 { - line += "\n" - } - fmt.Fprintf(&output, `
  • %s
  • `, index+1, index+1, line) - } - ctx.Data["FileContent"] = gotemplate.HTML(output.String()) - - output.Reset() - for i := 0; i < len(lines); i++ { - fmt.Fprintf(&output, `%d`, i+1, i+1) - } - ctx.Data["LineNums"] = gotemplate.HTML(output.String()) - case st.IsVideo(): ctx.Data["IsVideoFile"] = true case st.IsAudio(): ctx.Data["IsAudioFile"] = true case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()): ctx.Data["IsImageFile"] = true + case st.IsRepresentableAsText(): + if meta.Size >= setting.UI.MaxDisplayFileSize { + ctx.Data["IsFileTooLarge"] = true + break + } + + rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) + fileContentBytes, _ := io.ReadAll(io.LimitReader(rd, setting.UI.MaxDisplayFileSize)) + fileContentHTML := htmlutil.EscapeString(util.UnsafeBytesToString(fileContentBytes)) + escapeStatus, fileContentHTML := charset.EscapeControlHTML(fileContentHTML, ctx.Locale) + + output := &htmlutil.HTMLBuilder{} + output.WriteHTML(``) + writeLine := func(lineNum int, line template.HTML) { + output.WriteFormatf(``, lineNum, line) + } + prevLineIndex, prevLine := -1, template.HTML("") + for line := range util.StringSplitSeq(fileContentHTML, "\n") { + if prevLineIndex >= 0 { + writeLine(prevLineIndex+1, prevLine) + } + prevLineIndex, prevLine = prevLineIndex+1, line + } + if prevLine != "" { // trim last empty line + writeLine(prevLineIndex+1, prevLine) + } + output.WriteHTML(`
    %d%s
    `) + + ctx.Data["EscapeStatus"] = escapeStatus + ctx.Data["FileContentHTML"] = output.HTMLString() default: - // TODO: the logic is not the same as "renderFile" in "view.go" + // the logic is not the same as "renderFile" in "view.go" because here it just needs to render a simple view } ctx.HTML(http.StatusOK, tplSettingsLFSFile) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 4e274745e78..2dd11ab7ee8 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -13,7 +13,6 @@ import ( "net/http" "net/url" "path" - "strings" "time" activities_model "gitea.dev/models/activities" @@ -29,6 +28,7 @@ import ( "gitea.dev/modules/charset" "gitea.dev/modules/fileicon" "gitea.dev/modules/git" + "gitea.dev/modules/htmlutil" "gitea.dev/modules/lfs" "gitea.dev/modules/log" "gitea.dev/modules/markup" @@ -158,14 +158,14 @@ func markupRenderToHTML(ctx *context.Context, renderCtx *markup.RenderContext, r done := make(chan struct{}) go func() { - sb := &strings.Builder{} + sb := &htmlutil.HTMLBuilder{} if markup.RendererNeedPostProcess(renderer) { escaped, _ = charset.EscapeControlReader(markupRd, sb, ctx.Locale, charset.EscapeOptionsForView()) } else { escaped = &charset.EscapeStatus{} - _, _ = io.Copy(sb, markupRd) + _, _ = io.Copy(sb.OriginWriter(), markupRd) } - output = template.HTML(sb.String()) + output = sb.HTMLString() close(done) }() diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 64a7919820a..d67e7cfb5a7 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -20,6 +20,7 @@ import ( "gitea.dev/modules/base" "gitea.dev/modules/charset" "gitea.dev/modules/git" + "gitea.dev/modules/htmlutil" "gitea.dev/modules/log" "gitea.dev/modules/markup" "gitea.dev/modules/markup/markdown" @@ -252,13 +253,13 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository) renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) { - buf := &strings.Builder{} + buf := &htmlutil.HTMLBuilder{} markupRd, markupWr := io.Pipe() defer markupWr.Close() done := make(chan struct{}) go func() { escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.EscapeOptionsForView()) - output = template.HTML(buf.String()) + output = buf.HTMLString() buf.Reset() close(done) }() diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 505560dcc19..bef5ed22a79 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -339,7 +339,7 @@ type DiffInlineComputed struct { // computeDiffInline makes a DiffInline with computed content, e.g.: Unicode escaping, truncation hint, etc func computeDiffInline(s template.HTML, isTruncated bool, locale translation.Locale) DiffInlineComputed { sb, w := htmlutil.NewHTMLStringWriter() - status := charset.EscapeControlHTMLTo(s, locale, w) + status, _ := charset.EscapeControlReader(strings.NewReader(string(s)), w, locale) if isTruncated { w.WriteFormatf(`%s`, locale.Tr("repo.diff.line_truncated")) } diff --git a/templates/repo/settings/lfs.tmpl b/templates/repo/settings/lfs.tmpl index 0ba27bbdecd..88226c0f833 100644 --- a/templates/repo/settings/lfs.tmpl +++ b/templates/repo/settings/lfs.tmpl @@ -3,8 +3,8 @@

    {{ctx.Locale.Tr "repo.settings.lfs_filelist"}} ({{ctx.Locale.Tr "admin.total" .Total}})
    - {{ctx.Locale.Tr "repo.settings.lfs_locks"}} -  {{ctx.Locale.Tr "repo.settings.lfs_findpointerfiles"}} + {{ctx.Locale.Tr "repo.settings.lfs_locks"}} +  {{ctx.Locale.Tr "repo.settings.lfs_findpointerfiles"}}

    @@ -12,17 +12,21 @@ {{range .LFSFiles}} - {{else}} @@ -33,20 +37,12 @@
    - + {{ShortSha .Oid}} {{FormatByteSize .Size}} {{DateUtils.TimeSince .CreatedUnix}} - {{ctx.Locale.Tr "repo.settings.lfs_findcommits"}} - + +
    + {{ctx.Locale.Tr "repo.settings.lfs_findcommits"}} + +
    {{template "base/paginate" .}} - {{range .LFSFiles}} - - {{end}} + {{template "repo/settings/layout_footer" .}} diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl index 96148f57a9f..e03493596fd 100644 --- a/templates/repo/settings/lfs_file.tmpl +++ b/templates/repo/settings/lfs_file.tmpl @@ -12,13 +12,15 @@
    - {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} -
    + {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}} +
    {{if .IsFileTooLarge}} {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}} {{else if not .FileSize}} {{template "shared/fileisempty"}} - {{else if not .IsTextFile}} + {{else if .FileContentHTML}} + {{.FileContentHTML}} + {{else}}
    {{if .IsImageFile}} {{$.RawFileLink}} @@ -34,15 +36,6 @@ {{ctx.Locale.Tr "repo.file_view_raw"}} {{end}}
    - {{else if .FileSize}} - - - - - - - -
    {{.LineNums}}
    {{.FileContent}}
    {{end}}
    diff --git a/templates/repo/settings/lfs_pointers.tmpl b/templates/repo/settings/lfs_pointers.tmpl index 2a697aa88f7..89a541544c7 100644 --- a/templates/repo/settings/lfs_pointers.tmpl +++ b/templates/repo/settings/lfs_pointers.tmpl @@ -31,12 +31,13 @@ {{range .Pointers}} - + {{ShortSha .SHA}} - + {{$lfsObjectViewable := and .Exists .InRepo}} + {{ShortSha .Oid}} diff --git a/web_src/css/base.css b/web_src/css/base.css index 6fa5260d93e..58cbb1cd5ca 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -324,6 +324,12 @@ a.silenced:hover { text-decoration-line: none; } +a.disabled { + cursor: default; + pointer-events: none; + opacity: var(--opacity-disabled); +} + a.label, .ui .menu a, .ui.cards a.card { @@ -657,18 +663,6 @@ overflow-menu .ui.label:empty { vertical-align: top; } -.lines-code pre { - background-color: inherit; - margin: 0; - padding: 0 !important; -} - -.lines-code pre li { - display: block; - width: calc(100% - 1ch); - padding-left: 1ch; -} - .lines-escape { width: 0; white-space: nowrap; diff --git a/web_src/css/modules/button.css b/web_src/css/modules/button.css index 97a6ed60374..4efbe538e18 100644 --- a/web_src/css/modules/button.css +++ b/web_src/css/modules/button.css @@ -104,12 +104,12 @@ .ui.compact.buttons .button, .ui.compact.button { gap: var(--gap-inline); - padding: 0.42em /* around 8px */ 1.07em /* around 15px */; + padding: 6px 15px; min-height: 32px; } .ui.compact.icon.buttons .button, .ui.compact.icon.button { - padding: 0.57em /* around 8px */; + padding: 6px 8px; } /* reference size: mini: padding-x=16, height=30 ; compact: padding-x=12, height=26 */ diff --git a/web_src/css/repo.css b/web_src/css/repo.css index c1023d55a8c..ee9086c6ce3 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1780,6 +1780,7 @@ tbody.commit-list { padding: 10px; border: 1px solid transparent; border-bottom: none; + border-radius: 0; } .branch-selector-dropdown .branch-tag-item.active {