// Package html provides functions for programmatically composing and rendering HTML.
package html
import (
"fmt"
"io"
"strings"
)
// when composing html, the Attr convenience function is recommended to construct input attributes
type Attributes map[string]string
// immutable
// calling creates a new copy with the passed in attributes and child nodes applied only to the copy
// input parameters
// rendering of child nodes
// instances of tags can be used to create further tags with extended set of attributes and child tags
// builtin tags in the tags sub-package
// custom tags are supported via the NewTag constructor. Functions with the same signature, but created by other
// means, will not be rendered, unless they return a tag created by NewTag() or a builtin tag
type Tag func(...any) Tag
type Template[Data any] func(Data) Tag
// convenience function primarily aimed to help with construction of html with tags
// the names and values are applied using fmt.Sprint, tolerating fmt.Stringer implementations
func Attr(a ...any) Attributes {
if len(a)%2 != 0 {
a = append(a, "")
}
am := make(Attributes)
for i := 0; i < len(a); i += 2 {
am[fmt.Sprint(a[i])] = fmt.Sprint(a[i+1])
}
return am
}
// defines a new tag with name and initial attributes and child nodes
func NewTag(name string, children ...any) Tag {
if handleQuery(name, children) {
children = children[:len(children)-1]
}
return func(children1 ...any) Tag {
if name == "br" {
}
return NewTag(name, append(children, children1...)...)
}
}
// returns the name of a tag
func Name(t Tag) string {
q := nameQuery{}
t()(&q)
return q.value
}
// returns all attributes of a tag
func AllAttributes(t Tag) Attributes {
q := attributesQuery{}
t()(&q)
a := make(Attributes)
for name, value := range q.value {
a[name] = value
}
return a
}
// returns the value of a named attribute if exists, empty string otherwise
func Attribute(t Tag, name string) string {
q := attributeQuery{name: name}
t()(&q)
return q.value
}
// creates a new tag with all the existing attributes and child nodes of the input tag, and the new attribute value
func SetAttribute(t Tag, name string, value any) Tag {
return t()(Attr(name, value))
}
// creates a new tag with all the existing attributes and child nodes of the input tag, except the attribute to
// be deleted
func DeleteAttribute(t Tag, name string) Tag {
n := Name(t)
a := AllAttributes(t)
c := Children(t)
delete(a, name)
return NewTag(n, append(c, a)...)
}
// the same as Attribute(t, "class")
func Class(t Tag) string {
return Attribute(t, "class")
}
// the same as SetAttribute(t, "class", class)
func SetClass(t Tag, class string) Tag {
return SetAttribute(t, "class", class)
}
// like SetClass, but it appends the new class to the existing classes, regardless if the same class exists
func AddClass(t Tag, class string) Tag {
current := Class(t)
if current != "" {
class = fmt.Sprintf("%s %s", current, class)
}
return SetClass(t, class)
}
// like DeleteAttribute, but it only deletes the specified class from the class attribute
func DeleteClass(t Tag, class string) Tag {
c := Class(t)
cc := strings.Split(c, " ")
var ccc []string
for _, ci := range cc {
if ci != class {
ccc = append(ccc, ci)
}
}
return SetClass(t, strings.Join(ccc, " "))
}
// returns the child nodes of a tag
func Children(t Tag) []any {
var q childrenQuery
t()(&q)
c := make([]any, len(q.value))
copy(c, q.value)
return c
}
// renders html with t as the root node with indentation
// child nodes are rendered via fmt.Sprint, tolerating fmt.Stringer implementations
// consecutive spaces are considered to be so on purpose, and are converted into
// spaces around tags can behave different from when using unindented rendering
// as a last resort, one can use rendered html inside a verbatim tag
func RenderIndent(out io.Writer, indent string, pwidth int, t Tag) error {
r := renderer{out: out, indent: indent, pwidth: pwidth}
t()(&r)
return r.err
}
// renders html with t as the root node without indentation
func Render(out io.Writer, t Tag) error {
return RenderIndent(out, "", 0, t)
}
// creates a new tag from t marking it verbatim. The content of verbatim tags is rendered without HTML escaping.
// This may cause security issues when using it in an incosiderate way. The tag can contain non-tag child nodes.
// Verbatim content gets indented when rendering with indentation
func Verbatim(t Tag) Tag {
return t()(renderGuide{verbatim: true})
}
// marks a tag as script-style content for rendering. Script-style content is not escaped and not indented
func ScriptContent(t Tag) Tag {
return t()(renderGuide{script: true})
}
// inline tags are not broken into separate lines when rendering with indentation
// deprecated in HTML, but only used for indentation
func Inline(t Tag) Tag {
return t()(renderGuide{inline: true})
}
// void tags do not accept child nodes
// deprecated in HTML, but only used for indentation
func Void(t Tag) Tag {
return t()(renderGuide{void: true})
}
// same name, same attributes, same child tags, child nodes in the same order and equal by reference or value
// depending on the child node type
func Eq(t ...Tag) bool {
tt := make([]Tag, len(t))
for i := range t {
tt[i] = t[i]()
}
return eq(tt...)
}
// turns a template into a tag for composition
func FromTemplate[Data any](f Template[Data]) Tag {
return func(a ...any) Tag {
var (
t Data
ok bool
)
for i := range a {
t, ok = a[0].(Data)
if !ok {
continue
}
a = append(a[:i], a[i+1:]...)
break
}
return f(t)(a...)
}
}
// in the functional programming sense
func Map(data []any, tag Tag) []Tag {
var tags []Tag
for _, d := range data {
tags = append(tags, tag(d))
}
return tags
}