components/components.go (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | // Package components provides high-level components and helpers that are composed of low-level elements and attributes.
package components // import "alin.ovh/gomponents/components"
import (
"io"
"sort"
"strings"
g "alin.ovh/gomponents"
. "alin.ovh/gomponents/html"
)
// HTML5Props for [HTML5].
// Title is set no matter what, Description and Language elements only if the strings are non-empty.
type HTML5Props struct {
Title string
Description string
Language string
Head []g.Node
Body []g.Node
HTMLAttrs []g.Node
}
// HTML5 document template.
func HTML5(p HTML5Props) g.Node {
return Doctype(
HTML(g.If(p.Language != "", Lang(p.Language)), g.Group(p.HTMLAttrs),
Head(
Meta(Charset("utf-8")),
Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
TitleEl(g.Text(p.Title)),
g.If(p.Description != "", Meta(Name("description"), Content(p.Description))),
g.Group(p.Head),
),
Body(g.Group(p.Body)),
),
)
}
// Classes is a map of strings to booleans, which Renders to an attribute with name "class".
// The attribute value is a sorted, space-separated string of all the map keys,
// for which the corresponding map value is true.
type Classes map[string]bool
// Render satisfies [g.Node].
func (c Classes) Render(w io.Writer) error {
_, err := c.WriteTo(w)
return err
}
// WriteTo satisfies [io.WriterTo].
func (c Classes) WriteTo(w io.Writer) (int64, error) {
included := make([]string, 0, len(c))
for c, include := range c {
if include {
included = append(included, c)
}
}
sort.Strings(included)
return Class(strings.Join(included, " ")).(g.NodeWriter).WriteTo(w)
}
func (c Classes) Type() g.NodeType {
return g.AttributeType
}
// String satisfies [fmt.Stringer].
func (c Classes) String() string {
var b strings.Builder
_ = c.Render(&b)
return b.String()
}
// JoinAttrs with the given name only on the first level of the given nodes.
// This means that attributes on non-direct descendants are ignored.
// Attribute values are joined by spaces.
// Note that this renders all first-level attributes to check whether they should be processed.
func JoinAttrs(name string, children ...g.Node) g.Node {
var attrValues []string
var result []g.Node
firstAttrIndex := -1
// Process all children
for _, child := range children {
// Handle groups explicitly because they may contain attributes
if group, ok := child.(g.Group); ok {
for _, groupChild := range group {
isGivenAttr, attrValue := extractAttrValue(name, groupChild)
if !isGivenAttr || attrValue == "" {
result = append(result, groupChild)
continue
}
attrValues = append(attrValues, attrValue)
if firstAttrIndex == -1 {
firstAttrIndex = len(result)
result = append(result, nil)
}
}
continue
}
// Handle non-group nodes essentially the same way
isGivenAttr, attrValue := extractAttrValue(name, child)
if !isGivenAttr || attrValue == "" {
result = append(result, child)
continue
}
attrValues = append(attrValues, attrValue)
if firstAttrIndex == -1 {
firstAttrIndex = len(result)
result = append(result, nil)
}
}
// If no attributes were found, just return the result now
if firstAttrIndex == -1 {
return g.Group(result)
}
// Insert joined attribute at the position of the first attribute
result[firstAttrIndex] = g.Attr(name, strings.Join(attrValues, " "))
return g.Group(result)
}
type nodeTypeDescriber interface {
Type() g.NodeType
}
func extractAttrValue(name string, n g.Node) (bool, string) {
// Ignore everything that is not an attribute
if n, ok := n.(nodeTypeDescriber); !ok || n.Type() == g.ElementType {
return false, ""
}
var b strings.Builder
if err := n.Render(&b); err != nil {
return false, ""
}
rendered := b.String()
if !strings.HasPrefix(rendered, " "+name+`="`) || !strings.HasSuffix(rendered, `"`) {
return false, ""
}
v := strings.TrimPrefix(rendered, " "+name+`="`)
v = strings.TrimSuffix(v, `"`)
return true, v
}
|