From d73a290d66836b93ddc34b08d77bfb7f02a69ec9 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Sat, 1 Nov 2025 21:23:56 +0100 Subject: [PATCH] use textedit --- escape.go | 158 +++++++++++++--------------------------- go.mod | 6 +- go.sum | 2 + indent.go | 98 ++++++------------------- lib.go | 12 ++-- wrap.go | 210 ++++++++++++++++++++++++------------------------------ 6 files changed, 176 insertions(+), 310 deletions(-) diff --git a/escape.go b/escape.go index b6a8e20..ac14b14 100644 --- a/escape.go +++ b/escape.go @@ -1,113 +1,55 @@ package html import ( - "bufio" "bytes" - "errors" + "code.squareroundforest.org/arpio/textedit" "io" - "unicode" ) const unicodeNBSP = 0xa0 -type escapeWriter struct { - out *bufio.Writer - nonbreakSpaces bool - spaceStarted, lastSpace bool - err error -} - -func newEscapeWriter(out io.Writer, nonbreakSpaces bool) *escapeWriter { - return &escapeWriter{ - out: bufio.NewWriter(out), - nonbreakSpaces: nonbreakSpaces, - } -} - -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 err != nil { - w.err = err - return len(p), w.err - } - - if r == unicode.ReplacementChar { - continue - } - - space := w.nonbreakSpaces && 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 - } - +func escapeEdit(nonbreakSpaces bool) func(rune, []rune) ([]rune, []rune) { + return func(r rune, state []rune) ([]rune, []rune) { + var rr []rune switch r { - case '<': - w.write([]rune("<")...) - case '>': - w.write([]rune(">")...) - case '&': - w.write([]rune("&")...) - case unicodeNBSP: - w.write([]rune(" ")...) - default: - w.write(r) - } - } + case ' ': + if !nonbreakSpaces { + return []rune{r}, nil + } - return len(p), w.err + if len(state) == 0 { + return nil, []rune{' '} + } + + return []rune(" "), []rune(" ") + case '<': + rr = []rune("<") + case '>': + rr = []rune(">") + case '&': + rr = []rune("&") + case unicodeNBSP: + rr = []rune(" ") + default: + rr = []rune{r} + } + + return append(state, rr...), nil + } } -func (w *escapeWriter) Flush() error { - if w.err != nil { - return w.err - } +func escapeReleaseState(state []rune) []rune { + return state +} - if w.spaceStarted { - w.write(' ') - w.spaceStarted = false - } - - w.err = w.out.Flush() - return w.err +func newEscapeWriter(out io.Writer, nonbreakSpaces bool) *textedit.Writer { + return textedit.New( + out, + textedit.Func( + escapeEdit(nonbreakSpaces), + escapeReleaseState, + ), + ) } func escape(s string, nonbreakSpaces bool) string { @@ -119,18 +61,16 @@ func escape(s string, nonbreakSpaces bool) string { } func escapeAttribute(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]) - } - } + var b bytes.Buffer + w := textedit.New( + &b, + textedit.Replace( + "&", "&", + "\"", """, + ), + ) - return string(rr) + w.Write([]byte(value)) + // w.Flush() + return b.String() } diff --git a/go.mod b/go.mod index f35aeec..95ad847 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,9 @@ module code.squareroundforest.org/arpio/html -go 1.25.0 +go 1.25.3 require code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 + +require code.squareroundforest.org/arpio/textedit v0.0.0-20251101195945-0569369ef5a7 + +replace code.squareroundforest.org/arpio/textedit => ../textedit diff --git a/go.sum b/go.sum index a66f3d0..a0dbdb6 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I= code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= +code.squareroundforest.org/arpio/textedit v0.0.0-20251101195945-0569369ef5a7 h1:+pvm8bXYg7hn7ATyC1REYTeFWvrp5Ky7tFgyscB0KWo= +code.squareroundforest.org/arpio/textedit v0.0.0-20251101195945-0569369ef5a7/go.mod h1:nXdFdxdI69JrkIT97f+AEE4OgplmxbgNFZC5j7gsdqs= diff --git a/indent.go b/indent.go index 7878271..6b95407 100644 --- a/indent.go +++ b/indent.go @@ -1,18 +1,12 @@ package html import ( - "bufio" - "bytes" - "errors" + "code.squareroundforest.org/arpio/textedit" "io" - "unicode" ) -type indentWriter struct { - out *bufio.Writer - indent string +type indentState struct { started, lineStarted bool - err error } func indentLen(indent string) int { @@ -30,79 +24,33 @@ func indentLen(indent string) int { return l } -func newIndentWriter(out io.Writer, indent string) *indentWriter { - return &indentWriter{ - out: bufio.NewWriter(out), - indent: indent, - } -} - -func (w *indentWriter) write(r ...rune) { - if w.err != nil { - return - } - - for _, ri := range r { - if _, w.err = w.out.WriteRune(ri); w.err != nil { - return - } - } -} - -func (w *indentWriter) Write(p []byte) (int, error) { - if w.err != nil { - return 0, w.err - } - - if len(p) == 0 { - return 0, nil - } - - 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 err != nil { - w.err = err - return len(p), w.err - } - - if r == unicode.ReplacementChar { - continue - } - +func indentEdit(indent []rune) func(rune, indentState) ([]rune, indentState) { + return func(r rune, s indentState) ([]rune, indentState) { + var ret []rune if r == '\n' { - w.write('\n') - w.started = true - w.lineStarted = false - continue + s.started = true + s.lineStarted = false + ret = append(ret, '\n') + return ret, s } - if w.started && !w.lineStarted { - w.write([]rune(w.indent)...) + if s.started && !s.lineStarted { + ret = append(ret, indent...) } - w.write(r) - w.started = true - w.lineStarted = true + ret = append(ret, r) + s.started = true + s.lineStarted = true + return ret, s } - - return len(p), w.err } -func (w *indentWriter) Flush() error { - if w.err != nil { - return w.err - } - - w.err = w.out.Flush() - return w.err +func newIndentWriter(out io.Writer, indent string) *textedit.Writer { + return textedit.New( + out, + textedit.Func( + indentEdit([]rune(indent)), + func(indentState) []rune { return nil }, + ), + ) } diff --git a/lib.go b/lib.go index 91cfe4f..b543582 100644 --- a/lib.go +++ b/lib.go @@ -343,12 +343,6 @@ func Void(t Tag) Tag { return t()(renderGuide{void: true}) } -// Indent provides default indentation with the tabs as indentation string, a paragraph with of 120 and a -// minimum paragraph with of 60. Indentation and with concerns only the HTML text, not the displayed document. -func Indent() Indentation { - return Indentation{Indent: "\t"} -} - // WriteIndent renders html with t as the root nodes using the specified indentation and wrapping. // // Non-tag child nodes are rendered via fmt.Sprint. Consecutive spaces are considered to be so on purpose, and @@ -386,9 +380,11 @@ func WriteIndent(out io.Writer, indent Indentation, t ...Tag) error { return nil } -// Write renders html with t as the root nodes with the default indentation and wrapping. +// Write renders html with t as the root nodes with the default indentation and wrapping. Tabs are used as +// indentation string, and a paragraph with of 120. The minimum paragraph width is 60. Indentation and width +// concerns only the HTML text, not the displayed document. func Write(out io.Writer, t ...Tag) error { - return WriteIndent(out, Indent(), t...) + return WriteIndent(out, Indentation{Indent: "\t"}, t...) } // WriteRaw renders html with t as the root nodes without indentation or wrapping. diff --git a/wrap.go b/wrap.go index 997cedd..1b7a71a 100644 --- a/wrap.go +++ b/wrap.go @@ -1,171 +1,147 @@ package html import ( - "bytes" - "errors" + "code.squareroundforest.org/arpio/textedit" "io" "unicode" ) -type wrapper struct { - out *indentWriter - width int - line, word *bytes.Buffer +type wrapState struct { + line, word []rune inWord, inTag, inSingleQuote, inQuote bool lastSpace, started bool - err error } -func newWrapper(out io.Writer, width int, indent string) *wrapper { - return &wrapper{ - out: newIndentWriter(out, indent), - width: width, - line: bytes.NewBuffer(nil), - word: bytes.NewBuffer(nil), - } +type wrapper struct { + indent *textedit.Writer + wrap *textedit.Writer } -func (w *wrapper) feed() error { - withSpace := w.lastSpace && w.line.Len() > 0 - l := w.line.Len() + w.word.Len() - if withSpace && w.word.Len() > 0 { +func wrapFeed(width int, s wrapState) ([]rune, wrapState) { + var ret []rune + withSpace := s.lastSpace && len(s.line) > 0 + l := len(s.line) + len(s.word) + if withSpace && len(s.word) > 0 { l++ } - feedLine := l > w.width && w.line.Len() > 0 + feedLine := l > width && len(s.line) > 0 if feedLine { - if w.started { - if _, err := w.out.Write([]byte{'\n'}); err != nil { - return err - } + if s.started { + ret = append(ret, '\n') } - if _, err := io.Copy(w.out, w.line); err != nil { - return err - } - - w.line.Reset() - w.started = true + ret = append(ret, s.line...) + s.line = nil + s.started = true } if !feedLine && withSpace { - w.line.WriteRune(' ') + s.line = append(s.line, ' ') } - io.Copy(w.line, w.word) - w.word.Reset() - return nil + s.line = append(s.line, s.word...) + s.word = nil + return ret, s } -func (w *wrapper) 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 +func wrapEdit(width int) func(rune, wrapState) ([]rune, wrapState) { + return func(r rune, s wrapState) ([]rune, wrapState) { + var ret []rune + if s.inSingleQuote { + s.inSingleQuote = r != '\'' + s.word = append(s.word, r) + return ret, s } - if err != nil { - w.err = err - return len(p), w.err + if s.inQuote { + s.inQuote = r != '"' + s.word = append(s.word, r) + return ret, s } - if r == unicode.ReplacementChar { - continue - } - - if w.inSingleQuote { - w.inSingleQuote = r != '\'' - w.word.WriteRune(r) - continue - } - - if w.inQuote { - w.inQuote = r != '"' - w.word.WriteRune(r) - continue - } - - if w.inTag { - w.inSingleQuote = r == '\'' - w.inQuote = r == '"' - w.inTag = r != '>' - w.inWord = !w.inTag && !unicode.IsSpace(r) - if w.inTag || !unicode.IsSpace(r) { - w.word.WriteRune(r) + if s.inTag { + s.inSingleQuote = r == '\'' + s.inQuote = r == '"' + s.inTag = r != '>' + s.inWord = !s.inTag && !unicode.IsSpace(r) + if s.inTag || !unicode.IsSpace(r) { + s.word = append(s.word, r) } - if !w.inTag { - if err := w.feed(); err != nil { - w.err = err - return len(p), w.err - } - - w.lastSpace = unicode.IsSpace(r) + if !s.inTag { + ret, s = wrapFeed(width, s) + s.lastSpace = unicode.IsSpace(r) } - continue + return ret, s } - if w.inWord { - w.inTag = r == '<' - w.inWord = !w.inTag && !unicode.IsSpace(r) - if !w.inWord { - if err := w.feed(); err != nil { - w.err = err - return len(p), w.err - } - - w.lastSpace = unicode.IsSpace(r) + if s.inWord { + s.inTag = r == '<' + s.inWord = !s.inTag && !unicode.IsSpace(r) + if !s.inWord { + ret, s = wrapFeed(width, s) + s.lastSpace = unicode.IsSpace(r) } - if w.inWord || w.inTag { - w.word.WriteRune(r) + if s.inWord || s.inTag { + s.word = append(s.word, r) } - continue + return ret, s } if unicode.IsSpace(r) { - w.lastSpace = true - continue + s.lastSpace = true + return ret, s } - w.word.WriteRune(r) - w.inTag = r == '<' - w.inWord = !w.inTag + s.word = append(s.word, r) + s.inTag = r == '<' + s.inWord = !s.inTag + return ret, s } +} - return len(p), w.err +func wrapReleaseState(width int) func(wrapState) []rune { + return func(s wrapState) []rune { + var ret []rune + if s.inTag || s.inWord { + ret, s = wrapFeed(width, s) + } + + ret1, _ := wrapFeed(0, s) + return append(ret, ret1...) + } +} + +func newWrapper(out io.Writer, width int, indent string) *wrapper { + indentWriter := newIndentWriter(out, indent) + return &wrapper{ + indent: indentWriter, + wrap: textedit.New( + indentWriter, + textedit.Func( + wrapEdit(width), + wrapReleaseState(width), + ), + ), + } +} + +func (w *wrapper) Write(p []byte) (int, error) { + return w.wrap.Write(p) } func (w *wrapper) Flush() error { - if w.err != nil { - return w.err - } - - if w.inTag || w.inWord { - if err := w.feed(); err != nil { - w.err = err - return err - } - } - - w.width = 0 - if err := w.feed(); err != nil { - w.err = err + if err := w.wrap.Flush(); err != nil { return err } - w.err = w.out.Flush() - return w.err + if err := w.indent.Flush(); err != nil { + return err + } + + return nil }