// Package gomponents provides HTML components in Go, that render to HTML 5.
//
// The primary interface is a [Node]. It defines a function Render, which should render the [Node]
// to the given writer as a string.
//
// All DOM elements and attributes can be created by using the [El] and [Attr] functions.
//
// The functions [Text], [Textf], [Raw], and [Rawf] can be used to create text nodes, either HTML-escaped or unescaped.
//
// See also helper functions [Map], [If], and [Iff] for mapping data to nodes and inserting them conditionally.
//
// There's also the [Group] type, which is a slice of [Node]-s that can be rendered as one [Node].
//
// For basic HTML elements and attributes, see the package html.
//
// For higher-level HTML components, see the package components.
//
// For HTTP helpers, see the package http.
package gomponents // import "alin.ovh/gomponents"
import (
"fmt"
"html/template"
"io"
"iter"
"strings"
)
// Node is a DOM node that can Render itself to a [io.Writer].
type Node interface {
Render(w io.Writer) error
}
// NodeWriter is a [Node] that can also be used as an [io.WriterTo]
type NodeWriter interface {
Node
io.WriterTo
}
// NodeType describes what type of [Node] it is, currently either an [ElementType] or an [AttributeType].
// This decides where a [Node] should be rendered.
// Nodes default to being [ElementType].
type NodeType int
const (
ElementType = NodeType(iota)
AttributeType
)
// nodeTypeDescriber can be implemented by Nodes to let callers know whether the [Node] is
// an [ElementType] or an [AttributeType].
// See [NodeType].
type nodeTypeDescriber interface {
Type() NodeType
}
// NodeFunc is a render function that is also a [Node] of [ElementType].
type NodeFunc func(io.Writer) error
// Render satisfies [Node].
func (n NodeFunc) Render(w io.Writer) error {
return n(w)
}
// Type satisfies [nodeTypeDescriber].
func (n NodeFunc) Type() NodeType {
return ElementType
}
// String satisfies [fmt.Stringer].
func (n NodeFunc) String() string {
var b strings.Builder
_ = n.Render(&b)
return b.String()
}
// NodeWriterFunc is a render function that is also a [Node] of [ElementType].
type NodeWriterFunc func(io.Writer) (int64, error)
// Render satisfies [Node].
func (n NodeWriterFunc) Render(w io.Writer) error {
_, err := n(w)
return err
}
// WriteTo satisfies [io.WriterTo].
func (n NodeWriterFunc) WriteTo(w io.Writer) (int64, error) {
return n(w)
}
// Type satisfies [nodeTypeDescriber].
func (n NodeWriterFunc) Type() NodeType {
return ElementType
}
// String satisfies [fmt.Stringer].
func (n NodeWriterFunc) String() string {
var b strings.Builder
_ = n.Render(&b)
return b.String()
}
var (
lt = []byte("<")
gt = []byte(">")
ltSlash = []byte(" 1 {
panic("attribute must be just name or name and value pair")
}
return attrFunc(func(w io.Writer) (total int64, err error) {
var n int
sw, ok := w.(io.StringWriter)
n, err = w.Write(space)
if err != nil {
return
}
total += int64(n)
// Attribute name
if ok {
n, err = sw.WriteString(name)
} else {
n, err = w.Write([]byte(name))
}
if err != nil {
return
}
total += int64(n)
if len(value) == 0 {
return
}
n, err = w.Write(equalQuote)
if err != nil {
return
}
total += int64(n)
// Attribute value
if ok {
n, err = sw.WriteString(template.HTMLEscapeString(value[0]))
} else {
n, err = w.Write([]byte(template.HTMLEscapeString(value[0])))
}
if err != nil {
return
}
total += int64(n)
n, err = w.Write(quote)
if err != nil {
return
}
total += int64(n)
return
})
}
// attrFunc is a render function that is also a [Node] of [AttributeType].
// It's basically the same as [NodeWriterFunc], but for attributes.
type attrFunc func(io.Writer) (int64, error)
// Render satisfies [Node].
func (a attrFunc) Render(w io.Writer) error {
_, err := a(w)
return err
}
// WriteTo satisfies [io.WriterTo].
func (a attrFunc) WriteTo(w io.Writer) (int64, error) {
return a(w)
}
// Type satisfies [nodeTypeDescriber].
func (a attrFunc) Type() NodeType {
return AttributeType
}
// String satisfies [fmt.Stringer].
func (a attrFunc) String() string {
var b strings.Builder
_ = a.Render(&b)
return b.String()
}
// Text creates a text DOM [Node] that Renders the escaped string t.
func Text(t string) Node {
return NodeWriterFunc(func(w io.Writer) (int64, error) {
if w, ok := w.(io.StringWriter); ok {
n, err := w.WriteString(template.HTMLEscapeString(t))
return int64(n), err
}
n, err := w.Write([]byte(template.HTMLEscapeString(t)))
return int64(n), err
})
}
// Textf creates a text DOM [Node] that Renders the interpolated and escaped string format.
func Textf(format string, a ...any) Node {
return NodeWriterFunc(func(w io.Writer) (int64, error) {
if w, ok := w.(io.StringWriter); ok {
n, err := w.WriteString(template.HTMLEscapeString(fmt.Sprintf(format, a...)))
return int64(n), err
}
n, err := w.Write([]byte(template.HTMLEscapeString(fmt.Sprintf(format, a...))))
return int64(n), err
})
}
// Raw creates a text DOM [Node] that just Renders the unescaped string t.
func Raw(t string) Node {
return NodeWriterFunc(func(w io.Writer) (int64, error) {
if w, ok := w.(io.StringWriter); ok {
n, err := w.WriteString(t)
return int64(n), err
}
n, err := w.Write([]byte(t))
return int64(n), err
})
}
// Rawf creates a text DOM [Node] that just Renders the interpolated and unescaped string format.
func Rawf(format string, a ...any) Node {
return NodeWriterFunc(func(w io.Writer) (int64, error) {
if w, ok := w.(io.StringWriter); ok {
n, err := w.WriteString(fmt.Sprintf(format, a...))
return int64(n), err
}
n, err := fmt.Fprintf(w, format, a...)
return int64(n), err
})
}
// Map a slice of anything to a [Group] (which is just a slice of [Node]-s).
func Map[T any](ts []T, cb func(T) Node) Group {
nodes := make([]Node, 0, len(ts))
for _, t := range ts {
nodes = append(nodes, cb(t))
}
return nodes
}
// Map a slice of anything to a [Group] (which is just a slice of [Node]-s).
func MapWithIndex[T any](ts []T, cb func(int, T) Node) Group {
nodes := make([]Node, 0, len(ts))
for k, t := range ts {
nodes = append(nodes, cb(k, t))
}
return nodes
}
// Map a map of anything to a [Group] (which is just a slice of [Node]-s).
func MapMap[K comparable, T any](ts map[K]T, cb func(K, T) Node) Group {
nodes := make([]Node, 0, len(ts))
for k, t := range ts {
nodes = append(nodes, cb(k, t))
}
return nodes
}
// Map an iterator of anything to an IterNode (which is just an [iter.Seq] of [Node]-s)
func MapIter[T any](ts iter.Seq[T], cb func(T) Node) IterNode {
return IterNode{
func(yield func(Node) bool) {
for t := range ts {
if !yield(cb(t)) {
return
}
}
},
}
}
// Map an iterator of pairs of values to an IterNode (which is just an [iter.Seq] of [Node]-s)
func MapIter2[K, V any](ts iter.Seq2[K, V], cb func(K, V) Node) IterNode {
return IterNode{
func(yield func(Node) bool) {
for k, v := range ts {
if !yield(cb(k, v)) {
return
}
}
},
}
}
type IterNode struct {
iter.Seq[Node]
}
func (it IterNode) WriteTo(w io.Writer) (int64, error) {
var written int64
for node := range it.Seq {
n, err := node.(NodeWriter).WriteTo(w)
if err != nil {
return written, err
}
written += n
}
return written, nil
}
func (it IterNode) Render(w io.Writer) error {
_, err := it.WriteTo(w)
return err
}
// Group a slice of [Node]-s into one Node, while still being usable like a regular slice of [Node]-s.
// A [Group] can render directly, but if any of the direct children are [AttributeType], they will be ignored,
// to not produce invalid HTML.
type Group []Node
// String satisfies [fmt.Stringer].
func (g Group) String() string {
var b strings.Builder
_ = g.Render(&b)
return b.String()
}
// WriteTo satisfies [io.Writer].
func (g Group) WriteTo(w io.Writer) (int64, error) {
var total int64
for _, c := range g {
n, err := renderChild(w, c, ElementType)
if err != nil {
return total, err
}
total += n
}
return total, nil
}
// Render satisfies [Node].
func (g Group) Render(w io.Writer) error {
_, err := g.WriteTo(w)
return err
}
// If condition is true, return the given [Node]. Otherwise, return nil.
// This helper function is good for inlining elements conditionally.
// If it's important that the given [Node] is only evaluated if condition is true
// (for example, when using nilable variables), use [Iff] instead.
func If(condition bool, n Node, otherwise ...Node) Node {
var o Node
switch len(otherwise) {
case 0:
case 1:
o = otherwise[0]
default:
panic("If must have just one or two nodes")
}
if condition {
return n
}
return o
}
// Iff condition is true, call the given function. Otherwise, return nil.
// This helper function is good for inlining elements conditionally when the node depends on nilable data,
// or some other code that could potentially panic.
// If you just need simple conditional rendering, see [If].
func Iff(condition bool, f func() Node, otherwise ...func() Node) Node {
var o func() Node
switch len(otherwise) {
case 0:
case 1:
o = otherwise[0]
default:
panic("Iff must have just one or two nodes")
}
if condition {
return f()
}
if o != nil {
return o()
}
return nil
}
gomponents.go (view raw)