about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--README.md6
-rw-r--r--attr/attributes_test.go2
-rw-r--r--attr/boolean_test.go2
-rw-r--r--attr/simple_test.go2
-rw-r--r--components/documents_test.go4
-rw-r--r--el/elements_test.go12
-rw-r--r--el/simple_test.go36
-rw-r--r--gomponents.go66
-rw-r--r--gomponents_test.go45
9 files changed, 103 insertions, 72 deletions
diff --git a/README.md b/README.md
index 118a1c7..cba1e66 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,8 @@
 [![GoDoc](https://godoc.org/github.com/maragudk/gomponents?status.svg)](https://godoc.org/github.com/maragudk/gomponents)
 [![codecov](https://codecov.io/gh/maragudk/gomponents/branch/master/graph/badge.svg)](https://codecov.io/gh/maragudk/gomponents)
 
-gomponents are declarative view components in Go, that can render to HTML.
-gomponents aims to make it easy to build HTML pages of reusable components,
+gomponents are declarative view components in Go, that can render to HTML5.
+gomponents aims to make it easy to build HTML5 pages of reusable components,
 without the use of a template language. Think server-side-rendered React,
 but without the virtual DOM and diffing.
 
@@ -15,7 +15,7 @@ for background.
 
 ## Features
 
-- Write declarative HTML in Go without all the strings, so you get
+- Write declarative HTML5 in Go without all the strings, so you get
   - Type safety
   - Auto-completion
   - Nice formatting with `gofmt`
diff --git a/attr/attributes_test.go b/attr/attributes_test.go
index efb7044..da04a20 100644
--- a/attr/attributes_test.go
+++ b/attr/attributes_test.go
@@ -20,7 +20,7 @@ func TestClasses(t *testing.T) {
 
 	t.Run("renders as attribute in an element", func(t *testing.T) {
 		e := g.El("div", attr.Classes{"hat": true})
-		assert.Equal(t, `<div class="hat" />`, e)
+		assert.Equal(t, `<div class="hat"></div>`, e)
 	})
 
 	t.Run("also works with fmt", func(t *testing.T) {
diff --git a/attr/boolean_test.go b/attr/boolean_test.go
index 4e6bb37..309b025 100644
--- a/attr/boolean_test.go
+++ b/attr/boolean_test.go
@@ -26,7 +26,7 @@ func TestBooleanAttributes(t *testing.T) {
 	for name, fn := range cases {
 		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
 			n := g.El("div", fn())
-			assert.Equal(t, fmt.Sprintf(`<div %v />`, name), n)
+			assert.Equal(t, fmt.Sprintf(`<div %v></div>`, name), n)
 		})
 	}
 }
diff --git a/attr/simple_test.go b/attr/simple_test.go
index eb2cfe3..6181322 100644
--- a/attr/simple_test.go
+++ b/attr/simple_test.go
@@ -43,7 +43,7 @@ func TestSimpleAttributes(t *testing.T) {
 	for name, fn := range cases {
 		t.Run(fmt.Sprintf(`should output %v="hat"`, name), func(t *testing.T) {
 			n := g.El("div", fn("hat"))
-			assert.Equal(t, fmt.Sprintf(`<div %v="hat" />`, name), n)
+			assert.Equal(t, fmt.Sprintf(`<div %v="hat"></div>`, name), n)
 		})
 	}
 }
diff --git a/components/documents_test.go b/components/documents_test.go
index 2aabd5a..863ac96 100644
--- a/components/documents_test.go
+++ b/components/documents_test.go
@@ -20,7 +20,7 @@ func TestHTML5(t *testing.T) {
 			Body:        []g.Node{el.Div()},
 		})
 
-		assert.Equal(t, `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Hat</title><meta name="description" content="Love hats." /><link rel="stylesheet" href="/hat.css" /></head><body><div /></body></html>`, e)
+		assert.Equal(t, `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Hat</title><meta name="description" content="Love hats."><link rel="stylesheet" href="/hat.css"></head><body><div></div></body></html>`, e)
 	})
 
 	t.Run("returns no language, description, and extra head/body elements if empty", func(t *testing.T) {
@@ -28,6 +28,6 @@ func TestHTML5(t *testing.T) {
 			Title: "Hat",
 		})
 
-		assert.Equal(t, `<!doctype html><html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Hat</title></head><body /></html>`, e)
+		assert.Equal(t, `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Hat</title></head><body></body></html>`, e)
 	})
 }
