// 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 }