From 33802919af510554bc55f115d3cef33f2b53a0d8 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Mon, 6 Oct 2025 23:49:11 +0200 Subject: [PATCH] test lib --- escape.go | 42 ++-- lib.go | 78 ++++-- lib_test.go | 621 +++++++++++++++++++++++++++++++++++++++++++---- notes.txt | 1 + render.go | 51 ++-- render_test.go | 2 +- validate.go | 11 +- validate_test.go | 28 +++ 8 files changed, 707 insertions(+), 127 deletions(-) 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("
foo&bar
") != "<div>foo&bar</div>" { + t.Fatal(html.Escape("
foo&bar
")) + } + }) + + 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 != "
" { + 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 { - name string - rank int - members []member + if !a.Has("foo") || !a.Has("baz") { + t.Fatal(a.Has("foo"), a.Has("baz")) } - ) - memberHTML := html.FromTemplate( - func(m member) html.Tag { - return Li( - Div("Name: ", m.name), - Div("Level: ", m.level), - ) - }, - ) + if a.Names()[0] != "foo" || a.Names()[1] != "baz" { + t.Fatal(a.Names()[0], a.Names()[1]) + } - teamHTML := html.FromTemplate( - func(t team) html.Tag { - return Div( - H3(t.name), - P("Rank: ", t.rank), - Ul(html.MapChildren(t.members, memberHTML)...), - ) - }, - ) + if a.Value("foo") != "bar" || a.Value("baz") != "qux" { + t.Fatal(a.Value("foo"), a.Value("baz")) + } + }) - myTeam := team{ - name: "Foo", - rank: 3, - members: []member{{ - name: "Bar", - level: 4, - }, { - name: "Baz", - level: 1, - }, { - name: "Qux", - level: 4, - }}, - } + t.Run("delete attribute", func(t *testing.T) { + div := Div(Attr("foo", "bar", "baz", "qux")) + div = html.DeleteAttribute(div, "foo") + a := html.AllAttributes(div) + if a.Has("foo") || !a.Has("baz") || a.Value("baz") != "qux" { + t.Fatal(a.Has("foo"), a.Has("baz"), a.Value("baz")) + } + }) - var b bytes.Buffer - if err := html.RenderIndent(&b, html.Indent(), teamHTML(myTeam)); err != nil { - t.Fatal(err) - } + t.Run("setting attribute immutable", func(t *testing.T) { + div0 := Div(Attr("foo", "bar")) + 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 = `
+ 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() != "
foo
bar
baz
" { + 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() != "
foo
bar
baz
" { + 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() != "
foo
foobar
" { + 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() != "
\n\t

foo

\n\t

bar

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

foo

bar

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