package html
import (
"bytes"
"fmt"
"io"
)
const defaultPWidth = 112
type renderGuide struct {
inline bool
void bool
script bool
verbatim bool
}
type renderer struct {
out io.Writer
indent string
pwidth int
currentIndent string
err error
}
func mergeRenderingGuides(rgs []renderGuide) renderGuide {
var rg renderGuide
for _, rgi := range rgs {
rg.inline = rg.inline || rgi.inline
rg.void = rg.void || rgi.void
rg.script = rg.script || rgi.script
rg.verbatim = rg.verbatim || rgi.verbatim
}
return rg
}
func attributeEscape(value string) string {
var rr []rune
r := []rune(value)
for i := range r {
switch r[i] {
case '"':
rr = append(rr, []rune(""")...)
case '&':
rr = append(rr, []rune("&")...)
default:
rr = append(rr, r[i])
}
}
return string(rr)
}
func htmlEscape(s string) string {
var (
rr []rune
lastWS, wsStart bool
)
r := []rune(s)
for i := range r {
switch r[i] {
case '<':
rr = append(rr, []rune("<")...)
case '>':
rr = append(rr, []rune(">")...)
case '&':
rr = append(rr, []rune("&")...)
case ' ', 0xA0:
if wsStart && lastWS {
rr = append(rr[:len(rr)-1], []rune(" ")...)
} else if lastWS {
rr = append(rr, []rune(" ")...)
} else {
rr = append(rr, r[i])
}
default:
rr = append(rr, r[i])
}
ws := r[i] == ' ' || r[i] == 0xA0
wsStart = ws && !lastWS
lastWS = ws
}
return string(rr)
}
func render(r *renderer, name string, children []any) {
if r.err != nil {
return
}
printf := 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", name, r.err)
}
}
a, c, rgs := groupChildren(children)
rg := mergeRenderingGuides(rgs)
printf(r.currentIndent)
printf("<%s", name)
for _, ai := range a {
for name, value := range ai {
printf(" %s=\"%s\"", name, attributeEscape(value))
}
}
printf(">")
if r.indent != "" && !rg.inline && len(c) > 0 {
printf("\n")
}
if rg.void {
return
}
var inlineBuffer *bytes.Buffer
if r.indent != "" {
inlineBuffer = bytes.NewBuffer(nil)
}
// TODO:
// - avoid rendering an inline buffer into another inline buffer
// - why?
// - 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 {
if tag, ok := ci.(Tag); ok {
if rg.inline {
var rgq renderGuidesQuery
tag(&rgq)
crg := mergeRenderingGuides(rgq.value)
if r.indent != "" && !crg.inline && inlineBuffer.Len() > 0 {
w := r.pwidth
if w == 0 {
w = defaultPWidth
}
inlineBuffer = wrap(inlineBuffer, w, "")
println(inlineBuffer.String())
if _, err := io.Copy(r.out, inlineBuffer); err != nil {
r.err = err
return
}
inlineBuffer = bytes.NewBuffer(nil)
}
if i > 0 && r.indent != "" && !crg.inline {
printf("\n")
}
rr := new(renderer)
*rr = *r
rr.indent = ""
rr.currentIndent = ""
tag(rr)
} else {
var rgq renderGuidesQuery
tag(&rgq)
crg := mergeRenderingGuides(rgq.value)
if r.indent != "" && !crg.inline && inlineBuffer.Len() > 0 {
w := r.pwidth
if w == 0 {
w = defaultPWidth
}
inlineBuffer = wrap(inlineBuffer, w, r.currentIndent+r.indent)
println(inlineBuffer.String())
if _, err := io.Copy(r.out, inlineBuffer); err != nil {
r.err = err
return
}
inlineBuffer = bytes.NewBuffer(nil)
}
if i > 0 && r.indent != "" && !crg.inline {
printf("\n")
}
rr := new(renderer)
*rr = *r
rr.currentIndent += r.indent
if r.indent != "" && crg.inline {
rr.out = inlineBuffer
}
tag(rr)
}
continue
}
s := fmt.Sprint(ci)
if s == "" {
continue
}
if !rg.verbatim && !rg.script {
s = htmlEscape(s)
}
if r.indent == "" {
printf(s)
continue
}
inlineBuffer.WriteString(s)
}
if r.indent != "" && inlineBuffer.Len() > 0 {
w := r.pwidth
if w == 0 {
w = defaultPWidth
}
var indent string
if !rg.inline && !rg.script {
indent = r.currentIndent + r.indent
}
inlineBuffer = wrap(inlineBuffer, w, indent)
if _, err := io.Copy(r.out, inlineBuffer); err != nil {
r.err = err
return
}
if !rg.inline {
printf("\n")
}
}
if !rg.inline {
printf(r.currentIndent)
}
printf("%s>", name)
if r.indent != "" && !rg.inline {
printf("\n")
}
}