render:
- wrap - inline chilren tag - refactor render
This commit is contained in:
parent
59e3a7d2c8
commit
1308c164a7
5
Makefile
5
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
|
||||
|
||||
|
||||
99
lib.go
99
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)
|
||||
}
|
||||
|
||||
25
lib_test.go
25
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() != `<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) {
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
12
print_test.go
Normal 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)))
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,4 @@
|
||||
Attr
|
||||
NewTag
|
||||
Define
|
||||
Doctype
|
||||
Comment
|
||||
|
||||
2
query.go
2
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
|
||||
}
|
||||
|
||||
|
||||
@ -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
535
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")
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@ -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())
|
||||
}
|
||||
})
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -9,7 +9,6 @@ code
|
||||
data
|
||||
dfn
|
||||
em
|
||||
h1, h2, h3, h4, h5, h6
|
||||
i
|
||||
kbd
|
||||
label
|
||||
|
||||
5
tags.inlinechildren.txt
Normal file
5
tags.inlinechildren.txt
Normal file
@ -0,0 +1,5 @@
|
||||
h1, h2, h3, h4, h5, h6
|
||||
li
|
||||
p
|
||||
td
|
||||
th
|
||||
@ -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")
|
||||
|
||||
@ -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"))
|
||||
|
||||
14
tags/inlinechildren.gen.go
Normal file
14
tags/inlinechildren.gen.go
Normal 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"))
|
||||
@ -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
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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")))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
189
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
|
||||
}
|
||||
|
||||
42
wrap_test.go
42
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() != "<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) {
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user