package html import ( "fmt" "io" "strings" ) const defaultPWidth = 112 type renderGuide struct { inline bool inlineChildren bool void bool script bool preformatted 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.preformatted = rg.preformatted || rgi.preformatted 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) 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) writeEscaped(s string, nonbreakSpaces bool) { if r.err != nil { return } ew := newEscapeWriter(r.out, nonbreakSpaces) if _, r.err = ew.Write([]byte(s)); r.err != nil { return } r.err = ew.Flush() } func (r *renderer) copyEscaped(rd io.Reader, nonbreakSpaces bool) { if r.err != nil { return } ew := newEscapeWriter(r.out, nonbreakSpaces) if _, r.err = io.Copy(ew, rd); r.err != nil { return } r.err = ew.Flush() } func (r *renderer) writeIndented(indent, s string) { if r.err != nil { return } iw := newIndentWriter(r.out, indent) if _, r.err = iw.Write([]byte{'\n'}); r.err != nil { return } if _, r.err = iw.Write([]byte(s)); r.err != nil { return } r.err = iw.Flush() } func (r *renderer) copyIndented(indent string, rd io.Reader) { if r.err != nil { return } iw := newIndentWriter(r.out, indent) if _, r.err = iw.Write([]byte{'\n'}); r.err != nil { return } if _, r.err = io.Copy(iw, rd); r.err != nil { return } r.err = iw.Flush() } func (r *renderer) renderAttributes(tagName string, a []Attributes) { printf := r.getPrintf(tagName) isDeclaration := strings.HasPrefix(tagName, "!") for i, ai := range a { for j, name := range ai.names { if isDeclaration { f := " %s" if i == 0 && j == 0 { f = "%s" } printf(f, name) continue } value := ai.values[name] isTrue, _ := value.(bool) if isTrue { printf(" %s", name) continue } printf(" %s=\"%s\"", name, escapeAttribute(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 c == nil { continue } rd, isReader := c.(io.Reader) if isReader && (rg.verbatim || rg.script) { if r.err == nil { _, r.err = io.Copy(r.out, rd) } continue } if isReader { r.copyEscaped(rd, !rg.preformatted) continue } if ct, ok := c.(Tag); ok { if rg.preformatted { ct = Preformatted(ct) } ct(r) continue } s := fmt.Sprint(c) if s == "" { continue } if rg.verbatim || rg.script { printf(s) continue } r.writeEscaped(s, !rg.preformatted) } printf("", name) } func (r *renderer) renderChildTag(tagName string, rg renderGuide, lastBlock bool, ct Tag) (bool, bool) { printf := r.getPrintf(tagName) if rg.preformatted { cr := new(renderer) *cr = *r cr.indent = Indentation{} Preformatted(ct)(cr) return false, false } var rgq renderGuidesQuery ct(&rgq) crg := mergeRenderingGuides(rgq.value) if crg.inline { if lastBlock { printf("\n%s", r.currentIndent) } newWrapper := r.ensureWrapper() ct(r) return false, newWrapper } r.clearWrapper() cr := new(renderer) *cr = *r if rg.inline || rg.inlineChildren { 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 } return true, false } func (r *renderer) renderReaderChild(tagName string, rg renderGuide, lastBlock bool, rd io.Reader) bool { printf := r.getPrintf(tagName) if rg.script { r.clearWrapper() printf("\n") if r.err == nil { _, r.err = io.Copy(r.out, rd) } return false } if rg.verbatim { r.clearWrapper() indent := r.currentIndent if rg.inline || rg.inlineChildren { indent += r.indent.Indent } r.copyIndented(indent, rd) return false } if rg.preformatted { r.clearWrapper() r.copyEscaped(rd, false) return false } if lastBlock { printf("\n%s", r.currentIndent) } newWrapper := r.ensureWrapper() r.copyEscaped(rd, true) return newWrapper } func (r *renderer) renderChildScript(tagName string, c any) { s := fmt.Sprint(c) if s == "" { return } r.clearWrapper() printf := r.getPrintf(tagName) printf("\n%s", s) } func (r *renderer) renderVerbatimChild(rg renderGuide, c any) { s := fmt.Sprint(c) if s == "" { return } r.clearWrapper() if rg.preformatted { _, r.err = r.out.Write([]byte(s)) return } indent := r.currentIndent if rg.inline || rg.inlineChildren { indent += r.indent.Indent } r.writeIndented(indent, s) } func (r *renderer) renderChildContent(tagName string, rg renderGuide, lastBlock bool, c any) bool { s := fmt.Sprint(c) if s == "" { return false } if rg.preformatted { r.writeEscaped(s, false) return false } if lastBlock { printf := r.getPrintf(tagName) printf("\n%s", r.currentIndent) } newWrapper := r.ensureWrapper() r.writeEscaped(s, true) return newWrapper } func (r *renderer) renderIndented(name string, rg renderGuide, a []Attributes, children []any) { var newWrapper bool if rg.inline || rg.inlineChildren { newWrapper = r.ensureWrapper() } printf := r.getPrintf(name) printf("<%s", name) r.renderAttributes(name, a) printf(">") if rg.void { if newWrapper { r.clearWrapper() } return } if len(children) == 0 { printf("", name) if newWrapper { r.clearWrapper() } return } block := !rg.inline && !rg.inlineChildren lastBlock := block originalIndent, originalWidth := r.currentIndent, r.pwidth if block { r.currentIndent += r.indent.Indent r.pwidth -= indentLen(r.indent.Indent) if r.pwidth < r.indent.MinPWidth { r.pwidth = r.indent.MinPWidth } } if rg.preformatted { r.clearWrapper() printf("\n") } for _, c := range children { if c == nil { continue } if ct, isTag := c.(Tag); isTag { var nw bool lastBlock, nw = r.renderChildTag(name, rg, lastBlock, ct) if nw { newWrapper = true } continue } if rd, isReader := c.(io.Reader); isReader { if r.renderReaderChild(name, rg, lastBlock, rd) { newWrapper = true } lastBlock = rg.verbatim || rg.script continue } if rg.script { r.renderChildScript(name, c) lastBlock = true continue } if rg.verbatim { r.renderVerbatimChild(rg, c) lastBlock = true continue } if r.renderChildContent(name, rg, lastBlock, c) { newWrapper = true } lastBlock = false } if block { r.currentIndent, r.pwidth = originalIndent, originalWidth r.clearWrapper() } if lastBlock || block { printf("\n%s", r.currentIndent) } printf("", name) if newWrapper && !block { r.clearWrapper() } } 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 } r.renderIndented(name, rg, a, c) }