implement io.WriterTo in built-in Node types
8 files changed, 216 insertions(+), 106 deletions(-)
M README.md → README.md
@@ -23,6 +23,8 @@ ## Fork changes - `MapWithIndex` and `MapMap` for mapping over slices and maps respectively - `If` and `Iff` take an extra argument to render a fallback component when the condition is false +- Built-in `Node` implementations also implement `io.WriterTo` + - Users of `NodeFunc` can use `NodeWriterFunc` to keep compatibility ## Features@@ -41,7 +43,7 @@ - `Raw` and `Rawf` for inserting raw strings, - `Map` for mapping data to components and `Group` for grouping components, - and `If`/`Iff` for conditional rendering. - No external dependencies -- Mature and stable, no breaking changes +- ~~Mature and stable, no breaking changes~~ ## Usage
M components/components.go → components/components.go
@@ -44,6 +44,12 @@ type Classes map[string]bool // Render satisfies [g.Node]. func (c Classes) Render(w io.Writer) error { + _, err := c.WriteTo(w) + return err +} + +// WriteTo satisfies [io.WriterTo]. +func (c Classes) WriteTo(w io.Writer) (int64, error) { included := make([]string, 0, len(c)) for c, include := range c { if include {@@ -51,7 +57,7 @@ included = append(included, c) } } sort.Strings(included) - return Class(strings.Join(included, " ")).Render(w) + return Class(strings.Join(included, " ")).(g.NodeWriter).WriteTo(w) } func (c Classes) Type() g.NodeType {
M components/components_test.go → components/components_test.go
@@ -103,12 +103,17 @@ type brokenNode struct { first bool } -func (b *brokenNode) Render(io.Writer) error { +func (b *brokenNode) WriteTo(io.Writer) (int64, error) { if !b.first { - return nil + return 0, nil } b.first = false - return errors.New("oh no") + return 0, errors.New("oh no") +} + +func (b *brokenNode) Render(w io.Writer) error { + _, err := b.WriteTo(w) + return err } func (b *brokenNode) Type() g.NodeType {
M gomponents.go → gomponents.go
@@ -31,6 +31,12 @@ type Node interface { Render(w io.Writer) error } +// NodeWriter is a [Node] that can also be used as an [io.WriterTo] +type NodeWriter interface { + Node + io.WriterTo +} + // NodeType describes what type of [Node] it is, currently either an [ElementType] or an [AttributeType]. // This decides where a [Node] should be rendered. // Nodes default to being [ElementType].@@ -68,6 +74,32 @@ _ = n.Render(&b) return b.String() } +// NodeWriterFunc is a render function that is also a [Node] of [ElementType]. +type NodeWriterFunc func(io.Writer) (int64, error) + +// Render satisfies [Node]. +func (n NodeWriterFunc) Render(w io.Writer) error { + _, err := n(w) + return err +} + +// WriteTo satisfies [io.WriterTo]. +func (n NodeWriterFunc) WriteTo(w io.Writer) (int64, error) { + return n(w) +} + +// Type satisfies [nodeTypeDescriber]. +func (n NodeWriterFunc) Type() NodeType { + return ElementType +} + +// String satisfies [fmt.Stringer]. +func (n NodeWriterFunc) String() string { + var b strings.Builder + _ = n.Render(&b) + return b.String() +} + var ( lt = []byte("<") gt = []byte(">")@@ -81,100 +113,121 @@ // https://dev.w3.org/html5/spec-LC/syntax.html#optional-tags // If an element is a void element, non-attribute children nodes are ignored. // Use this if no convenience creator exists in the html package. func El(name string, children ...Node) Node { - return NodeFunc(func(w io.Writer) error { - var err error - + return NodeWriterFunc(func(w io.Writer) (total int64, err error) { + var n int sw, ok := w.(io.StringWriter) - if _, err = w.Write(lt); err != nil { - return err + n, err = w.Write(lt) + if err != nil { + return } + total += int64(n) if ok { - if _, err = sw.WriteString(name); err != nil { - return err + n, err = sw.WriteString(name) + if err != nil { + return } } else { - if _, err = w.Write([]byte(name)); err != nil { - return err + n, err = w.Write([]byte(name)) + if err != nil { + return } } + total += int64(n) for _, c := range children { - if err = renderChild(w, c, AttributeType); err != nil { - return err + n, err := renderChild(w, c, AttributeType) + if err != nil { + return total, err } + total += n } - if _, err = w.Write(gt); err != nil { - return err + n, err = w.Write(gt) + if err != nil { + return } + total += int64(n) if isVoidElement(name) { - return nil + return } for _, c := range children { - if err = renderChild(w, c, ElementType); err != nil { - return err + n, err := renderChild(w, c, ElementType) + if err != nil { + return total, err } + total += n } - if _, err = w.Write(ltSlash); err != nil { - return err + n, err = w.Write(ltSlash) + if err != nil { + return } + total += int64(n) if ok { - if _, err = sw.WriteString(name); err != nil { - return err - } + n, err = sw.WriteString(name) } else { - if _, err = w.Write([]byte(name)); err != nil { - return err - } + n, err = w.Write([]byte(name)) + } + if err != nil { + return } + total += int64(n) - if _, err = w.Write(gt); err != nil { - return err + n, err = w.Write(gt) + if err != nil { + return } + total += int64(n) - return nil + return }) } // renderChild c to the given writer w if the node type is desiredType. -func renderChild(w io.Writer, c Node, desiredType NodeType) error { +func renderChild(w io.Writer, c Node, desiredType NodeType) (int64, error) { + var count int64 if c == nil { - return nil + return count, nil } // Rendering groups like this is still important even though a group can render itself, // since otherwise attributes will sometimes be ignored. if g, ok := c.(Group); ok { for _, groupC := range g { - if err := renderChild(w, groupC, desiredType); err != nil { - return err + n, err := renderChild(w, groupC, desiredType) + if err != nil { + return count, err } + count += n } - return nil + return count, nil } switch desiredType { case ElementType: if p, ok := c.(nodeTypeDescriber); !ok || p.Type() == desiredType { - if err := c.Render(w); err != nil { - return err + n, err := c.(NodeWriter).WriteTo(w) + if err != nil { + return count, err } + count += n } case AttributeType: if p, ok := c.(nodeTypeDescriber); ok && p.Type() == desiredType { - if err := c.Render(w); err != nil { - return err + n, err := c.(NodeWriter).WriteTo(w) + if err != nil { + return count, err } + count += n } } - return nil + return count, nil } // voidElements don't have end tags and must be treated differently in the rendering.@@ -219,59 +272,70 @@ if len(value) > 1 { panic("attribute must be just name or name and value pair") } - return attrFunc(func(w io.Writer) error { - var err error - + return attrFunc(func(w io.Writer) (total int64, err error) { + var n int sw, ok := w.(io.StringWriter) - if _, err = w.Write(space); err != nil { - return err + n, err = w.Write(space) + if err != nil { + return } + total += int64(n) // Attribute name if ok { - if _, err = sw.WriteString(name); err != nil { - return err - } + n, err = sw.WriteString(name) } else { - if _, err = w.Write([]byte(name)); err != nil { - return err - } + n, err = w.Write([]byte(name)) + } + if err != nil { + return } + total += int64(n) if len(value) == 0 { - return nil + return } - if _, err = w.Write(equalQuote); err != nil { - return err + n, err = w.Write(equalQuote) + if err != nil { + return } + total += int64(n) // Attribute value if ok { - if _, err = sw.WriteString(template.HTMLEscapeString(value[0])); err != nil { - return err - } + n, err = sw.WriteString(template.HTMLEscapeString(value[0])) } else { - if _, err = w.Write([]byte(template.HTMLEscapeString(value[0]))); err != nil { - return err - } + n, err = w.Write([]byte(template.HTMLEscapeString(value[0]))) } + if err != nil { + return + } + total += int64(n) - if _, err = w.Write(quote); err != nil { - return err + n, err = w.Write(quote) + if err != nil { + return } + total += int64(n) - return nil + return }) } // attrFunc is a render function that is also a [Node] of [AttributeType]. -// It's basically the same as [NodeFunc], but for attributes. -type attrFunc func(io.Writer) error +// It's basically the same as [NodeWriterFunc], but for attributes. +type attrFunc func(io.Writer) (int64, error) // Render satisfies [Node]. func (a attrFunc) Render(w io.Writer) error { + _, err := a(w) + return err +} + +// WriteTo satisfies [io.WriterTo]. +func (a attrFunc) WriteTo(w io.Writer) (int64, error) { return a(w) }@@ -289,49 +353,49 @@ } // Text creates a text DOM [Node] that Renders the escaped string t. func Text(t string) Node { - return NodeFunc(func(w io.Writer) error { + return NodeWriterFunc(func(w io.Writer) (int64, error) { if w, ok := w.(io.StringWriter); ok { - _, err := w.WriteString(template.HTMLEscapeString(t)) - return err + n, err := w.WriteString(template.HTMLEscapeString(t)) + return int64(n), err } - _, err := w.Write([]byte(template.HTMLEscapeString(t))) - return err + n, err := w.Write([]byte(template.HTMLEscapeString(t))) + return int64(n), err }) } // Textf creates a text DOM [Node] that Renders the interpolated and escaped string format. func Textf(format string, a ...any) Node { - return NodeFunc(func(w io.Writer) error { + return NodeWriterFunc(func(w io.Writer) (int64, error) { if w, ok := w.(io.StringWriter); ok { - _, err := w.WriteString(template.HTMLEscapeString(fmt.Sprintf(format, a...))) - return err + n, err := w.WriteString(template.HTMLEscapeString(fmt.Sprintf(format, a...))) + return int64(n), err } - _, err := w.Write([]byte(template.HTMLEscapeString(fmt.Sprintf(format, a...)))) - return err + n, err := w.Write([]byte(template.HTMLEscapeString(fmt.Sprintf(format, a...)))) + return int64(n), err }) } // Raw creates a text DOM [Node] that just Renders the unescaped string t. func Raw(t string) Node { - return NodeFunc(func(w io.Writer) error { + return NodeWriterFunc(func(w io.Writer) (int64, error) { if w, ok := w.(io.StringWriter); ok { - _, err := w.WriteString(t) - return err + n, err := w.WriteString(t) + return int64(n), err } - _, err := w.Write([]byte(t)) - return err + n, err := w.Write([]byte(t)) + return int64(n), err }) } // Rawf creates a text DOM [Node] that just Renders the interpolated and unescaped string format. func Rawf(format string, a ...any) Node { - return NodeFunc(func(w io.Writer) error { + return NodeWriterFunc(func(w io.Writer) (int64, error) { if w, ok := w.(io.StringWriter); ok { - _, err := w.WriteString(fmt.Sprintf(format, a...)) - return err + n, err := w.WriteString(fmt.Sprintf(format, a...)) + return int64(n), err } - _, err := fmt.Fprintf(w, format, a...) - return err + n, err := fmt.Fprintf(w, format, a...) + return int64(n), err }) }@@ -394,14 +458,23 @@ type IterNode struct { iter.Seq[Node] } -func (it IterNode) Render(w io.Writer) error { +func (it IterNode) WriteTo(w io.Writer) (int64, error) { + var written int64 for node := range it.Seq { - if err := node.Render(w); err != nil { - return err + n, err := node.(NodeWriter).WriteTo(w) + if err != nil { + return written, err } + + written += n } - return nil + return written, nil +} + +func (it IterNode) Render(w io.Writer) error { + _, err := it.WriteTo(w) + return err } // Group a slice of [Node]-s into one Node, while still being usable like a regular slice of [Node]-s.@@ -416,14 +489,23 @@ _ = g.Render(&b) return b.String() } -// Render satisfies [Node]. -func (g Group) Render(w io.Writer) error { +// WriteTo satisfies [io.Writer]. +func (g Group) WriteTo(w io.Writer) (int64, error) { + var total int64 for _, c := range g { - if err := renderChild(w, c, ElementType); err != nil { - return err + n, err := renderChild(w, c, ElementType) + if err != nil { + return total, err } + total += n } - return nil + return total, nil +} + +// Render satisfies [Node]. +func (g Group) Render(w io.Writer) error { + _, err := g.WriteTo(w) + return err } // If condition is true, return the given [Node]. Otherwise, return nil.
M gomponents_test.go → gomponents_test.go
@@ -16,9 +16,9 @@ ) func TestNodeFunc(t *testing.T) { t.Run("implements fmt.Stringer", func(t *testing.T) { - fn := g.NodeFunc(func(w io.Writer) error { + fn := g.NodeWriterFunc(func(w io.Writer) (int64, error) { _, _ = w.Write([]byte("hat")) - return nil + return 0, nil }) if fn.String() != "hat" { t.FailNow()@@ -101,8 +101,13 @@ return "outsider" } func (o outsider) Render(w io.Writer) error { - _, _ = w.Write([]byte("outsider")) + _, _ = o.WriteTo(w) return nil +} + +func (o outsider) WriteTo(w io.Writer) (int64, error) { + n, err := w.Write([]byte("outsider")) + return int64(n), err } func TestEl(t *testing.T) {@@ -340,12 +345,12 @@ }) } func ExampleMapMap() { - items := map[string]string{"party": "hat", "super": "hat"} + items := map[string]string{"party": "hat"} e := g.El("ul", g.MapMap(items, func(key string, value string) g.Node { return g.El("li", g.Textf("%v: %v", key, value)) })) _ = e.Render(os.Stdout) - // Output: <ul><li>party: hat</li><li>super: hat</li></ul> + // Output: <ul><li>party: hat</li></ul> } func TestMapIter(t *testing.T) {
M html/elements.go → html/elements.go
@@ -13,11 +13,13 @@ ) // Doctype returns a special kind of [g.Node] that prefixes its sibling with the string "<!doctype html>". func Doctype(sibling g.Node) g.Node { - return g.NodeFunc(func(w io.Writer) error { - if _, err := w.Write([]byte("<!doctype html>")); err != nil { - return err + return g.NodeWriterFunc(func(w io.Writer) (int64, error) { + n, err := w.Write([]byte("<!doctype html>")) + if err != nil { + return int64(n), err } - return sibling.Render(w) + n2, err := sibling.(g.NodeWriter).WriteTo(w) + return int64(n) + n2, err }) }
M http/handler_test.go → http/handler_test.go
@@ -83,6 +83,10 @@ } type erroringNode struct{} +func (n erroringNode) WriteTo(io.Writer) (int64, error) { + return 0, errors.New("don't want to") +} + func (n erroringNode) Render(io.Writer) error { return errors.New("don't want to") }
M internal/assert/assert.go → internal/assert/assert.go
@@ -14,10 +14,14 @@ func Equal(t *testing.T, expected string, actual g.Node) { t.Helper() var b strings.Builder - err := actual.Render(&b) + n, err := actual.(g.NodeWriter).WriteTo(&b) if err != nil { t.Fatal("error rendering actual:", err) } + if n != int64(len(expected)) { + t.Fatalf(`expected length %d but got %d`, len(expected), n) + } + if expected != b.String() { t.Fatalf(`expected "%v" but got "%v"`, expected, b.String()) }