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("", name) if r.indent != "" && !rg.inline { printf("\n") } }