diff options
Diffstat (limited to 'gomponents.go')
-rw-r--r-- | gomponents.go | 143 |
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. |