mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-09 17:37:20 -06:00
Every line of file, blame, diff and code preview views goes through `EscapeControlHTML`, which streamed it through a 32 KiB read buffer and copied every tag and text part. Views render the `EscapeStatus` before the content, so every caller holds the complete HTML and streaming can't help. It is now escaped in one pass over the string and returned unchanged when nothing needs escaping. - Skip text of printable ASCII, tabs and newlines, such lines return immediately - Render markup and wiki pages into a buffer instead of a pipe and goroutine, and remove `EscapeControlReader` - Analyze long text parts in windows overlapping by the detection context, which bounds memory without changing results - Escape invalid bytes before a tag, they were moved into the tag part unescaped and could hide a `<math>` tag | | before | after | |---|---|---| | ASCII line | 5.1 µs, 59 allocs | 0.08 µs, 1 alloc | | line with one `é` | 3.9 µs, 30 allocs | 0.29 µs, 3 allocs | | 150 KB document | 2.5 ms, 5.6 MB | 0.27 ms, 0.6 KB | --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: silverwind <me@silverwind.io>
177 lines
4.1 KiB
Go
177 lines
4.1 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package htmlutil
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// ParseSizeAndClass get size and class from string with default values
|
|
// If present, "others" expects the new size first and then the classes to use
|
|
func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int, string) {
|
|
size := defaultSize
|
|
if len(others) >= 1 {
|
|
if v, ok := others[0].(int); ok && v != 0 {
|
|
size = v
|
|
}
|
|
}
|
|
class := defaultClass
|
|
if len(others) >= 2 {
|
|
if v, ok := others[1].(string); ok && v != "" {
|
|
if class != "" {
|
|
class += " "
|
|
}
|
|
class += v
|
|
}
|
|
}
|
|
return size, class
|
|
}
|
|
|
|
func htmlFormatArgs(s template.HTML, rawArgs []any) []any {
|
|
if !strings.Contains(string(s), "%") || len(rawArgs) == 0 {
|
|
panic("HTMLFormat requires one or more arguments")
|
|
}
|
|
args := slices.Clone(rawArgs)
|
|
for i, v := range args {
|
|
switch v := v.(type) {
|
|
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
|
|
// for most basic types (including template.HTML which is safe), just do nothing and use it
|
|
case string:
|
|
args[i] = template.HTMLEscapeString(v)
|
|
case template.URL:
|
|
args[i] = template.HTMLEscapeString(string(v))
|
|
case fmt.Stringer:
|
|
args[i] = template.HTMLEscapeString(v.String())
|
|
default:
|
|
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
|
|
}
|
|
}
|
|
return args
|
|
}
|
|
|
|
func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML {
|
|
return template.HTML(fmt.Sprintf(string(s), htmlFormatArgs(s, rawArgs)...))
|
|
}
|
|
|
|
func HTMLPrintf(w io.Writer, s template.HTML, rawArgs ...any) (int, error) {
|
|
return fmt.Fprintf(w, string(s), htmlFormatArgs(s, rawArgs)...)
|
|
}
|
|
|
|
func HTMLPrint(w io.Writer, s template.HTML) (int, error) {
|
|
return io.WriteString(w, string(s))
|
|
}
|
|
|
|
func HTMLPrintTag(w io.Writer, tag template.HTML, attrs map[string]string) (written int, err error) {
|
|
n, err := io.WriteString(w, "<"+string(tag))
|
|
written += n
|
|
if err != nil {
|
|
return written, err
|
|
}
|
|
for k, v := range attrs {
|
|
n, err = fmt.Fprintf(w, ` %s="%s"`, template.HTMLEscapeString(k), template.HTMLEscapeString(v))
|
|
written += n
|
|
if err != nil {
|
|
return written, err
|
|
}
|
|
}
|
|
n, err = io.WriteString(w, ">")
|
|
written += n
|
|
return written, err
|
|
}
|
|
|
|
func EscapeString(s string) template.HTML {
|
|
return template.HTML(template.HTMLEscapeString(s))
|
|
}
|
|
|
|
type HTMLWriter interface {
|
|
Err() error
|
|
OriginWriter() io.Writer
|
|
WriteString(s string) HTMLWriter
|
|
WriteHTML(s template.HTML) HTMLWriter
|
|
WriteFormatf(fmt template.HTML, args ...any) HTMLWriter
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (h *htmlWriter) WriteString(s string) HTMLWriter {
|
|
if _, err := io.WriteString(h.w, template.HTMLEscapeString(s)); err != nil {
|
|
h.errs = append(h.errs, err)
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (h *htmlWriter) WriteHTML(s template.HTML) HTMLWriter {
|
|
if _, err := io.WriteString(h.w, string(s)); err != nil {
|
|
h.errs = append(h.errs, err)
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (h *htmlWriter) WriteFormatf(fmt template.HTML, args ...any) HTMLWriter {
|
|
if _, err := HTMLPrintf(h.w, fmt, args...); err != nil {
|
|
h.errs = append(h.errs, err)
|
|
}
|
|
return h
|
|
}
|
|
|
|
func NewHTMLWriter(w io.Writer) HTMLWriter {
|
|
return &htmlWriter{w: w}
|
|
}
|
|
|
|
type HTMLBuilder struct {
|
|
sb strings.Builder
|
|
}
|
|
|
|
func (b *HTMLBuilder) Err() error {
|
|
return nil
|
|
}
|
|
|
|
func (b *HTMLBuilder) OriginWriter() io.Writer {
|
|
return &b.sb
|
|
}
|
|
|
|
func (b *HTMLBuilder) WriteString(s string) HTMLWriter {
|
|
b.sb.WriteString(template.HTMLEscapeString(s))
|
|
return b
|
|
}
|
|
|
|
func (b *HTMLBuilder) WriteHTML(s template.HTML) HTMLWriter {
|
|
b.sb.WriteString(string(s))
|
|
return b
|
|
}
|
|
|
|
func (b *HTMLBuilder) WriteFormatf(fmt template.HTML, args ...any) HTMLWriter {
|
|
_, _ = HTMLPrintf(&b.sb, fmt, args...)
|
|
return b
|
|
}
|
|
|
|
func (b *HTMLBuilder) HTMLString() template.HTML {
|
|
return template.HTML(b.sb.String())
|
|
}
|
|
|
|
func (b *HTMLBuilder) String() string {
|
|
return b.sb.String()
|
|
}
|