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