mirror of
https://github.com/mjl-/mox.git
synced 2024-12-29 09:53:47 +03:00
208 lines
4 KiB
Go
208 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
|
||
|
}
|