all repos — gomponents @ v1.8.1

HTML components in pure Go

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
}