185 lines
4.8 KiB
Go
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
|
|
}
|