251 lines
4.5 KiB
Go
251 lines
4.5 KiB
Go
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")
|
|
}
|
|
}
|