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

158
escape.go
View File

@ -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("&lt;")...)
case '>':
w.write([]rune("&gt;")...)
case '&':
w.write([]rune("&amp;")...)
case unicodeNBSP:
w.write([]rune("&nbsp;")...)
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("&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 {
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("&quot;")...)
case '&':
rr = append(rr, []rune("&amp;")...)
default:
rr = append(rr, r[i])
}
}
var b bytes.Buffer
w := textedit.New(
&b,
textedit.Replace(
"&", "&amp;",
"\"", "&quot;",
),
)
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
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

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/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
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 },
),
)
}

12
lib.go
View File

@ -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.

210
wrap.go
View File

@ -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
}