From 2f496f4ca2a9889dfdabb1f18cf25b0bc4f49b54 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Sun, 5 Oct 2025 21:25:53 +0200 Subject: [PATCH] support reader as content --- escape.go | 13 ++-- escape_test.go | 2 +- indent.go | 92 ++++++++++++++++++++++++ lib_test.go | 2 +- print_test.go | 2 +- render.go | 143 +++++++++++++++++++++++++++++++++---- tags/block.gen.go | 2 + tags/inline.gen.go | 2 + tags/inlinechildren.gen.go | 2 + tags/promote.gen.go | 2 + tags/script.gen.go | 2 + tags/void.block.gen.go | 2 + tags/void.inline.gen.go | 2 + wrap.go | 24 +++---- writer_test.go | 6 +- 15 files changed, 262 insertions(+), 36 deletions(-) create mode 100644 indent.go diff --git a/escape.go b/escape.go index 2b9e96d..2454fae 100644 --- a/escape.go +++ b/escape.go @@ -1,19 +1,19 @@ package html import ( - "io" "bufio" "bytes" "errors" + "io" "unicode" ) -const unicodeNBSP = 0xa0 +const unicodeNBSP = 0xa0 type escapeWriter struct { - out *bufio.Writer + out *bufio.Writer spaceStarted, lastSpace bool - err error + err error } func attributeEscape(value string) string { @@ -67,6 +67,11 @@ func (w *escapeWriter) Write(p []byte) (int, error) { return len(p), nil } + if err != nil { + w.err = err + return len(p), w.err + } + if r == unicode.ReplacementChar { continue } diff --git a/escape_test.go b/escape_test.go index 553c281..c704fd7 100644 --- a/escape_test.go +++ b/escape_test.go @@ -1,10 +1,10 @@ package html_test import ( + "bytes" "code.squareroundforest.org/arpio/html" . "code.squareroundforest.org/arpio/html/tags" "testing" - "bytes" ) func TestEscape(t *testing.T) { diff --git a/indent.go b/indent.go new file mode 100644 index 0000000..ea4244a --- /dev/null +++ b/indent.go @@ -0,0 +1,92 @@ +package html + +import ( + "bufio" + "bytes" + "errors" + "io" + "unicode" +) + +type indentWriter struct { + out *bufio.Writer + indent string + started, lineStarted bool + err error +} + +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 + } + + if r == '\n' { + w.write('\n') + w.lineStarted = false + continue + } + + if w.started && !w.lineStarted { + w.write([]rune(w.indent)...) + } + + w.write(r) + w.started = true + w.lineStarted = true + } + + 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 +} diff --git a/lib_test.go b/lib_test.go index d7231d1..4c4a98f 100644 --- a/lib_test.go +++ b/lib_test.go @@ -4,8 +4,8 @@ import ( "bytes" "code.squareroundforest.org/arpio/html" . "code.squareroundforest.org/arpio/html/tags" - "testing" "code.squareroundforest.org/arpio/notation" + "testing" ) func TestLib(t *testing.T) { diff --git a/print_test.go b/print_test.go index c1408d0..11bdddd 100644 --- a/print_test.go +++ b/print_test.go @@ -1,8 +1,8 @@ package html_test import ( - "fmt" "code.squareroundforest.org/arpio/notation" + "fmt" ) func printBytes(a ...any) { diff --git a/render.go b/render.go index 650fc49..3720640 100644 --- a/render.go +++ b/render.go @@ -3,7 +3,6 @@ package html import ( "fmt" "io" - "strings" ) const defaultPWidth = 112 @@ -38,15 +37,6 @@ func mergeRenderingGuides(rgs []renderGuide) renderGuide { return rg } -func indentLines(indent string, s string) string { - l := strings.Split(s, "\n") - for i := range l { - l[i] = fmt.Sprintf("%s%s", indent, l[i]) - } - - return strings.Join(l, "\n") -} - func (r *renderer) getPrintf(tagName string) func(f string, a ...any) { return func(f string, a ...any) { if r.err != nil { @@ -93,6 +83,22 @@ func (r *renderer) renderUnindented(name string, rg renderGuide, a []Attributes, continue } + if rd, ok := c.(io.Reader); ok { + if rg.verbatim || rg.script { + _, r.err = io.Copy(r.out, rd) + continue + } + + ew := newEscapeWriter(r.out) + _, r.err = io.Copy(ew, rd) + if r.err != nil { + ew.Flush() + r.err = ew.err + } + + continue + } + s := fmt.Sprint(c) if s == "" { continue @@ -100,6 +106,7 @@ func (r *renderer) renderUnindented(name string, rg renderGuide, a []Attributes, if rg.verbatim || rg.script { printf(s) + continue } if r.err == nil { @@ -152,6 +159,54 @@ func (r *renderer) renderInline(name string, rg renderGuide, a []Attributes, chi var lastBlock bool for _, c := range children { + rd, isReader := c.(io.Reader) + if isReader && rg.verbatim { + r.clearWrapper() + if r.err == nil { + iw := newIndentWriter(r.out, r.currentIndent+r.indent.Indent) + iw.Write([]byte{'\n'}) + iw.Write([]byte(r.currentIndent + r.indent.Indent)) + _, r.err = io.Copy(iw, rd) + if r.err == nil { + iw.Flush() + r.err = iw.err + } + } + + lastBlock = true + continue + } + + if isReader && rg.script { + r.clearWrapper() + printf("\n") + _, r.err = io.Copy(r.out, rd) + lastBlock = true + continue + } + + if isReader { + if lastBlock { + printf("\n%s", r.currentIndent) + } + + if r.ensureWrapper() { + newWrapper = true + } + + if r.err == nil { + ew := newEscapeWriter(r.out) + _, r.err = io.Copy(ew, rd) + if r.err == nil { + ew.Flush() + r.err = ew.err + } + } + + lastBlock = false + continue + } + ct, isTag := c.(Tag) if !isTag && rg.verbatim { if c == nil { @@ -164,8 +219,15 @@ func (r *renderer) renderInline(name string, rg renderGuide, a []Attributes, chi } r.clearWrapper() - s = indentLines(r.currentIndent+r.indent.Indent, s) - printf("\n%s", s) + if r.err == nil { + iw := newIndentWriter(r.out, r.currentIndent+r.indent.Indent) + iw.Write([]byte{'\n'}) + iw.Write([]byte(r.currentIndent + r.indent.Indent)) + iw.Write([]byte(s)) + iw.Flush() + r.err = iw.err + } + lastBlock = true continue } @@ -283,6 +345,52 @@ func (r *renderer) renderBlock(name string, rg renderGuide, a []Attributes, chil } for _, c := range children { + rd, isReader := c.(io.Reader) + if isReader && rg.verbatim { + r.clearWrapper() + printf("\n") + if r.err == nil { + iw := newIndentWriter(r.out, r.currentIndent+r.indent.Indent) + iw.Write([]byte{'\n'}) + iw.Write([]byte(r.currentIndent + r.indent.Indent)) + _, r.err = io.Copy(iw, rd) + if r.err == nil { + iw.Flush() + r.err = iw.err + } + } + + lastBlock = true + continue + } + + if isReader && rg.script { + r.clearWrapper() + printf("\n") + _, r.err = io.Copy(r.out, rd) + lastBlock = true + continue + } + + if isReader { + if lastBlock { + printf("\n%s", r.currentIndent) + } + + r.ensureWrapper() + if r.err == nil { + ew := newEscapeWriter(r.out) + _, r.err = io.Copy(ew, rd) + if r.err == nil { + ew.Flush() + r.err = ew.err + } + } + + lastBlock = false + continue + } + ct, isTag := c.(Tag) if !isTag && rg.verbatim { if c == nil { @@ -295,8 +403,15 @@ func (r *renderer) renderBlock(name string, rg renderGuide, a []Attributes, chil } r.clearWrapper() - s = indentLines(r.currentIndent, s) - printf("\n%s", s) + if r.err == nil { + iw := newIndentWriter(r.out, r.currentIndent) + iw.Write([]byte{'\n'}) + iw.Write([]byte(r.currentIndent + r.indent.Indent)) + iw.Write([]byte(s)) + iw.Flush() + r.err = iw.err + } + lastBlock = true continue } diff --git a/tags/block.gen.go b/tags/block.gen.go index 974da0d..b94b0aa 100644 --- a/tags/block.gen.go +++ b/tags/block.gen.go @@ -1,7 +1,9 @@ // generated by ../script/generate-tags.go package tags + import "code.squareroundforest.org/arpio/html" + var Address = html.Define("address") var Article = html.Define("article") var Audio = html.Define("audio") diff --git a/tags/inline.gen.go b/tags/inline.gen.go index 15763ec..6d84356 100644 --- a/tags/inline.gen.go +++ b/tags/inline.gen.go @@ -1,7 +1,9 @@ // generated by ../script/generate-tags.go package tags + import "code.squareroundforest.org/arpio/html" + var A = html.Inline(html.Define("a")) var Abbr = html.Inline(html.Define("abbr")) var B = html.Inline(html.Define("b")) diff --git a/tags/inlinechildren.gen.go b/tags/inlinechildren.gen.go index 9c18cd3..0b0a993 100644 --- a/tags/inlinechildren.gen.go +++ b/tags/inlinechildren.gen.go @@ -1,7 +1,9 @@ // generated by ../script/generate-tags.go package tags + import "code.squareroundforest.org/arpio/html" + var H1 = html.InlineChildren(html.Define("h1")) var H2 = html.InlineChildren(html.Define("h2")) var H3 = html.InlineChildren(html.Define("h3")) diff --git a/tags/promote.gen.go b/tags/promote.gen.go index fe6c521..3893eb2 100644 --- a/tags/promote.gen.go +++ b/tags/promote.gen.go @@ -1,7 +1,9 @@ // generated by ../script/promote-to-tags.go package tags + import "code.squareroundforest.org/arpio/html" + var Attr = html.Attr var Define = html.Define var Doctype = html.Doctype diff --git a/tags/script.gen.go b/tags/script.gen.go index 06f0399..cec0a0d 100644 --- a/tags/script.gen.go +++ b/tags/script.gen.go @@ -1,6 +1,8 @@ // generated by ../script/generate-tags.go package tags + import "code.squareroundforest.org/arpio/html" + var Script = html.ScriptContent(html.Define("script")) var Style = html.ScriptContent(html.Define("style")) diff --git a/tags/void.block.gen.go b/tags/void.block.gen.go index 2df6aef..9ced5c0 100644 --- a/tags/void.block.gen.go +++ b/tags/void.block.gen.go @@ -1,7 +1,9 @@ // generated by ../script/generate-tags.go package tags + import "code.squareroundforest.org/arpio/html" + var Area = html.Void(html.Define("area")) var Base = html.Void(html.Define("base")) var Hr = html.Void(html.Define("hr")) diff --git a/tags/void.inline.gen.go b/tags/void.inline.gen.go index 224625d..816cc45 100644 --- a/tags/void.inline.gen.go +++ b/tags/void.inline.gen.go @@ -1,7 +1,9 @@ // generated by ../script/generate-tags.go package tags + import "code.squareroundforest.org/arpio/html" + var Br = html.Inline(html.Void(html.Define("br"))) var Embed = html.Inline(html.Void(html.Define("embed"))) var Img = html.Inline(html.Void(html.Define("img"))) diff --git a/wrap.go b/wrap.go index eed2c0b..6052b4e 100644 --- a/wrap.go +++ b/wrap.go @@ -8,9 +8,8 @@ import ( ) type wrapper struct { - out io.Writer + out *indentWriter width int - indent string line, word *bytes.Buffer inWord, inTag, inSingleQuote, inQuote, lastSpace, started bool err error @@ -18,11 +17,10 @@ type wrapper struct { func newWrapper(out io.Writer, width int, indent string) *wrapper { return &wrapper{ - out: out, - width: width, - indent: indent, - line: bytes.NewBuffer(nil), - word: bytes.NewBuffer(nil), + out: newIndentWriter(out, indent), + width: width, + line: bytes.NewBuffer(nil), + word: bytes.NewBuffer(nil), } } @@ -39,10 +37,6 @@ func (w *wrapper) feed() error { if _, err := w.out.Write([]byte{'\n'}); err != nil { return err } - - if _, err := w.out.Write([]byte(w.indent)); err != nil { - return err - } } if _, err := io.Copy(w.out, w.line); err != nil { @@ -79,6 +73,11 @@ func (w *wrapper) Write(p []byte) (int, error) { return len(p), nil } + if err != nil { + w.err = err + return len(p), w.err + } + if r == unicode.ReplacementChar { continue } @@ -165,5 +164,6 @@ func (w *wrapper) Flush() error { return err } - return nil + w.err = w.out.Flush() + return w.err } diff --git a/writer_test.go b/writer_test.go index 0c7c750..052f231 100644 --- a/writer_test.go +++ b/writer_test.go @@ -5,12 +5,12 @@ import ( "io" ) -type errorWriter struct{ - out io.Writer +type errorWriter struct { + out io.Writer failAfter int } -func(ew *errorWriter) Write(p []byte) (int, error) { +func (ew *errorWriter) Write(p []byte) (int, error) { wp := p if len(wp) > ew.failAfter { wp = wp[:ew.failAfter]