1
0
html/lib.go
2025-10-05 20:06:39 +02:00

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]any
// 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])] = 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) any {
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 fmt.Sprint(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 &nbsp;
// 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
}