diff options
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | attr/attributes_test.go | 2 | ||||
-rw-r--r-- | attr/boolean_test.go | 2 | ||||
-rw-r--r-- | attr/simple_test.go | 2 | ||||
-rw-r--r-- | components/documents_test.go | 4 | ||||
-rw-r--r-- | el/elements_test.go | 12 | ||||
-rw-r--r-- | el/simple_test.go | 36 | ||||
-rw-r--r-- | gomponents.go | 66 | ||||
-rw-r--r-- | gomponents_test.go | 45 |
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 @@ [](https://godoc.org/github.com/maragudk/gomponents) [](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, "<div />", e) + e := g.Text("<div>") + assert.Equal(t, "<div>", 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, "<div />", e) + e := g.Textf("<%v>", "div") + assert.Equal(t, "<div>", 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) { |