1
0
- wrap
- inline chilren tag
- refactor render
This commit is contained in:
Arpad Ryszka 2025-10-05 14:27:48 +02:00
parent 59e3a7d2c8
commit 1308c164a7
26 changed files with 943 additions and 284 deletions

View File

@ -7,7 +7,7 @@ all: clean fmt build cover
build: $(SOURCES) tags promote-to-tags build: $(SOURCES) tags promote-to-tags
go build 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 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 tags/inline.gen.go: $(SOURCES) tags.inline.txt
go run script/generate-tags.go Inline < tags.inline.txt > tags/inline.gen.go 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 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 go run script/generate-tags.go Void < tags.void.block.txt > tags/void.block.gen.go

95
lib.go
View File

@ -2,6 +2,7 @@
package html package html
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"strings" "strings"
@ -22,6 +23,21 @@ type Tag func(...any) Tag
type Template[Data any] func(Data) 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 // 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 // the names and values are applied using fmt.Sprint, tolerating fmt.Stringer implementations
func Attr(a ...any) Attributes { func Attr(a ...any) Attributes {
@ -38,17 +54,39 @@ func Attr(a ...any) Attributes {
} }
// defines a new tag with name and initial attributes and child nodes // 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) { if handleQuery(name, children) {
children = children[:len(children)-1] children = children[:len(children)-1]
} }
return func(children1 ...any) Tag { return func(children1 ...any) Tag {
if name == "br" { return Define(name, append(children, children1...)...)
}
} }
return NewTag(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 // returns the name of a tag
@ -89,7 +127,7 @@ func DeleteAttribute(t Tag, name string) Tag {
a := AllAttributes(t) a := AllAttributes(t)
c := Children(t) c := Children(t)
delete(a, name) delete(a, name)
return NewTag(n, append(c, a)...) return Define(n, append(c, a)...)
} }
// the same as Attribute(t, "class") // 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 &nbsp; // consecutive spaces are considered to be so on purpose, and are converted into &nbsp;
// spaces around tags can behave different from when using unindented rendering // spaces around tags can behave different from when using unindented rendering
// as a last resort, one can use rendered html inside a verbatim tag // 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 { func RenderIndent(out io.Writer, indent Indentation, t Tag) error {
r := renderer{out: out, indent: indent, pwidth: pwidth} 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) t()(&r)
return r.err return r.err
} }
// renders html with t as the root node without indentation // renders html with t as the root node without indentation
func Render(out io.Writer, t Tag) error { 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. // 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 { func InlineChildren(t Tag) Tag {
return t return t()(renderGuide{inlineChildren: true})
} }
// inline tags are not broken into separate lines when rendering with indentation // 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 // 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 { return func(a ...any) Tag {
var ( var (
t Data d Data
ok bool ok bool
) )
for i := range a { for i := range a {
t, ok = a[0].(Data) d, ok = a[0].(Data)
if !ok { if !ok {
continue continue
} }
@ -209,33 +261,24 @@ func FromTemplate[Data any](f Template[Data]) Tag {
break break
} }
return f(t)(a...) return t(d)(a...)
} }
} }
// in the functional programming sense // 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 var ret []Tag
for _, d := range data { for _, d := range data {
var retd Tag ret = append(ret, tag(d))
for i := len(tags) - 1; i >= 0; i-- {
retd = tags[i](d)
}
if retd == nil {
retd = tag(d)
continue
}
retd = tag(retd)
} }
return ret 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 var a []any
c := Map(data, tag, tags...)
for _, ci := range c { for _, ci := range c {
a = append(a, ci) a = append(a, ci)
} }

View File

@ -1,10 +1,11 @@
package html_test package html_test
import ( import (
"bytes"
"code.squareroundforest.org/arpio/html" "code.squareroundforest.org/arpio/html"
. "code.squareroundforest.org/arpio/html/tags" . "code.squareroundforest.org/arpio/html/tags"
"testing" "testing"
"bytes" "code.squareroundforest.org/arpio/notation"
) )
func TestLib(t *testing.T) { func TestLib(t *testing.T) {
@ -57,17 +58,13 @@ func TestLib(t *testing.T) {
} }
var b bytes.Buffer 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) t.Fatal(err)
} }
if b.String() != `<div> const expect = `<div>
<h3> <h3>Foo</h3>
Foo <p>Rank: 3</p>
</h3>
<p>
Rank: 3
</p>
<ul> <ul>
<li> <li>
<div> <div>
@ -94,9 +91,15 @@ func TestLib(t *testing.T) {
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>`
` {
if b.String() != expect {
notation.Println([]byte(expect)[48:96])
notation.Println(b.Bytes()[48:96])
t.Fatal(b.String()) t.Fatal(b.String())
} }
}) })
t.Run("ampty attributes", func(t *testing.T) {
})
} }

