2021-06-08 09:12:04 -07:00
|
|
|
// Package gomponents provides view components in Go, that render to HTML 5.
|
2022-05-25 05:36:26 -07:00
|
|
|
//
|
|
|
|
// The primary interface is a Node. It describes a function Render, which should render the Node
|
|
|
|
// to the given writer as a string.
|
|
|
|
//
|
2020-09-14 01:47:14 -07:00
|
|
|
// All DOM elements and attributes can be created by using the El and Attr functions.
|
2022-05-25 05:36:26 -07:00
|
|
|
// The functions Text, Textf, and Raw can be used to create text nodes.
|
|
|
|
// See also helper functions Group, Map, and If.
|
|
|
|
//
|
|
|
|
// For basic HTML elements and attributes, see the package html.
|
|
|
|
// For higher-level HTML components, see the package components.
|
|
|
|
// For SVG elements and attributes, see the package svg.
|
|
|
|
// For HTTP helpers, see the package http.
|
2020-09-13 13:50:19 -07:00
|
|
|
package gomponents
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
2020-09-21 02:27:37 -07:00
|
|
|
"io"
|
2020-09-13 13:50:19 -07:00
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2020-11-02 01:03:05 -08:00
|
|
|
// Node is a DOM node that can Render itself to a io.Writer.
|
2020-09-13 13:50:19 -07:00
|
|
|
type Node interface {
|
2020-11-02 01:03:05 -08:00
|
|
|
Render(w io.Writer) error
|
2020-09-13 13:50:19 -07:00
|
|
|
}
|
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
// NodeType describes what type of Node it is, currently either an ElementType or an AttributeType.
|
|
|
|
// This decides where a Node should be rendered.
|
2020-11-16 03:58:42 -08:00
|
|
|
// Nodes default to being ElementType.
|
|
|
|
type NodeType int
|
2020-09-24 04:19:52 -07:00
|
|
|
|
|
|
|
const (
|
2020-11-16 03:58:42 -08:00
|
|
|
ElementType = NodeType(iota)
|
|
|
|
AttributeType
|
2020-09-24 04:19:52 -07:00
|
|
|
)
|
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
// nodeTypeDescriber can be implemented by Nodes to let callers know whether the Node is
|
|
|
|
// an ElementType or an AttributeType. This is used for rendering.
|
2020-11-16 03:58:42 -08:00
|
|
|
type nodeTypeDescriber interface {
|
|
|
|
Type() NodeType
|
|
|
|
}
|
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
// NodeFunc is a render function that is also a Node of ElementType.
|
2020-11-02 01:03:05 -08:00
|
|
|
type NodeFunc func(io.Writer) error
|
2020-09-13 13:50:19 -07:00
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
// Render satisfies Node.
|
2020-11-02 01:03:05 -08:00
|
|
|
func (n NodeFunc) Render(w io.Writer) error {
|
|
|
|
return n(w)
|
2020-09-13 13:50:19 -07:00
|
|
|
}
|
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
// Type satisfies nodeTypeDescriber.
|
2020-11-16 03:58:42 -08:00
|
|
|
func (n NodeFunc) Type() NodeType {
|
|
|
|
return ElementType
|
2020-09-24 04:19:52 -07:00
|
|
|
}
|
|
|
|
|
2020-09-18 05:38:09 -07:00
|
|
|
// String satisfies fmt.Stringer.
|
|
|
|
func (n NodeFunc) String() string {
|
2020-11-02 01:03:05 -08:00
|
|
|
var b strings.Builder
|
|
|
|
_ = n.Render(&b)
|
|
|
|
return b.String()
|
2020-09-18 05:38:09 -07:00
|
|
|
}
|
|
|
|
|
2020-09-13 13:50:19 -07:00
|
|
|
// El creates an element DOM Node with a name and child Nodes.
|
2020-11-16 03:38:24 -08:00
|
|
|
// 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
|
2022-05-27 05:16:19 -07:00
|
|
|
// If an element is a void element, non-attribute children nodes are ignored.
|
2020-12-08 12:59:47 -08:00
|
|
|
// Use this if no convenience creator exists.
|
2021-01-07 01:20:03 -08:00
|
|
|
func El(name string, children ...Node) Node {
|
|
|
|
return NodeFunc(func(w2 io.Writer) error {
|
2020-11-02 01:03:05 -08:00
|
|
|
w := &statefulWriter{w: w2}
|
2020-09-13 13:50:19 -07:00
|
|
|
|
2020-11-02 01:03:05 -08:00
|
|
|
w.Write([]byte("<" + name))
|
2020-09-13 13:50:19 -07:00
|
|
|
|
|
|
|
for _, c := range children {
|
2020-11-16 03:58:42 -08:00
|
|
|
renderChild(w, c, AttributeType)
|
2020-09-13 13:50:19 -07:00
|
|
|
}
|
|
|
|
|
2020-12-08 12:59:47 -08:00
|
|
|
w.Write([]byte(">"))
|
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
if isVoidElement(name) {
|
2020-11-02 01:03:05 -08:00
|
|
|
return w.err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, c := range children {
|
2020-11-16 03:58:42 -08:00
|
|
|
renderChild(w, c, ElementType)
|
2020-09-13 13:50:19 -07:00
|
|
|
}
|
|
|
|
|
2020-11-02 01:03:05 -08:00
|
|
|
w.Write([]byte("</" + name + ">"))
|
|
|
|
return w.err
|
2021-01-07 01:20:03 -08:00
|
|
|
})
|
2020-09-13 13:50:19 -07:00
|
|
|
}
|
|
|
|
|
2020-11-02 01:03:05 -08:00
|
|
|
// renderChild c to the given writer w if the node type is t.
|
2020-11-16 03:58:42 -08:00
|
|
|
func renderChild(w *statefulWriter, c Node, t NodeType) {
|
2020-11-02 01:03:05 -08:00
|
|
|
if w.err != nil || c == nil {
|
2020-11-16 03:38:24 -08:00
|
|
|
return
|
2020-10-23 03:14:23 -07:00
|
|
|
}
|
2020-11-02 01:03:05 -08:00
|
|
|
|
2020-10-22 00:07:57 -07:00
|
|
|
if g, ok := c.(group); ok {
|
|
|
|
for _, groupC := range g.children {
|
2020-11-16 03:38:24 -08:00
|
|
|
renderChild(w, groupC, t)
|
2020-10-22 00:07:57 -07:00
|
|
|
}
|
2020-11-16 03:38:24 -08:00
|
|
|
return
|
2020-11-02 01:03:05 -08:00
|
|
|
}
|
|
|
|
|
2020-11-16 03:38:24 -08:00
|
|
|
switch t {
|
2020-11-16 03:58:42 -08:00
|
|
|
case ElementType:
|
|
|
|
if p, ok := c.(nodeTypeDescriber); !ok || p.Type() == ElementType {
|
2020-11-16 03:38:24 -08:00
|
|
|
w.err = c.Render(w.w)
|
|
|
|
}
|
2020-11-16 03:58:42 -08:00
|
|
|
case AttributeType:
|
|
|
|
if p, ok := c.(nodeTypeDescriber); ok && p.Type() == AttributeType {
|
2020-11-16 03:38:24 -08:00
|
|
|
w.err = c.Render(w.w)
|
|
|
|
}
|
2020-11-02 01:03:05 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-17 05:04:24 -07:00
|
|
|
// statefulWriter only writes if no errors have occurred earlier in its lifetime.
|
2020-11-02 01:03:05 -08:00
|
|
|
type statefulWriter struct {
|
|
|
|
w io.Writer
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *statefulWriter) Write(p []byte) {
|
|
|
|
if w.err != nil {
|
2020-10-22 00:07:57 -07:00
|
|
|
return
|
|
|
|
}
|
2020-11-02 01:03:05 -08:00
|
|
|
_, w.err = w.w.Write(p)
|
2020-10-22 00:07:57 -07:00
|
|
|
}
|
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
// 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 = map[string]struct{}{
|
|
|
|
"area": {},
|
|
|
|
"base": {},
|
|
|
|
"br": {},
|
|
|
|
"col": {},
|
|
|
|
"command": {},
|
|
|
|
"embed": {},
|
|
|
|
"hr": {},
|
|
|
|
"img": {},
|
|
|
|
"input": {},
|
|
|
|
"keygen": {},
|
|
|
|
"link": {},
|
|
|
|
"meta": {},
|
|
|
|
"param": {},
|
|
|
|
"source": {},
|
|
|
|
"track": {},
|
|
|
|
"wbr": {},
|
|
|
|
}
|
|
|
|
|
|
|
|
func isVoidElement(name string) bool {
|
|
|
|
_, ok := voidElements[name]
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
2020-12-08 12:59:47 -08:00
|
|
|
// Attr creates an attribute DOM Node with a name and optional value.
|
|
|
|
// If only a name is passed, it's a name-only (boolean) attribute (like "required").
|
|
|
|
// If a name and value are passed, it's a name-value attribute (like `class="header"`).
|
|
|
|
// More than one value make Attr panic.
|
2020-09-13 13:50:19 -07:00
|
|
|
// Use this if no convenience creator exists.
|
|
|
|
func Attr(name string, value ...string) Node {
|
|
|
|
switch len(value) {
|
|
|
|
case 0:
|
2020-10-29 05:07:22 -07:00
|
|
|
return &attr{name: name}
|
2020-09-13 13:50:19 -07:00
|
|
|
case 1:
|
2020-10-29 05:07:22 -07:00
|
|
|
return &attr{name: name, value: &value[0]}
|
2020-09-13 13:50:19 -07:00
|
|
|
default:
|
|
|
|
panic("attribute must be just name or name and value pair")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type attr struct {
|
|
|
|
name string
|
|
|
|
value *string
|
|
|
|
}
|
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
// Render satisfies Node.
|
2020-11-02 01:03:05 -08:00
|
|
|
func (a *attr) Render(w io.Writer) error {
|
2020-09-13 13:50:19 -07:00
|
|
|
if a.value == nil {
|
2020-11-02 01:03:05 -08:00
|
|
|
_, err := w.Write([]byte(" " + a.name))
|
|
|
|
return err
|
2020-09-13 13:50:19 -07:00
|
|
|
}
|
2021-05-18 05:21:53 -07:00
|
|
|
_, err := w.Write([]byte(" " + a.name + `="` + template.HTMLEscapeString(*a.value) + `"`))
|
2020-11-02 01:03:05 -08:00
|
|
|
return err
|
2020-09-13 13:50:19 -07:00
|
|
|
}
|
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
// Type satisfies nodeTypeDescriber.
|
2020-11-16 03:58:42 -08:00
|
|
|
func (a *attr) Type() NodeType {
|
|
|
|
return AttributeType
|
2020-09-24 04:19:52 -07:00
|
|
|
}
|
|
|
|
|
2020-09-18 05:38:09 -07:00
|
|
|
// String satisfies fmt.Stringer.
|
2020-10-29 05:07:22 -07:00
|
|
|
func (a *attr) String() string {
|
2020-11-02 01:03:05 -08:00
|
|
|
var b strings.Builder
|
|
|
|
_ = a.Render(&b)
|
|
|
|
return b.String()
|
2020-09-18 05:38:09 -07:00
|
|
|
}
|
|
|
|
|
2020-09-13 13:50:19 -07:00
|
|
|
// Text creates a text DOM Node that Renders the escaped string t.
|
2021-01-07 01:20:03 -08:00
|
|
|
func Text(t string) Node {
|
|
|
|
return NodeFunc(func(w io.Writer) error {
|
2020-11-02 01:03:05 -08:00
|
|
|
_, err := w.Write([]byte(template.HTMLEscapeString(t)))
|
|
|
|
return err
|
2021-01-07 01:20:03 -08:00
|
|
|
})
|
2020-09-13 13:50:19 -07:00
|
|
|
}
|
|
|
|
|
2020-09-23 13:10:35 -07:00
|
|
|
// Textf creates a text DOM Node that Renders the interpolated and escaped string t.
|
2021-01-07 01:20:03 -08:00
|
|
|
func Textf(format string, a ...interface{}) Node {
|
|
|
|
return NodeFunc(func(w io.Writer) error {
|
2020-11-02 01:03:05 -08:00
|
|
|
_, err := w.Write([]byte(template.HTMLEscapeString(fmt.Sprintf(format, a...))))
|
|
|
|
return err
|
2021-01-07 01:20:03 -08:00
|
|
|
})
|
2020-09-23 13:10:35 -07:00
|
|
|
}
|
|
|
|
|
2020-12-08 12:59:47 -08:00
|
|
|
// Raw creates a text DOM Node that just Renders the unescaped string t.
|
2021-01-07 01:20:03 -08:00
|
|
|
func Raw(t string) Node {
|
|
|
|
return NodeFunc(func(w io.Writer) error {
|
2020-11-02 01:03:05 -08:00
|
|
|
_, err := w.Write([]byte(t))
|
|
|
|
return err
|
2021-01-07 01:20:03 -08:00
|
|
|
})
|
2020-09-13 13:50:19 -07:00
|
|
|
}
|
2020-09-21 02:27:37 -07:00
|
|
|
|
2020-10-22 00:07:57 -07:00
|
|
|
type group struct {
|
|
|
|
children []Node
|
|
|
|
}
|
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
// String satisfies fmt.Stringer.
|
2020-11-02 01:03:05 -08:00
|
|
|
func (g group) String() string {
|
|
|
|
panic("cannot render group directly")
|
|
|
|
}
|
|
|
|
|
2022-05-27 05:16:19 -07:00
|
|
|
// Render satisfies Node.
|
2020-11-02 01:03:05 -08:00
|
|
|
func (g group) Render(io.Writer) error {
|
|
|
|
panic("cannot render group directly")
|
2020-10-22 00:07:57 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Group multiple Nodes into one Node. Useful for concatenation of Nodes in variadic functions.
|
|
|
|
// The resulting Node cannot Render directly, trying it will panic.
|
|
|
|
// Render must happen through a parent element created with El or a helper.
|
|
|
|
func Group(children []Node) Node {
|
|
|
|
return group{children: children}
|
|
|
|
}
|
2020-11-02 01:59:16 -08:00
|
|
|
|
2020-12-22 01:53:22 -08:00
|
|
|
// If condition is true, return the given Node. Otherwise, return nil.
|
|
|
|
// This helper function is good for inlining elements conditionally.
|
|
|
|
func If(condition bool, n Node) Node {
|
|
|
|
if condition {
|
|
|
|
return n
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|