diff --git a/Makefile b/Makefile index 87c189e..06698ea 100644 --- a/Makefile +++ b/Makefile @@ -7,30 +7,38 @@ all: clean fmt build cover build: $(SOURCES) tags promote-to-tags go build -tags: tags/block.gen.go tags/inline.gen.go tags/void.block.gen.go tags/void.inline.gen.go tags/inlinechildren.gen.go tags/script.gen.go +tags: \ + tag/block.gen.go \ + tag/inline.gen.go \ + tag/void.block.gen.go \ + tag/void.inline.gen.go \ + tag/inlinechildren.gen.go \ + tag/script.gen.go + go fmt tag/* -promote-to-tags: tags/promote.gen.go +promote-to-tags: tag/promote.gen.go + go fmt tag/* -tags/block.gen.go: $(SOURCES) tags.block.txt - go run script/generate-tags.go < tags.block.txt > tags/block.gen.go +tag/block.gen.go: $(SOURCES) tag.block.txt + go run script/generate-tags.go < tag.block.txt > tag/block.gen.go -tags/inline.gen.go: $(SOURCES) tags.inline.txt - go run script/generate-tags.go Inline < tags.inline.txt > tags/inline.gen.go +tag/inline.gen.go: $(SOURCES) tag.inline.txt + go run script/generate-tags.go Inline < tag.inline.txt > tag/inline.gen.go -tags/inlinechildren.gen.go: $(SOURCES) tags.inlinechildren.txt - go run script/generate-tags.go InlineChildren < tags.inlinechildren.txt > tags/inlinechildren.gen.go +tag/inlinechildren.gen.go: $(SOURCES) tag.inlinechildren.txt + go run script/generate-tags.go InlineChildren < tag.inlinechildren.txt > tag/inlinechildren.gen.go -tags/void.block.gen.go: $(SOURCES) tags.void.block.txt - go run script/generate-tags.go Void < tags.void.block.txt > tags/void.block.gen.go +tag/void.block.gen.go: $(SOURCES) tag.void.block.txt + go run script/generate-tags.go Void < tag.void.block.txt > tag/void.block.gen.go -tags/void.inline.gen.go: $(SOURCES) tags.void.inline.txt - go run script/generate-tags.go Void Inline < tags.void.inline.txt > tags/void.inline.gen.go +tag/void.inline.gen.go: $(SOURCES) tag.void.inline.txt + go run script/generate-tags.go Void Inline < tag.void.inline.txt > tag/void.inline.gen.go -tags/script.gen.go: $(SOURCES) tags.script.txt - go run script/generate-tags.go ScriptContent < tags.script.txt > tags/script.gen.go +tag/script.gen.go: $(SOURCES) tag.script.txt + go run script/generate-tags.go ScriptContent < tag.script.txt > tag/script.gen.go -tags/promote.gen.go: $(SOURCES) promote-to-tags.txt - go run script/promote-to-tags.go < promote-to-tags.txt > tags/promote.gen.go +tag/promote.gen.go: $(SOURCES) promote-to-tags.txt + go run script/promote-to-tags.go < promote-to-tags.txt > tag/promote.gen.go fmt: $(SOURCES) tags go fmt ./... @@ -49,4 +57,4 @@ showcover: .cover clean: go clean - rm -f tags/*.gen.go + rm -f tag/*.gen.go diff --git a/eq.go b/eq.go index 336cb6b..260f81f 100644 --- a/eq.go +++ b/eq.go @@ -6,13 +6,17 @@ func eq2(t1, t2 Tag) bool { } a1, a2 := AllAttributes(t1), AllAttributes(t2) - if len(a1) != len(a2) { + if len(a1.names) != len(a2.names) { return false } - for name := range a1 { - v1 := a1[name] - v2, ok := a2[name] + for i, name := range a1.names { + if a2.names[i] != name { + return false + } + + v1 := a1.values[name] + v2, ok := a2.values[name] if !ok || v1 != v2 { return false } diff --git a/escape_test.go b/escape_test.go index c704fd7..93d9b19 100644 --- a/escape_test.go +++ b/escape_test.go @@ -3,7 +3,7 @@ package html_test import ( "bytes" "code.squareroundforest.org/arpio/html" - . "code.squareroundforest.org/arpio/html/tags" + . "code.squareroundforest.org/arpio/html/tag" "testing" ) diff --git a/indent.go b/indent.go index ea4244a..7878271 100644 --- a/indent.go +++ b/indent.go @@ -15,6 +15,21 @@ type indentWriter struct { err error } +func indentLen(indent string) int { + var l int + r := []rune(indent) + for _, ri := range r { + if ri == '\t' { + l += 8 + continue + } + + l++ + } + + return l +} + func newIndentWriter(out io.Writer, indent string) *indentWriter { return &indentWriter{ out: bufio.NewWriter(out), @@ -66,6 +81,7 @@ func (w *indentWriter) Write(p []byte) (int, error) { if r == '\n' { w.write('\n') + w.started = true w.lineStarted = false continue } diff --git a/lib.go b/lib.go index 076bfcc..bbde2a0 100644 --- a/lib.go +++ b/lib.go @@ -9,7 +9,10 @@ import ( ) // when composing html, the Attr convenience function is recommended to construct input attributes -type Attributes map[string]any +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 @@ -31,13 +34,6 @@ type Indentation struct { Indent string } -func (t Tag) String() string { - buf := bytes.NewBuffer(nil) - r := renderer{out: buf} - t()(r) - return buf.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 func Attr(a ...any) Attributes { @@ -45,14 +41,43 @@ func Attr(a ...any) Attributes { a = append(a, "") } - am := make(Attributes) + var am Attributes + am.values = make(map[string]any) for i := 0; i < len(a); i += 2 { - am[fmt.Sprint(a[i])] = a[i+1] + name := fmt.Sprint(a[i]) + am.names = append(am.names, name) + am.values[name] = a[i+1] } return am } +func (a Attributes) Names() []string { + if len(a.names) == 0 { + return nil + } + + n := make([]string, len(a.names)) + copy(n, a.names) + return n +} + +func (a Attributes) Has(name string) bool { + _, ok := a.values[name] + return ok +} + +func (a Attributes) Value(name string) any { + return a.values[name] +} + +func (t Tag) String() string { + buf := bytes.NewBuffer(nil) + r := renderer{out: buf} + t()(r) + return buf.String() +} + // defines a new tag with name and initial attributes and child nodes func Define(name string, children ...any) Tag { if handleQuery(name, children) { @@ -86,7 +111,7 @@ func Doctype(children ...any) Tag { } func Comment(children ...any) Tag { - return Inline(Declaration(append(append([]any{"--"}, children...), "--"))) + return Inline(Declaration(append(append([]any{"--"}, children...), "--")...)) } // returns the name of a tag @@ -100,9 +125,11 @@ func Name(t Tag) string { func AllAttributes(t Tag) Attributes { q := attributesQuery{} t()(&q) - a := make(Attributes) - for name, value := range q.value { - a[name] = value + a := Attributes{values: make(map[string]any)} + for _, name := range q.value.names { + value := q.value.values[name] + a.names = append(a.names, name) + a.values[name] = value } return a @@ -126,7 +153,14 @@ func DeleteAttribute(t Tag, name string) Tag { n := Name(t) a := AllAttributes(t) c := Children(t) - delete(a, name) + 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 + } + } + return Define(n, append(c, a)...) } @@ -174,6 +208,10 @@ func Children(t Tag) []any { return c } +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   diff --git a/lib_test.go b/lib_test.go index 4c4a98f..e6d4aa0 100644 --- a/lib_test.go +++ b/lib_test.go @@ -3,7 +3,7 @@ package html_test import ( "bytes" "code.squareroundforest.org/arpio/html" - . "code.squareroundforest.org/arpio/html/tags" + . "code.squareroundforest.org/arpio/html/tag" "code.squareroundforest.org/arpio/notation" "testing" ) @@ -58,7 +58,7 @@ func TestLib(t *testing.T) { } var b bytes.Buffer - if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, teamHTML(myTeam)); err != nil { + if err := html.RenderIndent(&b, html.Indent(), teamHTML(myTeam)); err != nil { t.Fatal(err) } diff --git a/notes.txt b/notes.txt index 60ba713..6e021bb 100644 --- a/notes.txt +++ b/notes.txt @@ -3,9 +3,4 @@ recommendation is not to mutate children. Ofc, creatively breaking the rules is right audience test wrapped templates test empty block -escape extra space between tag boundaries -declarations: -comments: -render nil as empty -support readers review which tags should be of type inline-children diff --git a/query.go b/query.go index bfd1988..8e48cb5 100644 --- a/query.go +++ b/query.go @@ -53,13 +53,14 @@ func groupChildren(c []any) ([]Attributes, []any, []renderGuide) { func mergeAttributes(c []any) Attributes { a, _, _ := groupChildren(c) if len(a) == 0 { - return nil + return Attributes{} } - to := make(Attributes) + to := Attributes{values: make(map[string]any)} for _, ai := range a { - for name, value := range ai { - to[name] = value + for _, name := range ai.names { + to.names = append(to.names, name) + to.values[name] = ai.values[name] } } @@ -69,7 +70,7 @@ func mergeAttributes(c []any) Attributes { func findAttribute(c []any, name string) (any, bool) { a, _, _ := groupChildren(c) for i := len(a) - 1; i >= 0; i-- { - value, ok := a[i][name] + value, ok := a[i].values[name] if ok { return value, true } diff --git a/query_test.go b/query_test.go index 047e029..3bccc47 100644 --- a/query_test.go +++ b/query_test.go @@ -3,7 +3,7 @@ package html_test import ( "bytes" "code.squareroundforest.org/arpio/html" - . "code.squareroundforest.org/arpio/html/tags" + . "code.squareroundforest.org/arpio/html/tag" "testing" ) @@ -11,7 +11,7 @@ func TestQuery(t *testing.T) { t.Run("group children", func(t *testing.T) { inlineDiv := html.Inline(Div(Attr("foo", "bar"), "baz")) attr := html.AllAttributes(inlineDiv) - if len(attr) != 1 || attr["foo"] != "bar" { + if len(attr.Names()) != 1 || attr.Value("foo") != "bar" { t.Fatal() } @@ -35,7 +35,7 @@ func TestQuery(t *testing.T) { t.Run("has attributes", func(t *testing.T) { div := Div(Attr("foo", "bar")) attr := html.AllAttributes(div) - if len(attr) != 1 || attr["foo"] != "bar" { + if len(attr.Names()) != 1 || attr.Value("foo") != "bar" { t.Fatal() } }) @@ -43,7 +43,7 @@ func TestQuery(t *testing.T) { t.Run("no attributes", func(t *testing.T) { div := Div() attr := html.AllAttributes(div) - if len(attr) != 0 { + if len(attr.Names()) != 0 { t.Fatal() } }) @@ -84,7 +84,7 @@ func TestQuery(t *testing.T) { t.Run("all attributes", func(t *testing.T) { div := Div(Attr("foo", "bar", "baz", "qux")) attr := html.AllAttributes(div) - if len(attr) != 2 || attr["foo"] != "bar" || attr["baz"] != "qux" { + if len(attr.Names()) != 2 || attr.Value("foo") != "bar" || attr.Value("baz") != "qux" { t.Fatal() } }) @@ -109,7 +109,7 @@ func TestQuery(t *testing.T) { div := Div(Span("foo")) var b bytes.Buffer - if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, div); err != nil { + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { t.Fatal(err) } diff --git a/render.go b/render.go index 3720640..9aa206c 100644 --- a/render.go +++ b/render.go @@ -3,6 +3,7 @@ package html import ( "fmt" "io" + "strings" ) const defaultPWidth = 112 @@ -52,10 +53,24 @@ func (r *renderer) getPrintf(tagName string) func(f string, a ...any) { func (r *renderer) renderAttributes(tagName string, a []Attributes) { printf := r.getPrintf(tagName) + isDeclaration := strings.HasPrefix(tagName, "!") for _, ai := range a { - for name, value := range ai { - if isTrue, _ := value.(bool); isTrue { - printf(" &s", name) + 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 + } + + printf(" \"%s\"", escaped) + continue + } + + if isTrue { + printf(" %s", name) continue } @@ -91,7 +106,7 @@ func (r *renderer) renderUnindented(name string, rg renderGuide, a []Attributes, ew := newEscapeWriter(r.out) _, r.err = io.Copy(ew, rd) - if r.err != nil { + if r.err == nil { ew.Flush() r.err = ew.err } @@ -165,7 +180,6 @@ func (r *renderer) renderInline(name string, rg renderGuide, a []Attributes, chi if r.err == nil { iw := newIndentWriter(r.out, r.currentIndent+r.indent.Indent) iw.Write([]byte{'\n'}) - iw.Write([]byte(r.currentIndent + r.indent.Indent)) _, r.err = io.Copy(iw, rd) if r.err == nil { iw.Flush() @@ -222,7 +236,6 @@ func (r *renderer) renderInline(name string, rg renderGuide, a []Attributes, chi if r.err == nil { iw := newIndentWriter(r.out, r.currentIndent+r.indent.Indent) iw.Write([]byte{'\n'}) - iw.Write([]byte(r.currentIndent + r.indent.Indent)) iw.Write([]byte(s)) iw.Flush() r.err = iw.err @@ -298,7 +311,7 @@ func (r *renderer) renderInline(name string, rg renderGuide, a []Attributes, chi cr := new(renderer) *cr = *r cr.currentIndent += cr.indent.Indent - cr.pwidth -= len([]rune(cr.indent.Indent)) + cr.pwidth -= indentLen(cr.indent.Indent) if cr.pwidth < cr.indent.MinPWidth { cr.pwidth = cr.indent.MinPWidth } @@ -339,7 +352,7 @@ func (r *renderer) renderBlock(name string, rg renderGuide, a []Attributes, chil lastBlock := true originalIndent, originalWidth := r.currentIndent, r.pwidth r.currentIndent += r.indent.Indent - r.pwidth -= len([]rune(r.indent.Indent)) + r.pwidth -= indentLen(r.indent.Indent) if r.pwidth < r.indent.MinPWidth { r.pwidth = r.indent.MinPWidth } @@ -348,11 +361,9 @@ func (r *renderer) renderBlock(name string, rg renderGuide, a []Attributes, chil rd, isReader := c.(io.Reader) if isReader && rg.verbatim { r.clearWrapper() - printf("\n") if r.err == nil { - iw := newIndentWriter(r.out, r.currentIndent+r.indent.Indent) + iw := newIndentWriter(r.out, r.currentIndent) iw.Write([]byte{'\n'}) - iw.Write([]byte(r.currentIndent + r.indent.Indent)) _, r.err = io.Copy(iw, rd) if r.err == nil { iw.Flush() @@ -406,7 +417,6 @@ func (r *renderer) renderBlock(name string, rg renderGuide, a []Attributes, chil if r.err == nil { iw := newIndentWriter(r.out, r.currentIndent) iw.Write([]byte{'\n'}) - iw.Write([]byte(r.currentIndent + r.indent.Indent)) iw.Write([]byte(s)) iw.Flush() r.err = iw.err diff --git a/render_test.go b/render_test.go index 478384e..0d5b849 100644 --- a/render_test.go +++ b/render_test.go @@ -3,36 +3,18 @@ package html_test import ( "bytes" "code.squareroundforest.org/arpio/html" - . "code.squareroundforest.org/arpio/html/tags" - "errors" + . "code.squareroundforest.org/arpio/html/tag" "strings" "testing" ) -type failingWriter struct { - after int -} - -func failWriteAfter(n int) *failingWriter { - return &failingWriter{n} -} - -func (w *failingWriter) Write(p []byte) (int, error) { - w.after -= len(p) - if w.after < 0 { - return len(p) + w.after, errors.New("test error") - } - - return len(p), nil -} - func TestRender(t *testing.T) { t.Run("merge render guides", func(t *testing.T) { foo := html.Inline(html.Verbatim(Define("foo"))) foo = foo("") var b bytes.Buffer - if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, foo); err != nil { + if err := html.RenderIndent(&b, html.Indent(), foo); err != nil { t.Fatal(err) } @@ -41,17 +23,45 @@ func TestRender(t *testing.T) { } }) - t.Run("attribute escaping", func(t *testing.T) { - span := Span(Attr("foo", "bar=\"&\"")) + t.Run("attributes", func(t *testing.T) { + t.Run("escaping", func(t *testing.T) { + span := Span(Attr("foo", "bar=\"&\"")) - var b bytes.Buffer - if err := html.Render(&b, span); err != nil { - t.Fatal(err) - } + var b bytes.Buffer + if err := html.Render(&b, span); err != nil { + t.Fatal(err) + } - if b.String() != "" { - t.Fatal(b.String()) - } + if b.String() != "" { + t.Fatal(b.String()) + } + }) + + t.Run("true form", func(t *testing.T) { + span := Span(Attr("foo", true)) + + var b bytes.Buffer + if err := html.Render(&b, span); err != nil { + t.Fatal(err) + } + + if b.String() != "" { + t.Fatal(b.String()) + } + }) + + t.Run("declaration", func(t *testing.T) { + comment := Comment("foo & bar & baz", "qux") + + var b bytes.Buffer + if err := html.Render(&b, comment); err != nil { + t.Fatal(err) + } + + if b.String() != "" { + t.Fatal(b.String()) + } + }) }) t.Run("html escape", func(t *testing.T) { @@ -85,32 +95,32 @@ func TestRender(t *testing.T) { t.Run("write error", func(t *testing.T) { t.Run("fail immediately", func(t *testing.T) { div := Div(Span("foo")) - w := failWriteAfter(0) - if err := html.Render(w, div); err == nil || !strings.Contains(err.Error(), "test error") { + w := &errorWriter{} + if err := html.Render(w, div); err == nil || !strings.Contains(err.Error(), "test write error") { t.Fatal() } }) t.Run("fail in tag", func(t *testing.T) { div := Div(Span("foo")) - w := failWriteAfter(6) - if err := html.Render(w, div); err == nil || !strings.Contains(err.Error(), "test error") { + w := &errorWriter{failAfter: 6} + if err := html.Render(w, div); err == nil || !strings.Contains(err.Error(), "test write error") { t.Fatal() } }) t.Run("partial text children", func(t *testing.T) { div := Div("foo", Div("bar"), "baz") - w := failWriteAfter(5) - if err := html.RenderIndent(w, html.Indentation{Indent: "\t"}, div); err == nil || !strings.Contains(err.Error(), "test error") { + w := &errorWriter{failAfter: 5} + if err := html.RenderIndent(w, html.Indent(), div); err == nil || !strings.Contains(err.Error(), "test write error") { t.Fatal() } }) t.Run("text children", func(t *testing.T) { div := Div("foo", "bar", "baz") - w := failWriteAfter(5) - if err := html.RenderIndent(w, html.Indentation{Indent: "\t"}, div); err == nil || !strings.Contains(err.Error(), "test error") { + w := &errorWriter{failAfter: 5} + if err := html.RenderIndent(w, html.Indent(), div); err == nil || !strings.Contains(err.Error(), "test write error") { t.Fatal() } }) @@ -121,7 +131,7 @@ func TestRender(t *testing.T) { div := Div(Span("foo")) var b bytes.Buffer - if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, div); err != nil { + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { t.Fatal(err) } @@ -131,10 +141,10 @@ func TestRender(t *testing.T) { }) t.Run("empty tag", func(t *testing.T) { - div := Div(Br()) + div := Div(Br) var b bytes.Buffer - if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, div); err != nil { + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { t.Fatal(err) } @@ -147,7 +157,7 @@ func TestRender(t *testing.T) { div := Div("foo bar baz", Div("qux quux"), "corge") var b bytes.Buffer - if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, div); err != nil { + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { t.Fatal(err) } @@ -160,13 +170,823 @@ func TestRender(t *testing.T) { div := Div(Span("foo bar baz", Div("qux quux"), "corge")) var b bytes.Buffer - if err := html.RenderIndent(&b, html.Indentation{Indent: "XYZ"}, div); err != nil { + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { t.Fatal(err) } - if b.String() != "
\nXYZfoo bar baz\nXYZXYZ
\nXYZXYZXYZqux quux\nXYZXYZ
\nXYZcorge
\n
" { + if b.String() != "
\n\tfoo bar baz\n\t\t
\n\t\t\tqux quux\n\t\t
\n\tcorge
\n
" { + t.Fatal(b.String()) + } + }) + + t.Run("verbatim", func(t *testing.T) { + foo := html.Verbatim(Define("foo")) + foo = foo("bar\nbaz\nqux") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), foo); err != nil { + t.Fatal(err) + } + + if b.String() != "\n\tbar\n\tbaz\n\tqux\n" { + t.Fatal(b.String()) + } + }) + + t.Run("script not indented", func(t *testing.T) { + script := Div(Script("foo()\nbar()\nbaz()")) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), script); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\t\n
" { t.Fatal(b.String()) } }) }) + + t.Run("simple", func(t *testing.T) { + t.Run("unindented", func(t *testing.T) { + div := Div("foo") + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
foo
" { + t.Fatal(b.String()) + } + }) + + t.Run("unindented number", func(t *testing.T) { + div := Div(42) + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
42
" { + t.Fatal(b.String()) + } + }) + + t.Run("unindented after block", func(t *testing.T) { + div := Div(Div, "foo") + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
foo
" { + t.Fatal(b.String()) + } + }) + + t.Run("unindented after inline", func(t *testing.T) { + div := Div(Span, "foo") + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
foo
" { + t.Fatal(b.String()) + } + }) + + t.Run("inline", func(t *testing.T) { + span := Span("foo") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "foo" { + t.Fatal(b.String()) + } + }) + + t.Run("inline number", func(t *testing.T) { + span := Span(42) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "42" { + t.Fatal(b.String()) + } + }) + + t.Run("inline after block", func(t *testing.T) { + span := Span(Div, "foo") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "\n\t
\nfoo
" { + t.Fatal(b.String()) + } + }) + + t.Run("inline after inline", func(t *testing.T) { + span := Span(Span, "foo") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "foo" { + t.Fatal(b.String()) + } + }) + + t.Run("inline tag after block", func(t *testing.T) { + span := Span(Div, Span("foo")) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "\n\t
\nfoo
" { + t.Fatal(b.String()) + } + }) + + t.Run("inline children", func(t *testing.T) { + p := P("foo") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), p); err != nil { + t.Fatal(err) + } + + if b.String() != "

foo

" { + t.Fatal(b.String()) + } + }) + + t.Run("inline children number", func(t *testing.T) { + p := P(42) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), p); err != nil { + t.Fatal(err) + } + + if b.String() != "

42

" { + t.Fatal(b.String()) + } + }) + + t.Run("inline children after inline children", func(t *testing.T) { + p := P(P, "foo") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), p); err != nil { + t.Fatal(err) + } + + if b.String() != "

\n\t

\nfoo

" { + t.Fatal(b.String()) + } + }) + + t.Run("block", func(t *testing.T) { + div := Div("foo") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\tfoo\n
" { + t.Fatal(b.String()) + } + }) + + t.Run("block number", func(t *testing.T) { + div := Div(42) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\t42\n
" { + t.Fatal(b.String()) + } + }) + + t.Run("block after block", func(t *testing.T) { + div := Div(Div, 42) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\t
\n\t42\n
" { + t.Fatal(b.String()) + } + }) + + t.Run("block after inline", func(t *testing.T) { + div := Div(Span, 42) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\t42\n
" { + t.Fatal(b.String()) + } + }) + }) + + t.Run("nil child", func(t *testing.T) { + t.Run("unindented", func(t *testing.T) { + div := Div("foo", nil, "bar") + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
foobar
" { + t.Fatal(b.String()) + } + }) + + t.Run("unindented verbatim", func(t *testing.T) { + div := html.Verbatim(Div("foo &", nil, " bar")) + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
foo & bar
" { + t.Fatal(b.String()) + } + }) + + t.Run("unindented script", func(t *testing.T) { + script := Script("let foo =", nil, " bar && baz") + + var b bytes.Buffer + if err := html.Render(&b, script); err != nil { + t.Fatal(err) + } + + if b.String() != "" { + t.Fatal(b.String()) + } + }) + + t.Run("inline", func(t *testing.T) { + span := Span("foo", nil, "bar") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "foobar" { + t.Fatal(b.String()) + } + }) + + t.Run("inline verbatim", func(t *testing.T) { + span := html.Verbatim(Span("foo &", nil, " bar")) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "\n\tfoo &\n\t bar\n" { + t.Fatal(b.String()) + } + }) + + t.Run("inline script", func(t *testing.T) { + script := html.Inline(Script("let foo =", nil, " bar && baz")) + + 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("block", func(t *testing.T) { + div := Div("foo", nil, "bar") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\tfoobar\n
" { + t.Fatal(b.String()) + } + }) + + t.Run("block verbatim", func(t *testing.T) { + div := html.Verbatim(Div("foo &", nil, " bar")) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\tfoo &\n\t bar\n
" { + t.Fatal(b.String()) + } + }) + + t.Run("block script", func(t *testing.T) { + script := Script("let foo =", nil, " bar && baz") + + 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("empty child", func(t *testing.T) { + t.Run("unindented", func(t *testing.T) { + div := Div("foo", "", "bar") + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
foobar
" { + t.Fatal(b.String()) + } + }) + + t.Run("unindented verbatim", func(t *testing.T) { + div := html.Verbatim(Div("foo &", "", " bar")) + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
foo & bar
" { + t.Fatal(b.String()) + } + }) + + t.Run("unindented script", func(t *testing.T) { + script := Script("let foo =", "", " bar && baz") + + var b bytes.Buffer + if err := html.Render(&b, script); err != nil { + t.Fatal(err) + } + + if b.String() != "" { + t.Fatal(b.String()) + } + }) + + t.Run("inline", func(t *testing.T) { + span := Span("foo", "", "bar") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "foobar" { + t.Fatal(b.String()) + } + }) + + t.Run("inline verbatim", func(t *testing.T) { + span := html.Verbatim(Span("foo &", "", " bar")) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "\n\tfoo &\n\t bar\n" { + t.Fatal(b.String()) + } + }) + + t.Run("inline script", func(t *testing.T) { + script := html.Inline(Script("let foo =", "", " bar && baz")) + + 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("block", func(t *testing.T) { + div := Div("foo", "", "bar") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\tfoobar\n
" { + t.Fatal(b.String()) + } + }) + + t.Run("block verbatim", func(t *testing.T) { + div := html.Verbatim(Div("foo &", "", " bar")) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\tfoo &\n\t bar\n
" { + t.Fatal(b.String()) + } + }) + + t.Run("block script", func(t *testing.T) { + script := Script("let foo =", "", " bar && baz") + + 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("reader child", func(t *testing.T) { + t.Run("unindented", func(t *testing.T) { + rd := bytes.NewBufferString("foo & bar & baz") + div := Div(rd) + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
foo & bar & baz
" { + t.Fatal(b.String()) + } + }) + + t.Run("unindented verbatim", func(t *testing.T) { + rd := bytes.NewBufferString("foo & bar & baz") + div := html.Verbatim(Div(rd)) + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
foo & bar & baz
" { + t.Fatal(b.String()) + } + }) + + t.Run("unindented script", func(t *testing.T) { + rd := bytes.NewBufferString("let foo = bar() && baz()") + script := Script(rd) + + var b bytes.Buffer + if err := html.Render(&b, script); err != nil { + t.Fatal(err) + } + + if b.String() != "" { + t.Fatal(b.String()) + } + }) + + t.Run("inline", func(t *testing.T) { + rd := bytes.NewBufferString("foo & bar & baz") + span := Span(rd) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "foo & bar & baz" { + t.Fatal(b.String()) + } + }) + + t.Run("inline verbatim", func(t *testing.T) { + rd := bytes.NewBufferString("foo & bar & baz") + span := html.Verbatim(Span(rd)) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "\n\tfoo & bar & baz\n" { + t.Fatal(b.String()) + } + }) + + t.Run("inline script", func(t *testing.T) { + rd := bytes.NewBufferString("let foo = bar() && baz()") + script := html.Inline(Script(rd)) + + 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("inline after block", func(t *testing.T) { + rd := bytes.NewBufferString("foo & bar & baz") + span := Span(Div, rd) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "\n\t
\nfoo & bar & baz
" { + t.Fatal(b.String()) + } + }) + + t.Run("block", func(t *testing.T) { + rd := bytes.NewBufferString("foo & bar & baz") + div := Div(rd) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\tfoo & bar & baz\n
" { + t.Fatal(b.String()) + } + }) + + t.Run("block verbatim", func(t *testing.T) { + rd := bytes.NewBufferString("foo & bar & baz") + div := html.Verbatim(Div(rd)) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\tfoo & bar & baz\n
" { + t.Fatal(b.String()) + } + }) + + t.Run("block script", func(t *testing.T) { + rd := bytes.NewBufferString("let foo = bar() && baz()") + script := Script(rd) + + 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("void", func(t *testing.T) { + t.Run("unindented", 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("inline", func(t *testing.T) { + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), Br); err != nil { + t.Fatal(err) + } + + if b.String() != "
" { + t.Fatal(b.String()) + } + }) + + t.Run("block", func(t *testing.T) { + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), Hr); err != nil { + t.Fatal(err) + } + + if b.String() != "
" { + t.Fatal(b.String()) + } + }) + }) + + t.Run("no children", func(t *testing.T) { + t.Run("unindented", func(t *testing.T) { + var b bytes.Buffer + if err := html.Render(&b, Div); err != nil { + t.Fatal(err) + } + + if b.String() != "
" { + t.Fatal(b.String()) + } + }) + + t.Run("inline", func(t *testing.T) { + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), Span); err != nil { + t.Fatal(err) + } + + if b.String() != "" { + t.Fatal(b.String()) + } + }) + + t.Run("block", func(t *testing.T) { + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), Div); err != nil { + t.Fatal(err) + } + + if b.String() != "
" { + t.Fatal(b.String()) + } + }) + }) + + t.Run("verbatim", func(t *testing.T) { + t.Run("unindented", func(t *testing.T) { + div := html.Verbatim(Div("foo & bar")) + + var b bytes.Buffer + if err := html.Render(&b, div); err != nil { + t.Fatal(err) + } + + if b.String() != "
foo & bar
" { + t.Fatal(b.String()) + } + }) + + t.Run("inline", func(t *testing.T) { + span := html.Verbatim(Span("foo & bar")) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), span); err != nil { + t.Fatal(err) + } + + if b.String() != "\n\tfoo & bar\n" { + t.Fatal(b.String()) + } + }) + + t.Run("block", func(t *testing.T) { + div := html.Verbatim(Div("foo & bar")) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indent(), div); err != nil { + t.Fatal(err) + } + + if b.String() != "
\n\tfoo & bar\n
" { + t.Fatal(b.String()) + } + }) + }) + + t.Run("script", func(t *testing.T) { + t.Run("unindented", func(t *testing.T) { + script := Script("let foo = bar && baz") + + var b bytes.Buffer + if err := html.Render(&b, script); err != nil { + t.Fatal(err) + } + + if b.String() != "" { + t.Fatal(b.String()) + } + }) + + t.Run("inline", func(t *testing.T) { + script := html.Inline(Script("let foo = bar && baz")) + + 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("block", func(t *testing.T) { + script := Script("let foo = bar && baz") + + 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("wrap", func(t *testing.T) { + t.Run("simple", func(t *testing.T) { + p := P("foo bar baz qux quux corge") + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indentation{Indent: "\t", PWidth: 15}, p); err != nil { + t.Error(err) + } + + if b.String() != "

foo bar baz\nqux quux corge\n

" { + t.Error(b.String()) + } + }) + + t.Run("min width", func(t *testing.T) { + div := Div(P("foo bar baz qux quux corge")) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indentation{Indent: "\t", PWidth: 21, MinPWidth: 18}, div); err != nil { + t.Error(err) + } + + if b.String() != "
\n\t

foo bar baz qux\n\tquux corge

\n
" { + t.Error(b.String()) + } + }) + + t.Run("min width inline", func(t *testing.T) { + span := Span(P("foo bar baz qux quux corge")) + + var b bytes.Buffer + if err := html.RenderIndent(&b, html.Indentation{Indent: "\t", PWidth: 21, MinPWidth: 18}, span); err != nil { + t.Error(err) + } + + if b.String() != "\n\t

foo bar baz qux\n\tquux corge

\n
" { + t.Error(b.String()) + } + }) + }) } diff --git a/script/generate-tags.go b/script/generate-tags.go index 78a2621..24eae8c 100644 --- a/script/generate-tags.go +++ b/script/generate-tags.go @@ -47,7 +47,7 @@ func main() { printf("// generated by ../script/generate-tags.go\n") printf("\n") - printf("package tags\n") + printf("package tag\n") printf("import \"code.squareroundforest.org/arpio/html\"\n") for _, si := range ss { exp := fmt.Sprintf("html.Define(\"%s\")", si) diff --git a/script/promote-to-tags.go b/script/promote-to-tags.go index ec733de..c3dd1a6 100644 --- a/script/promote-to-tags.go +++ b/script/promote-to-tags.go @@ -46,7 +46,7 @@ func main() { printf("// generated by ../script/promote-to-tags.go\n") printf("\n") - printf("package tags\n") + printf("package tag\n") printf("import \"code.squareroundforest.org/arpio/html\"\n") for _, si := range ss { printf("var %s = html.%s\n", si, si) diff --git a/tags.block.txt b/tag.block.txt similarity index 100% rename from tags.block.txt rename to tag.block.txt diff --git a/tags.inline.txt b/tag.inline.txt similarity index 100% rename from tags.inline.txt rename to tag.inline.txt diff --git a/tags.inlinechildren.txt b/tag.inlinechildren.txt similarity index 100% rename from tags.inlinechildren.txt rename to tag.inlinechildren.txt diff --git a/tags.script.txt b/tag.script.txt similarity index 100% rename from tags.script.txt rename to tag.script.txt diff --git a/tags.void.block.txt b/tag.void.block.txt similarity index 100% rename from tags.void.block.txt rename to tag.void.block.txt diff --git a/tags.void.inline.txt b/tag.void.inline.txt similarity index 100% rename from tags.void.inline.txt rename to tag.void.inline.txt diff --git a/tags/block.gen.go b/tag/block.gen.go similarity index 99% rename from tags/block.gen.go rename to tag/block.gen.go index b94b0aa..9fcb837 100644 --- a/tags/block.gen.go +++ b/tag/block.gen.go @@ -1,6 +1,6 @@ // generated by ../script/generate-tags.go -package tags +package tag import "code.squareroundforest.org/arpio/html" diff --git a/tags/inline.gen.go b/tag/inline.gen.go similarity index 99% rename from tags/inline.gen.go rename to tag/inline.gen.go index 6d84356..a2d538a 100644 --- a/tags/inline.gen.go +++ b/tag/inline.gen.go @@ -1,6 +1,6 @@ // generated by ../script/generate-tags.go -package tags +package tag import "code.squareroundforest.org/arpio/html" diff --git a/tags/inlinechildren.gen.go b/tag/inlinechildren.gen.go similarity index 97% rename from tags/inlinechildren.gen.go rename to tag/inlinechildren.gen.go index 0b0a993..911c1a3 100644 --- a/tags/inlinechildren.gen.go +++ b/tag/inlinechildren.gen.go @@ -1,6 +1,6 @@ // generated by ../script/generate-tags.go -package tags +package tag import "code.squareroundforest.org/arpio/html" diff --git a/tags/promote.gen.go b/tag/promote.gen.go similarity index 93% rename from tags/promote.gen.go rename to tag/promote.gen.go index 3893eb2..ee4d67f 100644 --- a/tags/promote.gen.go +++ b/tag/promote.gen.go @@ -1,6 +1,6 @@ // generated by ../script/promote-to-tags.go -package tags +package tag import "code.squareroundforest.org/arpio/html" diff --git a/tags/script.gen.go b/tag/script.gen.go similarity index 93% rename from tags/script.gen.go rename to tag/script.gen.go index cec0a0d..3a520c6 100644 --- a/tags/script.gen.go +++ b/tag/script.gen.go @@ -1,6 +1,6 @@ // generated by ../script/generate-tags.go -package tags +package tag import "code.squareroundforest.org/arpio/html" diff --git a/tags/void.block.gen.go b/tag/void.block.gen.go similarity index 96% rename from tags/void.block.gen.go rename to tag/void.block.gen.go index 9ced5c0..3a874e4 100644 --- a/tags/void.block.gen.go +++ b/tag/void.block.gen.go @@ -1,6 +1,6 @@ // generated by ../script/generate-tags.go -package tags +package tag import "code.squareroundforest.org/arpio/html" diff --git a/tags/void.inline.gen.go b/tag/void.inline.gen.go similarity index 96% rename from tags/void.inline.gen.go rename to tag/void.inline.gen.go index 816cc45..ac2b751 100644 --- a/tags/void.inline.gen.go +++ b/tag/void.inline.gen.go @@ -1,6 +1,6 @@ // generated by ../script/generate-tags.go -package tags +package tag import "code.squareroundforest.org/arpio/html" diff --git a/validate.go b/validate.go index 46906b0..208d130 100644 --- a/validate.go +++ b/validate.go @@ -21,6 +21,10 @@ func validateSymbol(s string) error { } func validateTagName(name string) error { + if strings.HasPrefix(name, "!") { + return nil + } + return validateSymbol(name) } @@ -41,7 +45,7 @@ func validate(name string, children []any) error { isDeclaration := strings.HasPrefix(name, "!") for _, ai := range a { - for name := range ai { + for _, name := range ai.names { if isDeclaration { continue } diff --git a/validate_test.go b/validate_test.go index ddc3949..b5981c5 100644 --- a/validate_test.go +++ b/validate_test.go @@ -3,7 +3,7 @@ package html_test import ( "bytes" "code.squareroundforest.org/arpio/html" - . "code.squareroundforest.org/arpio/html/tags" + . "code.squareroundforest.org/arpio/html/tag" "testing" ) diff --git a/wrap.go b/wrap.go index 6052b4e..523f4d8 100644 --- a/wrap.go +++ b/wrap.go @@ -8,11 +8,12 @@ import ( ) type wrapper struct { - out *indentWriter - width int - line, word *bytes.Buffer - inWord, inTag, inSingleQuote, inQuote, lastSpace, started bool - err error + out *indentWriter + width int + line, word *bytes.Buffer + inWord, inTag, inSingleQuote, inQuote bool + lastSpace, started bool + err error } func newWrapper(out io.Writer, width int, indent string) *wrapper { diff --git a/wrap_test.go b/wrap_test.go index 3c7d7c5..d97f6d9 100644 --- a/wrap_test.go +++ b/wrap_test.go @@ -3,7 +3,7 @@ package html_test import ( "bytes" "code.squareroundforest.org/arpio/html" - . "code.squareroundforest.org/arpio/html/tags" + . "code.squareroundforest.org/arpio/html/tag" "testing" ) @@ -56,7 +56,7 @@ func TestWrap(t *testing.T) { ew := &errorWriter{} if err := html.RenderIndent( ew, - html.Indentation{Indent: "\t"}, + html.Indent(), Span(Span("foo"), Span("bar"), Span("baz")), ); err == nil { t.Fatal() @@ -117,7 +117,7 @@ func TestWrap(t *testing.T) { div := Div(Span("foo bar baz qux quux corge")) var buf bytes.Buffer - if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t", PWidth: 9}, div); err != nil { + if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t", PWidth: 15}, div); err != nil { t.Fatal(err) } @@ -130,7 +130,7 @@ func TestWrap(t *testing.T) { div := Div(Span("foo"), " ", Span("bar")) var buf bytes.Buffer - if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t"}, div); err != nil { + if err := html.RenderIndent(&buf, html.Indent(), div); err != nil { t.Fatal(err) } @@ -143,7 +143,7 @@ func TestWrap(t *testing.T) { div := Div("foo\nbar\tbaz") var buf bytes.Buffer - if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t"}, div); err != nil { + if err := html.RenderIndent(&buf, html.Indent(), div); err != nil { t.Fatal(err) }