diff --git a/el/elements_test.go b/el/elements_test.go
index e90be9f..adce6df 100644
--- a/el/elements_test.go
+++ b/el/elements_test.go
@@ -17,7 +17,7 @@ func (w *erroringWriter) Write(p []byte) (n int, err error) {
 
 func TestDocument(t *testing.T) {
 	t.Run("returns doctype and children", func(t *testing.T) {
-		assert.Equal(t, `<!doctype html><html />`, el.Document(g.El("html")))
+		assert.Equal(t, `<!doctype html><html></html>`, el.Document(g.El("html")))
 	})
 
 	t.Run("errors on write error in Render", func(t *testing.T) {
@@ -28,13 +28,13 @@ func TestDocument(t *testing.T) {
 
 func TestForm(t *testing.T) {
 	t.Run("returns a form element with action and method attributes", func(t *testing.T) {
-		assert.Equal(t, `<form action="/" method="post" />`, el.Form("/", "post"))
+		assert.Equal(t, `<form action="/" method="post"></form>`, el.Form("/", "post"))
 	})
 }
 
 func TestInput(t *testing.T) {
 	t.Run("returns an input element with attributes type and name", func(t *testing.T) {
-		assert.Equal(t, `<input type="text" name="hat" />`, el.Input("text", "hat"))
+		assert.Equal(t, `<input type="text" name="hat">`, el.Input("text", "hat"))
 	})
 }
 
@@ -52,7 +52,7 @@ func TestOption(t *testing.T) {
 
 func TestProgress(t *testing.T) {
 	t.Run("returns a progress element with attributes value and max", func(t *testing.T) {
-		assert.Equal(t, `<progress value="5.5" max="10" />`, el.Progress(5.5, 10))
+		assert.Equal(t, `<progress value="5.5" max="10"></progress>`, el.Progress(5.5, 10))
 	})
 }
 
@@ -65,7 +65,7 @@ func TestSelect(t *testing.T) {
 
 func TestTextarea(t *testing.T) {
 	t.Run("returns a textarea element with attribute name", func(t *testing.T) {
-		assert.Equal(t, `<textarea name="hat" />`, el.Textarea("hat"))
+		assert.Equal(t, `<textarea name="hat"></textarea>`, el.Textarea("hat"))
 	})
 }
 
@@ -77,6 +77,6 @@ func TestA(t *testing.T) {
 
 func TestImg(t *testing.T) {
 	t.Run("returns an img element with href and alt attributes", func(t *testing.T) {
-		assert.Equal(t, `<img src="hat.png" alt="hat" id="image" />`, el.Img("hat.png", "hat", g.Attr("id", "image")))
+		assert.Equal(t, `<img src="hat.png" alt="hat" id="image">`, el.Img("hat.png", "hat", g.Attr("id", "image")))
 	})
 }
diff --git a/el/simple_test.go b/el/simple_test.go
index d388e1e..f01ba51 100644
--- a/el/simple_test.go
+++ b/el/simple_test.go
@@ -12,19 +12,15 @@ import (
 func TestSimpleElements(t *testing.T) {
 	cases := map[string]func(...g.Node) g.NodeFunc{
 		"address":    el.Address,
-		"area":       el.Area,
 		"article":    el.Article,
 		"aside":      el.Aside,
 		"audio":      el.Audio,
-		"base":       el.Base,
 		"blockquote": el.BlockQuote,
 		"body":       el.Body,
-		"br":         el.Br,
 		"button":     el.Button,
 		"canvas":     el.Canvas,
 		"cite":       el.Cite,
 		"code":       el.Code,
-		"col":        el.Col,
 		"colgroup":   el.ColGroup,
 		"data":       el.Data,
 		"datalist":   el.DataList,
@@ -32,22 +28,18 @@ func TestSimpleElements(t *testing.T) {
 		"dialog":     el.Dialog,
 		"div":        el.Div,
 		"dl":         el.Dl,
-		"embed":      el.Embed,
 		"fieldset":   el.FieldSet,
 		"figure":     el.Figure,
 		"footer":     el.Footer,
 		"head":       el.Head,
 		"header":     el.Header,
 		"hgroup":     el.HGroup,
-		"hr":         el.Hr,
 		"html":       el.HTML,
 		"iframe":     el.IFrame,
 		"legend":     el.Legend,
 		"li":         el.Li,
-		"link":       el.Link,
 		"main":       el.Main,
 		"menu":       el.Menu,
-		"meta":       el.Meta,
 		"meter":      el.Meter,
 		"nav":        el.Nav,
 		"noscript":   el.NoScript,
@@ -55,12 +47,10 @@ func TestSimpleElements(t *testing.T) {
 		"ol":         el.Ol,
 		"optgroup":   el.OptGroup,
 		"p":          el.P,
-		"param":      el.Param,
 		"picture":    el.Picture,
 		"pre":        el.Pre,
 		"script":     el.Script,
 		"section":    el.Section,
-		"source":     el.Source,
 		"span":       el.Span,
 		"style":      el.Style,
 		"summary":    el.Summary,
@@ -72,13 +62,35 @@ func TestSimpleElements(t *testing.T) {
 		"thead":      el.THead,
 		"tr":         el.Tr,
 		"ul":         el.Ul,
-		"wbr":        el.Wbr,
 	}
 
 	for name, fn := range cases {
 		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
 			n := fn(g.Attr("id", "hat"))
-			assert.Equal(t, fmt.Sprintf(`<%v id="hat" />`, name), n)
+			assert.Equal(t, fmt.Sprintf(`<%v id="hat"></%v>`, name, name), n)
+		})
+	}
+}
+
+func TestSimpleVoidKindElements(t *testing.T) {
+	cases := map[string]func(...g.Node) g.NodeFunc{
+		"area":   el.Area,
+		"base":   el.Base,
+		"br":     el.Br,
+		"col":    el.Col,
+		"embed":  el.Embed,
+		"hr":     el.Hr,
+		"link":   el.Link,
+		"meta":   el.Meta,
+		"param":  el.Param,
+		"source": el.Source,
+		"wbr":    el.Wbr,
+	}
+
+	for name, fn := range cases {
+		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
+			n := fn(g.Attr("id", "hat"))
+			assert.Equal(t, fmt.Sprintf(`<%v id="hat">`, name), n)
 		})
 	}
 }
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.
diff --git a/gomponents_test.go b/gomponents_test.go
index 0c5dc68..26199c3 100644
--- a/gomponents_test.go
+++ b/gomponents_test.go
@@ -86,22 +86,33 @@ func (o outsider) Render(w io.Writer) error {
 func TestEl(t *testing.T) {
 	t.Run("renders an empty element if no children given", func(t *testing.T) {
 		e := g.El("div")
-		assert.Equal(t, "<div />", e)
+		assert.Equal(t, "<div></div>", e)
+	})
+
+	t.Run("renders an empty element without closing tag if it's a void kind element", func(t *testing.T) {
+		e := g.El("hr")
+		assert.Equal(t, "<hr>", e)
+
+		e = g.El("br")
+		assert.Equal(t, "<br>", e)
+
+		e = g.El("img")
+		assert.Equal(t, "<img>", e)
 	})
 
 	t.Run("renders an empty element if only attributes given as children", func(t *testing.T) {
 		e := g.El("div", g.Attr("class", "hat"))
-		assert.Equal(t, `<div class="hat" />`, e)
+		assert.Equal(t, `<div class="hat"></div>`, e)
 	})
 
 	t.Run("renders an element, attributes, and element children", func(t *testing.T) {
-		e := g.El("div", g.Attr("class", "hat"), g.El("span"))
-		assert.Equal(t, `<div class="hat"><span /></div>`, e)
+		e := g.El("div", g.Attr("class", "hat"), g.El("br"))
+		assert.Equal(t, `<div class="hat"><br></div>`, e)
 	})
 
 	t.Run("renders attributes at the correct place regardless of placement in parameter list", func(t *testing.T) {
-		e := g.El("div", g.El("span"), g.Attr("class", "hat"))
-		assert.Equal(t, `<div class="hat"><span /></div>`, e)
+		e := g.El("div", g.El("br"), g.Attr("class", "hat"))
+		assert.Equal(t, `<div class="hat"><br></div>`, e)
 	})
 
 	t.Run("renders outside if node does not implement placer", func(t *testing.T) {
@@ -110,8 +121,8 @@ func TestEl(t *testing.T) {
 	})
 
 	t.Run("does not fail on nil node", func(t *testing.T) {
-		e := g.El("div", nil, g.El("span"), nil, g.El("span"))
-		assert.Equal(t, `<div><span /><span /></div>`, e)
+		e := g.El("div", nil, g.El("br"), nil, g.El("br"))
+		assert.Equal(t, `<div><br><br></div>`, e)
 	})
 
 	t.Run("returns render error on cannot write", func(t *testing.T) {
@@ -129,30 +140,30 @@ func (w *erroringWriter) Write(p []byte) (n int, err error) {
 
 func TestText(t *testing.T) {
 	t.Run("renders escaped text", func(t *testing.T) {
-		e := g.Text("<div />")
-		assert.Equal(t, "&lt;div /&gt;", e)
+		e := g.Text("<div>")
+		assert.Equal(t, "&lt;div&gt;", e)
 	})
 }
 
 func TestTextf(t *testing.T) {
 	t.Run("renders interpolated and escaped text", func(t *testing.T) {
-		e := g.Textf("<%v />", "div")
-		assert.Equal(t, "&lt;div /&gt;", e)
+		e := g.Textf("<%v>", "div")
+		assert.Equal(t, "&lt;div&gt;", e)
 	})
 }
 
 func TestRaw(t *testing.T) {
 	t.Run("renders raw text", func(t *testing.T) {
-		e := g.Raw("<div />")
-		assert.Equal(t, "<div />", e)
+		e := g.Raw("<div>")
+		assert.Equal(t, "<div>", e)
 	})
 }
 
 func TestGroup(t *testing.T) {
 	t.Run("groups multiple nodes into one", func(t *testing.T) {
-		children := []g.Node{g.El("div", g.Attr("id", "hat")), g.El("div")}
-		e := g.El("div", g.Attr("class", "foo"), g.El("div"), g.Group(children))
-		assert.Equal(t, `<div class="foo"><div /><div id="hat" /><div /></div>`, e)
+		children := []g.Node{g.El("br", g.Attr("id", "hat")), g.El("hr")}
+		e := g.El("div", g.Attr("class", "foo"), g.El("img"), g.Group(children))
+		assert.Equal(t, `<div class="foo"><img><br id="hat"><hr></div>`, e)
 	})
 
 	t.Run("panics on direct render", func(t *testing.T) {