package html import "strings" // Node is anything renderable. type Node interface { Render(ctx *Context) string } // voidElements is the set of HTML elements that must not have a closing tag. var voidElements = map[string]bool{ "area": true, "base": true, "br": true, "col": true, "embed": true, "hr": true, "img": true, "input": true, "link": true, "meta": true, "source": true, "track": true, "wbr": true, } // escapeAttr escapes a string for use in an HTML attribute value. func escapeAttr(s string) string { s = strings.ReplaceAll(s, "&", "&") s = strings.ReplaceAll(s, "\"", """) s = strings.ReplaceAll(s, "'", "'") s = strings.ReplaceAll(s, "<", "<") s = strings.ReplaceAll(s, ">", ">") return s } // --- rawNode --- type rawNode struct { content string } // Raw creates a node that renders without escaping (escape hatch for trusted content). func Raw(content string) Node { return &rawNode{content: content} } func (n *rawNode) Render(_ *Context) string { return n.content } // --- elNode --- type elNode struct { tag string children []Node attrs map[string]string } // El creates an HTML element node with children. func El(tag string, children ...Node) Node { return &elNode{ tag: tag, children: children, attrs: make(map[string]string), } } func (n *elNode) Render(ctx *Context) string { var b strings.Builder b.WriteByte('<') b.WriteString(n.tag) for key, val := range n.attrs { b.WriteByte(' ') b.WriteString(key) b.WriteString(`="`) b.WriteString(escapeAttr(val)) b.WriteByte('"') } b.WriteByte('>') if voidElements[n.tag] { return b.String() } for _, child := range n.children { b.WriteString(child.Render(ctx)) } b.WriteString("') return b.String() }