about summary refs log tree commit diff stats
path: root/gomponents.go
diff options
context:
space:
mode:
authorMarkus Wüstenberg2020-11-16 12:38:24 +0100
committerGitHub2020-11-16 12:38:24 +0100
commit794c3b26acbd3931b7973ff7e09a42b0ac414b1c (patch)
treeac909b96ce6cca65b71828deca179db8e22e2995 /gomponents.go
parent87d09c382434c9198c50356954fedf0ab8e5576f (diff)
downloadgomponents-794c3b26acbd3931b7973ff7e09a42b0ac414b1c.tar.lz
gomponents-794c3b26acbd3931b7973ff7e09a42b0ac414b1c.tar.zst
gomponents-794c3b26acbd3931b7973ff7e09a42b0ac414b1c.zip
Render correct HTML5 (#44)
Previously, elements of kind void and empty elements generally would be rendered auto-closing (with a final `/` character in the start tag), which is allowed sometimes but arguably wrong. See https://dev.w3.org/html5/spec-LC/syntax.html#end-tags

This created problems with for example `textarea` and `script`, which cannot be auto-closing, or the browser renders it wrong.

Also clarified in the docs that this library outputs HTML5.

Fixes #42.
Diffstat (limited to 'gomponents.go')
-rw-r--r--gomponents.go66
1 files changed, 37 insertions, 29 deletions
diff --git a/gomponents.go b/gomponents.go
index 02e52ca..14df153 100644
--- a/gomponents.go
+++ b/gomponents.go
@@ -1,4 +1,4 @@
-// Package gomponents provides declarative view components in Go, that can render to HTML.
+// Package gomponents provides declarative view components in Go, that can render to HTML5.
 // The primary interface is a Node, which has a single function Render, which should render
 // the Node to a string. Furthermore, NodeFunc is a function which implements the Node interface
 // by calling itself on Render.
@@ -14,6 +14,10 @@ import (
 	"strings"
 )
 
+// voidElements don't have end tags and must be treated differently in the rendering.
+// See https://dev.w3.org/html5/spec-LC/syntax.html#void-elements
+var voidElements = []string{"area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"}
+
 // Node is a DOM node that can Render itself to a io.Writer.
 type Node interface {
 	Render(w io.Writer) error
@@ -51,33 +55,32 @@ func (n NodeFunc) String() string {
 	return b.String()
 }
 
+// nodeType is for DOM Nodes that are either an element or an attribute.
 type nodeType int
 
 const (
-	attrType = nodeType(iota)
-	elementType
+	elementType = nodeType(iota)
+	attributeType
 )
 
 // El creates an element DOM Node with a name and child Nodes.
 // Use this if no convenience creator exists.
+// See https://dev.w3.org/html5/spec-LC/syntax.html#elements-0 for how elements are rendered.
+// No tags are ever omitted from normal tags, even though it's allowed for elements given at
+// https://dev.w3.org/html5/spec-LC/syntax.html#optional-tags
+// If an element is a void kind, non-attribute nodes are ignored.
 func El(name string, children ...Node) NodeFunc {
 	return func(w2 io.Writer) error {
 		w := &statefulWriter{w: w2}
 
 		w.Write([]byte("<" + name))
 
-		if len(children) == 0 {
-			w.Write([]byte(" />"))
-			return w.err
-		}
-
-		hasOutsideChildren := false
 		for _, c := range children {
-			hasOutsideChildren = renderChild(w, c, attrType) || hasOutsideChildren
+			renderChild(w, c, attributeType)
 		}
 
-		if !hasOutsideChildren {
-			w.Write([]byte(" />"))
+		if isVoidKind(name) {
+			w.Write([]byte(">"))
 			return w.err
 		}
 
@@ -92,33 +95,38 @@ func El(name string, children ...Node) NodeFunc {
 	}
 }
 
+func isVoidKind(name string) bool {
+	for _, e := range voidElements {
+		if name == e {
+			return true
+		}
+	}
+	return false
+}
+
 // 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 {
+func renderChild(w *statefulWriter, c Node, t nodeType) {
 	if w.err != nil || c == nil {
-		return false
+		return
 	}
 
-	isOutside := false
 	if g, ok := c.(group); ok {
 		for _, groupC := range g.children {
-			isOutside = renderChild(w, groupC, t) || isOutside
+			renderChild(w, groupC, t)
 		}
-		return isOutside
-	}
-
-	if p, ok := c.(Placer); !ok || p.Place() == Outside {
-		isOutside = true
+		return
 	}
 
-	switch {
-	case t == attrType && !isOutside:
-		w.err = c.Render(w.w)
-	case t == elementType && isOutside:
-		w.err = c.Render(w.w)
+	switch t {
+	case elementType:
+		if p, ok := c.(Placer); !ok || p.Place() == Outside {
+			w.err = c.Render(w.w)
+		}
+	case attributeType:
+		if p, ok := c.(Placer); ok && p.Place() == Inside {
+			w.err = c.Render(w.w)
+		}
 	}
-
-	return isOutside
 }
 
 // statefulWriter only writes if no errors have occured earlier in its lifetime.