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
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

99
lib.go
View File

@ -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 &nbsp;
// 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)
}

View File

@ -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() != `<div>
<h3>
Foo
</h3>
<p>
Rank: 3
</p>
const expect = `<div>
<h3>Foo</h3>
<p>Rank: 3</p>
<ul>
<li>
<div>
@ -94,9 +91,15 @@ func TestLib(t *testing.T) {
</div>
</li>
</ul>
</div>
` {
</div>`
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) {
})
}

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
right audience
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
NewTag
Define
Doctype
Comment

View File

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

View File

@ -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() != "<div>\n\t<span>foo</span>\n</div>\n" {
if b.String() != "<div>\n\t<span>foo</span>\n</div>" {
t.Fatal(b.String())
}
})

535
render.go
View File

@ -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("&gt;")...)
case '&':
rr = append(rr, []rune("&amp;")...)
case ' ', 0xA0:
case unicodeNBSP:
rr = append(rr, []rune("&nbsp;")...)
case ' ':
if wsStart && lastWS {
rr = append(rr[:len(rr)-1], []rune("&nbsp;&nbsp;")...)
} 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")
}
}
*/

View File

@ -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("<bar><baz></bar>")
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() != "<foo><bar><baz></bar></foo>" {
if b.String() != "<foo>\n\t<bar><baz></bar>\n</foo>" {
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() != "<div>\n\t<span>foo</span>\n</div>\n" {
if b.String() != "<div>\n\t<span>foo</span>\n</div>" {
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() != "<div>\n\t<br>\n</div>\n" {
if b.String() != "<div>\n\t<br>\n</div>" {
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() != "<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())
}
})
@ -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() != "<div>\nXYZ<span>foo bar baz\nXYZXYZ<div>\nXYZXYZXYZqux quux\nXYZXYZ</div>\nXYZcorge</span>\n</div>" {
t.Fatal(b.String())
}
})

View File

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

View File

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

View File

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

View File

@ -9,7 +9,6 @@ code
data
dfn
em
h1, h2, h3, h4, h5, h6
i
kbd
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
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")

View File

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

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
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

View File

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

View File

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

View File

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

View File

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

View File

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

189
wrap.go
View File

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

View File

@ -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() != "<span>foo</span>" {
t.Fatal(buf.String(), buf.Len(), len("<span>foo</span>"), buf.Bytes(), []byte("<span>foo</span>"))
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() != "<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.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() != "<span>foo\n<span>bar</span>\nbaz</span>" {
t.Fatal()
if buf.String() != "<span>\nfoo\n<span qux=\"42\">\nbar\n</span>\nbaz\n</span>" {
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() != "<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())
}
})
@ -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() != "<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.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) {
})
}