288 lines
6.8 KiB
Go
288 lines
6.8 KiB
Go
// Package html provides functions for programmatically composing and rendering HTML.
|
|
package html
|
|
|
|
import (
|
|
"bytes"
|
|
"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
|
|
|
|
type Indentation struct {
|
|
PWidth int
|
|
MinPWidth int
|
|
|
|
// not used for the top level
|
|
Indent string
|
|
}
|
|
|
|
func (t Tag) String() string {
|
|
buf := bytes.NewBuffer(nil)
|
|
r := renderer{out: buf}
|
|
t()(r)
|
|
return buf.String()
|
|
}
|
|
|
|
// 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 Define(name string, children ...any) Tag {
|
|
if handleQuery(name, children) {
|
|
children = children[:len(children)-1]
|
|
}
|
|
|
|
return func(children1 ...any) Tag {
|
|
return Define(name, append(children, children1...)...)
|
|
}
|
|
}
|
|
|
|
func Declaration(children ...any) Tag {
|
|
var name string
|
|
if len(children) == 0 {
|
|
name = "!"
|
|
} else {
|
|
name, children = fmt.Sprintf("!%v", children[0]), children[1:]
|
|
}
|
|
|
|
a := make([]any, len(children)*2)
|
|
for i, c := range children {
|
|
a[2*i] = c
|
|
a[2*i+1] = true
|
|
}
|
|
|
|
return Void(Define(name, Attr(a...)))
|
|
}
|
|
|
|
func Doctype(children ...any) Tag {
|
|
return Declaration(append([]any{"doctype"}, children...)...)
|
|
}
|
|
|
|
func Comment(children ...any) Tag {
|
|
return Inline(Declaration(append(append([]any{"--"}, children...), "--")))
|
|
}
|
|
|
|
// 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 Define(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 Indentation, t Tag) error {
|
|
if indent.PWidth < indent.MinPWidth {
|
|
indent.PWidth = indent.MinPWidth
|
|
}
|
|
|
|
if indent.Indent != "" && indent.PWidth == 0 {
|
|
indent.PWidth = 120
|
|
indent.MinPWidth = 60
|
|
}
|
|
|
|
r := renderer{
|
|
out: out,
|
|
indent: indent,
|
|
pwidth: indent.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, Indentation{}, 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})
|
|
}
|
|
|
|
func InlineChildren(t Tag) Tag {
|
|
return t()(renderGuide{inlineChildren: 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](t Template[Data]) Tag {
|
|
return func(a ...any) Tag {
|
|
var (
|
|
d Data
|
|
ok bool
|
|
)
|
|
|
|
for i := range a {
|
|
d, ok = a[0].(Data)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
a = append(a[:i], a[i+1:]...)
|
|
break
|
|
}
|
|
|
|
return t(d)(a...)
|
|
}
|
|
}
|
|
|
|
// in the functional programming sense
|
|
func Map[Data any](data []Data, tag Tag) []Tag {
|
|
var ret []Tag
|
|
for _, d := range data {
|
|
ret = append(ret, tag(d))
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func MapChildren[Data any](data []Data, tag Tag) []any {
|
|
c := Map(data, tag)
|
|
|
|
var a []any
|
|
for _, ci := range c {
|
|
a = append(a, ci)
|
|
}
|
|
|
|
return a
|
|
}
|