use textedit
This commit is contained in:
parent
5226e5e2c9
commit
d73a290d66
158
escape.go
158
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()
|
||||
}
|
||||
|
||||
6
go.mod
6
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
98
indent.go
98
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 },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
12
lib.go
12
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.
|
||||
|
||||
210
wrap.go
210
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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user