package html import ( "fmt" "io" "strings" ) const defaultPWidth = 112 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 (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) isDeclaration := strings.HasPrefix(tagName, "!") for _, ai := range a { for _, name := range ai.names { value := ai.values[name] isTrue, _ := value.(bool) if isTrue && isDeclaration { escaped := attributeEscape(name) if escaped == name { printf(" %s", name) continue } printf(" \"%s\"", escaped) continue } if isTrue { printf(" %s", name) continue } printf(" %s=\"%s\"", name, attributeEscape(fmt.Sprint(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 } if c == nil { continue } if rd, ok := c.(io.Reader); ok { if rg.verbatim || rg.script { _, r.err = io.Copy(r.out, rd) continue } ew := newEscapeWriter(r.out) _, r.err = io.Copy(ew, rd) if r.err == nil { ew.Flush() r.err = ew.err } continue } s := fmt.Sprint(c) if s == "" { continue } if rg.verbatim || rg.script { printf(s) continue } if r.err == nil { ew := newEscapeWriter(r.out) ew.Write([]byte(s)) ew.Flush() r.err = ew.err } } printf("", 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 { rd, isReader := c.(io.Reader) if isReader && rg.verbatim { r.clearWrapper() if r.err == nil { iw := newIndentWriter(r.out, r.currentIndent+r.indent.Indent) iw.Write([]byte{'\n'}) _, r.err = io.Copy(iw, rd) if r.err == nil { iw.Flush() r.err = iw.err } } lastBlock = true continue } if isReader && rg.script { r.clearWrapper() printf("\n") _, r.err = io.Copy(r.out, rd) lastBlock = true continue } if isReader { if lastBlock { printf("\n%s", r.currentIndent) } if r.ensureWrapper() { newWrapper = true } if r.err == nil { ew := newEscapeWriter(r.out) _, r.err = io.Copy(ew, rd) if r.err == nil { ew.Flush() r.err = ew.err } } lastBlock = false continue } ct, isTag := c.(Tag) if !isTag && rg.verbatim { if c == nil { continue } s := fmt.Sprint(c) if s == "" { continue } r.clearWrapper() if r.err == nil { iw := newIndentWriter(r.out, r.currentIndent+r.indent.Indent) iw.Write([]byte{'\n'}) iw.Write([]byte(s)) iw.Flush() r.err = iw.err } lastBlock = true continue } if !isTag && rg.script { if c == nil { continue } s := fmt.Sprint(c) if s == "" { continue } r.clearWrapper() printf("\n%s", s) lastBlock = true continue } if !isTag { if c == nil { continue } s := fmt.Sprint(c) if s == "" { continue } if lastBlock { printf("\n%s", r.currentIndent) } if r.ensureWrapper() { newWrapper = true } if r.err == nil { ew := newEscapeWriter(r.out) ew.Write([]byte(s)) ew.Flush() r.err = ew.err } 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 -= indentLen(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("", 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("", name) return } lastBlock := true originalIndent, originalWidth := r.currentIndent, r.pwidth r.currentIndent += r.indent.Indent r.pwidth -= indentLen(r.indent.Indent) if r.pwidth < r.indent.MinPWidth { r.pwidth = r.indent.MinPWidth } for _, c := range children { rd, isReader := c.(io.Reader) if isReader && rg.verbatim { r.clearWrapper() if r.err == nil { iw := newIndentWriter(r.out, r.currentIndent) iw.Write([]byte{'\n'}) _, r.err = io.Copy(iw, rd) if r.err == nil { iw.Flush() r.err = iw.err } } lastBlock = true continue } if isReader && rg.script { r.clearWrapper() printf("\n") _, r.err = io.Copy(r.out, rd) lastBlock = true continue } if isReader { if lastBlock { printf("\n%s", r.currentIndent) } r.ensureWrapper() if r.err == nil { ew := newEscapeWriter(r.out) _, r.err = io.Copy(ew, rd) if r.err == nil { ew.Flush() r.err = ew.err } } lastBlock = false continue } ct, isTag := c.(Tag) if !isTag && rg.verbatim { if c == nil { continue } s := fmt.Sprint(c) if s == "" { continue } r.clearWrapper() if r.err == nil { iw := newIndentWriter(r.out, r.currentIndent) iw.Write([]byte{'\n'}) iw.Write([]byte(s)) iw.Flush() r.err = iw.err } lastBlock = true continue } if !isTag && rg.script { if c == nil { continue } s := fmt.Sprint(c) if s == "" { continue } r.clearWrapper() printf("\n%s", s) lastBlock = true continue } if !isTag { if c == nil { continue } s := fmt.Sprint(c) if s == "" { continue } if lastBlock { printf("\n%s", r.currentIndent) } r.ensureWrapper() if r.err == nil { ew := newEscapeWriter(r.out) ew.Write([]byte(s)) ew.Flush() r.err = ew.err } 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", 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) }