View File

@ -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 recommendation is not to mutate children. Ofc, creatively breaking the rules is always well appreciated by the
right audience right audience
test wrapped templates test wrapped templates
test empty block
escape extra space between tag boundaries
declarations: <!doctype html>
comments: <!-- foo -->
attritubes, when bool true, then just the name of the attribute
implement stringer for the tag

12
print_test.go Normal file
View File

@ -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)))
}
}

View File

@ -1,2 +1,4 @@
Attr Attr
NewTag Define
Doctype
Comment

View File

@ -121,7 +121,7 @@ func handleQuery(name string, children []any) bool {
return true return true
} }
render(r, name, children[:last]) r.render(name, children[:last])
return true return true
} }

View File

@ -109,11 +109,11 @@ func TestQuery(t *testing.T) {
div := Div(Span("foo")) div := Div(Span("foo"))
var b bytes.Buffer 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) t.Fatal(err)
} }
if b.String() != "<div>\n\t<span>foo</span>\n</div>\n" { if b.String() != "<div>\n\t<span>foo</span>\n</div>" {
t.Fatal(b.String()) t.Fatal(b.String())
} }
}) })

527
render.go
View File

@ -1,15 +1,19 @@
package html package html
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"strings"
) )
const defaultPWidth = 112 const (
defaultPWidth = 112
unicodeNBSP = 0xa0
)
type renderGuide struct { type renderGuide struct {
inline bool inline bool
inlineChildren bool
void bool void bool
script bool script bool
verbatim bool verbatim bool
@ -17,7 +21,8 @@ type renderGuide struct {
type renderer struct { type renderer struct {
out io.Writer out io.Writer
indent string originalOut io.Writer
indent Indentation
pwidth int pwidth int
currentIndent string currentIndent string
err error err error
@ -27,6 +32,7 @@ func mergeRenderingGuides(rgs []renderGuide) renderGuide {
var rg renderGuide var rg renderGuide
for _, rgi := range rgs { for _, rgi := range rgs {
rg.inline = rg.inline || rgi.inline rg.inline = rg.inline || rgi.inline
rg.inlineChildren = rg.inlineChildren || rgi.inlineChildren
rg.void = rg.void || rgi.void rg.void = rg.void || rgi.void
rg.script = rg.script || rgi.script rg.script = rg.script || rgi.script
rg.verbatim = rg.verbatim || rgi.verbatim rg.verbatim = rg.verbatim || rgi.verbatim
@ -67,7 +73,9 @@ func htmlEscape(s string) string {
rr = append(rr, []rune("&gt;")...) rr = append(rr, []rune("&gt;")...)
case '&': case '&':
rr = append(rr, []rune("&amp;")...) rr = append(rr, []rune("&amp;")...)
case ' ', 0xA0: case unicodeNBSP:
rr = append(rr, []rune("&nbsp;")...)
case ' ':
if wsStart && lastWS { if wsStart && lastWS {
rr = append(rr[:len(rr)-1], []rune("&nbsp;&nbsp;")...) rr = append(rr[:len(rr)-1], []rune("&nbsp;&nbsp;")...)
} else if lastWS { } else if lastWS {
@ -79,7 +87,7 @@ func htmlEscape(s string) string {
rr = append(rr, r[i]) rr = append(rr, r[i])
} }
ws := r[i] == ' ' || r[i] == 0xA0 ws := r[i] == ' '
wsStart = ws && !lastWS wsStart = ws && !lastWS
lastWS = ws lastWS = ws
} }
@ -87,12 +95,317 @@ func htmlEscape(s string) string {
return string(rr) 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 { if r.err != nil {
return return
} }
printf := func(f string, a ...any) { _, 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
}
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 { if r.err != nil {
return return
} }
@ -102,16 +415,189 @@ func render(r *renderer, name string, children []any) {
r.err = fmt.Errorf("tag %s: %w", name, r.err) r.err = fmt.Errorf("tag %s: %w", name, r.err)
} }
} }
}
a, c, rgs := groupChildren(children) func renderAttributes(out io.Writer, a []Attributes) {
rg := mergeRenderingGuides(rgs) printf := getPrintf(out)
printf(r.currentIndent)
printf("<%s", name)
for _, ai := range a { for _, ai := range a {
for name, value := range ai { for name, value := range ai {
printf(" %s=\"%s\"", name, attributeEscape(value)) 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(">") printf(">")
if r.indent != "" && !rg.inline && len(c) > 0 { if r.indent != "" && !rg.inline && len(c) > 0 {
@ -123,9 +609,6 @@ func render(r *renderer, name string, children []any) {
} }
var inlineBuffer *bytes.Buffer var inlineBuffer *bytes.Buffer
if r.indent != "" {
inlineBuffer = bytes.NewBuffer(nil)
}
// TODO: // TODO:
// - avoid rendering an inline buffer into another inline buffer // - 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 // - or, if inline, just use the inline buffer without indentation
// - check the wrapping again, if it preserves or eliminates the spaces the right way // - check the wrapping again, if it preserves or eliminates the spaces the right way
for i, ci := range c { 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 tag, ok := ci.(Tag); ok {
if rg.inline { if rg.inline {
if inlineBuffer == nil {
inlineBuffer = bytes.NewBuffer(nil)
}
var rgq renderGuidesQuery var rgq renderGuidesQuery
tag(&rgq) tag(&rgq)
crg := mergeRenderingGuides(rgq.value) crg := mergeRenderingGuides(rgq.value)
@ -145,7 +641,6 @@ func render(r *renderer, name string, children []any) {
} }
inlineBuffer = wrap(inlineBuffer, w, "") inlineBuffer = wrap(inlineBuffer, w, "")
println(inlineBuffer.String())
if _, err := io.Copy(r.out, inlineBuffer); err != nil { if _, err := io.Copy(r.out, inlineBuffer); err != nil {
r.err = err r.err = err
return return
@ -174,7 +669,6 @@ func render(r *renderer, name string, children []any) {
} }
inlineBuffer = wrap(inlineBuffer, w, r.currentIndent+r.indent) inlineBuffer = wrap(inlineBuffer, w, r.currentIndent+r.indent)
println(inlineBuffer.String())
if _, err := io.Copy(r.out, inlineBuffer); err != nil { if _, err := io.Copy(r.out, inlineBuffer); err != nil {
r.err = err r.err = err
return return
@ -248,3 +742,4 @@ func render(r *renderer, name string, children []any) {
printf("\n") printf("\n")
} }
} }
*/

View File

@ -28,15 +28,15 @@ func (w *failingWriter) Write(p []byte) (int, error) {
func TestRender(t *testing.T) { func TestRender(t *testing.T) {
t.Run("merge render guides", func(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("<bar><baz></bar>") foo = foo("<bar><baz></bar>")
var b bytes.Buffer 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) t.Fatal(err)
} }
if b.String() != "<foo><bar><baz></bar></foo>" { if b.String() != "<foo>\n\t<bar><baz></bar>\n</foo>" {
t.Fatal(b.String()) t.Fatal(b.String())
} }
}) })
@ -102,7 +102,7 @@ func TestRender(t *testing.T) {
t.Run("partial text children", func(t *testing.T) { t.Run("partial text children", func(t *testing.T) {
div := Div("foo", Div("bar"), "baz") div := Div("foo", Div("bar"), "baz")
w := failWriteAfter(5) 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() t.Fatal()
} }
}) })
@ -110,7 +110,7 @@ func TestRender(t *testing.T) {
t.Run("text children", func(t *testing.T) { t.Run("text children", func(t *testing.T) {
div := Div("foo", "bar", "baz") div := Div("foo", "bar", "baz")
w := failWriteAfter(5) 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() t.Fatal()
} }
}) })
@ -121,11 +121,11 @@ func TestRender(t *testing.T) {
div := Div(Span("foo")) div := Div(Span("foo"))
var b bytes.Buffer 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) t.Fatal(err)
} }
if b.String() != "<div>\n\t<span>foo</span>\n</div>\n" { if b.String() != "<div>\n\t<span>foo</span>\n</div>" {
t.Fatal(b.String()) t.Fatal(b.String())
} }
}) })
@ -134,11 +134,11 @@ func TestRender(t *testing.T) {
div := Div(Br()) div := Div(Br())
var b bytes.Buffer 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) t.Fatal(err)
} }
if b.String() != "<div>\n\t<br>\n</div>\n" { if b.String() != "<div>\n\t<br>\n</div>" {
t.Fatal(b.String()) t.Fatal(b.String())
} }
}) })
@ -147,11 +147,11 @@ func TestRender(t *testing.T) {
div := Div("foo bar baz", Div("qux quux"), "corge") div := Div("foo bar baz", Div("qux quux"), "corge")
var b bytes.Buffer 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) t.Fatal(err)
} }
if b.String() != "<div>\n\tfoo bar baz\n\t<div>\n\t\tqux quux\n\t</div>\n\tcorge\n</div>\n" { if b.String() != "<div>\n\tfoo bar baz\n\t<div>\n\t\tqux quux\n\t</div>\n\tcorge\n</div>" {
t.Fatal(b.String()) t.Fatal(b.String())
} }
}) })
@ -160,11 +160,11 @@ func TestRender(t *testing.T) {
div := Div(Span("foo bar baz", Div("qux quux"), "corge")) div := Div(Span("foo bar baz", Div("qux quux"), "corge"))
var b bytes.Buffer 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) t.Fatal(err)
} }
if b.String() != "" { if b.String() != "<div>\nXYZ<span>foo bar baz\nXYZXYZ<div>\nXYZXYZXYZqux quux\nXYZXYZ</div>\nXYZcorge</span>\n</div>" {
t.Fatal(b.String()) t.Fatal(b.String())
} }
}) })

