1
0
html/lib.go

481 lines
15 KiB
Go

// 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})
}
// 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 &nbsp;. Spaces around tags, in special cases, can behave different from when using
// unindented rendering.
//
// It returns an error only when either one or more tags are invalid, or the underlying writer returns an
// error.
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. Tabs are used as
// indentation string, and a paragraph with of 120. The minimum paragraph width is 60. Indentation and width
// concerns only the HTML text, not the displayed document.
//
// It returns an error only when either one or more tags are invalid, or the underlying writer returns an
// error.
func Write(out io.Writer, t ...Tag) error {
return WriteIndent(out, Indentation{Indent: "\t"}, t...)
}
// WriteRaw renders html with t as the root nodes without indentation or wrapping.
//
// It returns an error only when either one or more tags are invalid, or the underlying writer returns an
// error.
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)
}