├── LICENSE.md ├── README.md ├── go.mod ├── wordwrap.go └── wordwrap_test.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mitchell Hashimoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-wordwrap 2 | 3 | `go-wordwrap` (Golang package: `wordwrap`) is a package for Go that 4 | automatically wraps words into multiple lines. The primary use case for this 5 | is in formatting CLI output, but of course word wrapping is a generally useful 6 | thing to do. 7 | 8 | ## Installation and Usage 9 | 10 | Install using `go get github.com/mitchellh/go-wordwrap`. 11 | 12 | Full documentation is available at 13 | http://godoc.org/github.com/mitchellh/go-wordwrap 14 | 15 | Below is an example of its usage ignoring errors: 16 | 17 | ```go 18 | wrapped := wordwrap.WrapString("foo bar baz", 3) 19 | fmt.Println(wrapped) 20 | ``` 21 | 22 | Would output: 23 | 24 | ``` 25 | foo 26 | bar 27 | baz 28 | ``` 29 | 30 | ## Word Wrap Algorithm 31 | 32 | This library doesn't use any clever algorithm for word wrapping. The wrapping 33 | is actually very naive: whenever there is whitespace or an explicit linebreak. 34 | The goal of this library is for word wrapping CLI output, so the input is 35 | typically pretty well controlled human language. Because of this, the naive 36 | approach typically works just fine. 37 | 38 | In the future, we'd like to make the algorithm more advanced. We would do 39 | so without breaking the API. 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mitchellh/go-wordwrap 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /wordwrap.go: -------------------------------------------------------------------------------- 1 | package wordwrap 2 | 3 | import ( 4 | "bytes" 5 | "unicode" 6 | ) 7 | 8 | const nbsp = 0xA0 9 | 10 | // WrapString wraps the given string within lim width in characters. 11 | // 12 | // Wrapping is currently naive and only happens at white-space. A future 13 | // version of the library will implement smarter wrapping. This means that 14 | // pathological cases can dramatically reach past the limit, such as a very 15 | // long word. 16 | func WrapString(s string, lim uint) string { 17 | // Initialize a buffer with a slightly larger size to account for breaks 18 | init := make([]byte, 0, len(s)) 19 | buf := bytes.NewBuffer(init) 20 | 21 | var current uint 22 | var wordBuf, spaceBuf bytes.Buffer 23 | var wordBufLen, spaceBufLen uint 24 | 25 | for _, char := range s { 26 | if char == '\n' { 27 | if wordBuf.Len() == 0 { 28 | if current+spaceBufLen > lim { 29 | current = 0 30 | } else { 31 | current += spaceBufLen 32 | spaceBuf.WriteTo(buf) 33 | } 34 | spaceBuf.Reset() 35 | spaceBufLen = 0 36 | } else { 37 | current += spaceBufLen + wordBufLen 38 | spaceBuf.WriteTo(buf) 39 | spaceBuf.Reset() 40 | spaceBufLen = 0 41 | wordBuf.WriteTo(buf) 42 | wordBuf.Reset() 43 | wordBufLen = 0 44 | } 45 | buf.WriteRune(char) 46 | current = 0 47 | } else if unicode.IsSpace(char) && char != nbsp { 48 | if spaceBuf.Len() == 0 || wordBuf.Len() > 0 { 49 | current += spaceBufLen + wordBufLen 50 | spaceBuf.WriteTo(buf) 51 | spaceBuf.Reset() 52 | spaceBufLen = 0 53 | wordBuf.WriteTo(buf) 54 | wordBuf.Reset() 55 | wordBufLen = 0 56 | } 57 | 58 | spaceBuf.WriteRune(char) 59 | spaceBufLen++ 60 | } else { 61 | wordBuf.WriteRune(char) 62 | wordBufLen++ 63 | 64 | if current+wordBufLen+spaceBufLen > lim && wordBufLen < lim { 65 | buf.WriteRune('\n') 66 | current = 0 67 | spaceBuf.Reset() 68 | spaceBufLen = 0 69 | } 70 | } 71 | } 72 | 73 | if wordBuf.Len() == 0 { 74 | if current+spaceBufLen <= lim { 75 | spaceBuf.WriteTo(buf) 76 | } 77 | } else { 78 | spaceBuf.WriteTo(buf) 79 | wordBuf.WriteTo(buf) 80 | } 81 | 82 | return buf.String() 83 | } 84 | -------------------------------------------------------------------------------- /wordwrap_test.go: -------------------------------------------------------------------------------- 1 | package wordwrap 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestWrapString(t *testing.T) { 9 | cases := []struct { 10 | Input, Output string 11 | Lim uint 12 | }{ 13 | // A simple word passes through. 14 | { 15 | "foo", 16 | "foo", 17 | 4, 18 | }, 19 | // A single word that is too long passes through. 20 | // We do not break words. 21 | { 22 | "foobarbaz", 23 | "foobarbaz", 24 | 4, 25 | }, 26 | // Lines are broken at whitespace. 27 | { 28 | "foo bar baz", 29 | "foo\nbar\nbaz", 30 | 4, 31 | }, 32 | // Lines are broken at whitespace, even if words 33 | // are too long. We do not break words. 34 | { 35 | "foo bars bazzes", 36 | "foo\nbars\nbazzes", 37 | 4, 38 | }, 39 | // A word that would run beyond the width is wrapped. 40 | { 41 | "fo sop", 42 | "fo\nsop", 43 | 4, 44 | }, 45 | // Do not break on non-breaking space. 46 | { 47 | "foo bar\u00A0baz", 48 | "foo\nbar\u00A0baz", 49 | 10, 50 | }, 51 | // Whitespace that trails a line and fits the width 52 | // passes through, as does whitespace prefixing an 53 | // explicit line break. A tab counts as one character. 54 | { 55 | "foo\nb\t r\n baz", 56 | "foo\nb\t r\n baz", 57 | 4, 58 | }, 59 | // Trailing whitespace is removed if it doesn't fit the width. 60 | // Runs of whitespace on which a line is broken are removed. 61 | { 62 | "foo \nb ar ", 63 | "foo\nb\nar", 64 | 4, 65 | }, 66 | // An explicit line break at the end of the input is preserved. 67 | { 68 | "foo bar baz\n", 69 | "foo\nbar\nbaz\n", 70 | 4, 71 | }, 72 | // Explicit break are always preserved. 73 | { 74 | "\nfoo bar\n\n\nbaz\n", 75 | "\nfoo\nbar\n\n\nbaz\n", 76 | 4, 77 | }, 78 | // Complete example: 79 | { 80 | " This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* baz \nBAM ", 81 | " This\nis a\nlist: \n\n\t* foo\n\t* bar\n\n\n\t* baz\nBAM", 82 | 6, 83 | }, 84 | // Multi-byte characters 85 | { 86 | strings.Repeat("\u2584 ", 4), 87 | "\u2584 \u2584" + "\n" + 88 | strings.Repeat("\u2584 ", 2), 89 | 4, 90 | }, 91 | } 92 | 93 | for i, tc := range cases { 94 | actual := WrapString(tc.Input, tc.Lim) 95 | if actual != tc.Output { 96 | t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.Input, tc.Output, actual) 97 | } 98 | } 99 | } 100 | --------------------------------------------------------------------------------