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("<")...) case '>': rr = append(rr, []rune(">")...) case '&': rr = append(rr, []rune("&")...) case unicodeNBSP: rr = append(rr, []rune(" ")...) case ' ': 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] == ' ' 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("", 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("", 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 -= 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", 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("", 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("", 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("", 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("", 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("", name) if r.indent != "" && !rg.inline { printf("\n") } } */