diff --git a/lib.go b/lib.go index 462e0b7..047af00 100644 --- a/lib.go +++ b/lib.go @@ -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 int + + // 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 i := range a.names { - if a.names[i] == name { - a.names = append(a.names[:i], a.names[i+1:]...) - break + 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)...) } -// 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) - return r.err + 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) } diff --git a/lib_test.go b/lib_test.go index ab4f2db..3acec4e 100644 --- a/lib_test.go +++ b/lib_test.go @@ -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() != "\n

foo bar baz

" { + 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() != "\n\n\t\n\t\t

foo bar baz

\n\t\n" { + t.Fatal(b.String()) + } + }) }) t.Run("templates", func(t *testing.T) { diff --git a/notes.txt b/notes.txt index dcdde8d..5e9a6ea 100644 --- a/notes.txt +++ b/notes.txt @@ -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