blob: b84b9df331b4e603fe6b0b42b69d707132f3e4e3 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build ignore
// Wrap wraps long lines intelligently.
//
// Usage:
//
// go run wrap.go [-w] [file ...]
//
// By default, wrap prints the line-wrapped version of the input files to standard output.
// If no input file is listed, standard input is used.
//
// The -w flag causes wrap to update the files in place, overwriting each with its
// line-wrapped equivalent. Wrap leaves the file unchanged if only a few lines
// need wrapping.
//
// Examples
//
// go run wrap.go -w your.article
// go run wrap.go -w *.article
//
package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
)
func usage() {
fmt.Fprintf(os.Stderr, "usage: wrap [-w] [file ...]\n")
os.Exit(2)
}
var writeBack = flag.Bool("w", false, "write conversions back to original files")
var exitStatus = 0
func main() {
log.SetPrefix("wrap: ")
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
args := flag.Args()
if len(args) == 0 {
if *writeBack {
log.Fatalf("cannot use -w with standard input")
}
convert(os.Stdin, "")
return
}
for _, arg := range args {
f, err := os.Open(arg)
if err != nil {
log.Print(err)
exitStatus = 1
continue
}
target := ""
if *writeBack {
target = arg
}
err = convert(f, target)
f.Close()
if err != nil {
log.Print(err)
exitStatus = 1
}
}
os.Exit(exitStatus)
}
// convert reads content from r and line-wraps it.
// If target is the empty string, convert writes the wrapped result to standard output.
// If target is non-empty and there were more than a few line wraps needed,
// convert writes the result to the file named by target.
func convert(r io.Reader, target string) error {
data, err := ioutil.ReadAll(r)
if err != nil {
return err
}
changes := 0
lines := strings.Split(string(data), "\n")
for i, line := range lines {
if len(line) < 80 {
continue
}
switch line[0] {
case '.', '#', ':', '\t', ' ':
continue
}
w := wrap(line)
if line != w {
if strings.HasPrefix(line, "- ") {
w = strings.Replace(w, "\n", "\n ", -1)
}
changes += strings.Count(w, "\n")
lines[i] = w
}
}
wrapped := []byte(strings.Join(lines, "\n"))
if target == "" {
os.Stdout.Write(wrapped)
return nil
}
// Don't rewrite if not much changed.
if changes < 5 {
return nil
}
return ioutil.WriteFile(target, wrapped, 0666)
}
func wrap(text string) string {
if len(text) < 120 {
return text
}
// Wrap after sentence boundaries when possible.
// See https://rhodesmill.org/brandon/2012/one-sentence-per-line/.
// Note: wrapAt guarantees line[i] == ' '.
text = wrapAt(text, func(line string, i int) bool {
return i > 40 && i+20 < len(line) &&
(line[i-1] == '.' || line[i-1] == '!') && (line[i+1] == ' ' || !('a' <= line[i+1] && line[i+1] <= 'z'))
})
// Wrap after phrase boundaries next.
text = wrapAt(text, func(line string, i int) bool {
return i > 40 && i+20 < len(line) && (line[i-1] == ';' || line[i-1] == ':')
})
text = wrapAt(text, func(line string, i int) bool {
return i > 40 && i+20 < len(line) && line[i-1] == ','
})
// Wrap long lines that are left at spaces.
text = wrapAt(text, func(line string, i int) bool {
return i > 70 && i+20 < len(line)
})
return text
}
// wrapAt wraps long lines in text, returning the result.
// It calls canWrapAt(line, start, i) to ask whether the given line
// should be wrapped just before offset i; line[i] is known to be a space.
func wrapAt(text string, canWrapAt func(line string, i int) bool) string {
lines := strings.Split(text, "\n")
for i, line := range lines {
var out string
start := 0
brackets := 0
for i := 0; i < len(line); {
if line[i] == '[' {
brackets++
}
if line[i] == ']' {
brackets--
}
if brackets == 0 && line[i] == ' ' && i+1 < len(line) && line[i+1] != '-' && canWrapAt(line[start:], i-start) {
out += trimRight(line[start:i]) + "\n"
i++
for i < len(line) && line[i] == ' ' {
i++
}
start = i
continue
}
i++
}
out += trimRight(line[start:])
lines[i] = out
}
return strings.Join(lines, "\n")
}
func trimRight(s string) string {
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t') {
s = s[:len(s)-1]
}
return s
}