package html import ( "io" "bufio" "bytes" "errors" "unicode" ) const unicodeNBSP = 0xa0 type escapeWriter struct { out *bufio.Writer spaceStarted, lastSpace bool err error } func attributeEscape(value string) string { var rr []rune r := []rune(value) for i := range r { switch r[i] { case '"': rr = append(rr, []rune(""")...) case '&': rr = append(rr, []rune("&")...) default: rr = append(rr, r[i]) } } return string(rr) } func newEscapeWriter(out io.Writer) *escapeWriter { return &escapeWriter{out: bufio.NewWriter(out)} } func (w *escapeWriter) write(r ...rune) { if w.err != nil { return } for _, ri := range r { if _, err := w.out.WriteRune(ri); err != nil { w.err = err return } } } func (w *escapeWriter) Write(p []byte) (int, error) { if w.err != nil { return 0, w.err } runes := bytes.NewBuffer(nil) if n, err := runes.Write(p); err != nil { w.err = err return n, w.err } for { r, _, err := runes.ReadRune() if errors.Is(err, io.EOF) { return len(p), nil } if r == unicode.ReplacementChar { continue } space := r == ' ' switch { case space && w.spaceStarted: w.write([]rune("  ")...) case space && w.lastSpace: w.write([]rune(" ")...) case w.spaceStarted: w.write(' ') } w.spaceStarted = space && !w.lastSpace w.lastSpace = space if space { continue } switch r { case '<': w.write([]rune("<")...) case '>': w.write([]rune(">")...) case '&': w.write([]rune("&")...) case unicodeNBSP: w.write([]rune(" ")...) default: w.write(r) } } return len(p), w.err } func (w *escapeWriter) Flush() error { if w.err != nil { return w.err } if w.spaceStarted { w.write(' ') w.spaceStarted = false } w.err = w.out.Flush() return w.err }