about summary refs log tree commit diff stats
path: root/gomponents.go
diff options
context:
space:
mode:
Diffstat (limited to 'gomponents.go')
-rw-r--r--gomponents.go143
1 files changed, 88 insertions, 55 deletions
diff --git a/gomponents.go b/gomponents.go
index f4b8467..178f0a2 100644
--- a/gomponents.go
+++ b/gomponents.go
@@ -14,9 +14,9 @@ import (
 	"strings"
 )
 
-// Node is a DOM node that can Render itself to a string representation.
+// Node is a DOM node that can Render itself to a io.Writer.
 type Node interface {
-	Render() string
+	Render(w io.Writer) error
 }
 
 // Placer can be implemented to tell Render functions where to place the string representation of a Node
@@ -34,10 +34,10 @@ const (
 )
 
 // NodeFunc is render function that is also a Node.
-type NodeFunc func() string
+type NodeFunc func(io.Writer) error
 
-func (n NodeFunc) Render() string {
-	return n()
+func (n NodeFunc) Render(w io.Writer) error {
+	return n(w)
 }
 
 func (n NodeFunc) Place() Placement {
@@ -46,64 +46,92 @@ func (n NodeFunc) Place() Placement {
 
 // String satisfies fmt.Stringer.
 func (n NodeFunc) String() string {
-	return n.Render()
+	var b strings.Builder
+	_ = n.Render(&b)
+	return b.String()
 }
 
+type nodeType int
+
+const (
+	attrType = nodeType(iota)
+	elementType
+)
+
 // El creates an element DOM Node with a name and child Nodes.
 // Use this if no convenience creator exists.
 func El(name string, children ...Node) NodeFunc {
-	return func() string {
-		var b, inside, outside strings.Builder
+	return func(w2 io.Writer) error {
+		w := &statefulWriter{w: w2}
 
-		b.WriteString("<")
-		b.WriteString(name)
+		w.Write([]byte("<" + name))
 
 		if len(children) == 0 {
-			b.WriteString(" />")
-			return b.String()
+			w.Write([]byte(" />"))
+			return w.err
 		}
 
+		hasOutsideChildren := false
 		for _, c := range children {
-			renderChild(c, &inside, &outside)
+			hasOutsideChildren = renderChild(w, c, attrType) || hasOutsideChildren
 		}
 
-		b.WriteString(inside.String())
+		if !hasOutsideChildren {
+			w.Write([]byte(" />"))
+			return w.err
+		}
+
+		w.Write([]byte(">"))
 
-		if outside.Len() == 0 {
-			b.WriteString(" />")
-			return b.String()
+		for _, c := range children {
+			renderChild(w, c, elementType)
 		}
 
-		b.WriteString(">")
-		b.WriteString(outside.String())
-		b.WriteString("</")
-		b.WriteString(name)
-		b.WriteString(">")
-		return b.String()
+		w.Write([]byte("</" + name + ">"))
+		return w.err
 	}
 }
 
-func renderChild(c Node, inside, outside *strings.Builder) {
-	if c == nil {
-		return
+// renderChild c to the given writer w if the node type is t.
+// Returns whether the child would be written Outside, regardless of whether it is actually written.
+func renderChild(w *statefulWriter, c Node, t nodeType) bool {
+	if w.err != nil || c == nil {
+		return false
 	}
+
+	isOutside := false
 	if g, ok := c.(group); ok {
 		for _, groupC := range g.children {
-			renderChild(groupC, inside, outside)
+			isOutside = renderChild(w, groupC, t) || isOutside
 		}
-		return
+		return isOutside
 	}
-	if p, ok := c.(Placer); ok {
-		switch p.Place() {
-		case Inside:
-			inside.WriteString(c.Render())
-		case Outside:
-			outside.WriteString(c.Render())
-		}
+
+	if p, ok := c.(Placer); !ok || p.Place() == Outside {
+		isOutside = true
+	}
+
+	switch {
+	case t == attrType && !isOutside:
+		w.err = c.Render(w.w)
+	case t == elementType && isOutside:
+		w.err = c.Render(w.w)
+	}
+
+	return isOutside
+}
+
+// statefulWriter only writes if no errors have occured earlier in its lifetime.
+type statefulWriter struct {
+	w   io.Writer
+	err error
+}
+
+func (w *statefulWriter) Write(p []byte) {
+	if w.err != nil {
 		return
 	}
-	// If c doesn't implement Placer, default to outside
-	outside.WriteString(c.Render())
+	_, w.err = w.w.Write(p)
 }
 
 // Attr creates an attr DOM Node.
@@ -127,11 +155,13 @@ type attr struct {
 	value *string
 }
 
-func (a *attr) Render() string {
+func (a *attr) Render(w io.Writer) error {
 	if a.value == nil {
-		return " " + a.name
+		_, err := w.Write([]byte(" " + a.name))
+		return err
 	}
-	return " " + a.name + `="` + *a.value + `"`
+	_, err := w.Write([]byte(" " + a.name + `="` + *a.value + `"`))
+	return err
 }
 
 func (a *attr) Place() Placement {
@@ -140,42 +170,45 @@ func (a *attr) Place() Placement {
 
 // String satisfies fmt.Stringer.
 func (a *attr) String() string {
-	return a.Render()
+	var b strings.Builder
+	_ = a.Render(&b)
+	return b.String()
 }
 
 // Text creates a text DOM Node that Renders the escaped string t.
 func Text(t string) NodeFunc {
-	return func() string {
-		return template.HTMLEscapeString(t)
+	return func(w io.Writer) error {
+		_, err := w.Write([]byte(template.HTMLEscapeString(t)))
+		return err
 	}
 }
 
 // Textf creates a text DOM Node that Renders the interpolated and escaped string t.
 func Textf(format string, a ...interface{}) NodeFunc {
-	return func() string {
-		return template.HTMLEscapeString(fmt.Sprintf(format, a...))
+	return func(w io.Writer) error {
+		_, err := w.Write([]byte(template.HTMLEscapeString(fmt.Sprintf(format, a...))))
+		return err
 	}
 }
 
 // Raw creates a raw Node that just Renders the unescaped string t.
 func Raw(t string) NodeFunc {
-	return func() string {
-		return t
+	return func(w io.Writer) error {
+		_, err := w.Write([]byte(t))
+		return err
 	}
 }
 
-// Write to the given io.Writer, returning any error.
-func Write(w io.Writer, n Node) error {
-	_, err := w.Write([]byte(n.Render()))
-	return err
-}
-
 type group struct {
 	children []Node
 }
 
-func (g group) Render() string {
-	panic("cannot render group")
+func (g group) String() string {
+	panic("cannot render group directly")
+}
+
+func (g group) Render(io.Writer) error {
+	panic("cannot render group directly")
 }
 
 // Group multiple Nodes into one Node. Useful for concatenation of Nodes in variadic functions.