mox/vendor/github.com/mjl-/xfmt/xfmt.go
Mechiel Lukkien cb229cb6cf
mox!
2023-01-30 14:27:06 +01:00

207 lines
4 KiB
Go

// Package xfmt reformats text, wrapping it while recognizing comments.
package xfmt
import (
"bufio"
"fmt"
"io"
"strings"
)
// Config tells format how to reformat text.
type Config struct {
MaxWidth int // Max width of content (excluding indenting), after which lines are wrapped.
BreakPrefixes []string // String prefixes that cause a line to break, instead of being merged into the previous line.
}
// Format reads text from r and writes reformatted text to w, according to
// instructions in config. Lines ending with \r\n are formatted with \r\n as well.
func Format(w io.Writer, r io.Reader, config Config) error {
f := &formatter{
in: bufio.NewReader(r),
out: bufio.NewWriter(w),
config: config,
}
return f.format()
}
type formatter struct {
in *bufio.Reader
out *bufio.Writer
config Config
curLine string
curLineend string
}
type parseError error
func (f *formatter) format() (rerr error) {
defer func() {
e := recover()
if e != nil {
if pe, ok := e.(parseError); ok {
rerr = pe
} else {
panic(e)
}
}
}()
for {
line, end := f.gatherLine()
if line == "" && end == "" {
break
}
prefix, rem := parseLine(line)
for _, s := range f.splitLine(rem) {
f.write(prefix)
f.write(s)
f.write(end)
}
}
return f.out.Flush()
}
func (f *formatter) check(err error, action string) {
if err != nil {
panic(parseError(fmt.Errorf("%s: %s", action, err)))
}
}
func (f *formatter) write(s string) {
_, err := f.out.Write([]byte(s))
f.check(err, "write")
}
func (f *formatter) peekLine() (string, string) {
if f.curLine != "" || f.curLineend != "" {
return f.curLine, f.curLineend
}
line, err := f.in.ReadString('\n')
if err != io.EOF {
f.check(err, "read")
}
if line == "" {
return "", ""
}
if strings.HasSuffix(line, "\r\n") {
f.curLine, f.curLineend = line[:len(line)-2], "\r\n"
} else if strings.HasSuffix(line, "\n") {
f.curLine, f.curLineend = line[:len(line)-1], "\n"
} else {
f.curLine, f.curLineend = line, ""
}
return f.curLine, f.curLineend
}
func (f *formatter) consumeLine() {
if f.curLine == "" && f.curLineend == "" {
panic("bad")
}
f.curLine = ""
f.curLineend = ""
}
func (f *formatter) gatherLine() (string, string) {
var curLine, curLineend string
var curPrefix string
for {
line, end := f.peekLine()
if line == "" && end == "" {
break
}
if curLine == "" {
curLineend = end
}
prefix, rem := parseLine(line)
if prefix == "" && rem == "" {
if curLine == "" {
f.consumeLine()
}
break
}
if curLine != "" && (curPrefix != prefix || rem == "" || f.causeBreak(rem)) {
break
}
curPrefix = prefix
if curLine != "" {
curLine += " "
}
curLine += rem
f.consumeLine()
// Control at begin or end of line are not merged.
if curLine != "" && curLine[len(curLine)-1] < 0x20 {
break
}
}
return curPrefix + curLine, curLineend
}
func (f *formatter) causeBreak(s string) bool {
c := s[0]
if c < 0x20 {
return true
}
for _, ss := range f.config.BreakPrefixes {
if strings.HasPrefix(s, ss) {
return true
}
}
// Don't merge lines starting with eg "1. ".
for i, c := range s {
if c >= '0' && c <= '9' {
continue
}
if i > 0 && c == '.' && strings.HasPrefix(s[i:], ". ") {
return true
}
break
}
return false
}
func parseLine(s string) (string, string) {
orig := s
s = strings.TrimLeft(orig, " \t")
prefix := orig[:len(orig)-len(s)]
if strings.HasPrefix(s, "//") {
prefix += "//"
s = s[2:]
} else if strings.HasPrefix(s, "#") {
prefix += "#"
s = s[1:]
}
ns := strings.TrimLeft(s, " \t")
prefix += s[:len(s)-len(ns)]
s = ns
return prefix, s
}
func (f *formatter) splitLine(s string) []string {
if len(s) <= f.config.MaxWidth {
return []string{s}
}
line := ""
r := []string{}
for _, w := range strings.Split(s, " ") {
if line != "" && len(line)+1+len(w) > f.config.MaxWidth {
r = append(r, line)
line = w
continue
}
if line != "" {
line += " "
}
line += w
}
if line != "" {
r = append(r, line)
}
return r
}