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.
This commit is contained in:
parent
87d09c3824
commit
794c3b26ac
@ -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`
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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")))
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
return
|
||||
}
|
||||
|
||||
if p, ok := c.(Placer); !ok || p.Place() == Outside {
|
||||
isOutside = true
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user