diff --git a/Makefile b/Makefile index 93d55d6..87c189e 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ 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/script.gen.go +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 promote-to-tags: tags/promote.gen.go @@ -17,6 +17,9 @@ tags/block.gen.go: $(SOURCES) tags.block.txt tags/inline.gen.go: $(SOURCES) tags.inline.txt go run script/generate-tags.go Inline < tags.inline.txt > tags/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 + 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 diff --git a/lib.go b/lib.go index 1afee05..405c844 100644 --- a/lib.go +++ b/lib.go @@ -2,6 +2,7 @@ package html import ( + "bytes" "fmt" "io" "strings" @@ -22,6 +23,21 @@ type Tag func(...any) Tag type Template[Data any] func(Data) Tag +type Indentation struct { + PWidth int + MinPWidth int + + // not used for the top level + 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 { @@ -38,19 +54,41 @@ func Attr(a ...any) Attributes { } // defines a new tag with name and initial attributes and child nodes -func NewTag(name string, children ...any) Tag { +func Define(name string, children ...any) Tag { if handleQuery(name, children) { children = children[:len(children)-1] } return func(children1 ...any) Tag { - if name == "br" { - } - - return NewTag(name, append(children, children1...)...) + return Define(name, append(children, children1...)...) } } +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...)...) +} + +func Comment(children ...any) Tag { + return Inline(Declaration(append(append([]any{"--"}, children...), "--"))) +} + // returns the name of a tag func Name(t Tag) string { q := nameQuery{} @@ -89,7 +127,7 @@ func DeleteAttribute(t Tag, name string) Tag { a := AllAttributes(t) c := Children(t) delete(a, name) - return NewTag(n, append(c, a)...) + return Define(n, append(c, a)...) } // the same as Attribute(t, "class") @@ -141,15 +179,29 @@ func Children(t Tag) []any { // consecutive spaces are considered to be so on purpose, and are converted into   // spaces around tags can behave different from when using unindented rendering // as a last resort, one can use rendered html inside a verbatim tag -func RenderIndent(out io.Writer, indent string, pwidth int, t Tag) error { - r := renderer{out: out, indent: indent, pwidth: pwidth} +func RenderIndent(out io.Writer, indent Indentation, t Tag) error { + if indent.PWidth < indent.MinPWidth { + indent.PWidth = indent.MinPWidth + } + + if indent.Indent != "" && indent.PWidth == 0 { + indent.PWidth = 120 + indent.MinPWidth = 60 + } + + r := renderer{ + out: out, + indent: indent, + pwidth: indent.PWidth, + } + t()(&r) return r.err } // renders html with t as the root node without indentation func Render(out io.Writer, t Tag) error { - return RenderIndent(out, "", 0, t) + return RenderIndent(out, Indentation{}, t) } // creates a new tag from t marking it verbatim. The content of verbatim tags is rendered without HTML escaping. @@ -165,7 +217,7 @@ func ScriptContent(t Tag) Tag { } func InlineChildren(t Tag) Tag { - return t + return t()(renderGuide{inlineChildren: true}) } // inline tags are not broken into separate lines when rendering with indentation @@ -192,15 +244,15 @@ func Eq(t ...Tag) bool { } // turns a template into a tag for composition -func FromTemplate[Data any](f Template[Data]) Tag { +func FromTemplate[Data any](t Template[Data]) Tag { return func(a ...any) Tag { var ( - t Data + d Data ok bool ) for i := range a { - t, ok = a[0].(Data) + d, ok = a[0].(Data) if !ok { continue } @@ -209,33 +261,24 @@ func FromTemplate[Data any](f Template[Data]) Tag { break } - return f(t)(a...) + return t(d)(a...) } } // in the functional programming sense -func Map[Data any](data []Data, tag Tag, tags ...Tag) []Tag { +func Map[Data any](data []Data, tag Tag) []Tag { var ret []Tag for _, d := range data { - var retd Tag - for i := len(tags) - 1; i >= 0; i-- { - retd = tags[i](d) - } - - if retd == nil { - retd = tag(d) - continue - } - - retd = tag(retd) + ret = append(ret, tag(d)) } return ret } -func MapChildren[Data any](data []Data, tag Tag, tags ...Tag) []any { +func MapChildren[Data any](data []Data, tag Tag) []any { + c := Map(data, tag) + var a []any - c := Map(data, tag, tags...) for _, ci := range c { a = append(a, ci) } diff --git a/lib_test.go b/lib_test.go index b5df03a..d7231d1 100644 --- a/lib_test.go +++ b/lib_test.go @@ -1,10 +1,11 @@ package html_test import ( + "bytes" "code.squareroundforest.org/arpio/html" . "code.squareroundforest.org/arpio/html/tags" "testing" - "bytes" + "code.squareroundforest.org/arpio/notation" ) func TestLib(t *testing.T) { @@ -57,17 +58,13 @@ func TestLib(t *testing.T) { } var b bytes.Buffer - if err := html.RenderIndent(&b, "\t", 0, teamHTML(myTeam)); err != nil { + if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, teamHTML(myTeam)); err != nil { t.Fatal(err) } - if b.String() != `
-

- Foo -

-

- Rank: 3 -

+ const expect = `
+

Foo

+

Rank: 3

-
-` { +
` + + if b.String() != expect { + notation.Println([]byte(expect)[48:96]) + notation.Println(b.Bytes()[48:96]) t.Fatal(b.String()) } }) + + t.Run("ampty attributes", func(t *testing.T) { + }) } diff --git a/notes.txt b/notes.txt index 7b4e7b9..c3f9f9a 100644 --- a/notes.txt +++ b/notes.txt @@ -8,3 +8,9 @@ explain the immutability guarantee in the Go docs: for children yes, for childre recommendation is not to mutate children. Ofc, creatively breaking the rules is always well appreciated by the right audience test wrapped templates +test empty block +escape extra space between tag boundaries +declarations: +comments: +attritubes, when bool true, then just the name of the attribute +implement stringer for the tag diff --git a/print_test.go b/print_test.go new file mode 100644 index 0000000..c1408d0 --- /dev/null +++ b/print_test.go @@ -0,0 +1,12 @@ +package html_test + +import ( + "fmt" + "code.squareroundforest.org/arpio/notation" +) + +func printBytes(a ...any) { + for _, ai := range a { + notation.Println([]byte(fmt.Sprint(ai))) + } +} diff --git a/promote-to-tags.txt b/promote-to-tags.txt index ce2aad6..ffa5ab8 100644 --- a/promote-to-tags.txt +++ b/promote-to-tags.txt @@ -1,2 +1,4 @@ Attr -NewTag +Define +Doctype +Comment diff --git a/query.go b/query.go index 29278cb..c7c678d 100644 --- a/query.go +++ b/query.go @@ -121,7 +121,7 @@ func handleQuery(name string, children []any) bool { return true } - render(r, name, children[:last]) + r.render(name, children[:last]) return true } diff --git a/query_test.go b/query_test.go index de3f911..ad605f0 100644 --- a/query_test.go +++ b/query_test.go @@ -109,11 +109,11 @@ func TestQuery(t *testing.T) { div := Div(Span("foo")) var b bytes.Buffer - if err := html.RenderIndent(&b, "\t", 0, div); err != nil { + if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, div); err != nil { t.Fatal(err) } - if b.String() != "
\n\tfoo\n
\n" { + if b.String() != "
\n\tfoo\n
" { t.Fatal(b.String()) } }) diff --git a/render.go b/render.go index fbbf3d8..31421dd 100644 --- a/render.go +++ b/render.go @@ -1,23 +1,28 @@ package html import ( - "bytes" "fmt" "io" + "strings" ) -const defaultPWidth = 112 +const ( + defaultPWidth = 112 + unicodeNBSP = 0xa0 +) type renderGuide struct { - inline bool - void bool - script bool - verbatim bool + inline bool + inlineChildren bool + void bool + script bool + verbatim bool } type renderer struct { out io.Writer - indent string + originalOut io.Writer + indent Indentation pwidth int currentIndent string err error @@ -27,6 +32,7 @@ func mergeRenderingGuides(rgs []renderGuide) renderGuide { var rg renderGuide for _, rgi := range rgs { rg.inline = rg.inline || rgi.inline + rg.inlineChildren = rg.inlineChildren || rgi.inlineChildren rg.void = rg.void || rgi.void rg.script = rg.script || rgi.script rg.verbatim = rg.verbatim || rgi.verbatim @@ -67,7 +73,9 @@ func htmlEscape(s string) string { rr = append(rr, []rune(">")...) case '&': rr = append(rr, []rune("&")...) - case ' ', 0xA0: + case unicodeNBSP: + rr = append(rr, []rune(" ")...) + case ' ': if wsStart && lastWS { rr = append(rr[:len(rr)-1], []rune("  ")...) } else if lastWS { @@ -79,7 +87,7 @@ func htmlEscape(s string) string { rr = append(rr, r[i]) } - ws := r[i] == ' ' || r[i] == 0xA0 + ws := r[i] == ' ' wsStart = ws && !lastWS lastWS = ws } @@ -87,12 +95,317 @@ func htmlEscape(s string) string { return string(rr) } -func render(r *renderer, name string, children []any) { +func indentLines(indent string, s string) string { + l := strings.Split(s, "\n") + for i := range l { + l[i] = fmt.Sprintf("%s%s", indent, l[i]) + } + + return strings.Join(l, "\n") +} + +func (r *renderer) getPrintf(tagName string) func(f string, a ...any) { + return func(f string, a ...any) { + if r.err != nil { + return + } + + _, r.err = fmt.Fprintf(r.out, f, a...) + if r.err != nil { + r.err = fmt.Errorf("tag %s: %w", tagName, r.err) + } + } +} + +func (r *renderer) renderAttributes(tagName string, a []Attributes) { + printf := r.getPrintf(tagName) + for _, ai := range a { + for name, value := range ai { + printf(" %s=\"%s\"", name, attributeEscape(value)) + } + } +} + +func (r *renderer) renderUnindented(name string, rg renderGuide, a []Attributes, children []any) { + printf := r.getPrintf(name) + printf("<%s", name) + r.renderAttributes(name, a) + printf(">") + if rg.void { + return + } + + for _, c := range children { + if ct, ok := c.(Tag); ok { + ct(r) + continue + } + + s := fmt.Sprint(c) + if s == "" { + continue + } + + if !rg.verbatim && !rg.script { + s = htmlEscape(s) + } + + printf(s) + } + + printf("", name) +} + +func (r *renderer) ensureWrapper() bool { + if _, ok := r.out.(*wrapper); ok { + return false + } + + r.originalOut = r.out + r.out = newWrapper(r.originalOut, r.pwidth, r.currentIndent) + return true +} + +func (r *renderer) clearWrapper() { + w, ok := r.out.(*wrapper) + if !ok { + return + } + + if err := w.Flush(); err != nil { + r.err = err + } + + r.out = r.originalOut +} + +func (r *renderer) renderInline(name string, rg renderGuide, a []Attributes, children []any) { + newWrapper := r.ensureWrapper() + printf := r.getPrintf(name) + printf("<%s", name) + r.renderAttributes(name, a) + printf(">") + if rg.void { + if newWrapper { + r.clearWrapper() + } + + return + } + + var lastBlock bool + for _, c := range children { + ct, isTag := c.(Tag) + if !isTag && rg.verbatim { + s := fmt.Sprint(c) + if s == "" { + continue + } + + r.clearWrapper() + s = indentLines(r.currentIndent+r.indent.Indent, s) + printf("\n%s", s) + lastBlock = true + continue + } + + if !isTag && rg.script { + s := fmt.Sprint(c) + if s == "" { + continue + } + + r.clearWrapper() + printf("\n%s", s) + lastBlock = true + continue + } + + if !isTag { + s := fmt.Sprint(c) + if s == "" { + continue + } + + if lastBlock { + printf("\n%s", r.currentIndent) + } + + if r.ensureWrapper() { + newWrapper = true + } + + s = htmlEscape(s) + printf(s) + lastBlock = false + continue + } + + var rgq renderGuidesQuery + ct(&rgq) + crg := mergeRenderingGuides(rgq.value) + if crg.inline { + if lastBlock { + printf("\n%s", r.currentIndent) + } + + if r.ensureWrapper() { + newWrapper = true + } + + ct(r) + lastBlock = false + continue + } + + r.clearWrapper() + cr := new(renderer) + *cr = *r + cr.currentIndent += cr.indent.Indent + cr.pwidth -= len([]rune(cr.indent.Indent)) + if cr.pwidth < cr.indent.MinPWidth { + cr.pwidth = cr.indent.MinPWidth + } + + printf("\n%s", cr.currentIndent) + ct(cr) + if cr.err != nil { + r.err = cr.err + } + + lastBlock = true + } + + if lastBlock { + printf("\n%s", r.currentIndent) + } + + printf("", name) + if newWrapper { + r.clearWrapper() + } +} + +func (r *renderer) renderBlock(name string, rg renderGuide, a []Attributes, children []any) { + printf := r.getPrintf(name) + printf("<%s", name) + r.renderAttributes(name, a) + printf(">") + if rg.void { + return + } + + if len(children) == 0 { + printf("", name) + return + } + + lastBlock := true + originalIndent, originalWidth := r.currentIndent, r.pwidth + r.currentIndent += r.indent.Indent + r.pwidth -= len([]rune(r.indent.Indent)) + if r.pwidth < r.indent.MinPWidth { + r.pwidth = r.indent.MinPWidth + } + + for _, c := range children { + ct, isTag := c.(Tag) + if !isTag && rg.verbatim { + s := fmt.Sprint(c) + if s == "" { + continue + } + + r.clearWrapper() + s = indentLines(r.currentIndent, s) + printf("\n%s", s) + lastBlock = true + continue + } + + if !isTag && rg.script { + s := fmt.Sprint(c) + if s == "" { + continue + } + + r.clearWrapper() + printf("\n%s", s) + lastBlock = true + continue + } + + if !isTag { + s := fmt.Sprint(c) + if s == "" { + continue + } + + if lastBlock { + printf("\n%s", r.currentIndent) + } + + r.ensureWrapper() + s = htmlEscape(s) + printf(s) + lastBlock = false + continue + } + + var rgq renderGuidesQuery + ct(&rgq) + crg := mergeRenderingGuides(rgq.value) + if crg.inline { + if lastBlock { + printf("\n%s", r.currentIndent) + } + + r.ensureWrapper() + ct(r) + lastBlock = false + continue + } + + r.clearWrapper() + cr := new(renderer) + *cr = *r + printf("\n%s", cr.currentIndent) + ct(cr) + if cr.err != nil { + r.err = cr.err + } + + lastBlock = true + } + + r.clearWrapper() + r.currentIndent, r.pwidth = originalIndent, originalWidth + printf("\n%s", r.currentIndent, name) +} + +func (r *renderer) render(name string, children []any) { if r.err != nil { return } - printf := func(f string, a ...any) { + a, c, rgs := groupChildren(children) + rg := mergeRenderingGuides(rgs) + if r.indent.Indent == "" && r.indent.PWidth <= 0 { + r.renderUnindented(name, rg, a, c) + return + } + + if rg.inline || rg.inlineChildren { + r.renderInline(name, rg, a, c) + return + } + + r.renderBlock(name, rg, a, c) +} + +/* +func getPrintf(out io.Writer) func(f string, a ...any) { + return func(f string, a ...any) { if r.err != nil { return } @@ -102,16 +415,189 @@ func render(r *renderer, name string, children []any) { r.err = fmt.Errorf("tag %s: %w", name, r.err) } } +} - a, c, rgs := groupChildren(children) - rg := mergeRenderingGuides(rgs) - printf(r.currentIndent) - printf("<%s", name) +func renderAttributes(out io.Writer, a []Attributes) { + printf := getPrintf(out) for _, ai := range a { for name, value := range ai { printf(" %s=\"%s\"", name, attributeEscape(value)) } } +} + +func renderUnindented(r *renderer, name string, rg renderGuide, a []Attributes, children []any) { + printf := getPrintf(r.out) + printf("<%s", name) + renderAttributes(r.out, a) + printf(">") + if rg.void { + return + } + + for _, c := range children { + if ct, ok := c.(Tag); ok { + ct(r) + continue + } + + s := fmt.Sprint(c) + if s == "" { + continue + } + + if !rg.verbatim && !rg.script { + s = htmlEscape(s) + } + + printf(s) + } + + printf("", name) +} + +func renderInline(r *renderer, name string, rg renderGuide, a []Attributes, children []any) { + printf := getPrintf(r.out) + printf("<%s", name) + renderAttributes(r.out, a) + printf(">") + if rg.void { + return + } + + for _, c := range children { + if ct, ok := c.(Tag); ok { + var rgq renderGuidesQuery + ct(&rgq) + crg := mergeRenderingGuides(rgq.value) + if crg.inline { + ct(r) + continue + } + + printf("\n") + cr := new(renderer) + *cr = *r + cr.currentIndent += cr.indent + ct(cr) + continue + } + + s := fmt.Sprint(c) + if s == "" { + continue + } + + if !rg.verbatim && !rg.script { + s = htmlEscape(s) + } + + printf(s) + } + + printf("", name) +} + +func renderBlock(r *renderer, name string, rg renderGuide, a []Attributes, children []any) { + if r.direct == nil { + r.direct = r.out + } + + printf := getPrintf(r.direct) + printf(r.currentIndent) + printf("<%s", name) + renderAttributes(r.direct, a) + printf(">") + if len(c) == 0 { + printf("", name) + return + } + + if r.indent != "" { + printf("\n") + } + + var ( + inlineBuffer bytes.Buffer + cr *renderer + lastInline bool + ) + + for i, c := range children { + if ct, ok := c.(Tag); ok { + var rgq renderGuidesQuery + ct(&rgq) + crg := mergeRenderingGuides(rgq.value) + if crg.inline { + if cr == nil { + cr = new(renderer) + *cr = *r + cr.currentIndent += cr.indent + } + + cr.out = &inlineBuffer + if !lastInline { + printf(r.currentIndent + r.indent) + } + + ct(cr) + lastInline = true + continue + } + + inline := inlineBuffer.String() + if inline != "" { + // flush + // newline + } + + continue + } + + lastInline = true + } + + inline := inlineBuffer.String() + if inline != "" { + // flush inline + } + + if r.indent != "" { + printf("\n") + printf(r.currentIndent) + } + + printf("", name) + if r.indent != "" { + printf("\n") + } +} + +func render(r *renderer, name string, children []any) { + if r.err != nil { + return + } + + a, c, rgs := groupChildren(children) + rg := mergeRenderingGuides(rgs) + if r.indent == "" { + renderUnindented(r, name, rg, a, c) + return + } + + if rg.inline { + // TODO: + // - may need to wrap it here + // - could use a wrapping buffer + renderInline(r, name, rg, a, c) + return + } + + renderBlock(r, name, rg, a, c) + + // -- + + printf("<%s", name) printf(">") if r.indent != "" && !rg.inline && len(c) > 0 { @@ -123,9 +609,6 @@ func render(r *renderer, name string, children []any) { } var inlineBuffer *bytes.Buffer - if r.indent != "" { - inlineBuffer = bytes.NewBuffer(nil) - } // TODO: // - avoid rendering an inline buffer into another inline buffer @@ -133,8 +616,21 @@ func render(r *renderer, name string, children []any) { // - or, if inline, just use the inline buffer without indentation // - check the wrapping again, if it preserves or eliminates the spaces the right way for i, ci := range c { + // tag && rg.inline && crg.inline + // tag && rg.inline && !crg.inline + // tag && !rg.inline && crg.inline + // tag && !rg.inline && !crg.inline + // !tag && rg.inline && crg.inline + // !tag && rg.inline && !crg.inline + // !tag && !rg.inline && crg.inline + // !tag && !rg.inline && !crg.inline + if tag, ok := ci.(Tag); ok { if rg.inline { + if inlineBuffer == nil { + inlineBuffer = bytes.NewBuffer(nil) + } + var rgq renderGuidesQuery tag(&rgq) crg := mergeRenderingGuides(rgq.value) @@ -145,7 +641,6 @@ func render(r *renderer, name string, children []any) { } inlineBuffer = wrap(inlineBuffer, w, "") - println(inlineBuffer.String()) if _, err := io.Copy(r.out, inlineBuffer); err != nil { r.err = err return @@ -174,7 +669,6 @@ func render(r *renderer, name string, children []any) { } inlineBuffer = wrap(inlineBuffer, w, r.currentIndent+r.indent) - println(inlineBuffer.String()) if _, err := io.Copy(r.out, inlineBuffer); err != nil { r.err = err return @@ -248,3 +742,4 @@ func render(r *renderer, name string, children []any) { printf("\n") } } +*/ diff --git a/render_test.go b/render_test.go index eee466e..478384e 100644 --- a/render_test.go +++ b/render_test.go @@ -28,15 +28,15 @@ func (w *failingWriter) Write(p []byte) (int, error) { func TestRender(t *testing.T) { t.Run("merge render guides", func(t *testing.T) { - foo := html.Inline(html.Verbatim(NewTag("foo"))) + foo := html.Inline(html.Verbatim(Define("foo"))) foo = foo("") var b bytes.Buffer - if err := html.RenderIndent(&b, "\t", 0, foo); err != nil { + if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, foo); err != nil { t.Fatal(err) } - if b.String() != "" { + if b.String() != "\n\t\n" { t.Fatal(b.String()) } }) @@ -102,7 +102,7 @@ func TestRender(t *testing.T) { t.Run("partial text children", func(t *testing.T) { div := Div("foo", Div("bar"), "baz") w := failWriteAfter(5) - if err := html.RenderIndent(w, "\t", 0, div); err == nil || !strings.Contains(err.Error(), "test error") { + if err := html.RenderIndent(w, html.Indentation{Indent: "\t"}, div); err == nil || !strings.Contains(err.Error(), "test error") { t.Fatal() } }) @@ -110,7 +110,7 @@ func TestRender(t *testing.T) { t.Run("text children", func(t *testing.T) { div := Div("foo", "bar", "baz") w := failWriteAfter(5) - if err := html.RenderIndent(w, "\t", 0, div); err == nil || !strings.Contains(err.Error(), "test error") { + if err := html.RenderIndent(w, html.Indentation{Indent: "\t"}, div); err == nil || !strings.Contains(err.Error(), "test error") { t.Fatal() } }) @@ -121,11 +121,11 @@ func TestRender(t *testing.T) { div := Div(Span("foo")) var b bytes.Buffer - if err := html.RenderIndent(&b, "\t", 0, div); err != nil { + if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, div); err != nil { t.Fatal(err) } - if b.String() != "
\n\tfoo\n
\n" { + if b.String() != "
\n\tfoo\n
" { t.Fatal(b.String()) } }) @@ -134,11 +134,11 @@ func TestRender(t *testing.T) { div := Div(Br()) var b bytes.Buffer - if err := html.RenderIndent(&b, "\t", 0, div); err != nil { + if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, div); err != nil { t.Fatal(err) } - if b.String() != "
\n\t
\n
\n" { + if b.String() != "
\n\t
\n
" { t.Fatal(b.String()) } }) @@ -147,11 +147,11 @@ func TestRender(t *testing.T) { div := Div("foo bar baz", Div("qux quux"), "corge") var b bytes.Buffer - if err := html.RenderIndent(&b, "\t", 0, div); err != nil { + if err := html.RenderIndent(&b, html.Indentation{Indent: "\t"}, div); err != nil { t.Fatal(err) } - if b.String() != "
\n\tfoo bar baz\n\t
\n\t\tqux quux\n\t
\n\tcorge\n
\n" { + if b.String() != "
\n\tfoo bar baz\n\t
\n\t\tqux quux\n\t
\n\tcorge\n
" { t.Fatal(b.String()) } }) @@ -160,11 +160,11 @@ func TestRender(t *testing.T) { div := Div(Span("foo bar baz", Div("qux quux"), "corge")) var b bytes.Buffer - if err := html.RenderIndent(&b, "XYZ", 0, div); err != nil { + if err := html.RenderIndent(&b, html.Indentation{Indent: "XYZ"}, div); err != nil { t.Fatal(err) } - if b.String() != "" { + if b.String() != "
\nXYZfoo bar baz\nXYZXYZ
\nXYZXYZXYZqux quux\nXYZXYZ
\nXYZcorge
\n
" { t.Fatal(b.String()) } }) diff --git a/script/generate-tags.go b/script/generate-tags.go index 6637bc2..78a2621 100644 --- a/script/generate-tags.go +++ b/script/generate-tags.go @@ -50,7 +50,7 @@ func main() { printf("package tags\n") printf("import \"code.squareroundforest.org/arpio/html\"\n") for _, si := range ss { - exp := fmt.Sprintf("html.NewTag(\"%s\")", si) + exp := fmt.Sprintf("html.Define(\"%s\")", si) for _, a := range os.Args[1:] { exp = fmt.Sprintf("html.%s(%s)", a, exp) } diff --git a/script/promote-to-tags.go b/script/promote-to-tags.go index 3832459..ec733de 100644 --- a/script/promote-to-tags.go +++ b/script/promote-to-tags.go @@ -44,7 +44,7 @@ func main() { _, err = fmt.Fprintf(os.Stdout, f, a...) } - printf("// generated by ../script/generate-tags.go\n") + printf("// generated by ../script/promote-to-tags.go\n") printf("\n") printf("package tags\n") printf("import \"code.squareroundforest.org/arpio/html\"\n") diff --git a/tags.block.txt b/tags.block.txt index 4628ef5..8e61228 100644 --- a/tags.block.txt +++ b/tags.block.txt @@ -27,7 +27,6 @@ header hgroup html ins -li link main map @@ -37,7 +36,6 @@ nav noscript ol optgroup -p picture pre rp @@ -46,11 +44,9 @@ section summary table tbody -td template textarea tfoot -th thead title tr diff --git a/tags.inline.txt b/tags.inline.txt index 9cf359b..a0d7cde 100644 --- a/tags.inline.txt +++ b/tags.inline.txt @@ -9,7 +9,6 @@ code data dfn em -h1, h2, h3, h4, h5, h6 i kbd label diff --git a/tags.inlinechildren.txt b/tags.inlinechildren.txt new file mode 100644 index 0000000..b7df65a --- /dev/null +++ b/tags.inlinechildren.txt @@ -0,0 +1,5 @@ +h1, h2, h3, h4, h5, h6 +li +p +td +th diff --git a/tags/block.gen.go b/tags/block.gen.go index bd830ed..974da0d 100644 --- a/tags/block.gen.go +++ b/tags/block.gen.go @@ -2,61 +2,57 @@ package tags import "code.squareroundforest.org/arpio/html" -var Address = html.NewTag("address") -var Article = html.NewTag("article") -var Audio = html.NewTag("audio") -var Aside = html.NewTag("aside") -var Blockquote = html.NewTag("blockquote") -var Body = html.NewTag("body") -var Canvas = html.NewTag("canvas") -var Caption = html.NewTag("caption") -var Center = html.NewTag("center") -var Col = html.NewTag("col") -var Colgroup = html.NewTag("colgroup") -var Datalist = html.NewTag("datalist") -var Dd = html.NewTag("dd") -var Del = html.NewTag("del") -var Details = html.NewTag("details") -var Dialog = html.NewTag("dialog") -var Div = html.NewTag("div") -var Dl = html.NewTag("dl") -var Dt = html.NewTag("dt") -var Fieldset = html.NewTag("fieldset") -var Figcaption = html.NewTag("figcaption") -var Figure = html.NewTag("figure") -var Footer = html.NewTag("footer") -var Form = html.NewTag("form") -var Head = html.NewTag("head") -var Header = html.NewTag("header") -var Hgroup = html.NewTag("hgroup") -var Html = html.NewTag("html") -var Ins = html.NewTag("ins") -var Li = html.NewTag("li") -var Link = html.NewTag("link") -var Main = html.NewTag("main") -var Map = html.NewTag("map") -var Math = html.NewTag("math") -var Menu = html.NewTag("menu") -var Nav = html.NewTag("nav") -var Noscript = html.NewTag("noscript") -var Ol = html.NewTag("ol") -var Optgroup = html.NewTag("optgroup") -var P = html.NewTag("p") -var Picture = html.NewTag("picture") -var Pre = html.NewTag("pre") -var Rp = html.NewTag("rp") -var Search = html.NewTag("search") -var Section = html.NewTag("section") -var Summary = html.NewTag("summary") -var Table = html.NewTag("table") -var Tbody = html.NewTag("tbody") -var Td = html.NewTag("td") -var Template = html.NewTag("template") -var Textarea = html.NewTag("textarea") -var Tfoot = html.NewTag("tfoot") -var Th = html.NewTag("th") -var Thead = html.NewTag("thead") -var Title = html.NewTag("title") -var Tr = html.NewTag("tr") -var Ul = html.NewTag("ul") -var Video = html.NewTag("video") +var Address = html.Define("address") +var Article = html.Define("article") +var Audio = html.Define("audio") +var Aside = html.Define("aside") +var Blockquote = html.Define("blockquote") +var Body = html.Define("body") +var Canvas = html.Define("canvas") +var Caption = html.Define("caption") +var Center = html.Define("center") +var Col = html.Define("col") +var Colgroup = html.Define("colgroup") +var Datalist = html.Define("datalist") +var Dd = html.Define("dd") +var Del = html.Define("del") +var Details = html.Define("details") +var Dialog = html.Define("dialog") +var Div = html.Define("div") +var Dl = html.Define("dl") +var Dt = html.Define("dt") +var Fieldset = html.Define("fieldset") +var Figcaption = html.Define("figcaption") +var Figure = html.Define("figure") +var Footer = html.Define("footer") +var Form = html.Define("form") +var Head = html.Define("head") +var Header = html.Define("header") +var Hgroup = html.Define("hgroup") +var Html = html.Define("html") +var Ins = html.Define("ins") +var Link = html.Define("link") +var Main = html.Define("main") +var Map = html.Define("map") +var Math = html.Define("math") +var Menu = html.Define("menu") +var Nav = html.Define("nav") +var Noscript = html.Define("noscript") +var Ol = html.Define("ol") +var Optgroup = html.Define("optgroup") +var Picture = html.Define("picture") +var Pre = html.Define("pre") +var Rp = html.Define("rp") +var Search = html.Define("search") +var Section = html.Define("section") +var Summary = html.Define("summary") +var Table = html.Define("table") +var Tbody = html.Define("tbody") +var Template = html.Define("template") +var Textarea = html.Define("textarea") +var Tfoot = html.Define("tfoot") +var Thead = html.Define("thead") +var Title = html.Define("title") +var Tr = html.Define("tr") +var Ul = html.Define("ul") +var Video = html.Define("video") diff --git a/tags/inline.gen.go b/tags/inline.gen.go index 9f2cc2c..15763ec 100644 --- a/tags/inline.gen.go +++ b/tags/inline.gen.go @@ -2,47 +2,41 @@ package tags import "code.squareroundforest.org/arpio/html" -var A = html.Inline(html.NewTag("a")) -var Abbr = html.Inline(html.NewTag("abbr")) -var B = html.Inline(html.NewTag("b")) -var Bdi = html.Inline(html.NewTag("bdi")) -var Bdo = html.Inline(html.NewTag("bdo")) -var Button = html.Inline(html.NewTag("button")) -var Cite = html.Inline(html.NewTag("cite")) -var Code = html.Inline(html.NewTag("code")) -var Data = html.Inline(html.NewTag("data")) -var Dfn = html.Inline(html.NewTag("dfn")) -var Em = html.Inline(html.NewTag("em")) -var H1 = html.Inline(html.NewTag("h1")) -var H2 = html.Inline(html.NewTag("h2")) -var H3 = html.Inline(html.NewTag("h3")) -var H4 = html.Inline(html.NewTag("h4")) -var H5 = html.Inline(html.NewTag("h5")) -var H6 = html.Inline(html.NewTag("h6")) -var I = html.Inline(html.NewTag("i")) -var Kbd = html.Inline(html.NewTag("kbd")) -var Label = html.Inline(html.NewTag("label")) -var Legend = html.Inline(html.NewTag("legend")) -var Mark = html.Inline(html.NewTag("mark")) -var Meter = html.Inline(html.NewTag("meter")) -var Object = html.Inline(html.NewTag("object")) -var Option = html.Inline(html.NewTag("option")) -var Output = html.Inline(html.NewTag("output")) -var Progress = html.Inline(html.NewTag("progress")) -var Q = html.Inline(html.NewTag("q")) -var Rt = html.Inline(html.NewTag("rt")) -var Ruby = html.Inline(html.NewTag("ruby")) -var S = html.Inline(html.NewTag("s")) -var Samp = html.Inline(html.NewTag("samp")) -var Select = html.Inline(html.NewTag("select")) -var Selectedcontent = html.Inline(html.NewTag("selectedcontent")) -var Slot = html.Inline(html.NewTag("slot")) -var Small = html.Inline(html.NewTag("small")) -var Span = html.Inline(html.NewTag("span")) -var Strong = html.Inline(html.NewTag("strong")) -var Sub = html.Inline(html.NewTag("sub")) -var Sup = html.Inline(html.NewTag("sup")) -var Svg = html.Inline(html.NewTag("svg")) -var Time = html.Inline(html.NewTag("time")) -var U = html.Inline(html.NewTag("u")) -var Var = html.Inline(html.NewTag("var")) +var A = html.Inline(html.Define("a")) +var Abbr = html.Inline(html.Define("abbr")) +var B = html.Inline(html.Define("b")) +var Bdi = html.Inline(html.Define("bdi")) +var Bdo = html.Inline(html.Define("bdo")) +var Button = html.Inline(html.Define("button")) +var Cite = html.Inline(html.Define("cite")) +var Code = html.Inline(html.Define("code")) +var Data = html.Inline(html.Define("data")) +var Dfn = html.Inline(html.Define("dfn")) +var Em = html.Inline(html.Define("em")) +var I = html.Inline(html.Define("i")) +var Kbd = html.Inline(html.Define("kbd")) +var Label = html.Inline(html.Define("label")) +var Legend = html.Inline(html.Define("legend")) +var Mark = html.Inline(html.Define("mark")) +var Meter = html.Inline(html.Define("meter")) +var Object = html.Inline(html.Define("object")) +var Option = html.Inline(html.Define("option")) +var Output = html.Inline(html.Define("output")) +var Progress = html.Inline(html.Define("progress")) +var Q = html.Inline(html.Define("q")) +var Rt = html.Inline(html.Define("rt")) +var Ruby = html.Inline(html.Define("ruby")) +var S = html.Inline(html.Define("s")) +var Samp = html.Inline(html.Define("samp")) +var Select = html.Inline(html.Define("select")) +var Selectedcontent = html.Inline(html.Define("selectedcontent")) +var Slot = html.Inline(html.Define("slot")) +var Small = html.Inline(html.Define("small")) +var Span = html.Inline(html.Define("span")) +var Strong = html.Inline(html.Define("strong")) +var Sub = html.Inline(html.Define("sub")) +var Sup = html.Inline(html.Define("sup")) +var Svg = html.Inline(html.Define("svg")) +var Time = html.Inline(html.Define("time")) +var U = html.Inline(html.Define("u")) +var Var = html.Inline(html.Define("var")) diff --git a/tags/inlinechildren.gen.go b/tags/inlinechildren.gen.go new file mode 100644 index 0000000..9c18cd3 --- /dev/null +++ b/tags/inlinechildren.gen.go @@ -0,0 +1,14 @@ +// generated by ../script/generate-tags.go + +package tags +import "code.squareroundforest.org/arpio/html" +var H1 = html.InlineChildren(html.Define("h1")) +var H2 = html.InlineChildren(html.Define("h2")) +var H3 = html.InlineChildren(html.Define("h3")) +var H4 = html.InlineChildren(html.Define("h4")) +var H5 = html.InlineChildren(html.Define("h5")) +var H6 = html.InlineChildren(html.Define("h6")) +var Li = html.InlineChildren(html.Define("li")) +var P = html.InlineChildren(html.Define("p")) +var Td = html.InlineChildren(html.Define("td")) +var Th = html.InlineChildren(html.Define("th")) diff --git a/tags/promote.gen.go b/tags/promote.gen.go index b396c03..fe6c521 100644 --- a/tags/promote.gen.go +++ b/tags/promote.gen.go @@ -1,6 +1,8 @@ -// generated by ../script/generate-tags.go +// generated by ../script/promote-to-tags.go package tags import "code.squareroundforest.org/arpio/html" var Attr = html.Attr -var NewTag = html.NewTag +var Define = html.Define +var Doctype = html.Doctype +var Comment = html.Comment diff --git a/tags/script.gen.go b/tags/script.gen.go index 05f32ca..06f0399 100644 --- a/tags/script.gen.go +++ b/tags/script.gen.go @@ -2,5 +2,5 @@ package tags import "code.squareroundforest.org/arpio/html" -var Script = html.ScriptContent(html.NewTag("script")) -var Style = html.ScriptContent(html.NewTag("style")) +var Script = html.ScriptContent(html.Define("script")) +var Style = html.ScriptContent(html.Define("style")) diff --git a/tags/void.block.gen.go b/tags/void.block.gen.go index ec15556..2df6aef 100644 --- a/tags/void.block.gen.go +++ b/tags/void.block.gen.go @@ -2,10 +2,10 @@ package tags import "code.squareroundforest.org/arpio/html" -var Area = html.Void(html.NewTag("area")) -var Base = html.Void(html.NewTag("base")) -var Hr = html.Void(html.NewTag("hr")) -var Iframe = html.Void(html.NewTag("iframe")) -var Meta = html.Void(html.NewTag("meta")) -var Source = html.Void(html.NewTag("source")) -var Track = html.Void(html.NewTag("track")) +var Area = html.Void(html.Define("area")) +var Base = html.Void(html.Define("base")) +var Hr = html.Void(html.Define("hr")) +var Iframe = html.Void(html.Define("iframe")) +var Meta = html.Void(html.Define("meta")) +var Source = html.Void(html.Define("source")) +var Track = html.Void(html.Define("track")) diff --git a/tags/void.inline.gen.go b/tags/void.inline.gen.go index cb605c6..224625d 100644 --- a/tags/void.inline.gen.go +++ b/tags/void.inline.gen.go @@ -2,8 +2,8 @@ package tags import "code.squareroundforest.org/arpio/html" -var Br = html.Inline(html.Void(html.NewTag("br"))) -var Embed = html.Inline(html.Void(html.NewTag("embed"))) -var Img = html.Inline(html.Void(html.NewTag("img"))) -var Input = html.Inline(html.Void(html.NewTag("input"))) -var Wbr = html.Inline(html.Void(html.NewTag("wbr"))) +var Br = html.Inline(html.Void(html.Define("br"))) +var Embed = html.Inline(html.Void(html.Define("embed"))) +var Img = html.Inline(html.Void(html.Define("img"))) +var Input = html.Inline(html.Void(html.Define("input"))) +var Wbr = html.Inline(html.Void(html.Define("wbr"))) diff --git a/validate.go b/validate.go index de0ea0d..46906b0 100644 --- a/validate.go +++ b/validate.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "regexp" + "strings" ) var ( @@ -38,8 +39,13 @@ func validate(name string, children []any) error { return fmt.Errorf("tag %s is void but it has children", name) } + isDeclaration := strings.HasPrefix(name, "!") for _, ai := range a { for name := range ai { + if isDeclaration { + continue + } + if err := validateAttributeName(name); err != nil { return fmt.Errorf("tag %s: %w", name, err) } @@ -48,7 +54,7 @@ func validate(name string, children []any) error { for _, ci := range c { if tag, ok := ci.(Tag); ok { - if rg.verbatim || rg.script { + if rg.script { return fmt.Errorf("tag %s does not allow child elements", name) } diff --git a/validate_test.go b/validate_test.go index 8e307c6..ddc3949 100644 --- a/validate_test.go +++ b/validate_test.go @@ -10,7 +10,7 @@ import ( func TestValidate(t *testing.T) { t.Run("symbol", func(t *testing.T) { t.Run("invalid", func(t *testing.T) { - mytag := html.NewTag("foo+bar") + mytag := html.Define("foo+bar") var b bytes.Buffer if err := html.Render(&b, mytag); err == nil { @@ -19,7 +19,7 @@ func TestValidate(t *testing.T) { }) t.Run("invalid with allowed chars number", func(t *testing.T) { - mytag := html.NewTag("0foo") + mytag := html.Define("0foo") var b bytes.Buffer if err := html.Render(&b, mytag); err == nil { @@ -28,7 +28,7 @@ func TestValidate(t *testing.T) { }) t.Run("invalid with allowed chars delimiter", func(t *testing.T) { - mytag := html.NewTag("-foo") + mytag := html.Define("-foo") var b bytes.Buffer if err := html.Render(&b, mytag); err == nil { @@ -37,7 +37,7 @@ func TestValidate(t *testing.T) { }) t.Run("valid", func(t *testing.T) { - mytag := html.NewTag("foo") + mytag := html.Define("foo") var b bytes.Buffer if err := html.Render(&b, mytag); err != nil { @@ -46,7 +46,7 @@ func TestValidate(t *testing.T) { }) t.Run("valid with special chars", func(t *testing.T) { - mytag := html.NewTag("foo-bar-1") + mytag := html.Define("foo-bar-1") var b bytes.Buffer if err := html.Render(&b, mytag); err != nil { @@ -77,8 +77,8 @@ func TestValidate(t *testing.T) { div := html.Verbatim(Div(Br())) var b bytes.Buffer - if err := html.Render(&b, div); err == nil { - t.Fatal() + if err := html.Render(&b, div); err != nil { + t.Fatal(err) } }) diff --git a/wrap.go b/wrap.go index bbdf077..df2adae 100644 --- a/wrap.go +++ b/wrap.go @@ -2,88 +2,161 @@ package html import ( "bytes" + "errors" + "io" "unicode" ) -func words(buf *bytes.Buffer) []string { - var ( - words []string - currentWord []rune - inTag bool - ) +type wrapper struct { + out io.Writer + width int + indent string + line, word *bytes.Buffer + inWord, inTag, inSingleQuote, inQuote, lastSpace, started bool + err error +} +func newWrapper(out io.Writer, width int, indent string) *wrapper { + return &wrapper{ + out: out, + width: width, + indent: indent, + line: bytes.NewBuffer(nil), + word: bytes.NewBuffer(nil), + } +} + +func (w *wrapper) feed() error { + withSpace := w.lastSpace && w.line.Len() > 0 + l := w.line.Len() + w.word.Len() + if withSpace && w.word.Len() > 0 { + l++ + } + + feedLine := l > w.width && w.line.Len() > 0 + if feedLine { + if w.started { + if _, err := w.out.Write([]byte{'\n'}); err != nil { + return err + } + + if _, err := w.out.Write([]byte(w.indent)); err != nil { + return err + } + } + + if _, err := io.Copy(w.out, w.line); err != nil { + return err + } + + w.line.Reset() + w.started = true + } + + if !feedLine && withSpace { + w.line.WriteRune(' ') + } + + io.Copy(w.line, w.word) + w.word.Reset() + return nil +} + +func (w *wrapper) Write(p []byte) (int, error) { + if w.err != nil { + return 0, w.err + } + + runes := bytes.NewBuffer(p) for { - r, _, err := buf.ReadRune() - if err != nil { - break + r, _, err := runes.ReadRune() + if errors.Is(err, io.EOF) { + return len(p), nil } if r == unicode.ReplacementChar { + w.err = errors.New("broken unicode stream") + return len(p), w.err + } + + if w.inSingleQuote { + w.inSingleQuote = r != '\'' + w.word.WriteRune(r) continue } - if !inTag && unicode.IsSpace(r) { - if len(currentWord) > 0 { - words, currentWord = append(words, string(currentWord)), nil + if w.inQuote { + w.inQuote = r != '"' + w.word.WriteRune(r) + continue + } + + if w.inTag { + w.inSingleQuote = r == '\'' + w.inQuote = r == '"' + w.inTag = r != '>' + w.word.WriteRune(r) + if !w.inTag { + if err := w.feed(); err != nil { + w.err = err + return len(p), err + } + + w.lastSpace = unicode.IsSpace(r) } continue } - currentWord = append(currentWord, r) - inTag = inTag && r != '>' || r == '<' - } + if w.inWord { + w.inTag = r == '<' + w.inWord = !w.inTag && !unicode.IsSpace(r) + if !w.inWord { + if err := w.feed(); err != nil { + w.err = err + return len(p), err + } - if len(currentWord) > 0 { - words = append(words, string(currentWord)) - } + w.lastSpace = unicode.IsSpace(r) + } - return words -} + if w.inWord || w.inTag { + w.word.WriteRune(r) + } -func wrap(buf *bytes.Buffer, pwidth int, indent string) *bytes.Buffer { - var ( - lines [][]string - currentLine []string - currentLen int - ) - - words := words(buf) - for _, w := range words { - if currentLen != 0 { - currentLen++ - } - - currentLen += len(w) - if currentLen > pwidth && len(currentLine) > 0 { - lines = append(lines, currentLine) - currentLine = []string{w} - currentLen = len(w) continue } - currentLine = append(currentLine, w) - } - - if len(currentLine) > 0 { - lines = append(lines, currentLine) - } - - ret := bytes.NewBuffer(nil) - for i, l := range lines { - if i > 0 { - ret.WriteRune('\n') + if unicode.IsSpace(r) { + w.lastSpace = true + continue } - ret.WriteString(indent) - for j, w := range l { - if j > 0 { - ret.WriteRune(' ') - } - - ret.WriteString(w) - } + w.word.WriteRune(r) + w.inTag = r == '<' + w.inWord = !w.inTag } - return ret + return len(p), nil +} + +func (w *wrapper) Flush() error { + if w.err != nil { + return w.err + } + + if w.inTag || w.inWord { + if err := w.feed(); err != nil { + w.err = err + return err + } + } + + w.width = 0 + if err := w.feed(); err != nil { + w.err = err + return err + } + + return nil } diff --git a/wrap_test.go b/wrap_test.go index e19a6b1..1c4bb37 100644 --- a/wrap_test.go +++ b/wrap_test.go @@ -13,12 +13,8 @@ func TestWrap(t *testing.T) { span := Span(string(b)) var buf bytes.Buffer - if err := html.RenderIndent(&buf, "\t", 0, span); err != nil { - t.Fatal(err) - } - - if buf.String() != "foo" { - t.Fatal(buf.String(), buf.Len(), len("foo"), buf.Bytes(), []byte("foo")) + if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t"}, span); err == nil { + t.Fatal() } }) @@ -26,25 +22,27 @@ func TestWrap(t *testing.T) { span := Span("foo bar baz") var buf bytes.Buffer - if err := html.RenderIndent(&buf, "\t", 2, span); err != nil { + if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t", PWidth: 2}, span); err != nil { t.Fatal(err) } - if buf.String() != "foo\nbar\nbaz" { + expect := "\nfoo\nbar\nbaz\n" + if buf.String() != expect { + printBytes(buf.String(), expect) t.Fatal(buf.String()) } }) t.Run("tag not split", func(t *testing.T) { - span := Span("foo ", Span("bar"), " baz") + span := Span("foo ", Span("bar", Attr("qux", 42)), " baz") var buf bytes.Buffer - if err := html.RenderIndent(&buf, "\t", 2, span); err != nil { + if err := html.RenderIndent(&buf, html.Indentation{Indent: "X", PWidth: 2}, span); err != nil { t.Fatal(err) } - if buf.String() != "foo\nbar\nbaz" { - t.Fatal() + if buf.String() != "\nfoo\n\nbar\n\nbaz\n" { + t.Fatal(buf.String()) } }) @@ -52,11 +50,11 @@ func TestWrap(t *testing.T) { div := Div(Span("foo bar baz qux quux corge")) var buf bytes.Buffer - if err := html.RenderIndent(&buf, "\t", 9, div); err != nil { + if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t", PWidth: 9}, div); err != nil { t.Fatal(err) } - if buf.String() != "
\n\tfoo\n\tbar baz\n\tqux quux\n\tcorge\n
\n" { + if buf.String() != "
\n\t\n\tfoo bar\n\tbaz qux\n\tquux\n\tcorge\n\t\n
" { t.Fatal(buf.String()) } }) @@ -65,12 +63,24 @@ func TestWrap(t *testing.T) { div := Div(Span("foo"), " ", Span("bar")) var buf bytes.Buffer - if err := html.RenderIndent(&buf, "\t", 0, div); err != nil { + if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t"}, div); err != nil { t.Fatal(err) } - if buf.String() != "
\n\tfoo bar\n
\n" { + if buf.String() != "
\n\tfoo bar\n
" { t.Fatal(buf.String()) } }) + + t.Run("multiple lines", func(t *testing.T) { + }) + + t.Run("special whitespace characters", func(t *testing.T) { + }) + + t.Run("spaces around tags", func(t *testing.T) { + }) + + t.Run("one line primitives", func(t *testing.T) { + }) }