221 lines
5.7 KiB
Go
221 lines
5.7 KiB
Go
// 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
|
|
}
|