1
0

use textedit

This commit is contained in:
Arpad Ryszka 2025-11-01 21:23:56 +01:00
parent 5226e5e2c9
commit d73a290d66
6 changed files with 176 additions and 310 deletions

156
escape.go
View File

@ -1,113 +1,55 @@
package html package html
import ( import (
"bufio"
"bytes" "bytes"
"errors" "code.squareroundforest.org/arpio/textedit"
"io" "io"
"unicode"
) )
const unicodeNBSP = 0xa0 const unicodeNBSP = 0xa0
type escapeWriter struct { func escapeEdit(nonbreakSpaces bool) func(rune, []rune) ([]rune, []rune) {
out *bufio.Writer return func(r rune, state []rune) ([]rune, []rune) {
nonbreakSpaces bool var rr []rune
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
}
switch r { switch r {
case '<': case ' ':
w.write([]rune("&lt;")...) if !nonbreakSpaces {
case '>': return []rune{r}, nil
w.write([]rune("&gt;")...)
case '&':
w.write([]rune("&amp;")...)
case unicodeNBSP:
w.write([]rune("&nbsp;")...)
default:
w.write(r)
}
} }
return len(p), w.err if len(state) == 0 {
return nil, []rune{' '}
}
return []rune("&nbsp;"), []rune("&nbsp;")
case '<':
rr = []rune("&lt;")
case '>':
rr = []rune("&gt;")
case '&':
rr = []rune("&amp;")
case unicodeNBSP:
rr = []rune("&nbsp;")
default:
rr = []rune{r}
}
return append(state, rr...), nil
}
} }
func (w *escapeWriter) Flush() error { func escapeReleaseState(state []rune) []rune {
if w.err != nil { return state
return w.err }
}
if w.spaceStarted { func newEscapeWriter(out io.Writer, nonbreakSpaces bool) *textedit.Writer {
w.write(' ') return textedit.New(
w.spaceStarted = false out,
} textedit.Func(
escapeEdit(nonbreakSpaces),
w.err = w.out.Flush() escapeReleaseState,
return w.err ),
)
} }
func escape(s string, nonbreakSpaces bool) string { func escape(s string, nonbreakSpaces bool) string {
@ -119,18 +61,16 @@ func escape(s string, nonbreakSpaces bool) string {
} }
func escapeAttribute(value string) string { func escapeAttribute(value string) string {
var rr []rune var b bytes.Buffer
r := []rune(value) w := textedit.New(
for i := range r { &b,
switch r[i] { textedit.Replace(
case '"': "&", "&amp;",
rr = append(rr, []rune("&quot;")...) "\"", "&quot;",
case '&': ),
rr = append(rr, []rune("&amp;")...) )
default:
rr = append(rr, r[i])
}
}
return string(rr) w.Write([]byte(value))
// w.Flush()
return b.String()
} }

6
go.mod
View File

@ -1,5 +1,9 @@
module code.squareroundforest.org/arpio/html 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/notation v0.0.0-20250826181910-5140794b16b2
require code.squareroundforest.org/arpio/textedit v0.0.0-20251101195945-0569369ef5a7
replace code.squareroundforest.org/arpio/textedit => ../textedit

2
go.sum
View File

@ -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 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I=
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= 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=

View File

@ -1,18 +1,12 @@
package html package html
import ( import (
"bufio" "code.squareroundforest.org/arpio/textedit"
"bytes"
"errors"
"io" "io"
"unicode"
) )
type indentWriter struct { type indentState struct {
out *bufio.Writer
indent string
started, lineStarted bool started, lineStarted bool
err error
} }
func indentLen(indent string) int { func indentLen(indent string) int {
@ -30,79 +24,33 @@ func indentLen(indent string) int {
return l return l
} }
func newIndentWriter(out io.Writer, indent string) *indentWriter { func indentEdit(indent []rune) func(rune, indentState) ([]rune, indentState) {
return &indentWriter{ return func(r rune, s indentState) ([]rune, indentState) {
out: bufio.NewWriter(out), var ret []rune
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
}
if r == '\n' { if r == '\n' {
w.write('\n') s.started = true
w.started = true s.lineStarted = false
w.lineStarted = false ret = append(ret, '\n')
continue return ret, s
} }
if w.started && !w.lineStarted { if s.started && !s.lineStarted {
w.write([]rune(w.indent)...) ret = append(ret, indent...)
} }
w.write(r) ret = append(ret, r)
w.started = true s.started = true
w.lineStarted = true s.lineStarted = true
return ret, s
} }
return len(p), w.err
} }
func (w *indentWriter) Flush() error { func newIndentWriter(out io.Writer, indent string) *textedit.Writer {
if w.err != nil { return textedit.New(
return w.err out,
} textedit.Func(
indentEdit([]rune(indent)),
w.err = w.out.Flush() func(indentState) []rune { return nil },
return w.err ),
)
} }

12
lib.go
View File

@ -343,12 +343,6 @@ func Void(t Tag) Tag {
return t()(renderGuide{void: true}) 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. // 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 // 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 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 { 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. // WriteRaw renders html with t as the root nodes without indentation or wrapping.

210
wrap.go
View File

@ -1,171 +1,147 @@
package html package html
import ( import (
"bytes" "code.squareroundforest.org/arpio/textedit"
"errors"
"io" "io"
"unicode" "unicode"
) )
type wrapper struct { type wrapState struct {
out *indentWriter line, word []rune
width int
line, word *bytes.Buffer
inWord, inTag, inSingleQuote, inQuote bool inWord, inTag, inSingleQuote, inQuote bool
lastSpace, started bool lastSpace, started bool
err error
} }
func newWrapper(out io.Writer, width int, indent string) *wrapper { type wrapper struct {
return &wrapper{ indent *textedit.Writer
out: newIndentWriter(out, indent), wrap *textedit.Writer
width: width,
line: bytes.NewBuffer(nil),
word: bytes.NewBuffer(nil),
}
} }
func (w *wrapper) feed() error { func wrapFeed(width int, s wrapState) ([]rune, wrapState) {
withSpace := w.lastSpace && w.line.Len() > 0 var ret []rune
l := w.line.Len() + w.word.Len() withSpace := s.lastSpace && len(s.line) > 0
if withSpace && w.word.Len() > 0 { l := len(s.line) + len(s.word)
if withSpace && len(s.word) > 0 {
l++ l++
} }
feedLine := l > w.width && w.line.Len() > 0 feedLine := l > width && len(s.line) > 0
if feedLine { if feedLine {
if w.started { if s.started {
if _, err := w.out.Write([]byte{'\n'}); err != nil { ret = append(ret, '\n')
return err
}
} }
if _, err := io.Copy(w.out, w.line); err != nil { ret = append(ret, s.line...)
return err s.line = nil
} s.started = true
w.line.Reset()
w.started = true
} }
if !feedLine && withSpace { if !feedLine && withSpace {
w.line.WriteRune(' ') s.line = append(s.line, ' ')
} }
io.Copy(w.line, w.word) s.line = append(s.line, s.word...)
w.word.Reset() s.word = nil
return nil return ret, s
} }
func (w *wrapper) Write(p []byte) (int, error) { func wrapEdit(width int) func(rune, wrapState) ([]rune, wrapState) {
if w.err != nil { return func(r rune, s wrapState) ([]rune, wrapState) {
return 0, w.err var ret []rune
if s.inSingleQuote {
s.inSingleQuote = r != '\''
s.word = append(s.word, r)
return ret, s
} }
runes := bytes.NewBuffer(nil) if s.inQuote {
if n, err := runes.Write(p); err != nil { s.inQuote = r != '"'
w.err = err s.word = append(s.word, r)
return n, w.err return ret, s
} }
for { if s.inTag {
r, _, err := runes.ReadRune() s.inSingleQuote = r == '\''
if errors.Is(err, io.EOF) { s.inQuote = r == '"'
return len(p), nil s.inTag = r != '>'
s.inWord = !s.inTag && !unicode.IsSpace(r)
if s.inTag || !unicode.IsSpace(r) {
s.word = append(s.word, r)
} }
if err != nil { if !s.inTag {
w.err = err ret, s = wrapFeed(width, s)
return len(p), w.err s.lastSpace = unicode.IsSpace(r)
} }
if r == unicode.ReplacementChar { return ret, s
continue
} }
if w.inSingleQuote { if s.inWord {
w.inSingleQuote = r != '\'' s.inTag = r == '<'
w.word.WriteRune(r) s.inWord = !s.inTag && !unicode.IsSpace(r)
continue if !s.inWord {
ret, s = wrapFeed(width, s)
s.lastSpace = unicode.IsSpace(r)
} }
if w.inQuote { if s.inWord || s.inTag {
w.inQuote = r != '"' s.word = append(s.word, r)
w.word.WriteRune(r)
continue
} }
if w.inTag { return ret, s
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 !w.inTag {
if err := w.feed(); err != nil {
w.err = err
return len(p), w.err
}
w.lastSpace = unicode.IsSpace(r)
}
continue
}
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 w.inWord || w.inTag {
w.word.WriteRune(r)
}
continue
} }
if unicode.IsSpace(r) { if unicode.IsSpace(r) {
w.lastSpace = true s.lastSpace = true
continue return ret, s
} }
w.word.WriteRune(r) s.word = append(s.word, r)
w.inTag = r == '<' s.inTag = r == '<'
w.inWord = !w.inTag s.inWord = !s.inTag
return ret, s
}
}
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)
} }
return len(p), w.err 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 { func (w *wrapper) Flush() error {
if w.err != nil { if err := w.wrap.Flush(); 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
return err return err
} }
w.err = w.out.Flush() if err := w.indent.Flush(); err != nil {
return w.err return err
}
return nil
} }