// 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 struct { names []string values 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 } // 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, true) } var am Attributes am.values = make(map[string]any) for i := 0; i < len(a); i += 2 { name := fmt.Sprint(a[i]) am.names = append(am.names, name) am.values[name] = a[i+1] } return am } func (a Attributes) Names() []string { if len(a.names) == 0 { return nil } n := make([]string, len(a.names)) copy(n, a.names) return n } func (a Attributes) Has(name string) bool { _, ok := a.values[name] return ok } func (a Attributes) Value(name string) any { return a.values[name] } func (t Tag) String() string { buf := bytes.NewBuffer(nil) Render(buf, t) return buf.String() } // 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 { a := make([]any, len(children)*2) for i, c := range children { a[2*i] = c a[2*i+1] = true } return Void(Define("!", Attr(a...))) } func Comment(children ...any) Tag { return Inline( Declaration( append( append([]any{"--"}, children...), "--", )..., ), ) } func Doctype(children ...any) Tag { a := []any{"doctype"} for _, c := range children { s := fmt.Sprint(c) if !symbolExp.MatchString(s) { s = fmt.Sprintf("\"%s\"", EscapeAttribute(s)) } a = append(a, s) } return Declaration(a...) } // 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 := Attributes{values: make(map[string]any)} for _, name := range q.value.names { value := q.value.values[name] a.names = append(a.names, name) a.values[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.values, name) for i := range a.names { if a.names[i] == name { a.names = append(a.names[:i], a.names[i+1:]...) break } } return Define(n, append(c, a)...) } // the same as Attribute(t, "class") func Class(t Tag) string { c := Attribute(t, "class") if c == nil { return "" } return fmt.Sprint(c) } // 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 } func Indent() Indentation { return Indentation{Indent: "\t"} } // 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 ( data Data found bool children []any ) for _, ai := range a { if !found { if d, ok := ai.(Data); ok { data = d found = true continue } } children = append(children, ai) } return t(data)(children...) } } // 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 } func Escape(s string) string { return escape(s) } // does not escape single quotes func EscapeAttribute(s string) string { return escapeAttribute(s) }