test lib
This commit is contained in:
parent
c5d2eb8d60
commit
33802919af
42
escape.go
42
escape.go
@ -16,23 +16,6 @@ type escapeWriter struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func attributeEscape(value string) string {
|
|
||||||
var rr []rune
|
|
||||||
r := []rune(value)
|
|
||||||
for i := range r {
|
|
||||||
switch r[i] {
|
|
||||||
case '"':
|
|
||||||
rr = append(rr, []rune(""")...)
|
|
||||||
case '&':
|
|
||||||
rr = append(rr, []rune("&")...)
|
|
||||||
default:
|
|
||||||
rr = append(rr, r[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(rr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newEscapeWriter(out io.Writer) *escapeWriter {
|
func newEscapeWriter(out io.Writer) *escapeWriter {
|
||||||
return &escapeWriter{out: bufio.NewWriter(out)}
|
return &escapeWriter{out: bufio.NewWriter(out)}
|
||||||
}
|
}
|
||||||
@ -122,3 +105,28 @@ func (w *escapeWriter) Flush() error {
|
|||||||
w.err = w.out.Flush()
|
w.err = w.out.Flush()
|
||||||
return w.err
|
return w.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func escape(s string) string {
|
||||||
|
var b bytes.Buffer
|
||||||
|
w := newEscapeWriter(&b)
|
||||||
|
w.Write([]byte(s))
|
||||||
|
w.Flush()
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeAttribute(value string) string {
|
||||||
|
var rr []rune
|
||||||
|
r := []rune(value)
|
||||||
|
for i := range r {
|
||||||
|
switch r[i] {
|
||||||
|
case '"':
|
||||||
|
rr = append(rr, []rune(""")...)
|
||||||
|
case '&':
|
||||||
|
rr = append(rr, []rune("&")...)
|
||||||
|
default:
|
||||||
|
rr = append(rr, r[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(rr)
|
||||||
|
}
|
||||||
|
|||||||
78
lib.go
78
lib.go
@ -38,7 +38,7 @@ type Indentation struct {
|
|||||||
// the names and values are applied using fmt.Sprint, tolerating fmt.Stringer implementations
|
// the names and values are applied using fmt.Sprint, tolerating fmt.Stringer implementations
|
||||||
func Attr(a ...any) Attributes {
|
func Attr(a ...any) Attributes {
|
||||||
if len(a)%2 != 0 {
|
if len(a)%2 != 0 {
|
||||||
a = append(a, "")
|
a = append(a, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
var am Attributes
|
var am Attributes
|
||||||
@ -73,8 +73,7 @@ func (a Attributes) Value(name string) any {
|
|||||||
|
|
||||||
func (t Tag) String() string {
|
func (t Tag) String() string {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
r := renderer{out: buf}
|
Render(buf, t)
|
||||||
t()(r)
|
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,28 +89,38 @@ func Define(name string, children ...any) Tag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Declaration(children ...any) Tag {
|
func Declaration(children ...any) Tag {
|
||||||
var name string
|
|
||||||
if len(children) == 0 {
|
|
||||||
name = "!"
|
|
||||||
} else {
|
|
||||||
name, children = fmt.Sprintf("!%v", children[0]), children[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
a := make([]any, len(children)*2)
|
a := make([]any, len(children)*2)
|
||||||
for i, c := range children {
|
for i, c := range children {
|
||||||
a[2*i] = c
|
a[2*i] = c
|
||||||
a[2*i+1] = true
|
a[2*i+1] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return Void(Define(name, Attr(a...)))
|
return Void(Define("!", Attr(a...)))
|
||||||
}
|
|
||||||
|
|
||||||
func Doctype(children ...any) Tag {
|
|
||||||
return Declaration(append([]any{"doctype"}, children...)...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Comment(children ...any) Tag {
|
func Comment(children ...any) Tag {
|
||||||
return Inline(Declaration(append(append([]any{"--"}, children...), "--")...))
|
return Inline(
|
||||||
|
Declaration(
|
||||||
|
append(
|
||||||
|
append([]any{"--"}, children...),
|
||||||
|
"--",
|
||||||
|
)...,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the name of a tag
|
// returns the name of a tag
|
||||||
@ -166,7 +175,12 @@ func DeleteAttribute(t Tag, name string) Tag {
|
|||||||
|
|
||||||
// the same as Attribute(t, "class")
|
// the same as Attribute(t, "class")
|
||||||
func Class(t Tag) string {
|
func Class(t Tag) string {
|
||||||
return fmt.Sprint(Attribute(t, "class"))
|
c := Attribute(t, "class")
|
||||||
|
if c == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprint(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// the same as SetAttribute(t, "class", class)
|
// the same as SetAttribute(t, "class", class)
|
||||||
@ -285,21 +299,24 @@ func Eq(t ...Tag) bool {
|
|||||||
func FromTemplate[Data any](t Template[Data]) Tag {
|
func FromTemplate[Data any](t Template[Data]) Tag {
|
||||||
return func(a ...any) Tag {
|
return func(a ...any) Tag {
|
||||||
var (
|
var (
|
||||||
d Data
|
data Data
|
||||||
ok bool
|
found bool
|
||||||
|
children []any
|
||||||
)
|
)
|
||||||
|
|
||||||
for i := range a {
|
for _, ai := range a {
|
||||||
d, ok = a[0].(Data)
|
if !found {
|
||||||
if !ok {
|
if d, ok := ai.(Data); ok {
|
||||||
continue
|
data = d
|
||||||
|
found = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a = append(a[:i], a[i+1:]...)
|
children = append(children, ai)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return t(d)(a...)
|
return t(data)(children...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -323,3 +340,12 @@ func MapChildren[Data any](data []Data, tag Tag) []any {
|
|||||||
|
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Escape(s string) string {
|
||||||
|
return escape(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// does not escape single quotes
|
||||||
|
func EscapeAttribute(s string) string {
|
||||||
|
return escapeAttribute(s)
|
||||||
|
}
|
||||||
|
|||||||
621
lib_test.go
621
lib_test.go
@ -5,64 +5,481 @@ import (
|
|||||||
"code.squareroundforest.org/arpio/html"
|
"code.squareroundforest.org/arpio/html"
|
||||||
. "code.squareroundforest.org/arpio/html/tag"
|
. "code.squareroundforest.org/arpio/html/tag"
|
||||||
"code.squareroundforest.org/arpio/notation"
|
"code.squareroundforest.org/arpio/notation"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLib(t *testing.T) {
|
func TestLib(t *testing.T) {
|
||||||
t.Run("templated tag", func(t *testing.T) {
|
t.Run("escape", func(t *testing.T) {
|
||||||
type (
|
t.Run("html", func(t *testing.T) {
|
||||||
member struct {
|
if html.Escape("<div>foo&bar</div>") != "<div>foo&bar</div>" {
|
||||||
name string
|
t.Fatal(html.Escape("<div>foo&bar</div>"))
|
||||||
level int
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("attribute", func(t *testing.T) {
|
||||||
|
if html.EscapeAttribute("\"foo&bar\"") != ""foo&bar"" {
|
||||||
|
t.Fatal(html.EscapeAttribute("\"foo&bar\""))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("tags", func(t *testing.T) {
|
||||||
|
t.Run("tag name", func(t *testing.T) {
|
||||||
|
if html.Name(Div) != "div" {
|
||||||
|
t.Fatal(html.Name(Div))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom tag", func(t *testing.T) {
|
||||||
|
foo := html.Define("foo")
|
||||||
|
if html.Name(foo) != "foo" {
|
||||||
|
t.Fatal(html.Name(foo))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid tag name", func(t *testing.T) {
|
||||||
|
foo := html.Define("foo+bar")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, foo); err == nil {
|
||||||
|
t.Fatal("failed to fail")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
s := fmt.Sprint(Div)
|
||||||
|
if s != "<div></div>" {
|
||||||
|
t.Fatal(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("attributes", func(t *testing.T) {
|
||||||
|
t.Run("empty attributes", func(t *testing.T) {
|
||||||
|
div := Div(Attr())
|
||||||
|
if len(html.AllAttributes(div).Names()) != 0 {
|
||||||
|
t.Fatal(html.AllAttributes(div).Names())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("set default value", func(t *testing.T) {
|
||||||
|
input := Input(Attr("disabled"))
|
||||||
|
if html.Attribute(input, "disabled") != true {
|
||||||
|
t.Fatal(html.Attribute(input, "disabled"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("set attribute standard way", func(t *testing.T) {
|
||||||
|
div := Div(Attr("foo", "bar"))
|
||||||
|
if html.Attribute(div, "foo") != "bar" {
|
||||||
|
t.Fatal(html.Attribute(div, "foo"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("set attribute function", func(t *testing.T) {
|
||||||
|
div := html.SetAttribute(Div, "foo", "bar")
|
||||||
|
if html.Attribute(div, "foo") != "bar" {
|
||||||
|
t.Fatal(html.Attribute(div, "foo"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("set multiple attributes", func(t *testing.T) {
|
||||||
|
div := Div(Attr("foo", "bar", "baz", "qux"))
|
||||||
|
if html.Attribute(div, "foo") != "bar" || html.Attribute(div, "baz") != "qux" {
|
||||||
|
t.Fatal(html.Attribute(div, "foo"), html.Attribute(div, "baz"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all attributes", func(t *testing.T) {
|
||||||
|
div := Div(Attr("foo", "bar", "baz", "qux"))
|
||||||
|
a := html.AllAttributes(div)
|
||||||
|
if len(a.Names()) != 2 {
|
||||||
|
t.Fatal(len(a.Names()))
|
||||||
}
|
}
|
||||||
|
|
||||||
team struct {
|
if !a.Has("foo") || !a.Has("baz") {
|
||||||
name string
|
t.Fatal(a.Has("foo"), a.Has("baz"))
|
||||||
rank int
|
|
||||||
members []member
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
memberHTML := html.FromTemplate(
|
if a.Names()[0] != "foo" || a.Names()[1] != "baz" {
|
||||||
func(m member) html.Tag {
|
t.Fatal(a.Names()[0], a.Names()[1])
|
||||||
return Li(
|
}
|
||||||
Div("Name: ", m.name),
|
|
||||||
Div("Level: ", m.level),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
teamHTML := html.FromTemplate(
|
if a.Value("foo") != "bar" || a.Value("baz") != "qux" {
|
||||||
func(t team) html.Tag {
|
t.Fatal(a.Value("foo"), a.Value("baz"))
|
||||||
return Div(
|
}
|
||||||
H3(t.name),
|
})
|
||||||
P("Rank: ", t.rank),
|
|
||||||
Ul(html.MapChildren(t.members, memberHTML)...),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
myTeam := team{
|
t.Run("delete attribute", func(t *testing.T) {
|
||||||
name: "Foo",
|
div := Div(Attr("foo", "bar", "baz", "qux"))
|
||||||
rank: 3,
|
div = html.DeleteAttribute(div, "foo")
|
||||||
members: []member{{
|
a := html.AllAttributes(div)
|
||||||
name: "Bar",
|
if a.Has("foo") || !a.Has("baz") || a.Value("baz") != "qux" {
|
||||||
level: 4,
|
t.Fatal(a.Has("foo"), a.Has("baz"), a.Value("baz"))
|
||||||
}, {
|
}
|
||||||
name: "Baz",
|
})
|
||||||
level: 1,
|
|
||||||
}, {
|
|
||||||
name: "Qux",
|
|
||||||
level: 4,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
t.Run("setting attribute immutable", func(t *testing.T) {
|
||||||
if err := html.RenderIndent(&b, html.Indent(), teamHTML(myTeam)); err != nil {
|
div0 := Div(Attr("foo", "bar"))
|
||||||
t.Fatal(err)
|
div1 := div0(Attr("baz", "qux"))
|
||||||
}
|
if html.Attribute(div0, "foo") != "bar" || html.Attribute(div0, "baz") != nil {
|
||||||
|
t.Fatal(html.Attribute(div0, "foo"), html.Attribute(div0, "baz"))
|
||||||
|
}
|
||||||
|
|
||||||
const expect = `<div>
|
if html.Attribute(div1, "foo") != "bar" || html.Attribute(div1, "baz") != "qux" {
|
||||||
|
t.Fatal(html.Attribute(div1, "foo"), html.Attribute(div1, "baz"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid attribute name", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, Div(Attr("foo+bar", "baz"))); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("classes", func(t *testing.T) {
|
||||||
|
t.Run("set class", func(t *testing.T) {
|
||||||
|
div := html.SetClass(Div, "foo bar")
|
||||||
|
if html.Class(div) != "foo bar" {
|
||||||
|
t.Fatal(html.Class(div))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("add class", func(t *testing.T) {
|
||||||
|
div := html.AddClass(Div, "foo")
|
||||||
|
div = html.AddClass(div, "bar")
|
||||||
|
if html.Class(div) != "foo bar" {
|
||||||
|
t.Fatal(html.Class(div))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("delete class", func(t *testing.T) {
|
||||||
|
div := html.SetClass(Div, "foo")
|
||||||
|
div = html.DeleteClass(div, "foo")
|
||||||
|
if html.Class(div) != "" {
|
||||||
|
t.Fatal(html.Class(div))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("delete class from multiple", func(t *testing.T) {
|
||||||
|
div := html.SetClass(Div, "foo bar")
|
||||||
|
div = html.DeleteClass(div, "foo")
|
||||||
|
if html.Class(div) != "bar" {
|
||||||
|
t.Fatal(html.Class(div))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("delete multiple of the same class from multiple classes", func(t *testing.T) {
|
||||||
|
div := html.SetClass(Div, "foo bar baz bar foo")
|
||||||
|
div = html.DeleteClass(div, "bar")
|
||||||
|
if html.Class(div) != "foo baz foo" {
|
||||||
|
t.Fatal(html.Class(div))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("setting class immutable", func(t *testing.T) {
|
||||||
|
div0 := html.SetClass(Div, "foo")
|
||||||
|
div1 := html.SetClass(div0, "bar")
|
||||||
|
if html.Class(div0) != "foo" || html.Class(div1) != "bar" {
|
||||||
|
t.Fatal(html.Class(div0), html.Class(div1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("children", func(t *testing.T) {
|
||||||
|
t.Run("no children", func(t *testing.T) {
|
||||||
|
if len(html.Children(Div)) != 0 {
|
||||||
|
t.Fatal(len(html.Children(Div)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get children", func(t *testing.T) {
|
||||||
|
div := Div("foo", "bar", "baz")
|
||||||
|
c := html.Children(div)
|
||||||
|
if len(c) != 3 {
|
||||||
|
t.Fatal(len(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c[0] != "foo" || c[1] != "bar" || c[2] != "baz" {
|
||||||
|
t.Fatal(c[0], c[1], c[2])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("map", func(t *testing.T) {
|
||||||
|
tags := html.Map([]string{"foo", "bar", "baz"}, Div)
|
||||||
|
tagChildren := make([]any, len(tags))
|
||||||
|
for i := range tags {
|
||||||
|
tagChildren[i] = tags[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, Div(tagChildren...)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div><div>foo</div><div>bar</div><div>baz</div></div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("map children", func(t *testing.T) {
|
||||||
|
tags := html.MapChildren([]string{"foo", "bar", "baz"}, Div)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, Div(tags...)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div><div>foo</div><div>bar</div><div>baz</div></div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("adding children immutable", func(t *testing.T) {
|
||||||
|
div0 := Div("foo")
|
||||||
|
div1 := div0("bar")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, Div(div0, div1)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div><div>foo</div><div>foobar</div></div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("render", func(t *testing.T) {
|
||||||
|
t.Run("block", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), Div("foo")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>\n\tfoo\n</div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inline", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), Span("foo")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<span>foo</span>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inline children", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), Div(P("foo"), P("bar"))); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>\n\t<p>foo</p>\n\t<p>bar</p>\n</div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inline overrides inline children", func(t *testing.T) {
|
||||||
|
inlineP := html.Inline(P)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), Div(inlineP("foo"), inlineP("bar"))); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>\n\t<p>foo</p><p>bar</p>\n</div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("void", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, Br); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<br>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("void with children", func(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, Br("foo")); err == nil {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("verbatim unindented", func(t *testing.T) {
|
||||||
|
div := html.Verbatim(Div("foo &\nbar"))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, div); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>foo &\nbar</div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("verbatim indented", func(t *testing.T) {
|
||||||
|
div := html.Verbatim(Div("foo &\nbar"))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), div); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>\n\tfoo &\n\tbar\n</div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("script unindented", func(t *testing.T) {
|
||||||
|
script := Script("let foo = 42\nlet bar = 84")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, script); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<script>let foo = 42\nlet bar = 84</script>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("script indented", func(t *testing.T) {
|
||||||
|
script := Script("let foo = 42\nlet bar = 84")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), script); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<script>\nlet foo = 42\nlet bar = 84\n</script>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("script overrides verbatim", func(t *testing.T) {
|
||||||
|
script := html.Verbatim(Script("let foo = 42\nlet bar = 84"))
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), script); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<script>\nlet foo = 42\nlet bar = 84\n</script>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("setting render guides immutable", func(t *testing.T) {
|
||||||
|
div0 := Div("foo")
|
||||||
|
div1 := html.Inline(div0)
|
||||||
|
div := Div(div0, div1)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), div); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>\n\t<div>\n\t\tfoo\n\t</div>\n\t<div>foo</div>\n</div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("verify min pwidth", func(t *testing.T) {
|
||||||
|
p := P("foo bar baz")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indentation{PWidth: 8, MinPWidth: 12}, p); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<p>foo bar\nbaz</p>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("templates", func(t *testing.T) {
|
||||||
|
t.Run("template function", func(t *testing.T) {
|
||||||
|
double := func(i int) html.Tag { return Div(2 * i) }
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, double(42)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>84</div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("templated tag", func(t *testing.T) {
|
||||||
|
type (
|
||||||
|
member struct {
|
||||||
|
name string
|
||||||
|
level int
|
||||||
|
}
|
||||||
|
|
||||||
|
team struct {
|
||||||
|
name string
|
||||||
|
rank int
|
||||||
|
members []member
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
memberHTML := html.FromTemplate(
|
||||||
|
func(m member) html.Tag {
|
||||||
|
return Li(
|
||||||
|
Div("Name: ", m.name),
|
||||||
|
Div("Level: ", m.level),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
teamHTML := html.FromTemplate(
|
||||||
|
func(t team) html.Tag {
|
||||||
|
return Div(
|
||||||
|
H3(t.name),
|
||||||
|
P("Rank: ", t.rank),
|
||||||
|
Ul(html.MapChildren(t.members, memberHTML)...),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
myTeam := team{
|
||||||
|
name: "Foo",
|
||||||
|
rank: 3,
|
||||||
|
members: []member{{
|
||||||
|
name: "Bar",
|
||||||
|
level: 4,
|
||||||
|
}, {
|
||||||
|
name: "Baz",
|
||||||
|
level: 1,
|
||||||
|
}, {
|
||||||
|
name: "Qux",
|
||||||
|
level: 4,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), teamHTML(myTeam)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const expect = `<div>
|
||||||
<h3>Foo</h3>
|
<h3>Foo</h3>
|
||||||
<p>Rank: 3</p>
|
<p>Rank: 3</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -93,13 +510,113 @@ func TestLib(t *testing.T) {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>`
|
</div>`
|
||||||
|
|
||||||
if b.String() != expect {
|
if b.String() != expect {
|
||||||
notation.Println([]byte(expect)[48:96])
|
notation.Println([]byte(expect)[48:96])
|
||||||
notation.Println(b.Bytes()[48:96])
|
notation.Println(b.Bytes()[48:96])
|
||||||
t.Fatal(b.String())
|
t.Fatal(b.String())
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("templated tag additional children", func(t *testing.T) {
|
||||||
|
double := html.FromTemplate(func(i int) html.Tag { return Div(2 * i) })
|
||||||
|
double = double(42)
|
||||||
|
double = double("foo")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, double); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>84foo</div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("templated tag binding with multiple data items", func(t *testing.T) {
|
||||||
|
double := html.FromTemplate(func(i int) html.Tag { return Div(2 * i) })
|
||||||
|
double = double(42, 84)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, double); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div>8484</div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("templated tag immutable", func(t *testing.T) {
|
||||||
|
double := html.FromTemplate(func(i int) html.Tag { return Div(2 * i) })
|
||||||
|
double0 := double(42)
|
||||||
|
double1 := double0("foo")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, Div(double0, double1)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<div><div>84</div><div>84foo</div></div>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ampty attributes", func(t *testing.T) {
|
t.Run("declarations", func(t *testing.T) {
|
||||||
|
t.Run("comments", func(t *testing.T) {
|
||||||
|
comment := Comment("foo &\nbar")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), comment); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<!-- foo &\nbar -->" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("doctype", func(t *testing.T) {
|
||||||
|
doctype := Doctype("html")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), doctype); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<!doctype html>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("doctype complex", func(t *testing.T) {
|
||||||
|
doctype := Doctype("html", "public", "-//W3C//DTD HTML 4.01//EN", "http://www.w3.org/TR/html4/strict.dtd")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.RenderIndent(&b, html.Indent(), doctype); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<!doctype html public \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("custom declaration", func(t *testing.T) {
|
||||||
|
cdataTemplate := func(d any) html.Tag {
|
||||||
|
return html.Declaration("[CDATA[", d, "]]")
|
||||||
|
}
|
||||||
|
|
||||||
|
cdata := html.FromTemplate(cdataTemplate)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, cdata("foo &\nbar")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.String() != "<![CDATA[ foo &\nbar ]]>" {
|
||||||
|
t.Fatal(b.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,3 +3,4 @@ recommendation is not to mutate children. Ofc, creatively breaking the rules is
|
|||||||
right audience
|
right audience
|
||||||
test wrapped templates
|
test wrapped templates
|
||||||
review which tags should be of type inline-children
|
review which tags should be of type inline-children
|
||||||
|
export Escape and EscapeAttribute
|
||||||
|
|||||||
51
render.go
51
render.go
@ -137,27 +137,26 @@ func (r *renderer) copyIndented(indent string, rd io.Reader) {
|
|||||||
func (r *renderer) renderAttributes(tagName string, a []Attributes) {
|
func (r *renderer) renderAttributes(tagName string, a []Attributes) {
|
||||||
printf := r.getPrintf(tagName)
|
printf := r.getPrintf(tagName)
|
||||||
isDeclaration := strings.HasPrefix(tagName, "!")
|
isDeclaration := strings.HasPrefix(tagName, "!")
|
||||||
for _, ai := range a {
|
for i, ai := range a {
|
||||||
for _, name := range ai.names {
|
for j, name := range ai.names {
|
||||||
value := ai.values[name]
|
if isDeclaration {
|
||||||
isTrue, _ := value.(bool)
|
f := " %s"
|
||||||
if isTrue && isDeclaration {
|
if i == 0 && j == 0 {
|
||||||
escaped := attributeEscape(name)
|
f = "%s"
|
||||||
if escaped == name {
|
|
||||||
printf(" %s", name)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
printf(" \"%s\"", escaped)
|
printf(f, name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value := ai.values[name]
|
||||||
|
isTrue, _ := value.(bool)
|
||||||
if isTrue {
|
if isTrue {
|
||||||
printf(" %s", name)
|
printf(" %s", name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
printf(" %s=\"%s\"", name, attributeEscape(fmt.Sprint(value)))
|
printf(" %s=\"%s\"", name, escapeAttribute(fmt.Sprint(value)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -279,6 +278,17 @@ func (r *renderer) renderReaderChild(tagName string, rg renderGuide, block, last
|
|||||||
return newWrapper
|
return newWrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *renderer) renderChildScript(tagName string, c any) {
|
||||||
|
s := fmt.Sprint(c)
|
||||||
|
if s == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.clearWrapper()
|
||||||
|
printf := r.getPrintf(tagName)
|
||||||
|
printf("\n%s", s)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *renderer) renderVerbatimChild(block bool, c any) {
|
func (r *renderer) renderVerbatimChild(block bool, c any) {
|
||||||
s := fmt.Sprint(c)
|
s := fmt.Sprint(c)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
@ -294,17 +304,6 @@ func (r *renderer) renderVerbatimChild(block bool, c any) {
|
|||||||
r.writeIndented(indent, s)
|
r.writeIndented(indent, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) renderChildScript(tagName string, c any) {
|
|
||||||
s := fmt.Sprint(c)
|
|
||||||
if s == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r.clearWrapper()
|
|
||||||
printf := r.getPrintf(tagName)
|
|
||||||
printf("\n%s", s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *renderer) renderChildContent(tagName string, lastBlock bool, c any) bool {
|
func (r *renderer) renderChildContent(tagName string, lastBlock bool, c any) bool {
|
||||||
s := fmt.Sprint(c)
|
s := fmt.Sprint(c)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
@ -383,14 +382,14 @@ func (r *renderer) renderIndented(name string, rg renderGuide, a []Attributes, c
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if rg.verbatim {
|
if rg.script {
|
||||||
r.renderVerbatimChild(block, c)
|
r.renderChildScript(name, c)
|
||||||
lastBlock = true
|
lastBlock = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if rg.script {
|
if rg.verbatim {
|
||||||
r.renderChildScript(name, c)
|
r.renderVerbatimChild(block, c)
|
||||||
lastBlock = true
|
lastBlock = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ func TestRender(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.String() != "<!-- \"foo & bar & baz\" qux -->" {
|
if b.String() != "<!-- foo & bar & baz qux -->" {
|
||||||
t.Fatal(b.String())
|
t.Fatal(b.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
11
validate.go
11
validate.go
@ -7,10 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var symbolExp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-_.]*$`)
|
||||||
symbolExp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-_.]*$`)
|
|
||||||
scriptTagExp = regexp.MustCompile(`<\s*/?\s*[sS][cC][rR][iI][pP][tT]([^a-zA-Z0-9]+|$)`)
|
|
||||||
)
|
|
||||||
|
|
||||||
func validateSymbol(s string) error {
|
func validateSymbol(s string) error {
|
||||||
if !symbolExp.MatchString(s) {
|
if !symbolExp.MatchString(s) {
|
||||||
@ -37,13 +34,17 @@ func validate(name string, children []any) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isDeclaration := strings.HasPrefix(name, "!")
|
||||||
a, c, rgs := groupChildren(children)
|
a, c, rgs := groupChildren(children)
|
||||||
rg := mergeRenderingGuides(rgs)
|
rg := mergeRenderingGuides(rgs)
|
||||||
if rg.void && len(c) > 0 {
|
if rg.void && len(c) > 0 {
|
||||||
return fmt.Errorf("tag %s is void but it has children", name)
|
return fmt.Errorf("tag %s is void but it has children", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDeclaration := strings.HasPrefix(name, "!")
|
if isDeclaration && len(c) > 0 {
|
||||||
|
return fmt.Errorf("declarations cannot have children (%s)", name)
|
||||||
|
}
|
||||||
|
|
||||||
for _, ai := range a {
|
for _, ai := range a {
|
||||||
for _, name := range ai.names {
|
for _, name := range ai.names {
|
||||||
if isDeclaration {
|
if isDeclaration {
|
||||||
|
|||||||
@ -108,4 +108,32 @@ func TestValidate(t *testing.T) {
|
|||||||
t.Fatal()
|
t.Fatal()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("declaration valid", func(t *testing.T) {
|
||||||
|
decl := html.Declaration("foo", "bar", "baz qux")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, decl); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("declaration valid with non-symbol attribute name", func(t *testing.T) {
|
||||||
|
decl := html.Declaration("#", "foo")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, decl); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("declaration with children", func(t *testing.T) {
|
||||||
|
decl := html.Declaration("foo")
|
||||||
|
decl = decl("bar")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := html.Render(&b, decl); err == nil {
|
||||||
|
t.Fatal("failed to fail")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user