481 lines
15 KiB
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 . 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)
|
|
}
|