Files
PgTidy/pkg/format/doc.go
T

185 lines
4.8 KiB
Go

package format
import "strings"
// Doc is a Wadler/Prettier-style layout document. Build one from Text, Line,
// SoftLine, Group, Indent, IfBreak and Concat, then Render it at a line width:
// a Group is printed flat (Lines become spaces, SoftLines vanish) when it fits
// in the remaining width, otherwise broken (Lines become newlines).
//
// Rendering is relative to column 0; callers that splice the result mid-line
// re-indent continuation lines themselves, so width checks ignore that offset.
type Doc interface{ isDoc() }
type (
docText string
docLine struct{ soft, hard bool }
docConcat []Doc
docIndent struct{ d Doc }
docGroup struct{ d Doc }
docBreak struct{ broken, flat Doc }
)
func (docText) isDoc() {}
func (docLine) isDoc() {}
func (docConcat) isDoc() {}
func (docIndent) isDoc() {}
func (docGroup) isDoc() {}
func (docBreak) isDoc() {}
// Text is literal text. If it contains a newline the enclosing group can never
// be printed flat.
func Text(s string) Doc { return docText(s) }
// Line is a space when flat and a newline (plus indentation) when broken.
func Line() Doc { return docLine{} }
// SoftLine is nothing when flat and a newline (plus indentation) when broken.
func SoftLine() Doc { return docLine{soft: true} }
// HardLine is always a newline (plus indentation), even inside a flat group; a
// group containing one can never be printed flat.
func HardLine() Doc { return docLine{hard: true} }
// Lines turns multi-line text into Text pieces joined by HardLines so that each
// line picks up the surrounding Indent. Empty lines carry no indentation.
func Lines(s, nl string) Doc {
parts := strings.Split(s, nl)
ds := make([]Doc, 0, 2*len(parts))
for i, p := range parts {
if i > 0 {
ds = append(ds, HardLine())
}
ds = append(ds, Text(p))
}
return Concat(ds...)
}
// Concat joins docs in order.
func Concat(ds ...Doc) Doc { return docConcat(ds) }
// Indent indents every line break inside d by one indent unit.
func Indent(d Doc) Doc { return docIndent{d} }
// Group lays d out flat if it fits on the current line, otherwise broken.
func Group(d Doc) Doc { return docGroup{d} }
// IfBreak renders broken when the enclosing group is broken, flat otherwise.
func IfBreak(broken, flat Doc) Doc { return docBreak{broken, flat} }
type docCmd struct {
indent string
flat bool
d Doc
}
// Render lays out d. width <= 0 means unlimited (every group stays flat).
func Render(d Doc, width int, unit, nl string) string {
var b strings.Builder
col := 0
pending := "" // indentation owed to the next non-empty text
stack := []docCmd{{"", false, d}}
for len(stack) > 0 {
c := stack[len(stack)-1]
stack = stack[:len(stack)-1]
switch v := c.d.(type) {
case docText:
if len(v) > 0 {
b.WriteString(pending)
pending = ""
}
b.WriteString(string(v))
if i := strings.LastIndexByte(string(v), '\n'); i >= 0 {
col = len(v) - i - 1
} else {
col += len(v)
}
case docConcat:
for i := len(v) - 1; i >= 0; i-- {
stack = append(stack, docCmd{c.indent, c.flat, v[i]})
}
case docIndent:
stack = append(stack, docCmd{c.indent + unit, c.flat, v.d})
case docBreak:
if c.flat {
stack = append(stack, docCmd{c.indent, c.flat, v.flat})
} else {
stack = append(stack, docCmd{c.indent, c.flat, v.broken})
}
case docLine:
if c.flat && !v.hard {
if !v.soft {
b.WriteByte(' ')
col++
}
break
}
b.WriteString(nl)
pending = c.indent
col = len(c.indent)
case docGroup:
flat := c.flat || width <= 0 || fitsFlat(v.d, width-col, c.indent, unit, stack)
stack = append(stack, docCmd{c.indent, flat, v.d})
}
}
return b.String()
}
// fitsFlat reports whether d, printed flat, plus whatever follows it up to the
// next possible line break, fits in rem columns.
func fitsFlat(d Doc, rem int, indent, unit string, rest []docCmd) bool {
if rem < 0 {
return false
}
type item struct {
d Doc
flat bool
}
work := []item{{d, true}}
ri := len(rest) - 1
for rem >= 0 {
if len(work) == 0 {
if ri < 0 {
return true
}
work = append(work, item{rest[ri].d, rest[ri].flat})
ri--
continue
}
it := work[len(work)-1]
work = work[:len(work)-1]
switch v := it.d.(type) {
case docText:
if strings.ContainsRune(string(v), '\n') {
return false
}
rem -= len(v)
case docConcat:
for i := len(v) - 1; i >= 0; i-- {
work = append(work, item{v[i], it.flat})
}
case docIndent:
work = append(work, item{v.d, it.flat})
case docGroup:
work = append(work, item{v.d, it.flat})
case docBreak:
if it.flat {
work = append(work, item{v.flat, it.flat})
} else {
work = append(work, item{v.broken, it.flat})
}
case docLine:
if v.hard {
return false
}
if !it.flat {
return true // a real line break ends the measured run
}
if !v.soft {
rem--
}
}
}
return false
}