// Package html provides functions for programmatically composing and rendering HTML and HTML templates. package html import ( "bytes" "fmt" "io" "strings" ) // Attributes contain one or more named attributes of HTML tags. // // Use the Attr constructure to define attributes. Attributes can be assigned to tags by calling the tag itself // as a function with one or more Attributes instances as arguments, or by calling the SetAttribute convenience // function. In both cases, the original tag remains unchanged, and only the resulting tag will contain the // additional attributes. For setting CSS class attributes, the SetClass and the AddClass convenience functions // can also be used. type Attributes struct { names []string values map[string]any } // Tag instances represent HTML tags. // // The tag subpackage contains a set of the common tags used in HTML. It is recommended to use the tag // subpackage in code files dedicated to HTML composition and import the tag subpackage in these code files // inline, with the '.' notation. // // Custom tags can be defined with the Define function. // // When applying a tag value as a function, a new tag is created, and the original tag remains unchanged. // Therefore tags are immutable. When applying it without arguments, the resulting tag will be equivalent to the // original tag. For the rules of equivalence, see the documentation of the Eq function. // // When applying a tag value as a function with arguments, the arguments can be attributes or children. Children // can be other tags or any value. When a tag is rendered, non-tag children will be rendered with the fmt.Sprint // function. The original tag will remain unchanged, while the resulting tag will have the attributes and // children appended to the attributes and children of the original tag. This way a tag can be used as a // template for further, more specialized tags of the same name. A tag is a valid tag as is, it does not need to // be applied as a function unless additional attributers or children need to be added. // // Non-tag children are HTML escaped when rendering, unless the tag is marked as verbatim, with the Verbatim // function, or as script with the ScriptContent function. type Tag func(...any) Tag // Template instances are templates that convert any input data of a specified type to tags of custom internal // structure. Templates processing data consisting of a list of items must return the resulting tags wrapped by // a single tag. type Template[Data any] func(Data) Tag // Indentation is used to specify the indentation rules when using indented/wrapped rendering. type Indentation struct { // PWidth defines the max width of flow elements and text in the rendered HTML. That is the HTML text, and // not the formatted text that the HTML gives when displayed in a browser. The length of indentation is // subtraced from PWidth for indented child elements. If Indent is defined, it defaults to 120. PWidth int // MinPWidth defines the minimum width of rendered HTML flow elements and text. If Indent is defined, it // defaults to 60. MinPWidth int // Indent is the string used as leading space during indentation. Tabs count as 8. Indent string } // Attr defines Attributes to be passed in to tags for setting HTML attributes. Every argument at odd positions // is considered to be an attribute name, while every argument at even positions is the value associated with // the preceding name. // // In case of odd total number of arguments, the last one will be considered as a boolean true value, e.g. // Textarea(Attr("disabled")). // // Both the attribute names are rendered using the fmt.Sprint function. Names must be valid symbols. Values are // escaped when necessary. 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 } // Names returns a copy of the names of all the names in an attribute set. func (a Attributes) Names() []string { if len(a.names) == 0 { return nil } n := make([]string, len(a.names)) copy(n, a.names) return n } // Has returns true if a name appears in an attribute set. func (a Attributes) Has(name string) bool { _, ok := a.values[name] return ok } // Value returns the attribute value associated with a name, or nil if the attribute does not exist. func (a Attributes) Value(name string) any { return a.values[name] } // Define creates a new tag with a custom name and an optional initial set of attributes and children. The name // must be a valid symbol. 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...)...) } } // Declaration can be used to define custom SGML declarations. Declarations are not real HTML tags. Their name // as returned by the Name function will be universally '!'. The declaration children will not be escaped, it is // the responsibility of the caller code to always pass in valid declaration items. For comments, use the // Comment function, and for the doctype declaration use the Doctype function. 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...))) } // Comment defines HTML comments. It does not return a real HTML tag, but it can be used as such during // composing HTML documents or fragments. func Comment(children ...any) Tag { return Inline( Declaration( append( append([]any{"--"}, children...), "--", )..., ), ) } // Doctype defines an HTML doctype declaration. It does not return a real HTML tag, but it can be used as such // during rendering HTML documents. E.g. Doctype("html") for HTML5. The usage of other doctypes is not // recommended to be used with this package, because the package generally assumes HTML5 output. 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...) } // String returns the rendered HTML text. // // It is meant to be used for debugging. Use the Write functions to avoid buffering up an entire HTML document // as string. func (t Tag) String() string { buf := bytes.NewBuffer(nil) WriteRaw(buf, t) return buf.String() } // Name returns the name of a tag. func Name(t Tag) string { q := nameQuery{} t()(&q) return q.value } // AllAttributes 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 } // Attribute returns the value of a named attribute if exists, nil otherwise. func Attribute(t Tag, name string) any { q := attributeQuery{name: name} t()(&q) return q.value } // SetAttribute creates a new tag with all the existing attributes and child nodes of the input tag, and the new // attribute value. // // The input tag remains unchanged. func SetAttribute(t Tag, name string, value any) Tag { return t()(Attr(name, value)) } // DeleteAttribute creates a new tag with all the existing attributes and child nodes of the input tag, except // the attributes that need to be deleted. // // The input tag remains unchanged. func DeleteAttribute(t Tag, name ...string) Tag { n := Name(t) a := AllAttributes(t) c := Children(t) for _, n := range name { delete(a.values, n) var nn []string for i := range a.names { if a.names[i] == n { continue } nn = append(nn, a.names[i]) } a.names = nn } return Define(n, append(c, a)...) } // Class is the same as Attribute(t, "class"). When the class attribute is not set, it returns an empty string. func Class(t Tag) string { c := Attribute(t, "class") if c == nil { return "" } return fmt.Sprint(c) } // SetClass is the same as SetAttribute(t, "class", class). // // The input tag remains unchanged. func SetClass(t Tag, class string) Tag { return SetAttribute(t, "class", class) } // AddClass is like SetClass, but it appends the new class to the existing classes separated by space, // regardless if the same class exists. // // The input tag remains unchanged. func AddClass(t Tag, class string) Tag { current := Class(t) if current != "" { class = fmt.Sprintf("%s %s", current, class) } return SetClass(t, class) } // DeleteClass is like DeleteAttribute, but it only deletes the specified class from the class attribute value. // // The input tag remains unchanged. 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, " ")) } // Children 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 } // Preformatted creates a new tag from t that will prevent changing the whitespaces of text content enclosed by // a tag. // // The input tag remains unchanged. func Preformatted(t Tag) Tag { return t()(renderGuide{preformatted: true}) } // Verbatim creates a new tag from t marking the new one verbatim. The content of verbatim tags is rendered // without HTML escaping. This may cause security issues when using it in an incosiderate way. Verbatim content // gets indented when rendering with indentation. // // The input tag remains unchanged. func Verbatim(t Tag) Tag { return t()(renderGuide{verbatim: true}) } // ScriptContent marks a tag as script-style content for rendering. Script-style content is not escaped and not // indented. // // The input tag remains unchanged. func ScriptContent(t Tag) Tag { return t()(renderGuide{script: true}) } // InlineChildren marks a tag to have its children rendered as an inline flow, while the tag behaves as a block // from the perspective of the enclosing tags. It takes effect only when rendering with indentation and // wrapping. // // The input tag remains unchanged. func InlineChildren(t Tag) Tag { return t()(renderGuide{inlineChildren: true}) } // Inline tags are not broken into separate lines when rendering with indentation and wrapping. // // The input tag remains unchanged. func Inline(t Tag) Tag { return t()(renderGuide{inline: true}) } // Void tags do not accept children. // // The input tag remains unchanged. func Void(t Tag) Tag { return t()(renderGuide{void: true}) } // Indent provides default indentation with the tabs as indentation string, a paragraph with of 120 and a // minimum paragraph with of 60. Indentation and with concerns only the HTML text, not the displayed document. func Indent() Indentation { return Indentation{Indent: "\t"} } // WriteIndent renders html with t as the root nodes using the specified indentation and wrapping. // // Non-tag child nodes are rendered via fmt.Sprint. Consecutive spaces are considered to be so on purpose, and // are converted into  . Spaces around tags, in special cases, can behave different from when using // unindented rendering. func WriteIndent(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, } for i, ti := range t { if i > 0 { if _, err := out.Write([]byte{'\n'}); err != nil { return err } } ti()(&r) if r.err != nil { return r.err } } return nil } // Write renders html with t as the root nodes with the default indentation and wrapping. func Write(out io.Writer, t ...Tag) error { return WriteIndent(out, Indent(), t...) } // WriteRaw renders html with t as the root nodes without indentation or wrapping. func WriteRaw(out io.Writer, t ...Tag) error { return WriteIndent(out, Indentation{}, t...) } // Eq returns if one or more tags are considered equivalent. Equivalent nodes, in the most cases, will result in // the same HTML text when rendered, but not every tag that renders the same is equivalent. The following rules // make two tags equivalent: same name, same number of attributes, same name and value of attributes, same order // of attributes, same number of children, children at the same position in the children list are equal by '==', // and same rendering rules. Since the children are compared via '==', it is recommended to keep the children // that were passed to a tag unchanged. func Eq(t ...Tag) bool { tt := make([]Tag, len(t)) for i := range t { tt[i] = t[i]() } return eq(tt...) } // FromTemplate turns a template into a tag that can be used for composition, a template tag. The template tag // can be used multiple times for template binding, but the tag resulting from a template binding cannot be used // for binding anymore, only for setting attributes, rendering rules or adding additional children to the top // level tag. 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...) } } // Map takes a list of data items and returns a list of tags where each have the data item at the same position // passed in a child. It can be used for template binding of list input. func Map[Data any](data []Data, tag Tag) []Tag { var ret []Tag for _, d := range data { ret = append(ret, tag(d)) } return ret } // MapChildren is like Map, but converts the resulting tag slice to a slice of 'any', such making it easier to // use during tag composition. 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 } // Escape escapes HTML. func Escape(s string) string { return escape(s, true) } // EscapeAttribute escape attribute values. It does not escape single-quotes, because it assumes attribute // values are always rendered with double-quotes. func EscapeAttribute(s string) string { return escapeAttribute(s) }