View File

@ -50,7 +50,7 @@ func main() {
printf("package tags\n") printf("package tags\n")
printf("import \"code.squareroundforest.org/arpio/html\"\n") printf("import \"code.squareroundforest.org/arpio/html\"\n")
for _, si := range ss { for _, si := range ss {
exp := fmt.Sprintf("html.NewTag(\"%s\")", si) exp := fmt.Sprintf("html.Define(\"%s\")", si)
for _, a := range os.Args[1:] { for _, a := range os.Args[1:] {
exp = fmt.Sprintf("html.%s(%s)", a, exp) exp = fmt.Sprintf("html.%s(%s)", a, exp)
} }

View File

@ -44,7 +44,7 @@ func main() {
_, err = fmt.Fprintf(os.Stdout, f, a...) _, 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("\n")
printf("package tags\n") printf("package tags\n")
printf("import \"code.squareroundforest.org/arpio/html\"\n") printf("import \"code.squareroundforest.org/arpio/html\"\n")

View File

@ -27,7 +27,6 @@ header
hgroup hgroup
html html
ins ins
li
link link
main main
map map
@ -37,7 +36,6 @@ nav
noscript noscript
ol ol
optgroup optgroup
p
picture picture
pre pre
rp rp
@ -46,11 +44,9 @@ section
summary summary
table table
tbody tbody
td
template template
textarea textarea
tfoot tfoot
th
thead thead
title title
tr tr

View File

@ -9,7 +9,6 @@ code
data data
dfn dfn
em em
h1, h2, h3, h4, h5, h6
i i
kbd kbd
label label

5
tags.inlinechildren.txt Normal file
View File

@ -0,0 +1,5 @@
h1, h2, h3, h4, h5, h6
li
p
td
th

View File

@ -2,61 +2,57 @@
package tags package tags
import "code.squareroundforest.org/arpio/html" import "code.squareroundforest.org/arpio/html"
var Address = html.NewTag("address") var Address = html.Define("address")
var Article = html.NewTag("article") var Article = html.Define("article")
var Audio = html.NewTag("audio") var Audio = html.Define("audio")
var Aside = html.NewTag("aside") var Aside = html.Define("aside")
var Blockquote = html.NewTag("blockquote") var Blockquote = html.Define("blockquote")
var Body = html.NewTag("body") var Body = html.Define("body")
var Canvas = html.NewTag("canvas") var Canvas = html.Define("canvas")
var Caption = html.NewTag("caption") var Caption = html.Define("caption")
var Center = html.NewTag("center") var Center = html.Define("center")
var Col = html.NewTag("col") var Col = html.Define("col")
var Colgroup = html.NewTag("colgroup") var Colgroup = html.Define("colgroup")
var Datalist = html.NewTag("datalist") var Datalist = html.Define("datalist")
var Dd = html.NewTag("dd") var Dd = html.Define("dd")
var Del = html.NewTag("del") var Del = html.Define("del")
var Details = html.NewTag("details") var Details = html.Define("details")
var Dialog = html.NewTag("dialog") var Dialog = html.Define("dialog")
var Div = html.NewTag("div") var Div = html.Define("div")
var Dl = html.NewTag("dl") var Dl = html.Define("dl")
var Dt = html.NewTag("dt") var Dt = html.Define("dt")
var Fieldset = html.NewTag("fieldset") var Fieldset = html.Define("fieldset")
var Figcaption = html.NewTag("figcaption") var Figcaption = html.Define("figcaption")
var Figure = html.NewTag("figure") var Figure = html.Define("figure")
var Footer = html.NewTag("footer") var Footer = html.Define("footer")
var Form = html.NewTag("form") var Form = html.Define("form")
var Head = html.NewTag("head") var Head = html.Define("head")
var Header = html.NewTag("header") var Header = html.Define("header")
var Hgroup = html.NewTag("hgroup") var Hgroup = html.Define("hgroup")
var Html = html.NewTag("html") var Html = html.Define("html")
var Ins = html.NewTag("ins") var Ins = html.Define("ins")
var Li = html.NewTag("li") var Link = html.Define("link")
var Link = html.NewTag("link") var Main = html.Define("main")
var Main = html.NewTag("main") var Map = html.Define("map")
var Map = html.NewTag("map") var Math = html.Define("math")
var Math = html.NewTag("math") var Menu = html.Define("menu")
var Menu = html.NewTag("menu") var Nav = html.Define("nav")
var Nav = html.NewTag("nav") var Noscript = html.Define("noscript")
var Noscript = html.NewTag("noscript") var Ol = html.Define("ol")
var Ol = html.NewTag("ol") var Optgroup = html.Define("optgroup")
var Optgroup = html.NewTag("optgroup") var Picture = html.Define("picture")
var P = html.NewTag("p") var Pre = html.Define("pre")
var Picture = html.NewTag("picture") var Rp = html.Define("rp")
var Pre = html.NewTag("pre") var Search = html.Define("search")
var Rp = html.NewTag("rp") var Section = html.Define("section")
var Search = html.NewTag("search") var Summary = html.Define("summary")
var Section = html.NewTag("section") var Table = html.Define("table")
var Summary = html.NewTag("summary") var Tbody = html.Define("tbody")
var Table = html.NewTag("table") var Template = html.Define("template")
var Tbody = html.NewTag("tbody") var Textarea = html.Define("textarea")
var Td = html.NewTag("td") var Tfoot = html.Define("tfoot")
var Template = html.NewTag("template") var Thead = html.Define("thead")
var Textarea = html.NewTag("textarea") var Title = html.Define("title")
var Tfoot = html.NewTag("tfoot") var Tr = html.Define("tr")
var Th = html.NewTag("th") var Ul = html.Define("ul")
var Thead = html.NewTag("thead") var Video = html.Define("video")
var Title = html.NewTag("title")
var Tr = html.NewTag("tr")
var Ul = html.NewTag("ul")
var Video = html.NewTag("video")

View File

@ -2,47 +2,41 @@
package tags package tags
import "code.squareroundforest.org/arpio/html" import "code.squareroundforest.org/arpio/html"
var A = html.Inline(html.NewTag("a")) var A = html.Inline(html.Define("a"))
var Abbr = html.Inline(html.NewTag("abbr")) var Abbr = html.Inline(html.Define("abbr"))
var B = html.Inline(html.NewTag("b")) var B = html.Inline(html.Define("b"))
var Bdi = html.Inline(html.NewTag("bdi")) var Bdi = html.Inline(html.Define("bdi"))
var Bdo = html.Inline(html.NewTag("bdo")) var Bdo = html.Inline(html.Define("bdo"))
var Button = html.Inline(html.NewTag("button")) var Button = html.Inline(html.Define("button"))
var Cite = html.Inline(html.NewTag("cite")) var Cite = html.Inline(html.Define("cite"))
var Code = html.Inline(html.NewTag("code")) var Code = html.Inline(html.Define("code"))
var Data = html.Inline(html.NewTag("data")) var Data = html.Inline(html.Define("data"))
var Dfn = html.Inline(html.NewTag("dfn")) var Dfn = html.Inline(html.Define("dfn"))
var Em = html.Inline(html.NewTag("em")) var Em = html.Inline(html.Define("em"))
var H1 = html.Inline(html.NewTag("h1")) var I = html.Inline(html.Define("i"))
var H2 = html.Inline(html.NewTag("h2")) var Kbd = html.Inline(html.Define("kbd"))
var H3 = html.Inline(html.NewTag("h3")) var Label = html.Inline(html.Define("label"))
var H4 = html.Inline(html.NewTag("h4")) var Legend = html.Inline(html.Define("legend"))
var H5 = html.Inline(html.NewTag("h5")) var Mark = html.Inline(html.Define("mark"))
var H6 = html.Inline(html.NewTag("h6")) var Meter = html.Inline(html.Define("meter"))
var I = html.Inline(html.NewTag("i")) var Object = html.Inline(html.Define("object"))
var Kbd = html.Inline(html.NewTag("kbd")) var Option = html.Inline(html.Define("option"))
var Label = html.Inline(html.NewTag("label")) var Output = html.Inline(html.Define("output"))
var Legend = html.Inline(html.NewTag("legend")) var Progress = html.Inline(html.Define("progress"))
var Mark = html.Inline(html.NewTag("mark")) var Q = html.Inline(html.Define("q"))
var Meter = html.Inline(html.NewTag("meter")) var Rt = html.Inline(html.Define("rt"))
var Object = html.Inline(html.NewTag("object")) var Ruby = html.Inline(html.Define("ruby"))
var Option = html.Inline(html.NewTag("option")) var S = html.Inline(html.Define("s"))
var Output = html.Inline(html.NewTag("output")) var Samp = html.Inline(html.Define("samp"))
var Progress = html.Inline(html.NewTag("progress")) var Select = html.Inline(html.Define("select"))
var Q = html.Inline(html.NewTag("q")) var Selectedcontent = html.Inline(html.Define("selectedcontent"))
var Rt = html.Inline(html.NewTag("rt")) var Slot = html.Inline(html.Define("slot"))
var Ruby = html.Inline(html.NewTag("ruby")) var Small = html.Inline(html.Define("small"))
var S = html.Inline(html.NewTag("s")) var Span = html.Inline(html.Define("span"))
var Samp = html.Inline(html.NewTag("samp")) var Strong = html.Inline(html.Define("strong"))
var Select = html.Inline(html.NewTag("select")) var Sub = html.Inline(html.Define("sub"))
var Selectedcontent = html.Inline(html.NewTag("selectedcontent")) var Sup = html.Inline(html.Define("sup"))
var Slot = html.Inline(html.NewTag("slot")) var Svg = html.Inline(html.Define("svg"))
var Small = html.Inline(html.NewTag("small")) var Time = html.Inline(html.Define("time"))
var Span = html.Inline(html.NewTag("span")) var U = html.Inline(html.Define("u"))
var Strong = html.Inline(html.NewTag("strong")) var Var = html.Inline(html.Define("var"))
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"))

View File

@ -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"))

View File

@ -1,6 +1,8 @@
// generated by ../script/generate-tags.go // generated by ../script/promote-to-tags.go
package tags package tags
import "code.squareroundforest.org/arpio/html" import "code.squareroundforest.org/arpio/html"
var Attr = html.Attr var Attr = html.Attr
var NewTag = html.NewTag var Define = html.Define
var Doctype = html.Doctype
var Comment = html.Comment

View File

@ -2,5 +2,5 @@
package tags package tags
import "code.squareroundforest.org/arpio/html" import "code.squareroundforest.org/arpio/html"
var Script = html.ScriptContent(html.NewTag("script")) var Script = html.ScriptContent(html.Define("script"))
var Style = html.ScriptContent(html.NewTag("style")) var Style = html.ScriptContent(html.Define("style"))

View File

@ -2,10 +2,10 @@
package tags package tags
import "code.squareroundforest.org/arpio/html" import "code.squareroundforest.org/arpio/html"
var Area = html.Void(html.NewTag("area")) var Area = html.Void(html.Define("area"))
var Base = html.Void(html.NewTag("base")) var Base = html.Void(html.Define("base"))
var Hr = html.Void(html.NewTag("hr")) var Hr = html.Void(html.Define("hr"))
var Iframe = html.Void(html.NewTag("iframe")) var Iframe = html.Void(html.Define("iframe"))
var Meta = html.Void(html.NewTag("meta")) var Meta = html.Void(html.Define("meta"))
var Source = html.Void(html.NewTag("source")) var Source = html.Void(html.Define("source"))
var Track = html.Void(html.NewTag("track")) var Track = html.Void(html.Define("track"))

View File

@ -2,8 +2,8 @@
package tags package tags
import "code.squareroundforest.org/arpio/html" import "code.squareroundforest.org/arpio/html"
var Br = html.Inline(html.Void(html.NewTag("br"))) var Br = html.Inline(html.Void(html.Define("br")))
var Embed = html.Inline(html.Void(html.NewTag("embed"))) var Embed = html.Inline(html.Void(html.Define("embed")))
var Img = html.Inline(html.Void(html.NewTag("img"))) var Img = html.Inline(html.Void(html.Define("img")))
var Input = html.Inline(html.Void(html.NewTag("input"))) var Input = html.Inline(html.Void(html.Define("input")))
var Wbr = html.Inline(html.Void(html.NewTag("wbr"))) var Wbr = html.Inline(html.Void(html.Define("wbr")))

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"regexp" "regexp"
"strings"
) )
var ( var (
@ -38,8 +39,13 @@ func validate(name string, children []any) error {
return fmt.Errorf("tag %s is void but it has children", name) return fmt.Errorf("tag %s is void but it has children", name)
} }
isDeclaration := strings.HasPrefix(name, "!")
for _, ai := range a { for _, ai := range a {
for name := range ai { for name := range ai {
if isDeclaration {
continue
}
if err := validateAttributeName(name); err != nil { if err := validateAttributeName(name); err != nil {
return fmt.Errorf("tag %s: %w", name, err) return fmt.Errorf("tag %s: %w", name, err)
} }
@ -48,7 +54,7 @@ func validate(name string, children []any) error {
for _, ci := range c { for _, ci := range c {
if tag, ok := ci.(Tag); ok { 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) return fmt.Errorf("tag %s does not allow child elements", name)
} }

View File

@ -10,7 +10,7 @@ import (
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
t.Run("symbol", func(t *testing.T) { t.Run("symbol", func(t *testing.T) {
t.Run("invalid", 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 var b bytes.Buffer
if err := html.Render(&b, mytag); err == nil { 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) { t.Run("invalid with allowed chars number", func(t *testing.T) {
mytag := html.NewTag("0foo") mytag := html.Define("0foo")
var b bytes.Buffer var b bytes.Buffer
if err := html.Render(&b, mytag); err == nil { 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) { t.Run("invalid with allowed chars delimiter", func(t *testing.T) {
mytag := html.NewTag("-foo") mytag := html.Define("-foo")
var b bytes.Buffer var b bytes.Buffer
if err := html.Render(&b, mytag); err == nil { if err := html.Render(&b, mytag); err == nil {
@ -37,7 +37,7 @@ func TestValidate(t *testing.T) {
}) })
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
mytag := html.NewTag("foo") mytag := html.Define("foo")
var b bytes.Buffer var b bytes.Buffer
if err := html.Render(&b, mytag); err != nil { 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) { 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 var b bytes.Buffer
if err := html.Render(&b, mytag); err != nil { if err := html.Render(&b, mytag); err != nil {
@ -77,8 +77,8 @@ func TestValidate(t *testing.T) {
div := html.Verbatim(Div(Br())) div := html.Verbatim(Div(Br()))
var b bytes.Buffer var b bytes.Buffer
if err := html.Render(&b, div); err == nil { if err := html.Render(&b, div); err != nil {
t.Fatal() t.Fatal(err)
} }
}) })

169
wrap.go
View File

@ -2,88 +2,161 @@ package html
import ( import (
"bytes" "bytes"
"errors"
"io"
"unicode" "unicode"
) )
func words(buf *bytes.Buffer) []string { type wrapper struct {
var ( out io.Writer
words []string width int
currentWord []rune indent string
inTag bool 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 { for {
r, _, err := buf.ReadRune() r, _, err := runes.ReadRune()
if err != nil { if errors.Is(err, io.EOF) {
break return len(p), nil
} }
if r == unicode.ReplacementChar { 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 continue
} }
if !inTag && unicode.IsSpace(r) { if w.inQuote {
if len(currentWord) > 0 { w.inQuote = r != '"'
words, currentWord = append(words, string(currentWord)), nil 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 continue
} }
currentWord = append(currentWord, r) if w.inWord {
inTag = inTag && r != '>' || r == '<' 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 { w.lastSpace = unicode.IsSpace(r)
words = append(words, string(currentWord))
} }
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 continue
} }
currentLine = append(currentLine, w) if unicode.IsSpace(r) {
w.lastSpace = true
continue
} }
if len(currentLine) > 0 { w.word.WriteRune(r)
lines = append(lines, currentLine) w.inTag = r == '<'
w.inWord = !w.inTag
} }
ret := bytes.NewBuffer(nil) return len(p), nil
for i, l := range lines {
if i > 0 {
ret.WriteRune('\n')
} }
ret.WriteString(indent) func (w *wrapper) Flush() error {
for j, w := range l { if w.err != nil {
if j > 0 { return w.err
ret.WriteRune(' ')
} }
ret.WriteString(w) if w.inTag || w.inWord {
if err := w.feed(); err != nil {
w.err = err
return err
} }
} }
return ret w.width = 0
if err := w.feed(); err != nil {
w.err = err
return err
}
return nil
} }

View File

@ -13,12 +13,8 @@ func TestWrap(t *testing.T) {
span := Span(string(b)) span := Span(string(b))
var buf bytes.Buffer var buf bytes.Buffer
if err := html.RenderIndent(&buf, "\t", 0, span); err != nil { if err := html.RenderIndent(&buf, html.Indentation{Indent: "\t"}, span); err == nil {
t.Fatal(err) t.Fatal()
}
if buf.String() != "<span>foo</span>" {
t.Fatal(buf.String(), buf.Len(), len("<span>foo</span>"), buf.Bytes(), []byte("<span>foo</span>"))
} }
}) })
@ -26,25 +22,27 @@ func TestWrap(t *testing.T) {
span := Span("foo bar baz") span := Span("foo bar baz")
var buf bytes.Buffer 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) t.Fatal(err)
} }
if buf.String() != "<span>foo\nbar\nbaz</span>" { expect := "<span>\nfoo\nbar\nbaz\n</span>"
if buf.String() != expect {
printBytes(buf.String(), expect)
t.Fatal(buf.String()) t.Fatal(buf.String())
} }
}) })
t.Run("tag not split", func(t *testing.T) { 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 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) t.Fatal(err)
} }
if buf.String() != "<span>foo\n<span>bar</span>\nbaz</span>" { if buf.String() != "<span>\nfoo\n<span qux=\"42\">\nbar\n</span>\nbaz\n</span>" {
t.Fatal() t.Fatal(buf.String())
} }
}) })
@ -52,11 +50,11 @@ func TestWrap(t *testing.T) {
div := Div(Span("foo bar baz qux quux corge")) div := Div(Span("foo bar baz qux quux corge"))
var buf bytes.Buffer 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) t.Fatal(err)
} }
if buf.String() != "<div>\n\t<span>foo\n\tbar baz\n\tqux quux\n\tcorge</span>\n</div>\n" { if buf.String() != "<div>\n\t<span>\n\tfoo bar\n\tbaz qux\n\tquux\n\tcorge\n\t</span>\n</div>" {
t.Fatal(buf.String()) t.Fatal(buf.String())
} }
}) })
@ -65,12 +63,24 @@ func TestWrap(t *testing.T) {
div := Div(Span("foo"), " ", Span("bar")) div := Div(Span("foo"), " ", Span("bar"))
var buf bytes.Buffer 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) t.Fatal(err)
} }
if buf.String() != "<div>\n\t<span>foo</span> <span>bar</span>\n</div>\n" { if buf.String() != "<div>\n\t<span>foo</span> <span>bar</span>\n</div>" {
t.Fatal(buf.String()) 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) {
})
} }