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 = `
-` {
+
`
+
+ 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("%s>", 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("%s>", 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("%s>", 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%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("%s>", 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("%s>", 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("%s>", 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("%s>", 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() != "\nXYZ
foo 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) {
+ })
}