1
0

document lib

This commit is contained in:
Arpad Ryszka 2025-10-07 02:00:37 +02:00
parent 33802919af
commit c923639245
3 changed files with 226 additions and 87 deletions

267
lib.go
View File

@ -1,4 +1,4 @@
// Package html provides functions for programmatically composing and rendering HTML.
// Package html provides functions for programmatically composing and rendering HTML and HTML templates.
package html
import (
@ -8,34 +8,69 @@ import (
"strings"
)
// when composing html, the Attr convenience function is recommended to construct input attributes
// 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
}
// 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
// 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
// not used for the top level
// Indent is the string used as leading space during indentation. Tabs count as 8.
Indent 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
// 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)
@ -52,6 +87,7 @@ func Attr(a ...any) Attributes {
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
@ -62,22 +98,19 @@ func (a Attributes) Names() []string {
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]
}
func (t Tag) String() string {
buf := bytes.NewBuffer(nil)
Render(buf, t)
return buf.String()
}
// defines a new tag with name and initial attributes and child nodes
// 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]
@ -88,6 +121,10 @@ func Define(name string, children ...any) Tag {
}
}
// 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 {
@ -98,6 +135,8 @@ func Declaration(children ...any) Tag {
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(
@ -109,6 +148,9 @@ func Comment(children ...any) Tag {
)
}
// 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 {
@ -123,14 +165,24 @@ func Doctype(children ...any) Tag {
return Declaration(a...)
}
// returns the name of a tag
// String returns the rendered HTML text.
//
// It is meant to be used for debugging. Use the Render function to avoid buffering up an entire HTML document
// as string.
func (t Tag) String() string {
buf := bytes.NewBuffer(nil)
Render(buf, t)
return buf.String()
}
// Name returns the name of a tag.
func Name(t Tag) string {
q := nameQuery{}
t()(&q)
return q.value
}
// returns all attributes of a tag
// AllAttributes returns all attributes of a tag.
func AllAttributes(t Tag) Attributes {
q := attributesQuery{}
t()(&q)
@ -144,36 +196,48 @@ func AllAttributes(t Tag) Attributes {
return a
}
// returns the value of a named attribute if exists, empty string otherwise
// 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
}
// creates a new tag with all the existing attributes and child nodes of the input tag, and the new attribute 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))
}
// 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 {
// 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)
delete(a.values, name)
for _, n := range name {
delete(a.values, n)
var nn []string
for i := range a.names {
if a.names[i] == name {
a.names = append(a.names[:i], a.names[i+1:]...)
break
if a.names[i] == n {
continue
}
nn = append(nn, a.names[i])
}
a.names = nn
}
return Define(n, append(c, a)...)
}
// the same as Attribute(t, "class")
// 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 {
@ -183,12 +247,17 @@ func Class(t Tag) string {
return fmt.Sprint(c)
}
// the same as SetAttribute(t, "class", class)
// 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)
}
// like SetClass, but it appends the new class to the existing classes, regardless if the same class exists
// 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 != "" {
@ -198,7 +267,9 @@ func AddClass(t Tag, class string) Tag {
return SetClass(t, class)
}
// like DeleteAttribute, but it only deletes the specified class from the class attribute
// 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, " ")
@ -213,7 +284,7 @@ func DeleteClass(t Tag, class string) Tag {
return SetClass(t, strings.Join(ccc, " "))
}
// returns the child nodes of a tag
// Children returns the child nodes of a tag.
func Children(t Tag) []any {
var q childrenQuery
t()(&q)
@ -222,16 +293,58 @@ func Children(t Tag) []any {
return c
}
// 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"}
}
// 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 {
// RenderIndent 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 RenderIndent(out io.Writer, indent Indentation, t ...Tag) error {
if indent.PWidth < indent.MinPWidth {
indent.PWidth = indent.MinPWidth
}
@ -247,45 +360,33 @@ func RenderIndent(out io.Writer, indent Indentation, t Tag) error {
pwidth: indent.PWidth,
}
t()(&r)
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
}
// renders html with t as the root node without indentation
func Render(out io.Writer, t Tag) error {
return RenderIndent(out, Indentation{}, t)
// Render renders html with t as the root nodes without indentation or wrapping.
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
// 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 {
@ -295,7 +396,10 @@ func Eq(t ...Tag) bool {
return eq(tt...)
}
// turns a template into a tag for composition
// 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 (
@ -320,7 +424,8 @@ func FromTemplate[Data any](t Template[Data]) Tag {
}
}
// in the functional programming sense
// 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 {
@ -330,6 +435,8 @@ func Map[Data any](data []Data, tag Tag) []Tag {
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)
@ -341,11 +448,13 @@ func MapChildren[Data any](data []Data, tag Tag) []any {
return a
}
// Escape escapes HTML.
func Escape(s string) string {
return escape(s)
}
// does not escape single quotes
// 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)
}

View File

@ -120,6 +120,15 @@ func TestLib(t *testing.T) {
}
})
t.Run("delete multiple attributes", func(t *testing.T) {
div := Div(Attr("foo", "bar", "baz", "qux", "quux", "corge"))
div = html.DeleteAttribute(div, "foo", "quux")
a := html.AllAttributes(div)
if a.Has("foo") || a.Has("quux") || !a.Has("baz") || a.Value("baz") != "qux" {
t.Fatal(a.Has("foo"), a.Has("quux"), a.Has("baz"), a.Value("baz"))
}
})
t.Run("setting attribute immutable", func(t *testing.T) {
div0 := Div(Attr("foo", "bar"))
div1 := div0(Attr("baz", "qux"))
@ -410,6 +419,32 @@ func TestLib(t *testing.T) {
t.Fatal(b.String())
}
})
t.Run("doc unindented", func(t *testing.T) {
doc := Html(Body(P("foo bar baz")))
var b bytes.Buffer
if err := html.Render(&b, Doctype("html"), doc); err != nil {
t.Fatal(err)
}
if b.String() != "<!doctype html>\n<html><body><p>foo bar baz</p></body></html>" {
t.Fatal(b.String())
}
})
t.Run("doc indented", func(t *testing.T) {
doc := Html(Body(P("foo bar baz")))
var b bytes.Buffer
if err := html.RenderIndent(&b, html.Indent(), Doctype("html"), doc); err != nil {
t.Fatal(err)
}
if b.String() != "<!doctype html>\n<html>\n\t<body>\n\t\t<p>foo bar baz</p>\n\t</body>\n</html>" {
t.Fatal(b.String())
}
})
})
t.Run("templates", func(t *testing.T) {

View File

@ -1,6 +1 @@
explain the immutability guarantee in the Go docs: for children yes, for children references no. The general
recommendation is not to mutate children. Ofc, creatively breaking the rules is always well appreciated by the
right audience
test wrapped templates
review which tags should be of type inline-children
export Escape and EscapeAttribute