1
0
html/render.go
Arpad Ryszka 1308c164a7 render:
- wrap
- inline chilren tag
- refactor render
2025-10-05 14:27:48 +02:00

746 lines
13 KiB
Go

package html
import (
"fmt"
"io"
"strings"
)
const (
defaultPWidth = 112
unicodeNBSP = 0xa0
)
type renderGuide struct {
inline bool
inlineChildren bool
void bool
script bool
verbatim bool
}
type renderer struct {
out io.Writer
originalOut io.Writer
indent Indentation
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.inlineChildren = rg.inlineChildren || rgi.inlineChildren
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("&lt;")...)
case '>':
rr = append(rr, []rune("&gt;")...)
case '&':
rr = append(rr, []rune("&amp;")...)
case unicodeNBSP:
rr = append(rr, []rune("&nbsp;")...)
case ' ':
if wsStart && lastWS {
rr = append(rr[:len(rr)-1], []rune("&nbsp;&nbsp;")...)
} else if lastWS {
rr = append(rr, []rune("&nbsp;")...)
} else {
rr = append(rr, r[i])
}
default:
rr = append(rr, r[i])
}
ws := r[i] == ' '
wsStart = ws && !lastWS
lastWS = ws
}
return string(rr)
}
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
}
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
}
_, r.err = fmt.Fprintf(r.out, f, a...)
if r.err != nil {
r.err = fmt.Errorf("tag %s: %w", name, r.err)
}
}
}
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 {
printf("\n")
}
if rg.void {
return
}
var inlineBuffer *bytes.Buffer
// 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 {
// 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)
if r.indent != "" && !crg.inline && inlineBuffer.Len() > 0 {
w := r.pwidth
if w == 0 {
w = defaultPWidth
}
inlineBuffer = wrap(inlineBuffer, w, "")
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)
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")
}
}
*/