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) 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) { if r.err != nil { return } ew := newEscapeWriter(r.out) if _, r.err = ew.Write([]byte(s)); r.err != nil { return } r.err = ew.Flush() } func (r *renderer) copyEscaped(rd io.Reader) { if r.err != nil { return } ew := newEscapeWriter(r.out) 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 _, 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 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) continue } if ct, ok := c.(Tag); ok { ct(r) continue } s := fmt.Sprint(c) if s == "" { continue } if rg.verbatim || rg.script { printf(s) continue } r.writeEscaped(s) } printf("", name) } func (r *renderer) renderChildTag(tagName string, block, lastBlock bool, ct Tag) (bool, bool) { printf := r.getPrintf(tagName) 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 !block { 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, block, lastBlock bool, rd io.Reader) bool { printf := r.getPrintf(tagName) if rg.verbatim { r.clearWrapper() indent := r.currentIndent if !block { indent += r.indent.Indent } r.copyIndented(indent, rd) return false } if rg.script { r.clearWrapper() printf("\n") if r.err == nil { _, r.err = io.Copy(r.out, rd) } return false } if lastBlock { printf("\n%s", r.currentIndent) } newWrapper := r.ensureWrapper() r.copyEscaped(rd) return newWrapper } func (r *renderer) renderVerbatimChild(block bool, c any) { s := fmt.Sprint(c) if s == "" { return } r.clearWrapper() indent := r.currentIndent if !block { indent += r.indent.Indent } r.writeIndented(indent, s) } 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) renderChildContent(tagName string, lastBlock bool, c any) bool { s := fmt.Sprint(c) if s == "" { return false } if lastBlock { printf := r.getPrintf(tagName) printf("\n%s", r.currentIndent) } newWrapper := r.ensureWrapper() r.writeEscaped(s) 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 } } for _, c := range children { if c == nil { continue } if ct, isTag := c.(Tag); isTag { var nw bool lastBlock, nw = r.renderChildTag(name, block, lastBlock, ct) if nw { newWrapper = true } continue } if rd, isReader := c.(io.Reader); isReader { if r.renderReaderChild(name, rg, block, lastBlock, rd) { newWrapper = true } lastBlock = rg.verbatim || rg.script continue } if rg.verbatim { r.renderVerbatimChild(block, c) lastBlock = true continue } if rg.script { r.renderChildScript(name, c) lastBlock = true continue } if r.renderChildContent(name, 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) }