482 lines
8.2 KiB
Go
482 lines
8.2 KiB
Go
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("</%s>", name)
|
|
}
|
|
|
|
func indentLen(indent string) int {
|
|
var l int
|
|
r := []rune(indent)
|
|
for _, ri := range r {
|
|
if ri == '\t' {
|
|
l += 8
|
|
continue
|
|
}
|
|
|
|
l++
|
|
}
|
|
|
|
return l
|
|
}
|
|
|
|
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("</%s>", 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("</%s>", 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)
|
|
}
|