diff --git a/escape.go b/escape.go
index 2454fae..6db5ce2 100644
--- a/escape.go
+++ b/escape.go
@@ -16,23 +16,6 @@ type escapeWriter struct {
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 {
return &escapeWriter{out: bufio.NewWriter(out)}
}
@@ -122,3 +105,28 @@ func (w *escapeWriter) Flush() error {
w.err = w.out.Flush()
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)
+}
diff --git a/lib.go b/lib.go
index bbde2a0..462e0b7 100644
--- a/lib.go
+++ b/lib.go
@@ -38,7 +38,7 @@ type Indentation struct {
// the names and values are applied using fmt.Sprint, tolerating fmt.Stringer implementations
func Attr(a ...any) Attributes {
if len(a)%2 != 0 {
- a = append(a, "")
+ a = append(a, true)
}
var am Attributes
@@ -73,8 +73,7 @@ func (a Attributes) Value(name string) any {
func (t Tag) String() string {
buf := bytes.NewBuffer(nil)
- r := renderer{out: buf}
- t()(r)
+ Render(buf, t)
return buf.String()
}
@@ -90,28 +89,38 @@ func Define(name string, 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)
for i, c := range children {
a[2*i] = c
a[2*i+1] = true
}
- return Void(Define(name, Attr(a...)))
-}
-
-func Doctype(children ...any) Tag {
- return Declaration(append([]any{"doctype"}, children...)...)
+ return Void(Define("!", Attr(a...)))
}
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
@@ -166,7 +175,12 @@ func DeleteAttribute(t Tag, name string) Tag {
// the same as Attribute(t, "class")
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)
@@ -285,21 +299,24 @@ func Eq(t ...Tag) bool {
func FromTemplate[Data any](t Template[Data]) Tag {
return func(a ...any) Tag {
var (
- d Data
- ok bool
+ data Data
+ found bool
+ children []any
)
- for i := range a {
- d, ok = a[0].(Data)
- if !ok {
- continue
+ for _, ai := range a {
+ if !found {
+ if d, ok := ai.(Data); ok {
+ data = d
+ found = true
+ continue
+ }
}
- a = append(a[:i], a[i+1:]...)
- break
+ children = append(children, ai)
}
- return t(d)(a...)
+ return t(data)(children...)
}
}
@@ -323,3 +340,12 @@ func MapChildren[Data any](data []Data, tag Tag) []any {
return a
}
+
+func Escape(s string) string {
+ return escape(s)
+}
+
+// does not escape single quotes
+func EscapeAttribute(s string) string {
+ return escapeAttribute(s)
+}
diff --git a/lib_test.go b/lib_test.go
index e6d4aa0..ab4f2db 100644
--- a/lib_test.go
+++ b/lib_test.go
@@ -5,64 +5,481 @@ import (
"code.squareroundforest.org/arpio/html"
. "code.squareroundforest.org/arpio/html/tag"
"code.squareroundforest.org/arpio/notation"
+ "fmt"
"testing"
)
func TestLib(t *testing.T) {
- t.Run("templated tag", func(t *testing.T) {
- type (
- member struct {
- name string
- level int
+ t.Run("escape", func(t *testing.T) {
+ t.Run("html", func(t *testing.T) {
+ if html.Escape("
+ 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() != "
" {
+ 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() != "
" {
+ 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() != "
" {
+ 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() != "
\n\tfoo\n
" {
+ 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() != "
foo" {
+ 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() != "
" {
+ 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() != "
" {
+ 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() != "
" {
+ 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() != "
foo &\nbar
" {
+ 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() != "
\n\tfoo &\n\tbar\n
" {
+ 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() != "" {
+ 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() != "" {
+ 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() != "" {
+ 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() != "
\n\t
\n\t\tfoo\n\t
\n\t
foo
\n
" {
+ 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() != "
foo bar\nbaz
" {
+ 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() != "
84
" {
+ 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 = `
Foo
Rank: 3
@@ -93,13 +510,113 @@ func TestLib(t *testing.T) {
`
- if b.String() != expect {
- notation.Println([]byte(expect)[48:96])
- notation.Println(b.Bytes()[48:96])
- t.Fatal(b.String())
- }
+ if b.String() != expect {
+ notation.Println([]byte(expect)[48:96])
+ notation.Println(b.Bytes()[48:96])
+ 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() != "
84foo
" {
+ 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() != "
8484
" {
+ 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() != "
" {
+ 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() != "" {
+ 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() != "" {
+ 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() != "" {
+ 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() != "" {
+ t.Fatal(b.String())
+ }
+ })
})
}
diff --git a/notes.txt b/notes.txt
index 47addf5..dcdde8d 100644
--- a/notes.txt
+++ b/notes.txt
@@ -3,3 +3,4 @@ recommendation is not to mutate children. Ofc, creatively breaking the rules is
right audience
test wrapped templates
review which tags should be of type inline-children
+export Escape and EscapeAttribute
diff --git a/render.go b/render.go
index 48953a7..c056e28 100644
--- a/render.go
+++ b/render.go
@@ -137,27 +137,26 @@ func (r *renderer) copyIndented(indent string, rd io.Reader) {
func (r *renderer) renderAttributes(tagName string, a []Attributes) {
printf := r.getPrintf(tagName)
isDeclaration := strings.HasPrefix(tagName, "!")
- for _, ai := range a {
- for _, name := range ai.names {
- value := ai.values[name]
- isTrue, _ := value.(bool)
- if isTrue && isDeclaration {
- escaped := attributeEscape(name)
- if escaped == name {
- printf(" %s", name)
- continue
+ for i, ai := range a {
+ for j, name := range ai.names {
+ if isDeclaration {
+ f := " %s"
+ if i == 0 && j == 0 {
+ f = "%s"
}
- printf(" \"%s\"", escaped)
+ printf(f, name)
continue
}
+ value := ai.values[name]
+ isTrue, _ := value.(bool)
if isTrue {
printf(" %s", name)
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
}
+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) {
s := fmt.Sprint(c)
if s == "" {
@@ -294,17 +304,6 @@ func (r *renderer) renderVerbatimChild(block bool, c any) {
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 {
s := fmt.Sprint(c)
if s == "" {
@@ -383,14 +382,14 @@ func (r *renderer) renderIndented(name string, rg renderGuide, a []Attributes, c
continue
}
- if rg.verbatim {
- r.renderVerbatimChild(block, c)
+ if rg.script {
+ r.renderChildScript(name, c)
lastBlock = true
continue
}
- if rg.script {
- r.renderChildScript(name, c)
+ if rg.verbatim {
+ r.renderVerbatimChild(block, c)
lastBlock = true
continue
}
diff --git a/render_test.go b/render_test.go
index 0d5b849..39a8c71 100644
--- a/render_test.go
+++ b/render_test.go
@@ -58,7 +58,7 @@ func TestRender(t *testing.T) {
t.Fatal(err)
}
- if b.String() != "" {
+ if b.String() != "" {
t.Fatal(b.String())
}
})
diff --git a/validate.go b/validate.go
index 208d130..c4ad951 100644
--- a/validate.go
+++ b/validate.go
@@ -7,10 +7,7 @@ import (
"strings"
)
-var (
- 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]+|$)`)
-)
+var symbolExp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-_.]*$`)
func validateSymbol(s string) error {
if !symbolExp.MatchString(s) {
@@ -37,13 +34,17 @@ func validate(name string, children []any) error {
return err
}
+ isDeclaration := strings.HasPrefix(name, "!")
a, c, rgs := groupChildren(children)
rg := mergeRenderingGuides(rgs)
if rg.void && len(c) > 0 {
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 _, name := range ai.names {
if isDeclaration {
diff --git a/validate_test.go b/validate_test.go
index b5981c5..4b7daf4 100644
--- a/validate_test.go
+++ b/validate_test.go
@@ -108,4 +108,32 @@ func TestValidate(t *testing.T) {
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")
+ }
+ })
}