aboutsummaryrefslogtreecommitdiff
path: root/content/wrap.go
diff options
context:
space:
mode:
Diffstat (limited to 'content/wrap.go')
-rw-r--r--content/wrap.go189
1 files changed, 189 insertions, 0 deletions
diff --git a/content/wrap.go b/content/wrap.go
new file mode 100644
index 0000000..316eb91
--- /dev/null
+++ b/content/wrap.go
@@ -0,0 +1,189 @@
+// 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 -w wrap.go your.article
+// go run -w wrap.go *.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
+}